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

91

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

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

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) ]
Полимераза
источник
7
Почему бы вам не использовать синтаксис таблицы данных? Это элегантно и эффективно. На этот вопрос нет ответа, поскольку он очень широкий. Да, есть dplyrметоды для таблиц данных, но в таблице данных также есть свои собственные сопоставимые методы
Rich Scriven
7
Я могу использовать синтаксис данных или конечно. Но почему-то я нахожу синтаксис dplyr более элегантным. Независимо от моих предпочтений в синтаксисе. Что я действительно хочу знать, так это: нужно ли мне использовать чистый синтаксис данных, чтобы получить 100% выгоду от мощности обработки данных.
Polymerase
3
Для недавнего теста, где dplyrиспользуется для data.frames и соответствующих data.tables, см. Здесь (и ссылки в нем).
Хенрик
2
@Polymerase - Я думаю, ответ на этот вопрос определенно «Да»
Rich Scriven
1
@Henrik: Позже я понял, что неправильно истолковал эту страницу, потому что они отображали только код для построения фрейма данных, но не код, который они использовали для построения data.table. Когда я понял это, я удалил свой комментарий (надеясь, что вы его не видели).
IRTFM

Ответы:

77

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

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

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

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

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

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

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

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

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). Выражение in 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 глубоко копирует всю таблицу data.table внутри, чтобы добавить новый столбец. @BrodieG уже упоминал об этом в своем ответе.

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

Прочие функции

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

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

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

HTH


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

Арун
источник
4
Абсолютно блестящий. Спасибо за это
Дэвид Аренбург
6
Это хороший ответ, но для dplyr было бы возможно (если маловероятно) реализовать эффективный filter()плюс, summarise()используя тот же подход, который dplyr использует для SQL, то есть создание выражения и последующее выполнение только один раз по запросу. Маловероятно, что это будет реализовано в ближайшем будущем, потому что dplyr для меня достаточно быстр, а реализовать планировщик / оптимизатор запросов относительно сложно.
Хэдли
Эффективное использование памяти также помогает в другой важной области - фактическом завершении задачи до того, как закончится память. При работе с большими наборами данных я столкнулся с этой проблемой как с dplyr, так и с pandas, тогда как data.table изящно завершил бы работу.
Заки
25

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

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

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

Г. Гротендик
источник
2
Используя этот microbenchmarkпакет, я обнаружил, что выполнение dplyrкода OP в исходной (фрейм данных) версии diamondsзаняло среднее время 0,012 секунды, в то время как среднее время потребовалось 0,024 секунды после преобразования diamondsв таблицу данных. Выполнение data.tableкода Г. Гротендика заняло 0,013 секунды. По крайней мере, в моей системе он выглядит примерно так же dplyrи data.tableимеет примерно такую ​​же производительность. Но почему будет dplyrмедленнее, если фрейм данных сначала преобразуется в таблицу данных?
eipi10
Уважаемый Г. Гротендик, это замечательно. Спасибо, что показали мне эту тестовую утилиту. Кстати, вы забыли [порядок (-Счетчик)] в версии с данными, чтобы сделать эквивалент аранжировки dplyr (desc (Счетчик)). После добавления этого datatable все еще быстрее примерно в 1,8 раза (вместо 2,9).
Polymerase
@ eipi10, можете ли вы снова запустить свой стенд с версией с данными здесь (добавлена ​​сортировка по desc Count на последнем шаге): diamondsDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median) (цена)), Count = .N), by = cut] [order (-Count)]
Полимераза
По-прежнему 0,013 секунды. Операция упорядочивания не занимает много времени, потому что она просто переупорядочивает итоговую таблицу, в которой всего четыре строки.
eipi10
1
Есть некоторые фиксированные накладные расходы на преобразование синтаксиса dplyr в синтаксис таблицы данных, поэтому, возможно, стоит попробовать различные размеры проблем. Также я, возможно, не реализовал самый эффективный код таблицы данных в dplyr; патчи всегда приветствуются
Хэдли
22

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

  • Да, вы используете 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вывода:

введите описание изображения здесь

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

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


РЕДАКТИРОВАТЬ : для подтверждения копирования:

> 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)
BrodieG
источник
Это здорово, спасибо. Означает ли это, что dplyr :: group_by () удвоит потребность в памяти (по сравнению с чистым синтаксисом данных) из-за этапа копирования внутренних данных? Это означает, что размер моего объекта данных составляет 1 ГБ, и я использую синтаксис цепочки dplyr, аналогичный синтаксису в исходном сообщении. Мне понадобится как минимум 2 ГБ свободной памяти для получения результатов?
Polymerase
2
Кажется, я исправил это в версии для разработчиков?
Хэдли
@hadley, я работал с версии CRAN. Глядя на разработчика, похоже, что вы частично решили проблему, но фактическая копия остается (не тестировалась, просто глядя на строки c (20, 30:32) в R / grouped-dt.r. Сейчас, вероятно, быстрее, но Бьюсь об заклад, медленный шаг - это копия.
BrodieG
3
Еще я жду функцию неглубокого копирования в data.table; до тех пор я думаю, что лучше перестраховаться, чем быстро.
Хэдли
2

Теперь вы можете использовать dtplyr , который является частью тидиверсии . Он позволяет вам использовать операторы стиля dplyr как обычно, но использует ленивую оценку и переводит ваши операторы в код data.table под капотом. Накладные расходы на перевод минимальны, но вы получаете все, если не большинство преимуществ data.table. Более подробная информация в официальном репозитории git здесь и на странице tidyverse .

Черное молоко
источник