loc / iloc
Selecting the right rows and columns is the most common Pandas operation. Master loc, iloc, boolean masks, and query — and the SettingWithCopyWarning that comes with chained indexing — and 70% of pandas work feels routine.
loc, iloc, masks, query, copy
EXAMPLE
import pandas as pd
import numpy as np
# 1) Sample frame
df = pd.DataFrame({
'name': ['mara', 'sam', 'alex', 'kim', 'lee'],
'age': [30, 22, 45, 18, 60],
'city': ['Sydney', 'Sydney', 'Melbourne', 'Brisbane', 'Sydney'],
'salary': [95_000, 72_000, 110_000, 0, 0],
'active': [True, True, False, True, False],
}, index=['e1', 'e2', 'e3', 'e4', 'e5'])
# 2) Column selection
df['name'] # Series
df[['name', 'salary']] # DataFrame (note the double brackets)
df.name # also works — but breaks for names like 'class'
# 3) Row selection by LABEL — .loc
df.loc['e3'] # one row by index label → Series
df.loc[['e1', 'e3']] # multiple rows → DataFrame
df.loc['e2':'e4'] # INCLUSIVE on both ends with .loc
# 4) Row selection by POSITION — .iloc
df.iloc[0] # first row (position 0)
df.iloc[-1] # last row
df.iloc[1:3] # rows at positions 1, 2 (exclusive on stop, like Python)
df.iloc[[0, 2, 4]]
# 5) Rows + columns together
df.loc['e2':'e4', ['name', 'salary']]
df.iloc[1:4, [0, 3]]
# 6) Boolean masks — the workhorse
df[df['age'] >= 30]
df[(df['city'] == 'Sydney') & (df['salary'] > 70_000)] # use & | ~ (NOT and / or / not)
df[df['city'].isin(['Sydney', 'Brisbane'])]
df[~df['active']] # inactive rows
df[df['name'].str.startswith('m')]
df[df['salary'].between(50_000, 100_000)]
# Round parentheses around each comparison are MANDATORY because & has higher precedence than ==.
# 7) .loc with a mask — explicit and the recommended idiom for assignment
df.loc[df['salary'] == 0, 'active'] = False
df.loc[df['city'] == 'Sydney', ['salary']] *= 1.10 # 10% raise
# 8) .query — readable filter expressions
df.query('age >= 30 and city == \"Sydney\"')
df.query('salary > @cutoff', local_dict={'cutoff': 70_000})
df.query('name.str.startswith("m")', engine='python')
# 9) Missing values
df2 = pd.DataFrame({'a': [1, np.nan, 3], 'b': ['x', 'y', None]})
df2.dropna() # drop rows with any NaN
df2.dropna(subset=['a']) # only check column 'a'
df2.fillna({'a': 0, 'b': ''})
df2['a'].isna() | df2['b'].isna() # mask of any-na rows
# 10) at / iat — fast scalar access
df.at['e1', 'salary'] # single cell by label
df.iat[0, 3] # single cell by position
df.at['e1', 'salary'] = 99_000 # fast assignment
# 11) Setting WITHOUT chained indexing
# ❌ Bad: chained — Pandas can't tell if you wanted a copy or the original
subset = df[df['city'] == 'Sydney']
subset['salary'] = 0 # SettingWithCopyWarning; original may or may not change
# ✓ Good: single .loc on the original
df.loc[df['city'] == 'Sydney', 'salary'] = 0
# When you actually want a separate frame
subset = df[df['city'] == 'Sydney'].copy()
subset['salary'] = 0 # safe — explicit copy
# 12) Index manipulation
df.reset_index(drop=False) # 'e1' etc. becomes a column
df.set_index('name') # name becomes the index
df.sort_index() # sort by index labels
# 13) MultiIndex selection
mi = df.set_index(['city', 'name']).sort_index()
mi.loc['Sydney'] # all Sydney rows
mi.loc[('Sydney', 'mara')] # one row
mi.loc[('Sydney', slice(None)), :] # cross-section
mi.xs('Sydney', level='city') # explicit cross-section
# 14) Where vs mask
df['salary'].where(df['active']) # NaN where condition is False
df['salary'].mask(df['active']) # NaN where condition is True
# 15) Sample, head, tail
df.head(3)
df.tail(2)
df.sample(n=2, random_state=42) # reproducible sample
# 16) Performance tips
# • Vectorize: df['a'] + df['b'] beats a Python for-loop by orders of magnitude
# • Avoid .iterrows() and .apply() unless you really need them
# • Prefer .loc / .iloc over chained slices for assignment
# • For huge frames, use categorical dtype on low-cardinality string columns
# • Index for repeated lookups: df.set_index('col') + df.loc[key]
# 17) Common bugs
# • df[df.age > 30] AND df.city == 'X' (Python and) → ValueError; use parens + &
# • Using df.col when col contains spaces — switch to df['col']
# • df.loc on integer index that LOOKS positional — .loc is label-based always
# • Chained indexing with SettingWithCopyWarning — silent partial updates; use single .loc
# • .iloc with a label key — KeyError or wrong row; mismatched .loc vs .iloc semantics
# • Forgetting .copy() when you'll mutate — accidental writes to the source frame
Why it matters
.loc for labels, .iloc for positions, boolean masks for everything else. When the SettingWithCopyWarning appears, you almost always want either df.loc[mask, col] = ... on the original frame, or an explicit .copy() when you really do want a detached frame to mutate.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import pandas as pd
df = pd.read_csv('users.csv')
print(df.loc[0, 'name']) # label-based
print(df.iloc[0, 1]) # position-based
print(df.loc[df['age'] > 30, ['name', 'age']])
Try it Yourself »
Exercise
Label-based row/column selector.
df.
[0, 'name']
Three letters.
Discussion
Loading…