ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

dplyr 的 transmute() 参数限制解析:.keep / .before / .after 为何报错,以及如何迁移到 mutate()

dplyr 的 transmute() 参数限制解析:.keep / .before / .after 为何报错,以及如何迁移到 mutate()
  • 数据分析

【免费下载链接】dplyr

dplyr: A grammar of data manipulation

项目地址:https://gitcode.com/gh_mirrors/dp/dplyr
点击查看免费下载

transmute()是 dplyr 中"只保留指定计算结果列"的数据变换动词,当前已进入 superseded(被取代)生命周期。本文基于 dplyr 仓库中的测试快照文档 tests/testthat/_snaps/transmute.md,从精确的错误消息出发,深入剖析transmute()不支持.keep、.before、.after三个参数的底层原因与报错机制,并给出迁移到mutate(.keep = "none")的完整方案。读完本文,你将理解 superseded 函数的参数设计边界、错误消息的产生路径,以及新旧 API 在列排序语义上的关键差异。

transmute() 的功能定位与生命周期状态

transmute()用于创建一个只包含指定计算列的新数据框。它的核心语义是:

  • 通过...传入>transmute <- function(.data, ...) { # dplyr 1.1.0 lifecycle::signal_stage( "superseded", "transmute()", I("mutate(.keep = 'none')") ) UseMethod("transmute") }

    根据 NEWS.md 的记录,dplyr 1.1.0 起transmute()被正式标记为 superseded,官方推荐的替代方案是mutate(.keep = "none")。也就是说:这一函数仍可正常使用,但 dplyr 团队不再为其添加新功能,新的参数能力只会出现在mutate()上——这正是后续参数报错的根本原因。

    快照中的三条错误消息:不支持的参数

    在 tests/testthat/_snaps/transmute.md 中,记录了transmute()的三个确定性错误输出(由expect_error()捕获并以快照形式锁定):

    transmute(mtcars, cyl2 = cyl, .keep = "all") #> Error in `transmute()`: #> ! The `.keep` argument is not supported. transmute(mtcars, cyl2 = cyl, .before = disp) #> Error in `transmute()`: #> ! The `.before` argument is not supported. transmute(mtcars, cyl2 = cyl, .after = disp) #> Error in `transmute()`: #> ! The `.after` argument is not supported.

    三个错误都满足以下共同特征:

    1. 错误前缀统一为Error in \transmute()`:`,指明错误发生在哪个动词内部;
    2. 消息格式统一为The \<参数名>` argument is not supported.`,明确告诉用户该参数不被当前动词支持;
    3. 错误类型为<error/rlang_error>,由 rlang 的abort()抛出,属于标准的 rlang 条件对象。

    对应测试位于 tests/testthat/test-transmute.R,使用expect_snapshot()将上述三条错误锁定为回归快照——任何对错误消息措辞的意外改动都会触发测试失败,从而保证面向用户的错误文本长期稳定。

    报错机制源码剖析:check_transmute_args()

    这三条错误并非散落在各处,而是统一由 R/transmute.R 中的check_transmute_args()辅助函数集中产生:

    check_transmute_args <- function( ..., .keep, .before, .after, error_call = caller_env() ) { if (!missing(.keep)) { abort("The `.keep` argument is not supported.", call = error_call) } if (!missing(.before)) { abort("The `.before` argument is not supported.", call = error_call) } if (!missing(.after)) { abort("The `.after` argument is not supported.", call = error_call) } enquos(...) }

    关键设计点可以从源码结构中推断:

    • 形参先声明、运行时检查:.keep、.before、.after虽然被声明为形式参数,但在transmute()的用户可见签名transmute(.data, ...)中并不出现。check_transmute_args()通过...捕获这些命名参数,再以missing()判断用户是否显式传入;
    • 遇错即抛:三个参数按.keep→.before→.after的顺序逐一检查,只要命中第一个就立刻abort(),因此一次调用最多只报出一个参数错误,不会同时列出全部问题;
    • 错误信息定位到调用点:abort()的call被设置为error_call(默认取caller_env()),所以错误提示会正确指向用户调用transmute()的位置,而不是内部辅助函数。

    为什么必须拒绝这三个参数

    从设计意图看,这属于"被取代函数的 API 冻结"策略:

    • .keep、.before、.after是mutate()在 dplyr 1.1.0 中转入 stable 的核心参数(见 NEWS.md);
    • 既然transmute()的官方替代品mutate(.keep = "none")已经完整覆盖其功能,再为transmute()追加这些参数只会造成两套 API 长期并行维护;
    • 因此在 R/transmute.R 中还有一条注释点明设计取舍:"我们不对transmute()暴露.by,因为它已经被 superseded"——同样的理由适用于.keep、.before、.after。

    如果你需要在transmute()中控制列保留或新列位置,官方给出的答案不是"想办法传参",而是切换到mutate()。

    transmute() 的完整实现流程

    在参数检查通过之后,transmute.data.frame()(R/transmute.R)的执行流程可分为五步:

    1. 收集 quosure:dplyr_quosures(!!!dots)将...中捕获的表达式转换为惰性求值的 quosure 列表;
    2. 计算分组上下文:调用compute_by(by = NULL, data = .data)。注意这里显式传入by = NULL——transmute()不暴露.by参数,分组信息完全来自数据框自身的分组属性(如group_by()的结果);
    3. 计算新列:mutate_cols(.data, dots, by)在数据掩码中逐表达式求值,返回新列列表;
    4. 应用列修改:dplyr_col_modify(.data, cols)将计算结果写回数据框;随后compact_null(cols)剔除值为NULL的列——被置空(= NULL)的列既不出现在输出中,也不会被误判为"新建列";
    5. 列选择与排序:
    cols_expr <- names(cols) # 表达式列,按 ... 中的出现顺序 cols_group <- by$names # 分组变量 cols_group <- setdiff(cols_group, cols_expr) cols_retain <- c(cols_group, cols_expr) # 组列在前,表达式列随后 dplyr_col_select(out, cols_retain)

    这解释了transmute()输出列的两个排序规则:未修改的分组列总是排在最前,然后是按照...中书写顺序排列的表达式列。

    与 mutate(.keep = "none") 的差异:列排序语义

    这是迁移时最容易踩坑的地方。表面上看mutate(.keep = "none")与transmute()都会"只保留计算结果",但二者在列排序上并不一致。

    根据 NEWS.md 的 dplyr 1.0.8 变更记录:

    mutate(.keep = "none")已不再与transmute()完全相同。transmute()本身未变,它完全忽略输入数据的列顺序,仅依据...中表达式的书写顺序;而mutate(.keep = "none")则被调整为保证输入中已有的列从不被移动,与其他.keep选项的行为保持一致。

    举例来说(对应 tests/testthat/test-transmute.R 中的回归测试):

    df <- tibble(x = 1:3, y = 4:6) transmute(df, x, z = x + 1, y) # 列顺序:x, z, y(严格按 ... 书写顺序) mutate(df, x, z = x + 1, y, .keep = "none") # mutate() 语义下 x、y 是输入中已有的列,会保持在原始位置: # 列顺序:x, y, z

    如果你只关心"删掉多余列"而希望保留原有列顺序,mutate(.keep = "none")更符合直觉;如果你依赖transmute()的"表达式即顺序"语义,迁移后需要显式调整列序(例如配合relocate())。

    快照测试之外:transmute() 的其余行为契约

    错误消息快照只是transmute()测试套件的一部分。tests/testthat/test-transmute.R 中的其他用例同样值得留意,它们佐证了 man/transmute.Rd 中定义的返回值契约:

    • 空 transmute 返回分组变量:transmute()无参数时,未分组数据返回零列(df[integer()]),分组数据返回仅含分组列的 tibble(见测试"transmute with no args returns grouping vars");
    • NULL删除列不产生警告:transmute(df, y = x + 1, z = y * 2, y = NULL)静默删除y,不会像某些改写场景那样警告;
    • 自动拼接(auto splicing):transmute(iris, tibble(Sepal.Length, Sepal.Width))可将一个 tibble 值自动展开为多列,等价于select(iris, Sepal.Length, Sepal.Width);
    • 非句法名分组变量保留:group_by(tibble(\a b` = 1L), `a b`) |> transmute()输出中仍保留名为a b` 的列(对应 issue #1138);
    • 组变量被改写时重组:transmute(gf, x = 1)会重算分组(分组数变为 1),而transmute(gf, z = 1)不改动组变量、分组结构保持不变;
    • 支持.data代词:transmute(mtcars, .data$cyl)与transmute(mtcars, cyl)等价(对应 issue #2715)。

    同时,...中每个值允许的类型(来自 man/transmute.Rd)包括:长度为 1 的向量(自动回收到正确长度)、与当前组等长的向量、NULL(删除列)、以及数据框 / tibble(展开为多列)。输出行数不受影响,若分组变量被改写则重组分组,数据框属性原样保留。

    迁移实践:从 transmute() 到 mutate()

    综合以上分析,推荐迁移路径如下:

    1. 单纯替换:将transmute(df, new1 = ..., new2 = ...)改写为mutate(df, new1 = ..., new2 = ..., .keep = "none");
    2. 核对列顺序:迁移后检查输出列顺序是否符合预期。若依赖原transmute()的"表达式书写顺序",且新旧列名相同,可用relocate()或调整mutate()中表达式次序来复现;
    3. 保留分组语义:transmute()不支持.by,若原本依赖数据框已有的group_by()分组,迁移后同样沿用group_by()即可;若原本希望临时分组计算,则直接使用mutate()的.by参数(这是transmute()无法提供的新能力);
    4. 利用新参数:迁移后即可免费获得.keep、.before、.after的完整控制能力——例如mutate(df, new = ..., .keep = "used", .before = 1)同时控制列保留与位置。

    若因历史原因暂时无法迁移,也请记住:transmute()每次调用都会触发 superseded 生命周期信号(其正式废弃可能在未来大版本发生),且不要向它传入.keep、.before、.after——这三个参数会立即抛出The \...` argument is not supported.` 错误,这正是 tests/testthat/_snaps/transmute.md 所锁定的行为契约。

    • 数据分析

    【免费下载链接】dplyr

    dplyr: A grammar of data manipulation

    项目地址:https://gitcode.com/gh_mirrors/dp/dplyr
    点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进