将表达式传递给函数,在data.table中进行评估,以便进行内部优化。

问题描述 投票:5回答:1

预读

我在SO这里看了一些资料。

在得到完美的答案后,我的 以前的问题我想彻底弄清楚如何在规范上处理。data.tables 在函数中。

基本问题

我最终想要的是创建一个函数,它将一些 R 表达式作为输入,并在上下文中对它们进行评估。data.table (均在 i 以及在 j 部分)。) 引用的答案告诉我,我必须使用一些 get/eval/substitute 组合,如果我的输入变得比单列更复杂(在这种情况下,我可以接受这个 ..stringwith = FALSE 方法[1])。)

我的真实数据比较大,所以我担心计算时间。

最终,如果我想有充分的灵活性(也就是传递表达式而不是裸露的列名),我明白我必须选择一个 eval 办法。

代码能说千言万语,让我们来说明一下我目前的发现。

设置

library(data.table)
iris <- copy(iris)
setDT(iris)

工作马功能

my_fun <- function(my_i, my_j, option_sel = 1, my_data = iris, by = NULL) {
   switch(option_sel,
      {
         ## option 1 - base R deparse
         my_data[eval(parse(text = deparse(substitute(my_i)))), 
                 eval(parse(text = deparse(substitute(my_j)))),
                 by]
      },
      {
         ## option 2 - base R even shorter
         my_data[eval(substitute(my_i)), 
                 eval(substitute(my_j)),
                 by]

      },
      {
         ## option 3 - rlang
         my_data[rlang::eval_tidy(rlang::enexpr(my_i)),
                 rlang::eval_tidy(rlang::enexpr(my_j), data = .SD),
                 by]

      },
      {
         ## option 4 - if passing only simple column name strings
         ## we can use `with` (in j only)
         my_data[,
                 my_j, with = FALSE,
                 by]

      },
      {
         ## option 5 - if passing only simple column name strings 
         ## we can use ..syntax (in 'j' only)
         my_data[,
                 ..my_j]
                 # , by] ## would give a strange error

      },
      {
         ## option 6 - if passing only simple column name strings
         ## we can use `get`
         my_data[,
                 setNames(.(get(my_j)), my_j),
                 by]

      }
   )
}

结果

## added the unnecessary NULL to enforce same format
## did not want to make complicated ifs for by in the func 
## but by is needed for meaningful benchmarks later
expected <- iris[Species == "setosa", sum(Sepal.Length), NULL]
sapply(1:3, function(i) 
               isTRUE(all.equal(expected,
                                my_fun(Species == "setosa", sum(Sepal.Length), i))))
# [1] TRUE TRUE TRUE

expected <- iris[, .(Sepal.Length), NULL]
sapply(4:6, function(i)
               isTRUE(all.equal(expected,
                                my_fun(my_j = "Sepal.Length", option_sel = i))))
# [1] TRUE TRUE TRUE

疑问

所有的选项都可以使用,但在创建这个(公认的不是那么)最小的例子时,我有几个问题。

  1. 要想从这个例子中获得最大的收益 data.table我必须使用内部优化器可以配置和优化的代码[2]。那么,在1-3选项中,哪一个选项(4-6只是为了完整,缺乏充分的灵活性)在以下情况下 "效果最好 "呢?data.table哪些是可以在内部优化的,以充分受益于 data.table? 我的快速基准显示 rlang 选项似乎是最快的。
  2. 我意识到,对于选项3,我必须提供 .SD 作为数据参数,在 j 部分,但不在 i 部分。这是由于范围化,这一点很清楚。但为什么 tidy_eval "看 "列名在 i 但不在 j?
  3. 有其他可以进一步优化的解决方案吗?
  4. 在选项5中使用by会出现一个奇怪的错误。为什么会这样?

基准

library(dplyr)
size     <- c(1e6, 1e7, 1e8)
grp_prop <- c(1e-6, 1e-4)

make_bench_dat <- function(size, grp_prop) {
   data.table(x = seq_len(size),
              g = sample(ceiling(size * grp_prop), size, grp_prop < 1))
}

res <- bench::press(
   size = size,
   grp_prop = grp_prop,
   {
      bench_dat <- make_bench_dat(size, grp_prop)
      bench::mark(
         deparse    = my_fun(TRUE, max(x), 1, bench_dat, by = "g"),
         substitute = my_fun(TRUE, max(x), 2, bench_dat, by = "g"),
         rlang      = my_fun(TRUE, max(x), 3, bench_dat, by = "g"), 
         relative = TRUE)
   }
)

summary(res) %>% select(expression, size, grp_prop, min, median)
# # A tibble: 18 x 5
#    expression      size grp_prop      min   median
#    <bch:expr>     <dbl>    <dbl> <bch:tm> <bch:tm>
#  1 deparse      1000000 0.000001  22.73ms  24.36ms
#  2 substitute   1000000 0.000001  22.56ms   25.3ms
#  3 rlang        1000000 0.000001   8.09ms   9.05ms
#  4 deparse     10000000 0.000001 274.24ms 308.72ms
#  5 substitute  10000000 0.000001 276.73ms 276.99ms
#  6 rlang       10000000 0.000001 114.52ms 119.21ms
#  7 deparse    100000000 0.000001    3.79s    3.79s
#  8 substitute 100000000 0.000001    3.92s    3.92s
#  9 rlang      100000000 0.000001    3.12s    3.12s
# 10 deparse      1000000 0.0001    29.57ms  36.25ms
# 11 substitute   1000000 0.0001    37.22ms  41.56ms
# 12 rlang        1000000 0.0001     19.3ms  24.07ms
# 13 deparse     10000000 0.0001   386.13ms 396.84ms
# 14 substitute  10000000 0.0001   330.22ms 332.42ms
# 15 rlang       10000000 0.0001   270.54ms 274.35ms
# 16 deparse    100000000 0.0001      4.51s    4.51s
# 17 substitute 100000000 0.0001       4.1s     4.1s
# 18 rlang      100000000 0.0001      2.87s    2.87s

[1] with = FALSE..columnName 然而,只有在 j 部分。

2]当我更换了一个显著的性能提升时,我才知道这一点。purrr::mapbase::lapply.

r data.table
1个回答
5
投票

不需要花哨的工具,只需要使用基础的R元编程功能。

my_fun2 = function(my_i, my_j, by, my_data) {
  dtq = substitute(
    my_data[.i, .j, .by],
    list(.i=substitute(my_i), .j=substitute(my_j), .by=substitute(by))
  )
  print(dtq)
  eval(dtq)
}

my_fun2(Species == "setosa", sum(Sepal.Length), my_data=as.data.table(iris))
my_fun2(my_j = "Sepal.Length", my_data=as.data.table(iris))

这样你就可以确保data.table会使用所有可能的优化,就像在键入 [ 手工调用。


请注意,在data.table中,我们正计划让替换变得更容易,参见PR中提出的解决方案。Rdatatabledata.table#4304.

然后使用额外的 env 变种替代将在内部为您处理

my_fun3 = function(my_i, my_j, by, my_data) {
  my_data[.i, .j, .by, env=list(.i=substitute(my_i), .j=substitute(my_j), .by=substitute(by)), verbose=TRUE]
}
my_fun3(Species == "setosa", sum(Sepal.Length), my_data=as.data.table(iris))
#Argument 'j'  after substitute: sum(Sepal.Length)
#Argument 'i'  after substitute: Species == "setosa"
#...
my_fun3(my_j = "Sepal.Length", my_data=as.data.table(iris))
#Argument 'j'  after substitute: Sepal.Length
#...
© www.soinside.com 2019 - 2024. All rights reserved.