Pandas Performance
Make big frames behave
Millions of rows only feel fast when you think vectorized. dtype choices, avoidable loops, and copy churn decide whether a job runs in seconds or stalls the notebook.
What you will learn
- Replace row-wise Python loops with vectorized ops
- Choose dtypes to cut memory drastically
- Profile and chunk heavy workflows
What is this?
Performance in pandas comes from using C-optimized vectorized operations over whole columns instead of Python-level loops. dtype hygiene (category, int8/16, float32) shrinks memory. apply() with a Python lambda is often a loop in disguise; merging, groupby and joins are designed to be fast. When frames exceed RAM, chunked reads and processing win.
Why do you need it?
- A 10M-row 'if plan matches, add flag' loop runs minutes; a vectorized np.where runs milliseconds.
- Correct dtypes cut memory 4-10x — the difference between OOM and a quiet job.
- apply over a groupby can be catastrophically slow; agg/transform stay C-fast.
- Dashboards and pipelines only stay live if the query cost is tiny.
Mental model
A warehouse crew scanning a whole pallet at once
Vectorized pandas
Scanning 1,000 boxes one at a time is telling each worker to walk each box; scanning rows means reading the pallet's barcode once. dtypes are choosing sturdy light pallets; profiling is counting which stations actually cost time before you upgrade anything.
How it works
- Vectorized ops (>, +, str methods, np.where, .clip) run on whole arrays in C.
- Avoid Python loops over rows; replace with numpy functions or vectorized pandas methods.
- Memory mining: category for low-cardinality strings, int8/int16/float32 for bounded numerics.
- apply is convenient but Python-speed; .map()/.vectorized lambdas or np routines beat it.
- Chunking: read_csv(chunksize=...) processes in blocks; concatenate only the needed aggregates.
- Measure first: df.info(memory_usage='deep') and ._repr_sql() / time-based profiling target the fix.
The speed pyramid.
Examples
Loop vs vectorized, same flag
import numpy as np, pandas as pddf = pd.DataFrame({"plan": ["pro", "basic", "pro", "basic"] * 250000}) def loop(df): out = [] for p in df.plan: out.append("key" if p == "pro" else "std") df2 = df.copy(); df2["seg"] = out; return df2 df["seg"] = np.where(df.plan == "pro", "key", "std")print(df.groupby("seg").size())# loop(df) # same answer, orders of magnitude slowernp.where assigns a whole column in one C pass; the equivalent Python loop iterates each row through the interpreter.
In production
- A 20M-row event frame converts plan/user_type to category dtype and drops memory 6x before grouping.
- A nightly feature job chunks 50GB CSVs and aggregates per chunk, never materializing the full frame.
- A dashboard query replaces an apply() with np.select and drops latency from 40s to under a second.
Best practices
- Vectorize first, optimize later — profile before micro-tuning.
- Use pd.cut/np.select for multi-way flags instead of stacked conditions.
- Mind the penalty of object dtype strings; convert stable columns to category.
- Keep the column set lean: drop unneeded columns pre-groupby to cut memory.
Common mistakes
- Writing a Python loop to build a flag that np.where does in one line.
- Leaving every string an object dtype and watching memory explode.
- Using apply where .map with a dict or numpy works.
- Optimizing a 10MB frame when a 10GB sibling is the real problem.
Debugging tips
- df.info(memory_usage='deep') shows where the giant allocation hides.
- Timing with %timeit on one op beats guessing which step is slow.
- If a chunked run differs from full-frame results, check chunk boundaries for state leakage.
- A category dtype misbehaving in math is storage, not content — convert back for arithmetic.
Interview checkpoint
Quick quiz
Which is typically fastest for a conditional flag?
Storing low-cardinality strings as 'category' reduces memory.