首页 / Pandas 入门教程 / 变换与过滤:transform 与 filter

Pandas 入门教程

变换与过滤:transform 与 filter

本教程共 54 篇 · 第 39 篇 · 更新于 2026-08-11 · 约 6 分钟阅读

PandasPandas 入门教程transformfilter分组变换分组过滤

本节目标:用 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 行——这是两者最直观的区别。

Tip

transform 的结果可以直接赋值成新列,这是它最常用的姿势:给每行补组内均值、组内排名、组内占比。

再看两个经典用法。组内占比:

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")
)

先算出组内均值,再统一填充,不会改变组内均值。

Warning

transform 里的函数必须返回与组等长的结果(或标量),否则报错。也别在函数里修改传入的组数据——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,覆盖分组分析的绝大多数需求。