函数的默认参数使用变量,使用dplyr

17

目标

我的目标是定义一些函数,供在dplyr动词内使用,并使用预定义的变量。这是因为我有一些需要大量参数的函数,其中许多参数总是相同的变量名。

我的理解:这很困难(也许是不可能的),因为dplyr会延迟评估用户指定的变量,但任何默认参数都不在函数调用中,因此对于dplyr是不可见的。

玩具示例

考虑以下示例,我使用dplyr计算变量是否已更改(在这种情况下没有什么意义):

library(dplyr)
mtcars  %>%
  mutate(cyl_change = cyl != lag(cyl))
现在,lag还支持如下的备用排序方式:
mtcars  %>%
  mutate(cyl_change = cyl != lag(cyl, order_by = gear))

但是如果我想创建自己的lag版本,它总是按gear排序呢?

失败的尝试

天真的方法是这样的:

lag2 <- function(x, n = 1L, order_by = gear) lag(x, n = n, order_by = order_by)

mtcars %>%
  mutate(cyl_change = cyl != lag2(cyl))

但这显然会引发错误:

未找到名为“gear”的对象

更现实的选项是这些,但它们也无法工作:

lag2 <- function(x, n = 1L) lag(x, n = n, order_by = ~gear)
lag2 <- function(x, n = 1L) lag(x, n = n, order_by = get(gear))
lag2 <- function(x, n = 1L) lag(x, n = n, order_by = getAnywhere(gear))
lag2 <- function(x, n = 1L) lag(x, n = n, order_by = lazyeval::lazy(gear))

问题

有没有办法让 lag2dplyr 操作的数据框中正确找到 gear

  • 不应该强制调用 lag2 时提供 gear
  • 可以在非 mtcars 数据集上使用 lag2(但它们必须具有 gear 作为其中之一的变量)。
  • 最好将 gear 设为函数的默认参数,这样如果需要仍然可以更改,但这并非必要。

gear是另一个向量,对吧?你没有将它传递给lag2的本地环境。尝试使用lag2 <- function(x, gear) {...}(注意,不需要写参数n)。 - alexwhitworth
向量“gear”需要在全局范围内定义或传递到函数中。可以使用以下任一方法:lag2 <- function(x) lag(x, order_by = mtcars$gear) 或按照Alex的建议重新定义您的函数:lag2 <- function(x, gear) lag(x, order_by = gear),mtcars %>% mutate(cyl_change = cyl != lag2(cyl, gear))。 - Dave2e
2
@Axeman 如果你愿意,我可以建议几种方法,几乎可以让你在data.table中达到你想要的目标,但是这些方法都不能与dplyr一起使用。 - eddi
1
@eddi 我很乐意学习,但是对于这个特定的项目,我已经非常致力于(multi)dplyr - Axeman
2
我从来没有理解人们如何会被困在使用特定的 R 库中,而这些库是专门设计用于使用大量其他库的。 - eddi
显示剩余6条评论
5个回答

10

data.table中,无论在[.data.table]的第二个参数中写了什么,都将首先由data.table软件包解析而不是常规的R解析器。可以将其视为常规语言解析器R内部的一种单独的语言解析器。该解析器会查找您使用了哪些变量实际上是您正在操作的data.table的列,并将其放入j-expression的环境中。

这意味着你必须以某种方式让这个解析器知道你将使用 gear,否则它就不会成为环境的一部分。以下是两种完成此操作的方法。

其中一种"简单"的方法是,在调用lag2时在j-expression中实际使用列名(除了在lag2内进行一些修改):

dt = as.data.table(mtcars)

lag2 = function(x) lag(x, order_by = get('gear', sys.frame(4)))

dt[, newvar := {gear; lag2(cyl)}]
# or
dt[, newvar := {.SD; lag2(cyl)}]

我个人认为这种解决方案有两个不良的属性 - 首先,我不确定 sys.frame(4) 有多脆弱 - 如果你把这个东西放在一个函数或一个包里,我不知道会发生什么。你可能可以绕过它并找到正确的框架,但这有点麻烦。其次 - 你要么必须在表达式中任何地方提及你感兴趣的特定变量,要么通过使用 .SD 将它们全部转储到环境中,同样也可以在表达式中 任何地方进行。

我更喜欢的第二个选择是利用 data.table解析器在变量查找之前在现场评估 eval表达式的事实,因此如果你在某个表达式中使用了一个变量,而你又对它进行了 eval,那么它就可以起作用:

lag3 = quote(function(x) lag(x, order_by = gear))

dt[, newvar := eval(lag3)(cyl)]

这种解决方案不会受到其他解决方案存在的问题的影响,但明显劣势是需要多输入一个eval


这让我想到也许函数也应该像eval一样“原地评估”,即整个函数表达式复制粘贴到您的表达式中,但这可能会增加疯狂的开销(基本上执行R解析器所做的所有操作,使用R函数),并且不值得。 - eddi

4
这个解决方案已经接近完成:
考虑一个稍微简单的玩具例子:
mtcars %>%
  mutate(carb2 = lag(carb, order_by = gear))

我们仍然使用lag和它的order_by参数,但不再进行任何计算。我们放弃了SE的mutate,转而使用NSE的mutate_,并使lag2构建一个函数调用作为字符向量。

lag2 <- function(x, n = 1, order_by = gear) {
  x <- deparse(substitute(x))
  order_by <- deparse(substitute(order_by))
  paste0('dplyr::lag(x = ', x, ', n = ', n, ', order_by = ', order_by, ')')
}

mtcars %>%
  mutate_(carb2 = lag2(carb))

这将给我们与上述相同的结果。

使用以下代码可以实现原始玩具示例:

mtcars %>%
  mutate_(cyl_change = paste('cyl !=', lag2(cyl)))

缺点:

  1. 我们必须使用SE mutate_
  2. 与原始示例中的扩展用法一样,我们还需要使用paste
  3. 这不是特别安全的,即gear应该来自哪里不是立即清楚的。在全局环境中为gearcarb分配值似乎没问题,但我猜想在某些情况下可能会出现意外错误。使用公式而不是字符向量会更安全,但这需要为其分配正确的环境才能运行,这对我来说仍然是个大问号。

3

这并不是很优雅,因为它需要一个额外的参数。但是,通过传递整个数据框,我们可以获得几乎所需的行为。

lag2 <- function(x, df, n = 1L, order_by = df[['gear']], ...) {
  lag(x, n = n, order_by = order_by, ...)
}

hack <- mtcars  %>%  mutate(cyl_change = cyl != lag2(cyl, .))
ans <- mtcars  %>%  mutate(cyl_change = cyl != lag(cyl, order_by = gear))
all.equal(hack, ans)
# [1] TRUE
  1. 应该可以在不提供gear的情况下调用lag2函数。

是的,但需要传递.

  1. 应该可以在不使用名为mtcars的数据集上使用lag2函数(但其中必须有gear作为变量之一)。

这个可以实现。

  1. 最好将gear作为函数的默认参数,这样如果需要仍然可以更改,但这并非关键。

这也可以实现:

hack_nondefault <- mtcars %>%  mutate(cyl_change = cyl != lag2(cyl, order_by = cyl))
ans_nondefault <- mtcars %>%  mutate(cyl_change = cyl != lag(cyl, order_by = cyl))
all.equal(hack_nondefault, ans_nondefault)
# [1] TRUE

请注意,如果您手动给出order_by,则指定带有.df不再必要,使用方式与原始的lag相同(非常好)。
补充说明:
似乎很难避免使用SE mutate_,就像OP提出的答案那样做一些简单的hackery,就像在我这里的答案中所做的那样,或者做一些涉及逆向工程的高级操作lazyeval::lazy_dots
证据:
1)dplyr::lag本身不使用任何NSE技巧
2)mutate只是调用mutate_(.data, .dots = lazyeval::lazy_dots(...))

这也是一个不错的解决方案,让我再考虑一下。传递字符向量也没有问题,因为这可以修复。 - Axeman
1
谢谢!我正在尝试学习NSE,但在注意到我的补充事实后感到有些绝望。这是一个真正的挑战。如果可以仅传递gear,请随意编辑(或评论,我会进行编辑)。我一开始不确定如何做到这一点。 - jaimedash
1
看看我的答案,那里有解决方案 :) - Axeman
1
哈!我应该再仔细阅读。所以,lag3 <- function(x, df, n = 1L, order_by=gear, ...) { order_by <- deparse(substitute(order_by)) lag(x, n = n, order_by = df[[order_by]], ...) }?但是,虽然它对第一个案例有效,但在第三个案例中会失败:mutate(cyl_change = cyl != lag3(cyl, ., order_by = cyl))cyl计算为c(6, 6, 4, 6, 8, 6, 8, 4, 4, 6, 6, 8, 8, 8, 8, 8, 8, 4, 4, 4, 4, 8, 8, 8, 8, 4, 4, 4, 8, 6, 8, 4)。 :\ - jaimedash
我明白了... 我不是很理解,但似乎“.”弄错了? - Axeman
1
好的,我现在已经修复了那个小问题(并进行了编辑)。我认为这是我能得到的最佳答案,几乎完美! - Axeman

1
你也可以通过以下方式解决问题:
library(dplyr)

lag2 <- function(df, x, n = 1L, order_by = gear) {
  order_var <- enquo(order_by)
  x <- enquo(x)
  var_name <- paste0(quo_name(x), "_change")

  df %>% 
    mutate(!!var_name := lag(!!x, n = n, order_by = !!order_var))
}

mtcars %>%
  lag2(cyl)

# A tibble: 32 x 12
#      mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb cyl_change
#    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>      <dbl>
#  1  21       6  160    110  3.9   2.62  16.5     0     1     4     4          8
#  2  21       6  160    110  3.9   2.88  17.0     0     1     4     4          6
#  3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1          6
#  4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1         NA
#  5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2          6
#  6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1          8
#  7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4          6
#  8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2          4
#  9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2          4
# 10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4          4
# ... with 22 more rows

我知道,再次需要将数据框传递给函数,但这样可以更清晰地了解期望 gear 的环境。同时,管道特性得到了良好保留,并且自动定义了新变量的名称。

评论:我相信在您首次发布此问题时,这种解决方案可能还不可用,但无论如何,将其保留在此以供将来参考可能是不错的。


1
这是我最终使用的答案。它基本上依赖于一个函数,该函数将任何默认函数值明确注入到惰性点对象的表达式中。
完整的函数(带有注释)在此答案的末尾。
限制:
  • 您需要至少一些额外的技巧才能使其正常工作(请参见下文)。
  • 它忽略了原始函数,但我认为这些函数没有默认函数参数。
  • 对于S3泛型,应该使用实际的方法。例如,seq.default而不是seq。如果目标是在自己的函数中注入默认值,则通常不会有太大问题。
例如,可以像这样使用此函数:
dots <- lazyeval::all_dots(a = ~x, b = ~lm(y ~ x, data = d))
add_defaults_to_dots(dots)
$a
<lazy>
  expr: x
  env:  <environment: R_GlobalEnv>

$b
<lazy>
  expr: lm(formula = y ~ x, data = d, subset = , weights = , na.action = ,  ...
  env:  <environment: R_GlobalEnv>
我们可以通过多种方式解决问题中的玩具问题。记住新功能和理想用例:
lag2 <- function(x, n = 1L, order_by = gear) lag(x, n = n, order_by = order_by)

mtcars %>%
  mutate(cyl_change = cyl != lag2(cyl))
  1. Use mutate_ with dots directly:

    dots <- lazyeval::all_dots(cyl_change = ~cyl != lag2(cyl), all_named = TRUE)
    dots <- add_defaults_to_dots(dots)
    mtcars %>% mutate_(.dots = dots)
    
  2. Redefine mutate to include the addition of defaults.

    mutate2 <- function(.data, ...) {
      dots <- lazyeval::lazy_dots(...)
      dots <- add_defaults_to_dots(dots)
      dplyr::mutate_(.data, .dots = dots)
    }
    
    mtcars %>% mutate2(cyl_change = cyl != lag2(cyl))
    
  3. Use S3 dispatch to do this as the default for any custom class:

    mtcars2 <- mtcars
    class(mtcars2) <- c('test', 'data.frame')
    
    mutate_.test <- function(.data, ..., .dots) {
      dots <- lazyeval::all_dots(.dots, ..., all_named = TRUE)
      dots <- add_defaults_to_dots(dots)
      dplyr::mutate_(tibble::as_tibble(.data), .dots = dots)
    }
    mtcars2 %>% mutate(cyl_change = cyl != lag2(cyl))
    
根据使用情况,我认为选项2和3是实现这一目标的最佳方法。选项3实际上具有完整的建议用例,但需要依赖一个额外的S3类。
功能:
add_defaults_to_dots <- function(dots) {
  # A recursive function that continues to add defaults to lower and lower levels.
  add_defaults_to_expr <- function(expr) {
    # First, if a call is a symbol or vector, there is nothing left to do but
    # return the value (since it is not a function call).
    if (is.symbol(expr) | is.vector(expr) | class(expr) == "formula") {
      return(expr)
    }
    # If it is a function however, we need to extract it.
    fun <- expr[[1]]
    # If it is a primitive function (like `+`) there are no defaults, and we
    # should not manipulate that call, but we do need to use recursion for cases
    # like a + f(b).
    if (is.primitive(match.fun(fun))) {
      new_expr <- expr
    } else {
      # If we have an actual non-primitive function call, we formally match the
      # call, so abbreviated arguments and order reliance work.
      matched_expr <- match.call(match.fun(fun), expr, expand.dots = TRUE)
      expr_list <- as.list(matched_expr)
      # Then we find the default arguments:
      arguments <- formals(eval(fun))
      # And overwrite the defaults for which other values were supplied:
      given <- expr_list[-1]
      arguments[names(given)] <- given
      # And finally build the new call:
      new_expr <- as.call(c(fun, arguments))
    }
    # Then, for all function arguments we run the function recursively.
    new_arguments <- as.list(new_expr)[-1]
    null <- sapply(new_arguments, is.null)
    new_arguments[!null] <- lapply(new_arguments[!null], add_defaults_to_expr)
    new_expr <- as.call(c(fun, new_arguments))
    return(new_expr)
  }
  # For lazy dots supplied, separate the expression and environments.
  exprs <- lapply(dots, `[[`, 'expr')
  envrs <- lapply(dots, `[[`, 'env')
  # Add the defaults to the expressions.
  new_exprs <- lapply(exprs, add_defaults_to_expr)
  # Add back the correct environments.
  new_calls <- Map(function(x, y) {
    lazyeval::as.lazy(x, y)
  }, new_exprs, envrs)
  return(new_calls)
}

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