Что я не могу сделать с dtplyr, что я могу в data.table

9

Должен ли я инвестировать свои усилия в обучение для обработки данных в R, особенно между dplyr, dtplyrи data.table?

  • Я использую в dplyrосновном, но когда данные слишком велики для этого, я буду использовать data.table, что встречается редко. Итак, теперь, когда dtplyrv1.0 вышел в качестве интерфейса data.table, на первый взгляд кажется, что мне больше не нужно беспокоиться об использовании data.tableинтерфейса.

  • Итак, какие наиболее полезные функции или аспекты data.tableэтого нельзя сделать dtplyrв данный момент, и что, скорее всего, никогда не будет сделано dtplyr?

  • На первый взгляд, dplyrс преимуществами data.tableзвучит как будто dtplyrобгонит dplyr. Будет ли какая-либо причина для использования, dplyrкогда dtplyrона полностью созреет?

Примечание: я не спрашиваю о dplyrvs data.table(как в data.table vs dplyr: один может делать что-то хорошо, другой не может или плохо? ), Но, учитывая, что один предпочтительнее другого для конкретной проблемы, почему бы не т dtplyrбыть инструментом для использования.

дуле арно
источник
1
Есть ли что-то, в чем вы можете преуспеть, в dplyrкотором вы не можете преуспеть data.table? Если нет, переключение data.tableбудет лучше, чем dtplyr.
sindri_baldur
2
Из dtplyrфайла readme: «Некоторые data.tableвыражения не имеют прямого dplyrэквивалента. Например, нет возможности выразить перекрестные или переходные соединения с помощью dplyr. ' и «Чтобы соответствовать dplyrсемантике, mutate() не изменяется на месте по умолчанию. Это означает, что большинство задействованных выражений mutate()должны делать копии, которые не были бы необходимы, если бы вы использовали data.tableнапрямую ». Во второй части есть обходной путь, но, учитывая частоту mutateего использования, это довольно большой недостаток в моих глазах.
ClancyStats

Ответы:

15

Я постараюсь дать мои лучшие руководства, но это не легко, потому что нужно знать все {data.table}, {dplyr}, {dtplyr}, а также {base R}. Я использую {data.table} и много пакетов {tidy-world} (кроме {dplyr}). Люблю оба, хотя я предпочитаю синтаксис data.table dplyr. Я надеюсь, что все пакеты tidy-world будут использовать {dtplyr} или {data.table} в качестве бэкэнда, когда это будет необходимо.

Как и в случае любого другого перевода (например, dplyr-to-sparkly / SQL), есть вещи, которые можно или нельзя перевести, по крайней мере, на данный момент. Я имею в виду, может быть, однажды {dtplyr} сможет сделать перевод на 100%, кто знает. Приведенный ниже список не является исчерпывающим и не является на 100% правильным, так как я постараюсь ответить наилучшим образом, основываясь на моих знаниях по смежным темам / пакетам / вопросам / и т. Д.

Важно отметить, что для тех ответов, которые не совсем точны, я надеюсь, что это даст вам некоторые рекомендации о том, на какие аспекты {data.table} вы должны обратить внимание, и сравните его с {dtplyr} и выясните ответы самостоятельно. Не принимайте эти ответы как должное.

И я надеюсь, что этот пост можно использовать как один из ресурсов для всех пользователей / создателей {dplyr}, {data.table} или {dtplyr} для обсуждений и совместной работы, а также для улучшения #RStats.

{data.table} используется не только для быстрых и эффективных операций с памятью. Многие люди, включая меня, предпочитают элегантный синтаксис {data.table}. Он также включает в себя другие быстрые операции, такие как функции временных рядов, такие как скользящее семейство (то есть frollapply), написанные на C. Он может использоваться с любыми функциями, включая Tidyverse. Я использую {data.table} + {purrr} много!

Сложность операций

Это можно легко перевести

library(data.table)
library(dplyr)
library(flights)
data <- data.table(diamonds)

# dplyr 
diamonds %>%
  filter(cut != "Fair") %>% 
  group_by(cut) %>% 
  summarize(
    avg_price    = mean(price),
    median_price = as.numeric(median(price)),
    count        = n()
  ) %>%
  arrange(desc(count))

# data.table
data [
  ][cut != 'Fair', by = cut, .(
      avg_price    = mean(price),
      median_price = as.numeric(median(price)),
      count        = .N
    )
  ][order( - count)]

{data.table} очень быстрый и эффективно использует память, потому что (почти?) все построено с нуля из C с ключевыми понятиями « обновление по ссылке , ключ (думаю, SQL)» и их постоянная оптимизация повсюду в пакете (то есть fifelse, fread/fread, радикс порядок сортировки принят базовый R), а убедившись , что синтаксис кратким и последовательным, поэтому я думаю , что это элегантно.

От введения в data.table основные операции с данными, такие как подмножество, группа, обновление, объединение и т. Д. , Хранятся вместе для

  • краткий и непротиворечивый синтаксис ...

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

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

Последний пункт, как пример,

# Calculate the average arrival and departure delay for all flights with “JFK” as the origin airport in the month of June.
flights[origin == 'JFK' & month == 6L,
        .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
  • Мы сначала подмножество в i, чтобы найти совпадающие индексы строк, где аэропорт отправления равен "JFK", а месяц равен 6L. Мы еще не помещаем в таблицу весь data.table, соответствующий этим строкам.

  • Теперь мы смотрим на j и обнаруживаем, что он использует только два столбца. И что нам нужно сделать, это вычислить их среднее значение (). Поэтому мы устанавливаем только те столбцы, которые соответствуют соответствующим строкам, и вычисляем их среднее значение ().

Поскольку три основных компонента запроса (i, j и by) находятся вместе внутри [...] , data.table может видеть все три и оптимизировать запрос в целом перед оценкой, а не каждый в отдельности . Поэтому мы можем избежать всего подмножества (то есть подмножества столбцов, кроме arr_delay и dep_delay), как для скорости, так и для эффективности памяти.

Учитывая это, чтобы воспользоваться преимуществами {data.table}, перевод {dtplr} должен быть правильным в этом отношении. Чем сложнее операции, тем сложнее переводы. Для простых операций, как указано выше, это, безусловно, можно легко перевести. Для сложных или тех, которые не поддерживаются {dtplyr}, вы должны выяснить себя, как упомянуто выше, нужно сравнить переведенный синтаксис и тест и ознакомиться с соответствующими пакетами.

Для сложных операций или неподдерживаемых операций я мог бы привести несколько примеров ниже. Опять же, я просто стараюсь изо всех сил. Будьте нежны со мной.

Обновление по ссылке

Я не буду вдаваться в подробности, но вот несколько ссылок

Основной ресурс: Ссылочная семантика

Более подробная информация: точно знать, когда data.table является ссылкой (против копии) другой data.table

Обновление по ссылке , на мой взгляд, самая важная особенность {data.table}, и это делает его таким быстрым и эффективным для памяти. dplyr::mutateне поддерживает его по умолчанию. Поскольку я не знаком с {dtplyr}, я не уверен, сколько и какие операции могут или не могут поддерживаться {dtplyr}. Как упоминалось выше, это также зависит от сложности операций, которые, в свою очередь, влияют на переводы.

Есть два способа использовать обновление по ссылке в {data.table}

  • оператор присваивания {data.table} :=

  • set-семейством: set, setnames, setcolorder, setkey, setDT, fsetdiff, и многое другое

:=чаще используется по сравнению с set. Для сложного и большого набора данных, обновление по ссылке является ключом к максимальной скорости и эффективности использования памяти. Простой способ мышления (не на 100% точный, поскольку детали намного сложнее, чем это, поскольку он включает в себя твердое / мелкое копирование и многие другие факторы), скажем, вы имеете дело с большим набором данных размером 10 ГБ, с 10 столбцами и 1 ГБ каждый , Чтобы манипулировать одним столбцом, вам нужно иметь дело только с 1 ГБ.

Ключевым моментом является то, что при обновлении по ссылке вам нужно иметь дело только с необходимыми данными. Вот почему при использовании {data.table}, особенно при работе с большим набором данных, мы всегда используем обновление по ссылке, когда это возможно. Например, манипулирование большим набором данных моделирования

# Manipulating list columns

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- data.table(df)

# data.table
dt [,
    by = Species, .(data   = .( .SD )) ][,  # `.(` shorthand for `list`
    model   := map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )) ][,
    summary := map(model, summary) ][,
    plot    := map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                           geom_point())]

# dplyr
df %>% 
  group_by(Species) %>% 
  nest() %>% 
  mutate(
    model   = map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )),
    summary = map(model, summary),
    plot    = map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                          geom_point())
  )

Операция вложенности list(.SD)может не поддерживаться {dtlyr}, поскольку пользователи tidyverse используют tidyr::nest? Поэтому я не уверен, что последующие операции можно перевести как способ {data.table} быстрее и меньше памяти.

ПРИМЕЧАНИЕ: результат data.table находится в «миллисекундах», dplyr в «минутах»

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- copy(data.table(df))

bench::mark(
  check = FALSE,

  dt[, by = Species, .(data = list(.SD))],
  df %>% group_by(Species) %>% nest()
)
# # A tibble: 2 x 13
#   expression                                   min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#   <bch:expr>                              <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
# 1 dt[, by = Species, .(data = list(.SD))] 361.94ms 402.04ms   2.49      705.8MB     1.24     2     1
# 2 df %>% group_by(Species) %>% nest()        6.85m    6.85m   0.00243     1.4GB     2.28     1   937
# # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>, time <list>,
# #   gc <list>

Есть много вариантов использования обновления по ссылке и даже пользователи {data.table} не будут постоянно использовать его расширенную версию, так как для этого требуется больше кодов. Независимо от того, поддерживает ли {dtplyr} это "из коробки", вы должны выяснить сами.

Многократное обновление по ссылке для одинаковых функций

Основной ресурс: элегантное назначение нескольких столбцов в data.table с помощью lapply ()

Это включает в себя либо наиболее часто используемые :=или set.

dt <- data.table( matrix(runif(10000), nrow = 100) )

# A few variants

for (col in paste0('V', 20:100))
  set(dt, j = col, value = sqrt(get(col)))

for (col in paste0('V', 20:100))
  dt[, (col) := sqrt(get(col))]

# I prefer `purrr::map` to `for`
library(purrr)
map(paste0('V', 20:100), ~ dt[, (.) := sqrt(get(.))])

По словам создателя {data.table} Мэтта Доула

(Обратите внимание, что может быть более распространено циклическое задание для большого количества строк, чем для большого количества столбцов.)

Присоединяйтесь + setkey + обновление по ссылке

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

Я сделал некоторые тесты здесь: data.table join + обновление по ссылке + setkey

Резюме

# For brevity, only the codes for join-operation are shown here. Please refer to the link for details

# Normal_join
x <- y[x, on = 'a']

# update_by_reference
x_2[y_2, on = 'a', c := c]

# setkey_n_update
setkey(x_3, a) [ setkey(y_3, a), on = 'a', c := c ]

ПРИМЕЧАНИЕ: dplyr::left_joinтакже был протестирован и является самым медленным с ~ 9000 мс, использует больше памяти, чем оба {data.table} update_by_referenceи setkey_n_update, но использует меньше памяти, чем normal_join {data.table}. Он занимал около 2,0 ГБ памяти. Я не включил его, поскольку хочу сосредоточиться исключительно на {data.table}.

Основные результаты

  • setkey + updateи updateв ~ 11 и ~ 6,5 раз быстрее, чем normal joinсоответственно
  • при первом соединении производительность setkey + updateаналогична тем, updateчто накладные расходы в setkeyзначительной степени компенсируют собственное повышение производительности
  • во втором и последующих соединениях, как setkeyэто не требуется, setkey + updateбыстрее, чем updateв ~ 1,8 раза (или быстрее, чем normal joinв ~ 11 раз)

Образ

Примеры

Для соединений с высокой производительностью и эффективностью использования памяти используйте либо, updateлибо setkey + update, если последнее выполняется быстрее за счет большего количества кодов.

Давайте рассмотрим некоторые псевдокоды , для краткости. Логика одинаковая.

Для одного или нескольких столбцов

a <- data.table(x = ..., y = ..., z = ..., ...)
b <- data.table(x = ..., y = ..., z = ..., ...)

# `update`
a[b, on = .(x), y := y]
a[b, on = .(x),  `:=` (y = y, z = z, ...)]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), y := y ]
setkey(a, x) [ setkey(b, x), on = .(x),  `:=` (y = y, z = z, ...) ]

Для многих столбцов

cols <- c('x', 'y', ...)
# `update`
a[b, on = .(x), (cols) := mget( paste0('i.', cols) )]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), (cols) := mget( paste0('i.', cols) ) ]

Обертка для быстрого и эффективного использования памяти ... многие из них ... с одинаковым шаблоном соединения, оберните их, как setjoinописано выше - с update - с или безsetkey

setjoin(a, b, on = ...)  # join all columns
setjoin(a, b, on = ..., select = c('columns_to_be_included', ...))
setjoin(a, b, on = ..., drop   = c('columns_to_be_excluded', ...))
# With that, you can even use it with `magrittr` pipe
a %>%
  setjoin(...) %>%
  setjoin(...)

С setkeyаргументом onможно опустить. Он также может быть включен для удобства чтения, особенно для сотрудничества с другими.

Большой ряд операций

  • как уже упоминалось выше, используйте set
  • предварительно заполнить таблицу, использовать методы обновления по ссылке
  • подмножество с использованием ключа (то есть setkey)

Связанный ресурс: добавьте строку по ссылке в конце объекта data.table

Резюме обновления по ссылке

Это всего лишь несколько случаев использования обновления по ссылке . Есть много других.

Как вы можете видеть, для расширенного использования при работе с большими данными существует множество вариантов использования и методов, использующих обновление по ссылке для большого набора данных. Это не так просто использовать в {data.table}, и если {dtplyr} поддерживает это, вы можете узнать сами.

В этом посте я остановлюсь на обновлении по ссылке, так как считаю, что это самая мощная функция {data.table} для быстрой и эффективной работы с памятью. Тем не менее, есть много, много других аспектов, которые делают его таким эффективным, и я думаю, что он не поддерживается {dtplyr}.

Другие ключевые аспекты

Что поддерживается / не поддерживается, это также зависит от сложности операций и от того, включает ли это встроенную функцию data.table, такую ​​как обновление по ссылке или setkey. И является ли переведенный код более эффективным (тот, который пишут пользователи data.table), также является еще одним фактором (т. Е. Код переведен, но является ли он эффективной версией?). Многие вещи взаимосвязаны.

Многие из этих аспектов взаимосвязаны с пунктами, упомянутыми выше

  • сложность операций

  • обновление по ссылке

Вы можете узнать, поддерживают ли {dtplyr} эти операции, особенно когда они объединены.

Еще один полезный прием при работе с маленьким или большим набором данных во время интерактивного сеанса {data.table} действительно оправдывает свое обещание значительно сократить время программирования и вычислений .

Установка ключа для постоянно используемой переменной как для скорости, так и для «перегруженных имен строк» ​​(подмножество без указания имени переменной).

dt <- data.table(iris)
setkey(dt, Species) 

dt['setosa',    do_something(...), ...]
dt['virginica', do_another(...),   ...]
dt['setosa',    more(...),         ...]

# `by` argument can also be omitted, particularly useful during interactive session
# this ultimately becomes what I call 'naked' syntax, just type what you want to do, without any placeholders. 
# It's simply elegant
dt['setosa', do_something(...), Species, ...]

Если ваши операции включают только простые, как в первом примере, {dtplyr} может выполнить работу. Для сложных / неподдерживаемых вы можете использовать это руководство, чтобы сравнить переведенные {dtplyr} с тем, как опытные пользователи data.table будут быстро и эффективно кодировать код с элегантным синтаксисом data.table. Перевод не означает, что это самый эффективный способ, так как могут быть разные методы для обработки больших объемов данных. Для еще большего набора данных вы можете объединить {data.table} с {disk.frame} , {fst} и {drake} и другими удивительными пакетами, чтобы получить лучшее из этого. Существует также {big.data.table}, но в настоящее время он неактивен.

Надеюсь, это поможет всем. Хорошего дня ☺☺

K22
источник
2

На ум приходят неуравновешенные и подвижные соединения. Похоже, что нет никаких планов по включению эквивалентных функций в dplyr, поэтому dtplyr ничего не нужно переводить.

Также есть изменение формы (оптимизированный dcast и melt, эквивалентный тем же функциям в reshape2), которого нет и в dplyr.

Все функции * _if и * _at в настоящее время также не могут быть переведены с помощью dtplyr, но они находятся в разработке.

EdTeD
источник
0

Обновление столбца при соединении. Некоторые трюки .SD. Многие функции f. И Бог знает, что еще, потому что #rdatatable - это больше, чем просто библиотека, и ее нельзя суммировать с помощью нескольких функций.

Это целая экосистема сама по себе

Мне никогда не был нужен dplyr с того дня, как я начал R. Поскольку data.table чертовски хорош

Викрам
источник