Indexing / Slicing
NumPy + Pandas have rich indexing: positional (.iloc), label-based (.loc), boolean masks, fancy indexing, multi-index. Get it right and queries are O(N) one-liners; get it wrong and you fight chained assignment warnings.
iloc, loc, boolean, fancy
EXAMPLE
import numpy as np
import pandas as pd
# === NumPy indexing ===
# 1) Single value
arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr[0, 0] # 1
arr[2, 1] # 8
# 2) Slicing
arr[0] # row 0: array([1, 2, 3])
arr[:, 0] # column 0: array([1, 4, 7])
arr[:2, 1:] # rows 0-1, cols 1+: [[2, 3], [5, 6]]
arr[::-1] # reversed rows
arr[..., 0] # ellipsis — all dims except last; column 0
# 3) Boolean indexing
mask = arr > 4
arr[mask] # array([5, 6, 7, 8, 9])
arr[arr % 2 == 0] # even values
# 4) Fancy indexing — pass arrays of indices
idx = np.array([0, 2])
arr[idx] # rows 0 and 2
arr[:, [0, 2]] # cols 0 and 2
arr[[0, 2], [1, 2]] # diagonal pick: arr[0,1] and arr[2,2] → array([2, 9])
# Combine masks
mask = (arr > 2) & (arr < 8)
arr[mask]
# 5) np.where — vectorised conditional
arr_clipped = np.where(arr > 5, 5, arr) # cap at 5
idx = np.where(arr > 5) # indices where condition is true
# tuple of arrays — one per dimension
# === Pandas indexing ===
df = pd.DataFrame({
'name': ['Ada', 'Bo', 'Cy', 'Di'],
'age': [32, 28, 41, 22],
'city': ['Sydney', 'Sydney', 'Melbourne', 'Brisbane'],
'score':[95, 88, 92, 75],
}, index=['u1', 'u2', 'u3', 'u4'])
# 6) .loc — label-based (inclusive!)
df.loc['u1'] # row by label
df.loc['u1', 'name'] # cell
df.loc['u1':'u3'] # rows u1 to u3 (inclusive)
df.loc[:, 'name'] # column
df.loc[:, ['name', 'age']] # multiple columns
df.loc['u1':'u3', ['name', 'city']] # rows + cols by label
# 7) .iloc — positional (exclusive end, like Python slices)
df.iloc[0] # first row
df.iloc[-1] # last row
df.iloc[0, 0] # cell at row 0, col 0
df.iloc[0:2] # rows 0, 1 (NOT 2)
df.iloc[:, 0] # first column
df.iloc[[0, 2], [1, 3]] # fancy positional
# 8) Direct subscript (mixed semantics — avoid for indexing!)
df['name'] # column (Series)
df[['name', 'age']] # subset of columns
df[df['age'] >= 30] # boolean mask
# Beware: df['u1'] looks for a COLUMN, not a row.
# 9) Boolean mask
mask = (df['age'] >= 30) & (df['city'] == 'Sydney')
df[mask]
df.loc[mask] # same
# 10) Multiple conditions
df[df['city'].isin(['Sydney', 'Melbourne'])]
df[df['name'].str.startswith('A')]
df[df['age'].between(25, 40)]
df[df['age'].isna()] # missing values
df[~df['age'].isna()] # not missing
# 11) Query — string-based
df.query('age >= 30 and city == "Sydney"')
df.query('city in ["Sydney", "Brisbane"]')
df.query('name.str.startswith("A")', engine='python')
# Variables — use @ prefix
min_age = 30
df.query('age >= @@min_age')
# 12) Set / update values
df.loc['u1', 'age'] = 33
df.loc[df['city'] == 'Sydney', 'age'] += 1
df.loc[:, 'bonus'] = df['score'] * 0.1
# 13) Chained indexing (anti-pattern!)
# BAD — SettingWithCopyWarning:
df[df['age'] >= 30]['city'] = 'XX' # may not modify the original!
# GOOD — use .loc:
df.loc[df['age'] >= 30, 'city'] = 'XX'
# 14) MultiIndex (hierarchical)
df2 = df.set_index(['city', 'name'])
df2.loc['Sydney'] # all Sydney rows
df2.loc[('Sydney', 'Ada')] # one row
df2.xs('Ada', level='name') # cross-section
df2.loc[('Sydney', slice(None)), :] # all names in Sydney
# Sort the index for performance
df2 = df2.sort_index()
# 15) at / iat — fast single-cell access
df.at['u1', 'age'] # like loc, but single cell only
df.iat[0, 0] # like iloc, single cell
# 16) Reindex — align to new index
new_idx = ['u1', 'u2', 'u3', 'u4', 'u5']
df.reindex(new_idx) # u5 gets NaN
df.reindex(new_idx, fill_value=0)
# 17) Drop
df.drop('u1') # drop row by label
df.drop(['u1', 'u2'])
df.drop(columns=['city'])
df.drop(columns=['city', 'score'])
# 18) Rename
df.rename(columns={'name': 'full_name'})
df.rename(index={'u1': 'user1'})
# 19) Replace + map
df['city'] = df['city'].replace('Sydney', 'SYD')
df['grade'] = df['score'].apply(lambda s: 'A' if s >= 90 else 'B' if s >= 80 else 'C')
# 20) Common patterns
# Filter + select + sort
df[df['age'] >= 30][['name', 'city']].sort_values('city')
# Update by condition
df.loc[df['score'] < 80, 'status'] = 'low'
# Conditional column
df['decade'] = (df['age'] // 10) * 10
# Add row
new_row = pd.DataFrame({'name': ['Ed'], 'age': [29], 'city': ['Perth'], 'score': [85]}, index=['u5'])
df = pd.concat([df, new_row])
# 21) NumPy slicing → view, NOT copy
a = np.array([1, 2, 3, 4, 5])
b = a[1:4]
b[0] = 99 # mutates a too!
print(a) # [1, 99, 3, 4, 5]
# Copy explicitly to break the link:
c = a[1:4].copy()
# 22) Pandas — .loc returns view OR copy (depends on context)
# Use .copy() defensively:
sub = df.loc[df['age'] >= 30].copy()
sub['age'] += 1 # safe; doesn't warn
# 23) iloc vs loc: gotcha
df = pd.DataFrame({'a': [1,2,3]}, index=[10, 20, 30])
df.loc[10] # 1 (label)
df.iloc[10] # IndexError — out of range positions
# 24) When to use which
# Single cell, fast → .at / .iat
# By label → .loc
# By position → .iloc
# Boolean filter → .loc with mask OR df[mask]
# String-based DSL → .query()
# Hierarchical → MultiIndex + .loc with tuples / slice(None)
# 25) Performance
# - .loc / .iloc beat repeated subscripts in a loop (vectorise)
# - Use boolean mask once; assign with .loc to avoid chained warnings
# - For huge DataFrames, look at .at / .iat for single cells
# - .query() can be 2-3x faster than equivalent boolean indexing for complex conditions
# - Use categorical dtype for low-cardinality string columns
Why it matters
Use .loc for label, .iloc for position; never mix in chained indexing. df[mask][\"col\"] = ... is the SettingWithCopyWarning trap — replace with df.loc[mask, \"col\"] = ... every time.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import numpy as np a = np.arange(12).reshape(3, 4) print(a[1, 2]) # row 1 col 2 print(a[:, 1]) # whole column 1 print(a[1:3, 0:2]) # sub-gridTry it Yourself »
Discussion
Loading…