Applying Functions and Vectorization
Per-row logic vs C-speed vector math
df.apply pipes row/column functions over a frame, while true vectorization (column algebra, np.select, pd.cut) runs in C — pick the fast path whenever the task is expressible element-wise.
What you will learn
- Use apply() and applymap() correctly
- Replace slow row-wise lambdas with vectorized expressions
- Use np.select / pd.cut for branching and binning
What is this?
df.apply(fn, axis=1) runs fn on each row, and .applymap runs an element function over a frame — both iterate in Python, so they're slow. Whenever the logic can be expressed as column arithmetic or a vectorized function (np.where, np.select, pd.cut, .map), that path executes in C across whole columns. Knowing which tool matches which job is a performance and reliability skill.
Why do you need it?
- row-wise apply on 1M rows can take minutes; the vectorized equivalent, milliseconds.
- Many 'apply' patterns are one-liner true vectorizations — an easy 1000× win.
- Branch-heavy logic belongs to np.select / np.where, offloading the loop to C.
- Senior-performers read apply as a code smell and reach for the vector version first.
Mental model
Having a manager give each employee hand-written instructions
Row-wise apply
Works, but each instruction costs time. Batching would be faster — as a supervisor's memo, vectorized ops broadcast one instruction to the whole column. apply is the hand-delivered memo; ufuncs are the group email.
How it works
- df.apply(fn, axis=1) passes each row to fn — slow, but fine for genuinely row-shaped logic.
- df['col'].apply(fn) maps a function element-wise — slower than df['col'].map(dict) for lookup maps.
- df['new'] = df['a'] * 2 + df['b'] runs pure C column algebra — the fast path.
- np.where(cond, a, b) and np.select([c1,c2], [r1,r2], default) vectorize branching.
- pd.cut(x, bins, labels=) creates categorical buckets in C.
Pick the fastest tool.
Examples
The three speeds
import pandas as pd, numpy as npdf = pd.DataFrame({"age": [23, 41, 67], "spend": [10, 500, 25]})# slow: applydf["status_apply"] = df.apply(lambda r: "loyal" if r.spend > 100 else "normal", axis=1)# fast: vectorized np.wheredf["status_vec"] = np.where(df["spend"] > 100, "loyal", "normal")# fast: pd.cut for 3 binsdf["age_band"] = pd.cut(df["age"], bins=[0, 35, 65, 99], labels=["young", "mid", "senior"])print(df[["status_apply", "status_vec", "age_band"]])np.where and np.where-based branches run in C; apply runs one Python function call per row. Both produce identical output, so the vector version wins whenever expressible.
In production
- A feature pipeline rewrote 40 row-wise applies into np.select/np.where — 30× faster nightly.
- An eligibility flow mapped SKU→category through .map(dict) over .apply with a lookup.
- A monitoring score bins latency with pd.cut before feeding a classifier.
Best practices
- Profile before optimizing; apply on small frames is irrelevant — it's big data where it bites.
- Express branching with np.select at any length (condition lists stay readable).
- Keep .map for categorical lookup tables — faster and skips lambda overhead.
- When apply is unavoidable, vectorize the per-row helper and call it once.
Common mistakes
- Handing apply a function already vectorized (like np.log) — np.log on the whole column is faster.
- apply(axis=1) dropping dtype performance when a column expr suffices.
- Using apply to mutate in place and relying on side effects.
Debugging tips
- Compare apply output vs the vectorized expression with df.equals/np.allclose on a sample.
- If apply is slow, the fix is usually rewriting, not parallelizing — check the per-element cost first.
- applymap vs column apply — use the narrowest scope you need.
Interview checkpoint
Quick quiz
Which is generally slowest for element-wise transforms on a big column?
np.where is a vectorized branching substitute for apply(lambda ...).