data.table vs dplyr: si può fare qualcosa che l'altro non può o fa male?




(3)

Panoramica

Sono relativamente familiare con data.table , non tanto con dplyr . Ho letto alcune vignette e alcuni esempi che sono dplyr su SO, e finora le mie conclusioni sono che:

  1. data.table e dplyr sono paragonabili in velocità, tranne quando ci sono molti (cioè> 10-100K) gruppi, e in alcune altre circostanze (vedi benchmark sotto)
  2. dplyr ha una sintassi più accessibile
  3. dplyr abstracts (o will) potenziali interazioni DB
  4. Ci sono alcune piccole differenze di funzionalità (vedi "Esempi / Utilizzo" sotto)

Nella mia mente 2. non sopporta molto peso perché sono abbastanza familiare con data.table , anche se capisco che per gli utenti nuovi a entrambi sarà un grande fattore. Vorrei evitare una discussione su quale sia più intuitivo, in quanto ciò è irrilevante per la mia specifica domanda posta dal punto di vista di qualcuno che abbia già familiarità con data.table . Vorrei anche evitare una discussione su come "più intuitivo" porti a un'analisi più rapida (certamente vero, ma ancora una volta, non è quello 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 combinazioni di tasti richieste rispetto al livello richiesto di esoterismo, dove meno di ciascuna è una buona cosa).
  2. Ci sono compiti analitici che vengono eseguiti in modo sostanziale (cioè più di 2 volte) in modo più efficiente in un pacchetto rispetto all'altro.

Una recente domanda su SO mi ha fatto riflettere un po 'di più, perché fino a quel momento non pensavo che dplyr avrebbe offerto molto più di quello che posso già fare in data.table . Ecco la soluzione dplyr (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 hackerare una soluzione data.table . Detto questo, anche le buone soluzioni data.table sono piuttosto buone (grazie Jean-Robert, Arun, e ho notato che ho preferito la singola affermazione sulla soluzione strettamente più ottimale):

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

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

Idealmente, quello che mi piacerebbe vedere sono alcuni buoni esempi in cui il modo dplyr o data.table è sostanzialmente più conciso o si comporta sostanzialmente meglio.

Esempi

uso
  • dplyr non consente le operazioni raggruppate che restituiscono un numero arbitrario di righe (dalla domanda di eddi , nota: sembra che verrà implementato in dplyr 0.5 , anche, @beginneR mostra una possibile soluzione di work-around usando do nella risposta alla domanda di @ eddi) .
  • data.table supporta i rolling join (grazie a @dholstius) così come i join sovrapposti
  • data.table internamente ottimizza le espressioni del modulo DT[col == value] o DT[col %in% values] per la velocità attraverso l'indicizzazione automatica che utilizza la ricerca binaria mentre utilizza la stessa sintassi di base R. Vedi qui per ulteriori dettagli e un piccolo punto di riferimento.
  • dplyr offre versioni standard di valutazione delle funzioni (ad esempio, regroup , dplyr ) che possono semplificare l'uso programmatico di dplyr (nota l'uso programmatico di data.table è sicuramente possibile, richiede solo qualche pensiero attento, sostituzione / quotazione, ecc, almeno per quanto ne data.table )
benchmark

Dati

Questo è per il primo esempio che ho mostrato nella sezione 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))

In risposta diretta al titolo della domanda ...

dplyr fa sicuramente cose che data.table non può fare.

Il tuo punto # 3

dplyr abstracts (o will) potenziali interazioni DB

è una risposta diretta alla tua domanda, ma non è elevato a un livello abbastanza alto. dplyr è davvero un front-end estendibile a molteplici meccanismi di archiviazione dei dati dove data.table è un'estensione a uno solo.

Guarda dplyr come un'interfaccia agnostica di back-end, con tutti i bersagli che usano lo stesso grammer, dove puoi estendere i bersagli e i gestori a volontà. data.table è, dal dplyr vista di dplyr , uno di questi obiettivi.

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

dplyrè possibile che le cose data.tablenon vadano o meno.

Basato sul progetto di lavorare in memoria, data.tablepotrebbe avere un tempo molto più difficile estendersi nell'elaborazione parallela delle query dplyr.

In risposta alle domande interne al 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 combinazioni 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 utilizzare l'uno a loro più familiare o quello che è effettivamente quello giusto per il lavoro a portata di mano. 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 ciò che hai già fatto per fare astrazioni più chiare .

Prestazione

Ci sono compiti analitici che vengono eseguiti in modo sostanziale (cioè più di 2 volte) in modo più efficiente in un pacchetto rispetto all'altro.

Di nuovo, no. data.tableeccelle nell'essere efficienti in tutto ciò che fa, dove dplyrdiventa l'onere di essere limitato sotto alcuni aspetti all'archivio dati sottostante e ai gestori registrati.

Questo significa che quando ti imbatti in un problema di prestazioni con data.tablete puoi essere sicuro che sia nella tua funzione di query e se in realtà è un collo di bottiglia, data.tableallora ti sei guadagnato la gioia di presentare un rapporto. Questo vale anche quando dplyrsi usa 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?


Ecco il mio tentativo di una risposta esauriente dal punto di vista di dllich, seguendo il profilo generale della risposta di Arun (ma in qualche modo riorganizzato sulla base di priorità diverse).

Sintassi

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

Una cosa veramente importante che dplyr fa per te è che limita le tue opzioni. Sostengo che la maggior parte dei problemi con una sola tabella può essere risolta con solo cinque verbi chiave: filtrare, selezionare, modificare, organizzare e riepilogare, insieme all'avverbio "per gruppo". Questo vincolo è di grande aiuto quando stai imparando la manipolazione dei dati, perché aiuta a pensare al problema. In dplyr, ognuno di questi verbi è mappato su una singola funzione. Ogni funzione svolge un solo lavoro ed è facile da capire da sola.

Puoi creare complessità piping queste semplici operazioni insieme con %>% . Ecco un esempio di uno dei post di Arun collegato a :

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 cogliere l'essenza di ciò che sta accadendo perché le funzioni sono tutti i verbi inglesi. Lo svantaggio dei verbi inglesi è che richiedono più digitazione di [ , ma penso che possa essere in gran parte mitigato da un migliore completamento automatico.

Ecco il codice data.table equivalente:

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 il ripetuto [ in un modo che mi sembra buono per il mio occhio]. Personalmente, quando guardo il codice che ho scritto 6 mesi fa, è come guardare un codice scritto da un estraneo, quindi sono arrivato a preferire il codice diretto, se verboso.

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

  • Dal momento che quasi tutte le operazioni della tabella di dati utilizzano [ è necessario un contesto aggiuntivo per capire cosa sta succedendo. Ad esempio, x[y] unisce due tabelle di dati o estrae 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 ovvio quando si sfoglia il codice, ed è più facile individuare group_by() rispetto all'argomento by [.data.table .

Mi piace anche che il pipe non sia limitato a un solo pacchetto. Puoi iniziare tidyr tuoi dati con tidyr e finire con un grafico su ggvis . E non sei limitato ai pacchetti che scrivo - chiunque può scrivere una funzione che forma una parte senza soluzione di continuità di una pipe di manipolazione dei dati. Anzi, preferisco preferibilmente 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 solo ai frame di dati ed è facilmente generalizzabile in altri contesti: grafica web interattiva , scraping web , gists , 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 di cui non si conosce il tempo di elaborazione. Ottimizziamo dplyr per espressività su dati medi; sentiti libero di usare data.table per la velocità raw su dati più grandi.

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

Detto ciò, le prestazioni di dplyr miglioreranno nel lungo periodo. Implementeremo sicuramente alcune delle grandi idee di data.table come l'ordinamento di Radix e l'uso dello stesso indice per join e filtri. Stiamo anche lavorando sulla parallelizzazione in modo da poter sfruttare i core multipli.

Caratteristiche

Alcune cose su cui abbiamo intenzione di lavorare nel 2015:

  • il pacchetto readr , per semplificare l' readr file dal disco e dalla memoria, analogo a fread() .

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

  • Raggruppamento più flessibile come esempi di bootstrap, rollup e altro

Sto anche investendo del tempo per migliorare i connettori del database di R, la capacità di parlare con le apis web e semplificare la scansione delle pagine html .


Dobbiamo coprire almeno questi aspetti per fornire una risposta / confronto esauriente (in nessun ordine particolare di importanza): Speed , Memory usage , Syntax e Features .

Il mio intento è quello di coprire ognuno di questi il ​​più chiaramente possibile dalla prospettiva data.table.

Nota: se non esplicitamente indicato diversamente, 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 , j e by è 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 della sintassi.

1. Velocità

Parecchi benchmark (sebbene per lo più su operazioni di raggruppamento) sono stati aggiunti alla domanda che mostra già data.table diventa più veloce di dplyr come il numero di gruppi e / o righe da raggruppare aumentando, compresi i benchmark 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 diverse, che confronta anche i pandas .

Su benchmark, sarebbe bello coprire anche questi aspetti rimanenti:

  • Operazioni di raggruppamento che coinvolgono un sottoinsieme di righe , ovvero operazioni di tipo DT[x > val, sum(y), by = z] .

  • Confrontate altre operazioni come aggiornamento e join .

  • Oltre al runtime, anche il benchmark di memoria per ogni operazione.

2. Utilizzo della memoria

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

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

  2. l'interfaccia data.table al momento 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 aggiornerà mai per riferimento. L'equivalente dplyr sarebbe (notare 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, soprattutto all'interno di una funzione, potrebbe non essere sempre auspicabile. Ma questa è una caratteristica incredibilmente utile: vedi this e this post per casi interessanti. E vogliamo tenerlo.

    Pertanto stiamo lavorando all'esportazione della funzione shallow() in data.table che fornirà all'utente entrambe le possibilità . Ad esempio, se è preferibile 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 usando shallow() , la vecchia funzionalità viene mantenuta:

    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 usando shallow() , capiamo che non si desidera modificare l'oggetto originale. Ci prendiamo cura di tutto internamente per assicurarci che, mentre assicuri anche di copiare le colonne, modifichi solo quando è assolutamente necessario . Una volta implementato, questo dovrebbe risolvere completamente il problema della trasparenza referenziale fornendo all'utente entrambe le possibilità.

    Inoltre, una volta che shallow() viene esportato l'interfaccia data.table di dplyr dovrebbe evitare quasi tutte le copie. Quindi chi preferisce la sintassi di dplyr può usarlo con data.tables.

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

  3. Aggrega 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) * mul per ogni riga in DT2 mentre ti DT2 per colonne x,y . Possiamo:

    • 1) aggregare DT1 per 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 sola (usando la funzione by = .EACHI ):

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

    Qual è il vantaggio?

    • Non è necessario allocare memoria per il risultato intermedio.

    • Non è necessario raggruppare / hash due volte (uno per l'aggregazione e altri per l'unione).

    • E, ancora più importante, l'operazione che volevamo eseguire è chiara osservando j in (2).

    Controlla questo post per una spiegazione dettagliata di by = .EACHI . Nessun risultato intermedio viene materializzato e il join + aggregato viene eseguito tutto in una volta.

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

    In dplyr dovresti unirti e aggregare o aggregare prima e poi unirti , nessuno dei quali è altrettanto efficiente, in termini di memoria (che a sua volta si traduce in velocità).

  4. Aggiorna e partecipa:

    Si consideri il codice data.table mostrato di seguito:

    DT1[DT2, col := i.mul]
    

    aggiunge / aggiorna il col colonna di DT1 con mul di DT2 su quelle file in cui la colonna chiave di DT2 corrisponde a DT1 . Non penso che esista un equivalente esatto di questa operazione in dplyr , cioè, senza evitare un'operazione *_join , che dovrebbe copiare l'intero DT1 solo per aggiungere una nuova colonna ad esso, che non è necessaria.

    Controlla questo post per uno scenario di utilizzo reale.

Per riassumere, è importante rendersi conto che ogni aspetto dell'ottimizzazione è importante. Come direbbe Grace Hopper , fai attenzione ai tuoi nanosecondi !

3. Sintassi

Diamo ora un'occhiata alla sintassi . Hadley ha commentato here :

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

Trovo questa osservazione inutile perché è molto soggettiva. Quello che possiamo forse provare è contrastare la coerenza nella sintassi . Confronteremo la sintassi data.table e dplyr side-by-side.

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 data.table è compatta e dplyr è abbastanza dettagliato. Le cose sono più o meno equivalenti nel caso (a).

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

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

    • Allo stesso modo nel caso (c), quando abbiamo la condizione if-else , 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 condizione if soddisfa e salta altrimenti, non possiamo utilizzare summarise() direttamente (AFAICT). Dobbiamo prima filter() e poi riepilogare perché summarise() si aspetta sempre un singolo valore .

      Mentre restituisce lo stesso risultato, l'uso di filter() rende l'operazione effettiva meno ovvia.

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

  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 usa la famigerata funzione di base lapply() , mentre dplyr introduce *_each() insieme a un sacco di funzioni di funs() .

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

    • Nel caso (b), la sintassi di dplyr è relativamente semplice. Migliorare le aggregazioni / aggiornamenti su più funzioni è sulla lista di data.table.

    • Nel caso (c), tuttavia, dplyr restituirà n() tante volte quante più colonne, invece di una sola volta. In data.table, tutto ciò che dobbiamo fare è restituire una lista in j . Ogni elemento della lista diventerà una colonna nel risultato. Quindi, possiamo usare, ancora una volta, la familiare funzione di base c() per concatenare .N ad una list che restituisce una list .

    Nota: ancora una volta, in data.table, tutto ciò che dobbiamo fare è restituire una lista in j . Ogni elemento della lista diventerà una colonna in risultato. È possibile utilizzare le funzioni di base c() , as.list() , lapply() , list() ecc ... per eseguire questa operazione, senza dover imparare nuove funzioni.

    Dovrai imparare solo le variabili speciali - .N e .SD almeno. L'equivalente in dplyr è n() e .

  3. Si unisce

    dplyr fornisce funzioni separate per ogni tipo di join dove come data.table consente i join usando la stessa sintassi DT[i, j, by] (e con ragione). Fornisce anche una funzione equivalente merge.data.table() come 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 ogni join molto più bello (sinistra, destra, interno, anti, semi, ecc.), Mentre ad altri potrebbero piacere DT[i, j, by] o merge() data.table, che è simile alla base R.

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

    • data.tables può selezionare le colonne durante l'unione (2), e in dplyr sarà necessario select() prima su entrambi i.framework prima di unirsi come mostrato sopra. Altrimenti si materializzerebbe l'unione con colonne non necessarie solo per rimuoverle in seguito e questo è inefficiente.

    • data.tables può aggregarsi mentre si unisce (3) e aggiornare mentre si unisce (4), utilizzando la funzione by = .EACHI . Perché materializzare l'intero risultato del join per aggiungere / aggiornare solo poche colonne?

    • data.table è in grado di rollare join (5) - roll forward, LOCF , roll backward, NOCB , nearest .

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

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

Ancora una volta, la sintassi è coerente con DT[i, j, by] con ulteriori argomenti 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 / non uguali, dovrai ricorrere a do() . Devi sapere in anticipo su tutte le tue funzioni restituire il 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))))
    
    • .SD equivalente di .SD è .

    • In data.table, puoi lanciare praticamente qualsiasi cosa in j - l'unica cosa da ricordare è che restituisca una lista in modo che ogni elemento della lista venga convertito in una colonna.

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

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

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

Per riassumere, ho evidenziato in particolare diversi casi in cui la sintassi di dplyr è inefficiente, limitata o non riesce a rendere le operazioni dirette. Ciò è particolarmente dovuto al fatto che data.table ottiene un po 'di backlash sulla sintassi "più difficile da leggere / imparare" (come quella incollata / collegata sopra). La maggior parte dei post che trattano dplyr parlano di operazioni più semplici. E questo è fantastico. Ma è importante rendersi conto della sua sintassi e dei limiti delle funzionalità, e devo ancora vedere un post su di esso.

data.table ha le sue stranezze (alcune delle quali ho sottolineato che stiamo tentando di risolvere). Stiamo anche tentando di migliorare i join di data.table come ho evidenziato here .

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

4. Caratteristiche

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

  • Lettore di file veloce e veloce è disponibile da molto tempo.

  • fwrite - NUOVO nell'attuale devel, v1.9.7, è ora disponibile uno scrittore di file veloci in parallelo . Vedi questo post per una spiegazione dettagliata sull'implementazione e #1664 per tenere traccia di ulteriori sviluppi.

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

  • Raggruppamento ad hoc : dplyr ordina automaticamente i risultati raggruppando le variabili durante il summarise() , che potrebbe non essere sempre desiderabile.

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

  • Join non equi : è una NUOVA funzione disponibile dalla v1.9.7 +. Permette join utilizzando altri operatori <=, <, >, >= insieme a tutti gli altri vantaggi di join di data.table.

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

  • setorder() in data.table che consente un rapido riordino dei dati. tabelle per riferimento.

  • dplyr fornisce un'interfaccia ai database usando la stessa sintassi, che data.table non al momento.

  • data.table fornisce equivalenti più veloci delle operazioni set dalla v1.9.7 + (scritta da Jan Gorecki) - fsetdiff , fintersect , funion e fsetequal con ulteriori argomenti all (come in SQL).

  • data.table carica in modo pulito senza avvisi di mascheramento e ha un meccanismo here descritto per la compatibilità con [.data.frame quando viene passato a qualsiasi pacchetto R. dplyr cambia le funzioni di base filter , lag e [ che possono causare problemi; ad es. here e here .

Finalmente:

  • Sui database: non c'è motivo per cui data.table non possa fornire un'interfaccia simile, ma questa non è una priorità ora. Potrebbe essere sbalzato se agli utenti piacesse molto quella funzione .. non è sicuro.

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

    • Al momento sono in corso progressi (in v1.9.7 devel) per parallelizzare parti in termini di tempo noto per incrementi di prestazioni incrementali con OpenMP .




dplyr