Aggregation Functions
Once a GroupBy object exists, an aggregation function collapses each group down to a single summary value (or a small set of values) per group. pandas ships many built-in aggregators — sum, mean, median, min, max, std, var, count, nunique, first, last — each optimized in Cythonized code for speed. Beyond calling these individually, the .agg() (alias .aggregate()) method is the Swiss-army knife of group summarization: it accepts a single function name, a list of function names to compute simultaneously, or a dictionary mapping specific columns to specific functions, giving you precise control over what gets computed and how results are labeled.
Cricket analogy: Once you group deliveries by bowler, an aggregator like sum or mean collapses each bowler's overs into one figure such as economy rate, and .agg() lets you request sum, mean and wicket count for Bumrah's spell all in one call.
Multiple Aggregations with agg()
Passing a list to .agg(), such as .agg(['sum', 'mean', 'count']), computes all three statistics for every selected column and returns a DataFrame with a column for each function. When different columns need different treatment — say, summing revenue but averaging discount_pct — a dictionary form like .agg({'revenue': 'sum', 'discount_pct': 'mean'}) applies each function only to its associated column. Named aggregation, using keyword arguments of the form new_col=('source_col', 'func'), goes a step further by letting you produce flatly-named output columns in one pass, avoiding the MultiIndex columns that list-based agg() produces.
Cricket analogy: Passing .agg(['sum','mean','count']) to a bowling group computes wickets, economy and matches together, while a dict like .agg({'runs':'sum','strike_rate':'mean'}) sums runs but averages strike rate per batter.
import pandas as pd
sales = pd.DataFrame({
'region': ['East', 'East', 'West', 'West', 'West'],
'revenue': [1200, 800, 950, 1600, 700],
'discount_pct': [0.1, 0.05, 0.15, 0.0, 0.2]
})
# multiple stats on one column
stats = sales.groupby('region')['revenue'].agg(['sum', 'mean', 'count'])
print(stats)
# different function per column via dict
mixed = sales.groupby('region').agg({'revenue': 'sum', 'discount_pct': 'mean'})
print(mixed)
# named aggregation avoids MultiIndex columns
clean = sales.groupby('region').agg(
total_revenue=('revenue', 'sum'),
avg_discount=('discount_pct', 'mean'),
n_orders=('revenue', 'count')
)
print(clean)
# total_revenue avg_discount n_orders
# region
# East 2000 0.075 2
# West 3250 0.117 3Custom Aggregation Functions
Any function that reduces a Series (or array) to a single scalar can be passed to .agg(), including lambdas and user-defined functions, e.g. .agg(lambda s: s.max() - s.min()) to compute a per-group range. Custom functions are convenient but slower than built-in string names because pandas cannot use its optimized Cython implementations for arbitrary Python callables — prefer built-in names (or NumPy ufuncs like np.ptp) when performance on large groups matters.
Cricket analogy: A custom lambda like .agg(lambda s: s.max() - s.min()) on a batter's innings computes the range between their highest and lowest score per series, but it runs slower than pandas' built-in max and min since Python executes it per group.
nunique() is a frequently underused aggregator: df.groupby('customer')['product'].nunique() answers 'how many distinct products did each customer buy?' — distinct from count(), which simply counts non-null rows including repeats.
Mixing string function names and callables in a single .agg() list, e.g. .agg(['sum', lambda x: x.max()]), produces an awkwardly named column like '<lambda>' — always rename or use named aggregation for clarity in production code.
- Built-in aggregators (sum, mean, count, nunique, etc.) are optimized and should be preferred over custom lambdas when possible.
- .agg() accepts a single function, a list of functions, or a dict mapping columns to functions.
- Named aggregation (new_col=('source', 'func')) produces flat, clearly labeled output columns.
- nunique() counts distinct values per group, while count() counts non-null occurrences (including duplicates).
- Custom callables passed to agg() run in pure Python and are slower than built-in string-named aggregators.
- Aggregation always reduces each group to a fixed-size summary, unlike transform which preserves the original shape.
Practice what you learned
1. What is the primary difference between an aggregation function and a transform function applied to a GroupBy object?
2. Which .agg() syntax lets you apply 'sum' to the revenue column and 'mean' to the discount_pct column in a single call?
3. What does named aggregation, e.g. total=('revenue','sum'), primarily solve?
4. What is the key difference between .count() and .nunique() when used after groupby on a column?
5. Why are custom lambda functions passed to .agg() generally slower than built-in named aggregators like 'sum'?
Was this page helpful?
You May Also Like
GroupBy Basics
Understand pandas' split-apply-combine model via groupby(), including grouping keys, selecting columns, and iterating over groups.
Transform and Filter on Groups
Learn how groupby's transform() and filter() differ from agg(): transform preserves row shape while filter selects whole groups by a condition.
Crosstabs and Pivot Analysis
Use pd.crosstab() to build frequency and cross-tabulation tables between categorical variables, and compare it to pivot_table for richer aggregation.
Pivot Tables
How pandas' pivot_table reshapes long-format data into a summarized cross-tabulation, aggregating values across row and column groupings similar to a spreadsheet pivot table.