如何使用非标准评估 NSE 在 data.table 中评估参数?

5

假设我有以下代码:

library(data.table)
cars1 = setDT(copy(cars))
cars2 = setDT(copy(cars))

car_list = list(cars1, cars2)
class(car_list) <- "dd"

`[.dd` <- function(x,...) {
  code = rlang::enquos(...)
  cars1 = x[[1]]
  rlang::eval_tidy(quo(cars1[!!!code]))
}

car_list[,.N, by = speed]

我希望通过定义[.dd函数来对cars1cars2执行任意操作,无论我在...中放入什么,都会使用[data.table句法在cars1cars2上执行。

例如:car_list[,.N, by = speed]应该执行以下操作:

cars1[,.N, by = speed]
cars2[,.N, by = speed]

我也想要

car_list[,speed*2]

待完成

cars1[,speed*2]
cars2[,speed*2]

基本上,在[.dd中的...需要接受任意代码。

我需要捕获...,所以我尝试使用code = rlang::enquos(...),然后rlang::eval_tidy(quo(cars1[!!!code]))不起作用并出现错误。

[.data.table(cars1, ~, ~.N, by = ~speed) : 缺少参数“i”,没有默认值。


@Roland,如果不必要,我就不需要使用rlang,但我不知道如何实现我想要的结果。因此,我提出了这个问题。 - xiaodai
@Roland 如果您能直接发布一个示例作为答案,我会接受这个答案。谢谢。 - xiaodai
1
尝试使用rlang :: enexprs替换enquos,并删除eval_tidy内部的quo调用,它不是必需的。 - Alexis
2
只是为了澄清,你不能在这里使用tidyeval,因为tidyeval需要被调用方支持,而data.table子集操作符不支持tidyeval。因此,不幸的是,这要实现起来要复杂得多。 - Konrad Rudolph
相关问题: https://dev59.com/cmkw5IYBdhLWcg3ws8xj - chinsoon12
显示剩余5条评论
3个回答

5

虽然不属于 rlang 类型的口头禅,但这种方法似乎运行得相当不错:lapply(dt_list, '[', ...),对我来说,代码更易读,因为它明确了使用的方法。如果我看到 car_list[, .N, by = speed],我会期望默认 data.table 方法。

将其作为函数使您可以兼顾两全:

class(car_list) <- "dd"

`[.dd` <- function(x,...) {
 lapply(x, '[', ...)
}

car_list[, .N, speed]
car_list[, speed * 2]
car_list[, .(.N, max(dist)), speed]
car_list[, `:=` (more_speed = speed+5)]

以下是该方法的一些示例:

car_list[, .N, speed]
# lapply(car_list, '[', j = .N, by = speed)
# or
# lapply(car_list, '[', , .N, speed)
[[1]]
    speed N
 1:     4 2
 2:     7 2
 3:     8 1
 4:     9 1
 5:    10 3
...
[[2]]
    speed N
 1:     4 2
 2:     7 2
 3:     8 1
 4:     9 1
 5:    10 3
...
car_list[, speed * 2]
# lapply(car_list, '[', j = speed*2)
# or
# lapply(car_list, '[', , speed*2)
[[1]]
 [1]  8  8 14 14 16 18 20 20 20 22 22 24 24 24 24 26 26
[18] 26 26 28 28 28 28 30 30 30 32 32 34 34 34 36 36 36
[35] 36 38 38 38 40 40 40 40 40 44 46 48 48 48 48 50

[[2]]
 [1]  8  8 14 14 16 18 20 20 20 22 22 24 24 24 24 26 26
[18] 26 26 28 28 28 28 30 30 30 32 32 34 34 34 36 36 36
[35] 36 38 38 38 40 40 40 40 40 44 46 48 48 48 48 50

car_list[, .(.N, max(dist)), speed]
# lapply(car_list, '[', j = list(.N, max(dist)), by = speed)
# or 
# lapply(car_list, '[', ,.(.N, max(dist)), speed)

[[1]]
    speed N  V2
 1:     4 2  10
 2:     7 2  22
 3:     8 1  16
 4:     9 1  10
 5:    10 3  34
...

[[2]]
    speed N  V2
 1:     4 2  10
 2:     7 2  22
 3:     8 1  16
 4:     9 1  10
 5:    10 3  34
...

这个可以使用:=操作符实现:

car_list[, `:=` (more_speed = speed+5)]
# or
# lapply(car_list, '[', , `:=` (more_speed = speed+5))

car_list
[[1]]
    speed dist more_speed
 1:     4    2          9
 2:     4   10          9
 3:     7    4         12
 4:     7   22         12
 5:     8   16         13
...

[[2]]
    speed dist more_speed
 1:     4    2          9
 2:     4   10          9
 3:     7    4         12
 4:     7   22         12
 5:     8   16         13

有点偏离重点了。这不是为我而设计的,而是为最终用户而设计的,所以我需要它可以像这样被调用 dt_list[...]。 - xiaodai
看编辑后的代码 [.dd <- function(x, ...) { lapply(x, '[', ...)} 也可以运行。虽然我无法在此框中对代码进行评论。顺便说一句,我同意,起初我认为我会因为没有真正回答问题而被贬值 :) - Cole

4

第一个R基础选项是substitute(...()),其后是do.call

library(data.table)
cars1 = setDT(copy(cars))
cars2 = setDT(copy(cars))
cars2[, speed := sort(speed, decreasing = TRUE)]

car_list = list(cars1, cars2)
class(car_list) <- "dd"

`[.dd` <- function(x,...) {
  a <- substitute(...()) #this is an alist
  expr <- quote(x[[i]])
  expr <- c(expr, a)
  res <- list()
  for (i in seq_along(x)) {
    res[[i]] <- do.call(data.table:::`[.data.table`, expr)
  }
  res
}

all.equal(
  car_list[,.N, by = speed],
  list(cars1[,.N, by = speed], cars2[,.N, by = speed])
)
#[1] TRUE

all.equal(
  car_list[, speed*2],
  list(cars1[, speed*2], cars2[, speed*2])
)
#[1] TRUE

第二个基本的R选项是match.call,修改调用并进行评估(在 lm 中可以找到此方法):

`[.dd` <- function(x,...) {
  thecall <- match.call()
  thecall[[1]] <- quote(`[`)
  thecall[[2]] <- quote(x[[i]])
  res <- list()
  for (i in seq_along(x)) {
    res[[i]] <- eval(thecall)
  }
  res
}

all.equal(
  car_list[,.N, by = speed],
  list(cars1[,.N, by = speed], cars2[,.N, by = speed])
)
#[1] TRUE

all.equal(
  car_list[, speed*2],
  list(cars1[, speed*2], cars2[, speed*2])
)
#[1] TRUE

我还没有测试这些方法是否会在使用:=时进行深复制。


我可以问一下...()是什么吗?我查看了R内部和R语言定义,只发现了...点点点参数。 - chinsoon12
1
我是从 R-devel 邮件列表中学到这个技巧的。你可以使用 substitute 函数创建一个像这样的调用:foo <- function(x) {substitute(x())}; foo(bar)。现在,我们将 x 替换为 ...。我不确定为什么我们不会得到一个调用对象,但是调用是一个内部的 pairlist,这就是我们得到的结果。如果你想要更容易理解的方法,可以使用 Hadley 在他的书中建议的方式:eval(substitute(alist(...))) - Roland
1
感谢。通过试错发现 quote(...) 也可以使用。将进一步阅读有关 nse 的内容。 - chinsoon12
可以处理 car_list[, abc := speed*3],而 @Alexis 的不能。因此,这就是解决方案! - xiaodai
有没有不使用“:::”的方法?因为 CRAN 不接受它。实际上,改用“[”也可以! - xiaodai
第一种方法也可以只使用\[``](第二种方法也是如此)。 - Roland

3

我的评论中提到的建议不够完整。 你确实可以使用rlang来支持整洁评估, 但由于data.table本身并不直接支持它, 所以最好使用表达式而不是引文, 在调用eval_tidy之前需要构建完整的最终表达式:

`[.dd` <- function(x, ...) {
  code <- rlang::enexprs(...)
  lapply(x, function(dt) {
    ex <- rlang::expr(dt[!!!code])
    rlang::eval_tidy(ex)
  })
}

car_list[, .N, by = speed]
[[1]]
    speed N
 1:     4 2
 2:     7 2
 3:     8 1
 4:     9 1
 5:    10 3
 6:    11 2
 7:    12 4
 8:    13 4
 9:    14 4
10:    15 3
11:    16 2
12:    17 3
13:    18 4
14:    19 3
15:    20 5
16:    22 1
17:    23 1
18:    24 4
19:    25 1

[[2]]
    speed N
 1:     4 2
 2:     7 2
 3:     8 1
 4:     9 1
 5:    10 3
 6:    11 2
 7:    12 4
 8:    13 4
 9:    14 4
10:    15 3
11:    16 2
12:    17 3
13:    18 4
14:    19 3
15:    20 5
16:    22 1
17:    23 1
18:    24 4
19:    25 1

这是一个很好的解决方案,但它无法处理car_list[, abc:=speed*3] - xiaodai
1
@xiaodai 如果你想使用 :=,你需要在 enexprs 中传递 .unquote_names = FALSErlang 也为其自身的目的使用 :=,因此它与 data.table 之间的交互需要特别考虑。 - Alexis

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