Pandas: 分组聚合
最后更新:2026-08-26
分组聚合是数据分析的"心脏"——按地区看销售额、按类别算均价、按月份统计订单量,本质都是"分组 → 计算 → 合并"。Pandas 的 groupby 实现了经典的 split-apply-combine 范式,本节用 Mermaid 图解让你彻底理解这个过程,并掌握 agg / transform / filter 三大操作。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。
1. 你将学到
- ❶ groupby 原理(split-apply-combine)
- ❷ 聚合函数(sum/mean/count/max/min)
- ❸ agg 多聚合与自定义
- ❹ transform 组内变换
- ❺ filter 组级筛选
2. Alice 按类别统计咖啡店销售
(1) 痛点:手动循环分组太繁琐
Alice 想按饮品类别统计销售额,手动写循环:
PYTHON
import pandas as pd
df = pd.DataFrame({
'drink': ['Latte', 'Americano', 'Mocha', 'Latte', 'Espresso',
'Americano', 'Latte', 'Mocha', 'Espresso', 'Latte'],
'category': ['Hot', 'Hot', 'Hot', 'Hot', 'Hot',
'Iced', 'Iced', 'Iced', 'Iced', 'Iced'],
'sales': [320, 280, 350, 310, 150, 200, 180, 160, 90, 120],
'quantity': [64, 56, 50, 62, 30, 40, 36, 22, 18, 24]
})
# Manual loop — tedious and error-prone
for cat in df['category'].unique():
subset = df[df['category'] == cat]
print(f"{cat}: sales={subset['sales'].sum()}, qty={subset['quantity'].sum()}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 解法:groupby 一行搞定
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:groupby 基础(难度⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'drink': ['Latte', 'Americano', 'Mocha', 'Latte', 'Espresso',
'Americano', 'Latte', 'Mocha', 'Espresso', 'Latte'],
'category': ['Hot', 'Hot', 'Hot', 'Hot', 'Hot',
'Iced', 'Iced', 'Iced', 'Iced', 'Iced'],
'sales': [320, 280, 350, 310, 150, 200, 180, 160, 90, 120],
'quantity': [64, 56, 50, 62, 30, 40, 36, 22, 18, 24]
})
# One line replaces the entire loop!
result = df.groupby('category')['sales'].sum()
print(result)
# category
# Hot 1410
# Iced 750
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
3. split-apply-combine 原理
(1) Mermaid 图解分组流程
graph TB
subgraph Split["① Split — 按类别拆分"]
S1["Hot: Latte 320<br>Americano 280<br>Mocha 350<br>Latte 310<br>Espresso 150"]
S2["Iced: Americano 200<br>Latte 180<br>Mocha 160<br>Espresso 90<br>Latte 120"]
end
subgraph Apply["② Apply — 每组求和"]
A1["Hot → 320+280+350<br>+310+150 = 1410"]
A2["Iced → 200+180+160<br>+90+120 = 750"]
end
subgraph Combine["③ Combine — 合并结果"]
C1["category | sales<br>Hot | 1410<br>Iced | 750"]
end
Split --> Apply --> Combine
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) GroupBy 对象是惰性的
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:GroupBy 惰性求值(难度⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Hot', 'Hot', 'Iced', 'Iced'],
'sales': [320, 280, 200, 180]
})
# groupby returns a GroupBy object — no computation yet
grouped = df.groupby('category')
print(type(grouped)) # <class 'pandas.core.groupby.DataFrameGroupBy'>
# Computation happens when you call an aggregation
print(grouped['sales'].sum())
# Inspect groups
print(grouped.groups) # {'Hot': [0, 1], 'Iced': [2, 3]}
print(grouped.ngroups) # 2
# Iterate over groups (rarely needed)
for name, group in grouped:
print(f"Group: {name}")
print(group)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
4. 常用聚合函数
(1) 内置聚合
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:内置聚合函数(难度⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Home', 'Home'],
'product': ['Laptop', 'Phone', 'Shirt', 'Pants', 'Lamp', 'Chair'],
'price': [999, 699, 45, 65, 30, 120],
'stock': [50, 120, 200, 180, 300, 80],
'rating': [4.7, 4.5, 4.2, 4.0, 3.8, 4.3]
})
# Single aggregation per column
print(df.groupby('category')['price'].mean())
# Electronics 849.0
# Clothing 55.0
# Home 75.0
# Multiple columns, same aggregation
print(df.groupby('category')[['price', 'stock']].sum())
# Common aggregation methods:
# .sum() .mean() .median() .min() .max()
# .count() .size() .std() .var() .first() .last()
# .nunique() .idxmax() .idxmin()
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) size vs count 区别
| 方法 | 计算内容 | NaN 处理 |
|---|---|---|
| size | 每组行数(含 NaN) | 包含 NaN |
| count | 每组非 NaN 值数 | 排除 NaN |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:size vs count(难度⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'category': ['A', 'A', 'B', 'B', 'A'],
'value': [1, np.nan, 3, 4, 5]
})
print(df.groupby('category').size())
# category
# A 3 ← 3 rows (including NaN row)
# B 2
print(df.groupby('category').count())
# value
# category
# A 2 ← 2 non-NaN values
# B 2
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
5. agg 多聚合
(1) 多聚合函数
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:agg 多聚合(难度⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Electronics', 'Electronics', 'Clothing', 'Clothing', 'Home', 'Home'],
'product': ['Laptop', 'Phone', 'Shirt', 'Pants', 'Lamp', 'Chair'],
'price': [999, 699, 45, 65, 30, 120],
'stock': [50, 120, 200, 180, 300, 80],
'rating': [4.7, 4.5, 4.2, 4.0, 3.8, 4.3]
})
# Multiple aggregations on one column
print(df.groupby('category')['price'].agg(['mean', 'min', 'max']))
# mean min max
# category
# Clothing 55.0 45 65
# Electronics 849.0 699 999
# Home 75.0 30 120
# Different aggregations per column
print(df.groupby('category').agg({
'price': ['mean', 'max'],
'stock': 'sum',
'rating': 'mean'
}))
# Named aggregations (cleaner column names)
result = df.groupby('category').agg(
avg_price=('price', 'mean'),
max_price=('price', 'max'),
total_stock=('stock', 'sum'),
avg_rating=('rating', 'mean')
)
print(result)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 自定义聚合函数
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:自定义聚合(难度⭐⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['A', 'A', 'A', 'B', 'B', 'B'],
'value': [10, 20, 30, 100, 200, 300]
})
# Custom function: range (max - min)
def value_range(series):
return series.max() - series.min()
result = df.groupby('category')['value'].agg(['mean', value_range])
print(result)
# mean value_range
# category
# A 20.0 20
# B 200.0 200
# Lambda in agg (less readable but concise)
result2 = df.groupby('category')['value'].agg([
('mean', 'mean'),
('range', lambda x: x.max() - x.min()),
('cv', lambda x: x.std() / x.mean()) # coefficient of variation
])
print(result2)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
6. transform 组内变换
(1) transform 保持原形状
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:transform 组内标准化(难度⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'student': ['Alice', 'Alice', 'Bob', 'Bob', 'Charlie', 'Charlie'],
'subject': ['Math', 'English', 'Math', 'English', 'Math', 'English'],
'score': [85, 90, 92, 88, 78, 82]
})
# Z-score normalization per student
df['z_score'] = df.groupby('student')['score'].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df)
# Fill missing with group mean (instead of global mean)
df2 = pd.DataFrame({
'category': ['A', 'A', 'A', 'B', 'B'],
'value': [10, 20, None, 100, None]
})
df2['value_filled'] = df2.groupby('category')['value'].transform(
lambda x: x.fillna(x.mean())
)
print(df2)
# Rank within group
df['rank'] = df.groupby('student')['score'].transform('rank', method='min')
print(df[['student', 'subject', 'score', 'rank']])
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) transform vs agg 对比
| 特性 | agg | transform |
|---|---|---|
| 返回形状 | 每组 1 行 | 与原 DataFrame 同长度 |
| 结果用途 | 汇总报告 | 回填原表 |
| 典型操作 | sum/mean/count | 标准化/填充/排名 |
| 广播 | ❌ | ✅ 自动广播到原行 |
7. filter 组级筛选
(1) 按组条件保留或丢弃
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:filter 筛选组(难度⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'category': ['Electronics', 'Electronics', 'Electronics',
'Books', 'Books',
'Clothing', 'Clothing', 'Clothing', 'Clothing'],
'product': ['Laptop', 'Phone', 'Tablet', 'Novel', 'Textbook',
'Shirt', 'Pants', 'Jacket', 'Socks'],
'sales': [5000, 3000, 2000, 200, 300, 800, 600, 400, 100]
})
# Keep only groups where total sales > 1000
big_categories = df.groupby('category').filter(lambda x: x['sales'].sum() > 1000)
print(big_categories)
# Electronics and Clothing kept; Books dropped (200+300=500 < 1000)
# Keep groups with at least 3 items
large_groups = df.groupby('category').filter(lambda x: len(x) >= 3)
print(large_groups['category'].unique()) # ['Electronics', 'Clothing']
# Keep groups where any product has sales > 4000
has_top_seller = df.groupby('category').filter(lambda x: x['sales'].max() > 4000)
print(has_top_seller['category'].unique()) # ['Electronics']
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
8. 多列分组与 as_index
(1) 多列分组
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:多列分组聚合(难度⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'region': ['North', 'North', 'North', 'South', 'South', 'South'],
'category': ['Electronics', 'Clothing', 'Electronics',
'Electronics', 'Clothing', 'Clothing'],
'product': ['Laptop', 'Shirt', 'Phone', 'Tablet', 'Pants', 'Jacket'],
'sales': [5000, 800, 3000, 2000, 600, 400]
})
# Group by multiple columns
result = df.groupby(['region', 'category'])['sales'].sum()
print(result)
# region category
# North Clothing 800
# Electronics 8000
# South Clothing 1000
# Electronics 2000
# as_index=False → keeps group columns as regular columns
result2 = df.groupby(['region', 'category'], as_index=False)['sales'].sum()
print(result2)
# region category sales
# 0 North Clothing 800
# 1 North Electronics 8000
# 2 South Clothing 1000
# 3 South Electronics 2000
# sort=False → preserve original order of first occurrence
result3 = df.groupby(['region', 'category'], sort=False)['sales'].sum()
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
9. 完整示例:电商多维度分组分析
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:多维度分组分析(难度⭐⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# ============================================
# Comprehensive example: E-commerce multi-dim
# groupby analysis with agg/transform/filter
# ============================================
# 1. Create sales data
np.random.seed(42)
df = pd.DataFrame({
'region': np.random.choice(['North', 'South', 'East', 'West'], 100),
'category': np.random.choice(['Electronics', 'Clothing', 'Home', 'Sports'], 100),
'product': [f'Item_{i:03d}' for i in range(100)],
'sales': np.round(np.random.uniform(50, 2000, 100), 2),
'quantity': np.random.randint(1, 50, 100),
'discount': np.random.choice([0, 0.1, 0.2, 0.3], 100)
})
# 2. agg: multi-metric summary per category
summary = df.groupby('category').agg(
total_sales=('sales', 'sum'),
avg_sales=('sales', 'mean'),
max_sales=('sales', 'max'),
order_count=('sales', 'count'),
avg_quantity=('quantity', 'mean')
).round(2)
print("=== Category Summary ===")
print(summary)
# 3. Multi-column groupby
region_cat = df.groupby(['region', 'category'], as_index=False).agg(
total_sales=('sales', 'sum'),
avg_discount=('discount', 'mean')
).round(3)
print("\n=== Region × Category ===")
print(region_cat.head(8))
# 4. transform: within-group ranking
df['sales_rank_in_region'] = df.groupby('region')['sales'].transform(
'rank', method='min', ascending=False
)
df['sales_pct_in_category'] = df.groupby('category')['sales'].transform(
lambda x: (x / x.sum() * 100).round(1)
)
print("\n=== Top 3 in North ===")
print(df[df['region'] == 'North'].nsmallest(3, 'sales_rank_in_region')
[['product', 'sales', 'sales_rank_in_region']])
# 5. filter: keep categories with > 20 orders
big_cats = df.groupby('category').filter(lambda x: len(x) > 20)
print(f"\n=== Big Categories ===")
print(big_cats['category'].value_counts())
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
❓ 常见问题
Q groupby 返回什么?
A groupby 返回 GroupBy 对象(惰性的),不立即计算。只有调用聚合函数(sum/mean/agg 等)时才执行 split-apply-combine。这种惰性设计让你可以链式组合多个操作,不用中间变量。
Q agg 和 apply 区别?
A agg 对每列应用聚合函数,返回每组一行。apply 对每组应用任意函数,返回结果更灵活(可返回 DataFrame)。简单聚合用 agg(更快更明确),复杂组内计算用 apply。agg 还支持命名聚合(name=(col, func)),列名更清晰。
Q transform 保持形状吗?
A 是的——transform 返回与原 DataFrame 相同长度的结果,每组内广播到原行。典型用途:组内标准化(z-score)、用组均值填充缺失值、组内排名。如果结果长度与组大小不一致会报错。
Q filter 是过滤行还是组?
A filter 过滤整个组——如果组的聚合结果不满足条件,该组所有行都被移除。不是过滤组内的行!与布尔索引不同:布尔索引按行条件过滤,filter 按组条件过滤。
Q as_index=False 有什么用?
A 默认 groupby 把分组列变成 Index(不是普通列)。as_index=False 让分组列保持为普通列,结果是一个干净的 DataFrame 而非带 MultiIndex 的 Series。需要后续操作分组列时(如 merge),as_index=False 更方便。
Q 多列分组结果怎么看?
A 多列分组产生 MultiIndex(层次索引)。用
result.loc[('North', 'Electronics')] 选特定组,或用 as_index=False 避免层次索引。reset_index() 也可以把层次索引展平为普通列。Q groupby + sort 会变慢吗?
A sort=True(默认)会对分组键排序,增加少量开销。大 DataFrame + 高基数分组键时,sort=False 可以加速。但排序后的结果更可读。建议:数据量小时用 sort=True(可读性优先),超大数据时 sort=False(性能优先)。
📖 小节
- groupby 的核心是 split-apply-combine:拆分→计算→合并
- GroupBy 对象惰性求值,调用聚合函数时才执行
- 内置聚合:sum/mean/count/size/min/max/std/nunique 等
- agg 支持多聚合和多列不同聚合,命名聚合语法最清晰
- transform 返回同长度结果,适合标准化/填充/排名
- filter 按组条件筛选,保留或丢弃整个组
- 多列分组产生 MultiIndex,as_index=False 避免层次索引
📝 作业
- 基础题(难度⭐):创建销售 DataFrame(region/category/sales),用 groupby + sum/mean/count 三种聚合,对比结果。
- 进阶题(难度⭐⭐):创建学生成绩表,用 agg 对每科计算 mean/max/min,用 transform 计算每个学生的 z-score 标准化成绩。
- 挑战题(难度⭐⭐⭐):模拟 50 行电商订单(region/category/sales/quantity/discount),完成:agg 多指标汇总 → 多列分组(region+category) → transform 组内排名 → filter 保留大组 → 综合报告。