Я постараюсь дать мои лучшие руководства, но это не легко, потому что нужно знать все {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), также является еще одним фактором (т. Е. Код переведен, но является ли он эффективной версией?). Многие вещи взаимосвязаны.
setkey
, См Ключи и быстрый набор на основе двоичного поиска
- Вторичные индексы и автоиндексирование
- Использование .SD для анализа данных
- функции временного ряда: подумайте
frollapply
. функции качения, подвижные агрегаты, раздвижное окно, скользящее среднее
- переходящее соединение , неравное соединение , (некоторые) "перекрестное" соединение
- {data.table} заложил основу в скорости и эффективности памяти, в будущем он может расширяться и включать много функций (например, как они реализуют функции временных рядов, упомянутые выше)
- в целом, более сложные операции по data.table - х
i
, j
или by
операций (вы можете использовать практически любые выражения в там), я думаю , тем труднее переводы, особенно когда она сочетается с обновлением по ссылке , setkey
и другим родной data.table функции какfrollapply
- Еще один момент связан с использованием базы R или Tidyverse. Я использую оба data.table + tidyverse (кроме dplyr / readr / tidyr). Для больших операций я часто сравниваю, например, функции
stringr::str_*
семейства и базы R, и нахожу, что базы R в некоторой степени быстрее и используют их. Дело в том, что не оставляйте себя только на tidyverse или 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}, но в настоящее время он неактивен.
Надеюсь, это поможет всем. Хорошего дня ☺☺
dplyr
котором вы не можете преуспетьdata.table
? Если нет, переключениеdata.table
будет лучше, чемdtplyr
.