Selecting with loc and iloc

pandas · Essentials · 12 min

Explicit selection avoids ambiguous indexing bugs and is the only reliable way to assign to a subset.

pandas has two explicit indexers, both taking [rows, columns]:

  • `.loc` selects by label: index labels for rows, column names for columns.
  • `.iloc` selects by integer position, like NumPy, regardless of labels.

Each position accepts a single item, a list, a slice or (for .loc) a boolean array:

df.loc["2024-01-03", "price"]          # one value
df.loc[["a", "c"], ["price", "qty"]]   # sub-table
df.iloc[0]                             # first row as a Series
df.iloc[:5, -2:]                       # first 5 rows, last 2 columns

One important difference: `.loc` slices include the end label, while .iloc slices exclude the end position, as in Python. df.loc["a":"c"] includes row "c"; df.iloc[0:2] returns two rows. Label slicing works best on a sorted index.

Selecting a single row or column returns a Series; selecting with lists or slices returns a DataFrame. df.at[label, col] and df.iat[i, j] are faster accessors for exactly one value.

Plain df[...] is a shortcut with mixed rules: a string or list selects columns, a slice selects rows, a boolean Series filters rows. It's fine for those cases, but use .loc/.iloc whenever you select rows and columns together and always when assigning:

df.loc[df["qty"] == 0, "status"] = "out"

A single .loc call both locates and sets in the original. Splitting it into two steps, df[df["qty"] == 0]["status"] = "out", is chained assignment: the first step produces a new object, so the original is never modified (pandas warns you).

Example

import pandas as pd

df = pd.DataFrame(
    {"price": [9.5, 12.0, 4.25, 7.0], "qty": [2, 0, 5, 1]},
    index=["a", "b", "c", "d"],
)
print(df.loc["b", "price"])          # 12.0
print(df.loc["a":"c", "qty"].tolist())   # [2, 0, 5]  end label included
print(df.iloc[0:2].index.tolist())   # ['a', 'b']  end position excluded
print(df.iloc[-1, 0])                # 7.0
print(df.loc[["d", "a"], ["qty"]])   # DataFrame, rows in that order

df.loc[df["qty"] == 0, "status"] = "out"   # one call: works
df.loc[df["qty"] > 0, "status"] = "ok"
print(df["status"].tolist())         # ['ok', 'out', 'ok', 'ok']

print(df.at["c", "qty"])             # 5  fast scalar access

Remember

  • .loc is label-based, .iloc position-based; both take [rows, cols]
  • .loc slices include the end label; .iloc slices exclude it
  • Assign with one .loc[row_selector, col] = value call
  • .at/.iat are the fast way to read or write one cell

Watch out for

  • Chained assignment such as df[mask]['col'] = x, which never updates df.

Check yourself

  1. `df` has the default index 0..9. How many rows does `df.loc[2:5]` return?
  2. Which statement reliably sets 'flag' to 1 for rows where x > 0?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pandas.pydata.org/docs/user_guide/indexing.html