By the end of this lesson, you will be able to:
sum, mean, and countagg() with multiple, named, and custom aggregation functionstransform() to broadcast group-level results back to every original row (normalize, rank, fill)filter() to keep or drop entire groups based on a conditionpd.Grouper, build pivot tables, and speed grouping up with categorical dtypesā±ļø Estimated Time: 45ā60 minutes
šÆ Project: Build a sales analytics dashboard that summarizes revenue by product, region, month, and sales rep using aggregation, pivot tables, and growth metrics.
Transform data with split-apply-combine: the Swiss Army knife of data analysis
Imagine you're organizing a library. You could group books by genre, then count how many are in each category, find the average page count per genre, or identify the oldest book in each group. GroupBy operations work the same way with data!
The GroupBy mechanism is pandas' answer to the age-old question: "What's the average/sum/max/min of X for each Y?" It's like having a team of specialized accountants who can instantly reorganize and summarize your data in countless ways. This is where data analysis becomes truly powerful!
Watch how data gets grouped and aggregated in real-time:
Group by one or more columns and apply aggregate functions
# Simple groupby
df.groupby('category').mean()
# Group by multiple columns
df.groupby(['category', 'region']).sum()
# Select specific columns after grouping
df.groupby('category')['sales'].sum()
# Multiple aggregations
df.groupby('category').agg({
'sales': 'sum',
'quantity': 'mean',
'price': ['min', 'max']
})
The toolkit of operations you can apply to groups
# Using agg() with multiple functions
result = df.groupby('category').agg({
'sales': ['sum', 'mean', 'std'],
'quantity': ['min', 'max'],
'customer_id': 'nunique'
})
# Custom aggregation functions
def range_calc(x):
return x.max() - x.min()
df.groupby('category')['price'].agg([
'mean',
'std',
('range', range_calc),
('q25', lambda x: x.quantile(0.25)),
('q75', lambda x: x.quantile(0.75))
])
Apply operations that return same-sized results as the input
# Normalize values within each group
df['normalized'] = df.groupby('category')['value'].transform(
lambda x: (x - x.mean()) / x.std()
)
# Fill missing values with group mean
df['filled'] = df.groupby('category')['value'].transform(
lambda x: x.fillna(x.mean())
)
# Rank within groups
df['rank'] = df.groupby('category')['sales'].rank(
method='dense', ascending=False
)
# Calculate percentage of group total
df['pct_of_group'] = df.groupby('category')['sales'].transform(
lambda x: x / x.sum() * 100
)
Keep or remove entire groups based on conditions
# Keep groups with sum > threshold
filtered = df.groupby('category').filter(
lambda x: x['sales'].sum() > 1000
)
# Keep groups with at least n members
filtered = df.groupby('category').filter(
lambda x: len(x) >= 5
)
# Keep groups where mean is above overall mean
overall_mean = df['value'].mean()
filtered = df.groupby('category').filter(
lambda x: x['value'].mean() > overall_mean
)
# Complex filtering
filtered = df.groupby('region').filter(
lambda x: (x['sales'].sum() > 5000) &
(x['customers'].nunique() > 10)
)
Sophisticated techniques for complex analyses
# Named aggregations (pandas 0.25+)
result = df.groupby('category').agg(
total_sales=('sales', 'sum'),
avg_price=('price', 'mean'),
customer_count=('customer_id', 'nunique')
)
# Group by time periods
df['date'] = pd.to_datetime(df['date'])
monthly = df.groupby(pd.Grouper(
key='date', freq='M'
))['sales'].sum()
# Group by custom function
df.groupby(df['name'].str[0])['value'].mean() # By first letter
# Multi-index operations
grouped = df.groupby(['year', 'quarter', 'category'])
result = grouped['sales'].sum().unstack(level='category')
Make your GroupBy operations lightning fast
# Use categorical data for grouping columns
df['category'] = df['category'].astype('category')
# Avoid lambda when possible
# Slow:
df.groupby('category').apply(lambda x: x['value'].sum())
# Fast:
df.groupby('category')['value'].sum()
# Use numba for custom functions
from numba import jit
@jit
def custom_agg(values):
# Complex calculation
return result
df.groupby('category')['value'].agg(custom_agg)
# Parallel processing for large datasets
import dask.dataframe as dd
ddf = dd.from_pandas(df, npartitions=4)
result = ddf.groupby('category').mean().compute()
Let's build a comprehensive sales analysis using GroupBy operations:
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# Create sample sales data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=365, freq='D')
n_records = 1000
sales_data = pd.DataFrame({
'date': np.random.choice(dates, n_records),
'product': np.random.choice(['Laptop', 'Phone', 'Tablet', 'Watch', 'Headphones'], n_records),
'category': np.random.choice(['Electronics', 'Accessories'], n_records),
'region': np.random.choice(['North', 'South', 'East', 'West'], n_records),
'sales_rep': np.random.choice(['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'], n_records),
'quantity': np.random.randint(1, 10, n_records),
'unit_price': np.random.uniform(50, 1500, n_records).round(2),
})
# Calculate total revenue
sales_data['revenue'] = sales_data['quantity'] * sales_data['unit_price']
# Add month and quarter columns for time-based analysis
sales_data['month'] = sales_data['date'].dt.to_period('M')
sales_data['quarter'] = sales_data['date'].dt.to_period('Q')
print("š SALES ANALYTICS DASHBOARD")
print("=" * 60)
# 1. Overall Performance by Product
print("\n1. PRODUCT PERFORMANCE:")
product_stats = sales_data.groupby('product').agg({
'revenue': ['sum', 'mean', 'count'],
'quantity': 'sum'
}).round(2)
product_stats.columns = ['Total Revenue', 'Avg Revenue', 'Transactions', 'Units Sold']
print(product_stats.sort_values('Total Revenue', ascending=False))
# 2. Regional Analysis
print("\n2. REGIONAL BREAKDOWN:")
regional_summary = sales_data.groupby(['region', 'category']).agg({
'revenue': 'sum',
'quantity': 'sum'
}).round(2)
print(regional_summary.unstack(fill_value=0))
# 3. Time Series Analysis
print("\n3. MONTHLY TRENDS:")
monthly_sales = sales_data.groupby('month').agg({
'revenue': 'sum',
'quantity': 'sum',
'date': 'count'
}).rename(columns={'date': 'transactions'})
print(monthly_sales.tail()) # Show last 5 months
# 4. Sales Rep Performance
print("\n4. SALES REP LEADERBOARD:")
rep_performance = sales_data.groupby('sales_rep').agg({
'revenue': ['sum', 'mean'],
'quantity': 'sum',
'product': lambda x: x.value_counts().index[0] # Most sold product
}).round(2)
rep_performance.columns = ['Total Revenue', 'Avg Deal Size', 'Units Sold', 'Top Product']
print(rep_performance.sort_values('Total Revenue', ascending=False))
# 5. Product-Region Cross Analysis
print("\n5. PRODUCT SUCCESS BY REGION:")
pivot_table = pd.pivot_table(
sales_data,
values='revenue',
index='product',
columns='region',
aggfunc='sum',
fill_value=0
).round(2)
print(pivot_table)
# 6. Advanced Metrics
print("\n6. ADVANCED METRICS:")
# Customer concentration (using sales_rep as proxy)
concentration = sales_data.groupby('product').apply(
lambda x: x.groupby('sales_rep')['revenue'].sum().std() /
x.groupby('sales_rep')['revenue'].sum().mean()
).round(2)
print("Product Revenue Concentration (lower = more balanced):")
print(concentration.sort_values())
# Growth metrics (comparing quarters)
quarterly_growth = sales_data.groupby('quarter')['revenue'].sum()
growth_rate = quarterly_growth.pct_change() * 100
print(f"\nQuarterly Growth Rates:")
print(growth_rate.dropna().round(2))
# 7. Statistical Summary by Category
print("\n7. CATEGORY STATISTICS:")
category_stats = sales_data.groupby('category')['revenue'].describe()
print(category_stats.round(2))
# 8. Top Performers
print("\n8. TOP PERFORMERS:")
# Top 3 products by region
top_by_region = sales_data.groupby(['region', 'product'])['revenue'].sum().reset_index()
top_3_per_region = top_by_region.groupby('region').apply(
lambda x: x.nlargest(3, 'revenue')
).reset_index(drop=True)
print(top_3_per_region)
print("\nā
Analysis Complete!")
print(f"Total Revenue: ${sales_data['revenue'].sum():,.2f}")
print(f"Total Transactions: {len(sales_data):,}")
print(f"Average Transaction Value: ${sales_data['revenue'].mean():.2f}")
as_index=False or .reset_index() for flat DataFramesdescribe(): Quick statistical overview of groupspipe(): Chain custom functions elegantly| Operation | Code | Description |
|---|---|---|
| Basic Aggregation | df.groupby('col').sum() |
Sum values by group |
| Multiple Aggregations | df.groupby('col').agg(['sum', 'mean']) |
Apply multiple functions |
| Custom Aggregation | df.groupby('col').agg(lambda x: x.max() - x.min()) |
Apply custom function |
| Transform | df.groupby('col')['val'].transform('mean') |
Broadcast result to original shape |
| Filter | df.groupby('col').filter(lambda x: len(x) > 2) |
Keep/remove entire groups |
| Apply | df.groupby('col').apply(lambda x: x.nlargest(2, 'val')) |
Apply any function to groups |
| Size | df.groupby('col').size() |
Count group members |
| Nth Element | df.groupby('col').nth(0) |
Get nth row from each group |
Keep a learning journal ā digital or physical. After this lesson, take a few minutes to write down:
āļø This lesson's prompt: "What's the average/sum/max of X for each Y?" is a question you can now answer instantly. What is one real question in that shape ā from work, a hobby, or a dataset you care about ā that GroupBy would let you answer?
agg() summarizes each group to fewer rows; transform() returns a result the same shape as the input; filter() keeps or drops whole groups.total=('col','sum')) produce clean, readable output columns.You can now slice a dataset by any dimension and summarize it ā computing per-group totals, averages, rankings, and time-based trends ā which is the core of nearly every real analytics report and dashboard.
transform() instead of agg()?Use agg() when you want one summary row per group (e.g. total sales per region). Use transform() when you want to keep every original row but attach a group-level value ā like each row's percentage of its group's total, or a group-mean fill for missing values.
By default the group keys become the result's index. Pass as_index=False to the groupby call, or chain .reset_index(), to get a flat DataFrame with the keys back as ordinary columns.
Convert grouping columns to the category dtype, replace apply(lambda ...) with built-in aggregations wherever possible, and for very large data consider tools like Dask that parallelize the operation.
You can now summarize data within a single table. Next you'll bring multiple tables together with merges and joins ā and combining that with GroupBy is where serious analysis begins.
agg() with named aggregations.transform().GroupBy is the moment raw rows turn into real insight. Once split-apply-combine clicks, you'll see it everywhere ā and you'll answer questions in seconds that used to take hours in a spreadsheet. Keep grouping!