Python-pandas-分组与聚合
·
Pandas 分组与聚合
df.groupby() 是 Pandas 最强大的功能之一,遵循分裂-应用-组合(Split-Apply-Combine)范式。
🔧 GroupBy 基础
创建 GroupBy 对象
import pandas as pd
import numpy as np
df = pd.DataFrame({
'部门': ['销售', '销售', '技术', '技术', '人事', '人事'],
'员工': ['张三', '李四', '王五', '赵六', '钱七', '孙八'],
'工资': [8000, 9000, 15000, 12000, 7000, 7500],
'奖金': [2000, 2500, 3000, 3500, 1500, 1800],
'入职年份': [2019, 2021, 2018, 2020, 2022, 2021]
})
# 按单列分组
grouped = df.groupby('部门')
# 返回 DataFrameGroupBy 对象,尚未执行任何计算
# 按多列分组
multi_grouped = df.groupby(['部门', '入职年份'])
# 查看分组
print(grouped.groups) # 返回 {组名: 行索引列表}
print(grouped.indices) # 同上但返回 numpy 数组
print(grouped.ngroups) # 分组数量
for name, group in grouped:
print(name, group.shape) # 遍历每个组
📊 聚合(Aggregation)
1. 基本聚合函数
# 对单个列聚合
print(grouped['工资'].sum()) # 每组工资总和
print(grouped['工资'].mean()) # 每组工资均值
print(grouped['工资'].count()) # 每组非空计数
print(grouped['工资'].std()) # 标准差
print(grouped['工资'].min()) # 最小值
print(grouped['工资'].max()) # 最大值
print(grouped['工资'].median()) # 中位数
print(grouped['工资'].first()) # 第一个值
print(grouped['工资'].last()) # 最后一个值
print(grouped['工资'].nunique()) # 唯一值数
print(grouped['工资'].var()) # 方差
print(grouped['工资'].sem()) # 均值的标准误差
print(grouped['工资'].quantile(0.75)) # 0.75 分位数
2. agg() — 多函数聚合 ⭐
# 方式一: 传入函数列表
result = grouped['工资'].agg(['sum', 'mean', 'std', 'count'])
print(result)
# sum mean std count
# 部门
# 人事 14500 7250 353.5 2
# 技术 27000 13500 2121.3 2
# 销售 17000 8500 707.1 2
# 方式二: 传入字典 -> 对不同列应用不同聚合
result = grouped.agg({
'工资': ['sum', 'mean'],
'奖金': ['min', 'max', 'sum']
})
print(result)
# 方式三: 命名聚合(推荐!)
result = grouped.agg(
总工资=('工资', 'sum'),
平均工资=('工资', 'mean'),
最高奖金=('奖金', 'max'),
人数=('员工', 'count')
)
print(result)
3. 常用内置字符串聚合
| 函数名 | 说明 | 函数名 | 说明 |
|---|---|---|---|
'sum' |
求和 | 'prod' |
乘积 |
'mean' |
均值 | 'std' |
标准差 |
'median' |
中位数 | 'var' |
方差 |
'min' |
最小值 | 'max' |
最大值 |
'count' |
非空计数 | 'size' |
包括 NaN 的计数 |
'first' |
第一个 | 'last' |
最后一个 |
'nunique' |
唯一值数 | 'sem' |
均值标准误 |
'quantile' |
分位数 | 'ohlc' |
Open-High-Low-Close |
4. 自定义聚合函数
# 自定义函数
def range_func(x):
return x.max() - x.min()
print(grouped['工资'].agg(range_func))
print(grouped['工资'].agg(lambda x: x.max() - x.min()))
# 多列自定义
print(grouped.agg(lambda x: x.max() - x.min()))
🔄 Transform — 组内变换
transform 返回与原始 DataFrame 等长的 Series,非常适合做组内标准化。
# 组内减去均值(去中心化)
df['工资_组内去中心'] = grouped['工资'].transform(lambda x: x - x.mean())
# 组内 Z-Score 标准化
df['工资_ZScore'] = grouped['工资'].transform(
lambda x: (x - x.mean()) / x.std()
)
# 组内填充均值
df['工资_组内补均值'] = grouped['工资'].transform(
lambda x: x.fillna(x.mean())
)
# 常用内置 transform
df['工资_排名'] = grouped['工资'].transform('rank') # 组内排名
df['工资_百分比'] = grouped['工资'].transform('pct_change') # 组内变化率
df['工资_组均值'] = grouped['工资'].transform('mean') # 组均值广播
agg vs transform 对比:
# agg 返回: 每个组一行
print(grouped['工资'].agg('mean')) # shape: (3,)
# transform 返回: 与原 DataFrame 相同行数
print(grouped['工资'].transform('mean')) # shape: (6,)
🎯 Filter — 组级筛选
# 保留组大小 >2 的组
filtered = grouped.filter(lambda g: len(g) > 2)
# 保留工资总和 > 20000 的组
filtered = grouped.filter(lambda g: g['工资'].sum() > 20000)
# 保留组内工资最大值 > 10000 的组
filtered = grouped.filter(lambda g: g['工资'].max() > 10000)
🔁 Apply — 最灵活的组操作
# apply 可以返回任意形状的 DataFrame/Series
def top_n(group, n=1):
return group.nlargest(n, '工资')
result = grouped.apply(top_n, n=1)
print(result)
# 返回标量
result = grouped.apply(lambda g: g['工资'].max() - g['工资'].min())
# apply 可返回多列
def summary(g):
return pd.Series({
'人数': len(g),
'平均工资': g['工资'].mean(),
'工资范围': g['工资'].max() - g['工资'].min()
})
result = grouped.apply(summary)
print(result)
📐 其他分组方式
1. 按函数分组
# 按索引长度的奇偶分组
df.groupby(lambda idx: idx % 2 == 0).sum()
# 按工资是否 > 10000 分组
df.groupby(lambda idx: df.loc[idx, '工资'] > 10000).sum()
2. 按映射字典分组
mapping = {'张三': '组A', '李四': '组A', '王五': '组B',
'赵六': '组B', '钱七': '组C', '孙八': '组C'}
df.groupby(mapping).sum() # 注意: 按 index 映射
df.groupby(df['员工'].map(mapping)).sum() # 按列值映射
3. groupby 的 as_index 参数
# as_index=True (默认): 分组键作为索引
result = df.groupby('部门', as_index=True).sum()
# as_index=False: 分组键保留为列(类似 SQL group by)
result = df.groupby('部门', as_index=False).sum()
4. 多个 groupby 参数
# level: 按 MultiIndex 的层级分组
# axis: 按列分组 (axis=1)
# sort: 是否对组键排序(默认 True,设为 False 提升性能)
# dropna: 是否丢弃 NaN 组键(默认 True)
# observed: 分类数据是否只显示出现的组
# 性能优化设置
df.groupby('部门', sort=False, dropna=True)
🧮 实用分组技巧
累计操作
df['工资_累计和'] = grouped['工资'].cumsum() # 组内累计和
df['工资_累计最大'] = grouped['工资'].cummax() # 组内累计最大值
df['工资_累计积'] = grouped['工资'].cumprod() # 组内累计积
组内偏移
df['工资_上一行'] = grouped['工资'].shift(1) # 组内上移
df['工资_下一行'] = grouped['工资'].shift(-1) # 组内下移
df['工资_变化'] = grouped['工资'].diff() # 组内差分
df['工资_变化率'] = grouped['工资'].pct_change() # 组内变化率
组内排名
df['工资排名'] = grouped['工资'].rank(method='dense', ascending=False)
# method: 'average', 'min', 'max', 'first', 'dense'
取每组的头/尾
df.groupby('部门').head(2) # 每组前 2 行
df.groupby('部门').tail(1) # 每组最后 1 行
df.groupby('部门').nth(0) # 每组第 0 个
df.groupby('部门').nth([0, -1]) # 每组第 0 和最后一个
df.groupby('部门').nlargest(2, '工资') # 每组工资最高的 2 行
df.groupby('部门').nsmallest(1, '奖金') # 每组奖金最低的 1 行
抽样
df.groupby('部门').sample(n=1, random_state=42) # 每组抽 1 个
df.groupby('部门').sample(frac=0.5, random_state=42) # 每组抽 50%
🔗 链式操作
# Pandas 支持方法链
result = (df
.groupby('部门')
.agg(总工资=('工资', 'sum'), 人数=('员工', 'count'))
.query('人数 > 1')
.sort_values('总工资', ascending=False)
.reset_index()
)
print(result)
[[pandas2-总览|← 返回总览]]
更多推荐



所有评论(0)