GroupBy: Split-Apply-Combine
How groupby() splits rows into groups, applies an aggregate to each group and combines the answers — by one or many columns, with as_index, get_group and loops.
Watch the lesson
1 video on CodeWithMunnaX
df["runs"].sum() gives one total for the whole league. But the real questions are per team: "How many runs did each team score?", "Which franchise has the best average?". You could filter each team one by one (df[df["team"] == "MI"]…) — but with 10 teams and 17 seasons that becomes hundreds of lines. groupby() does it in one line.
Definition
groupby()groups the rows of a DataFrame by the values of one or more key columns, so that an operation (usually an aggregate) can be run separately on each group. The process is called split-apply-combine.
- Key — the column whose values decide the groups (
team). Every distinct value becomes one group. - Split — rows with the same key value are put together (all MI rows, all CSK rows…).
- Apply — a function (
sum,mean, …) runs on each group independently. - Combine — the per-group answers are glued into a new Series/DataFrame whose index is the group keys.
Split-apply-combine diagram
text
SPLIT APPLY (sum runs) COMBINE
player team runs CSK: Dhoni 161 ┐
Kohli RCB 741 Jadeja 280 ┘ -> 441 team
Rohit MI 417 GT : Gill 890 -> 890 CSK 441
Dhoni CSK 161 --> MI : Rohit 417 ┐ --> GT 890
Gill GT 890 Bumrah 15 │ -> 1037 MI 1037
Jadeja CSK 280 Surya 605 ┘ RCB 1471
Bumrah MI 15 RCB: Kohli 741 ┐
Faf RCB 730 Faf 730 ┘ -> 1471
Surya MI 605Syntax
python
df.groupby(by, sort=True, as_index=True, dropna=True, observed=True)
df.groupby("team")["runs"].sum() # one column -> Series
df.groupby("team")[["runs", "wickets"]].sum() # many columns -> DataFrame
df.groupby(["season", "team"])["runs"].sum() # many keys -> MultiIndex| Parameter | Meaning | Default |
|---|---|---|
by | column name, list of column names (or a Series/function/dict) that defines the groups | required |
sort | sort the group keys in the result | True |
as_index | True: group keys become the result's index. False: keys stay as normal columns with a 0, 1, 2… index | True |
dropna | True: rows whose key is NaN are dropped. False: NaN becomes its own group | True |
observed | for categorical keys only: show only categories that actually appear | True (pandas 3) |
Example — the GroupBy object is lazy
python
g = df.groupby("team")
print(g)
print(g.ngroups)
print(g.groups)text
<pandas.api.typing.DataFrameGroupBy object at 0x00000145AF369FD0>
4
{'CSK': [2, 4], 'GT': [3], 'MI': [1, 5, 7], 'RCB': [0, 6]}How it works: groupby() alone computes nothing visible — it returns a DataFrameGroupBy object that remembers which row labels belong to which group (.groups). Only when you call an aggregate does the apply + combine happen. (The memory address will be different on your machine.)
Example — one key, one or more columns
python
print(df.groupby("team")["runs"].sum())
print(df.groupby("team")[["runs", "wickets"]].sum())text
team
CSK 441
GT 890
MI 1037
RCB 1471
Name: runs, dtype: int64
runs wickets
team
CSK 441 20
GT 890 0
MI 1037 20
RCB 1471 0How it works: single brackets ["runs"] select one column → the result is a Series. Double brackets [["runs", "wickets"]] select a list → the result is a DataFrame. Keys are sorted alphabetically (sort=True). Add .sort_values(ascending=False) to rank teams by total.
Note (pandas 3):
df.groupby("team").mean()raisesTypeError: dtype 'str' does not support operation 'mean'because theplayercolumn is text. Select number columns first, or use.mean(numeric_only=True).
Example — as_index=False keeps the key as a column
python
print(df.groupby("team", as_index=False)["runs"].sum())text
team runs
0 CSK 441
1 GT 890
2 MI 1037
3 RCB 1471This is identical to df.groupby("team")["runs"].sum().reset_index(). Use it when you want a flat table (e.g. to merge or export next).
Example — group by many columns
python
scores = pd.DataFrame({
"season": [2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024],
"team": ["MI", "CSK", "MI", "CSK", "MI", "CSK", "MI", "CSK"],
"player": ["Rohit", "Gaikwad", "Surya", "Dube", "Rohit", "Gaikwad", "Surya", "Dube"],
"runs": [332, 590, 605, 418, 417, 583, 345, 396],
})
r = scores.groupby(["season", "team"])["runs"].sum()
print(r)
print(r.loc[(2024, "MI")])text
season team
2023 CSK 1008
MI 937
2024 CSK 979
MI 762
Name: runs, dtype: int64
762How it works: with a list of keys, every unique combination (2023-CSK, 2023-MI, …) is one group. The result has a MultiIndex (two index levels). Read one value with a tuple: r.loc[(2024, "MI")]. r.unstack() turns the inner level into columns (a mini pivot table — see the Pivot & Reshape chapter).
Example — get_group() and looping over groups
python
print(df.groupby("team").get_group("MI"))
for team, group in df.groupby("team"):
print(team, "->", len(group), "players, total runs", group["runs"].sum())text
player team runs wickets matches
1 Rohit MI 417 0 14
5 Bumrah MI 15 20 13
7 Surya MI 605 0 16
CSK -> 2 players, total runs 441
GT -> 1 players, total runs 890
MI -> 3 players, total runs 1037
RCB -> 2 players, total runs 1471How it works: get_group(key) returns the sub-DataFrame of one group (for many keys pass a tuple: get_group((2024, "CSK"))). Iterating a GroupBy yields (key, sub_dataframe) pairs — useful for printing or custom reports, but for calculations prefer the built-in aggregates (they are much faster than a Python loop).
Other handy group methods
| Method | Returns |
|---|---|
size() | rows per group (including NaN) |
count() | non-missing values per column per group |
first() / last() | first / last row of each group (e.g. first match of every season — notebook 06) |
nth(n) | n-th row of each group |
idxmax() | row label of the max in each group → df.loc[df.groupby("team")["runs"].idxmax()] gives each team's top scorer |
Common mistakes
- ❌
print(df.groupby("team"))and expecting a table. ✅ Call an aggregate:df.groupby("team")["runs"].sum(). - ❌
df.groupby("team").mean()on a frame with text columns. ✅ Select number columns or passnumeric_only=True. - ❌ Forgetting the result's index is the key, then trying
result["team"]. ✅ Useas_index=Falseor.reset_index(). - ❌ Writing a
forloop withdf[df["team"] == t]for every team. ✅ Onegroupbycall does all teams at once. - ❌ Losing rows with a missing key without noticing. ✅
dropna=Falsekeeps aNaNgroup.
Interview tip
Q: Explain split-apply-combine.
A: groupby splits the rows into groups by the key's values, applies a function (aggregate, transform or filter) to each group independently, then combines the results into one object indexed by the group keys. It replaces manual loops over filtered subsets.
Key points to remember
- groupby() implements split-apply-combine: split rows by key values, apply a function per group, combine results.
- groupby() alone is lazy — it returns a DataFrameGroupBy object; call an aggregate to get a result.
- df.groupby('team')['runs'].sum() returns a Series; selecting a list of columns returns a DataFrame.
- Group keys become the index by default; as_index=False (or .reset_index()) keeps them as columns.
- Grouping by a list of columns makes one group per unique combination and a MultiIndex result.
- get_group(key) returns one group's rows; iterating a GroupBy yields (key, sub-DataFrame) pairs.
- Rows with a NaN key are dropped unless dropna=False; keys are sorted unless sort=False.
Try it yourself
Team totals with groupby
Group by season and team, get_group and loop
Practice: wickets per team
Revision Flashcards
Tap a card to flip it and see the answer.
