web-dev-qa-db-fra.com

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

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

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
94
datasmurf

L'approche par correspondance fonctionne lorsqu'il existe une clé unique dans la deuxième trame de données pour chaque valeur de clé dans 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 que la sélection sql ne s'exécute et qui explique en partie pourquoi c'est tellement lent. L'idée de sqldf est que les trames de données dans votre session R constituent la base de données, pas les tables dans 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 était affichée lit d1 et d2 depuis l'espace de travail dans la base de données principale de sqlite encombrant celles qui étaient là avec les index. Par conséquent, il effectue une jointure sans index. Si vous souhaitez utiliser les versions de d1 et d2 qui se trouvaient dans la base de données principale de sqlite, vous devez les désigner comme main.d1 et main.d2 et non comme d1 et d2. De plus, si vous essayez de le faire fonctionner aussi rapidement que possible, notez qu'une simple jointure ne peut pas utiliser d'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 vaut la peine de remarquer 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 tandis que dans le second exemple sqldf ,greg, 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, 200 fois plus rapide que plyr et 100 fois plus rapide que data.table. Ci-dessous, nous montrons le code d'entrée, les timings de sortie pour la fusion et les timings 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 ne peut gérer (si vous utilisez l'argument dbname de sqldf) tandis que les autres approches sont limitées au traitement dans la 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 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
44
G. Grothendieck

Les 132 secondes rapportées dans les résultats de Gabor pour data.table est en fait la synchronisation des fonctions de base colMeans et cbind (l'allocation de mémoire et la copie 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 bien plyr, veuillez donc vérifier avec Hadley avant de vous fier aux horaires plyr ici. Notez également que le data.table inclut le temps de conversion en data.table et définissez la clé, pour la précision.


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 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
39
Matt Dowle

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 à 3,37).

Mes horaires:

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

J'ai pensé qu'il serait intéressant de publier une référence avec dplyr dans le mix: (il y 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 configurer les données pour dplyr avec une table de données:

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

Mise à jour: J'ai supprimé data.tableBad et plyr et rien 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 table 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 à l'aide d'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 les nouveaux data.table et dplyr :)

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)

A pris environ 10-13 Go de RAM juste pour conserver 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 bélier mais j'ai fait exploser un bélier. 32 Go ne le traiteront pas de 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, la fin de la nuit m'est arrivée.

L'utilisation de dplyr avec un bloc de données semble être le moyen le moins efficace de traiter les résumés. Ces méthodes permettent-elles de comparer la fonctionnalité exacte de data.table et 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 regrouper ou de créer le tableau de données. Cela pourrait être une question de goût, mais je pense que la partie la plus importante est l'efficacité avec laquelle les données peuvent être modélisées.

11
dotcomken

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

Jointure interne: la fusion (df1, df2) fonctionnera pour ces exemples car R joint automatiquement les trames par des noms de variables communs, mais vous voudrez probablement spécifier la fusion (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érentes trames 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)
2
Amarjeet