Filtering Data in Pandas¶
Filtering is the most common operation in data analysis. Every report starts with "give me the rows that match these criteria." Pandas has multiple ways to filter — boolean indexing, .query(), and .isin() — each with its own strengths.
Learning Objectives¶
- Filter rows using boolean conditions
- Combine multiple conditions with
&,|,~ - Use
.query()for readable filter expressions - Filter with
.isin(),.between(), and.strmethods - Handle NaN values in filters
Boolean Indexing — The Core Pattern¶
import pandas as pd
df = pd.read_csv("orders.csv", parse_dates=["order_date"])
# Single condition — returns a boolean Series
mask = df["status"] == "completed"
print(mask.head())
# 0 True
# 1 True
# 2 False
# 3 True
# Apply the mask to filter rows
completed = df[mask]
# Or in one step:
completed = df[df["status"] == "completed"]
Sample output:
| order_id | customer_id | total | status |
|---|---|---|---|
| O001 | C001 | 298.00 | completed |
| O002 | C003 | 389.00 | completed |
| O004 | C001 | 35.99 | completed |
Comparison Operators¶
# Numeric comparisons
high_value = df[df["total"] > 200]
affordable = df[df["total"] <= 100]
mid_range = df[df["total"].between(50, 200)] # inclusive
# String equality
from_india = df[df["country"] == "India"]
not_cancelled = df[df["status"] != "cancelled"]
# Date comparison
recent = df[df["order_date"] >= "2024-01-01"]
january = df[df["order_date"].between("2024-01-01", "2024-01-31")]
Combining Conditions¶
Use & (AND), | (OR), ~ (NOT). Always wrap each condition in parentheses.
# AND — both must be true
high_completed = df[(df["status"] == "completed") & (df["total"] > 200)]
# OR — at least one must be true
pending_or_cancelled = df[(df["status"] == "pending") | (df["status"] == "cancelled")]
# NOT — negate a condition
not_cancelled = df[~(df["status"] == "cancelled")]
# Complex condition
target = df[
(df["order_date"] >= "2024-01-01") &
(df["total"] > 100) &
(df["status"].isin(["completed", "pending"]))
]
Use &, |, ~ — not and, or, not
df[condition1 and condition2] raises a ValueError. NumPy/Pandas boolean arrays use & and |. Always parenthesise: (cond1) & (cond2).
.isin() — Filter by a List of Values¶
# Orders with specific statuses
active = df[df["status"].isin(["completed", "pending", "processing"])]
# Filter out certain values — combine with ~
not_done = df[~df["status"].isin(["completed", "refunded"])]
# Filter by a list of customer IDs
vip_ids = ["C001", "C003", "C012"]
vip_orders = df[df["customer_id"].isin(vip_ids)]
.query() — Readable Filter Syntax¶
.query() lets you write conditions as a string — closer to natural language. Great for complex multi-condition filters.
# Equivalent boolean index vs query
result = df[(df["status"] == "completed") & (df["total"] > 200)]
result = df.query("status == 'completed' and total > 200")
# Using external variables
min_total = 150
result = df.query("total > @min_total and status == 'completed'")
# Date filtering
result = df.query("order_date >= '2024-01-01' and total > 100")
For mid-level analysts
.query() is often more readable than chained boolean conditions, especially for 3+ conditions. It also handles column names with spaces if you wrap them in backticks: .query("order date>= '2024-01-01'").
String Filtering — .str Accessor¶
# Email domain
gmail_customers = df[df["email"].str.endswith("@gmail.com")]
# Product name contains a word
electronics = df[df["product"].str.contains("Pro", case=False)]
# Starts/ends with
bulk_orders = df[df["order_id"].str.startswith("B")]
# String length
long_names = df[df["product"].str.len() > 15]
# Case-insensitive exact match
completed = df[df["status"].str.lower() == "completed"]
# Remove whitespace before comparing
df["status"] = df["status"].str.strip()
Handling NaN in Filters¶
# Find rows with missing delivery date
undelivered = df[df["delivery_date"].isna()]
# Find rows where delivery date exists
delivered = df[df["delivery_date"].notna()]
# NaN in conditions — fill before filtering
df_filled = df.fillna({"discount_pct": 0})
big_discount = df_filled[df_filled["discount_pct"] > 15]
NaN comparisons always return False
df[df["delivery_date"] == None] returns zero rows. Always use .isna() or .notna() to check for missing values.
Practical Filtering Patterns¶
Pattern 1 — Top N rows after filtering¶
# Top 5 highest-value completed orders
top5 = (
df[df["status"] == "completed"]
.sort_values("total", ascending=False)
.head(5)
)
Pattern 2 — Filter then select columns¶
# VIP customer order summary
vip_summary = df[df["segment"] == "VIP"][["order_id", "customer_id", "total", "order_date"]]
Pattern 3 — Filter with date range¶
# Q1 2024 completed orders
q1 = df.query("order_date >= '2024-01-01' and order_date <= '2024-03-31' and status == 'completed'")
Pattern 4 — Combine filters for an anomaly report¶
# Undelivered orders older than 14 days that are not cancelled
import pandas as pd
today = pd.Timestamp("2024-02-01")
overdue = df[
df["delivery_date"].isna() &
(df["status"] != "cancelled") &
(today - df["order_date"]).dt.days > 14
]
Practice Exercises¶
Warm-up
1. Filter df to only completed orders.
2. Find all orders with total > 100.
3. Find all orders from customers in ["C001", "C003"] using .isin().
Main
4. Find all orders that are pending OR cancelled from January 2024.
5. Use .query() to find completed orders with total between £100 and £400.
6. Find all products whose name contains "Pro" (case-insensitive).
7. Find orders where delivery_date is missing AND status is not cancelled (overdue orders).
Stretch
8. Write a filter that finds "problem orders": completed status but delivery_date is NULL, placed more than 10 days ago.
9. Write a function smart_filter(df, **kwargs) that accepts keyword arguments like status="completed", min_total=100, max_total=500 and applies all non-None filters dynamically.
Interview Questions¶
[Beginner] How do you filter a Pandas DataFrame to show only rows where status == "completed"?
[Beginner] What is the difference between .isna() and == None for checking missing values?
[Mid-level] You need to filter rows where status is in ["completed", "pending"] AND total > 100. Write this using both boolean indexing and .query().
[Mid-level] Why must you wrap each condition in parentheses when using & in Pandas?
[Senior] A DataFrame has 50 million rows. Filtering with boolean indexing takes 8 seconds. How would you improve this?