Использование параметров data.table я и j в функциях

Я пытаюсь написать некоторые функции-обертки, чтобы уменьшить дублирование кода с помощью data.table.

Вот пример, используя mtcars. Сначала настройте некоторые данные:

data(mtcars)
mtcars$car <- factor(gsub("(.*?) .*", "\\1", rownames(mtcars)), ordered=TRUE)
mtcars <- data.table(mtcars)

Теперь вот что я обычно пишу, чтобы получить сводку подсчетов по группам. В этом случае я группирую car:

mtcars[, list(Total=length(mpg)), by="car"][order(car)]

      car Total
      AMC     1
 Cadillac     1
   Camaro     1
...
   Toyota     2
  Valiant     1
    Volvo     1

Усложнение состоит в том, что, поскольку аргументы i и j оцениваются в кадре data.table, нужно использовать eval(...), если вы хотите передать переменные:

Это работает:

group <- "car"
mtcars[, list(Total=length(mpg)), by=eval(group)]

Но теперь я хочу заказать результаты по одной и той же переменной группировки. Я не могу получить какой-либо вариант следующего, чтобы дать мне правильные результаты. Обратите внимание, что я всегда получаю только одну строку результатов, а не упорядоченный набор.

mtcars[, list(Total=length(mpg)), by=eval(group)][order(group)]
   car Total
 Mazda     2

Я знаю почему: потому что group оценивается в parent.frame, а не в кадре data.table.

Как я могу оценить group в контексте data.table?

В общем, как я могу использовать это внутри функции? Мне нужна следующая функция, чтобы дать мне все результаты, а не только первую строку данных:

tableOrder <- function(x, group){
  x[, list(Total=length(mpg)), by=eval(group)][order(group)]
}

tableOrder(mtcars, "car")

Ответы

Ответ 1

Гэвин и Джош правы. Этот ответ заключается только в том, чтобы добавить больше фона. Идея состоит в том, что вы не только передаете имена переменных столбцов в такую ​​функцию, но и выражения имен столбцов, используя quote().
group = quote(car)
mtcars[, list(Total=length(mpg)), by=group][order(group)]
      group Total
        AMC     1
   Cadillac     1
     ...
     Toyota     2
    Valiant     1
      Volvo     1

Хотя, с признательностью, с которой сложно начать, он может быть более гибким. Это идея, во всяком случае. Внутри функций вам нужно substitute(), например:

tableOrder = function(x,.expr) {
    .expr = substitute(.expr)
    ans = x[,list(Total=length(mpg)),by=.expr]
    setkeyv(ans, head(names(ans),-1))    # see below re feature request #1780
    ans
}

tableOrder(mtcars, car)
      .expr Total
        AMC     1
   Cadillac     1
     Camaro     1
      ...
     Toyota     2
    Valiant     1
      Volvo     1

tableOrder(mtcars, substring(car,1,1))  # an expression, not just a column name
      .expr Total
 [1,]     A     1
 [2,]     C     3
 [3,]     D     3
 ...
 [8,]     P     2
 [9,]     T     2
[10,]     V     2

tableOrder(mtcars, list(cyl,gear%%2))   # by two expressions, so head(,-1) above
     cyl gear Total
[1,]   4    0     8
[2,]   4    1     3
[3,]   6    0     4
[4,]   6    1     3
[5,]   8    1    14

В v1.8.0 (июль 2012 г.) добавлен новый аргумент keyby, упрощающий:

tableOrder = function(x,.expr) {
    .expr = substitute(.expr)
    x[,list(Total=length(mpg)),keyby=.expr]
}

Комментарии и обратная связь в области i, j и by переменных выражений наиболее приветствуются. Другое, что вы можете сделать, это иметь таблицу, в которой столбец содержит выражения, а затем посмотреть, какое выражение помещать в i, j или by из этой таблицы.

Ответ 2

Используйте get(group) для обращения к объекту с именем group:

> mtcars[, list(Total=length(mpg)), by=eval(group)][order(get(group))]
        car Total
        AMC     1
   Cadillac     1
     Camaro     1
   Chrysler     1
     Datsun     1
      Dodge     1
     Duster     1
    Ferrari     1
       Fiat     2
       Ford     1
      Honda     1
     Hornet     2
    Lincoln     1
      Lotus     1
   Maserati     1
      Mazda     2
       Merc     7
    Pontiac     1
    Porsche     1
     Toyota     2
    Valiant     1
      Volvo     1
cn      car Total
> # vs
> mtcars[, list(Total=length(mpg)), by=eval(group)][order(group)]
       car Total
[1,] Mazda     2

Причина order(get(group)) заключается в том, что выражение оценивается в кадре data.table. Там get(group) будет искать переменную find car. Если вы оцениваете это в глобальной среде, он не существует

> get(group)
Error in get(group) : object 'car' not found

но это происходит в кадре, где выполняется оценка. group там не существует, но, следуя обычным правилам, он ищет резервные копии родительских кадров, пока не найдет что-то, что соответствует group, что в этом случае является глобальным env. Таким образом, вам нужно быть осторожным в отношении имени объекта, который вы используете как group в своей реальной функции, - вы не хотите использовать что-то, что может быть похоже на объект data.table, например. Используя что-то вроде .group, поскольку функция arg будет довольно безопасной, я думаю.

Вот ваша функция, измененная:

tableOrder <- function(x, .group){
  x[, list(Total=length(mpg)), by=eval(.group)][order(get(.group))]
}

> tableOrder(mtcars, "car")
        car Total
        AMC     1
   Cadillac     1
     Camaro     1
   Chrysler     1
     Datsun     1
....

Ответ 3

Для общего вопроса о том, как контролировать область видимости в data.table, ответ Gavin хорошо вас охватывает.

Чтобы действительно воспользоваться преимуществами сильных сторон data.table, вы должны установить ключ для объектов data.table. Ключ заставляет ваши данные быть отредактированы так, чтобы строки с одного уровня (или комбинации уровней) фактора группировки сохранялись в смежных блоках памяти. Это может в свою очередь значительно ускорить операции группировки по сравнению с "ad hoc" сортировки, используемой в вашем примере. (Искать "ad hoc" в datatable-faq (предупреждение, pdf) для получения дополнительной информации).

Во многих ситуациях (включая ваш пример) использование ключей также имеет счастливый побочный эффект, упрощающий код, необходимый для манипулирования data.table. Кроме того, он автоматически выводит результаты в порядке, указанном ключом, что часто является тем, что вы хотите.

Во-первых, если вам понадобится только подмножество в столбце 'car', вы можете просто сделать:

## Create data.table with a key
group <- "car"
mtcars <- data.table(mtcars, key = group)

## Outputs results in correct order
mtcars[, list(Total=length(mpg)), by = key(mtcars)]
        car Total
        AMC     1
   Cadillac     1
     Camaro     1
   Chrysler     1
     Datsun     1

Даже если ваш ключ содержит несколько столбцов, использование ключа по-прежнему делает более простой код (и вы получаете ускорение, которое, вероятно, является реальной причиной использования data.table в первую очередь!):

group <- "car"
mtcars <- data.table(mtcars, key = c("car", "gear"))
mtcars[, list(Total=length(mpg)), by = eval(group)]

EDIT: осторожная осторожность

Если аргумент by используется для выполнения группировки на основе столбца, который является частью ключа, но который не является первым элементом ключа, порядок результатов может по-прежнему нуждаться в последующей обработке. Итак, во втором примере выше, если key = c("gear", "car"), то "Dodge" сортируется до "Datsun". В такой ситуации я могу по-прежнему предпочесть переупорядочивать ключ заранее, а не изменять порядок результатов после факта. Возможно, Мэтью Доулл будет взвешивать, какой из этих двух предпочтительнее/быстрее.