dplyr::across与purrr::modify_if/at的区别及选型参考
mutate(across()) 和 purrr modify_* 系列的选型差异 二者并非完全等价,除了不需要额外加载purrr之外,绝大多数数据处理场景下都应该优先选择mutate(across()),核心差异如下:
- 设计定位完全不同
mutate()是dplyr专门为数据框(tibble/data.frame)变换设计的核心动词,执行逻辑自带数据框上下文感知:支持分组(group_by())运算,可以在处理函数中引用同数据框的其他列,还能配合.keep、.before/.after等参数控制列的保留规则、排列位置。
而modify_if()/modify_at()本质是purrr面向通用列表的修改函数,只是因为数据框本质是「由列构成的列表」才刚好能实现列修改效果。它会把每一列当作独立的列表元素单独处理,既感知不到数据框内的其他列,也完全不识别dplyr的分组属性,分组场景下直接用会得到不符合预期的全局计算结果。 - 功能灵活度差距明显
across()适配全套tidyselect列选择语法,支持复杂的列筛选规则:比如可以用where(is.numeric) & starts_with("sales")筛选数值型且列名以sales开头的列,也可以用c(year, cyl:cty)选中连续列段;同时支持给同一批列应用多个处理函数,通过.names参数自定义新列名,非常方便在保留原列的基础上生成衍生字段。比如要给所有数值列生成标准化后的新列,只需要一行代码:
相比之下mpg |> mutate(across(where(is.numeric), list(std = ~scale(.x)), .names = "{.col}_{.fn}"))modify_*系列的列选择能力非常有限:modify_if仅支持传入判断单列属性的谓词函数,modify_at仅支持传入列名或位置索引,不支持复杂选择规则,也只能原地修改选中列,无法直接生成新列。 - 长期维护和生态兼容性更好
目前tidyverse生态的列操作已经统一以across()作为核心接口,dplyr后续新增的所有数据处理特性(比如逐行运算、临时列上下文、特殊列属性保留)都会优先适配mutate(across())组合。在处理带特殊属性的列(比如haven读入的带标签列、sf空间对象的几何列、tsibble的时间索引列)时,mutate会专门保留列和数据框的特殊属性,而modify_*作为通用列表处理函数,偶尔会丢失这些属性导致后续操作出错。
什么时候适合用modify_*?
如果你本身就在写处理通用列表的purrr逻辑,输入对象是类列表结构、不需要用到数据框专属的分组、跨列引用等特性,且不想额外引入dplyr依赖时,可以用modify_*。只要是常规的数据清洗、分析流程,统一用mutate(across())即可,不需要混用两种写法增加代码理解成本。
可以看一个分组场景下二者行为差异的直观示例:
library(dplyr, warn.conflicts = FALSE) library(purrr) library(ggplot2) # mutate+across 会识别分组,按组做数值列中心化 mpg |> group_by(drv) |> mutate(across(where(is.numeric), ~.x - mean(.x))) |> slice_head(n = 2) # 结果中数值列减去的是对应drv分组的均值 # modify_if 不识别分组,会对整列做全局中心化 mpg |> group_by(drv) |> modify_if(is.numeric, ~.x - mean(.x)) |> slice_head(n = 2) # 结果中数值列减去的是全数据集的全局均值,不符合分组计算预期
内容的提问来源于stack exchange,提问作者Evan FNG
相关产品推荐
相关产品推荐

