Cosa non posso fare con dtplyr che posso in data.table


9

Dovrei investire il mio sforzo di apprendimento per la lotta ai dati in R, in particolare tra dplyr, dtplyre data.table?

  • Uso dplyrprincipalmente, ma quando i dati sono troppo grandi per quello che userò data.table, il che è un evento raro. Quindi ora che dtplyrv1.0 è disponibile come interfaccia per data.table, in superficie sembra che non mi debba mai più preoccupare di usare l' data.tableinterfaccia.

  • Quindi quali sono le caratteristiche o gli aspetti più utili di data.tableciò che al momento non si possono fare dtplyre che probabilmente non verranno mai fatti dtplyr?

  • A prima vista, dplyrcon i vantaggi di data.tablefa sembrare dtplyrche sorpasserà dplyr. Ci sarà qualche motivo per usare dplyruna volta che dtplyrè completamente maturato?

Nota: non sto chiedendo dplyr vs data.table(come in data.table vs dplyr: uno può fare qualcosa di buono l'altro non può o fa male? ), Ma dato che uno è preferito rispetto all'altro per un problema particolare, perché non dovrebbe t dtplyrtramite lo strumento da utilizzare.


1
C'è qualcosa in cui puoi fare bene in dplyrcui non puoi fare bene data.table? In caso contrario, passare data.tablea sarà migliore di dtplyr.
sindri_baldur,

2
Dal dtplyrreadme, 'Alcune data.tableespressioni non hanno dplyrequivalenti diretti . Ad esempio, non c'è modo di esprimere i cross-o rolling-join con dplyr". e 'Per abbinare la dplyrsemantica, mutate() non viene modificato in posizione per impostazione predefinita. Ciò significa che la maggior parte delle espressioni che coinvolgono mutate()devono fare una copia che non sarebbe necessaria se si stesse usando data.tabledirettamente. " C'è un modo per aggirare quella seconda parte, ma considerando la frequenza con cui mutateviene utilizzato, questo è un aspetto negativo piuttosto grande per i miei occhi.
ClancyStats,

Risposte:


15

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::mutatenon 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_joinstato anche testato ed è il più lento con ~ 9.000 ms, usa più memoria sia di {data.table} update_by_referencee 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 + updatee updatesono ~ 11 e ~ 6,5 volte più veloci di normal join, rispettivamente
  • al primo join, le prestazioni di setkey + updatesono simili a updatequelle che setkeycompensano ampiamente i propri guadagni in termini di prestazioni
  • al secondo e successivo join, poiché setkeynon richiesto, setkey + updateè più veloce di update~ 1,8 volte (o più veloce di normal join~ 11 volte)

Immagine

Esempi

Per join performanti ed efficienti in termini di memoria, utilizzare uno updateo 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 setjoinsopra - 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 onpuò 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.

Molti di questi aspetti sono interconnessi con i punti sopra menzionati

  • complessità delle operazioni

  • aggiornamento per riferimento

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 ☺☺


2

Mi vengono in mente i join non equi e i rolling rolling. Non sembra esserci alcun piano per includere funzioni equivalenti in dplyr, quindi non c'è niente da tradurre per dtplyr.

C'è anche il rimodellamento (dcast e melt ottimizzati equivalenti alle stesse funzioni in reshape2) che non è anche in dplyr.

Tutte le funzioni * _if e * _at al momento non possono essere tradotte con dtplyr ma quelle sono in lavorazione.


0

Aggiorna una colonna su join Alcuni trucchi .SD Molte funzioni f E dio sa cos'altro perché #rdatatable è più di una semplice libreria e non può essere sintetizzato con poche funzioni

È un intero ecosistema da solo

Non ho mai avuto bisogno di dplyr dal giorno in cui ho iniziato R. Perché data.table è così dannatamente buono

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.