分组与聚合
分组聚合是数据分析的核心操作,Pandas 的 groupby 实现了经典的"拆分-应用-合并"(Split-Apply-Combine)模式,能够高效地对数据进行分组统计。
分组聚合原理
"拆分-应用-合并"模式的工作流程:
- 拆分(Split):根据某些条件将数据分成若干组
- 应用(Apply):对每个组独立应用函数
- 合并(Combine):将结果合并成新的数据结构
原始数据 分组 应用函数 合并结果
┌─────┬─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ A │ 1 │ │ A │ 1 │ │ A │ mean │ │ A │ 2 │
├─────┼─────┤ ├─────┤ → │ A │ 3 │ → ├─────┤
│ B │ 2 │ │ A │ 3 │ ├─────┤ │ B │ 3 │
├─────┼─────┤ ├─────┤ │ B │ 2 │ └─────┘
│ A │ 3 │ │ B │ 2 │ │ B │ 4 │
├─────┼─────┤ ├─────┤ └─────┘
│ B │ 4 │ │ B │ 4 │
└─────┴─────┘ └─────┘
基本 GroupBy 操作
创建分组对象
import pandas as pd
import numpy as np
# 创建示例数据
df = pd.DataFrame({
'department': ['技术', '销售', '技术', '人事', '销售', '技术'],
'name': ['张三', '李四', '王五', '赵六', '钱七', '孙八'],
'salary': [15000, 12000, 18000, 10000, 14000, 16000],
'age': [28, 32, 25, 40, 35, 30]
})
# 创建 GroupBy 对象
grouped = df.groupby('department')
print(type(grouped)) # <class 'pandas.core.groupby.generic.DataFrameGroupBy'>
# 查看分组信息
print(grouped.groups) # {'人事': [3], '技术': [0, 2, 5], '销售': [1, 4]}
print(grouped.size()) # 每组大小
# department
# 人事 1
# 技术 3
# 销售 2
单列分组
# 按部门分组,计算薪资的平均值
result = df.groupby('department')['salary'].mean()
print(result)
# department
# 人事 10000.0
# 技术 16333.333333
# 销售 13000.0
# 也可以写成
result = df.groupby('department').salary.mean()
# 多列聚合
result = df.groupby('department')[['salary', 'age']].mean()
print(result)
# salary age
# department
# 人事 10000.0 40.000000
# 技术 16333.333333 27.666667
# 销售 13000.0 33.500000
多列分组
# 添加更多数据
df = pd.DataFrame({
'department': ['技术', '销售', '技术', '人事', '销售', '技术', '技术', '销售'],
'level': ['高级', '中级', '中级', '高级', '高级', '高级', '中级', '中级'],
'salary': [20000, 12000, 15000, 15000, 18000, 22000, 14000, 13000]
})
# 多列分组
result = df.groupby(['department', 'level'])['salary'].mean()
print(result)
# department level
# 人事 高级 15000.0
# 技术 中级 14500.0
# 高级 21000.0
# 销售 中级 12500.0
# 高级 18000.0
聚合函数
内置聚合函数
# 常用聚合函数
grouped = df.groupby('department')['salary']
print(grouped.sum()) # 求和
print(grouped.mean()) # 平均值
print(grouped.median()) # 中位数
print(grouped.std()) # 标准差
print(grouped.var()) # 方差
print(grouped.min()) # 最小值
print(grouped.max()) # 最大值
print(grouped.count()) # 计数(非空值)
print(grouped.size()) # 大小(包括空值)
print(grouped.first()) # 第一个值
print(grouped.last()) # 最后一个值
agg 方法
agg(或 aggregate)方法可以同时应用多个聚合函数:
# 单列多聚合
result = df.groupby('department')['salary'].agg(['mean', 'std', 'min', 'max'])
print(result)
# mean std min max
# department
# 人事 15000.0 NaN 15000 15000
# 技术 17750.0 3590.100525 14000 22000
# 销售 14333.333333 3082.207 12000 18000
# 重命名聚合列
result = df.groupby('department')['salary'].agg(
avg_salary='mean',
std_salary='std',
min_salary='min',
max_salary='max'
)
print(result)
# avg_salary std_salary min_salary max_salary
# department
# 人事 15000.0 NaN 15000 15000
# 技术 17750.0 3590.100525 14000 22000
# 销售 14333.333333 3082.207 12000 18000
# 多列不同聚合
result = df.groupby('department').agg({
'salary': ['mean', 'sum', 'count'],
'level': ['nunique', lambda x: x.mode().iloc[0]]
})
print(result)
# salary level
# mean sum count nunique <lambda_0>
# department
# 人事 15000.0 15000 1 1 高级
# 技术 17750.0 71000 4 2 高级
# 销售 14333.333333 43000 3 2 中级
自定义聚合函数
# 定义自定义函数
def salary_range(series):
"""计算薪资范围"""
return series.max() - series.min()
def top_n(series, n=2):
"""获取前 N 大的值"""
return series.nlargest(n).tolist()
# 使用自定义函数
result = df.groupby('department')['salary'].agg(['mean', salary_range])
print(result)
# mean salary_range
# department
# 人事 15000.0 0
# 技术 17750.0 8000
# 销售 14333.333333 6000
# 带参数的自定义函数
result = df.groupby('department')['salary'].agg(
lambda x: top_n(x, 3)
)
print(result)
# department
# 人事 [15000]
# 技术 [22000, 20000, 15000]
# 销售 [18000, 13000, 12000]
分组转换
transform 方法
transform 返回与原数据相同形状的结果,常用于组内标准化:
# 原始数据
df = pd.DataFrame({
'group': ['A', 'A', 'B', 'B', 'A', 'B'],
'value': [10, 20, 30, 40, 15, 35]
})
# 组内标准化(Z-score)
df['z_score'] = df.groupby('group')['value'].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df)
# group value z_score
# 0 A 10 -1.091089
# 1 A 20 0.872872
# 2 B 30 -0.872872
# 3 B 40 0.872872
# 4 A 15 0.218218
# 5 B 35 0.000000
# 添加组统计信息
df['group_mean'] = df.groupby('group')['value'].transform('mean')
df['group_max'] = df.groupby('group')['value'].transform('max')
print(df)
# group value group_mean group_max
# 0 A 10 15.000000 20
# 1 A 20 15.000000 20
# 2 B 30 35.000000 40
# 3 B 40 35.000000 40
# 4 A 15 15.000000 20
# 5 B 35 35.000000 40
# 填充组内缺失值
df.loc[1, 'value'] = np.nan
df['value_filled'] = df.groupby('group')['value'].transform(
lambda x: x.fillna(x.mean())
)
组内排名
# 组内排名
df['rank_in_group'] = df.groupby('group')['value'].transform('rank')
df['dense_rank'] = df.groupby('group')['value'].transform(
lambda x: x.rank(method='dense')
)
print(df[['group', 'value', 'rank_in_group', 'dense_rank']])
分组过滤
filter 方法
filter 根据组级别条件筛选数据:
# 示例数据
df = pd.DataFrame({
'group': ['A', 'A', 'B', 'B', 'B', 'C', 'C'],
'value': [10, 20, 30, 40, 50, 60, 70]
})
# 筛选组大小大于 2 的组
result = df.groupby('group').filter(lambda x: len(x) > 2)
print(result)
# group value
# 2 B 30
# 3 B 40
# 4 B 50
# 筛选组内平均值大于 40 的组
result = df.groupby('group').filter(lambda x: x['value'].mean() > 40)
print(result)
# group value
# 5 C 60
# 6 C 70
# 筛选组内总和大于 50 的组
result = df.groupby('group').filter(lambda x: x['value'].sum() > 50)
分组应用
apply 方法
apply 可以对每个组应用任意复杂的函数:
# 示例数据
df = pd.DataFrame({
'group': ['A', 'A', 'B', 'B', 'A'],
'value': [10, 20, 30, 40, 15]
})
# 自定义复杂操作
def normalize_and_top(group):
"""标准化并返回前两个值"""
normalized = (group['value'] - group['value'].mean()) / group['value'].std()
return normalized.nlargest(2)
result = df.groupby('group').apply(normalize_and_top)
print(result)
# group
# A 1 0.707107
# 4 0.000000
# B 3 0.707107
# 2 -0.707107
# 返回 DataFrame
def group_summary(group):
return pd.DataFrame({
'count': len(group),
'mean': group['value'].mean(),
'std': group['value'].std(),
'min': group['value'].min(),
'max': group['value'].max()
}, index=[group.name])
result = df.groupby('group').apply(group_summary)
print(result)
# count mean std min max
# group
# A 3 15.0 5.000000 10 20
# B 2 35.0 7.071068 30 40
高级分组
按函数分组
# 示例数据
df = pd.DataFrame({
'name': ['张三', '李四', '王五', '赵六', '钱七'],
'age': [25, 32, 45, 28, 55]
})
# 按年龄段分组
def age_group(age):
if age < 30:
return '青年'
elif age < 50:
return '中年'
else:
return '老年'
result = df.groupby(df['age'].apply(age_group))['name'].count()
print(result)
# age
# 中年 1
# 老年 1
# 青年 3
# 按索引分组
df.index = ['a', 'b', 'c', 'd', 'e']
result = df.groupby(lambda x: '元音' if x in 'ae' else '辅音')['name'].count()
按时间分组
# 时间序列数据
dates = pd.date_range('2024-01-01', periods=100, freq='D')
df = pd.DataFrame({
'date': dates,
'value': np.random.randn(100).cumsum()
})
# 按月分组
df['month'] = df['date'].dt.to_period('M')
monthly = df.groupby('month')['value'].sum()
# 使用 Grouper
monthly = df.groupby(pd.Grouper(key='date', freq='M'))['value'].sum()
# 按周分组
weekly = df.groupby(pd.Grouper(key='date', freq='W'))['value'].mean()
多级分组操作
# 多级分组
df = pd.DataFrame({
'year': [2023, 2023, 2023, 2024, 2024, 2024],
'quarter': ['Q1', 'Q2', 'Q3', 'Q1', 'Q2', 'Q3'],
'product': ['A', 'A', 'B', 'A', 'B', 'B'],
'sales': [100, 150, 200, 120, 180, 220]
})
# 多级分组聚合
result = df.groupby(['year', 'quarter'])['sales'].sum()
print(result)
# year quarter
# 2023 Q1 100
# Q2 150
# Q3 200
# 2024 Q1 120
# Q2 180
# Q3 220
# 解除多级索引
result = result.reset_index()
# 按级别操作
grouped = df.groupby(['year', 'quarter'])
print(grouped.level(0).sum()) # 按 year 分组
print(grouped.level(1).sum()) # 按 quarter 分组
实用技巧
分组填充缺失值
# 包含缺失值的数据
df = pd.DataFrame({
'group': ['A', 'A', 'B', 'B', 'A', 'B'],
'value': [10, np.nan, 30, 40, np.nan, 60]
})
# 用组内均值填充
df['value_filled'] = df.groupby('group')['value'].transform(
lambda x: x.fillna(x.mean())
)
print(df)
# group value value_filled
# 0 A 10.0 10.0
# 1 A NaN 10.0
# 2 B 30.0 30.0
# 3 B 40.0 40.0
# 4 A NaN 10.0
# 5 B 60.0 60.0
分组累计运算
# 分组累计求和
df = pd.DataFrame({
'group': ['A', 'A', 'A', 'B', 'B', 'B'],
'value': [10, 20, 30, 40, 50, 60]
})
df['cumsum'] = df.groupby('group')['value'].cumsum()
df['cummax'] = df.groupby('group')['value'].cummax()
df['cummin'] = df.groupby('group')['value'].cummin()
df['cumprod'] = df.groupby('group')['value'].cumprod()
print(df)
# group value cumsum cummax cummin cumprod
# 0 A 10 10 10 10 10
# 1 A 20 30 20 10 200
# 2 A 30 60 30 10 6000
# 3 B 40 40 40 40 40
# 4 B 50 90 50 40 2000
# 5 B 60 150 60 40 120000
分组滚动窗口
# 分组滚动平均
df['rolling_mean'] = df.groupby('group')['value'].transform(
lambda x: x.rolling(window=2, min_periods=1).mean()
)
print(df[['group', 'value', 'rolling_mean']])
# group value rolling_mean
# 0 A 10 10.0
# 1 A 20 15.0
# 2 A 30 25.0
# 3 B 40 40.0
# 4 B 50 45.0
# 5 B 60 55.0
分组差分
# 组内差分
df['diff'] = df.groupby('group')['value'].diff()
df['pct_change'] = df.groupby('group')['value'].pct_change()
print(df[['group', 'value', 'diff', 'pct_change']])
# group value diff pct_change
# 0 A 10 NaN NaN
# 1 A 20 10.0 1.000000
# 2 A 30 10.0 0.500000
# 3 B 40 NaN NaN
# 4 B 50 10.0 0.250000
# 5 B 60 10.0 0.200000
实战示例
示例 1:销售数据分析
# 创建销售数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=90, freq='D')
sales_data = pd.DataFrame({
'date': np.random.choice(dates, 1000),
'product': np.random.choice(['A', 'B', 'C'], 1000),
'region': np.random.choice(['华东', '华北', '华南', '西部'], 1000),
'quantity': np.random.randint(1, 50, 1000),
'price': np.random.uniform(100, 1000, 1000)
})
sales_data['revenue'] = sales_data['quantity'] * sales_data['price']
# 按产品和地区分析
summary = sales_data.groupby(['product', 'region']).agg({
'quantity': ['sum', 'mean'],
'revenue': ['sum', 'mean', 'std']
}).round(2)
print(summary)
# 月度销售趋势
sales_data['month'] = sales_data['date'].dt.to_period('M')
monthly = sales_data.groupby('month').agg({
'revenue': 'sum',
'quantity': 'sum'
})
print(monthly)
# 找出每个地区销量最高的产品
top_products = sales_data.groupby(['region', 'product'])['quantity'].sum()
top_products = top_products.reset_index()
top_products = top_products.loc[top_products.groupby('region')['quantity'].idxmax()]
print(top_products)
示例 2:员工薪资分析
# 员工数据
employees = pd.DataFrame({
'department': np.random.choice(['技术', '销售', '人事', '财务'], 100),
'level': np.random.choice(['初级', '中级', '高级', '专家'], 100),
'salary': np.random.randint(8000, 50000, 100),
'years': np.random.randint(1, 15, 100)
})
# 部门薪资统计
dept_stats = employees.groupby('department')['salary'].agg([
('avg', 'mean'),
('min', 'min'),
('max', 'max'),
('range', lambda x: x.max() - x.min()),
('count', 'count')
]).round(2)
print(dept_stats)
# 计算每个员工与部门平均薪资的差距
employees['dept_avg'] = employees.groupby('department')['salary'].transform('mean')
employees['salary_gap'] = employees['salary'] - employees['dept_avg']
employees['salary_ratio'] = employees['salary'] / employees['dept_avg']
# 找出各部门薪资最高的员工
top_earners = employees.loc[employees.groupby('department')['salary'].idxmax()]
print(top_earners[['department', 'salary', 'level']])
# 按部门级别分析
level_analysis = employees.groupby(['department', 'level']).agg({
'salary': 'mean',
'years': 'mean'
}).round(2)
print(level_analysis)
性能优化
使用分类类型
# 对于分组列,使用分类类型可以加速
df['department'] = df['department'].astype('category')
grouped = df.groupby('department')
避免 apply
# ❌ 慢:使用 apply
df.groupby('group').apply(lambda x: x['value'].mean())
# ✅ 快:使用内置方法
df.groupby('group')['value'].mean()
# ✅ 快:使用 agg
df.groupby('group').agg({'value': 'mean'})
预聚合大数据
# 对于大数据集,先聚合再操作
# 假设需要计算复杂的组间关系
grouped_data = df.groupby(['col1', 'col2']).agg({
'value': ['sum', 'mean', 'count']
}).reset_index()
# 然后在聚合后的数据上操作
result = grouped_data.groupby('col1')['value_sum'].sum()
小结
常用方法:
| 方法 | 返回值 | 用途 |
|---|---|---|
agg() | 聚合结果 | 多种聚合统计 |
transform() | 与原数据同形 | 组内标准化、填充 |
filter() | 筛选后的数据 | 组级别筛选 |
apply() | 任意形状 | 复杂自定义操作 |
内置聚合函数:
- 统计:
count,sum,mean,median,std,var - 极值:
min,max,first,last - 计数:
size,nunique
练习
- 创建一个销售数据集,按产品和地区分组计算总销售额和平均销量
- 使用
transform计算每个员工薪资与部门平均的差距 - 使用
filter筛选出销售额超过平均水平的地区 - 使用自定义聚合函数计算每组数据的中位数绝对偏差
- 按时间分组,计算每周的移动平均销售额
下一步
掌握了分组聚合后,让我们学习 透视表!