You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

dplyr::across()中双向/单向公式的.x替换差异及函数改造问询

问题与解答

背景介绍

我编写了一个函数formula_to_function_pair(),接收两侧均为purrr风格lambda表达式的双向公式(如f(.x) ~ g(.x)),将其转换为两个以.x为参数的函数:

library(rlang)
library(dplyr)
library(purrr)

formula_to_function_pair <- function(f) {
  f_env <- f_env(f)
  list(
    lhs_fun = new_function(args = pairlist2(.x = ,), body = f_lhs(f)),
    rhs_fun = new_function(args = pairlist2(.x = ,), body = f_rhs(f))
  )
}

# 单独调用正常工作
pair <- formula_to_function_pair(sum(.x) ~ mean(.x))
pair$lhs_fun(1:3) # 输出: 6
pair$rhs_fun(1:3) # 输出: 2

但在dplyr::across()中调用时,公式中的.x会被替换为当前列的符号(如y),导致生成的函数依赖列名而非.x:

# 生成的函数依赖y而非.x
tibble(y = 0L) %>%
  mutate(
    across(y, ~ list(formula_to_function_pair(sum(.x) ~ mean(.x))))
  ) %>%
  pull(y)
#> [[1]]
#> [[1]]$lhs_fun
#> function (.x) 
#> sum(y)
#> <environment: 0x10a6d24a8>
#> 
#> [[1]]$rhs_fun
#> function (.x) 
#> mean(y)
#> <environment: 0x10a6d24a8>

而使用单向公式~.x时,.x能被正常保留:

# 单向公式的.x未被替换
tibble(y = 0L) %>%
  mutate(
    across(
      y,
      ~ list(list(
        rlang::as_function(~sum(.x)),
        rlang::as_function(~mean(.x))
      ))
    )
  ) %>%
  pull(y)
#> [[1]]
#> [[1]][[1]]
#> <lambda>
#> function (..., .x = ..1, .y = ..2, . = ..1) 
#> sum(.x)
#> <environment: 0x10b6a53e0>
#> attr(,"class")
#> [1] "rlang_lambda_function" "function"             
#> 
#> [[1]][[2]]
#> <lambda>
#> function (..., .x = ..1, .y = ..2, . = ..1) 
#> mean(.x)
#> <environment: 0x10b6a53e0>
#> attr(,"class")
#> [1] "rlang_lambda_function" "function"

此外,若在across()的~ ...表达式中包含.y,双向公式的.x也能被保留,但这种方法不实用:

# 包含.y后.x未被替换,但方法不实用
tibble(y = 0L) %>%
  mutate(
    across(
      .cols = y,
      ~{
        quote(.y)
        list(formula_to_function_pair(sum(.x) ~ mean(.x)))
      }
    )
  ) %>%
  pull(y) %>%
  pluck(1)
#> $lhs_fun
#> function (.x) 
#> sum(.x)
#> <environment: 0x126bb86f8>
#> 
#> $rhs_fun
#> function (.x) 
#> mean(.x)
#> <environment: 0x126bb86f8>

问题1:如何修改formula_to_function_pair()以保留.x?

可以通过捕获传入公式的原始表达式,将被替换的列名符号还原为.x来实现:

formula_to_function_pair <- function(f) {
  # 捕获传入的公式表达式
  f_quo <- enquo(f)
  f_expr <- quo_get_expr(f_quo)
  
  # 提取被替换后的列符号(如y)
  col_sym <- if (is_call(f_lhs(f_expr))) {
    f_lhs(f_expr)[[2]]
  } else {
    f_lhs(f_expr)
  }
  
  # 将LHS和RHS中的列符号替换回.x
  lhs_body <- expr_replace(f_lhs(f_expr), col_sym, sym(".x"))
  rhs_body <- expr_replace(f_rhs(f_expr), col_sym, sym(".x"))
  
  list(
    lhs_fun = new_function(args = pairlist2(.x = ), body = lhs_body),
    rhs_fun = new_function(args = pairlist2(.x = ), body = rhs_body)
  )
}

测试修改后的函数:

tibble(y = 0L) %>%
  mutate(
    across(y, ~ list(formula_to_function_pair(sum(.x) ~ mean(.x))))
  ) %>%
  pull(y) %>%
  pluck(1)
#> $lhs_fun
#> function (.x) 
#> sum(.x)
#> <environment: 0x12345678>
#> 
#> $rhs_fun
#> function (.x) 
#> mean(.x)
#> <environment: 0x12345678>

该方法的核心是:利用enquo捕获未被求值的公式,识别出被across替换的列符号,再将其替换回.x,最终生成依赖.x的函数。


问题2:为何across()对不同表达式处理方式不同?

across()内部通过quo_eval_fns()处理传入的逻辑,当公式被判定为可内联公式时,会自动将.x/.替换为当前列的符号。而判定逻辑由is_inlinable_formula()实现:

  • 该函数会检查表达式中是否包含特定符号(如.y、..1、..2等),如果包含这些符号,公式会被判定为不可内联,不会触发.x的替换;
  • ~.x:属于单向lambda公式,across对其处理逻辑特殊,不会替换.x为列名;
  • .x ~ .x:双向公式且未包含上述特殊符号,被判定为可内联,因此.x被替换为当前列名;
  • {quote(.y); .x ~ .x}:表达式中包含.y,触发不可内联判定,.x因此被保留。

内容的提问来源于stack exchange,提问作者Ethan.S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 21:22:32