Cercherò di dare le mie migliori guide ma non è facile perché bisogna avere familiarità con tutti i {data.table}, {dplyr}, {dtplyr} e anche {base R}. Uso {data.table} e molti pacchetti {tidy-world} (tranne {dplyr}). Adoro entrambi, anche se preferisco la sintassi di data.table a quella di dplyr. Spero che tutti i pacchetti tidy-world utilizzino {dtplyr} o {data.table} come backend ogni volta che è necessario.
Come con qualsiasi altra traduzione (pensa dplyr-to-sparkly / SQL), ci sono cose che possono o non possono essere tradotte, almeno per ora. Voglio dire, forse un giorno {dtplyr} può farlo tradotto al 100%, chissà. L'elenco che segue non è esaustivo né è corretto al 100% poiché farò del mio meglio per rispondere in base alle mie conoscenze su argomenti / pacchetti / problemi / ecc.
È importante sottolineare che, per quelle risposte che non sono del tutto esatte, spero che ti dia alcune guide su quali aspetti di {data.table} dovresti prestare attenzione e, confrontalo con {dtplyr} e scopri le risposte da solo. Non dare per scontate queste risposte.
Spero che questo post possa essere utilizzato come una delle risorse per tutti gli utenti / creatori di {dplyr}, {data.table} o {dtplyr} per discussioni e collaborazioni e rendere #RStats ancora migliore.
{data.table} non è utilizzato solo per operazioni veloci ed efficienti in termini di memoria. Ci sono molte persone, incluso me stesso, che preferiscono l'elegante sintassi di {data.table}. Include anche altre operazioni veloci come le funzioni delle serie temporali come la famiglia di rotolamento (ovvero frollapply
) scritta in C. Può essere utilizzata con qualsiasi funzione, incluso Tidyverse. Uso molto {data.table} + {purrr}!
Complessità delle operazioni
Questo può essere facilmente tradotto
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} è molto veloce ed efficiente in termini di memoria perché (quasi?) tutto è costruito da zero da C con i concetti chiave di aggiornamento per riferimento , chiave (think SQL) e la loro inarrestabile ottimizzazione ovunque nel pacchetto (ad es fifelse
.fread/fread
ordinamento radix adottato dalla base R), assicurandosi che la sintassi sia concisa e coerente, ecco perché penso che sia elegante.
Dall'introduzione a data.table , le principali operazioni di manipolazione dei dati come sottoinsieme, gruppo, aggiornamento, join, ecc. Vengono tenute insieme per
sintassi concisa e coerente ...
eseguire analisi in modo fluido senza l'onere cognitivo di dover mappare ogni operazione ...
ottimizzare automaticamente le operazioni internamente e in modo molto efficace, conoscendo con precisione i dati richiesti per ciascuna operazione, portando a un codice molto veloce ed efficiente in termini di memoria
L'ultimo punto, ad esempio,
# 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))]
Per prima cosa, effettuiamo un sottoinsieme in i per trovare indici di riga corrispondenti in cui l'aeroporto di origine è uguale a "JFK" e il mese è uguale a 6L. Non sottoponiamo ancora sottoinsieme l'intero data.table corrispondente a quelle righe.
Ora, guardiamo j e scopriamo che utilizza solo due colonne. E quello che dobbiamo fare è calcolare la loro media (). Pertanto, effettuiamo il sottoinsieme di quelle colonne corrispondenti alle righe corrispondenti e calcoliamo la loro media ().
Poiché i tre componenti principali della query (i, j e by) sono insieme all'interno [...] , data.table può vedere tutti e tre e ottimizzare la query del tutto prima della valutazione, non ciascuno separatamente . Siamo quindi in grado di evitare l'intero sottoinsieme (ovvero, il sottoinsieme delle colonne oltre a arr_delay e dep_delay), sia per la velocità che per l'efficienza della memoria.
Dato che, per trarre vantaggio da {data.table}, la traduzione di {dtplr} deve essere corretta sotto questo aspetto. Più complesse sono le operazioni, più difficili sono le traduzioni. Per operazioni semplici come sopra, può certamente essere facilmente tradotto. Per quelli complessi, o quelli non supportati da {dtplyr}, devi scoprire te stesso come menzionato sopra, uno deve confrontare la sintassi e il benchmark tradotti ed essere familiari pacchetti correlati.
Per operazioni complesse o operazioni non supportate, potrei essere in grado di fornire alcuni esempi di seguito. Ancora una volta, sto solo facendo del mio meglio. Sii gentile con me.
Aggiornamento per riferimento
Non entrerò nell'intro / nei dettagli, ma qui ci sono alcuni link
Risorsa principale: semantica di riferimento
Maggiori dettagli: Comprendere esattamente quando un data.table è un riferimento a (contro una copia di) un altro data.table
Aggiornamento per riferimento , secondo me, la caratteristica più importante di {data.table} ed è ciò che la rende così veloce ed efficiente in termini di memoria. dplyr::mutate
non lo supporta per impostazione predefinita. Dato che non ho familiarità con {dtplyr}, non sono sicuro di quanto e quali operazioni possono o non possono essere supportate da {dtplyr}. Come accennato in precedenza, dipende anche dalla complessità delle operazioni, che a loro volta incidono sulle traduzioni.
Esistono due modi per utilizzare l' aggiornamento per riferimento in {data.table}
operatore di assegnazione di {data.table} :=
set
-Family: set
, setnames
, setcolorder
, setkey
, setDT
, fsetdiff
, e molti altri
:=
è più comunemente usato rispetto a set
. Per set di dati complessi e di grandi dimensioni, l' aggiornamento per riferimento è la chiave per ottenere la massima velocità ed efficienza della memoria. Il modo semplice di pensare (non accurato al 100%, poiché i dettagli sono molto più complicati di questo in quanto comporta una copia dura / superficiale e molti altri fattori), ad esempio, hai a che fare con un set di dati di grandi dimensioni da 10 GB, con 10 colonne e 1 GB ciascuno . Per manipolare una colonna, devi gestire solo 1 GB.
Il punto chiave è che, con l' aggiornamento per riferimento , devi solo gestire i dati richiesti. Ecco perché quando si utilizza {data.table}, in particolare quando si tratta di un set di dati di grandi dimensioni, si utilizza sempre l' aggiornamento per riferimento quando possibile. Ad esempio, manipolando un set di dati di modellazione di grandi dimensioni
# 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())
)
L'operazione di nidificazione list(.SD)
potrebbe non essere supportata da {dtlyr} come usano gli utenti tidyverse tidyr::nest
? Quindi non sono sicuro che le operazioni successive possano essere tradotte come il modo di {data.table} è più veloce e meno memoria.
NOTA: il risultato di data.table è in "millisecondi", dplyr in "minuto"
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>
Esistono molti casi d'uso di aggiornamento per riferimento e persino gli utenti di {data.table} non useranno sempre la versione avanzata in quanto richiedono più codici. Sia che {dtplyr} supporti questi out-of-the-box, devi scoprire te stesso.
Aggiornamento multiplo per riferimento per le stesse funzioni
Risorsa principale: assegnazione elegante di più colonne in data.table con lapply ()
Ciò comporta sia il più comunemente usato :=
o 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(.))])
Secondo il creatore di {data.table} Matt Dowle
(Nota che potrebbe essere più comune eseguire il ciclo impostato su un numero elevato di righe rispetto a un numero elevato di colonne.)
Unisci + setkey + aggiorna per riferimento
Di recente ho avuto bisogno di un join rapido con dati relativamente grandi e modelli di join simili, quindi utilizzo la potenza di aggiornamento per riferimento , anziché i normali join. Poiché richiedono più codici, li avvolgo in un pacchetto privato con valutazione non standard per riusabilità e leggibilità dove lo chiamo setjoin
.
Ho fatto alcuni benchmark qui: join data.table + aggiornamento per riferimento + setkey
Sommario
# 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 ]
NOTA: è dplyr::left_join
stato anche testato ed è il più lento con ~ 9.000 ms, usa più memoria sia di {data.table} update_by_reference
e setkey_n_update
, ma usa meno memoria di normal_join di {data.table}. Ha consumato circa ~ 2,0 GB di memoria. Non l'ho incluso perché voglio concentrarmi esclusivamente su {data.table}.
Risultati chiave
setkey + update
e update
sono ~ 11 e ~ 6,5 volte più veloci di normal join
, rispettivamente
- al primo join, le prestazioni di
setkey + update
sono simili a update
quelle che setkey
compensano ampiamente i propri guadagni in termini di prestazioni
- al secondo e successivo join, poiché
setkey
non richiesto, setkey + update
è più veloce di update
~ 1,8 volte (o più veloce di normal join
~ 11 volte)
Esempi
Per join performanti ed efficienti in termini di memoria, utilizzare uno update
o setkey + update
, dove quest'ultimo è più veloce al costo di più codici.
Vediamo alcuni pseudo codici, per brevità. Le logiche sono le stesse.
Per una o poche colonne
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, ...) ]
Per molte colonne
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) ) ]
Wrapper per join veloci ed efficienti in termini di memoria ... molti di loro ... con un pattern di join simile, avvolgili come setjoin
sopra - con update
- con o senzasetkey
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(...)
Con setkey
, l'argomento on
può essere omesso. Può anche essere incluso per la leggibilità, in particolare per la collaborazione con altri.
Grande operazione su file
- come menzionato sopra, utilizzare
set
- prepopolare la tabella, utilizzare le tecniche di aggiornamento per riferimento
- sottoinsieme usando chiave (es.
setkey
)
Risorsa correlata: aggiungere una riga per riferimento alla fine di un oggetto data.table
Riepilogo dell'aggiornamento per riferimento
Questi sono solo alcuni casi d'uso di aggiornamento per riferimento . Ce ne sono molti altri.
Come puoi vedere, per un uso avanzato della gestione di dati di grandi dimensioni, ci sono molti casi d'uso e tecniche che utilizzano l' aggiornamento per riferimento per set di dati di grandi dimensioni. Non è così facile da usare in {data.table} e se {dtplyr} lo supporta, puoi scoprirlo da solo.
Mi concentro sull'aggiornamento per riferimento in questo post poiché penso che sia la funzionalità più potente di {data.table} per operazioni veloci ed efficienti in termini di memoria. Detto questo, ci sono molti, molti altri aspetti che lo rendono anche così efficiente e penso che non siano supportati nativamente da {dtplyr}.
Altri aspetti chiave
Ciò che è / non è supportato, dipende anche dalla complessità delle operazioni e dal fatto che coinvolga la funzionalità nativa di data.table come aggiornamento per riferimento o setkey
. E se il codice tradotto è quello più efficiente (quello che gli utenti di data.table scriveranno) è anche un altro fattore (cioè il codice viene tradotto, ma è la versione efficiente?). Molte cose sono interconnesse.
setkey
. Vedere Chiavi e sottoinsieme basato sulla ricerca binaria rapida
- Indici secondari e autoindicizzazione
- Utilizzo di .SD per l'analisi dei dati
- funzioni delle serie temporali: pensare
frollapply
. funzioni di rotolamento, aggregati di rotolamento, finestra scorrevole, media mobile
- join a rotazione , join non equi , (alcuni) join "cross"
- {data.table} ha creato le basi per la velocità e l'efficienza della memoria, in futuro può estendersi per includere molte funzioni (come il modo in cui implementano le funzioni delle serie temporali sopra menzionate)
- in generale, le più complesse operazioni su di data.table
i
, j
o by
operazioni (è possibile utilizzare quasi tutte le espressioni in là), credo che le più dure le traduzioni, soprattutto quando si combinano con l'aggiornamento per riferimento , setkey
e altri data.table nativo funziona comefrollapply
- Un altro punto è legato all'utilizzo della base R o ordinata. Uso sia data.table + tidyverse (tranne dplyr / readr / tidyr). Per le operazioni di grandi dimensioni, spesso eseguo il benchmark, ad esempio, le
stringr::str_*
funzioni famiglia vs base R e trovo che la base R sia più veloce in una certa misura e le uso. Il punto è, non tenerti solo in ordine o data.table o ..., esplora altre opzioni per portare a termine il lavoro.
Molti di questi aspetti sono interconnessi con i punti sopra menzionati
Puoi scoprire se {dtplyr} supporta queste operazioni soprattutto quando sono combinate.
Un altro utile trucchetto quando si ha a che fare con set di dati piccoli o grandi, durante la sessione interattiva, {data.table} mantiene davvero la promessa di ridurre notevolmente i tempi di programmazione e di calcolo .
Chiave di impostazione per la variabile utilizzata ripetutamente sia per la velocità che per i "rownames sovralimentati" (sottoinsieme senza specificare il nome della variabile).
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, ...]
Se le tue operazioni coinvolgono solo quelle semplici come nel primo esempio, {dtplyr} può portare a termine il lavoro. Per quelli complessi / non supportati, è possibile utilizzare questa guida per confrontare quelli tradotti da {dtplyr} con il modo in cui gli utenti data.table stagionati codificherebbero in modo rapido e efficiente in termini di memoria con l'elegante sintassi di data.table. La traduzione non significa che sia il modo più efficiente in quanto potrebbero esserci diverse tecniche per gestire diversi casi di dati di grandi dimensioni. Per un set di dati ancora più grande, puoi combinare {data.table} con {disk.frame} , {fst} e {drake} e altri fantastici pacchetti per ottenere il meglio. C'è anche un {big.data.table} ma è attualmente inattivo.
Spero che aiuti tutti. Buona giornata ☺☺
dplyr
cui non puoi fare benedata.table
? In caso contrario, passaredata.table
a sarà migliore didtplyr
.