dplyr su data.table, sto davvero usando data.table?


91

Se utilizzo la sintassi dplyr sopra un datatable , ottengo tutti i vantaggi in termini di velocità di datatable pur utilizzando la sintassi di dplyr? In altre parole, uso impropriamente il datatable se lo interrogo con la sintassi dplyr? Oppure devo usare la sintassi datatable pura per sfruttare tutta la sua potenza.

Grazie in anticipo per qualsiasi consiglio. Esempio di codice:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count))

Risultati:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

Ecco l'equivalenza databile che mi è venuta. Non sono sicuro che sia conforme alla buona pratica DT. Ma mi chiedo se il codice sia davvero più efficiente della sintassi dplyr dietro le quinte:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]

7
Perché non dovresti usare la sintassi della tabella dati? È anche elegante ed efficiente. La domanda non è realmente rispondente in quanto è molto ampia. Sì, ci sono dplyrmetodi per le tabelle di dati, ma anche la tabella di dati ha i suoi metodi comparabili
Rich Scriven

7
Posso usare la sintassi o il corso datatable. Ma in qualche modo, trovo la sintassi dplyr più elegante. Indipendentemente dalla mia preferenza per la sintassi. Quello che voglio davvero sapere è: devo usare la sintassi datatable pura per ottenere il 100% di vantaggi dalla potenza datatable.
Polymerase

3
Per un benchmark recente in cui dplyrviene utilizzato su se data.framecorrispondenti data.tables, vedere qui (e riferimenti in esso).
Henrik

2
@Polymerase - Penso che la risposta a questa domanda sia decisamente "Sì"
Rich Scriven

1
@ Henrik: mi sono reso conto in seguito di aver interpretato male quella pagina perché mostravano solo il codice per la costruzione del dataframe ma non il codice che usavano per la costruzione di data.table. Quando me ne sono reso conto, ho cancellato il mio commento (sperando che tu non lo avessi visto).
IRTFM

Risposte:


77

Non esiste una risposta diretta / semplice perché le filosofie di entrambi questi pacchetti differiscono per certi aspetti. Quindi alcuni compromessi sono inevitabili. Ecco alcune delle preoccupazioni che potresti dover affrontare / considerare.

Operazioni che coinvolgono i(== filter()e slice()in dplyr)

Supponiamo DTcon diciamo 10 colonne. Considera queste espressioni data.table:

DT[a > 1, .N]                    ## --- (1)
DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)

(1) fornisce il numero di righe in DTcui colonna a > 1. (2) restituisce mean(b)raggruppati per c,dper la stessa espressione in i(1).

Le dplyrespressioni comunemente usate sarebbero:

DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)

Chiaramente, i codici data.table sono più brevi. Inoltre sono anche più efficienti in termini di memoria 1 . Perché? Perché sia ​​in (3) che in (4), filter()restituisce prima le righe per tutte le 10 colonne , quando in (3) abbiamo solo bisogno del numero di righe e in (4) abbiamo solo bisogno di colonne b, c, dper le operazioni successive. Per ovviare a questo, dobbiamo select()colonne apriori:

DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)

È essenziale evidenziare un'importante differenza filosofica tra i due pacchetti:

  • In data.table, ci piace tenere insieme queste operazioni correlate, e questo consente di guardare j-expression(dalla stessa chiamata di funzione) e rendersi conto che non c'è bisogno di colonne in (1). L'espressione in iviene calcolata ed .Nè solo la somma di quel vettore logico che fornisce il numero di righe; l'intero sottoinsieme non viene mai realizzato. In (2), solo la colonna b,c,dviene materializzata nel sottoinsieme, le altre colonne vengono ignorate.

  • Ma in dplyr, la filosofia è quella di avere una funzione di fare esattamente una cosa bene . Non c'è (almeno attualmente) alcun modo per sapere se l'operazione successiva filter()necessita di tutte quelle colonne che abbiamo filtrato. Dovrai pensare al futuro se desideri eseguire tali attività in modo efficiente. Personalmente lo trovo controintutitivo in questo caso.

Nota che in (5) e (6), abbiamo ancora sottoinsiemi di colonne ache non richiedono. Ma non sono sicuro di come evitarlo. Se la filter()funzione avesse un argomento per selezionare le colonne da restituire, potremmo evitare questo problema, ma in tal caso la funzione non eseguirà solo un'attività (che è anche una scelta di progettazione di dplyr).

Sottoassegna per riferimento

dplyr non si aggiornerà mai per riferimento. Questa è un'altra enorme differenza (filosofica) tra i due pacchetti.

Ad esempio, in data.table puoi fare:

DT[a %in% some_vals, a := NA]

che aggiorna la colonna a per riferimento solo sulle righe che soddisfano la condizione. Al momento dplyr copia in profondità l'intero data.table internamente per aggiungere una nuova colonna. @BrodieG lo ha già menzionato nella sua risposta.

Ma la copia completa può essere sostituita da una copia superficiale quando viene implementato FR # 617 . Rilevante anche: dplyr: FR # 614 . Nota che comunque, la colonna che modifichi verrà sempre copiata (quindi un po 'più lenta / meno efficiente in termini di memoria). Non sarà possibile aggiornare le colonne per riferimento.

Altre funzionalità

  • In data.table, puoi aggregare durante l'unione, e questo è più semplice da capire ed è efficiente in termini di memoria poiché il risultato del join intermedio non viene mai materializzato. Controlla questo post per un esempio. Non puoi (al momento?) Farlo usando la sintassi data.table / data.frame di dplyr.

  • La funzione di join rotanti di data.table non è supportata anche nella sintassi di dplyr.

  • Recentemente abbiamo implementato i join sovrapposti in data.table per unire su intervalli di intervallo ( ecco un esempio ), che è una funzione separata foverlaps()al momento, e quindi potrebbe essere utilizzato con gli operatori pipe (magrittr / pipeR? - non l'ho mai provato io stesso).

    Ma in definitiva, il nostro obiettivo è integrarlo in [.data.tablemodo da poter raccogliere le altre funzionalità come il raggruppamento, l'aggregazione durante l'adesione, ecc., Che avranno le stesse limitazioni descritte sopra.

  • Dalla 1.9.4, data.table implementa l'indicizzazione automatica utilizzando chiavi secondarie per sottoinsiemi basati sulla ricerca binaria veloce sulla sintassi R. Es: DT[x == 1]e DT[x %in% some_vals]creerà automaticamente un indice alla prima esecuzione, che verrà quindi utilizzato nei sottoinsiemi successivi dalla stessa colonna al sottoinsieme veloce utilizzando la ricerca binaria. Questa funzionalità continuerà ad evolversi. Controlla questa sintesi per una breve panoramica di questa funzione.

    Dal modo in cui filter()è implementato per data.tables, non sfrutta questa funzionalità.

  • Una caratteristica di dplyr è che fornisce anche l' interfaccia ai database utilizzando la stessa sintassi, che data.table non al momento.

Quindi, dovrai soppesare questi (e probabilmente altri punti) e decidere in base al fatto che questi compromessi siano accettabili per te.

HTH


(1) Si noti che l'efficienza della memoria influisce direttamente sulla velocità (soprattutto quando i dati diventano più grandi), poiché il collo di bottiglia nella maggior parte dei casi è lo spostamento dei dati dalla memoria principale alla cache (e l'utilizzo dei dati nella cache il più possibile - ridurre i mancati riscontri nella cache - in modo da ridurre gli accessi alla memoria principale). Non entrare nei dettagli qui.


4
Assolutamente brillante. Grazie per questo
David Arenburg

6
Questa è una buona risposta, ma sarebbe possibile (se non probabile) per dplyr implementare un filter()plus efficiente summarise()utilizzando lo stesso approccio che dplyr utilizza per SQL, ovvero creare un'espressione e quindi eseguirla solo una volta su richiesta. È improbabile che ciò venga implementato nel prossimo futuro perché dplyr è abbastanza veloce per me e l'implementazione di un pianificatore / ottimizzatore di query è relativamente difficile.
Hadley

Essere efficiente in termini di memoria aiuta anche in un'altra area importante: completare l'attività prima di esaurire la memoria. Quando lavoro con set di dati di grandi dimensioni, ho affrontato questo problema con dplyr e panda, mentre data.table avrebbe completato il lavoro con grazia.
Zaki

25

Provalo e basta.

library(rbenchmark)
library(dplyr)
library(data.table)

benchmark(
dplyr = diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count)),
data.table = diamondsDT[cut != "Fair", 
                        list(AvgPrice = mean(price),
                             MedianPrice = as.numeric(median(price)),
                             Count = .N), by = cut][order(-Count)])[1:4]

Su questo problema sembra che data.table sia 2,4 volte più veloce di dplyr usando data.table:

        test replications elapsed relative
2 data.table          100    2.39    1.000
1      dplyr          100    5.77    2.414

Rivisto in base al commento di Polymerase.


2
Utilizzando il microbenchmarkpacchetto, ho scoperto che l'esecuzione del dplyrcodice OP sulla versione originale (data frame) di diamondsrichiedeva un tempo medio di 0,012 secondi, mentre ci voleva un tempo mediano di 0,024 secondi dopo la conversione diamondsin una tabella dati. L'esecuzione del data.tablecodice di G. Grothendieck ha richiesto 0,013 secondi. Almeno sul mio sistema, sembra dplyre data.tableha circa le stesse prestazioni. Ma perché dovrebbe dplyressere più lento quando il frame di dati viene prima convertito in una tabella di dati?
eipi10

Caro G. Grothendieck, è meraviglioso. Grazie per avermi mostrato questa utility di benchmark. A proposito, hai dimenticato [order (-Count)] nella versione datatable per rendere l'equivalenza di dplyr's arrangia (desc (Count)). Dopo aver aggiunto questo, datatable è ancora più veloce di circa x1.8 (invece di 2.9).
Polimerasi

@ eipi10 puoi rieseguire il tuo banco con la versione databile qui (aggiunto l'ordinamento per desc Count nell'ultimo passaggio): diamondsDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median (prezzo)), Count = .N), by = cut] [order (-Count)]
Polymerase

Ancora 0,013 secondi. L'operazione di ordinamento richiede pochissimo tempo perché sta solo riordinando il tavolo finale, che ha solo quattro righe.
eipi10

1
C'è un sovraccarico fisso per la conversione dalla sintassi dplyr alla sintassi della tabella dati, quindi potrebbe valere la pena provare a variare le dimensioni del problema. Inoltre potrei non aver implementato il codice della tabella dati più efficiente in dplyr; le patch sono sempre benvenute
hadley il

22

Per rispondere alle tue domande:

  • Sì, stai usando data.table
  • Ma non in modo efficiente come faresti con la data.tablesintassi pura

In molti casi questo sarà un compromesso accettabile per coloro che desiderano la dplyrsintassi, anche se potrebbe essere più lento rispetto dplyrai semplici frame di dati.

Un fattore importante sembra essere che dplyrcopierà data.tableper impostazione predefinita durante il raggruppamento. Considera (usando microbenchmark):

Unit: microseconds
                                                               expr       min         lq    median
                                diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                          diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
 diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                               diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609

Il filtraggio è di velocità paragonabile, ma il raggruppamento no. Credo che il colpevole sia questa riga in dplyr:::grouped_dt:

if (copy) {
    data <- data.table::copy(data)
}

dove il copyvalore predefinito è TRUE(e non può essere facilmente modificato in FALSE che posso vedere). Questo probabilmente non rappresenta il 100% della differenza, ma l'overhead generale da solo su qualcosa delle dimensioni di diamondsmolto probabilmente non è la differenza completa.

Il problema è che per avere una grammatica coerente, dplyril raggruppamento viene eseguito in due passaggi. Per prima cosa imposta le chiavi su una copia della tabella dati originale che corrisponde ai gruppi e solo successivamente raggruppa. data.tablealloca solo la memoria per il gruppo di risultati più grande, che in questo caso è solo una riga, quindi fa una grande differenza nella quantità di memoria da allocare.

Cordiali saluti, se qualcuno se ne frega, l'ho trovato usando treeprof( install_github("brodieg/treeprof")), un visualizzatore di alberi sperimentale (e ancora molto alpha) per l' Rprofoutput:

inserisci qui la descrizione dell'immagine

Nota quanto sopra è attualmente funziona solo su Mac AFAIK. Inoltre, sfortunatamente, Rprofregistra le chiamate del tipo packagename::funnamecome anonime, quindi potrebbero effettivamente essere tutte le datatable::chiamate interne grouped_dtresponsabili, ma dai test rapidi sembrava che datatable::copyfosse quella più grande.

Detto questo, puoi vedere rapidamente come non ci siano molte spese generali intorno alla [.data.tablechiamata, ma c'è anche un ramo completamente separato per il raggruppamento.


MODIFICA : per confermare la copia:

> tracemem(diamondsDT)
[1] "<0x000000002747e348>"    
> diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
Source: local data table [5 x 2]

        cut AvgPrice
1      Fair 4358.758
2      Good 3928.864
3 Very Good 3981.760
4   Premium 4584.258
5     Ideal 3457.542
> diamondsDT[, mean(price), by = cut]
         cut       V1
1:     Ideal 3457.542
2:   Premium 4584.258
3:      Good 3928.864
4: Very Good 3981.760
5:      Fair 4358.758
> untracemem(diamondsDT)

È fantastico, grazie. Ciò significa che dplyr :: group_by () raddoppierà il requisito di memoria (rispetto alla sintassi databile pura) a causa del passaggio interno di copia dei dati? Significato se la dimensione del mio oggetto databile è 1 GB e utilizzo la sintassi dplyr concatenata simile a quella del post originale. Avrò bisogno di almeno 2 GB di memoria libera per ottenere i risultati?
Polimerasi

2
Mi sento come se l'avessi risolto nella versione dev?
Hadley

@hadley, stavo lavorando dalla versione CRAN. Guardando dev, sembra che tu abbia parzialmente risolto il problema, ma la copia effettiva rimane (non testata, solo guardando le righe c (20, 30:32) in R / grouped-dt.r. Probabilmente è più veloce ora, ma Scommetto che il passo lento è la copia.
BrodieG

3
Sto anche aspettando una funzione di copia superficiale in data.table; fino ad allora penso che sia meglio essere sicuri che veloci.
Hadley

2

Puoi usare dtplyr ora, che fa parte di tidyverse . Ti consente di usare le istruzioni di stile dplyr come al solito, ma utilizza una valutazione lenta e traduce le tue istruzioni in codice data.table sotto il cofano. L'overhead nella traduzione è minimo, ma si ricavano tutti, se non la maggior parte, dei vantaggi di data.table. Maggiori dettagli nel repository git ufficiale qui e nella pagina tidyverse .

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.