使用准引用将参数列表传递给函数

4

我正在尝试在R中编写一个函数,根据分组变量对数据帧进行汇总。分组变量以列表形式给出并传递给group_by_at,我希望将它们参数化。

我现在做的是这样的:

library(tidyverse)

d = tribble(
  ~foo, ~bar, ~baz,
  1, 2, 3,
  1, 3, 5
  4, 5, 6,
  4, 5, 1
)

sum_fun <- function(df, group_vars, sum_var) {
  sum_var = enquo(sum_var)
  return(
    df %>% 
      group_by_at(.vars = group_vars) %>% 
      summarize(sum(!! sum_var))
  )
}

d %>% sum_fun(group_vars = c("foo", "bar"), baz)

然而,我希望这样调用函数:
d %>% sum_fun(group_vars = c(foo, bar), baz)

这意味着分组变量不应在调用中进行评估,而应在函数中进行评估。我该如何重写该函数以实现此功能?
我尝试使用 `enquo` 来处理汇总变量,然后将 `group_vars` 替换为 `!! group_vars`,但会导致以下错误:
Error in !group_vars : invalid argument type

使用group_by(!!!group_vars)会产生以下结果:

Column `c(foo, bar)` must be length 2 (the number of rows) or one, not 4 

如何适当地重写该函数?

3个回答

9

我会建议使用vars来做引用。以下是使用mtcars数据集的例子。

library(tidyverse)

sum_fun <- function(.data, .summary_var, .group_vars) {
  summary_var <- enquo(.summary_var)

  .data %>%
    group_by_at(.group_vars) %>%
    summarise(mean = mean(!!summary_var))
}

sum_fun(mtcars, disp, .group_vars = vars(cyl, am))
#> # A tibble: 6 x 3
#> # Groups:   cyl [?]
#>     cyl    am  mean
#>   <dbl> <dbl> <dbl>
#> 1     4     0 136. 
#> 2     4     1  93.6
#> 3     6     0 205. 
#> 4     6     1 155  
#> 5     8     0 358. 
#> 6     8     1 326

您还可以用...(点-点-点)替换.group_vars

sum_fun2 <- function(.data, .summary_var, ...) {
  summary_var <- enquo(.summary_var)

  .data %>%
    group_by_at(...) %>%  # Forward `...`
    summarise(mean = mean(!!summary_var))
}

sum_fun2(mtcars, disp, vars(cyl, am))
#> # A tibble: 6 x 3
#> # Groups:   cyl [?]
#>     cyl    am  mean
#>   <dbl> <dbl> <dbl>
#> 1     4     0 136. 
#> 2     4     1  93.6
#> 3     6     0 205. 
#> 4     6     1 155  
#> 5     8     0 358. 
#> 6     8     1 326

如果您喜欢将输入作为列列表提供,则需要使用 enquos 函数来对 ...

sum_fun3 <- function(.data, .summary_var, ...) {
  summary_var <- enquo(.summary_var)

  group_var <- enquos(...)
  print(group_var)

  .data %>%
      group_by_at(group_var) %>% 
      summarise(mean = mean(!!summary_var))
}

sum_fun3(mtcars, disp, c(cyl, am))
#> [[1]]
#> <quosure>
#>   expr: ^c(cyl, am)
#>   env:  global
#> 
#> # A tibble: 6 x 3
#> # Groups:   cyl [?]
#>     cyl    am  mean
#>   <dbl> <dbl> <dbl>
#> 1     4     0 136. 
#> 2     4     1  93.6
#> 3     6     0 205. 
#> 4     6     1 155  
#> 5     8     0 358. 
#> 6     8     1 326

编辑:在.../.group_var后添加一个.addi_var

sum_fun4 <- function(.data, .summary_var, .addi_var, .group_vars) {
  summary_var <- enquo(.summary_var)

  .data %>%
    group_by_at(c(.group_vars, .addi_var)) %>%
    summarise(mean = mean(!!summary_var))
}

sum_fun4(mtcars, disp, .addi_var = vars(gear), .group_vars = vars(cyl, am))
#> # A tibble: 10 x 4
#> # Groups:   cyl, am [?]
#>      cyl    am  gear  mean
#>    <dbl> <dbl> <dbl> <dbl>
#>  1     4     0     3 120. 
#>  2     4     0     4 144. 
#>  3     4     1     4  88.9
#>  4     4     1     5 108. 
#>  5     6     0     3 242. 
#>  6     6     0     4 168. 
#>  7     6     1     4 160  
#>  8     6     1     5 145  
#>  9     8     0     3 358. 
#> 10     8     1     5 326

group_by_at()也可以接受列名的字符向量作为输入

sum_fun5 <- function(.data, .summary_var, .addi_var, ...) {

  summary_var <- enquo(.summary_var)
  addi_var    <- enquo(.addi_var)
  group_var   <- enquos(...)

  ### convert quosures to strings for `group_by_at`
  all_group <- purrr::map_chr(c(addi_var, group_var), quo_name)

  .data %>%
    group_by_at(all_group) %>% 
    summarise(mean = mean(!!summary_var))
}

sum_fun5(mtcars, disp, gear, cyl, am)
#> # A tibble: 10 x 4
#> # Groups:   gear, cyl [?]
#>     gear   cyl    am  mean
#>    <dbl> <dbl> <dbl> <dbl>
#>  1     3     4     0 120. 
#>  2     3     6     0 242. 
#>  3     3     8     0 358. 
#>  4     4     4     0 144. 
#>  5     4     4     1  88.9
#>  6     4     6     0 168. 
#>  7     4     6     1 160  
#>  8     5     4     1 108. 
#>  9     5     6     1 145  
#> 10     5     8     1 326

此内容创建于2018年10月09日,采用reprex包(v0.2.1.9000)。


1
这看起来更符合 tidyverse 的风格了 - 谢谢! - slhck
快速跟进:如果在我的 sum_fun 中,我想要有一个名为 .additional_var 的额外参数,它会在 group_by_at 调用中附加到 .group_vars 中,该怎么办? - slhck
@slhck: 请看我的编辑。这次我们需要稍微不同的方法来做。 - Tung
1
哦,有趣的方法 - 在R中总是有一千种方法来做事情。我刚刚发布了自己的问题-答案对,针对我发现的另一种方法:https://stackoverflow.com/questions/52736118/adding-column-names-to-vars-inside-a-dplyr-function/52736119#52736119 - slhck
@slhck:不错!谢谢分享! - Tung
1
我更改了被接受的答案,采用了更“现代”的dplyr方法(并将积分授予新贡献者以鼓励)。希望你能理解。 - slhck

3
您可以使用省略号...。以下是一个例子:
sum_fun <- function(df, sum_var, ...) {
  sum_var <- substitute(sum_var)
  grps    <- substitute(list(...))[-1L]
  return(
    df %>% 
      group_by_at(.vars = as.character(grps)) %>% 
      summarize(sum(!! sum_var))
  )
}

d %>% sum_fun(baz, foo, bar)

我们使用附加参数并将它们创建成一个列表。然后,我们使用非标准评估(substitute)来获取变量名并防止R对其进行评估。由于group_by_at期望字符或数字类型的对象,因此我们只需将名称向量转换为字符向量即可使函数得到预期的评估。
> d %>% sum_fun(baz, foo, bar)
# A tibble: 3 x 3
# Groups:   foo [?]
    foo   bar `sum(baz)`
  <dbl> <dbl>      <dbl>
1     1     2          3
2     1     3          5
3     4     5          7

如果你不想把分组变量作为其他参数提供,那么你当然可以使用命名参数:

sum_fun <- function(df, sum_var, grps) {
  sum_var <- enquo(sum_var)
  grps    <- as.list(substitute(grps))[-1L]
  return(
    df %>% 
      group_by_at(.vars = as.character(grps)) %>% 
      summarize(sum(!! sum_var))
  )
}

sum_fun(mtcars, sum_var = hp, grps = c(cyl, gear))

我使用substitute的原因是它能轻松地将表达式list(cyl, gear)分解成其组件。可能有一种使用rlang的方法,但我目前还没有深入研究过这个包。


谢谢你的回答。问题是,我在函数中有其他参数,或者可能会有额外的参数。我猜如果那是唯一的选择,我可以移动参数。substituteenquo之间有什么区别吗? - slhck
更新了我的答案。 - Martin Schmelzer

3
你可以使用 dplyr::group_by()dplyr::across()大括号大括号 {{ 的组合重写该函数。该方法适用于dplyr 1.0.0版本及以上。我已经编辑了原始示例和代码,以提高清晰度。
library(tidyverse)

my_data <- tribble(
  ~foo, ~bar, ~baz,
   "A",  "B",    3,
   "A",  "C",    5,
   "D",  "E",    6,
   "D",  "E",    1
)

sum_fun <- function(.data, group, sum_var) {
    .data %>% 
      group_by(across({{ group }})) %>% 
      summarize("sum_{{sum_var}}" := sum({{ sum_var }}))
}

sum_fun(my_data, group = c(foo, bar), sum_var = baz)
#> `summarise()` has grouped output by 'foo'. You can override using the `.groups` argument.
#> # A tibble: 3 x 3
#> # Groups:   foo [2]
#>   foo   bar   sum_baz
#>   <chr> <chr>   <dbl>
#> 1 A     B           3
#> 2 A     C           5
#> 3 D     E           7

这段内容是由 reprex package (v2.0.0) 创建于2021年9月6日。


这样会更加简洁!如果您能提及在哪个版本的dplyr中可以使用,那就太好了。(我的后续问答也可以从这种简化中受益:https://stackoverflow.com/a/52736119/435093) - slhck
已编辑!回答了你的后续问题并进行了简化。 - Michael McCarthy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接