当库函数使用非标准评估时,重构R代码

5

我有一些像这样的 R 代码:

library(dplyr)
library(datasets)

iris %.% group_by(Species) %.% filter(rank(Petal.Length, ties.method = 'random')<=2) %.% ungroup()

给予:

Source: local data frame [6 x 5]

  Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
1          4.3         3.0          1.1         0.1     setosa
2          4.6         3.6          1.0         0.2     setosa
3          5.0         2.3          3.3         1.0 versicolor
4          5.1         2.5          3.0         1.1 versicolor
5          4.9         2.5          4.5         1.7  virginica
6          6.0         3.0          4.8         1.8  virginica

这个操作按照物种分组,并且在每个分组中只保留两个花瓣长度最短的数据。由于我需要对不同的列和数量进行相同的操作,所以我的代码存在一些重复。例如:

iris %.% group_by(Species) %.% filter(rank(Petal.Length, ties.method = 'random')<=2) %.% ungroup()
iris %.% group_by(Species) %.% filter(rank(-Petal.Length, ties.method = 'random')<=2) %.% ungroup()
iris %.% group_by(Species) %.% filter(rank(Petal.Width, ties.method = 'random')<=3) %.% ungroup()
iris %.% group_by(Species) %.% filter(rank(-Petal.Width, ties.method = 'random')<=3) %.% ungroup()

我希望将此内容提取到一个函数中。朴素的方法不起作用:
keep_min_n_by_species <- function(expr, n) {
  iris %.% group_by(Species) %.% filter(rank(expr, ties.method = 'random') <= n) %.% ungroup()
}

keep_min_n_by_species(Petal.Width, 2)

Error in filter_impl(.data, dots(...), environment()) : 
  object 'Petal.Width' not found 

据我所知,表达式rank(Petal.Length, ties.method = 'random') <= 2在由filter函数引入的不同上下文中进行评估,该函数为Petal.Length表达式提供了一种含义。我不能只是用一个变量替换Petal.Length,因为它将在错误的上下文中评估。我尝试使用不同组合的substituteeval,阅读过这个页面:非标准评估。我无法找到适当的组合。我认为问题可能是我不仅想从调用者(Petal.Length)传递一个表达式给filter去评估-我想构造一个新的更大的表达式(rank(Petal.Length, ties.method = 'random') <= 2),然后将整个表达式传递给filter去评估。

  1. 如何将表达式重构为函数?
  2. 更一般地说,如何将R表达式提取到函数中?
  3. 更一般地说,我的思路是否有误?在我熟悉的其他主流语言中(e.g. Python, C++, C#),这是一个相对简单的操作,我经常希望通过它来消除代码中的重复。在R中,非标准评估似乎使它成为一个非常不明显的操作(至少对我来说)。我应该完全做其他事情吗?

http://adv-r.had.co.nz/Computing-on-the-language.html - James
1
我相信Hadley正在使用lazyeval包处理这个问题,该包将提供通用框架,以在其他包中实现标准版本的NSE函数。 - baptiste
2个回答

6

dplyr版本0.3开始使用lazyeval包来解决这个问题,正如@baptiste所提到的,并且还有一组新的函数使用标准评估(与NSE版本具有相同的函数名称,但结尾为_)。这里有一个介绍:https://github.com/hadley/dplyr/blob/master/vignettes/nse.Rmd

话虽如此,我不知道你正在尝试做什么的最佳实践(虽然我也在尝试做同样的事情)。我有一些可行的东西,但像我说的那样,我不知道它是否是最好的方法。请注意使用filter_()而不是filter(),并将参数作为引用的字符字符串传递:

devtools::install_github("hadley/dplyr")
devtools::install_github("hadley/lazyeval")

library(dplyr)
library(lazyeval)

keep_min_n_by_species <- function(expr, n, rev = FALSE) {
  iris %>% 
    group_by(Species) %>% 
    filter_(interp(~rank(if (rev) -x else x, ties.method = 'random') <= y, # filter_, not filter
                   x = as.name(expr), y = n)) %>% 
    ungroup()
}

keep_min_n_by_species("Petal.Width", 3) # "Petal.Width" as character string
keep_min_n_by_species("Petal.Width", 3, rev = TRUE)

根据@hadley的评论更新:

keep_min_n_by_species <- function(expr, n) {
  expr <- lazy(expr)

  formula <- interp(~rank(x, ties.method = 'random') <= y,
                    x = expr, y = n)

  iris %>% 
    group_by(Species) %>% 
    filter_(formula) %>% 
    ungroup()
}

keep_min_n_by_species(Petal.Width, 3)
keep_min_n_by_species(-Petal.Width, 3)

1
我会做 expr <- lazyeval::lazy(expr) (所以你不需要引用它),并在 filter_() 调用外部创建公式。 - hadley
啊,非常好-谢谢!我已经更新了我的回答,并且加入了我认为你所指的内容。 - andyteucher
1
完美 :) 这正是我会做的方式。 - hadley
我想我之前错过了编辑。现在我将这个答案设为已接受,因为编辑看起来更容易理解、记忆,并且得到了 Hadley 的认可。 - Weeble

4
如何?
keep_min_n_by_species <- function(expr, n) {
    mc <- match.call()
    fx <- bquote(rank(.(mc$expr), ties.method = 'random') <= .(mc$n))
    iris %.% group_by(Species) %.% filter(fx) %.% ungroup()
}

看起来这样可以让所有语句在没有错误的情况下运行

keep_min_n_by_species(Petal.Width, 2)
keep_min_n_by_species(-Petal.Width, 2)
keep_min_n_by_species(Petal.Width, 3)
keep_min_n_by_species(-Petal.Width, 3)

这个想法是使用 match.call() 来捕获传递给函数的未求值表达式。然后我们使用 bquote() 将过滤器构建为一个调用对象。


太好了!我有点难过,因为它似乎很深奥。如果没有更多的R编程经验,我想我不可能自己解决这个问题。如果一天左右没有更详细的答案,我会回来接受这个答案。 - Weeble
我建议避免使用match.call(),而是使用更具体的函数。在这种情况下,评估n会更安全。 - hadley

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