Why this matters
As a Data Analyst, you constantly slice, filter, and edit data. Mastering pandas indexers (.loc, .iloc, .at, .iat) lets you do this precisely and fast. Real tasks include:
- Filtering users by date range and selecting only needed columns for a report
- Fixing a single incorrect value quickly without copying data
- Building reproducible data cleaning steps with clear label- or position-based logic
Quick tip
Choose the indexer by what you know: labels (.loc/.at) or positions (.iloc/.iat). Don’t mix them inside the same bracket pair.
Concept explained simply
You have two questions when selecting data:
- Do I select by label (row/column names) or by position (0-based integers)?
- Do I need a range/collection or a single cell?
Mental model
- .loc: label-based selection for rows and columns. Endpoints in label slices are inclusive.
- .iloc: position-based selection for rows and columns. Endpoints in slices follow Python’s exclusive stop.
- .at: fastest label-based access for a single scalar (one row label + one column label).
- .iat: fastest position-based access for a single scalar (one row position + one column position).
Quick reference
- Row by labels: df.loc['Q2':'Q4']
- Row by positions: df.iloc[1:4]
- Row and columns by labels: df.loc['Q2':'Q4', ['city', 'sales']]
- Row and columns by positions: df.iloc[1:4, [0, 2]]
- Boolean filter (recommended): df.loc[df['sales'] > 100, ['city', 'sales']]
- Single value by labels: df.at['Q3', 'sales']
- Single value by positions: df.iat[2, 1]
Inclusive vs exclusive
- .loc slice: df.loc['A':'C'] includes 'C'
- .iloc slice: df.iloc[0:3] includes rows at positions 0,1,2
Worked examples
Example 1: Label-based selection with .loc
import pandas as pd
df = pd.DataFrame({
'city': ['NY', 'NY', 'NY', 'NY'],
'sales': [100, 120, 90, 110],
'returns': [5, 3, 4, 2]
}, index=['Q1', 'Q2', 'Q3', 'Q4'])
# Rows Q2..Q4 (inclusive), columns 'city' and 'sales'
result = df.loc['Q2':'Q4', ['city', 'sales']]
print(result)
What happens?
The row slice 'Q2':'Q4' includes Q2, Q3, Q4 (inclusive end), and only the two specified columns are returned.
Example 2: Position-based selection with .iloc
import pandas as pd
df2 = pd.DataFrame({
'A': [10, 20, 30, 40],
'B': [1, 2, 3, 4],
'C': [7, 8, 9, 10]
}, index=[101, 102, 103, 104])
# First 3 rows, last 2 columns by position
slice_pos = df2.iloc[:3, -2:]
print(slice_pos)
What happens?
Positions are 0-based, and the stop index is exclusive. -2: selects the last two columns (B, C).
Example 3: Fast scalar updates with .at and .iat
import pandas as pd
df = pd.DataFrame({
'city': ['NY', 'NY', 'NY', 'NY'],
'sales': [100, 120, 90, 110]
}, index=['Q1', 'Q2', 'Q3', 'Q4'])
# Update a single value by label
df.at['Q3', 'sales'] = 0
# Update a single value by position
# (row 0, col 1) becomes 200
val = df.iat[0, 1]
df.iat[0, 1] = val * 2
print(df)
Why .at/.iat?
They are optimized for single-cell get/set and avoid overhead of .loc/.iloc when you only need one scalar.
Example 4: Boolean filtering done right
import pandas as pd
df = pd.DataFrame({
'dept': ['A', 'A', 'B', 'B'],
'score': [70, 85, 60, 90]
})
mask = df['score'] >= 80
# Filter rows and select only 'dept'
high = df.loc[mask, ['dept']]
print(high)
# Safer assignment using .loc (avoids SettingWithCopy warnings)
df.loc[mask, 'score'] = df.loc[mask, 'score'] + 1
Exercises
Try these small tasks locally. Then open the solution to compare. Use the checklist to self-verify.
Exercise 1 — Label-based selection and scalar update
Create the DataFrame and perform the selection and update.
import pandas as pd
df = pd.DataFrame({
'city': ['NY', 'NY', 'NY', 'NY'],
'sales': [100, 120, 90, 110],
'returns': [5, 3, 4, 2]
}, index=['Q1', 'Q2', 'Q3', 'Q4'])
# 1) Select rows Q2..Q4 and only columns city, sales
# 2) Set sales at Q3 to 0 (use a scalar accessor)
# 3) Print the selection again
Hints
- Use df.loc['Q2':'Q4', ['city', 'sales']]
- Use df.at['Q3', 'sales'] = 0
Expected output (after the update):
city sales
Q2 NY 120
Q3 NY 0
Q4 NY 110
Show solution
result = df.loc['Q2':'Q4', ['city', 'sales']]
df.at['Q3', 'sales'] = 0
print(df.loc['Q2':'Q4', ['city', 'sales']])
Exercise 2 — Position-based slice and scalar edit
import pandas as pd
df2 = pd.DataFrame({
'A': [10, 20, 30, 40],
'B': [1, 2, 3, 4],
'C': [7, 8, 9, 10]
}, index=[101, 102, 103, 104])
# 1) Print the first 3 rows and last 2 columns using .iloc
# 2) Multiply the scalar at row position 2, column position 0 by 10 using .iat
# 3) Print the first 3 rows to verify
Hints
- df2.iloc[:3, -2:]
- df2.iat[2, 0] = df2.iat[2, 0] * 10
Expected output (for the first 3 rows after update):
A B C
101 10 1 7
102 20 2 8
103 300 3 9
Show solution
print(df2.iloc[:3, -2:])
df2.iat[2, 0] = df2.iat[2, 0] * 10
print(df2.iloc[:3])
Exercise checklist
- You used .loc when selecting by labels and .iloc when selecting by positions
- You used .at/.iat only for single scalars
- Your slices behaved as expected: .loc inclusive, .iloc exclusive
- No SettingWithCopy warnings appeared during assignments
Common mistakes and self-check
- Mixing labels and positions together: df.loc[0:2, 0] is invalid; use .iloc for positions
- Forgetting .loc is inclusive on label slices, accidentally including the end row
- Chained indexing: df[df['x'] > 0]['y'] = 1 can lead to SettingWithCopy; prefer df.loc[df['x'] > 0, 'y'] = 1
- Misspelled column names silently returning KeyError or empty results
- Assuming index is sorted when slicing labels; if order matters, sort_index() first
Self-check mini test
- Can you explain the difference between .loc and .iloc in one sentence?
- Can you show one safe boolean filter with column selection using .loc?
- Can you update one cell by label and by position?
Mini challenge
Given a DataFrame of monthly metrics indexed by month labels (e.g., '2024-01', '2024-02', ...), do the following:
- Select months '2024-03' through '2024-06' and only columns ['visits', 'signups'] using .loc
- Increase 'signups' in '2024-04' by 5 using a scalar accessor
- Create a mask where conversion_rate >= 0.05 and set a new column 'tier' to 'A' for those rows using .loc assignment
Hint
Remember: label slice inclusive, and use df.at['2024-04', 'signups'] for single-cell update.
Who this is for
- Aspiring and junior Data Analysts who use pandas for day-to-day analysis
- Anyone cleaning, filtering, or reporting on tabular data in Python
Prerequisites
- Basic Python syntax (lists, dicts, functions)
- Intro to pandas: creating DataFrames, reading CSVs
Learning path
- Start: Understand DataFrame/Series and indices
- Then: Learn .loc and boolean filtering
- Next: Learn .iloc for position-based slices
- Optimize: Use .at/.iat for single-cell speed
- Apply: Build a small cleaning pipeline using these indexers
Practical projects
- Sales cleanup: Load monthly sales, select last quarter by labels, fix one bad entry with .at, and export
- User segmentation: Filter active users with .loc mask, select just needed columns, and compute summary
- QA audit: Randomly sample 100 rows using .iloc, then spot-correct individual cells with .iat
Next steps
- Practice mixed row/column selection patterns until they feel natural
- Refactor any chained indexing in your scripts to .loc assignments
- Take the Quick Test below to confirm mastery. Available to everyone; sign in to save your progress.