Quel est le moyen le plus rapide de fusionner / joindre des data.frames dans R?

97

Par exemple (je ne sais pas si l'exemple le plus représentatif):

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

Voici ce que j'ai jusqu'à présent:

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
datasmurf
la source
La bonne façon de faire la manière sqldf est indiquée ci-dessous par Gabor: créez un seul index (disons sur d1) et utilisez d1.main au lieu de d1 dans l'instruction select (sinon, il n'utilisera pas l'index). Le chronométrage est dans ce cas de 13,6 sec. Construire des index sur les deux tables n'est pas non plus nécessaire dans le cas de data.table, il suffit de faire "dt2 <- data.table (d2)" et le timing sera de 3,9 secondes.
datasmurf
Les deux réponses fournissent des informations précieuses, qui méritent d'être lues toutes les deux (même si une seule peut être «acceptée»).
datasmurf
vous comparez la jointure gauche à la jointure intérieure dans votre question
jangorecki

Réponses:

46

L'approche de correspondance fonctionne lorsqu'il existe une clé unique dans la deuxième trame de données pour chaque valeur de clé de la première. S'il y a des doublons dans la deuxième trame de données, les approches de correspondance et de fusion ne sont pas les mêmes. Le match est, bien sûr, plus rapide car il n'en fait pas autant. En particulier, il ne recherche jamais de clés en double. (suite après le code)

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

Dans le code sqldf qui a été publié dans la question, il peut sembler que des index ont été utilisés sur les deux tables mais, en fait, ils sont placés sur des tables qui ont été écrasées avant l'exécution de sql select et qui, en partie, explique pourquoi c'est si lent. L'idée de sqldf est que les trames de données de votre session R constituent la base de données, pas les tables de sqlite. Ainsi, chaque fois que le code fait référence à un nom de table non qualifié, il le recherchera dans votre espace de travail R - pas dans la base de données principale de sqlite. Ainsi, l'instruction select qui a été affichée lit d1 et d2 de l'espace de travail dans la base de données principale de sqlite écrasant celles qui étaient là avec les index. En conséquence, il effectue une jointure sans index. Si vous vouliez utiliser les versions de d1 et d2 qui se trouvaient dans la base de données principale de sqlite, vous devriez les appeler main.d1 et main. d2 et non comme d1 et d2. De plus, si vous essayez de le faire fonctionner aussi vite que possible, notez qu'une simple jointure ne peut pas utiliser les index sur les deux tables afin que vous puissiez gagner du temps pour créer l'un des index. Dans le code ci-dessous, nous illustrons ces points.

Il est intéressant de noter que le calcul précis peut faire une énorme différence sur le package le plus rapide. Par exemple, nous faisons une fusion et un agrégat ci-dessous. On voit que les résultats sont presque inversés pour les deux. Dans le premier exemple, du plus rapide au plus lent, nous obtenons: data.table, plyr, merge et sqldf tandis que dans le second exemple sqldf, aggregate, data.table et plyr - presque l'inverse du premier. Dans le premier exemple, sqldf est 3 fois plus lent que data.table et dans le second, il est 200 fois plus rapide que plyr et 100 fois plus rapide que data.table. Ci-dessous, nous montrons le code d'entrée, les horaires de sortie pour la fusion et les horaires de sortie pour l'agrégat. Il est également intéressant de noter que sqldf est basé sur une base de données et peut donc gérer des objets plus grands que R peut gérer (si vous utilisez l'argument dbname de sqldf) tandis que les autres approches sont limitées au traitement en mémoire principale. Nous avons également illustré sqldf avec sqlite, mais il prend également en charge les bases de données H2 et 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"))
)

Les résultats des deux appels de référence comparant les calculs de fusion sont:

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

Les résultats de l'appel de référence comparant les calculs agrégés sont:

        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
G. Grothendieck
la source
Merci, Gabor. Excellents points, j'ai apporté quelques ajustements via des commentaires à la question initiale. En fait, je suppose que l'ordre peut changer même dans le cas de "fusion" en fonction des tailles relatives des tables, de la multiplicité des clés, etc. (c'est pourquoi j'ai dit que je ne suis pas sûr que mon exemple soit représentatif). Néanmoins, il est agréable de voir toutes les différentes solutions au problème.
datasmurf
J'apprécie également le commentaire sur le cas de «l'agrégation». Bien que ce soit différent de la configuration de «fusion» dans la question, il est très pertinent. J'aurais en fait posé une question à ce sujet dans une question distincte, mais il y en a déjà une ici stackoverflow.com/questions/3685492/… . Vous voudrez peut-être y contribuer également, car sur la base des résultats ci-dessus, la solution sqldf pourrait battre toutes les réponses existantes;)
datasmurf
40

Les 132 secondes rapportées dans les résultats de Gabor data.tablesont en fait des fonctions de base de synchronisation colMeanset cbind(l'allocation de mémoire et la copie induites par l'utilisation de ces fonctions). Il existe également de bonnes et de mauvaises manières d'utiliser 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)

Veuillez noter que je ne connais pas bien le plyr alors veuillez vérifier auprès de Hadley avant de vous fier aux plyrhoraires ici. Notez également que le data.tablene comprend le temps de conversion data.tableet de définir la clé, pour la farceur.


Cette réponse a été mise à jour depuis la réponse initiale en décembre 2010. Les résultats de référence précédents sont indiqués ci-dessous. Veuillez consulter l'historique des révisions de cette réponse pour voir ce qui a changé.

              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
Matt Dowle
la source
Étant donné que ddply ne fonctionne qu'avec des trames de données, cet exemple donne les pires performances. J'espère avoir une meilleure interface pour ce type d'opération courante dans une future version.
hadley
1
FYI: vous ne pouvez pas utiliser les .Internalappels dans les packages CRAN, voir la politique de référentiel CRAN .
Joshua Ulrich
@JoshuaUlrich Vous pourriez quand la réponse a été écrite il y a près de 2 ans, iirc. Je vais mettre à jour cette réponse comme data.tableoptimise automatiquement meanmaintenant (sans appeler en .Internalinterne).
Matt Dowle
@MatthewDowle: Ouais, je ne sais pas quand / si cela a changé. Je sais juste que c'est le cas maintenant. Et c'est parfaitement bien dans votre réponse, cela ne fonctionnera tout simplement pas dans les packages.
Joshua Ulrich
1
@AleksandrBlekh Merci. J'ai lié vos commentaires ici à la demande de fonctionnalité existante n ° 599 . Allons-y. Votre exemple de code montre bien la forboucle, c'est bien. Pourriez-vous ajouter plus d'informations sur "l'analyse SEM" à ce problème? Par exemple, je suppose que SEM = Microscope électronique à balayage? En savoir plus sur l'application la rend plus intéressante pour nous et nous aide à établir des priorités.
Matt Dowle
16

Pour une tâche simple (valeurs uniques des deux côtés de la jointure), j'utilise match:

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

C'est beaucoup plus rapide que la fusion (sur ma machine de 0,13 s à 3,37 s).

Mes horaires:

  • merge: 3,32 s
  • plyr: 0,84 s
  • match: 0,12 s
Marek
la source
4
Merci, Marek. Une explication de la raison pour laquelle c'est si rapide (crée une table d'index / de hachage) peut être trouvée ici: tolstoy.newcastle.edu.au/R/help/01c/2739.html
datasmurf
11

J'ai pensé qu'il serait intéressant de publier un benchmark avec dplyr dans le mix: (avait beaucoup de choses en cours)

            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)"

Vient d'ajouter:

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

et configurez les données pour dplyr avec une table de données:

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

Actualisé: j'ai supprimé data.tableBad et plyr et rien d'autre que RStudio ouvert (i7, 16 Go de RAM).

Avec data.table 1.9 et dplyr avec 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

Avec data.table 1.9 et dplyr avec tableau de données:

            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'

Par souci de cohérence, voici l'original avec all et data.table 1.9 et dplyr en utilisant un tableau de données:

            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

Je pense que ces données sont trop petites pour la nouvelle table data.table et déplyr :)

Ensemble de données plus grand:

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)

Il a fallu environ 10 à 13 Go de RAM juste pour contenir les données avant d'exécuter le benchmark.

Résultats:

            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

J'ai essayé un milliard mais j'ai fait sauter le bélier. 32 Go le géreront sans problème.


[Edit by Arun] (dotcomken, pourriez-vous s'il vous plaît exécuter ce code et coller vos résultats d'analyse comparative? Merci).

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

Selon la demande d'Arun, voici la sortie de ce que vous m'avez fourni pour exécuter:

        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

Désolé pour la confusion, tard dans la nuit m'est arrivé.

L'utilisation de dplyr avec une base de données semble être le moyen le moins efficace de traiter les résumés. Ces méthodes pour comparer la fonctionnalité exacte de data.table et dplyr avec leurs méthodes de structure de données sont-elles incluses? Je préférerais presque séparer cela car la plupart des données devront être nettoyées avant de group_by ou de créer le data.table. Cela peut être une question de goût, mais je pense que le plus important est l'efficacité avec laquelle les données peuvent être modélisées.

dotcomken
la source
1
Bonne mise à jour. Merci. Je pense que votre machine est une bête par rapport à cet ensemble de données. Quelle est la taille de votre cache L2 (et L3 s'il existe)?
Arun
i7 L2 est 2x256 Ko 8 voies, L3 est 4 Mo 16 voies. SSD 128 Go, Win 7 sur un Dell
inspiron
1
Pourriez-vous reformater votre exemple. Je suis un peu confus. Data.table est-il meilleur (dans cet exemple) que dplyr? Si oui, dans quelles circonstances.
csgillespie
1

En utilisant la fonction de fusion et ses paramètres facultatifs:

Jointure interne: merge (df1, df2) fonctionnera pour ces exemples car R joint automatiquement les cadres par des noms de variables communs, mais vous voudrez probablement spécifier merge (df1, df2, by = "CustomerId") pour vous assurer que vous correspondaient uniquement aux champs souhaités. Vous pouvez également utiliser les paramètres by.x et by.y si les variables correspondantes ont des noms différents dans les différents blocs de données.

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)
Amarjeet
la source
La question portait sur la performance. Vous avez simplement fourni la syntaxe des jointures. Bien qu'utile, cela ne répond pas à la question. Cette réponse manque de données de référence utilisant les exemples du PO pour montrer qu'il fonctionne mieux, ou du moins très compétitif.
Michael Tuchman