pandas 分组变换详解用 groupby.transform 将组内聚合结果广播回每一行【免费下载链接】pandasFlexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more项目地址: https://gitcode.com/gh_mirrors/pa/pandas分组聚合aggregation之后如何把结果与原始数据逐行对齐是数据分析中绕不开的经典问题。在 pandas 中groupby对象提供的transform机制专为此设计——它允许按组计算一个统计量再把这个统计量广播回组内每一行的操作在一句代码内完成。本文以 pandas 官方与其他数据分析工具对比系列文档中的核心示例为骨架见 includes/transform.rst结合 用户指南 groupby 章节 与 源码实现深入讲解transform的用法、底层机制与高效实践。读完本文你将掌握去均值中心化按组填充缺失值组内标准化等分组变换的一行式写法并理解其与 SASproc summarymerge、Statabysortegen等传统多步流程的本质差异。问题的起点聚合结果如何回到原始行在很多数据分析场景中我们并不满足于得到每组一个数的汇总表而是希望把组级统计量作为一个新列附着到原始表的每一行上。典型的例子是按组去均值把total_bill减去其所在smoker组的平均值得到该顾客相对其组内平均水平的偏离量。在 SQL 或传统统计软件中这通常需要先聚合、再回连两步SAS的做法是先proc summary按组算出均值再proc sortmerge把均值表合并回原表见 comparison_with_sas.rstproc summary datatips missing nway; class smoker; var total_bill; output outsmoker_means mean(total_bill)group_bill; run; proc sort datatips; by smoker; run; data tips; merge tips(ina) smoker_means(inb); by smoker; adj_total_bill total_bill - group_bill; if a and b; run;Stata则用bysortegen生成组均值列后再相减见 comparison_with_stata.rstbysort sex smoker: egen group_bill mean(total_bill) generate adj_total_bill total_bill - group_bill这两种写法要么需要多次过程调用和显式的表连接要么依赖egen的组内广播约定。pandas 将这一能力内建为groupby.transform一句话即可完成同样的逻辑。核心示例一行代码完成按组去均值官方对比文档给出的 pandas 写法如下沿用tips示例数据集可通过pd.read_csv从 CSV 或 seaborn 提供的 tips 数据加载gb tips.groupby(smoker)[total_bill] tips[adj_total_bill] tips[total_bill] - gb.transform(mean)逐句拆解tips.groupby(smoker)[total_bill]按smoker列分组并取出total_bill这一列得到一个SeriesGroupBy对象gb.transform(mean)对每个组计算均值并将均值广播回组内每一行——返回一个与tips等长、索引完全一致的 Series用原始列减去广播后的组均值得到新列adj_total_bill并赋回tips。关键点在于第 2 步transform(mean)传入的是字符串别名mean但结果不是压缩后的聚合表而是与输入等长的广播结果。这正是transform与agg/aggregate的本质区别——后者把每组压缩为一行前者把每组结果拉回原始行数。transform 的两种输入字符串别名与自定义函数transform的输入远比mean这一个字符串丰富。根据 用户指南 与 源码 docstring它接受两大类输入1. 字符串别名内置方法内置变换类方法cumsum、cummin、cumprod、cummax、diff、ffill、pct_change、rank、shift等它们天然逐组计算并返回与组等长的结果内置聚合类方法mean、sum、std、max、min、count等传入transform后结果会被广播到整个组。官方文档中的演示数据来自文档构造的speeds示例grouped speeds.groupby(class)[[max_speed]] grouped.transform(cumsum) # 组内累积和逐行返回 grouped.transform(sum) # 组内总和广播到每一行当传入的聚合方法具备高效实现时这种广播路径同样高效——例如组均值、组和这类算子走的是向量化 C 扩展路径而不是逐组调用 Python 函数。2. 用户自定义函数UDF除了字符串别名transform也接受 Python 函数。UDF 必须满足以下约束详见 groupby.rst 中 UDF 要求返回值必须与组块大小相同或可广播到组块大小例如返回标量grouped.transform(lambda x: x.iloc[-1])函数按列作用于组块首次调用经由chunk.apply分发禁止对组块做原地修改——组块应视为不可变对象原地改动可能产生未定义结果若函数支持一次性处理整个组块的所有列则从第二个组块起会走快速路径。# 组内标准化每个值减去组均值再除以组标准差 transformed ts.groupby(lambda x: x.year).transform( lambda x: (x - x.mean()) / x.std() )注意groupby的分组键也可以是一个函数如上例按年份分组或任意长度的对齐序列这正是groupby灵活性的体现。低维结果自动广播变换函数的输出维度若低于输入例如返回标量结果会自动广播以匹配输入形状ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())每个时间戳都会获得其所属年份的最大值减最小值。源码视角transform 的实现与引擎参数从源码看transform在SeriesGroupBypandas/core/groupby/generic.py#L622与DataFrameGroupBy同文件 L2529上均有定义其 docstring 明确了签名与能力边界transform(func, *args, engineNone, engine_kwargsNone, **kwargs)func既可是字符串内置方法别名也可是 Python 函数还可配合enginenumba传入 Numba JIT 函数enginecython默认走 C 扩展、numbaJIT 编译或None回落到cython或受全局配置compute.use_numba影响engine_kwargsNumba 引擎下可接受nogil与parallel两个布尔键默认均为False返回值与原始对象索引完全一致的 Series/DataFrame填充的是逐组变换后的值。从代码结构可以推断transform的核心执行路径会在pandas/core/groupby/transform.py中完成先按组切分数据对每个组块调用函数或字符串别名对应的内置实现再把结果按原始索引回填组装。使用字符串别名时走的是预编译的 Cython 算子因此比逐组调用 Python lambda 快得多——这正是用户指南反复强调优先用内置方法替代 UDF的原因见 groupby_efficient_transforms 小节。从源码测试看行为契约仓库测试文件 pandas/tests/groupby/transform/test_transform.py 为上述行为提供了大量可验证的断言例如result grp.transform(mean)与逐组手动计算的期望值比对test_transform.py#L199高效写法ts - grouped.transform(mean)与 UDF 写法ts.groupby(...).transform(lambda x: (x - x.mean()) / x.std())结果等价L280-L287在as_indexFalse、含分类列分组observedTrue/False等组合下的广播一致性L1336-L1346。这些测试从侧面印证了无论分组键是普通列、分类列还是外部 Seriestransform都保证返回与输入等长、索引对齐的结果。实战场景填充缺失值、标准化与高效写法按组均值填充缺失值transform最常见的实战用途之一是用组均值填补组内缺失值grouped data_df.groupby(key) transformed grouped.transform(lambda x: x.fillna(x.mean()))验证两条性质变换前后组均值保持不变且变换后不再含缺失值transformed.groupby(key).count()与grouped.count()对比、grouped_trans.size()等于组大小详见 groupby.rst#L975-L1009。用内置方法替代 UDF 提升性能用户指南明确指出用 UDF 做变换往往不如内置方法高效建议把复杂操作拆成利用内置方法的链式调用。以下三组等价写法中右侧均优于左侧被注释掉的 UDF 版本见 groupby.rst#L1013-L1032# 1) 组内标准化 # result ts.groupby(lambda x: x.year).transform( # lambda x: (x - x.mean()) / x.std() # ) grouped ts.groupby(lambda x: x.year) result (ts - grouped.transform(mean)) / grouped.transform(std) # 2) 组内极差 # result ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min()) grouped ts.groupby(lambda x: x.year) result grouped.transform(max) - grouped.transform(min) # 3) 组均值填充缺失值 # result data_df.groupby(key).transform(lambda x: x.fillna(x.mean())) grouped data_df.groupby(key) result data_df.fillna(grouped.transform(mean))这三条正是本文核心示例按组去均值的推广tips[total_bill] - gb.transform(mean)与写法 1 本质相同——用内置transform(mean)的广播结果与原始列做算术运算既简洁又走高速路径。需要注意的行为与版本细节索引对齐2.0.0 起DataFrameGroupBy.transform的变换函数若返回 DataFrame结果索引会与输入索引对齐若想避免对齐可在函数内调用.to_numpy()见 groupby.rst#L917-L922。dtype 推断与agg类似transform结果的 dtype 由变换函数决定若不同组产生不同 dtype将按DataFrame构造规则推导公共 dtype。引擎选择默认cython引擎只支持字符串别名与特定内置路径Numba 引擎要求 UDF 以values, index为首两个形参适合需要 JIT 加速的自定义逻辑。内存模型pandas 完全在内存中运行SAS 数据集则存在磁盘上因此能处理的数据规模受机器内存限制但内存内的组内广播通常比落盘 回连更快——这也是 comparison_with_sas.rst 中Disk vs memory一节的结论。小结groupby.transform把按组聚合 广播回原行这个在 SASproc summarymerge和 Statabysortegen中需要多步拼接的操作压缩为一行自带索引对齐的表达式。它既能接受mean、sum等字符串别名自动广播也能接受自定义函数做任意逐组变换并通过cython/numba引擎提供性能选项。日常分析中优先组合内置变换方法往往能同时获得可读性与性能——正如官方对比文档与用户指南反复示范的那样。【免费下载链接】pandasFlexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more项目地址: https://gitcode.com/gh_mirrors/pa/pandas创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考