【Python】Pandas `agg()`聚合函数回顾
·
我经常在pd.agg()里面字典元组乱用,报错好几次才会用对,系统梳理了一下agg()函数的使用,同时和apply(),transform()做个简单对比。
我认为关键点在于:分清agg(),transform(), 和apply()传入的数据是什么(即lambda x的x是什么),和输出结果的形式,即可选择正确的函数;详见6.4小节
0. 模拟数据集
import pandas as pd
import numpy as np
np.random.seed(42)
n = 30
df = pd.DataFrame({
'PatientID': [f'P{1000+i}' for i in range(n)],
'Histology': np.random.choice(['Adeno', 'Squamous', 'Other'], size=n),
'age': np.random.randint(30, 90, size=n),
'Survival.time': np.random.randint(1, 2000, size=n),
'status': np.random.choice([0,1], size=n) # 0: censored, 1: event
})
df.loc[0:2, 'Histology'] = ['Adeno','Adeno','Squamous'] # 保证有重复组
df.head()
1. agg() 的核心语法与概念速览
DataFrame.agg(func=None, axis=0, *args, **kwargs)
Series.agg(func=None, *args, **kwargs)
func:聚合函数 — 字符串 / 函数对象 / 列表 / 字典(关键)axis:0(按列 / 每列做聚合)或 1(按行 / 每行做聚合)agg()用于“聚合”操作,通常要求对每个传入的列/组返回标量(或一组标量),结果形状规则。
2. func 参数的四种传入方式
2.1 字符串(内置聚合函数)
对单列或全部数值列使用内置名称(速度快、易读):
df['age'].agg('mean')
df.agg('mean') # 每列的均值(非数值列忽略或报错视情况)
常用字符串:'sum','mean','std','var','min','max','median','count','nunique','first','last'
2.2 函数对象(自定义或 numpy 等)
可以传入函数对象或 lambda;也可以传入接受额外参数的函数 —— 见参数传递章节。
def my_range(x):
return x.max() - x.min()
df['age'].agg(my_range)
df['age'].agg(lambda x: x.max() - x.min())
对 numpy 函数传参示例(稍后详细说明):
import numpy as np
df['age'].agg(np.percentile, q=75) # 有些 numpy 函数可以这样传参
2.3 列表:对同一列应用多个函数(或多列同样的函数集合)
- 单列:返回
Series(index 为函数名或位置) - 多列:返回
DataFrame(行索引为函数名)
df['age'].agg(['mean', 'std', 'max'])
df[['age','Survival.time']].agg(['mean','max'])
# 混合字符串与函数
df['age'].agg(['mean', np.median, lambda x: x.max()-x.min()])
2.4 字典—— 针对不同列应用不同函数
字典方式能非常灵活地指定每列的聚合操作,返回的就是索引列名。
df.agg({
'age': ['mean','std'],
'Survival.time': 'median',
'PatientID': 'count'
})
命名元组风格
当希望重命名返回的索引名称、结果直接可用于下游分析,使用 agg(new_name=('orig_col','func')) 风格非常清爽:
df.groupby('Histology').agg(
avg_age=('age','mean'),
median_surv=('Survival.time','median'),
n_patients=('PatientID','count')
).reset_index()
| 语法类型 | 命名元组风格 | 字典风格 |
|---|---|---|
| 代码示例 | agg(新列名=('原列', '函数')) |
agg({'原列': ['函数1', '函数2']}) |
| 记忆口诀 | “我想要的 = (从哪来, 怎么做)” | “原列: [干啥, 干啥]” |
| 思维方向 | 从左到右(目标导向) | 从右到左(源导向) |
pandas 之所以设计为:字典=原列映射,元组=新列定义,是因为字典天然表达已有对象的处理,而元组天然表达“从哪里来、怎么做、叫什么”三元关系。
3. 与 groupby 结合
# 基础:对每个组每列做平均
df.groupby('Histology').agg('mean')
# 多函数
df.groupby('Histology').agg(['mean','max','count'])
# 不同列不同函数(字典风格)
df.groupby('Histology').agg({
'age': ['mean','std'],
'Survival.time': ['median','min'],
'PatientID': 'count'
})
重命名技巧(强烈推荐):
res = df.groupby('Histology').agg(
avg_age=('age','mean'),
max_surv=('Survival.time','max'),
count_patients=('PatientID','count')
).reset_index()
这样得到的 res 列名平铺、可读、便于导出/画图。
4. agg() 的参数传递规则(向聚合函数传额外参数)
有几种常见方式把额外参数传给 agg 中的函数:
方式1 — 直接在 agg 中传参(适用于接受 **kwargs 的函数)
df['age'].agg(np.percentile, q=75)
注意:并非所有函数都按此方式工作,行为依函数签名而定。
方式2 — functools.partial 或 闭包(通用稳健)
from functools import partial
df['age'].agg(partial(np.percentile, q=75))
方式3 — 封装函数
def pct75(x):
return np.percentile(x, 75)
df['age'].agg(pct75)
5. 返回值类型规则
| func 类型 | 对单列调用 | 对多列调用 |
|---|---|---|
| 单字符串 | 标量 | Series |
| 单函数 | 标量 | Series |
| 函数列表 | Series | DataFrame |
| 字典 | — | DataFrame(多层列索引或命名列) |
6. agg() vs transform() vs apply()
6.1 agg()(aggregate)
- 目的:返回聚合结果(通常每组一个或少数几个标量)
- 返回形态:groupby 后通常是每组一行(或多行函数结果),用于汇总表格
- 函数应返回:标量(或一组标量,按函数个数)
6.2 transform()
- 目的:对每个组或列进行元素级变换,返回与原对象相同形状的结果(index 对齐保持)
- 常见用途:标准化(减组内均值/除以组内 std)、补充列(把组统计映射回行级)
- 要求:函数必须返回与组长度相同的序列(或可广播的标量)
- 示例:
# 组内去中心化:每个样本减去其所属组的均值(结果对齐回原 df)
df['age_centered'] = df.groupby('Histology')['age'].transform(lambda x: x - x.mean())
# 组内 Z-score
df['age_z'] = df.groupby('Histology')['age'].transform(lambda x: (x - x.mean())/x.std())
记忆口诀:agg() 给你“每组的总结”(少量行),transform() 给你“每行的变换值”(和原 df 一样多)。
6.3 apply()
- 灵活性最高:可以对每个组返回任意形状(Series / DataFrame / 标量)
- 代价:通常较慢(会触发 Python 级循环)
- 何时用:当逻辑无法用
agg或transform直接表达时(比如生成复杂子表、排序后取 top-k 并保留原行等)
6.4 三者对比
| 维度 | agg() | transform() | apply() |
|---|---|---|---|
| 目的 | 聚合:返回每组的汇总统计 | 变换:返回与原数据同形的转换结果 | 灵活处理:执行任意复杂操作 |
| 返回形态 | 每组一行(或每函数一行)的汇总表 ⟹ 结果行数 ≤ 组数 | 与原对象相同形状(按行对齐) ⟹ 结果行数 = 原行数 | 任意形状(Series/DataFrame/标量) |
| 函数要求 | 必须返回标量(或一组标量) | 必须返回与组长度相同的序列或可广播的标量 | 可返回任意类型和形状 |
| 传入对象⭐️ | Series(整列聚合) | Series(整列变换) | DataFrame(整表) |
| 典型场景 | 计算每组的和、均值、最大值等统计量 | 组内标准化、Z-score、填充缺失值、映射组统计回原行 | 排序后取top-k、生成复杂子表、多列自定义计算 |
| 性能 | 快(向量化/Cython优化) | 快(向量化/Cython优化) | 较慢(Python级循环) |
| 根据目的选函数⭐️ | 返回“整列的总结” | 返回对应列“每行的变换值”(和原来一样多) | 既不是总结,也不是逐个对应的变换值 |
7. 常见陷阱
✅ 推荐
- 对分组聚合优先使用内置字符串或 numpy 函数(性能更优)
- 使用命名元组形式
agg(new_name=('col','func'))来获得整洁列名 - 复杂逻辑写成独立函数,再传入
agg(利于测试与复用) - 当需要把组统计映回每行时用
transform(),不要用agg()+ merge(更简洁)
❌ 避免
- 在
agg中嵌套使用apply或df.apply(性能差) agg中返回长度不一致的序列(会报错)- 对非数值列使用不合适的数值聚合(例如对 ID 用
mean)
8. 示例
# 1) 每组 age 的均值与中位数
df.groupby('Histology')['age'].agg(['mean','median'])
# 2) 不同列不同函数(字典风格)
df.groupby('Histology').agg({
'age': ['mean','std'],
'Survival.time': 'median',
'PatientID':'count'
})
# 3) 命名元组风格(推荐)
res = df.groupby('Histology').agg(
avg_age=('age','mean'),
std_age=('age','std'),
median_surv=('Survival.time','median'),
n_patients=('PatientID','count')
).reset_index()
# 4) 使用自定义函数 + 重命名
def above_median_count(x):
return (x > x.median()).sum()
res2 = df.groupby('Histology').agg(
n_above_median_surv=('Survival.time', above_median_count),
range_age=('age', lambda x: x.max() - x.min())
).reset_index()
# 5) transform 用法示例(把组均值映回每行)
df['age_minus_group_mean'] = df.groupby('Histology')['age'].transform(lambda x: x - x.mean())
9. 小结
agg()的关键在于func参数:字符串 / 函数 / 列表 / 字典 四种模式(字典 + 命名元组最灵活、实用)。agg()返回的结果更适合“汇总表”;transform()返回与原 df 等长的序列(用于行级变换);apply()最灵活但最慢。- 推荐在
groupby中使用命名元组风格来获得清晰、可复用的汇总表(便于下游分析与绘图)。 - 对需传参的函数优先使用
partial或封装函数以保证行为一致。
更多推荐



所有评论(0)