Передача выражений в функцию для оценки в таблице data.table для внутренней оптимизации - PullRequest
7 голосов
/ 27 мая 2020

Предварительное чтение

Здесь я просмотрел некоторый материал по SO:

и после получения идеального ответ на мою предыдущую проблему , я пытаюсь раз и навсегда понять, как канонически справляться с data.tables в функциях.

Основная проблема

В конечном итоге я хочу создать функцию, которая принимает несколько R выражений в качестве входных данных и оценивает их в контексте data.table (как в i, так и в j части ). Процитированные ответы говорят мне, что я должен использовать некоторую комбинацию get/eval/substitute, если мои входные данные становятся более сложными, чем просто один столбец (в этом случае я мог бы жить с подходом ..string или with = FALSE [1]).

Мои реальные данные довольно большие, поэтому меня беспокоит время вычислений.

В конечном итоге, если я хочу иметь полную гибкость (то есть передавать выражения, а не имена столбцов), я понял что мне нужно go для подхода eval:

Codes говорит тысячу слов, поэтому давайте проиллюстрируем то, что я узнал до сих пор:

Setup

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. Использование by с опцией 5 приводит к странной ошибке. Почему?

Тесты

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::map на base::lapply.

1 Ответ

5 голосов
/ 27 мая 2020

Нет необходимости в необычных инструментах, просто используйте базовые функции метапрограммирования 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 будет использовать все возможные оптимизации, как при вводе [ call вручную.


Обратите внимание, что в data.table мы планируем упростить замену, см. Решение, предложенное в PR Rdatatable / data.table # 4304 .

Затем, используя extra env замена var будет обработана для вас внутри

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
#...
...