data.table vs dplyr: uno può fare qualcosa di buono l'altro non può o fa male?


760

Panoramica

Conosco relativamente data.tablepoco, non tanto dplyr. Ho letto alcune dplyrvignette ed esempi che sono emersi su SO, e finora le mie conclusioni sono che:

  1. data.tablee dplyrsono comparabili in termini di velocità, tranne quando ci sono molti gruppi (cioè> 10-100 K) e in alcune altre circostanze (vedere i benchmark di seguito)
  2. dplyr ha una sintassi più accessibile
  3. dplyr estrae (o volontà) potenziali interazioni DB
  4. Vi sono alcune differenze di funzionalità minori (vedere "Esempi / Utilizzo" di seguito)

Nella mia mente 2. non ha molto peso perché ne ho abbastanza familiarità data.table, anche se capisco che per gli utenti nuovi ad entrambi sarà un grande fattore. Vorrei evitare una discussione su quale sia più intuitiva, in quanto irrilevante per la mia domanda specifica posta dal punto di vista di qualcuno che ha già familiarità data.table. Vorrei anche evitare una discussione su come "più intuitivo" porta ad un'analisi più rapida (sicuramente vero, ma di nuovo, non di ciò che mi interessa di più qui).

Domanda

Quello che voglio sapere è:

  1. Ci sono compiti analitici che sono molto più facili da codificare con l'uno o l'altro pacchetto per le persone che hanno familiarità con i pacchetti (cioè una combinazione di sequenze di tasti richieste rispetto al livello richiesto di esoterismo, dove meno di ciascuna è una buona cosa).
  2. Esistono attività analitiche che vengono eseguite sostanzialmente (cioè più di 2 volte) in modo più efficiente in un pacchetto rispetto a un altro.

Una recente domanda SO mi ha fatto riflettere un po 'di più su questo, perché fino a quel momento non pensavo che dplyravrebbe offerto molto al di là di quello che già posso fare data.table. Ecco la dplyrsoluzione (dati alla fine di Q):

dat %.%
  group_by(name, job) %.%
  filter(job != "Boss" | year == min(year)) %.%
  mutate(cumu_job2 = cumsum(job2))

Che era molto meglio del mio tentativo di hacking di una data.tablesoluzione. Detto questo, anche le buone data.tablesoluzioni sono abbastanza buone (grazie Jean-Robert, Arun, e nota che ho preferito una singola affermazione rispetto alla soluzione strettamente ottimale):

setDT(dat)[,
  .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], 
  by=list(id, job)
]

La sintassi per quest'ultima può sembrare molto esoterica, ma in realtà è piuttosto semplice se sei abituato data.table(cioè non usa alcuni dei trucchi più esoterici).

Idealmente quello che mi piacerebbe vedere sono alcuni buoni esempi in cui il modo dplyro data.tableè sostanzialmente più conciso o offre prestazioni sostanzialmente migliori.

Esempi

uso
  • dplyrnon consente operazioni raggruppate che restituiscono un numero arbitrario di righe (dalla domanda di eddi , nota: sembra che sarà implementato in dplyr 0.5 , inoltre, @beginneR mostra un potenziale rimedio usando donella risposta alla domanda di @ eddi).
  • data.tablesupporta i join a rotazione (grazie a @dholstius) nonché i join a sovrapposizione
  • data.tableottimizza internamente le espressioni del modulo DT[col == value]o DT[col %in% values]per la velocità attraverso l'indicizzazione automatica che utilizza la ricerca binaria utilizzando la stessa sintassi R di base. Vedi qui per maggiori dettagli e un piccolo benchmark.
  • dplyroffre versioni di valutazione standard di funzioni (ad es regroup. summarize_each_) che possono semplificare l'uso programmatico di dplyr(notare che l'uso programmatico di data.tableè sicuramente possibile, richiede solo un po 'di attenta riflessione, sostituzione / quotazione, ecc., almeno per quanto ne so)
Punti di riferimenti

Dati

Questo è per il primo esempio che ho mostrato nella sezione delle domande.

dat <- structure(list(id = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 2L), name = c("Jane", "Jane", "Jane", "Jane", 
"Jane", "Jane", "Jane", "Jane", "Bob", "Bob", "Bob", "Bob", "Bob", 
"Bob", "Bob", "Bob"), year = c(1980L, 1981L, 1982L, 1983L, 1984L, 
1985L, 1986L, 1987L, 1985L, 1986L, 1987L, 1988L, 1989L, 1990L, 
1991L, 1992L), job = c("Manager", "Manager", "Manager", "Manager", 
"Manager", "Manager", "Boss", "Boss", "Manager", "Manager", "Manager", 
"Boss", "Boss", "Boss", "Boss", "Boss"), job2 = c(1L, 1L, 1L, 
1L, 1L, 1L, 0L, 0L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L)), .Names = c("id", 
"name", "year", "job", "job2"), class = "data.frame", row.names = c(NA, 
-16L))

9
La soluzione che è simile nella lettura a dplyrquella è:as.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)]
eddi

7
Per il numero 1 entrambi dplyre i data.tableteam stanno lavorando su parametri di riferimento, quindi una risposta ci sarà ad un certo punto. # 2 (sintassi) imO è rigorosamente falso, ma questo si avventura chiaramente nel territorio dell'opinione pubblica, quindi voterò anche per chiudere.
eddi,

13
bene, ancora una volta imO, l'insieme di problemi che sono più chiaramente espressi (d)plyrha la misura 0
eddi

28
@BrodieG l'unica cosa che mi infastidisce davvero di entrambi dplyre plyrper quanto riguarda la sintassi ed è fondamentalmente il motivo principale per cui non mi piace la loro sintassi, è che devo imparare troppe (leggi più di 1) funzioni extra (con nomi che ancora non ha senso per me), ricorda cosa fanno, quali argomenti prendono, ecc. È sempre stata una grande svolta per me dalla filosofia plyr.
eddi,

43
@eddi [ironico] l'unica cosa che mi infastidisce davvero della sintassi data.table è che devo imparare come interagiscono troppi argomenti funzionali e cosa significano le scorciatoie criptiche (ad es .SD.). [seriamente] Penso che queste siano differenze di design legittime che si rivolgono a persone diverse
Hadley,

Risposte:


532

Abbiamo bisogno di copertura, almeno questi aspetti per fornire una vasta risposta / confronto (in nessun ordine particolare di importanza): Speed, Memory usage, Syntaxe Features.

Il mio intento è quello di coprire ognuno di questi nel modo più chiaro possibile dalla prospettiva data.table.

Nota: se non diversamente specificato, facendo riferimento a dplyr, ci riferiamo all'interfaccia data.frame di dplyr i cui interni sono in C ++ usando Rcpp.


La sintassi data.table è coerente nella sua forma - DT[i, j, by]. Per mantenere i, je byinsieme è di progettazione. Mantenendo insieme le operazioni correlate, consente di ottimizzare facilmente le operazioni per la velocità e, soprattutto , l' utilizzo della memoria , e fornisce anche alcune potenti funzionalità , il tutto mantenendo la coerenza nella sintassi.

1. Velocità

Un certo numero di parametri di riferimento (sebbene principalmente sulle operazioni di raggruppamento) sono stati aggiunti alla domanda che mostra già data.table aumenta più rapidamente di dplyr all'aumentare del numero di gruppi e / o righe da raggruppare, compresi i parametri di riferimento di Matt sul raggruppamento da 10 milioni a 2 miliardi di righe (100 GB di RAM) su 100-10 milioni di gruppi e colonne di raggruppamento variabili, che confronta anche pandas. Vedi anche benchmark aggiornati , che includono Sparke pydatatablepure.

Per quanto riguarda i benchmark, sarebbe bello coprire anche questi aspetti rimanenti:

  • Raggruppare le operazioni che coinvolgono un sottoinsieme di righe , ad esempio, DT[x > val, sum(y), by = z]digitare operazioni.

  • Confronta le altre operazioni come aggiornamento e join .

  • Indica inoltre il footprint di memoria per ogni operazione oltre al runtime.

2. Utilizzo della memoria

  1. Le operazioni che coinvolgono filter()o slice()in dplyr possono essere memoria inefficiente (sia su data.frames che data.tables). Vedi questo post .

    Nota che il commento di Hadley parla della velocità (che dplyr è abbondante e veloce per lui), mentre qui la principale preoccupazione è la memoria .

  2. Al momento l'interfaccia data.table consente di modificare / aggiornare le colonne per riferimento (si noti che non è necessario riassegnare il risultato a una variabile).

    # sub-assign by reference, updates 'y' in-place
    DT[x >= 1L, y := NA]

    Ma dplyr non si aggiorna mai per riferimento. L'equivalente dplyr sarebbe (si noti che il risultato deve essere riassegnato):

    # copies the entire 'y' column
    ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))

    Una preoccupazione per questo è la trasparenza referenziale . L'aggiornamento di un oggetto data.table per riferimento, specialmente all'interno di una funzione, potrebbe non essere sempre desiderabile. Ma questa è una funzione incredibilmente utile: vedi questo e questo post per casi interessanti. E vogliamo mantenerlo.

    Pertanto stiamo lavorando per esportare la shallow()funzione in data.table che fornirà all'utente entrambe le possibilità . Ad esempio, se è desiderabile non modificare l'input data.table all'interno di una funzione, si può quindi fare:

    foo <- function(DT) {
        DT = shallow(DT)          ## shallow copy DT
        DT[, newcol := 1L]        ## does not affect the original DT 
        DT[x > 2L, newcol := 2L]  ## no need to copy (internally), as this column exists only in shallow copied DT
        DT[x > 2L, x := 3L]       ## have to copy (like base R / dplyr does always); otherwise original DT will 
                                  ## also get modified.
    }

    Non utilizzando shallow(), viene mantenuta la vecchia funzionalità:

    bar <- function(DT) {
        DT[, newcol := 1L]        ## old behaviour, original DT gets updated by reference
        DT[x > 2L, x := 3L]       ## old behaviour, update column x in original DT.
    }

    Creando una copia superficiale utilizzando shallow(), comprendiamo che non si desidera modificare l'oggetto originale. Ci occupiamo di tutto internamente per garantire che, pur garantendo anche la copia delle colonne, si modifichi solo quando è assolutamente necessario . Una volta implementato, questo dovrebbe risolvere del tutto il problema della trasparenza referenziale fornendo allo stesso tempo all'utente entrambe le possibilità.

    Inoltre, una volta shallow()esportata l'interfaccia data.table di dplyr dovrebbe evitare quasi tutte le copie. Quindi coloro che preferiscono la sintassi di dplyr possono usarlo con data.tables.

    Ma mancheranno ancora molte funzionalità fornite da data.table, incluso il (sotto) assegnamento per riferimento.

  3. Aggregati durante l'adesione:

    Supponiamo di avere due data.tables come segue:

    DT1 = data.table(x=c(1,1,1,1,2,2,2,2), y=c("a", "a", "b", "b"), z=1:8, key=c("x", "y"))
    #    x y z
    # 1: 1 a 1
    # 2: 1 a 2
    # 3: 1 b 3
    # 4: 1 b 4
    # 5: 2 a 5
    # 6: 2 a 6
    # 7: 2 b 7
    # 8: 2 b 8
    DT2 = data.table(x=1:2, y=c("a", "b"), mul=4:3, key=c("x", "y"))
    #    x y mul
    # 1: 1 a   4
    # 2: 2 b   3

    E ti piacerebbe ottenere sum(z) * mulper ogni riga DT2mentre ti unisci per colonne x,y. Possiamo:

    • 1) aggregare DT1per ottenere sum(z), 2) eseguire un join e 3) moltiplicare (o)

      # data.table way
      DT1[, .(z = sum(z)), keyby = .(x,y)][DT2][, z := z*mul][]
      
      # dplyr equivalent
      DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>% 
          right_join(DF2) %>% mutate(z = z * mul)
    • 2) fai tutto in una volta (usando la by = .EACHIfunzione):

      DT1[DT2, list(z=sum(z) * mul), by = .EACHI]

    Qual è il vantaggio?

    • Non è necessario allocare memoria per il risultato intermedio.

    • Non dobbiamo raggruppare / hash due volte (uno per l'aggregazione e l'altro per l'adesione).

    • E, soprattutto, l'operazione che volevamo eseguire è chiara osservando jin (2).

    Controlla questo post per una spiegazione dettagliata di by = .EACHI. Non si materializzano risultati intermedi e il join + aggregato viene eseguito tutto in una volta.

    Dai un'occhiata a questo , questo e questo post per scenari di utilizzo reali.

    In dplyrsi dovrebbe aderire e aggregati o inerti e poi unire , nessuno dei quali sono i più efficienti, in termini di memoria (che a sua volta si traduce in velocità).

  4. Aggiornamento e iscrizione:

    Considera il codice data.table mostrato di seguito:

    DT1[DT2, col := i.mul]

    aggiunge / aggiorna DT1la colonna colcon mulda DT2su quelle righe in cui DT2la colonna chiave corrisponde DT1. Non credo che esista un equivalente esatto di questa operazione dplyr, vale a dire, senza evitare *_joinun'operazione, che dovrebbe copiare l'intero DT1solo per aggiungere una nuova colonna, che non è necessario.

    Controlla questo post per uno scenario di utilizzo reale.

Riassumendo, è importante rendersi conto che ogni bit di ottimizzazione è importante. Come direbbe Grace Hopper , attenzione ai tuoi nanosecondi !

3. Sintassi

Vediamo ora la sintassi . Hadley ha commentato qui :

Le tabelle di dati sono estremamente veloci ma penso che la loro concisione renda più difficile l'apprendimento e che il codice che lo utilizza sia più difficile da leggere dopo averlo scritto ...

Trovo inutile questa osservazione perché è molto soggettiva. Ciò che possiamo forse provare è contrastare la coerenza nella sintassi . Confronteremo data.table e dplyr sintassi fianco a fianco.

Lavoreremo con i dati fittizi mostrati di seguito:

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
  1. Operazioni di aggregazione / aggiornamento di base.

    # case (a)
    DT[, sum(y), by = z]                       ## data.table syntax
    DF %>% group_by(z) %>% summarise(sum(y)) ## dplyr syntax
    DT[, y := cumsum(y), by = z]
    ans <- DF %>% group_by(z) %>% mutate(y = cumsum(y))
    
    # case (b)
    DT[x > 2, sum(y), by = z]
    DF %>% filter(x>2) %>% group_by(z) %>% summarise(sum(y))
    DT[x > 2, y := cumsum(y), by = z]
    ans <- DF %>% group_by(z) %>% mutate(y = replace(y, which(x > 2), cumsum(y)))
    
    # case (c)
    DT[, if(any(x > 5L)) y[1L]-y[2L] else y[2L], by = z]
    DF %>% group_by(z) %>% summarise(if (any(x > 5L)) y[1L] - y[2L] else y[2L])
    DT[, if(any(x > 5L)) y[1L] - y[2L], by = z]
    DF %>% group_by(z) %>% filter(any(x > 5L)) %>% summarise(y[1L] - y[2L])
    • La sintassi di data.table è compatta e dplyr è piuttosto dettagliata. Le cose sono più o meno equivalenti nel caso (a).

    • Nel caso (b), abbiamo dovuto usare filter()in dplyr durante il riepilogo . Ma durante l' aggiornamento , abbiamo dovuto spostare la logica all'interno mutate(). In data.table tuttavia, esprimiamo entrambe le operazioni con la stessa logica: operare su righe dove x > 2, ma nel primo caso, ottenere sum(y), mentre nel secondo caso aggiornare quelle righe ycon la somma cumulativa.

      Questo è ciò che intendiamo quando diciamo che il DT[i, j, by]modulo è coerente .

    • Analogamente nel caso (c), quando abbiamo una if-elsecondizione, siamo in grado di esprimere la logica "così com'è" sia in data.table che in dplyr. Tuttavia, se desideriamo restituire solo quelle righe in cui la ifcondizione soddisfa e saltare altrimenti, non possiamo utilizzare summarise()direttamente (AFAICT). Dobbiamo filter()prima riassumere e poi riassumere perché summarise()si aspetta sempre un singolo valore .

      Mentre restituisce lo stesso risultato, l'utilizzo filter()qui rende l'operazione meno ovvia.

      Potrebbe anche essere possibile usarlo anche filter()nel primo caso (non mi sembra ovvio), ma il punto è che non dovremmo farlo.

  2. Aggregazione / aggiornamento su più colonne

    # case (a)
    DT[, lapply(.SD, sum), by = z]                     ## data.table syntax
    DF %>% group_by(z) %>% summarise_each(funs(sum)) ## dplyr syntax
    DT[, (cols) := lapply(.SD, sum), by = z]
    ans <- DF %>% group_by(z) %>% mutate_each(funs(sum))
    
    # case (b)
    DT[, c(lapply(.SD, sum), lapply(.SD, mean)), by = z]
    DF %>% group_by(z) %>% summarise_each(funs(sum, mean))
    
    # case (c)
    DT[, c(.N, lapply(.SD, sum)), by = z]     
    DF %>% group_by(z) %>% summarise_each(funs(n(), mean))
    • Nel caso (a), i codici sono più o meno equivalenti. data.table utilizza la familiare funzione base lapply(), mentre dplyrintroduce *_each()insieme a un mucchio di funzioni funs().

    • data.table :=richiede che vengano forniti i nomi delle colonne, mentre dplyr lo genera automaticamente.

    • Nel caso (b), la sintassi di dplyr è relativamente semplice. Il miglioramento di aggregazioni / aggiornamenti su più funzioni è nell'elenco di data.table.

    • Nel caso (c), tuttavia, dplyr restituirebbe n()tante volte quante colonne, invece di una sola volta. In data.table, tutto ciò che dobbiamo fare è restituire un elenco j. Ogni elemento dell'elenco diventerà una colonna nel risultato. Quindi, possiamo usare, ancora una volta, la familiare funzione base c()per concatenare .Na a listche restituisce a list.

    Nota: ancora una volta, in data.table, tutto ciò che dobbiamo fare è restituire un elenco j. Ogni elemento dell'elenco diventerà una colonna nel risultato. È possibile utilizzare c(), as.list(), lapply(), list()funzioni ecc ... di base per raggiungere questo obiettivo, senza dover imparare eventuali nuove funzioni.

    Dovrai imparare solo le variabili speciali - .Ne .SDalmeno. L'equivalente in dplyr sono n()e.

  3. Si unisce

    dplyr fornisce funzioni separate per ogni tipo di join dove data.table consente i join usando la stessa sintassi DT[i, j, by](e con la ragione). Fornisce anche una merge.data.table()funzione equivalente in alternativa.

    setkey(DT1, x, y)
    
    # 1. normal join
    DT1[DT2]            ## data.table syntax
    left_join(DT2, DT1) ## dplyr syntax
    
    # 2. select columns while join    
    DT1[DT2, .(z, i.mul)]
    left_join(select(DT2, x, y, mul), select(DT1, x, y, z))
    
    # 3. aggregate while join
    DT1[DT2, .(sum(z) * i.mul), by = .EACHI]
    DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>% 
        inner_join(DF2) %>% mutate(z = z*mul) %>% select(-mul)
    
    # 4. update while join
    DT1[DT2, z := cumsum(z) * i.mul, by = .EACHI]
    ??
    
    # 5. rolling join
    DT1[DT2, roll = -Inf]
    ??
    
    # 6. other arguments to control output
    DT1[DT2, mult = "first"]
    ??
    • Alcuni potrebbero trovare una funzione separata per ciascun join molto più gradevole (sinistra, destra, interno, anti, semi ecc.), Mentre come altri potrebbero piacere a data.table's DT[i, j, by], o merge()che è simile alla base R.

    • Tuttavia i join dplyr fanno proprio questo. Niente di più. Nientemeno.

    • data.tables può selezionare le colonne durante l'unione (2), e in dplyr dovrai select()prima accedervi su entrambi i data.frames prima di unirti come mostrato sopra. Altrimenti si materializzerebbe il join con colonne non necessarie solo per rimuoverle in seguito e questo è inefficiente.

    • data.tables può aggregarsi durante l'unione (3) e anche aggiornare durante l'unione (4), usando la by = .EACHIfunzione. Perché materializzare l'intero risultato del join per aggiungere / aggiornare solo alcune colonne?

    • data.table è in grado di eseguire il roll dei join (5) - roll forward, LOCF , rollback, NOCB , il più vicino .

    • data.table ha anche un mult =argomento che seleziona per primo , ultima o tutte le corrispondenze (6).

    • data.table ha allow.cartesian = TRUE argomenti per proteggere da join non validi accidentali.

Ancora una volta, la sintassi è coerente con DT[i, j, by]argomenti aggiuntivi che consentono di controllare ulteriormente l'output.

  1. do()...

    Il riepilogo di dplyr è appositamente progettato per funzioni che restituiscono un singolo valore. Se la tua funzione restituisce valori multipli / disuguali, dovrai ricorrere a do(). Devi conoscere in anticipo tutte le funzioni che restituiscono valore.

    DT[, list(x[1], y[1]), by = z]                 ## data.table syntax
    DF %>% group_by(z) %>% summarise(x[1], y[1]) ## dplyr syntax
    DT[, list(x[1:2], y[1]), by = z]
    DF %>% group_by(z) %>% do(data.frame(.$x[1:2], .$y[1]))
    
    DT[, quantile(x, 0.25), by = z]
    DF %>% group_by(z) %>% summarise(quantile(x, 0.25))
    DT[, quantile(x, c(0.25, 0.75)), by = z]
    DF %>% group_by(z) %>% do(data.frame(quantile(.$x, c(0.25, 0.75))))
    
    DT[, as.list(summary(x)), by = z]
    DF %>% group_by(z) %>% do(data.frame(as.list(summary(.$x))))
    • .SDL 'equivalente è .

    • In data.table puoi inserire praticamente qualsiasi cosa j : l'unica cosa da ricordare è che restituisca un elenco in modo che ogni elemento dell'elenco venga convertito in una colonna.

    • In dplyr, non posso farlo. do()Devi ricorrere a seconda di quanto sei sicuro che la tua funzione restituisca sempre un singolo valore. Ed è abbastanza lento.

Ancora una volta, la sintassi di data.table è coerente con DT[i, j, by]. Possiamo solo continuare a lanciare espressionij senza doverci preoccupare di queste cose.

Dai un'occhiata a questa domanda SO e questa . Mi chiedo se sarebbe possibile esprimere la risposta come semplice usando la sintassi di dplyr ...

Per riassumere, ho sottolineato in particolare diversi casi in cui la sintassi di dplyr è inefficiente, limitata o non riesce a rendere le operazioni semplici. Questo in particolare perché data.table ottiene un po 'di contraccolpo sulla sintassi "più difficile da leggere / apprendere" (come quella incollata / collegata sopra). La maggior parte dei post che coprono dplyr parlano delle operazioni più semplici. E questo è fantastico. Ma è importante rendersi conto anche della sua sintassi e dei limiti delle funzionalità, e devo ancora vedere un post su di esso.

data.table ha anche le sue peculiarità (alcune delle quali ho sottolineato che stiamo tentando di risolvere). Stiamo anche tentando di migliorare i join di data.table, come ho sottolineato qui .

Ma si dovrebbe anche considerare il numero di funzionalità che manca a dplyr rispetto a data.table.

4. Caratteristiche

Ho sottolineato la maggior parte delle funzionalità qui e anche in questo post. Inoltre:

  • fread - il lettore di file veloce è disponibile da molto tempo.

  • fwrite : è ora disponibile un writer di file veloce parallelizzato . Vedi questo post per una spiegazione dettagliata dell'implementazione e # 1664 per tenere traccia di ulteriori sviluppi.

  • Indicizzazione automatica : un'altra utile funzionalità per ottimizzare la sintassi di base R così com'è, internamente.

  • Raggruppamento ad hoc : dplyrordina automaticamente i risultati raggruppando le variabili durante summarise(), che potrebbero non essere sempre desiderabili.

  • Numerosi vantaggi nei join data.table (per velocità / efficienza della memoria e sintassi) di cui sopra.

  • <=, <, >, >=Join non equi : consente join utilizzando altri operatori insieme a tutti gli altri vantaggi dei join data.table.

  • Recentemente i join di intervallo sovrapposti sono stati implementati in data.table. Controlla questo post per una panoramica con benchmark.

  • setorder() funzione in data.table che consente un riordino molto veloce di data.tables per riferimento.

  • dplyr fornisce l' interfaccia con i database usando la stessa sintassi, che data.table non fa al momento.

  • data.tablefornisce equivalenti più veloce di operazioni di set (scritto da Jan Gorecki) - fsetdiff, fintersect, funione fsetequalcon l'aggiunta diall argomenti (come in SQL).

  • data.table si carica in modo pulito senza avvisi di mascheramento e ha un meccanismo descritto qui per la [.data.framecompatibilità quando passato a qualsiasi pacchetto R. dplyr cambia funzioni di base filter, lage [che può causare problemi; ad esempio qui e qui .


Finalmente:

  • Sui database - non vi è alcun motivo per cui data.table non possa fornire un'interfaccia simile, ma questa non è una priorità ora. Potrebbe essere sconvolto se agli utenti piacesse molto quella funzionalità ... non ne sono sicuro.

  • Sul parallelismo: tutto è difficile, finché qualcuno non va avanti e lo fa. Naturalmente ci vorrà uno sforzo (essendo thread-safe).

    • Attualmente si stanno compiendo progressi (nello sviluppo v1.9.7) verso la parallelizzazione di parti note che richiedono tempo per ottenere incrementi di prestazioni incrementali OpenMP.

9
@bluefeet: non credo che tu abbia fatto un gran servizio a tutti noi spostando quella discussione in chat. Avevo l'impressione che Arun fosse uno degli sviluppatori e questo avrebbe potuto dare utili spunti.
IRTFM,

2
Quando sono andato a chattare usando il tuo link, mi è sembrato che tutto il materiale che seguiva il commento a partire da "Dovresti usare un filtro" ... era sparito. Mi sto perdendo qualcosa riguardo al meccanismo di chat SO?
IRTFM,

6
Penso che in tutti i casi in cui si utilizza l'assegnazione per riferimento ( :=), l' dplyrequivalente dovrebbe essere utilizzato anche <-come in DF <- DF %>% mutate...anziché soloDF %>% mutate...
David Arenburg,

4
Per quanto riguarda la sintassi. Credo che dplyrpossa essere più facile per gli utenti che erano abituati alla plyrsintassi, ma che data.tablepotrebbe essere più facile per gli utenti che erano soliti interrogare sintassi di lingue come SQLe l'algebra relazionale dietro di essa, che riguarda la trasformazione dei dati tabulari. @Arun dovresti notare che gli operatori di set sono fattibili molto facilmente eseguendo il wrapping della data.tablefunzione e, naturalmente, porta una notevole velocità.
jangorecki,

9
Ho letto questo post così tante volte e mi ha aiutato molto a capire data.table e ad essere in grado di usarlo meglio. Io, nella maggior parte dei casi, preferisco data.table su dplyr o panda o PL / pgSQL. Tuttavia, non riuscivo a smettere di pensare a come esprimerlo. La sintassi non è facile, chiara o dettagliata. In effetti, anche dopo aver usato molto data.table, spesso faccio ancora fatica a comprendere il mio codice, ho scritto letteralmente una settimana fa. Questo è un esempio di vita di un linguaggio di sola scrittura. it.wikipedia.org/wiki/Write-only_language Quindi, speriamo, un giorno saremo in grado di usare dplyr su data.table.
Ufo

385

Ecco il mio tentativo di risposta esaustiva dal punto di vista dplyr, seguendo il profilo generale della risposta di Arun (ma in qualche modo riorganizzato in base a priorità diverse).

Sintassi

C'è una certa soggettività nella sintassi, ma sostengo la mia affermazione che la concisione di data.table rende più difficile l'apprendimento e più difficile da leggere. Questo in parte perché dplyr sta risolvendo un problema molto più semplice!

Una cosa davvero importante che dplyr fa per te è che limita tue opzioni. Sostengo che la maggior parte dei problemi di una sola tabella possono essere risolti con solo cinque verbi chiave che filtrano, selezionano, mutano, organizzano e riassumono, insieme a un avverbio "per gruppo". Questo vincolo è di grande aiuto quando stai imparando la manipolazione dei dati, perché ti aiuta a pensare al problema. In dplyr, ciascuno di questi verbi è mappato su una singola funzione. Ogni funzione fa un lavoro ed è facile da capire da sola.

Si crea complessità collegando queste semplici operazioni insieme %>%. Ecco un esempio da uno dei post a cui Arun è collegato :

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

Anche se non hai mai visto dplyr prima (o anche R!), Puoi ancora ottenere l'essenza di ciò che sta accadendo perché le funzioni sono tutti verbi inglesi. Lo svantaggio dei verbi inglesi è che richiedono una maggiore digitazione rispetto a [, ma penso che possa essere ampiamente mitigato da un completamento automatico migliore.

Ecco l'equivalente codice data.table:

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

È più difficile seguire questo codice a meno che tu non abbia già familiarità con data.table. (Inoltre, non sono riuscito a capire come indentare i ripetuti [ in un modo che mi sembra bello). Personalmente, quando guardo il codice che ho scritto 6 mesi fa, è come guardare un codice scritto da uno sconosciuto, quindi sono arrivato a preferire un codice semplice, seppur dettagliato.

Altri due fattori minori che penso riducono leggermente la leggibilità:

  • Poiché quasi tutte le operazioni della tabella di dati vengono utilizzate, [è necessario un contesto aggiuntivo per capire cosa sta accadendo. Ad esempio, l' x[y] unione di due tabelle di dati o l'estrazione di colonne da un frame di dati? Questo è solo un piccolo problema, perché nel codice ben scritto i nomi delle variabili dovrebbero suggerire cosa sta succedendo.

  • Mi piace che group_by()sia un'operazione separata in dplyr. Cambia radicalmente il calcolo quindi penso che dovrebbe essere evidente quando scrematura il codice, ed è più facile da individuare group_by()rispetto alla byargomento [.data.table.

Mi piace anche che il pipe non sia limitato a un solo pacchetto. Puoi iniziare mettendo in ordine i tuoi dati con tidyr e finire con un grafico in ggvis . E non sei limitato ai pacchetti che scrivo: chiunque può scrivere una funzione che costituisce una parte continua di una pipe di manipolazione dei dati. In realtà, preferisco piuttosto il precedente codice data.table riscritto con %>%:

diamonds %>% 
  data.table() %>% 
  .[cut != "Fair", 
    .(AvgPrice = mean(price),
      MedianPrice = as.numeric(median(price)),
      Count = .N
    ), 
    by = cut
  ] %>% 
  .[order(-Count)]

E l'idea di piping con %>%non si limita ai soli frame di dati ed è facilmente generalizzata ad altri contesti: grafica web interattiva , web scraping , sintesi , contratti di runtime , ...)

Memoria e prestazioni

Li ho raggruppati insieme, perché, per me, non sono così importanti. La maggior parte degli utenti R lavora con meno di 1 milione di righe di dati e dplyr è sufficientemente veloce per quella dimensione di dati che non si è a conoscenza del tempo di elaborazione. Ottimizziamo dplyr per espressività su dati medi; sentiti libero di usare data.table per la massima velocità su dati più grandi.

La flessibilità di dplyr significa anche che puoi facilmente modificare le caratteristiche prestazionali usando la stessa sintassi. Se le prestazioni di dplyr con il backend del frame di dati non sono abbastanza buone per te, puoi usare il backend data.table (anche se con un set di funzionalità un po 'limitato). Se i dati con cui stai lavorando non rientrano nella memoria, puoi utilizzare un back-end del database.

Detto questo, le prestazioni dplyr miglioreranno nel lungo termine. Implementeremo sicuramente alcune delle grandi idee di data.table come l'ordinamento radix e l'utilizzo dello stesso indice per join e filtri. Stiamo anche lavorando alla parallelizzazione in modo da poter sfruttare più core.

Caratteristiche

Alcune cose su cui stiamo pianificando di lavorare nel 2015:

  • il readrpacchetto, per semplificare l'ottenimento di file dal disco e nella memoria, analogo a fread().

  • Join più flessibili, incluso il supporto per i join non equi.

  • Raggruppamento più flessibile come campioni bootstrap, rollup e altro

Sto anche investendo tempo nel miglioramento dei connettori del database di R , nella capacità di parlare con le API Web e nel semplificare la raschiatura delle pagine HTML .


27
Solo una nota a margine, sono d'accordo con molti dei tuoi argomenti (anche se preferisco data.tableio stesso la sintassi), ma puoi facilmente usarli %>%per eseguire il pipe delle data.tableoperazioni se non ti piace lo [stile. %>%non è specifico dplyr, ma deriva da un pacchetto separato (di cui sei anche tu coautore), quindi non sono sicuro di capire cosa stai cercando di dire nella maggior parte del tuo paragrafo Sintassi .
David Arenburg,

11
@DavidArenburg buon punto. Ho riscritto la sintassi per rendere più chiari i miei punti principali e per evidenziare che è possibile utilizzare %>%con data.table
Hadley,

5
Grazie Hadley, questa è una prospettiva utile. Per quanto riguarda il rientro, in genere lo faccio DT[\n\texpression\n][\texpression\n](in sostanza ), che in realtà funziona piuttosto bene. Sto mantenendo la risposta di Arun come risposta mentre risponde più direttamente alle mie domande specifiche che non riguardano tanto l'accessibilità della sintassi, ma penso che questa sia una buona risposta per le persone che cercano di avere un'idea generale delle differenze / comunanza tra dplyre data.table.
BrodieG,

33
Perché lavorare su Fastread quando c'è già fread()? Il tempo non sarebbe speso meglio per migliorare fread () o lavorare su altre cose (sottosviluppate)?
EDi,

10
L'API di data.tablesi basa su un massiccio abuso della []notazione. Questa è la sua più grande forza e la sua più grande debolezza.
Paul,

65

In risposta diretta al titolo della domanda ...

dplyr sicuramente fa cose che data.tablenon possono.

Il tuo punto n. 3

dplyr estrae (o volontà) potenziali interazioni DB

è una risposta diretta alla tua domanda ma non è elevata a un livello abbastanza alto. dplyrè veramente un front-end estendibile a più meccanismi di archiviazione dei dati, così come data.tableun'estensione a uno singolo.

Guarda dplyrcome un'interfaccia agnostica di back-end, con tutti i target usando la stessa grammatica, dove puoi estendere i target e i gestori a piacimento. data.tableè, dal dplyrpunto di vista, uno di quegli obiettivi.

Non vedrai mai (spero) un giorno che data.tabletenti di tradurre le tue query per creare istruzioni SQL che funzionano con archivi di dati su disco o in rete.

dplyrpuò eventualmente fare le cose data.tablenon lo farà o potrebbe non fare altrettanto.

Basato sulla progettazione del lavoro in memoria, data.tablepotrebbe avere un tempo molto più difficile estendersi all'elaborazione parallela delle query rispetto a dplyr.


In risposta alle domande nel corpo ...

uso

Ci sono compiti analitici che sono molto più facili da codificare con l'uno o l'altro pacchetto per le persone che hanno familiarità con i pacchetti (cioè una combinazione di sequenze di tasti richieste rispetto al livello richiesto di esoterismo, dove meno di ciascuna è una buona cosa).

Questo può sembrare un punt ma la vera risposta è no. Le persone che hanno familiarità con gli strumenti sembrano usare quella più familiare o quella che in realtà è quella giusta per il lavoro da svolgere. Detto questo, a volte vuoi presentare una particolare leggibilità, a volte un livello di prestazioni, e quando hai bisogno di un livello abbastanza alto di entrambi potresti aver bisogno di un altro strumento per andare avanti con quello che devi già fare astrazioni più chiare .

Prestazione

Esistono attività analitiche che vengono eseguite sostanzialmente (cioè più di 2 volte) in modo più efficiente in un pacchetto rispetto a un altro.

Ancora una volta no. data.tablesi distingue per essere efficiente in tutto ciò che fa, dove ha dplyrl'onere di essere limitato per alcuni aspetti all'archivio dati sottostante e ai gestori registrati.

Ciò significa che quando si riscontra un problema di prestazioni con data.tablete puoi essere abbastanza sicuro che sia nella tua funzione di query e se in realtà è un collo di bottiglia, data.tableallora ti sei conquistato la gioia di presentare un rapporto. Questo vale anche quando dplyrsi utilizza data.tablecome back-end; si può vedere un po ' di testa proveniente dplyrma le probabilità sono esso è la query.

Quando dplyrsi verificano problemi di prestazioni con i back-end, è possibile aggirarli registrando una funzione per la valutazione ibrida o (nel caso dei database) manipolando la query generata prima dell'esecuzione.

Vedi anche la risposta accettata a quando plyr è meglio di data.table?


3
Non riesci a completare una data.table con tbl_dt? Perché non ottenere il meglio da entrambi i mondi?
aaa90210

22
Dimentichi di menzionare l'istruzione inversa "data.table fa sicuramente cose che dplyr non può", il che è anche vero.
jangorecki,

25
La risposta di Arun lo spiega bene. Il più importante (in termini di prestazioni) sarebbe la fread, l'aggiornamento per riferimento, i rolling rolling, i join sovrapposti. Credo che non ci siano pacchetti (non solo dplyr) in grado di competere con queste funzionalità. Un bell'esempio può essere l'ultima diapositiva di questa presentazione.
jangorecki,

15
Totalmente, data.table è il motivo per cui uso ancora R. Altrimenti userei i panda. È persino migliore / più veloce dei panda.
Marbel,

8
Mi piace data.table per la sua semplicità e somiglianza con la struttura della sintassi SQL. Il mio lavoro consiste nel fare analisi dei dati e grafica ad hoc molto intense ogni giorno per la modellazione statistica e ho davvero bisogno di uno strumento abbastanza semplice per fare cose complicate. Ora posso ridurre il mio toolkit a solo data.table per dati e reticolo per grafico nel mio lavoro quotidiano. Per fare un esempio posso persino fare operazioni come questa: $ DT [gruppo == 1, y_hat: = predict (fit1, data = .SD),] $, che è davvero pulito e lo considero un grande vantaggio di SQL in ambiente R classico.
xappppp,
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.