Що я не можу зробити з 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
З dtplyrreadme: "Деякі 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. Я сподіваюсь, що всі пакети в охайному світі використовуватимуть {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порядок сортування в radix, прийнятий базою 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. Ми ще не підмістили весь таблицю даних, що відповідає цим рядкам.

  • Тепер ми дивимось на j та виявляємо, що він використовує лише два стовпчики. І що ми повинні зробити - це обчислити їх середнє значення (). Тому ми підмножимо лише ті стовпці, що відповідають відповідним рядкам, і обчислимо їх середнє значення ().

Оскільки три основні компоненти запиту (i, j і by) знаходяться всередині [...] , data.table може переглянути всі три та повністю оптимізувати запит перед оцінкою, а не кожен окремо . Тому ми можемо уникнути всього підмножини (тобто підмножини стовпців, крім arr_delay та dep_delay), як для швидкості, так і для ефективності пам'яті.

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

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

Оновлення за довідкою

Я не буду входити в вступ / деталі, але ось кілька посилань

Основний ресурс: Довідкова семантика

Детальніше: Розуміння того, коли саме таблиця даних є посиланням на (проти копії) іншого таблиця даних

Оновлення за посиланням , на мій погляд, найважливіша особливість {data.table}, і саме це робить його таким швидким та ефективної пам'яті. dplyr::mutateне підтримує його за замовчуванням. Оскільки я не знайомий із {dtplyr}, я не впевнений, наскільки та які операції може підтримувати {dtplyr}. Як було сказано вище, це також залежить від складності операцій, що в свою чергу впливає на переклади.

Існує два способи використання оновлення за посиланням у {data.table}

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

  • set-сімейство: set, setnames, setcolorder, setkey, setDT, fsetdiff, і багато іншого

:=частіше використовується порівняно з set. Для складного та великого набору даних оновлення за посиланням є ключовим фактором для досягнення максимальної швидкості та ефективності пам'яті. Простий спосіб мислення (не на 100% точний, оскільки деталі набагато складніші, ніж це, оскільки він передбачає складну / неглибоку копію та багато інших факторів). . Щоб маніпулювати одним стовпцем, вам потрібно мати лише 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}, як користувачі підводних людей 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 + update-by-reference + 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, але використовуйте менше пам'яті, ніж нормальний_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}, але він наразі неактивний.

Я сподіваюся, що це допомагає всім. Приємного дня ☺☺


2

Приходять в голову приєднання, які не є еквівалентом, і приєднання до кочення. Здається, немає ніяких планів, щоб взагалі включати еквівалентні функції в dplyr, тому dtplyr не може нічого перекласти.

Існує також переформатування (оптимізоване dcast та розплав, еквівалентне тим же функціям у reshape2), яке також не в dplyr.

Усі функції * _if та * _at наразі теж не можуть бути перекладені за допомогою dtplyr, але вони є у роботі.


0

Оновіть стовпчик при приєднанні Деякі .SD-трюки Багато функцій f І Бог знає що ще, тому що #rdatatable - це не просто проста бібліотека, і її неможливо підсумувати кількома функціями

Це сама екосистема

Я ніколи не потребував dplyr з того дня, коли я почав R. Тому що data.table так проклято добре

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