transform() & filter()
Put a group-level value (team total, team average, rank) back on every row with transform(), and keep or drop whole groups with filter().
agg() shrinks each group to one row. But many questions need the group value next to every player: "What share of his team's runs did Kohli score?", "Is Rohit above or below MI's average?". For that you need the team total repeated on each row — transform(). Other questions are about whole groups: "Show only teams that scored more than 1000 runs." — that is filter().
Definition
transform(func)applies a function to each group and returns a result with the same index and length as the original data, so each row receives the value computed for its own group.
filter(func)calls a function on each group's sub-DataFrame; the function must return True or False, and the result contains all rows of the groups that returned True (with their original index), dropping the other groups entirely.
- Same length as the original — 8 players in, 8 values out. That is why the result can be stored as a new column with
df["new"] = .... - Its own group's value — Kohli and Faf (both RCB) both receive RCB's total, 1471.
- Whole groups —
filternever keeps half a team: either all of MI's rows stay or none do.
agg vs transform vs filter
text
agg("sum") transform("sum") filter(sum > 1000)
team runs team runs runs team runs
RCB 741 --> CSK 441 1471 (RCB) RCB 741
MI 417 GT 890 1037 (MI) MI 417
CSK 161 MI 1037 441 (CSK) MI 15
GT 890 RCB 1471 890 (GT) RCB 730
... (1 row per group) ... (1 value per ROW) MI 605
(rows of passing groups)agg | transform | filter | |
|---|---|---|---|
| Output length | number of groups | number of rows (same as input) | rows of the kept groups |
| Output index | group keys | original row index | original row index |
| Function returns | one value per group | one value per group or one value per row | True / False per group |
| Typical use | summary table | new column, normalising, filling NaN per group | drop small / weak groups |
Syntax
python
df.groupby("team")["runs"].transform("sum") # string name
df.groupby("team")["runs"].transform(lambda s: s - s.mean())
df.groupby("team").filter(lambda g: g["runs"].sum() > 1000)| Parameter | Meaning | Default |
|---|---|---|
func (transform) | function name string ("sum", "mean", "count", "rank"…) or callable that receives each group's Series | required |
func (filter) | callable that receives each group's DataFrame and returns one bool | required |
dropna (filter) | True: drop rows of failing groups. False: keep them but fill with NaN | True |
Example — team total and share on every row
python
df = pd.DataFrame({
"player": ["Kohli", "Rohit", "Dhoni", "Gill", "Jadeja", "Bumrah", "Faf", "Surya"],
"team": ["RCB", "MI", "CSK", "GT", "CSK", "MI", "RCB", "MI"],
"runs": [741, 417, 161, 890, 280, 15, 730, 605],
})
df["team_total"] = df.groupby("team")["runs"].transform("sum")
df["share_%"] = (df["runs"] / df["team_total"] * 100).round(1)
print(df)text
player team runs team_total share_%
0 Kohli RCB 741 1471 50.4
1 Rohit MI 417 1037 40.2
2 Dhoni CSK 161 441 36.5
3 Gill GT 890 890 100.0
4 Jadeja CSK 280 441 63.5
5 Bumrah MI 15 1037 1.4
6 Faf RCB 730 1471 49.6
7 Surya MI 605 1037 58.3How it works: transform("sum") computes the 4 team totals, then broadcasts each total back to the rows of that team, keeping the original order (0–7). Because the result lines up with df's index, plain column arithmetic works.
Example — compare each player with his team average
python
d2 = df[["player", "team", "runs"]].copy()
d2["team_avg"] = d2.groupby("team")["runs"].transform("mean").round(1)
d2["vs_avg"] = (d2["runs"] - d2["team_avg"]).round(1)
print(d2)text
player team runs team_avg vs_avg
0 Kohli RCB 741 735.5 5.5
1 Rohit MI 417 345.7 71.3
2 Dhoni CSK 161 220.5 -59.5
3 Gill GT 890 890.0 0.0
4 Jadeja CSK 280 220.5 59.5
5 Bumrah MI 15 345.7 -330.7
6 Faf RCB 730 735.5 -5.5
7 Surya MI 605 345.7 259.3The same vs_avg column can be made in one step with a lambda: df.groupby("team")["runs"].transform(lambda s: s - s.mean()) — here the function returns a whole Series (one value per row), which transform also accepts.
Example — rank inside each team and fill missing values per group
python
d3 = df[["player", "team", "runs"]].copy()
d3["rank_in_team"] = d3.groupby("team")["runs"].rank(ascending=False).astype(int)
print(d3.sort_values(["team", "rank_in_team"]))text
player team runs rank_in_team
4 Jadeja CSK 280 1
2 Dhoni CSK 161 2
3 Gill GT 890 1
7 Surya MI 605 1
1 Rohit MI 417 2
5 Bumrah MI 15 3
0 Kohli RCB 741 1
6 Faf RCB 730 2A very common cleaning trick (video 12 data cleaning) is to fill a missing score with its own team's mean rather than the overall mean:
python
import numpy as np
m = pd.DataFrame({"team": ["MI", "MI", "MI", "CSK", "CSK"], "runs": [40, np.nan, 60, 90, np.nan]})
m["runs_filled"] = m["runs"].fillna(m.groupby("team")["runs"].transform("mean"))
print(m)text
team runs runs_filled
0 MI 40.0 40.0
1 MI NaN 50.0
2 MI 60.0 60.0
3 CSK 90.0 90.0
4 CSK NaN 90.0Running totals per group work the same way: df.groupby("player")["runs"].cumsum() gives each player's career runs match by match.
Example — filter(): keep whole groups
python
d = df[["player", "team", "runs"]]
print(d.groupby("team").filter(lambda g: g["runs"].sum() > 1000))text
player team runs
0 Kohli RCB 741
1 Rohit MI 417
5 Bumrah MI 15
6 Faf RCB 730
7 Surya MI 605How it works: the lambda receives each team's sub-DataFrame g. RCB (1471) and MI (1037) return True, so all their rows are kept — even Bumrah's 15. CSK (441) and GT (890) return False and disappear. The original index is preserved. Another typical condition is group size: filter(lambda g: len(g) >= 2) drops teams with only one player.
Compare with a normal row filter d[d["runs"] > 600], which tests each row on its own and keeps Gill but drops Bumrah. The same group-level filter can also be written with a transformed mask: d[d.groupby("team")["runs"].transform("sum") > 1000] — identical result.
Common mistakes
- ❌
df["team_total"] = df.groupby("team")["runs"].sum()→ mostlyNaN(index is team names, not 0–7). ✅ Use.transform("sum"). - ❌ A
filterfunction that returns a Series of booleans. ✅ It must return a singleTrue/Falseper group — useg["runs"].sum() > 1000, notg["runs"] > 100. - ❌ Expecting
filterto pick rows inside a group. ✅ It keeps or drops whole groups; for rows use boolean indexing. - ❌ Filling missing values with the overall mean when groups differ a lot. ✅
fillna(groupby(...).transform("mean"))fills per group.
Interview tip
Q: What is the difference between agg and transform on a GroupBy?
A: agg returns one row per group (a reduced summary indexed by the keys). transform returns an object the same length as the original, with each row holding its group's value, so it can be assigned directly as a new column.
Key points to remember
- transform() returns a result with the same index and length as the original, so it can be assigned as a new column.
- Each row gets the value computed for its own group, e.g. df.groupby('team')['runs'].transform('sum').
- Typical transform uses: share of team total, difference from group mean, rank within group, filling NaN with the group mean.
- filter() calls a function on each group's DataFrame; it must return one True/False and keeps or drops the whole group.
- filter() keeps the original row index; it is different from row-level boolean indexing.
- Assigning a groupby().sum() result as a column misaligns (NaN) because its index is the group keys — use transform instead.
Try it yourself
transform: team total, share and difference from average
Fill NaN per group and filter whole groups
Practice: each bowler's share of team wickets
Revision Flashcards
Tap a card to flip it and see the answer.
