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. groupbyas_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-总览|← 返回总览]]

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐