在 group_by() %>% mutate() 函数调用中使用带引号的变量。

7

可重复的例子

cats <-
  data.frame(
    name = c(letters[1:10]),
    weight = c(rnorm(5, 10, 1), rnorm(5, 20, 3)),
    type = c(rep("not_fat", 5), rep("fat", 5))
  )

get_means <- function(df, metric, group) {
  df %>%
    group_by(.[[group]]) %>%
    mutate(mean_stat = mean(.[[metric]])) %>%
    pull(mean_stat) %>%
    unique()
}

get_means(cats, metric = "weight", group = "type")

我尝试的方法

我期望得到两个值,但实际上只得到了一个值。看起来 groupby 失败了。

我尝试了包括 quo()、eval() 和 substitute()、UQ()、!! 等许多其他方法来使 group_by() 内部的内容正常工作。

这似乎非常简单,但我无法解决它。

代码背景

将变量放在引号中的决定是因为我在 ggplot aes_string() 调用中使用了它们。我在函数中排除了 ggplot 代码以简化代码,否则如果我们能够使用标准评估,那么问题就很简单了。

6个回答

7
我认为在tidyeval框架中“预期”的做法是使用名称(而不是字符串)输入参数,然后使用enquo()引用这些参数。 ggplot2了解整洁的评估运算符,因此这对于ggplot2也起作用。
首先,让我们调整您示例中的dplyr摘要函数:
library(tidyverse)
library(rlang)

get_means <- function(df, metric, group) {

  metric = enquo(metric)
  group = enquo(group)

  df %>%
    group_by(!!group) %>%
    summarise(!!paste0("mean_", as_label(metric)) := mean(!!metric))
}

get_means(cats, weight, type)
  type    mean_weight
1 fat            20.0
2 not_fat        10.2
get_means(iris, Petal.Width, Species)
  Species    mean_Petal.Width
1 setosa                0.246
2 versicolor            1.33 
3 virginica             2.03
现在加入 ggplot:
get_means <- function(df, metric, group) {

  metric = enquo(metric)
  group = enquo(group)

  df %>%
    group_by(!!group) %>%
    summarise(mean_stat = mean(!!metric)) %>% 
    ggplot(aes(!!group, mean_stat)) + 
      geom_point()
}

get_means(cats, weight, type)

enter image description here

我不确定你想要什么类型的图表,但是你可以使用整洁评估绘制数据和摘要值。例如:

plot_func = function(data, metric, group) {

  metric = enquo(metric)
  group = enquo(group)

  data %>% 
    ggplot(aes(!!group, !!metric)) + 
      geom_point() +
      geom_point(data=. %>% 
                   group_by(!!group) %>%
                   summarise(!!metric := mean(!!metric)),
                 shape="_", colour="red", size=8) + 
      expand_limits(y=0) +
      scale_y_continuous(expand=expand_scale(mult=c(0,0.02)))
}

plot_func(cats, weight, type)

enter image description here

提醒一下,你可以使用...参数和enquos来允许函数接受任意数量的分组变量(包括没有分组变量),而不是使用enquo(这也需要使用!!!(取消引用-拼接)而不是!!(取消引用))。

get_means <- function(df, metric, ...) {

  metric = enquo(metric)
  groups = enquos(...)

  df %>%
    group_by(!!!groups) %>%
    summarise(!!paste0("mean_", quo_text(metric)) := mean(!!metric))
}
get_means(mtcars, mpg, cyl, vs)
    cyl    vs mean_mpg
1     4     0     26  
2     4     1     26.7
3     6     0     20.6
4     6     1     19.1
5     8     0     15.1
get_means(mtcars, mpg)
  mean_mpg
1     20.1

2
不错的回答!只是请注意,在那种情况下quo_text()是不合适的。它是一个多行解析器。您可以使用as_label()as_name()代替,它们保证返回单行字符串。后者检查其输入是否为变量名而不是函数调用,在许多情况下是适当的。在这里,as_label()很好,因为您的函数接受变量的内联转换,例如,您可以传递get_means(mtcars, mpg * 100) - Lionel Henry
感谢@lionel。跟进问题:像quo_text()一样,as_label()也是一个rlang函数。我的(可能不正确的)印象是,“平均”的tidyeval用户在正常编程过程中不需要使用rlang函数。有没有一种方法可以仅使用标准tidyverse包中的函数生成动态复合列名? - eipi10
1
你说得对。我们也会在tidyverse包中导出as_label()函数。 - Lionel Henry

4
如果您想使用字符串作为名称,就像您的示例一样,正确的方法是使用sym将字符串转换为符号,并使用!!取消引用:
get_means <- function(df, metric, group) {
    df %>%
      group_by(!!sym(group)) %>%
      mutate(mean_stat = mean(!!sym(metric))) %>%
      pull(mean_stat) %>%
      unique()
}

get_means(cats, metric = "weight", group = "type")
[1] 10.06063 17.45906

如果您想在函数中使用裸名(bare names),那么请使用 enquo!!
get_means <- function(df, metric, group) {
    group <- enquo(group)
    metric <- enquo(metric)
    df %>%
      group_by(!!group) %>%
      mutate(mean_stat = mean(!!metric)) %>%
      pull(mean_stat) %>%
      unique()
}

get_means(cats, metric = weight, group = type)
[1] 10.06063 17.45906

你的例子中发生了什么?

有趣的是,.[[group]]可以用于分组,但不是你想象中的方式。它将数据框架中指定的列子集作为向量,然后将其作为新变量进行分组:

cats %>%
    group_by(.[['type']])

# A tibble: 10 x 4
# Groups:   .[["type"]] [2]
   name  weight type    `.[["type"]]`
   <fct>  <dbl> <fct>   <fct>        
 1 a       9.60 not_fat not_fat      
 2 b       8.71 not_fat not_fat      
 3 c      12.0  not_fat not_fat      
 4 d       8.48 not_fat not_fat      
 5 e      11.5  not_fat not_fat      
 6 f      17.0  fat     fat          
 7 g      20.3  fat     fat          
 8 h      17.3  fat     fat          
 9 i      15.3  fat     fat          
10 j      17.4  fat     fat  

您的问题涉及到mutate语句。而不是选择mutate(mean_stat = mean(.[['weight']])),它只是将weight列提取为向量,计算平均值,然后将单个值分配给新列。

cats %>%
    group_by(.[['type']]) %>%
      mutate(mean_stat = mean(.[['weight']]))
# A tibble: 10 x 5
# Groups:   .[["type"]] [2]
   name  weight type    `.[["type"]]` mean_stat
   <fct>  <dbl> <fct>   <fct>             <dbl>
 1 a       9.60 not_fat not_fat            13.8
 2 b       8.71 not_fat not_fat            13.8
 3 c      12.0  not_fat not_fat            13.8
 4 d       8.48 not_fat not_fat            13.8
 5 e      11.5  not_fat not_fat            13.8
 6 f      17.0  fat     fat                13.8
 7 g      20.3  fat     fat                13.8
 8 h      17.3  fat     fat                13.8
 9 i      15.3  fat     fat                13.8
10 j      17.4  fat     fat                13.8

1
何时需要使用 sym,何时不需要?例如,您可以执行 group_by(!!group),它似乎可以正常工作。 - thc
1
sym turns strings into symbols that you can use, enquo turns bare-name passed into a function into things that can be used. So if you pass "type" into the function, you need sym, but if you pass type in, use enquo - divibisan
我认为这个问题已经得到了回答。所以,在这里发表评论。最好还要展示一个可以接受带引号和不带引号参数的函数。 - akrun

3

magrittr代词.代表整个数据,所以您已经计算了所有观测值的平均值。相反,使用tidy eval代词.data来表示当前分组数据框的切片:

get_means <- function(df, metric, group) {
  df %>%
    group_by(.data[[group]]) %>%
    mutate(mean_stat = mean(.data[[metric]])) %>%
    pull(mean_stat) %>%
    unique()
}

2
我建议进行轻微修改(如果我正确理解了您想要实现的目标):
 get_means <- function(df, metric, group) {
      df %>%
        group_by(!!sym(group)) %>%
        summarise(mean_stat = mean(!!sym(metric)))%>% pull(mean_stat)
    }
    get_means(cats, "weight", "type")

[1] 20.671772  9.305811

与以下代码完全相同:

cats %>% group_by(type) %>% summarise(mean_stat=mean(weight)) %>%
  pull(mean_stat)

[1] 20.671772  9.305811

1
尽管大多数情况下是等价的,但与非引用符相比,子集.data更安全一些。这是因为.data[[col]]会检查数据框中是否存在该变量。对于不习惯整洁评估的人来说,它看起来也更简单。 - Lionel Henry

0

使用across().data{}进行重命名更新答案,并将原始函数参数保留为字符串,以符合OP的要求:

library(tidyverse)

get_means <- function(dat = mtcars, metric = "wt", group = "cyl") {
  dat %>%
    group_by(across(all_of(c(group)))) %>%
    summarise("{paste0('mean_',metric)}" := mean(.data[[metric]]), .groups="keep")
}

get_means()


请见:?dplyr_data_masking 以获取更详细的讨论。

0

使用 *_at 函数:

library(dplyr)
get_means <- function(df, metric, group) {
  df %>%
    group_by_at(group) %>%
    mutate_at(metric,list(mean_stat = mean)) %>%
    pull(mean_stat) %>%
    unique()
}

get_means(cats, metric = "weight", group = "type")
# [1] 10.12927 20.40541

数据

set.seed(1)
cats <-
  data.frame(
    name = c(letters[1:10]),
    weight = c(rnorm(5, 10, 1), rnorm(5, 20, 3)),
    type = c(rep("not_fat", 5), rep("fat", 5))
  )

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