dplyr на data.table, чи справді я використовую data.table?


91

Якщо я використовую синтаксис dplyr поверх таблиці даних , чи отримую я всі переваги швидкості роботи таблиці даних, одночасно використовуючи синтаксис dplyr? Іншими словами, чи неправильно я використовую таблицю даних, якщо я запитую її із синтаксисом 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) ]

7
Чому б вам не використовувати синтаксис таблиці даних? Це також елегантно та ефективно. Питання насправді не відповідає, оскільки воно дуже широке. Так, існують dplyrметоди для таблиць даних, але таблиця даних також має свої власні порівнянні методи
Rich Scriven

7
Я можу використовувати синтаксис даних або курс. Але якось я вважаю синтаксис dplyr більш витонченим. Незалежно від моєї переваги синтаксису. Насправді я хочу знати: чи потрібно мені використовувати чистий синтаксис даних, щоб отримати 100% переваги потужності даних.
Полімераза

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стовпці де 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)

Очевидно, що коди data.table коротші. Крім того, вони також ефективніше пам’яті 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 глибоко копіює всі data.table внутрішньо, щоб додати новий стовпець. @BrodieG вже згадав про це у своїй відповіді.

Але глибоку копію можна замінити неглибокою, коли реалізовано FR # 617 . Також актуально: dplyr: FR # 614 . Зауважте, що все-таки стовпець, який ви модифікуєте, буде завжди копіюватися (тому трохи повільніше / менш ефективно використовувати пам’ять). Неможливо оновити стовпці за посиланням.

Інші функціональні можливості

  • У data.table ви можете агрегувати під час приєднання, і це простіше зрозуміти та ефективно використовувати пам’ять, оскільки проміжний результат об’єднання ніколи не матеріалізується. Перегляньте цей пост для прикладу. Ви не можете (наразі?) Зробити це, використовуючи синтаксис data.table / data.frame dplyr.

  • Функція рухомого з’єднання 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 для мене досить швидкий, а реалізація планувальника / оптимізатора запитів порівняно складна.
hadley

Ефективність пам’яті також допомагає в іншій важливій області - фактичному виконанні завдання перед тим, як закінчиться пам’ять. При роботі з великими наборами даних я стикався з цією проблемою як з dplyr, так і з пандами, тоді як 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,

Шановний Г. Гротендік, це чудово. Дякуємо, що показали мені цю тестову програму. До речі, ви забули [order (-Count)] у версії даних, щоб зробити еквівалентність розташування dplyr (desc (Count)). Після додавання цього таблиця даних все ще швидше приблизно на x1,8 (замість 2,9).
Полімераза

@ eipi10 чи можете ви повторно запустити свою стенду з версією даних, що додається сюди (додано сортування за описом Кількість на останньому кроці): diamondsDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median) (price)), Count = .N), by = cut] [замовлення (-Count)]
Полімераза

Ще 0,013 секунди. Операція впорядкування займає навряд чи час, оскільки це просто переупорядкування фінальної таблиці, яка має лише чотири рядки.
eipi10

1
Існує кілька виправлених накладних витрат для перетворення синтаксису dplyr у синтаксис таблиці даних, тому, можливо, варто спробувати різні розміри проблем. Також я, можливо, не реалізував найефективніший код таблиці даних у dplyr; патчі завжди вітаються
hadley

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дзвінка, але існує також повністю окрема гілка для групування.


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)

Це чудово, дякую. Чи означає це, що dplyr :: group_by () подвоїть вимогу до пам'яті (порівняно із синтаксисом чистих даних) через внутрішній крок копіювання даних? Значення, якщо розмір мого об’єкта даних, що становить 1 Гб, і я використовую синтаксис з ланцюжком dplyr, подібний до синтаксису в оригінальній публікації. Мені знадобиться принаймні 2 Гб вільної пам’яті для отримання результатів?
Полімераза

2
Я відчуваю, що це виправив у версії розробника?
Hadley

@hadley, я працював із версії CRAN. Дивлячись на dev, схоже, ви частково вирішили проблему, але фактична копія залишається (не перевірялася, просто дивлячись на рядки c (20, 30:32) у R / grouped-dt.r. Зараз це, швидше за все, швидше, але Б'юся об заклад, повільним кроком є ​​копія.
BrodieG,

3
Я також чекаю функції неглибокого копіювання в data.table; до того часу я думаю, що краще бути в безпеці, ніж швидко.
Hadley

2

Ви можете використовувати dtplyr зараз, який є частиною tidyverse . Це дозволяє використовувати оператори стилю dplyr як зазвичай, але використовує ледачу оцінку та переводить ваші оператори в код data.table під капотом. Накладні витрати при перекладі мінімальні, але ви отримуєте всі, якщо ні, більшість переваг data.table. Детальніше на офіційному репозиторії git тут та на сторінці tidyverse .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.