pandas .map vs .apply vs .applymap — Pick the Right One
Video: pandas .map vs .apply vs .applymap — Pick the Right One by CelesteAI
Three methods. Three different jobs. And one of them was removed in pandas 3.0 — which is why your copy-pasted Stack Overflow snippet just threw AttributeError.
This post walks through .map, .apply, and .applymap on a tiny prices DataFrame and shows exactly which method belongs where. It’s the question Python data folks Google before they have a good mental model of pandas, and the answer is short enough to memorize.
The setup
Three tickers, three days, three columns. Small enough to fit in your head.
import pandas as pd
df = pd.DataFrame(
{
"AAPL": [185.50, 186.20, 184.10],
"MSFT": [415.30, 418.70, 414.10],
"GOOG": [142.40, 143.10, 141.80],
},
index=pd.to_datetime(["2025-01-15", "2025-01-16", "2025-01-17"]),
)
1. Series.map — Series only, dict is the killer feature
.map is the one you use most. It’s Series-only. It takes a dict, a Series, or a function — and that dict overload is what makes it special.
tickers = pd.Series(["AAPL", "MSFT", "GOOG"])
sectors = tickers.map({"AAPL": "Tech", "MSFT": "Tech", "GOOG": "Comm"})
# Tech, Tech, Comm
If you’re translating ticker → sector, country code → country name, status code → label, this is the one-liner. No merge, no loop, no for statement. Just a dict and .map.
2. Series.apply — Series too, but only takes a function
lower = tickers.apply(lambda s: s.lower())
# aapl, msft, goog
On a Series, .apply is essentially a slower .map — but it only takes a function (no dict overload). Reach for it when the transformation needs logic that doesn’t fit a dict.
3. DataFrame.apply axis=0 — function gets each column as a Series
This is where .apply shows its real power. On a DataFrame, the axis argument changes everything. The default, axis=0, hands your function each column as a Series.
col_std = df.apply(lambda s: s.std())
# AAPL 1.07
# MSFT 2.39
# GOOG 0.65
Per-column reductions — standard deviation, mean, custom z-scores, anything where you want one number out per column — go here.
4. DataFrame.apply axis=1 — function gets each row as a Series
Flip the axis and your function receives each row as a Series.
row_max = df.apply(lambda r: r.max(), axis=1)
# 2025-01-15 415.3
# 2025-01-16 418.7
# 2025-01-17 414.1
This is the row-wise tool. But it’s also a Python loop under the hood — axis=1 is slow on large DataFrames for the same reason iterrows is slow. When you can replace it with column arithmetic (df["A"] + df["B"]), do that.
5. DataFrame.map — the modern element-wise
formatted = df.map(lambda x: f"${x:,.2f}")
# AAPL MSFT GOOG
# 2025-01-15 $185.50 $415.30 $142.40
# 2025-01-16 $186.20 $418.70 $143.10
# 2025-01-17 $184.10 $414.10 $141.80
Format every cell. Cast every cell. Sanitize every string. Whatever you want to do to every cell of the DataFrame independently, DataFrame.map does it.
This method was added in pandas 2.1 as the replacement for applymap. Same signature, same behavior, new name.
6. applymap — gone in pandas 3.0
If your code does this:
df.applymap(lambda x: round(x, 1))
…on pandas 3.0 you get:
AttributeError: 'DataFrame' object has no attribute 'applymap'
applymap was deprecated in pandas 2.1 and removed in pandas 3.0. The fix is a one-line rename:
df.map(lambda x: round(x, 1))
That’s it. Identical signature, identical behavior.
The decision tree
| Input | Goal | Method |
|---|---|---|
| Series | Dict lookup | .map(dict) |
| Series | Function | .map(fn) or .apply(fn) |
| DataFrame | Element-wise | .map(fn) |
| DataFrame | Per-column | .apply(fn) (axis=0) |
| DataFrame | Per-row | .apply(fn, axis=1) |
| DataFrame | Anything vectorizable | column arithmetic, not .apply |
The last row is the most important. .apply is still a Python loop in disguise — axis=1 especially. When you can rewrite a per-row computation as column arithmetic, the vectorized form runs hundreds of times faster.
Want the deeper dive?
Pandas for Finance Ep 6 builds per-column returns, wealth curves, Sharpe ratios, and drawdowns entirely with vectorized arithmetic — the case where you skip .apply entirely and let pandas push everything into NumPy.