Pandas 聚合与分组
问题
如何使用 Pandas 进行分组聚合?groupby、agg、pivot_table 分别怎么用?
答案
分组聚合是数据分析中最核心的操作(等价于 SQL 的 GROUP BY),面试手写频率极高。
groupby 核心机制
Pandas 的 groupby 遵循 Split-Apply-Combine 三步范式:
基础 groupby
groupby 基本用法
import pandas as pd
df = pd.DataFrame({
'city': ['北京', '上海', '北京', '上海', '广州', '广州'],
'dept': ['技术', '技术', '市场', '市场', '技术', '市场'],
'salary': [20000, 25000, 15000, 18000, 22000, 13000],
'age': [28, 32, 25, 30, 27, 24]
})
# 单列分组 + 单个聚合
df.groupby('city')['salary'].mean()
# 单列分组 + 多个聚合
df.groupby('city')['salary'].agg(['mean', 'median', 'count', 'sum'])
# 多列分组
df.groupby(['city', 'dept'])['salary'].mean()
# 分组后重置索引(常用)
df.groupby('city')['salary'].mean().reset_index()
agg 多列多聚合
agg 高级用法
# 不同列使用不同聚合函数
result = df.groupby('city').agg({
'salary': ['mean', 'max', 'min'], # 薪资:均值、最大、最小
'age': 'mean', # 年龄:均值
'dept': 'nunique' # 部门:去重计数
})
# 命名聚合(推荐写法,列名更清晰)
result = df.groupby('city').agg(
avg_salary=('salary', 'mean'),
max_salary=('salary', 'max'),
employee_count=('salary', 'count'),
avg_age=('age', 'mean'),
dept_count=('dept', 'nunique')
).reset_index()
命名聚合语法
pd.NamedAgg(column, aggfunc) 或简写 (column, aggfunc),生成的列名可自定义,比多层索引清晰得多,推荐在面试中使用。
常见聚合函数
| 函数 | 说明 | 等价 SQL |
|---|---|---|
mean() | 均值 | AVG() |
sum() | 求和 | SUM() |
count() | 计数(不含 NaN) | COUNT(col) |
size() | 计数(含 NaN) | COUNT(*) |
min() / max() | 最小/最大 | MIN() / MAX() |
median() | 中位数 | 无直接等价 |
std() / var() | 标准差/方差 | STDDEV() / VAR() |
nunique() | 去重计数 | COUNT(DISTINCT) |
first() / last() | 第一个/最后一个 | 无直接等价 |
自定义聚合
自定义聚合函数
# lambda 聚合
df.groupby('city')['salary'].agg(lambda x: x.max() - x.min()) # 极差
# 多个自定义聚合
df.groupby('city').agg(
salary_range=('salary', lambda x: x.max() - x.min()),
salary_cv=('salary', lambda x: x.std() / x.mean()), # 变异系数
top_salary=('salary', lambda x: x.nlargest(2).mean()) # Top2 均值
)
# 百分位
df.groupby('city')['salary'].agg(
p25=lambda x: x.quantile(0.25),
p50=lambda x: x.quantile(0.50),
p75=lambda x: x.quantile(0.75)
)
transform 与 filter
transform 返回与原 DataFrame 等长的结果(类似 SQL 窗口函数),filter 按条件筛选分组:
transform 和 filter
# transform: 每个人薪资占所在城市平均薪资的比例
df['salary_ratio'] = df.groupby('city')['salary'].transform('mean')
df['ratio'] = df['salary'] / df['salary_ratio']
# transform: 等价于 SQL 窗口函数
# SQL: AVG(salary) OVER (PARTITION BY city)
df['city_avg'] = df.groupby('city')['salary'].transform('mean')
# transform: 组内排名
df['rank'] = df.groupby('city')['salary'].rank(ascending=False, method='dense')
# filter: 只保留平均薪资 > 18000 的城市的数据
df.groupby('city').filter(lambda x: x['salary'].mean() > 18000)
pivot_table 透视表
透视表是分组聚合的可视化友好形式(类似 Excel 数据透视表):
pivot_table 用法
# 基础透视表:按城市和部门统计平均薪资
pd.pivot_table(
df,
values='salary', # 值(被聚合的列)
index='city', # 行标签
columns='dept', # 列标签
aggfunc='mean', # 聚合函数
fill_value=0, # 缺失值填充
margins=True, # 添加汇总行/列
margins_name='合计'
)
# 多值多函数
pd.pivot_table(
df,
values=['salary', 'age'],
index='city',
aggfunc={'salary': ['mean', 'sum'], 'age': 'mean'}
)
crosstab 交叉表
交叉表特别适合统计频次和占比:
crosstab 用法
# 频次统计
pd.crosstab(df['city'], df['dept'])
# 带边际汇总
pd.crosstab(df['city'], df['dept'], margins=True, margins_name='合计')
# 占比(normalize)
pd.crosstab(df['city'], df['dept'], normalize='index') # 行占比(每行加起来=1)
pd.crosstab(df['city'], df['dept'], normalize='columns') # 列占比
pd.crosstab(df['city'], df['dept'], normalize='all') # 全表占比
Pandas vs SQL 聚合对照
| 需求 | SQL | Pandas |
|---|---|---|
| 简单聚合 | SELECT city, AVG(salary) FROM t GROUP BY city | df.groupby('city')['salary'].mean() |
| 多列多函数 | SELECT city, AVG(salary), MAX(age) FROM t GROUP BY city | df.groupby('city').agg(avg_s=('salary','mean'), max_a=('age','max')) |
| HAVING | ... HAVING AVG(salary) > 20000 | .filter(lambda x: x['salary'].mean() > 20000) 或 聚合后筛选 |
| 窗口函数 | AVG(salary) OVER(PARTITION BY city) | df.groupby('city')['salary'].transform('mean') |
| COUNT DISTINCT | COUNT(DISTINCT dept) | df.groupby('city')['dept'].nunique() |
| 透视表 | SELECT ... CASE WHEN ... GROUP BY | pd.pivot_table(...) |
常见面试问题
Q1: groupby 后的结果是什么类型?
答案:
df.groupby('col')返回DataFrameGroupBy对象(惰性的,不立即计算)df.groupby('col')['salary'].mean()返回 Series(Index 是分组键)- 加
.reset_index()可转回 DataFrame
Q2: count 和 size 的区别?
答案:
| 方法 | 是否计入 NaN | 返回类型 |
|---|---|---|
count() | ❌ 不计入 | Series(每列单独计数) |
size() | ✅ 计入 | 单值 / Series |
# count:排除 NaN
df.groupby('city')['salary'].count() # 非空的 salary 数量
# size:包含 NaN
df.groupby('city').size() # 每组总行数
Q3: 如何用 Pandas 实现 SQL 的 HAVING?
答案:
# SQL: SELECT city, AVG(salary) FROM t GROUP BY city HAVING AVG(salary) > 20000
# 方式一:聚合后筛选(推荐)
result = df.groupby('city')['salary'].mean().reset_index()
result[result['salary'] > 20000]
# 方式二:filter
df.groupby('city').filter(lambda x: x['salary'].mean() > 20000)
# 注意:filter 返回的是原始行,不是聚合结果
Q4: transform 和 agg 的区别?
答案:
- agg:返回聚合后的结果,行数 = 分组数(类似 SQL GROUP BY)
- transform:返回与原表等长的结果,每行填入对应分组的聚合值(类似 SQL 窗口函数)
- 需要在原表上新增分组统计列时用
transform,需要独立的聚合表时用agg
Q5: pivot_table 和 groupby + unstack 的区别?
答案:
功能上等价,pivot_table 是 groupby + unstack 的语法糖:
# 等价写法
pd.pivot_table(df, values='salary', index='city', columns='dept', aggfunc='mean')
# ↔
df.groupby(['city', 'dept'])['salary'].mean().unstack(fill_value=0)
pivot_table 的优势:语法更直观,支持 margins(汇总行/列),更适合展示型分析。