Qual è il modo più veloce per unire / unire data.frames in R?


97

Ad esempio (non sono sicuro se l'esempio più rappresentativo però):

N <- 1e6
d1 <- data.frame(x=sample(N,N), y1=rnorm(N))
d2 <- data.frame(x=sample(N,N), y2=rnorm(N))

Questo è quello che ho ottenuto finora:

d <- merge(d1,d2)
# 7.6 sec

library(plyr)
d <- join(d1,d2)
# 2.9 sec

library(data.table)
dt1 <- data.table(d1, key="x")
dt2 <- data.table(d2, key="x")
d <- data.frame( dt1[dt2,list(x,y1,y2=dt2$y2)] )
# 4.9 sec

library(sqldf)
sqldf()
sqldf("create index ix1 on d1(x)")
sqldf("create index ix2 on d2(x)")
d <- sqldf("select * from d1 inner join d2 on d1.x=d2.x")
sqldf()
# 17.4 sec

Il modo corretto di eseguire il modo sqldf è indicato di seguito da Gabor: creare un solo indice (ad esempio su d1) e utilizzare d1.main invece di d1 nell'istruzione select (altrimenti non utilizzerà l'indice). Il tempo è in questo caso 13,6 sec. La creazione di indici su entrambe le tabelle in realtà non è necessaria neanche nel caso data.table, basta fare "dt2 <- data.table (d2)" e il tempo sarà 3,9 sec.
datasmurf

Entrambe le risposte forniscono informazioni preziose, vale la pena leggerle entrambe (sebbene solo una possa essere "accettata").
datasmurf

stai confrontando il join sinistro con il join interno nella tua domanda
jangorecki

Risposte:


46

L'approccio match funziona quando c'è una chiave univoca nel secondo frame di dati per ogni valore di chiave nel primo. Se sono presenti duplicati nel secondo frame di dati, gli approcci di corrispondenza e unione non sono gli stessi. La partita è, ovviamente, più veloce poiché non sta facendo tanto. In particolare non cerca mai chiavi duplicate. (continua dopo il codice)

DF1 = data.frame(a = c(1, 1, 2, 2), b = 1:4)
DF2 = data.frame(b = c(1, 2, 3, 3, 4), c = letters[1:5])
merge(DF1, DF2)
    b a c
  1 1 1 a
  2 2 1 b
  3 3 2 c
  4 3 2 d
  5 4 2 e
DF1$c = DF2$c[match(DF1$b, DF2$b)]
DF1$c
[1] a b c e
Levels: a b c d e

> DF1
  a b c
1 1 1 a
2 1 2 b
3 2 3 c
4 2 4 e

Nel codice sqldf che è stato pubblicato nella domanda, potrebbe sembrare che gli indici siano stati utilizzati sulle due tabelle ma, in realtà, sono posizionati su tabelle che sono state sovrascritte prima che sql select venga mai eseguito e che, in parte, spiega perché è così lento. L'idea di sqldf è che i frame di dati nella sessione R costituiscono il database, non le tabelle in sqlite. Pertanto, ogni volta che il codice fa riferimento a un nome di tabella non qualificato, lo cercherà nell'area di lavoro R, non nel database principale di sqlite. Quindi l'istruzione select mostrata legge d1 e d2 dallo spazio di lavoro nel database principale di sqlite, distruggendo quelli che erano lì con gli indici. Di conseguenza fa un join senza indici. Se volessi utilizzare le versioni di d1 e d2 che erano nel database principale di sqlite dovresti fare riferimento a loro come main.d1 e main. d2 e non come d1 e d2. Inoltre, se stai cercando di farlo funzionare il più velocemente possibile, tieni presente che un semplice join non può utilizzare gli indici su entrambe le tabelle, quindi puoi risparmiare il tempo necessario per creare uno degli indici. Nel codice seguente illustriamo questi punti.

Vale la pena notare che il calcolo preciso può fare un'enorme differenza su quale pacchetto è più veloce. Ad esempio, facciamo un'unione e un'aggregazione di seguito. Vediamo che i risultati sono quasi invertiti per i due. Nel primo esempio dal più veloce al più lento otteniamo: data.table, plyr, merge e sqldf mentre nel secondo esempio sqldf, aggregate, data.table e plyr - quasi il contrario del primo. Nel primo esempio sqldf è 3 volte più lento di data.table e nel secondo è 200 volte più veloce di plyr e 100 volte più veloce di data.table. Di seguito mostriamo il codice di input, i tempi di output per l'unione e i tempi di output per l'aggregato. Vale anche la pena notare che sqldf è basato su un database e quindi può gestire oggetti più grandi di quelli che R può gestire (se si utilizza l'argomento dbname di sqldf) mentre gli altri approcci sono limitati all'elaborazione nella memoria principale. Inoltre abbiamo illustrato sqldf con sqlite, ma supporta anche i database H2 e PostgreSQL.

library(plyr)
library(data.table)
library(sqldf)

set.seed(123)
N <- 1e5
d1 <- data.frame(x=sample(N,N), y1=rnorm(N))
d2 <- data.frame(x=sample(N,N), y2=rnorm(N))

g1 <- sample(1:1000, N, replace = TRUE)
g2<- sample(1:1000, N, replace = TRUE)
d <- data.frame(d1, g1, g2)

library(rbenchmark)

benchmark(replications = 1, order = "elapsed",
   merge = merge(d1, d2),
   plyr = join(d1, d2),
   data.table = { 
      dt1 <- data.table(d1, key = "x")
      dt2 <- data.table(d2, key = "x")
      data.frame( dt1[dt2,list(x,y1,y2=dt2$y2)] )
      },
   sqldf = sqldf(c("create index ix1 on d1(x)",
      "select * from main.d1 join d2 using(x)"))
)

set.seed(123)
N <- 1e5
g1 <- sample(1:1000, N, replace = TRUE)
g2<- sample(1:1000, N, replace = TRUE)
d <- data.frame(x=sample(N,N), y=rnorm(N), g1, g2)

benchmark(replications = 1, order = "elapsed",
   aggregate = aggregate(d[c("x", "y")], d[c("g1", "g2")], mean), 
   data.table = {
      dt <- data.table(d, key = "g1,g2")
      dt[, colMeans(cbind(x, y)), by = "g1,g2"]
   },
   plyr = ddply(d, .(g1, g2), summarise, avx = mean(x), avy=mean(y)),
   sqldf = sqldf(c("create index ix on d(g1, g2)",
      "select g1, g2, avg(x), avg(y) from main.d group by g1, g2"))
)

I risultati della chiamata di due benchmark che confrontano i calcoli di unione sono:

Joining by: x
        test replications elapsed relative user.self sys.self user.child sys.child
3 data.table            1    0.34 1.000000      0.31     0.01         NA        NA
2       plyr            1    0.44 1.294118      0.39     0.02         NA        NA
1      merge            1    1.17 3.441176      1.10     0.04         NA        NA
4      sqldf            1    3.34 9.823529      3.24     0.04         NA        NA

L'output della chiamata benchmark che confronta i calcoli aggregati sono:

        test replications elapsed  relative user.self sys.self user.child sys.child
4      sqldf            1    2.81  1.000000      2.73     0.02         NA        NA
1  aggregate            1   14.89  5.298932     14.89     0.00         NA        NA
2 data.table            1  132.46 47.138790    131.70     0.08         NA        NA
3       plyr            1  212.69 75.690391    211.57     0.56         NA        NA

Grazie, Gabor. Ottimi punti, ho apportato alcune modifiche tramite commenti alla domanda originale. In realtà immagino che l'ordine potrebbe cambiare anche nel caso di "unione" a seconda delle dimensioni relative delle tabelle, della molteplicità delle chiavi, ecc. (Ecco perché ho detto che non sono sicuro che il mio esempio sia rappresentativo). Tuttavia, è bello vedere tutte le diverse soluzioni al problema.
datasmurf

Apprezzo anche il commento sul caso di "aggregazione". Sebbene questo sia diverso dalla configurazione di "unione" nella domanda, è molto rilevante. In realtà l'avrei chiesto in una domanda separata, ma ce n'è già una qui stackoverflow.com/questions/3685492/… . Potresti voler contribuire anche a quello, poiché in base ai risultati sopra, la soluzione sqldf potrebbe battere tutte le risposte esistenti lì;)
datasmurf

40

I 132 secondi riportati nei risultati di Gabor per data.tablesono in realtà le funzioni di base di temporizzazione colMeanse cbind(l'allocazione della memoria e la copia indotte dall'uso di tali funzioni). Ci sono anche modi buoni e cattivi di utilizzo data.table.

benchmark(replications = 1, order = "elapsed", 
  aggregate = aggregate(d[c("x", "y")], d[c("g1", "g2")], mean),
  data.tableBad = {
     dt <- data.table(d, key = "g1,g2") 
     dt[, colMeans(cbind(x, y)), by = "g1,g2"]
  }, 
  data.tableGood = {
     dt <- data.table(d, key = "g1,g2") 
     dt[, list(mean(x),mean(y)), by = "g1,g2"]
  }, 
  plyr = ddply(d, .(g1, g2), summarise, avx = mean(x), avy=mean(y)),
  sqldf = sqldf(c("create index ix on d(g1, g2)",
      "select g1, g2, avg(x), avg(y) from main.d group by g1, g2"))
  ) 

            test replications elapsed relative user.self sys.self
3 data.tableGood            1    0.15    1.000      0.16     0.00
5          sqldf            1    1.01    6.733      1.01     0.00
2  data.tableBad            1    1.63   10.867      1.61     0.01
1      aggregate            1    6.40   42.667      6.38     0.00
4           plyr            1  317.97 2119.800    265.12    51.05

packageVersion("data.table")
# [1] ‘1.8.2’
packageVersion("plyr")
# [1] ‘1.7.1’
packageVersion("sqldf")
# [1] ‘0.4.6.4’
R.version.string
# R version 2.15.1 (2012-06-22)

Si prega di notare che non conosco bene il plyr, quindi per favore controlla con Hadley prima di fare affidamento sui plyrtempi qui. Si noti inoltre che il data.tabletempo per convertire data.tablee impostare la chiave, per fareness.


Questa risposta è stata aggiornata dalla risposta originaria nel dicembre 2010. Di seguito sono riportati i risultati del benchmark precedente. Vedere la cronologia delle revisioni di questa risposta per vedere cosa è cambiato.

              test replications elapsed   relative user.self sys.self
4   data.tableBest            1   0.532   1.000000     0.488    0.020
7            sqldf            1   2.059   3.870301     2.041    0.008
3 data.tableBetter            1   9.580  18.007519     9.213    0.220
1        aggregate            1  14.864  27.939850    13.937    0.316
2  data.tableWorst            1 152.046 285.800752   150.173    0.556
6 plyrwithInternal            1 198.283 372.712406   189.391    7.665
5             plyr            1 225.726 424.296992   208.013    8.004

Poiché ddply funziona solo con i frame di dati, questo è un esempio che offre prestazioni nel caso peggiore. Spero di avere un'interfaccia migliore per questo tipo di operazione comune in una versione futura.
Hadley

1
FYI: non è possibile utilizzare le .Internalchiamate nei pacchetti CRAN, vedere la politica del repository CRAN .
Joshua Ulrich,

@JoshuaUlrich Potresti quando la risposta è stata scritta quasi 2 anni fa, iirc. Aggiornerò questa risposta come data.tableottimizza automaticamente meanora (senza chiamare .Internalinternamente).
Matt Dowle

@ MatthewDowle: Sì, non sono sicuro di quando / se è cambiato. So solo che adesso è così. Ed è perfettamente a posto nella tua risposta, semplicemente non funzionerà nei pacchetti.
Joshua Ulrich,

1
@AleksandrBlekh Grazie. Ho collegato i tuoi commenti qui alla richiesta di funzionalità esistente # 599 . Andiamo lì. Il tuo codice di esempio mostra bene il forciclo, va bene. Potresti aggiungere ulteriori informazioni sull '"analisi SEM" a questo problema? Ad esempio, immagino che SEM = microscopio elettronico a scansione? Sapere di più sull'applicazione lo rende più interessante e ci aiuta a stabilire le priorità.
Matt Dowle

16

Per attività semplice (valori univoci su entrambi i lati del join) utilizzo match:

system.time({
    d <- d1
    d$y2 <- d2$y2[match(d1$x,d2$x)]
})

È molto più veloce dell'unione (sulla mia macchina da 0,13 a 3,37 secondi).

I miei tempi:

  • merge: 3,32 s
  • plyr: 0,84 s
  • match: 0,12 s

4
Grazie Marek. Qualche spiegazione del motivo per cui questo è così veloce (crea una tabella indice / hash) può essere trovata qui: tolstoy.newcastle.edu.au/R/help/01c/2739.html
datasmurf

11

Ho pensato che sarebbe stato interessante pubblicare un benchmark con dplyr nel mix: (aveva molte cose in esecuzione)

            test replications elapsed relative user.self sys.self
5          dplyr            1    0.25     1.00      0.25     0.00
3 data.tableGood            1    0.28     1.12      0.27     0.00
6          sqldf            1    0.58     2.32      0.57     0.00
2  data.tableBad            1    1.10     4.40      1.09     0.01
1      aggregate            1    4.79    19.16      4.73     0.02
4           plyr            1  186.70   746.80    152.11    30.27

packageVersion("data.table")
[1]1.8.10’
packageVersion("plyr")
[1]1.8’
packageVersion("sqldf")
[1]0.4.7’
packageVersion("dplyr")
[1]0.1.2’
R.version.string
[1] "R version 3.0.2 (2013-09-25)"

Appena aggiunto:

dplyr = summarise(dt_dt, avx = mean(x), avy = mean(y))

e imposta i dati per dplyr con una tabella dati:

dt <- tbl_dt(d)
dt_dt <- group_by(dt, g1, g2)

Aggiornato: ho rimosso data.tableBad e plyr e nient'altro che RStudio open (i7, 16 GB di ram).

Con data.table 1.9 e dplyr con data frame:

            test replications elapsed relative user.self sys.self
2 data.tableGood            1    0.02      1.0      0.02     0.00
3          dplyr            1    0.04      2.0      0.04     0.00
4          sqldf            1    0.46     23.0      0.46     0.00
1      aggregate            1    6.11    305.5      6.10     0.02

Con data.table 1.9 e dplyr con tabella dati:

            test replications elapsed relative user.self sys.self
2 data.tableGood            1    0.02        1      0.02     0.00
3          dplyr            1    0.02        1      0.02     0.00
4          sqldf            1    0.44       22      0.43     0.02
1      aggregate            1    6.14      307      6.10     0.01

packageVersion("data.table")
[1] '1.9.0'
packageVersion("dplyr")
[1] '0.1.2'

Per coerenza ecco l'originale con all e data.table 1.9 e dplyr utilizzando una tabella dati:

            test replications elapsed relative user.self sys.self
5          dplyr            1    0.01        1      0.02     0.00
3 data.tableGood            1    0.02        2      0.01     0.00
6          sqldf            1    0.47       47      0.46     0.00
1      aggregate            1    6.16      616      6.16     0.00
2  data.tableBad            1   15.45     1545     15.38     0.01
4           plyr            1  110.23    11023     90.46    19.52

Penso che questi dati siano troppo piccoli per i nuovi data.table e dplyr :)

Set di dati più grande:

N <- 1e8
g1 <- sample(1:50000, N, replace = TRUE)
g2<- sample(1:50000, N, replace = TRUE)
d <- data.frame(x=sample(N,N), y=rnorm(N), g1, g2)

Ci sono voluti circa 10-13 GB di RAM solo per contenere i dati prima di eseguire il benchmark.

Risultati:

            test replications elapsed relative user.self sys.self
1          dplyr            1   14.88        1      6.24     7.52
2 data.tableGood            1   28.41        1     18.55      9.4

Ho provato un miliardo ma ho fatto saltare in aria. 32 GB lo gestiranno senza problemi.


[Modifica di Arun] (dotcomken, potresti eseguire questo codice e incollare i risultati del benchmarking? Grazie).

require(data.table)
require(dplyr)
require(rbenchmark)

N <- 1e8
g1 <- sample(1:50000, N, replace = TRUE)
g2 <- sample(1:50000, N, replace = TRUE)
d <- data.frame(x=sample(N,N), y=rnorm(N), g1, g2)

benchmark(replications = 5, order = "elapsed", 
  data.table = {
     dt <- as.data.table(d) 
     dt[, lapply(.SD, mean), by = "g1,g2"]
  }, 
  dplyr_DF = d %.% group_by(g1, g2) %.% summarise(avx = mean(x), avy=mean(y))
) 

Come da richiesta di Arun, ecco l'output di ciò che mi hai fornito per eseguire:

        test replications elapsed relative user.self sys.self
1 data.table            5   15.35     1.00     13.77     1.57
2   dplyr_DF            5  137.84     8.98    136.31     1.44

Scusa per la confusione, a tarda notte mi ha colpito.

L'uso di dplyr con data frame sembra essere il modo meno efficiente per elaborare i riepiloghi. Sono inclusi questi metodi per confrontare l'esatta funzionalità di data.table e dplyr con i loro metodi di struttura dati? Preferirei quasi separarlo poiché la maggior parte dei dati dovrà essere pulita prima di group_by o creare data.table. Potrebbe essere una questione di gusti, ma penso che la parte più importante sia l'efficienza con cui i dati possono essere modellati.


1
Bel aggiornamento. Grazie. Penso che la tua macchina sia una bestia rispetto a questo set di dati .. Qual è la dimensione della tua cache L2 (e L3 se esiste)?
Arun

i7 L2 è 2x256 KB a 8 vie, L3 è 4 MB a 16 vie. SSD da 128 GB, Win 7 su un Dell Inspiron
dotcomken

1
Potresti riformattare il tuo esempio. Sono un po 'confuso. Data.table è migliore (in questo esempio) di dplyr? In tal caso, in quali circostanze.
csgillespie

1

Utilizzando la funzione di unione e i suoi parametri opzionali:

Inner join: merge (df1, df2) funzionerà per questi esempi perché R unisce automaticamente i frame con nomi di variabili comuni, ma è molto probabile che tu voglia specificare merge (df1, df2, by = "CustomerId") per assicurarti di corrispondevano solo ai campi desiderati. È inoltre possibile utilizzare i parametri by.x e by.y se le variabili corrispondenti hanno nomi diversi nei diversi frame di dati.

Outer join: merge(x = df1, y = df2, by = "CustomerId", all = TRUE)

Left outer: merge(x = df1, y = df2, by = "CustomerId", all.x = TRUE)

Right outer: merge(x = df1, y = df2, by = "CustomerId", all.y = TRUE)

Cross join: merge(x = df1, y = df2, by = NULL)

La domanda riguardava le prestazioni. Hai semplicemente fornito la sintassi per i join. Sebbene utile, non risponde alla domanda. Questa risposta manca di dati di riferimento che utilizzano gli esempi del PO per dimostrare che funziona meglio, o almeno altamente competitivo.
Michael Tuchman
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.