groupby and aggregation

pandas · Practical · 18 min

Group-level summaries (per customer, per month, per product) are the backbone of analysis and reporting.

`groupby` follows the split-apply-combine pattern: split rows by key, apply a function to each group, combine the results. It's the pandas counterpart to SQL's GROUP BY.

df.groupby("region")["sales"].sum()

The result is indexed by the group keys. Group by several columns with a list; the result then has a MultiIndex, or pass as_index=False (or call reset_index()) to keep keys as ordinary columns.

For several statistics, use named aggregation, which produces clean, flat column names:

df.groupby("region").agg(
    total=("sales", "sum"),
    orders=("order_id", "count"),
    avg_price=("price", "mean"),
)

Know the difference between three kinds of operation:

  • aggregate (sum, mean, agg): one row per group
  • transform: a result the same length as the input, aligned with the original rows. df.groupby("region")["sales"].transform("mean") puts each row's group mean next to it, ready for sales / group_mean.
  • filter: keeps or drops whole groups, e.g. .filter(lambda g: len(g) >= 10)

Other details worth knowing: size() counts rows per group including missing values, while count() counts non-missing values per column. Rows whose key is NaN are dropped unless you pass dropna=False. For categorical keys, observed=True (the default from pandas 3.0) shows only categories that actually occur.

Built-in aggregation names ("sum", "mean", "max", "nunique", "first") run in optimised code. A custom Python function via agg(lambda ...) or apply runs once per group in Python and can be dramatically slower with many groups.

Example

import pandas as pd

df = pd.DataFrame({
    "region": ["N", "S", "N", "S", "N", "E"],
    "rep": ["ana", "bo", "ana", "di", "cy", "ed"],
    "sales": [100, 250, 50, 80, 120, 60],
})
print(df.groupby("region")["sales"].sum().to_dict())   # {'E': 60, 'N': 270, 'S': 330}

summary = df.groupby("region", as_index=False).agg(
    total=("sales", "sum"),
    reps=("rep", "nunique"),
    best=("sales", "max"),
)
print(summary)
#   region  total  reps  best
# 0      E     60     1    60
# 1      N    270     2   120
# 2      S    330     2   250

df["share"] = df["sales"] / df.groupby("region")["sales"].transform("sum")
print(df["share"].round(2).tolist())   # [0.37, 0.76, 0.19, 0.24, 0.44, 1.0]

big = df.groupby("region").filter(lambda g: len(g) >= 2)
print(sorted(big["region"].unique()))  # ['N', 'S']

Remember

  • groupby = split by key, apply, combine; keys become the index unless as_index=False
  • Named aggregation agg(new=('col', 'func')) gives tidy output
  • transform returns a row-aligned result; filter keeps whole groups
  • Built-in aggregation names are fast; Python lambdas per group are slow

Watch out for

  • Forgetting that NaN keys are dropped by default, so group totals don't add up to the overall total.

Check yourself

  1. You need each row's sales divided by its region's total. Which groupby operation fits?
  2. What's the difference between `g.size()` and `g.count()`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pandas.pydata.org/docs/user_guide/groupby.html