Selecting Columns & Rows¶
How to pick specific data from a DataFrame. The single most-used skill in Pandas.
Quick reference¶
| Goal | Code |
|---|---|
| One column → Series | df["col"] or df.col |
| Multiple columns | df[["c1", "c2"]] |
| Row by label | df.loc["row_label"] |
| Row by position | df.iloc[0] |
| Specific cell by label | df.loc["row_label", "col"] |
| Specific cell by position | df.iloc[0, 1] |
| Boolean filter | df[df.col > 5] |
Set up¶
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol", "Dave", "Eve"],
"age": [25, 30, 35, 40, 45],
"city": ["Mumbai", "Delhi", "Mumbai", "Pune", "Delhi"],
"salary": [50000, 60000, 75000, 90000, 100000],
}, index=["a", "b", "c", "d", "e"])
print(df)
We'll use this for the rest of the chapter.
Selecting columns¶
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol"],
"age": [25, 30, 35],
"salary": [50000, 60000, 75000],
})
# Single column → Series
print(df["name"])
print(type(df["name"]).__name__) # Series
print()
# Same — attribute access (only works for valid Python names)
print(df.name)
print()
# Multiple columns → DataFrame
print(df[["name", "salary"]])
print(type(df[["name", "salary"]]).__name__) # DataFrame
Heads up:
df["name"]works always.df.nameworks only if the column name is a valid Python identifier (no spaces, no leading digits, not a reserved word).
Selecting rows by label — .loc[]¶
import pandas as pd
df = pd.DataFrame({
"age": [25, 30, 35, 40, 45],
"city": ["Mumbai", "Delhi", "Mumbai", "Pune", "Delhi"],
}, index=["a", "b", "c", "d", "e"])
# Single row by label
print(df.loc["b"])
print(type(df.loc["b"]).__name__) # Series
# Multiple rows
print()
print(df.loc[["a", "c", "e"]])
# Range of labels — INCLUSIVE on both ends
print()
print(df.loc["b":"d"])
Selecting rows by position — .iloc[]¶
import pandas as pd
df = pd.DataFrame({
"age": [25, 30, 35, 40, 45],
"city": ["Mumbai", "Delhi", "Mumbai", "Pune", "Delhi"],
}, index=["a", "b", "c", "d", "e"])
# Single row by position
print(df.iloc[0])
# First 3 rows — EXCLUSIVE on the end (like Python lists)
print()
print(df.iloc[0:3])
# Last 2 rows
print()
print(df.iloc[-2:])
# Pick specific positions
print()
print(df.iloc[[0, 2, 4]])
.loc vs .iloc — the key difference¶
.loc |
.iloc |
|
|---|---|---|
| Selects by | label | integer position |
| Slice endpoint | inclusive | exclusive (like Python) |
| Default index (0,1,2,…) | uses those numbers as labels | uses them as positions — looks the same |
| Custom index | uses your labels | still uses 0,1,2 positions |
Rule of thumb: use .loc 90% of the time. .iloc is for "first 100 rows" / "every 10th row" type ops.
Cell selection — row + column at once¶
import pandas as pd
df = pd.DataFrame({
"age": [25, 30, 35],
"city": ["Mumbai", "Delhi", "Pune"],
}, index=["a", "b", "c"])
# loc: [row_label, col_label]
print(df.loc["b", "age"]) # 30
# iloc: [row_pos, col_pos]
print(df.iloc[1, 0]) # 30
# Slice of rows × specific cols (loc)
print(df.loc["a":"b", "city"])
# Slice of rows × slice of cols
print(df.iloc[0:2, 0:2])
at and iat — single-cell access (fast)¶
For getting / setting one cell, these are faster than .loc / .iloc:
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, index=["x", "y", "z"])
print(df.at["y", "a"]) # 2
print(df.iat[1, 0]) # 2
# Set
df.at["y", "a"] = 99
print(df)
Boolean filtering — df[condition]¶
The bread-and-butter row filter:
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol", "Dave"],
"age": [25, 30, 35, 40],
"salary": [50000, 60000, 75000, 90000],
})
# Rows where age > 30
print(df[df["age"] > 30])
print()
# Rows where age > 30 AND salary > 70000
print(df[(df["age"] > 30) & (df["salary"] > 70000)])
print()
# Rows where city is one of several
df2 = pd.DataFrame({
"name": ["Alice","Bob","Carol","Dave"],
"city": ["Mumbai","Delhi","Pune","Mumbai"],
})
print(df2[df2["city"].isin(["Mumbai", "Pune"])])
Always wrap conditions in parens for
&/|. Don't useand/or— those don't work element-wise.
.query() — readable filtering¶
For complex conditions:
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol", "Dave"],
"age": [25, 30, 35, 40],
"salary": [50000, 60000, 75000, 90000],
})
# Use column names like variables
print(df.query("age > 30 and salary > 70000"))
print()
# Reference Python vars with @
min_age = 30
print(df.query("age > @min_age"))
Selecting by both row condition AND column subset¶
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol", "Dave"],
"age": [25, 30, 35, 40],
"city": ["Mumbai", "Delhi", "Pune", "Mumbai"],
"salary": [50000, 60000, 75000, 90000],
})
# Rows where age>30, only the name and salary columns
print(df.loc[df["age"] > 30, ["name", "salary"]])
This is the most useful pattern — combining a filter with column selection.
Slice with step¶
import pandas as pd
df = pd.DataFrame({"x": range(10)})
# Every 2nd row
print(df.iloc[::2])
# Reverse
print(df.iloc[::-1])
Setting values via .loc¶
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol"],
"age": [25, 30, 35],
"active": [True, True, True],
})
# Set a single cell
df.loc[0, "age"] = 26
print(df)
# Set a column based on a condition
df.loc[df["age"] > 30, "active"] = False
print(df)
# Set multiple columns at once
df.loc[df["age"] < 30, ["age", "active"]] = [20, False]
print(df)
Always use .loc for assignment — df[df.x > 5]["col"] = 99 gives the dreaded SettingWithCopyWarning and may not actually work.
Combining selection — practical example¶
import pandas as pd
import numpy as np
rng = np.random.default_rng(0)
n = 12
df = pd.DataFrame({
"id": range(1, n+1),
"name": [f"User{i}" for i in range(1, n+1)],
"city": rng.choice(["Mumbai", "Delhi", "Pune"], size=n),
"age": rng.integers(20, 60, size=n),
"salary": rng.integers(40_000, 150_000, size=n),
})
# Find high earners in Mumbai
print("High earners in Mumbai:")
print(df.loc[
(df["city"] == "Mumbai") & (df["salary"] > 100_000),
["name", "age", "salary"]
])
Cheatsheet — pick the right tool¶
| If you need to... | Use |
|---|---|
| One column as Series | df["col"] |
| Multiple columns | df[["c1", "c2"]] |
| Row by label | df.loc["label"] |
| Row by position | df.iloc[0] |
| Cell by label | df.loc["row", "col"] or df.at["row", "col"] |
| Cell by position | df.iloc[0, 0] or df.iat[0, 0] |
| Filter rows | df[df["col"] > 5] or df.query("col > 5") |
| Filter rows + pick cols | df.loc[mask, ["c1", "c2"]] |
| Set a value | df.loc[mask, "col"] = value |
| Membership filter | df[df["c"].isin([...])] |
| Negation | df[~df["c"].isin([...])] |
Common pitfalls¶
- ❗ Using
df[df.x > 5]["col"] = 99— givesSettingWithCopyWarning. Always assign via.loc. - ❗
and/orin conditions — element-wise needs&/|. And wrap each condition in parens. - ❗ Mixing labels and positions —
df.loc[0:3]with a custom index is by label and INCLUSIVE;df.iloc[0:3]is by position and EXCLUSIVE. Different results. - ❗
df.namefailing —nameis a special attribute. Always usedf["name"]for clarity. - ❗ Forgetting
.copy()for new datasets —sub = df[df.x > 5]is a view. Modifyingsubcan affectdf. Use.copy()if you want independence.
Practice¶
What does this print?
Expected: 3
Set the 'active' column to False for all rows where age > 30 (without warnings)
Expected: False
Quiz — Quick check¶
What you remember
Q1. When you slice with .loc, is the end label inclusive or exclusive?
- Exclusive — like Python lists
- Inclusive — both ends are included
- Depends on the dtype
- Configurable
Why:
df.loc["a":"d"]includes rows with labels "a" through "d".df.iloc[0:4]excludes index 4 (like Python). This trips up everyone the first time.
Q2. When should you use .iloc instead of .loc?
- Always — it's faster
- When you want positional access (first N rows, every other row, etc.) independent of the labels
- Only for boolean masks
- Never —
.locis enough
Why:
.locis for label-based selection;.ilocis for position-based. Most filtering and assignment uses.loc;.ilocis best for index-aware operations like "first 100 rows".
Q3. Which raises SettingWithCopyWarning?
-
df.loc[df.x > 5, "y"] = 99 -
df[df.x > 5]["y"] = 99 -
df["y"] = 99 - None
Why: Chained assignment (
df[...][...] = ...) operates on a temporary copy, so the assignment doesn't propagate todf. Use the single-step.locform to guarantee it works.
Common doubts¶
Is df.col ever a bad idea?
Yes — attribute access fails for column names that aren't valid Python identifiers (df["full name"], df["1st_quarter"]). It also collides with Pandas method names (df.shape, df.index — these are attributes of the DataFrame, not columns). Bracket-style df["col"] always works and is unambiguous.
What does SettingWithCopyWarning mean and how do I fix it?
Pandas isn't sure whether your assignment will modify the original DataFrame or a temporary copy. To make it unambiguous, always use .loc[mask, column] = value (single-step). Avoid df[df.x > 5]["col"] = value (two-step).
When should I use .at and .iat?
For getting or setting a single cell. .at/.iat are faster than .loc/.iloc for that case. For multi-cell operations, .loc/.iloc are required.