Я спробую дати свої найкращі вказівки, але це непросто, оскільки потрібно ознайомитись з усіма {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) - це ще один фактор (тобто код переведений, але чи є це версія з ефективним?). Багато речей пов’язані між собою.
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
.