我经常在pd.agg()里面字典元组乱用,报错好几次才会用对,系统梳理了一下agg()函数的使用,同时和apply(),transform()做个简单对比。

我认为关键点在于:分清agg(), transform(), 和apply()传入的数据是什么(即lambda xx是什么),和输出结果的形式,即可选择正确的函数;详见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 级循环)
  • 何时用:当逻辑无法用 aggtransform 直接表达时(比如生成复杂子表、排序后取 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 中嵌套使用 applydf.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 或封装函数以保证行为一致。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐