Dplyr на data.table, я действительно использую data.table?

Если я использую синтаксис dplyr поверх datatable, получаю ли я все преимущества скорости данных, используя синтаксис dplyr? Другими словами, я неправильно использую datatable, если я запрашиваю его с помощью синтаксиса dplyr? Или мне нужно использовать чистый синтаксис, чтобы использовать всю его мощность.

Заранее благодарим за любой совет. Пример кода:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count))

Результаты:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

Вот эквивалентность данных, с которой я столкнулся. Не уверен, что он соответствует хорошей практике DT. Но мне интересно, действительно ли код более эффективен, чем синтаксис dplyr за сценой:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]

Ответ 1

Нет простого или простого ответа, потому что философия обоих этих пакетов отличается в некоторых аспектах. Поэтому некоторые компромиссы неизбежны. Вот некоторые из проблем, которые могут возникнуть для решения/рассмотрения.

Операции с i (== filter() и slice() в dplyr)

Предположим DT, скажем, 10 столбцов. Рассмотрим эти выражения data.table:

DT[a > 1, .N]                    ## --- (1)
DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)

(1) дает количество строк в DT, где находится столбец a > 1. (2) возвращает mean(b), сгруппированное по c,d для того же выражения в i как (1).

Обычно используемые выражения dplyr:

DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)

Очевидно, что коды данных. Кроме того, они также более эффективны с точки зрения памяти 1. Зачем? Поскольку в обоих (3) и (4), filter() сначала возвращает строки для всех 10 столбцов, когда в (3) нам просто нужно количество строк, а в (4) нам нужны только столбцы b, c, d для последовательного операции. Чтобы преодолеть это, мы должны select() столбцы apriori:

DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)

Необходимо выделить важное философское различие между двумя пакетами:

  • В data.table нам нравится держать эти связанные операции вместе, и это позволяет смотреть на j-expression (из одного и того же вызова функции) и понимать, что нет необходимости в каких-либо столбцах в (1). Выражение в i вычисляется, а .N является просто суммой этого логического вектора, который дает количество строк; все подмножество никогда не реализуется. В (2) только один столбец b,c,d материализуется в подмножестве, другие столбцы игнорируются.

  • Но в dplyr философия состоит в том, чтобы функция выполняла точно одну вещь. Существует (по крайней мере, в настоящее время) никакого способа определить, нужна ли операция после filter() всем тем столбцам, которые мы отфильтровали. Вам нужно подумать заранее, если вы хотите эффективно выполнять такие задачи. В этом случае я лично считаю это контр-интуитивным.

Заметим, что в (5) и (6) мы все еще подмножим столбец a, который нам не нужен. Но я не уверен, как этого избежать. Если функция filter() имела аргумент для выбора возвращаемых столбцов, мы могли бы избежать этой проблемы, но тогда функция не будет выполнять только одну задачу (что также является выбором дизайна dplyr).

Подцепить по ссылке

dplyr никогда не будет обновляться по ссылке. Это еще одна огромная (философская) разница между двумя пакетами.

Например, в data.table вы можете:

DT[a %in% some_vals, a := NA]

который обновляет столбец a ссылкой только на те строки, которые удовлетворяют условию. В настоящий момент dplyr полностью копирует всю таблицу данных внутри, чтобы добавить новый столбец. @BrodieG уже упомянул об этом в своем ответе.

Но глубокую копию можно заменить мелкой копией, когда FR # 617. Также уместно: dplyr: FR # 614. Обратите внимание, что все же, измененный вами столб всегда будет скопирован (следовательно, медленнее или меньше памяти). Невозможно обновить столбцы по ссылке.

Другие функции

  • В data.table вы можете объединиться при объединении, и это более прямолинейно понять и эффективно использовать память, поскольку результат промежуточного соединения никогда не материализуется. Например, этот пост. Вы не можете (на данный момент?) Сделать это, используя синтаксис dplyr data.table/data.frame.

  • data.table функция roll joinings не поддерживается в синтаксисе dplyr.

  • Недавно мы реализовали объединения перекрытий в data.table для объединения диапазонов интервалов (здесь пример), который является отдельной функцией foverlaps() на момент, и поэтому может быть использован с операторами труб (magrittr/pipeR? - никогда не пробовал это сам).

    Но в конечном итоге наша цель - интегрировать его в [.data.table, чтобы мы могли собрать другие функции, такие как группировка, агрегация при соединении и т.д., которые будут иметь те же ограничения, что и выше.

  • Начиная с 1.9.4, data.table реализует автоматическую индексацию с использованием вторичных ключей для подмножеств с быстрым бинарным поиском на регулярном синтаксисе R. Пример: DT[x == 1] и DT[x %in% some_vals] автоматически создадут индекс для первого запуска, который затем будет использоваться для последовательных подмножеств из одного столбца в быстрое подмножество с использованием двоичного поиска. Эта функция будет продолжать развиваться. Проверьте этот метод для краткого обзора этой функции.

    С помощью метода filter() для data.tables он не использует эту функцию.

  • Функция dplyr заключается в том, что она также обеспечивает интерфейс к базам данных с использованием того же синтаксиса, который не содержит данных. момент.

Итак, вам придется взвесить эти (и, возможно, другие моменты) и решить, основываясь на том, приемлемы ли эти компромиссы для вас.

НТН


(1) Обратите внимание, что эффективность памяти напрямую влияет на скорость (особенно по мере увеличения данных), поскольку узкое место в большинстве случаев перемещает данные из основной памяти в кеш (и максимально использует данные в кеше) уменьшить промахи в кэше - чтобы уменьшить доступ к основной памяти). Не вдаваясь в подробности здесь.

Ответ 2

Просто попробуйте.

library(rbenchmark)
library(dplyr)
library(data.table)

benchmark(
dplyr = diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count)),
data.table = diamondsDT[cut != "Fair", 
                        list(AvgPrice = mean(price),
                             MedianPrice = as.numeric(median(price)),
                             Count = .N), by = cut][order(-Count)])[1:4]

По этой проблеме кажется, что data.table в 2.4 раза быстрее, чем dplyr, используя data.table:

        test replications elapsed relative
2 data.table          100    2.39    1.000
1      dplyr          100    5.77    2.414

Пересмотренный на основе комментария Полимеразы.

Ответ 3

Чтобы ответить на ваши вопросы:

  • Да, вы используете data.table
  • Но не так эффективно, как вы бы с чистым синтаксисом data.table

Во многих случаях это будет приемлемым компромиссом для тех, кто хочет синтаксис dplyr, хотя он может быть медленнее, чем dplyr с прозрачными кадрами данных.

Один большой фактор, по-видимому, заключается в том, что dplyr будет копировать data.table по умолчанию при группировке. Рассмотрим (используя микрофункцию):

Unit: microseconds
                                                               expr       min         lq    median
                                diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                          diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
 diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                               diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609

Фильтрация имеет сопоставимую скорость, но группировка - нет. Я считаю, что виновником является эта строка в dplyr:::grouped_dt:

if (copy) {
    data <- data.table::copy(data)
}

где copy по умолчанию используется TRUE (и его нельзя легко изменить на FALSE, который я вижу). Вероятно, это не составляет 100% разницы, но общие накладные расходы на что-то размером diamonds скорее всего не являются полной разницей.

Проблема заключается в том, что для того, чтобы иметь согласованную грамматику, dplyr выполняет группировку в два этапа. Сначала он устанавливает ключи в копии исходной таблицы данных, которая соответствует группам, и только позже она группируется. data.table просто выделяет память для самой большой группы результатов, которая в этом случае является только одной строкой, поэтому имеет большое значение в том, сколько памяти необходимо выделить.

FYI, если кто-то заботится, я нашел это, используя treeprof (install_github("brodieg/treeprof")), экспериментальный (и все еще очень альфа) просмотрщик дерева для вывода Rprof:

enter image description here

Обратите внимание на то, что выше в данный момент работает только на macs AFAIK. К сожалению, Rprof записывает вызовы типа packagename::funname как анонимные, поэтому на самом деле могут быть любые и все вызовы datatable:: внутри grouped_dt, которые отвечают, но из быстрого тестирования это выглядело как datatable::copy большой.

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


EDIT: подтверждение копирования:

> tracemem(diamondsDT)
[1] "<0x000000002747e348>"    
> diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
Source: local data table [5 x 2]

        cut AvgPrice
1      Fair 4358.758
2      Good 3928.864
3 Very Good 3981.760
4   Premium 4584.258
5     Ideal 3457.542
> diamondsDT[, mean(price), by = cut]
         cut       V1
1:     Ideal 3457.542
2:   Premium 4584.258
3:      Good 3928.864
4: Very Good 3981.760
5:      Fair 4358.758
> untracemem(diamondsDT)