变换与过滤:transform 与 filter
本教程共 54 篇 · 第 39 篇 · 更新于 2026-08-11 · 约 6 分钟阅读
本节目标:用 transform 给每行补上组内计算结果(保持行数不变),用 filter 按组整体筛数据,理解分组后 apply 的灵活用法。
聚合把每组压成一行。但很多需求要”每行都保留,同时带上组内的统计值”——比如给每个员工加上”部门平均薪资”列。这类操作叫变换(transform)。还有一类需求是”人数太少的组整个删掉”,叫过滤(filter)。
transform:保持形状的组内计算
transform 的输入是组内的列(Series),输出必须和输入一样长(或一个标量广播回去)。结果行数和原表完全一致:
import pandas as pd
import numpy as np
df = pd.DataFrame({
"部门": ["技术", "销售", "技术", "运营", "销售", "技术"],
"姓名": ["张三", "李四", "王五", "赵六", "钱七", "孙八"],
"薪资": [12000, 15000, 11000, 18000, 14000, 13000],
"奖金": [2000, 3000, 1000, 5000, 2500, 1800],
})
df["部门平均薪资"] = df.groupby("部门")["薪资"].transform("mean")
transform 传字符串 "mean",每组算出的平均值被广播回组内每一行。原来 6 行,结果还是 6 行,只是多了一列。聚合的结果每组一行,变换的结果每组 N 行——这是两者最直观的区别。
Tiptransform 的结果可以直接赋值成新列,这是它最常用的姿势:给每行补组内均值、组内排名、组内占比。
再看两个经典用法。组内占比:
df["薪资占比"] = df["薪资"] / df.groupby("部门")["薪资"].transform("sum")
组内排名:
df["组内名次"] = df.groupby("部门")["薪资"].transform("rank", ascending=False)
多列一起变换也支持:
df.groupby("部门")[["薪资", "奖金"]].transform("mean")
内置变换方法
很多方法天生就是”逐组逐行”的,直接挂在分组对象上调用:
g = df.groupby("部门")["薪资"]
g.cumsum() # 组内累计求和
g.diff() # 组内相邻差
g.shift(1) # 组内下移一行
g.rank() # 组内排名
g.ffill() # 组内向前填充
g.cumcount() # 组内第几个(从 0 数起)
这些都属于 transform 家族,返回值都和原数据等长。
transform 自定义函数
内置方法不够时,可以传自定义函数。函数接收组内一列(Series),返回等长的 Series 或标量:
df.groupby("部门")["薪资"].transform(lambda x: (x - x.mean()) / x.std())
这是组内标准化(z-score):每组均值变 0、标准差变 1。
用组内均值填充缺失值也是常见场景:
df_missing = pd.DataFrame({
"部门": ["技术", "技术", "销售"],
"薪资": [10000, np.nan, 15000],
})
df_missing["薪资"] = df_missing["薪资"].fillna(
df_missing.groupby("部门")["薪资"].transform("mean")
)
先算出组内均值,再统一填充,不会改变组内均值。
Warningtransform 里的函数必须返回与组等长的结果(或标量),否则报错。也别在函数里修改传入的组数据——pandas 不保证修改会生效。
filter:按组整体过滤
filter 接收一个函数,函数以”整个组”为参数,返回 True 保留该组、False 丢弃整组:
df.groupby("部门").filter(lambda x: len(x) >= 2)
只保留人数不少于 2 的部门。函数里也能用具体列:
df.groupby("部门").filter(lambda x: x["薪资"].mean() > 13000)
过滤后返回的是原表的子集,行数会变,但列结构不变。
默认不满足条件的组直接删掉;想保留位置、用 NaN 占位,传 dropna=False:
df.groupby("部门").filter(lambda x: len(x) >= 2, dropna=False)
dropna=False 适合”行位置不能乱”的场景,比如下游还要按行号合并其他数据。
内置过滤:head / tail / nth
分组对象支持 head、tail,取每组前几行或后几行:
df.groupby("部门").head(1) # 每组第一行
df.groupby("部门").tail(1) # 每组最后一行
df.groupby("部门").nth(0) # 每组第 0 行
nth 更灵活:nth(1) 取每组第 2 行,nth(-1) 取每组最后一行,也支持列表 nth([0, -1]) 一次取多行。组里没有第 n 行时不报错,直接跳过。
分组后的 apply:万金油
聚合、变换、过滤三类没覆盖的场景,用 apply。它把整个组(DataFrame)交给函数,结果形状自由:
df.groupby("部门").apply(lambda x: x["薪资"].describe())
每组输出一组描述统计,结果按组堆叠。apply 会自动推断结果该”压成行”还是”拼成表”,但推断不一定符合预期,输出要检查。默认结果索引里会带上分组键,不需要时在 groupby 里传 group_keys=False:
df.groupby("部门", group_keys=False).apply(lambda x: x)
Note能用内置聚合、变换、过滤解决的,优先用内置方法:它们有专门优化的实现,比 apply 快得多。apply 留给”真没有现成方法”的场景。
小结
transform 保持行数、filter 删组、apply 兜底,三个工具配合 §38 的 agg,覆盖分组分析的绝大多数需求。