95 votes

Quel est le moyen le plus rapide de fusionner/joindre des cadres de données dans R ?

Par exemple (je ne suis pas sûr que ce soit 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 obtenu 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

44voto

G. Grothendieck Points 40825

L'approche de mise en correspondance fonctionne lorsqu'il existe une clé unique dans le second cadre de données pour chaque valeur de clé dans le premier. S'il y a des doublons dans le deuxième cadre de données, les approches de correspondance et de fusion ne sont pas les mêmes. La correspondance est, bien sûr, plus rapide car elle ne fait pas autant d'efforts. En particulier, elle ne recherche jamais les 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é posté dans la question, il pourrait 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 que le sql select ne soit exécuté et cela, en partie, explique pourquoi il est si lent. L'idée de sqldf est que les cadres de données dans votre session R constituent la base de données, pas les tables dans sqlite. Ainsi, chaque fois que le code se réfère à un nom de table non qualifié, il le cherchera dans votre espace de travail R, et non dans la base de données principale de sqlite. Ainsi, l'instruction select qui a été montrée lit d1 et d2 de l'espace de travail dans la base de données principale de sqlite en détruisant ceux qui étaient là avec les index. En conséquence, il fait une jointure sans index. Si vous vouliez utiliser les versions de d1 et d2 qui étaient dans la base de données principale de sqlite, vous devriez vous y référer comme main.d1 et main.d2 et non comme d1 et d2. De plus, si vous essayez de faire en sorte que l'opération soit aussi rapide que possible, notez qu'une simple jointure ne peut pas utiliser les index sur les deux tables, vous pouvez donc gagner du temps en créant 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 paquet le plus rapide. Par exemple, nous faisons une fusion et un agrégat ci-dessous. Nous voyons 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 alors que dans le second exemple, sqldf, aggregate, data.table et plyr -- presque l'inverse du premier. Dans le premier exemple, sqldf est 3x plus lent que data.table et dans le second, il est 200x plus rapide que plyr et 100 fois plus rapide que data.table. Nous montrons ci-dessous le code d'entrée, les temps de sortie pour la fusion et les temps 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 ceux que R peut gérer (si vous utilisez l'argument dbname de sqldf) alors que les autres approches sont limitées au traitement en mémoire principale. Nous avons également illustré sqldf avec sqlite mais il supporte également 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 les suivants :

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 les suivants :

        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

39voto

Matt Dowle Points 20936

Les 132 secondes rapportées dans les résultats de Gabor pour data.table est en fait des fonctions de base de chronométrage colMeans y cbind (l'allocation et la copie de mémoire induites par l'utilisation de ces fonctions). Il existe de bonnes et de mauvaises façons d'utiliser data.table aussi.

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 très bien le plyr. Veuillez donc vérifier auprès de Hadley avant de vous fier au plyr ici. Notez également que le data.table incluent le temps de conversion en data.table et mettre la clé, pour le plaisir.


Cette réponse a été mise à jour depuis sa réponse initiale en décembre 2010. Les résultats du benchmark précédent sont 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

14voto

Marek Points 18000

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 0.13s à 3.37s).

Mes horaires :

  • merge : 3.32s
  • plyr : 0.84s
  • match : 0.12s

11voto

dotcomken Points 117

J'ai pensé qu'il serait intéressant de poster un benchmark avec dplyr dans le mélange : (il y avait beaucoup de choses en cours d'exécution)

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

Je viens de l'ajouter :

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

et configurer les données pour dplyr avec un tableau de données :

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

Mis à jour : J'ai supprimé data.tableBad et plyr et rien d'autre que RStudio ouvert (i7, 16GB 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 data table :

            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'

Pour des raisons de cohérence, voici l'original avec tout et data.table 1.9 et dplyr 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 le nouveau data.table et dplyr :)

Un ensemble de données plus important :

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 mémoire vive pour conserver les données avant de lancer 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 la mémoire vive a explosé. 32GB le gèreront sans problème.


[Edit by Arun] (dotcomken , pouvez-vous s'il vous plaît exécuter ce code et coller vos résultats d'évaluation ? 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))
) 

Conformément à la demande d'Arun, voici la sortie de ce que vous m'avez fourni à 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, je me suis endormi tard.

L'utilisation de dplyr avec un cadre de données semble être le moyen le moins efficace de traiter les résumés. Est-ce des méthodes pour comparer la fonctionnalité exacte de data.table et de dplyr avec leurs méthodes de structure de données incluses ? Je préfèrerais presque séparer cela car la plupart des données devront être nettoyées avant de faire un group_by ou de créer le data.table. C'est 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.

1voto

Amarjeet Points 468

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

La 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 être sûr de ne faire correspondre que les 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 cadres 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)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X