在dplyr::summarize()函数中设置向量属性会产生什么影响?

7

我最近遇到了一些奇怪的 dplyr 行为,其中 summarize 会不停地引用上一个组中的对象。

这里有一个简单的可重现示例来说明这种令人惊讶的行为:

library(dplyr, warn.conflicts = FALSE)
tibble(x = rep(letters[1:3], times = 4),
       y = rnorm(12)) %>%
  group_by(x) %>%
  summarize(z1 = sum(y),
            z2 = {
              attr(y, "test") <- "test"
              sum(y)
            })
#> # A tibble: 3 × 3
#>   x         z1    z2
#>   <chr>  <dbl> <dbl>
#> 1 a      0.602 0.602
#> 2 b      1.22  0.602
#> 3 c     -0.310 0.602

reprex package(v2.0.1)于2022-10-31创建

我期望z1z2是相同的。 我不明白为什么给向量y设置属性意味着在后面的迭代中,“正确”的元素引用被隐藏了。

这个问题可以通过在最后一行使用sum(.data $ y)轻松解决,但我想了解summarize非标准评估中的作用域规则。 如果有帮助文档或解释指向tidyverse非标准评估框架中当前行为有意义的任何指针,都将不胜感激。


我正在使用R 4.1.1和dplyr 1.0.7。


我同意;我目前的理论是设置属性以某种方式意味着在dplyr用于评估summarize内容的任何环境中创建了y的新副本。我希望更好地理解这些环境如何协作,以避免将来出现微妙的错误。 - const-ae
非常有趣 - 我发现 this_y <<- y 给出的 y 是相同的,无论在调整属性之前还是之后分配了 y - Captain Hat
注意:在这种情况下,magrittr::set_attr() 的行为与预期相同。 - Captain Hat
这绝对与 y 是分组变量有关 - 将 y 分配给其他内容并更改其属性会产生预期的结果。 - Captain Hat
1
我已经删除了我的评论,因为我的初步怀疑是错误的,而且Allan Cameron非常好地展示了正在发生的事情。我唯一要补充的是,避免这种错误的最佳方法不是在应用分组后在管道中将整个数据框的列赋值给花括号 - 我认为这本身就是一个代码异味... - SamR
1个回答

4
这是一个与作用域相关的问题。如果你在summarize内部写入变量y,那么你数据中的y变量的第一组将被复制到一个名为y的本地变量中,该变量与数据框中的y不同。因为它是一个本地变量,所以它在搜索路径上出现在传递的数据框中的y之前。由于相同的环境用于summarize内后续组的计算,因此该本地变量对于每个组都存在。

我们可以通过以下方式看到这一点:

library(dplyr, warn.conflicts = FALSE)

set.seed(1)

tibble(x = rep(letters[1:3], times = 4),
       y = rnorm(12)) %>%
  group_by(x) %>% 
  summarize(z1 = sum(y),
            z2 = {
              y <- y
              sum(y)
            }) 
#> # A tibble: 3 x 3
#>   x         z1    z2
#>   <chr>  <dbl> <dbl>
#> 1 a      1.15   1.15
#> 2 b      2.76   1.15
#> 3 c     -0.690  1.15

只要我们从本地帧中删除y变量的本地副本,这种情况就不会发生:
library(dplyr, warn.conflicts = FALSE)

set.seed(1)

tibble(x = rep(letters[1:3], times = 4),
       y = rnorm(12)) %>%
  group_by(x) %>% 
  summarize(z1 = sum(y),
            z2 = {
              attr(y, "test") <- "test"
              x <- sum(y)
              rm(y)
              x
            }) 
#> # A tibble: 3 x 3
#>   x         z1     z2
#>   <chr>  <dbl>  <dbl>
#> 1 a      1.15   1.15 
#> 2 b      2.76   2.76 
#> 3 c     -0.690 -0.690

或者更好的方法是,不要使用与数据框中变量同名的本地变量进行写入:

tibble(x = rep(letters[1:3], times = 4),
       y = rnorm(12)) %>%
  group_by(x) %>% 
  summarize(z1 = sum(y),
            z2 = {
              new_y <- y
              attr(new_y, "test") <- "test"
              sum(new_y)
            }) 
#> # A tibble: 3 x 3
#>   x         z1     z2
#>   <chr>  <dbl>  <dbl>
#> 1 a      1.15   1.15 
#> 2 b      2.76   2.76 
#> 3 c     -0.690 -0.690

使用 reprex v2.0.2 于2022年10月31日创建


谢谢,y 变成局部变量的点很有道理。我只是惊讶于 dplyr 不会自动清理环境 / 在 summarize 中使用预期的作用域。 - const-ae
1
@const-ae 那将是一种非常糟糕的工作方式。如果您的工作空间中有一个名为 “x” 的变量,那么运行 任何 使用临时变量名为 “x”(像许多函数一样)的函数都会更改您的工作空间中的x,无论您是否想要这样做。您需要确保任何运行的函数都不使用存在于您的工作空间中的变量名称,这将容易出错且不实用。R是一种函数式语言,用户不希望函数有任何此类副作用。 - Allan Cameron
2
@const-ae 你可以将 summarize 函数内的花括号看作是循环语句周围的花括号,而不是函数周围的花括号。函数有自己的评估框架,并且与调用框架隔离,但它可以访问它。循环在同一评估框架中多次运行相同的代码(因此可能会覆盖变量),这就是你在 summarize 中的代码所做的。就好像你认为它应该像具有对 y 的访问权限的匿名函数一样运行,但事实并非如此。 - Allan Cameron
2
@const-ae 我不知道有关summarise内部作用域的任何文档,但是summarise的参数被捕获为_quosures_,也就是说,附带环境的表达式。这项工作主要在dplyr:::summarise_cols中完成,在此处,如果您跟踪逻辑,您将看到您的代码确实在相同的环境中循环运行。 - Allan Cameron
2
@const-ae 对于“为什么?”我不确定,但对于我作为一名常规的R用户来说,这是我期望的行为。如果想要避免名称冲突,可以运行一个匿名函数,即 z2 = (function(){attr(y, 'test') <- 'test'; sum(y)})(),这也符合我在R中期望的行为。 - Allan Cameron
显示剩余4条评论

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