从零开始的数据分析之旅
分组聚合(GroupBy)是数据分析中最重要的操作之一。它遵循拆分-应用-合并(Split-Apply-Combine)范式:先按某列拆分数据,对每组应用函数,最后合并结果。SQL中的GROUP BY、Excel中的数据透视表,本质上都是这个思想。
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])
透视表是分组聚合的二维呈现,类似Excel的透视表:
# 基本透视表
pivot = pd.pivot_table(df, values='销售额', index='地区',
columns='产品', aggfunc='mean')
# 多值多聚合
pivot2 = pd.pivot_table(df, values=['销售额', '数量'],
index='地区', columns='产品',
aggfunc={'销售额': 'mean', '数量': 'sum'})
# 频数交叉表
cross = pd.crosstab(df['地区'], df['产品'])
# 比例交叉表
cross_pct = pd.crosstab(df['地区'], df['产品'], normalize='index')
transform返回与原DataFrame相同形状的结果,常用于组内标准化:
# 每组的均值扩展到每一行
df['地区均值'] = df.groupby('地区')['销售额'].transform('mean')
df['与地区均值差'] = df['销售额'] - df['地区均值']
# 组内标准化
df['标准化销售额'] = df.groupby('地区')['销售额'].transform(
lambda x: (x - x.mean()) / x.std()
)
# 每组取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。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['昨日']
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+透视表")
| 陷阱 | 错误代码 | 正确代码 |
|---|---|---|
| 忘记选列 | 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) |
# 分组聚合速查
# 基本分组
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()
observed=True跳过空分类组。reset_index()展平 ③apply返回不一致形状会报错 ④transform必须返回与输入相同长度的结果。