Boolean Indexing
Boolean Indexing
Boolean indexing is the most common filtering technique in pandas. You create a boolean Series (True/False values) and pass it to the DataFrame to select only the rows where the condition is True.
How It Works
When you compare a DataFrame column to a value, pandas returns a boolean Series — True where the condition holds, False elsewhere:
import pandas as pd
df = pd.DataFrame({
"product": ["A", "B", "C", "D", "E"],
"category": ["Food", "Tech", "Food", "Tech", "Food"],
"price": [10.99, 299.99, 5.49, 149.99, 7.99],
"quantity": [50, 10, 100, 25, 80]
})
# Step 1: Create the boolean mask
mask = df["category"] == "Food"
print(mask)
# 0 True
# 1 False
# 2 True
# 3 False
# 4 True
# Name: category, dtype: bool
# Step 2: Apply the mask to filter rows
food_products = df[mask]
print(food_products)
# product category price quantity
# 0 A Food 10.99 50
# 2 C Food 5.49 100
# 4 E Food 7.99 80
Comparison Operators
df["price"] > 100 # greater than
df["price"] <= 50 # less than or equal
df["category"] == "Tech" # equal to
df["category"] != "Food" # not equal
df["price"].between(10, 100) # range (inclusive)
Each comparison produces a boolean Series. The mask selects rows where the value is True.
Combining Conditions
Use & (and), | (or), and ~ (not) to combine multiple conditions. Each condition must be wrapped in parentheses:
# AND: Food products with price below 10
mask = (df["category"] == "Food") & (df["price"] < 10)
result = df[mask]
# product category price quantity
# 2 C Food 5.49 100
# 4 E Food 7.99 80
# OR: Tech products OR items with quantity above 90
mask = (df["category"] == "Tech") | (df["quantity"] > 90)
result = df[mask]
# product category price quantity
# 1 B Tech 299.99 10
# 2 C Food 5.49 100
# NOT: Everything except Tech
mask = ~(df["category"] == "Tech")
result = df[mask]
The parentheses are mandatory. Without them, Python's operator precedence causes & to bind before ==, producing incorrect results or errors.
Why Boolean Indexing
Boolean indexing is explicit and flexible. You can build complex conditions step by step, inspect intermediate masks, and reuse masks across multiple operations. It is the foundation that other selection methods build on.
Common Pitfalls
- Forgetting parentheses:
df["a"] > 1 & df["b"] < 2fails. Write(df["a"] > 1) & (df["b"] < 2). - Using
and/orinstead of&/|: Python'sandandorwork on single boolean values, not pandas Series. Use&and|for element-wise operations. - Chaining filters:
df[df["a"] > 1][df["b"] < 2]works but is inefficient — it creates an intermediate DataFrame. Combine into one mask instead.
Reusing Masks
Store masks in variables when you need to apply the same filter multiple times:
high_value = df["price"] > 100
in_stock = df["quantity"] > 0
# Apply separately
expensive = df[high_value]
available = df[in_stock]
# Combine
available_expensive = df[high_value & in_stock]
This makes your code self-documenting. The mask name describes the intent, and you can reuse it without rewriting the condition.
Selecting with .loc
Selecting with .loc
.loc selects rows and columns by their labels. It is the primary method for label-based selection in pandas.
Selecting Rows by Label
import pandas as pd
df = pd.DataFrame({
"product": ["A", "B", "C"],
"price": [10.99, 299.99, 5.49],
"quantity": [50, 10, 100]
}, index=["r1", "r2", "r3"])
# Select a single row by label
row = df.loc["r1"]
# product A
# price 10.99
# quantity 50
# Name: r1, dtype: object
# Select multiple rows by label
subset = df.loc[["r1", "r3"]]
# product price quantity
# r1 A 10.99 50
# r3 C 5.49 100
# Slice by label (inclusive on both ends)
subset = df.loc["r1":"r2"]
# product price quantity
# r1 A 10.99 50
# r2 B 299.99 10
Selecting Rows and Columns Together
.loc accepts two arguments: row selector and column selector, separated by a comma.
# Specific rows, specific columns
result = df.loc[["r1", "r2"], ["product", "price"]]
# product price
# r1 A 10.99
# r2 B 299.99
# All rows, specific columns
result = df.loc[:, ["product", "quantity"]]
# product quantity
# r1 A 50
# r2 B 10
# r3 C 100
# Slice rows, slice columns
result = df.loc["r1":"r2", "product":"price"]
# product price
# r1 A 10.99
# r2 B 299.99
Boolean Mask with .loc
You can combine .loc with boolean masks for powerful filtering:
# Rows where price > 100, only product and quantity columns
result = df.loc[df["price"] > 100, ["product", "quantity"]]
# product quantity
# r2 B 10
# Multiple conditions
mask = (df["price"] > 5) & (df["quantity"] >= 50)
result = df.loc[mask, ["product", "price"]]
# product price
# r1 A 10.99
# r3 C 5.49
Why .loc Over Boolean Indexing Alone
Boolean indexing (df[mask]) selects rows but returns all columns. .loc lets you select rows and columns in a single operation, making your intent clear and your code concise.
Setting Values with .loc
.loc is also for assignment:
# Update a single value
df.loc["r1", "price"] = 12.99
# Update multiple values
df.loc[["r1", "r2"], "quantity"] = [60, 15]
# Update based on condition
mask = df["price"] < 10
df.loc[mask, "quantity"] = 0
Key Rule
.loc is label-based. If your DataFrame has a default integer index (0, 1, 2, ...), .loc[0] selects the row labeled 0, not the first row by position. If you need position-based selection, use .iloc.
When to Use .loc
Use .loc when your DataFrame has meaningful row labels — dates, product IDs, customer names. Label-based selection makes code self-documenting: df.loc["2024-01-15"] is clearer than df.iloc[0] when the first row might change order.
Selecting with Callable
Both .loc and .iloc accept callable (function) selectors. The function receives the index (for .loc) or the DataFrame (for .iloc) and returns a boolean mask:
# Select rows where the index label starts with '2024'
df.loc[lambda x: x.index.str.startswith('2024')]
# Select columns where the mean is above 100
df.loc[:, lambda x: x.mean() > 100]
Callables are useful for dynamic selection logic that would be verbose to write inline. They are evaluated once at selection time, so the function runs on the current state of the DataFrame.
Selecting with .iloc
Selecting with .iloc
.iloc selects rows and columns by their integer position (0-based). It works like Python list indexing but for DataFrames.
Selecting by Position
import pandas as pd
df = pd.DataFrame({
"product": ["A", "B", "C", "D"],
"price": [10.99, 299.99, 5.49, 149.99],
"quantity": [50, 10, 100, 25]
})
# Select the first row (position 0)
row = df.iloc[0]
# product A
# price 10.99
# quantity 50
# Name: 0, dtype: object
# Select rows 0 and 2
subset = df.iloc[[0, 2]]
# product price quantity
# 0 A 10.99 50
# 2 C 5.49 100
# Slice rows (start inclusive, end exclusive — like Python lists)
subset = df.iloc[1:3]
# product price quantity
# 1 B 299.99 10
# 2 C 5.49 100
Selecting Rows and Columns by Position
# Rows 0-1, columns 0-2
result = df.iloc[0:2, 0:2]
# product price
# 0 A 10.99
# 1 B 299.99
# First two rows, last column
result = df.iloc[:2, -1]
# 0 50
# 1 10
# Name: quantity, dtype: int64
# All rows, columns at positions 0 and 2
result = df.iloc[:, [0, 2]]
# product quantity
# 0 A 50
# 1 B 10
# 2 C 100
# 3 D 25
.loc vs .iloc
| Aspect | .loc | .iloc |
|---|---|---|
| Selection by | Label | Integer position |
| Slice end | Inclusive | Exclusive |
| Works with boolean masks | Yes | Yes |
| Default index behavior | .loc[0] finds label 0 |
.iloc[0] finds first row |
# With default integer index, these are equivalent:
df.loc[0] # row labeled 0
df.iloc[0] # first row by position
# But they diverge if the index is not 0, 1, 2, ...
df2 = df.set_index("product")
df2.loc["A"] # works — label "A" exists
df2.iloc[0] # works — first row regardless of label
df2.loc[0] # KeyError — no label 0
When to Use .iloc
Use .iloc when position matters more than labels:
- Extracting the first or last N rows:
df.iloc[:10]ordf.iloc[-5:]. - Selecting columns by position when names are unknown:
df.iloc[:, 0]. - Working with default integer indices:
.ilocis unambiguous. - Programmatic selection: When positions come from a variable or calculation.
Negative Indexing
.iloc supports negative indices, just like Python lists:
df.iloc[-1] # last row
df.iloc[-3:] # last 3 rows
df.iloc[:, -1] # last column
Common Mistake
Using .loc with positions when the index is not 0-based integers. If you set a string index and then call .loc[0], you get a KeyError. Use .iloc[0] for position-based access regardless of the index type.
Practical Example: Train/Test Split
When splitting data for machine learning, you often select by position:
# First 80% for training, last 20% for testing
split = int(len(df) * 0.8)
train = df.iloc[:split]
test = df.iloc[split:]
Position-based selection is the natural choice here because the split point is a number, not a label.
Filtering with .query()
Filtering with .query()
.query() filters DataFrame rows using a string expression. It produces cleaner syntax for complex conditions and is faster for large DataFrames.
Basic Syntax
import pandas as pd
df = pd.DataFrame({
"product": ["A", "B", "C", "D", "E"],
"category": ["Food", "Tech", "Food", "Tech", "Food"],
"price": [10.99, 299.99, 5.49, 149.99, 7.99],
"quantity": [50, 10, 100, 25, 80]
})
# Simple condition
result = df.query("price > 100")
# product category price quantity
# 1 B Tech 299.99 10
# 3 D Tech 149.99 25
# String comparison
result = df.query('category == "Food"')
# product category price quantity
# 0 A Food 10.99 50
# 2 C Food 5.49 100
# 4 E Food 7.99 80
Combining Conditions
# AND
result = df.query('category == "Food" and price < 10')
# product category price quantity
# 2 C Food 5.49 100
# 4 E Food 7.99 80
# OR
result = df.query('category == "Tech" or quantity > 90')
# product category price quantity
# 1 B Tech 299.99 10
# 2 C Food 5.49 100
# NOT
result = df.query('not category == "Tech"')
# product category price quantity
# 0 A Food 10.99 50
# 2 C Food 5.49 100
# 4 E Food 7.99 80
Using Variables in Queries
Reference Python variables with @:
threshold = 100
result = df.query("price > @threshold")
cat = "Food"
result = df.query("category == @cat and price < @threshold")
The @ symbol tells pandas to look up the variable in the surrounding Python scope, not as a DataFrame column name.
Why .query() Over Boolean Indexing
- Readability:
df.query('category == "Food" and price < 10')is more readable thandf[(df["category"] == "Food") & (df["price"] < 10)]. - No parentheses: The expression is a string, so you do not need parentheses around each condition.
- Performance: For large DataFrames,
.query()can be faster because it evaluates the expression in a single pass using pandas' internal engine. - Familiar syntax: The expression syntax resembles SQL, which lowers the barrier for analysts coming from SQL.
When .query() Falls Short
- Complex logic: If you need function calls or complex transformations in the condition, boolean indexing is more flexible.
- Column names with spaces: Use backticks:
df.query("column name> 5"). - Dynamic column names: If the column name is stored in a variable, boolean indexing is easier:
df[df[col_name] > 5].
Combining .query() with Column Selection
.query() filters rows. Chain it with column selection:
result = (df
.query("category == 'Food' and price < 10")
.loc[:, ["product", "price"]]
)
# product price
# 2 C 5.49
# 4 E 7.99
This pattern — filter with .query(), select with .loc — is clean and readable. Each method does one thing well.
Performance Consideration
For small DataFrames (under 100,000 rows), the performance difference between .query() and boolean indexing is negligible. For large DataFrames, .query() can be 10-20% faster because it avoids creating intermediate boolean arrays. However, readability should be your primary concern — choose the method that makes your code clearest.
Practice Problems
Given a DataFrame with columns 'department', 'salary', and 'years_experience', find all employees in 'Engineering' with salary above 90000 and at least 3 years of experience.
Solution
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"department": ["Eng", "Sales", "Eng", "Eng", "Sales"],
"salary": [95000, 72000, 88000, 105000, 68000],
"years_experience": [5, 2, 3, 7, 1]
})
mask = (
(df["department"] == "Eng")
& (df["salary"] > 90000)
& (df["years_experience"] >= 3)
)
result = df[mask]
print(result)
# name department salary years_experience
# 0 Alice Eng 95000 5
# 3 Diana Eng 105000 7 Given a DataFrame with string index labels, use .loc to select rows 'r2' and 'r4' with columns 'name' and 'salary'. Then use .iloc to select the first 3 rows and last 2 columns.
Solution
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"department": ["Eng", "Sales", "Eng", "Eng", "Sales"],
"salary": [95000, 72000, 88000, 105000, 68000]
}, index=["r1", "r2", "r3", "r4", "r5"])
# .loc: rows 'r2' and 'r4', columns 'name' and 'salary'
result1 = df.loc[["r2", "r4"], ["name", "salary"]]
print(result1)
# name salary
# r2 Bob 72000
# r4 Diana 105000
# .iloc: first 3 rows, last 2 columns
result2 = df.iloc[:3, -2:]
print(result2)
# department salary
# r1 Eng 95000
# r2 Sales 72000
# r3 Eng 88000 Given a DataFrame with columns 'product', 'category', 'price', and 'stock', use .query() to find all 'Electronics' products with price below 500 and stock above 0. Then select only the 'product' and 'price' columns.
Solution
import pandas as pd
df = pd.DataFrame({
"product": ["Laptop", "Phone", "Tablet", "Monitor", "Keyboard"],
"category": ["Electronics", "Electronics", "Electronics", "Electronics", "Accessories"],
"price": [999.99, 699.99, 399.99, 299.99, 79.99],
"stock": [10, 0, 25, 15, 50]
})
result = (df
.query("category == 'Electronics' and price < 500 and stock > 0")
.loc[:, ["product", "price"]]
)
print(result)
# product price
# 2 Tablet 399.99
# 3 Monitor 299.99 Quiz
1. What is the correct way to combine two boolean conditions in pandas?
2. What is the difference between .loc and .iloc?
3. How do you reference a Python variable inside a .query() expression?
Flashcards
Question
What is boolean indexing in pandas?
Click to reveal answer
Answer
Creating a boolean Series (True/False) from a condition and passing it to the DataFrame to select rows where the condition is True. Example: df[df['price'] > 100].
Question
Why must you wrap each condition in parentheses when combining with & or |?
Click to reveal answer
Answer
Python's operator precedence causes & to bind before ==. Without parentheses, df['a'] > 1 & df['b'] < 2 is parsed as df['a'] > (1 & df['b']) < 2, which fails. Write (df['a'] > 1) & (df['b'] < 2).
Question
What does .loc do that boolean indexing alone does not?
Click to reveal answer
Answer
.loc selects both rows and columns in a single operation: df.loc[row_mask, ['col1', 'col2']]. Boolean indexing (df[mask]) selects rows but returns all columns.
Question
When would you use .query() instead of boolean indexing?
Click to reveal answer
Answer
Use .query() for readability with complex conditions — the syntax resembles SQL and avoids parentheses. It is also faster for very large DataFrames. Use boolean indexing when you need function calls or dynamic column names.
Revision Notes
Key Takeaways
- 1. Boolean indexing filters rows by creating a True/False mask from conditions
- 2. Combine conditions with & (and), | (or), ~ (not) — always use parentheses
- 3. .loc selects by label; .iloc selects by integer position
- 4. .query() provides SQL-like filtering syntax and is faster for large DataFrames
- 5. Chain .query() with .loc for clean filter-then-select workflows
Interview Tips
- • Explain the difference between .loc (label-based) and .iloc (position-based) with examples
- • Describe why parentheses are required when combining boolean conditions in pandas
- • Mention when .query() is preferable to boolean indexing (readability, performance on large data)
Cheat Sheet
Cheat Sheet: Filtering and Selecting Rows
Boolean Indexing
mask = (df['col'] > 10) & (df['other'] == 'value')
df[mask] # filtered rows, all columns
df[mask, ['col1', 'col2']] # WRONG — use .loc
.loc — Label-based
df.loc['label'] # single row by label
df.loc[['l1', 'l2'], ['c1', 'c2']] # specific rows and columns
df.loc[mask, 'col'] # boolean mask + column
df.loc['r1':'r3'] # slice (inclusive)
.iloc — Position-based
df.iloc[0] # first row
df.iloc[0:3, 0:2] # rows 0-2, columns 0-1
df.iloc[-1] # last row
df.iloc[:, -1] # last column
.query() — String expressions
df.query('price > 100')
df.query('category == "Food" and price < 10')
df.query('price > @threshold') # Python variable
Key Rules
- & | ~ for combining (not and/or)
- Parentheses required around each condition
- .loc = labels, .iloc = positions
- .query() uses @ for Python variables