Python Central HubSign in

Data Analytics with Python / Phase 3: Data Manipulation with Pandas

Advanced GroupBy (transform, filter, named agg)

Go beyond basic groupby - broadcast group stats back with transform, fill missing values with group means, drop whole groups with filter, and run quantile bucket analysis.

Beyond sum and mean

Grouping and Aggregations covered the basics: split rows into groups, then collapse each group down to one number with .sum() or .agg(). This page covers three things agg can't do on its own:

  • transform — compute a per-group statistic, but keep the original number of rows (broadcast the group's answer back onto every row in that group).
  • filter — keep or drop entire groups based on a condition, instead of individual rows.
  • Fine control over agg: naming your output columns, and bucketing continuous values with qcut before grouping.

transform: broadcast a group stat back onto every row

groupby(...).mean() collapses rows down to one row per group. transform("mean") computes the same group means, but hands back a Series the same length as your original data — perfect for adding a "how does this row compare to its group?" column.

Python
import pandas as pd
 
sales = pd.DataFrame({
    "city": ["pune", "pune", "delhi", "delhi", "delhi"],
    "amount": [100, 200, 150, 120, 300],
})
 
sales["city_avg"] = sales.groupby("city")["amount"].transform("mean")
print(sales)

Every pune row now shows 150.0 (the average of 100 and 200); every delhi row shows 190.0. This is the "unwrapped" groupby McKinney describes — you can now do plain arithmetic like sales["amount"] - sales["city_avg"] without a manual merge.

Filling missing values with the group mean

A common real use of transform: instead of filling every missing value with one global average, fill it with that row's own group's average.

Python
import numpy as np
 
data = pd.Series(
    [10.0, np.nan, 30.0, np.nan, 100.0, 200.0],
    index=["a1", "a2", "a3", "b1", "b2", "b3"],
)
group_key = ["a", "a", "a", "b", "b", "b"]
 
filled = data.groupby(group_key).transform(lambda x: x.fillna(x.mean()))
print(filled)

a2 was missing, so it gets group "a"'s mean of its other values ((10 + 30) / 2 = 20.0) — not the mean of the whole Series.

Group-weighted average with apply

Some calculations combine two columns within a group — like a weighted average, where each row contributes according to its own weight. That needs apply, since it operates on the whole group at once rather than one column transformed independently:

Python
df = pd.DataFrame({
    "category": ["a", "a", "a", "b", "b"],
    "score": [80, 90, 70, 60, 95],
    "weight": [1, 2, 1, 3, 1],
})
 
def weighted_avg(group):
    return np.average(group["score"], weights=group["weight"])
 
result = df.groupby("category").apply(weighted_avg)
print(result)

filter: keep or drop whole groups

transform and apply change or summarize values within a group. filter decides whether an entire group stays in the result at all — pass a function that returns True/False for a group, and only groups where it's True survive:

Python
orders = pd.DataFrame({
    "key": ["a", "a", "b", "b", "b"],
    "value": [1, 2, 3, 4, 5],
})
 
kept = orders.groupby("key").filter(lambda g: g["value"].sum() > 8)
print(kept)
print(len(kept))

Group "a" sums to 3 (dropped); group "b" sums to 12 (kept) — so only "b"'s rows remain, and the DataFrame keeps its original columns and row labels, unlike agg which would collapse each group to one row.

Named aggregation: control your output column names

Passing plain function names to agg on multiple columns gives you generic labels like "amount" repeated under a hierarchy. Named aggregation — passing new_col=("source_col", "func") keyword arguments — lets you choose the output column names directly:

Python
orders = pd.DataFrame({
    "city": ["pune", "pune", "delhi", "delhi"],
    "amount": [100, 200, 150, 300],
})
 
summary = orders.groupby("city").agg(
    total=("amount", "sum"),
    orders=("amount", "count"),
)
print(summary)

Bucket analysis: qcut + groupby

pandas.qcut slices a continuous column into roughly equal-sized buckets by quantile. Feeding that result straight into groupby gives you quick "which bucket performs best?" answers:

Python
scores = pd.DataFrame({
    "student": list("ABCDEFGH"),
    "score": [55, 60, 65, 70, 75, 80, 85, 90],
})
 
scores["bucket"] = pd.qcut(scores["score"], 4, labels=["Q1", "Q2", "Q3", "Q4"])
print(scores.groupby("bucket")["score"].mean())

Common pitfalls

  • transform must return either a single scalar (broadcast) or a Series the exact same length as the input group — anything else raises an error.
  • filter's function must return one True/False per group, not per row — testing g["value"] > 8 (no .sum()) would compare row-by-row and error out.
  • Named aggregation only accepts new_name=("column", "func") tuples — you can't mix it with the older {"col": ["sum", "mean"]} dict style in the same call.

Visualize it

diagram transform vs filter vs agg mermaid
agg collapses each group to one row; transform keeps every row but replaces values with a group statistic; filter keeps or drops whole groups unchanged.
sketch transform broadcasts one number to every row in a group p5.js
Each row keeps its own value, but a new column shows the same group average repeated for every row in that group.

🧪 Try It Yourself

Exercise 1 – Broadcast a Group Mean With transform

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Fill NaN With the Group Mean

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Drop Whole Groups With filter

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Check your understanding

quizCheck yourself
  1. How does transform("mean") differ from mean() after a groupby?

    Show answer

    A — transform returns a Series the same length as the original data, with each row holding its group's mean — mean() collapses to one row per group. transform broadcasts the group answer back onto every original row, so you can add a column like city_avg.

  2. In the sales example, pune amounts are 100 and 200. What value does city_avg show on each pune row?

    Show answer

    B — 150.0 — The pune mean is (100 + 200) / 2 = 150.0, repeated on every pune row.

  3. For orders with key a (values 1, 2) and key b (values 3, 4, 5), what does groupby("key").filter(lambda g: g["value"].sum() > 8) keep?

    Show answer

    B — Only group b, 3 rows — Group a sums to 3 and is dropped, while group b sums to 12 and is kept with all of its original rows and labels.

  4. Which call produces output columns named total and orders directly?

    Show answer

    A — agg(total=("amount", "sum"), orders=("amount", "count")) — Named aggregation uses new_name=("column", "func") keyword arguments to choose output names.

Next

With grouping mastered in depth, continue to Cross-Tabulation and Pivot Table Depth to summarize two or more categorical columns at once.

Was this page helpful?

Let us know how we did