跳到主要内容

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 聚合对照

需求SQLPandas
简单聚合SELECT city, AVG(salary) FROM t GROUP BY citydf.groupby('city')['salary'].mean()
多列多函数SELECT city, AVG(salary), MAX(age) FROM t GROUP BY citydf.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 DISTINCTCOUNT(DISTINCT dept)df.groupby('city')['dept'].nunique()
透视表SELECT ... CASE WHEN ... GROUP BYpd.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_tablegroupby + 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(汇总行/列),更适合展示型分析。


相关链接