apply() on a DataFrame
DataFrame.apply() with axis=0 (one call per column) vs axis=1 (one call per row), returning a Series to create several columns, and the result_type option.
Watch the lesson
1 video on CodeWithMunnaX
On a DataFrame, apply does not work value-by-value. It hands your function a whole column or a whole row at a time. That is exactly what you need when a decision depends on several columns — "a player is a Match Winner if he scored 100+ runs or took 8+ wickets". This is the main pattern from video "08b apply()" and notebook 12.
text
player runs balls wickets
0 Virat 120 70 0
1 Rohit 45 50 0
2 Dhoni 75 60 0
3 Bumrah 10 12 10
4 Jadeja 50 35 6Definition
DataFrame.apply(func, axis=0)callsfunconce for each column (axis=0, the default) or once for each row (axis=1). Each call receives that column or row as a Series. If every call returns a single value, the result is a Series; if every call returns a Series (or list-like withresult_type="expand"), the result is a DataFrame.
- axis=0 → per column — the function receives a column Series (index = row labels). Think "go down the rows". Typical use: a summary per column.
- axis=1 → per row — the function receives a row Series (index = column names), so you write
row["runs"],row["wickets"]. Typical use: a new value per row built from several columns. - Series in, value or Series out — what you return decides the shape of the result.
text
axis=0 (default) axis=1
func(column) for each column func(row) for each row
runs balls wkts runs balls wkts
↓ ↓ ↓ → 120 70 0 → func(row 0)
120 70 0 → 45 50 0 → func(row 1)
45 50 0 → 75 60 0 → func(row 2)
result: 1 value per column result: 1 value per rowSyntax
python
df.apply(func, axis=0, raw=False, result_type=None, args=(), **kwargs)| Parameter | Meaning | Default |
|---|---|---|
func | function that receives one column/row as a Series | required |
axis | 0 or "index" → call per column; 1 or "columns" → call per row | 0 |
raw | True passes a plain NumPy array instead of a Series (faster, but no row["name"] access) | False |
result_type | only for axis=1: "expand" (list results → columns), "reduce" (force a Series), "broadcast" (keep the original shape and column names) | None |
args | tuple of extra positional arguments for func | () |
**kwargs | extra keyword arguments for func | — |
Example — axis=0: one result per column
python
import pandas as pd
df = pd.DataFrame({
"player": ["Virat", "Rohit", "Dhoni", "Bumrah", "Jadeja"],
"runs": [120, 45, 75, 10, 50],
"balls": [70, 50, 60, 12, 35],
"wickets": [0, 0, 0, 10, 6],
})
nums = df[["runs", "balls", "wickets"]]
print(nums.apply(max))
print(nums.apply(lambda col: col.max() - col.min()))text
runs 120
balls 70
wickets 10
dtype: int64
runs 110
balls 58
wickets 10
dtype: int64How it works: with the default axis=0 the function ran 3 times — once for the runs column, once for balls, once for wickets. Each call returned one number, so the result is a Series indexed by column name. (We selected only numeric columns because max - min makes no sense for player names.)
If the function returns a whole column, you get a DataFrame of the same shape back — a quick way to scale every column:
python
print(nums.apply(lambda col: col / col.max()).round(2))text
runs balls wickets
0 1.00 1.00 0.0
1 0.38 0.71 0.0
2 0.62 0.86 0.0
3 0.08 0.17 1.0
4 0.42 0.50 0.6Example — axis=1: one result per row (the course pattern)
python
def impact(row):
if row["runs"] >= 100 or row["wickets"] >= 8:
return "Match Winner"
elif row["runs"] >= 50 or row["wickets"] >= 4:
return "Good"
else:
return "Average"
df["impact"] = df.apply(impact, axis=1)
print(df)text
player runs balls wickets impact
0 Virat 120 70 0 Match Winner
1 Rohit 45 50 0 Average
2 Dhoni 75 60 0 Good
3 Bumrah 10 12 10 Match Winner
4 Jadeja 50 35 6 GoodHow it works: with axis=1 pandas passes each row as a Series whose index is the column names, so row["runs"] and row["wickets"] work. The function ran 5 times (once per player) and the 5 labels became the new impact column. This is the right tool when the logic mixes several columns with if/elif/else.
Example — return a Series to create several columns at once
python
def stats(row):
return pd.Series({
"strike_rate": round(row["runs"] / row["balls"] * 100, 1),
"points": row["runs"] + row["wickets"] * 20,
})
print(df.apply(stats, axis=1))text
strike_rate points
0 171.4 120.0
1 90.0 45.0
2 125.0 75.0
3 83.3 210.0
4 142.9 170.0How it works: each row returned a 2-item Series, so pandas stacked them into a 2-column DataFrame. Attach it with df.join(df.apply(stats, axis=1)). Notice points came out as float (120.0): a returned Series holding a float and an int is upcast to float64. Use .astype({"points": int}) if that matters.
Example — result_type: list results
If the function returns a list, pandas by default keeps each list as a single object. result_type="expand" spreads it into columns, and "broadcast" repeats a single value across the original columns:
python
print(nums.apply(lambda row: [row["runs"], row["balls"]], axis=1))
print(nums.apply(lambda row: [row["runs"], row["balls"]], axis=1, result_type="expand"))
print(nums.apply(lambda row: row.max(), axis=1, result_type="broadcast"))text
0 [120, 70]
1 [45, 50]
2 [75, 60]
3 [10, 12]
4 [50, 35]
dtype: object
0 1
0 120 70
1 45 50
2 75 60
3 10 12
4 50 35
runs balls wickets
0 120 120 120
1 50 50 50
2 75 75 75
3 12 12 12
4 50 50 50result_type | Function returns a list → result | Function returns a value → result |
|---|---|---|
None (default) | Series of lists | Series |
"expand" | DataFrame, columns 0, 1, … | Series |
"reduce" | Series of lists (never expand) | Series |
"broadcast" | DataFrame with original shape & column names | value repeated across the row |
Example — extra arguments
python
def points(row, run_w, wkt_w):
return row["runs"] * run_w + row["wickets"] * wkt_w
print(df.apply(points, axis=1, args=(1, 25)))text
0 120
1 45
2 75
3 260
4 200
dtype: int64When NOT to use apply(axis=1)
axis=1 calls a Python function once per row and builds a Series object for each row — it is the slowest common pattern in pandas. For plain arithmetic, write the column expression directly:
python
df["points"] = df["runs"] + df["wickets"] * 20 # vectorised — fast
df["points"] = df.apply(lambda r: r["runs"] + r["wickets"] * 20, axis=1) # same result, slowFor if/else labels, np.where / np.select (see Conditional Columns) are vectorised alternatives. Keep apply(axis=1) for genuinely complex row logic on small-to-medium data.
Common mistakes
- ❌ Forgetting
axis=1and writingrow["runs"]— with the defaultaxis=0the function receives columns, socol["runs"]raisesKeyError: 'runs'. ✅df.apply(func, axis=1)for row logic. - ❌ Applying a numeric function to a DataFrame that still has text columns. ✅ Select numeric columns first:
df[["runs", "balls"]].apply(...)ordf.select_dtypes("number"). - ❌
raw=Trueand thenrow["runs"]. ✅ Withraw=Truethe row is a NumPy array — use positions, or keepraw=False. - ❌ Using
apply(axis=1)for simple column maths on 1 million rows. ✅ Use vectorised column expressions.
Comparison — axis=0 vs axis=1
axis=0 (default) | axis=1 | |
|---|---|---|
| Function receives | one column (Series indexed by row labels) | one row (Series indexed by column names) |
| Number of calls | number of columns | number of rows |
| Access inside function | col.max(), col.mean() | row["runs"], row["wickets"] |
| Typical result | summary per column / transformed columns | new column built from several columns |
| Speed | usually fast (few calls) | slow on big data (one call per row) |
Interview tip
Q: In df.apply(f, axis=1), what does f receive, and why is it slow?
A: Each row, as a Series whose index is the column names. It is slow because pandas creates a new Series object and calls a Python function for every single row — a Python-level loop — whereas vectorised column operations run in one compiled loop.
Key points to remember
- DataFrame.apply passes whole columns (axis=0, default) or whole rows (axis=1) to your function as Series.
- Use axis=1 when the logic needs several columns of the same row: row['runs'], row['wickets'].
- Returning one value per call gives a Series; returning a Series per call gives a DataFrame.
- result_type='expand' turns list results into columns; 'broadcast' keeps the original shape and column names.
- Extra arguments go through args=(...) and keyword arguments, exactly like Series.apply.
- apply(axis=1) is a Python loop in disguise — prefer vectorised column maths or np.where/np.select when possible.
- Select numeric columns before applying numeric functions to avoid errors on text columns.
Try it yourself
axis=0 vs axis=1 side by side
Row logic: impact label + several new columns
result_type: expand and broadcast
Practice: salary slip (from class)
Revision Flashcards
Tap a card to flip it and see the answer.
