Qual é a maneira mais rápida de mesclar / juntar data.frames em R?


97

Por exemplo (não tenho certeza se é o exemplo mais representativo):

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

Isso é o que eu tenho até agora:

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

A maneira adequada de fazer o método sqldf é apontada abaixo por Gabor: crie apenas um índice (digamos em d1) e use d1.main em vez de d1 na instrução select (caso contrário, não usará o índice). O tempo é, neste caso, 13,6 seg. A construção de índices em ambas as tabelas também não é necessária no caso data.table, basta fazer "dt2 <- data.table (d2)" e o tempo será de 3,9 seg.
datasmurf

Ambas as respostas fornecem informações valiosas, vale a pena ler ambas (embora apenas uma possa ser "aceita").
datasmurf

você está comparando a junção esquerda com a junção interna em sua pergunta
jangorecki

Respostas:


46

A abordagem de correspondência funciona quando há uma chave exclusiva no segundo quadro de dados para cada valor de chave no primeiro. Se houver duplicatas no segundo quadro de dados, as abordagens de correspondência e mesclagem não são as mesmas. O Match é, obviamente, mais rápido, pois não está fazendo tanto. Em particular, nunca procura chaves duplicadas. (continua após o código)

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

No código sqldf postado na pergunta, pode parecer que os índices foram usados ​​nas duas tabelas, mas, na verdade, eles são colocados em tabelas que foram substituídas antes que o sql select seja executado e isso, em parte, explica por que é tão lento. A ideia do sqldf é que os quadros de dados em sua sessão R constituem o banco de dados, não as tabelas no sqlite. Assim, cada vez que o código se refere a um nome de tabela não qualificado, ele o procurará em seu espaço de trabalho R - não no banco de dados principal do sqlite. Portanto, a instrução select que foi mostrada lê d1 e d2 do espaço de trabalho para o banco de dados principal do sqlite, destruindo aqueles que estavam lá com os índices. Como resultado, ele faz uma junção sem índices. Se você quiser usar as versões de d1 e d2 que estão no banco de dados principal do sqlite, você deve se referir a elas como main.d1 e main. d2 e não como d1 e d2. Além disso, se você está tentando fazê-lo funcionar o mais rápido possível, observe que uma junção simples não pode usar índices em ambas as tabelas, portanto, você pode economizar tempo de criação de um dos índices. No código a seguir, ilustramos esses pontos.

Vale a pena notar que o cálculo preciso pode fazer uma grande diferença em qual pacote é mais rápido. Por exemplo, fazemos uma fusão e uma agregação abaixo. Vemos que os resultados são quase revertidos para os dois. No primeiro exemplo, do mais rápido para o mais lento, obtemos: data.table, plyr, merge e sqldf, enquanto no segundo exemplo sqldf, aggregate, data.table e plyr - quase o inverso do primeiro. No primeiro exemplo, sqldf é 3x mais lento que data.table e no segundo é 200x mais rápido que plyr e 100 vezes mais rápido que data.table. Abaixo, mostramos o código de entrada, os tempos de saída para a fusão e os tempos de saída para o agregado. Também vale a pena notar que sqldf é baseado em um banco de dados e, portanto, pode manipular objetos maiores do que R pode manipular (se você usar o argumento dbname de sqldf) enquanto as outras abordagens são limitadas ao processamento na memória principal. Além disso, ilustramos o sqldf com o sqlite, mas ele também suporta os bancos de dados 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"))
)

Os resultados das duas chamadas de benchmark comparando os cálculos de fusão são:

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

A saída da chamada de benchmark comparando os cálculos agregados são:

        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

Obrigado, Gabor. Excelentes pontos, fiz alguns ajustes via comentários à pergunta original. Na verdade, acho que a ordem pode mudar mesmo no caso de "mesclagem", dependendo dos tamanhos relativos das tabelas, multiplicidade de chaves etc. (é por isso que eu disse que não tenho certeza se meu exemplo é representativo). Mesmo assim, é bom ver todas as diferentes soluções para o problema.
datasmurf 01 de

Agradeço também o comentário sobre o caso "agregação". Embora seja diferente da configuração de "mesclagem" na questão, é muito relevante. Eu teria perguntado sobre isso em uma pergunta separada, mas já existe uma aqui stackoverflow.com/questions/3685492/… . Talvez você queira contribuir com isso também, pois com base nos resultados acima, a solução sqldf pode superar todas as respostas existentes lá;)
datasmurf 01 de

40

Os 132 segundos relatados nos resultados de Gabor para data.tablesão, na verdade, funções de base de tempo colMeanse cbind(a alocação de memória e cópia induzida pelo uso dessas funções). Existem maneiras boas e ruins de usar data.tabletambém.

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)

Observe que não conheço plyr bem, então verifique com Hadley antes de contar com os plyrhorários aqui. Observe também que data.tableincluem o tempo para converter data.tablee definir a chave, para fareness.


Esta resposta foi atualizada desde que respondida originalmente em dezembro de 2010. Os resultados do benchmark anterior estão abaixo. Por favor, veja o histórico de revisão desta resposta para ver o que mudou.

              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

Como o ddply funciona apenas com quadros de dados, este é o exemplo que produz o pior caso de desempenho. Espero ter uma interface melhor para este tipo de operação comum em uma versão futura.
hadley

1
FYI: você não pode usar .Internalchamadas em pacotes CRAN, consulte a Política de Repositório CRAN .
Joshua Ulrich

@JoshuaUlrich Você sabia quando a resposta foi escrita há quase 2 anos, iirc. Vou atualizar esta resposta como data.tableotimiza automaticamente meanagora (sem chamar .Internalinternamente).
Matt Dowle

@MatthewDowle: Sim, não tenho certeza de quando / se mudou. Eu só sei que é o caso agora. E está perfeitamente bem em sua resposta, só não funciona em pacotes.
Joshua Ulrich

1
@AleksandrBlekh Obrigado. Vinculei seus comentários aqui à solicitação de recurso existente # 599 . Vamos para lá. Seu código de exemplo mostra bem o forloop, isso é bom. Você poderia adicionar mais informações sobre "análise SEM" a esse problema? Por exemplo, estou supondo que SEM = microscópio eletrônico de varredura? Saber mais sobre o aplicativo torna-o mais interessante para nós e nos ajuda a priorizar.
Matt Dowle de

16

Para tarefas simples (valores únicos em ambos os lados da junção), eu uso match:

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

É muito mais rápido do que mesclar (na minha máquina, 0.13s a 3.37s).

Meus horários:

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

4
Obrigado, Marek. Alguma explicação de por que isso é tão rápido (cria uma tabela de índice / hash) pode ser encontrada aqui: tolstoy.newcastle.edu.au/R/help/01c/2739.html
datasmurf

11

Achei que seria interessante postar um benchmark com dplyr no mix: (tinha um monte de coisas funcionando)

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

Acabei de adicionar:

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

e configure os dados para dplyr com uma tabela de dados:

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

Atualizado: removi data.tableBad e plyr e nada além de RStudio aberto (i7, 16 GB de RAM).

Com data.table 1.9 e dplyr com 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

Com data.table 1.9 e dplyr com tabela de dados:

            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'

Para consistência, aqui está o original com todos e data.table 1.9 e dplyr usando uma tabela de dados:

            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

Acho que esses dados são muito pequenos para os novos data.table e dplyr :)

Conjunto de dados maior:

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)

Demorou cerca de 10-13 GB de RAM apenas para manter os dados antes de executar o benchmark.

Resultados:

            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

Tentei um bilhão, mas explodiu o aríete. 32 GB resolverá isso sem problemas.


[Edit by Arun] (dotcomken, você poderia executar este código e colar seus resultados de benchmarking? Obrigado).

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

De acordo com a solicitação de Arun aqui, a saída do que você me forneceu para executar:

        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

Desculpe pela confusão, tarde da noite me pegou.

Usar dplyr com data frame parece ser a maneira menos eficiente de processar resumos. Esses métodos são para comparar a funcionalidade exata de data.table e dplyr com seus métodos de estrutura de dados incluídos? Eu quase preferiria separar isso, pois a maioria dos dados precisará ser limpa antes de group_by ou criar a data.table. Pode ser uma questão de gosto, mas acho que a parte mais importante é a eficiência com que os dados podem ser modelados.


1
Boa atualização. Obrigado. Eu acho que sua máquina é uma besta comparada a este conjunto de dados .. Qual é o tamanho do seu cache L2 (e L3 se existir)?
Arun

i7 L2 tem 2x256 KB de 8 vias, L3 tem 4 MB de 16 vias. SSD de 128 GB, Win 7 em um Dell Inspiron
dotcomken

1
Você poderia reformatar seu exemplo. Estou um pouco confuso. Data.table é melhor (neste exemplo) do que dplyr? Se assim for, em que circunstâncias.
csgillespie

1

Usando a função merge e seus parâmetros opcionais:

Inner join: merge (df1, df2) funcionará para esses exemplos porque R une automaticamente os quadros por nomes de variáveis ​​comuns, mas você provavelmente deseja especificar merge (df1, df2, by = "CustomerId") para ter certeza de que estavam combinando apenas nos campos desejados. Você também pode usar os parâmetros by.x e by.y se as variáveis ​​correspondentes tiverem nomes diferentes nos diferentes quadros de dados.

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)

A questão era sobre desempenho. Você apenas forneceu a sintaxe para as junções. Embora útil, não responde à pergunta. Essa resposta carece de dados de benchmark usando os exemplos do OP para mostrar que ele tem um desempenho melhor, ou pelo menos altamente competitivo.
Michael Tuchman
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.