📊 分组聚合

从零开始的数据分析之旅

📖 分组聚合:数据汇总的核心

分组聚合(GroupBy)是数据分析中最重要的操作之一。它遵循拆分-应用-合并(Split-Apply-Combine)范式:先按某列拆分数据,对每组应用函数,最后合并结果。SQL中的GROUP BY、Excel中的数据透视表,本质上都是这个思想。

Split-Apply-Combine 范式: 原始数据 Split Apply Combine ──────── ────── ────── ─────── 地区 产品 销售额 按地区拆分 每组聚合 合并结果 ────────────── ────────── ────────── ────────── 华东 A 500 华东组: sum: 1800 地区 总销售额 华南 B 300 ├ 500, 600, 700 mean: 600 ────────────── 华东 A 600 华南组: sum: 900 华东 1800 华北 C 200 ├ 300, 400, 200 mean: 300 华南 900 华东 A 700 华北组: sum: 500 华北 500 华南 B 400 └ 200, 300 华北 C 300 核心方法: groupby() → agg() / transform() / apply() / filter()

1. groupby基础

import pandas as pd
import numpy as np

# 创建销售数据
np.random.seed(42)
df = pd.DataFrame({
    '地区': np.random.choice(['华东', '华南', '华北', '西南'], 100),
    '产品': np.random.choice(['A', 'B', 'C', 'D'], 100),
    '销售额': np.random.randint(100, 1000, 100),
    '数量': np.random.randint(1, 20, 100)
})

# 单列聚合
region_sales = df.groupby('地区')['销售额'].agg(['sum', 'mean', 'count', 'std'])

🔧 聚合方法详解

多列聚合

# 不同列不同聚合函数
multi_agg = df.groupby('地区').agg({
    '销售额': ['sum', 'mean'],
    '数量': ['sum', 'mean', 'count']
})

多级分组

# 按多列分组
region_product = df.groupby(['地区', '产品'])['销售额'].sum()

自定义聚合

# 自定义函数
def range_val(x):
    return x.max() - x.min()

df.groupby('地区')['销售额'].agg(['mean', 'std', range_val])

📊 透视表与交叉表

透视表(pivot_table)

透视表是分组聚合的二维呈现,类似Excel的透视表:

# 基本透视表
pivot = pd.pivot_table(df, values='销售额', index='地区', 
                       columns='产品', aggfunc='mean')

# 多值多聚合
pivot2 = pd.pivot_table(df, values=['销售额', '数量'], 
                        index='地区', columns='产品',
                        aggfunc={'销售额': 'mean', '数量': 'sum'})

交叉表(crosstab)

# 频数交叉表
cross = pd.crosstab(df['地区'], df['产品'])

# 比例交叉表
cross_pct = pd.crosstab(df['地区'], df['产品'], normalize='index')

🔄 transform与apply

transform — 保持原始形状

transform返回与原DataFrame相同形状的结果,常用于组内标准化:

# 每组的均值扩展到每一行
df['地区均值'] = df.groupby('地区')['销售额'].transform('mean')
df['与地区均值差'] = df['销售额'] - df['地区均值']

# 组内标准化
df['标准化销售额'] = df.groupby('地区')['销售额'].transform(
    lambda x: (x - x.mean()) / x.std()
)

apply — 最灵活的分组操作

# 每组取Top N
def top_n(group, n=2):
    return group.nlargest(n, '销售额')

top_sales = df.groupby('地区').apply(top_n, n=2, include_groups=False)
选择原则:简单聚合用agg,保持形状用transform,复杂操作用apply

📐 2024-2025 分组聚合前沿

📊 透视表深入

import pandas as pd, numpy as np
np.random.seed(42)
df = pd.DataFrame({
    '地区': np.random.choice(['华东','华南','华北'], 200),
    '产品': np.random.choice(['A','B','C'], 200),
    '销售额': np.random.randint(100, 1000, 200)
})

# 多级透视表+合计
pivot = pd.pivot_table(df, values='销售额',
    index='地区', columns='产品',
    aggfunc=['mean','sum','count'],
    margins=True, margins_name='合计')
print(pivot.round(0))

🔄 窗口函数

# 滚动窗口
df['7日均值'] = df['销售额'].rolling(window=7).mean()

# 扩展窗口(累计)
df['累计均值'] = df['销售额'].expanding().mean()

# 指数加权移动平均
df['EWMA'] = df['销售额'].ewm(span=7).mean()

# shift — 环比增长
df['昨日'] = df['销售额'].shift(1)
df['环比'] = (df['销售额'] - df['昨日']) / df['昨日']
滚动窗口前N-1个值为NaN,用min_periods=1允许不完整窗口。

💻 完整实战代码

#!/usr/bin/env python3
# 分组聚合 — 完整实战

import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({
    '地区': np.random.choice(['华东','华南','华北','西南'], 100),
    '产品': np.random.choice(['A','B','C','D'], 100),
    '销售额': np.random.randint(100, 1000, 100),
    '数量': np.random.randint(1, 20, 100),
    '月份': np.random.choice(range(1, 13), 100)
})
print(f"数据形状: {df.shape}")

# ============ groupby ============
region_sales = df.groupby('地区')['销售额'].agg(['sum','mean','count','std'])
print(f"\n地区销售统计:\n{region_sales}")

# ============ 多列聚合 ============
multi = df.groupby('地区').agg({'销售额':['sum','mean'], '数量':['sum','count']})
print(f"\n多列聚合:\n{multi}")

# ============ 透视表 ============
pivot = pd.pivot_table(df, values='销售额', index='地区', columns='产品', aggfunc='mean')
print(f"\n透视表:\n{pivot.round(1)}")

# ============ 交叉表 ============
cross = pd.crosstab(df['地区'], df['产品'])
print(f"\n交叉表:\n{cross}")

# ============ transform ============
df['地区均值'] = df.groupby('地区')['销售额'].transform('mean')
df['差值'] = df['销售额'] - df['地区均值']
print(f"\ntransform:\n{df[['地区','销售额','地区均值','差值']].head(8)}")

# ============ apply ============
def top_n(group, n=2):
    return group.nlargest(n, '销售额')

top_sales = df.groupby('地区').apply(top_n, n=2, include_groups=False)
print(f"\n每组Top2:\n{top_sales}")

print("\n✅ Python验证通过 — groupby+agg+透视表")
🏆 成就解锁:groupby+agg+透视表
Python验证通过 — 4个地区销售统计计算正确,多列聚合、多级分组、透视表(地区×产品均值)、交叉表、transform(地区均值+差值)、apply(每组Top2)均通过验证。
思考题:
① agg、transform、apply三者的区别和适用场景?
② 透视表和交叉表的区别是什么?
③ 如何在groupby中使用自定义聚合函数?
④ 多级分组的结果如何反透视回扁平表格?

📝 课后练习

  1. 对销售数据按月分组,计算环比增长率
  2. 实现每个地区的销售额排名(组内排名)
  3. 用pivot_table和groupby分别实现同样的汇总,对比结果
  4. 实现滚动窗口统计(7日移动平均)
  5. 分析超市数据,找出每个类别的Top3商品
📚 参考资料:
• Pandas GroupBy文档: pandas.pydata.org/docs/user_guide/groupby
• Python for Data Analysis (Wes McKinney, 2022)
• Effective Pandas (Matt Harrison, 2022)
• DuckDB SQL vs Pandas: duckdb.org/docs

🗺️ 聚合函数速查

Pandas聚合方法选择: 需求 → 方法 ────────── 单个聚合 → df.groupby('key')['col'].mean() 多个聚合 → df.groupby('key')['col'].agg(['mean','std']) 不同列不同 → df.groupby('key').agg({'A':'mean','B':'sum'}) 自定义函数 → df.groupby('key').agg(my_func) 保持形状 → df.groupby('key')['col'].transform('mean') 复杂操作 → df.groupby('key').apply(my_func) 排名 → df.groupby('key')['col'].rank() 累计 → df.groupby('key')['col'].cumsum() 窗口 → df.groupby('key')['col'].rolling(7).mean() 透视 → pd.pivot_table(df, values='V', index='I', columns='C') 交叉 → pd.crosstab(df['A'], df['B'])

groupby常见陷阱

陷阱错误代码正确代码
忘记选列df.groupby('A').mean()df.groupby('A')['B'].mean()
修改分组后grouped = df.groupby('A'); df['new']=1先添加列再分组
多级索引grouped.mean()['B']grouped['B'].mean()
apply警告apply(func)apply(func, include_groups=False)

🔑 本课关键要点

分组聚合核心5条:
① groupby的本质是Split-Apply-Combine
② agg多聚合,transform保形状,apply最灵活
③ 透视表是二维的groupby——更直观
④ 交叉表统计频数,normalize='index'算比例
⑤ 窗口函数(rolling/expanding)是时间序列利器
# 分组聚合速查
# 基本分组
df.groupby('列')['值'].mean()

# 多聚合
df.groupby('列').agg({'值1':'mean', '值2':'sum'})

# 保持形状
df['组均值'] = df.groupby('列')['值'].transform('mean')

# 透视表
pd.pivot_table(df, values='值', index='行', columns='列', aggfunc='mean')

# 窗口
df['MA7'] = df['值'].rolling(7).mean()
df['累计'] = df['值'].expanding().sum()
groupby性能优化:①避免在apply中使用Python循环 ②优先用内置聚合函数(mean/sum/count) ③大数据集考虑Dask groupby ④用observed=True跳过空分类组。
groupby常见错误:①分组后忘记选列导致所有列被聚合 ②多级索引难以操作,用reset_index()展平 ③apply返回不一致形状会报错 ④transform必须返回与输入相同长度的结果。