我有遗留代码,它大量使用 dplyr 以编程方式指定的变量进行分组,这种方式目前已被弃用或取代。下面给出了可重现的示例。我想用一个稳定的选项更新此代码,以确保它继续适用于未来版本的 dplyr。似乎有几种替代方法可以证明在简单情况下产生与原始代码相同的结果,但我想知道这些方法在边缘情况下是否真正等效。跳过早年需要使用 quo、enquo、sym、!!、!!! 等来解决使用 NSE 编程的挑战,第一个例子是group_by_(),如下所示:

library(dplyr)
Var1 <- "gear"
Var2 <- "cyl"

test1 <- mtcars %>% 
group_by_(Var1, Var2) %>% 
summarise(Mean_mpg = mean(mpg))

这工作正常,并且似乎仍然如此,但出现了group_by_()在 dplyr 0.7.0 中已弃用的警告。

一些遗留代码中使用的下一个选项是:

test2 <- mtcars %>% 
group_by_at(c(Var1, Var2)) %>% 
summarise(Mean_mpg = mean(mpg))

这也运行正常,但文档将其列为已取代,并建议使用 across()。遵循该建议:

test3 <- mtcars %>% 
group_by(across(c(Var1, Var2))) %>% 
summarise(Mean_mpg = mean(mpg))

这有效,但是会给出警告:“在 tidyselect 1.1.0 中,在选择中使用外部向量已被弃用。ℹ请使用all_of()any_of()代替。”

接受这个建议(也许应该表述为“以及”而不是“相反”?):

test4 <- mtcars %>% 
group_by(across(all_of(c(Var1, Var2)))) %>% 
summarise(Mean_mpg = mean(mpg))

“使用 dplyr 进行编程”小插图介绍了另一种方法:

test5 <- mtcars %>% 
group_by(across(c({{Var1}}, {{Var2}}))) %>% 
summarise(Mean_mpg = mean(mpg))

对于这个简单情况,所有这五个在 dplyr 版本 1.1.4 中都给出了相同的结果:

sapply(list(test2, test3, test4, test5), identical, test1)

我理解等across()有广泛的其他用途,但仅仅为了将少量变量传递给分组函数,是否有特定的底层原因(性能、错误捕获等)意味着 test1 和 test2 中形式的工作生产代码应该更新,如果是的话,最新的首选形式是什么?换句话说,是:

group_by_(Var1, Var2)

相同于:

group_by(across(all_of(c(Var1, Var2))))

此外,我知道这是一个无法明确回答的问题,但是否有人能了解这些问题会持续多久group_by_()group_by_at()即包含这些问题的遗留代码什么时候会开始失效?

4

  • 1
    您的问题应该直接向 dplyr 的程序员提出,在这种情况下,他们的 github 页面似乎是更合适的平台。


    – 


  • 2
    在您的用例中,您不需要group_by(),您只需添加一个.by =内部summarise()即可返回您想要的结果。而且您也不需要across()。所以mtcars |> summarise(Mean_mpg = mean(mpg), .by = all_of(c(Var1, Var2)))有效。


    – 


  • @LTyrone 这是个非常好的建议。你能把它作为答案吗?我看到这.bysummarise实验性的,所以目前可能还不是生产代码的好主意。不过,我可能会坚持认为它被归类为稳定的,因为它比三重嵌套等更简洁、更直观,group_by(across(all_of(c(Var1, Var2))))而且还有其他优点。我看到它来自 data.table;复制一个好主意没有错!


    – 

  • 根据 @G.Grothendieck 的回答,我在 Tidyverse 博客中发现了这一点:“我们还没有弃用不提供 .fns 的 across() 方法,但是我们计划在将来弃用,因为现在 pick() 是更好的选择”。似乎问题中列出的所有方法都已被弃用、取代或计划弃用。


    – 


最佳答案
2

在您的用例中,您不需要,您可以在里面group_by()添加。另外,您也不需要。所以这是一个选择:.by =summarise()across()

library(dplyr)

Var1 <- "gear"
Var2 <- "cyl"

mtcars |>
  summarise(Mean_mpg = mean(mpg), .by = all_of(c(Var1, Var2)))

#   gear cyl Mean_mpg
# 1    4   6   19.750
# 2    4   4   26.925
# 3    3   6   19.750
# 4    3   8   15.050
# 5    3   4   21.500
# 6    5   4   28.200
# 7    5   8   15.400
# 8    5   6   19.700

1

  • 正如问题评论中提到的,.by在撰写本文时仍处于实验阶段,因此对于生产代码来说还为时过早。一旦稳定下来,这将是一个具有可变分组列的分组摘要的绝佳解决方案,希望这能成为 dplyr 中的长期选择。


    – 

在一位评论者指出问题后,已经对此进行了修改。

我的理解是 group_by_ 已被弃用,因此将会消失。group_by_at 已被取代(并未弃用),因此它不会消失,但 dplyr 作者不太喜欢它。

尽管我们在上面的声明中已将警告设置为始终打开,但这些并不会导致错误或警告options

library(dplyr)
packageVersion("dplyr")      # 1.1.4
packageVersion("tidyselect") # 1.2.1
options(lifecycle_verbosity = "warning")  # force warnings

Var1 <- "cyl"; Var2 <- "gear"
mtcars %>% group_by(pick(any_of(c(Var1, Var2))))
mtcars %>% group_by(.data[[Var1]], .data[[Var2]])

如果代码在函数中,则

f1 <- function(data, Var1, Var2) data %>% group_by(pick(any_of(c(Var1, Var2))))
Var1 <- "cyl"; Var2 <- "gear"; f1(mtcars, Var1, Var2)

f2 <- function(data, Var1, Var2) data %>% group_by(.data[[Var1]], .data[[Var2]])
Var1 <- "cyl"; Var2 <- "gear"; f2(mtcars, Var1, Var2)

要理解的关键是pick使用 tidy-select 作为其参数(但group_by使用与 tidy-select 不同的数据屏蔽),因此请仔细阅读,因为所有使用的动词都以相同的方式工作。另请参阅小插图。

此外mutate,、、summary支持tidy-select 参数,并且函数支持类似的(无点)tidy-select 参数。这些可以代替单个语句使用。但是,请注意,即使… 相同,.by=… 和 group_by(pick(…)) 的结果也可能不 100% 相同,因为输出行的顺序可能不同。reframefilterslice.by=slice_*by=group_by(pick(...))

3

  • 快到了。group_by(!!Var1, !!Var2) 和 group_by({{Var1}}, {{Var2}}) 不会给出错误,但也不会给出正确答案。该选项group_by(.data[[Var1]], .data[[Var2]])简单、直观、工作正常,并且看起来是长期稳定解决方案的不错选择。pick 解决方案有效,尽管 dplyr 1.1.4 pick 参考文档指出:“您无法选择分组列,因为它们已经由动词自动处理(即 summarise() 或 mutate())。”此外,显然有计划across()在未提供 .fns 的情况下弃用(如本例)。


    – 

  • 已删除第三和第四个例子。 across从来没有出现在任何版本的答案中,也没有pick应用于已经分组的变量,所以我认为该评论只是为了指出读者可能希望知道的局限性。


    – 

  • 我认为我误解了“您不能选择分组列”这句话,它意味着您不能使用pick()来选择分组列(即在 中group_by()),而显然它意味着您不能pick()在已经分组的列上使用。我觉得有点模棱两可。对 across() 的引用只是表示,这在原始问题中所有未弃用、未取代的选项中都有,但现在似乎也计划弃用!我已将其作为对问题的评论添加。


    –