Jaki jest najszybszy sposób łączenia / łączenia data.frames w R?


97

Na przykład (nie jestem pewien, czy jest to najbardziej reprezentatywny przykład):

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

Oto, co mam do tej pory:

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

Właściwy sposób wykonania sqldf jest wskazany poniżej przez Gabor: stwórz tylko jeden indeks (powiedzmy na d1) i użyj d1.main zamiast d1 w instrukcji select (w przeciwnym razie nie użyje indeksu). Czas to w tym przypadku 13,6 sek. Budowanie indeksów na obu tabelach nie jest tak naprawdę konieczne w przypadku data.table, po prostu wykonaj "dt2 <- data.table (d2)", a czas będzie wynosić 3,9 sek.
— datasmurf

Obie odpowiedzi dostarczają cennych informacji, wartych przeczytania obu (choć tylko jedną można „zaakceptować”).
— datasmurf

porównujesz złączenie lewe ze
— złączem

Odpowiedzi:


46

Podejście dopasowania działa, gdy w drugiej ramce danych znajduje się unikalny klucz dla każdej wartości klucza w pierwszej. Jeśli w drugiej ramce danych znajdują się duplikaty, wówczas podejścia dopasowywania i łączenia nie są takie same. Mecz jest oczywiście szybszy, ponieważ nie robi tak dużo. W szczególności nigdy nie szuka zduplikowanych kluczy. (kontynuacja po kodzie)

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

W kodzie sqldf, który został opublikowany w pytaniu, może się wydawać, że indeksy zostały użyte w dwóch tabelach, ale w rzeczywistości są one umieszczane w tabelach, które zostały nadpisane przed uruchomieniem sql select i częściowo wyjaśnia dlaczego jest tak wolno. Idea sqldf polega na tym, że ramki danych w sesji R stanowią bazę danych, a nie tabele w sqlite. Zatem za każdym razem, gdy kod odwołuje się do niekwalifikowanej nazwy tabeli, będzie szukał jej w obszarze roboczym języka R - a nie w głównej bazie danych sqlite. W ten sposób instrukcja select, która została pokazana, odczytuje d1 i d2 z obszaru roboczego do głównej bazy danych sqlite, przebijając te, które były tam z indeksami. W rezultacie wykonuje łączenie bez indeksów. Jeśli chciałbyś skorzystać z wersji d1 i d2, które były w głównej bazie danych sqlite, musiałbyś nazywać je main.d1 i main. d2 a nie jak d1 i d2. Ponadto, jeśli starasz się, aby działał tak szybko, jak to możliwe, zwróć uwagę, że proste sprzężenie nie może korzystać z indeksów w obu tabelach, więc możesz zaoszczędzić czas na tworzeniu jednego z indeksów. W poniższym kodzie ilustrujemy te punkty.

Warto zauważyć, że precyzyjne obliczenia mogą mieć ogromny wpływ na to, który pakiet jest najszybszy. Na przykład poniżej wykonujemy scalanie i agregowanie. Widzimy, że wyniki w obu przypadkach są prawie odwrotne. W pierwszym przykładzie od najszybszego do najwolniejszego otrzymujemy: data.table, plyr, merge i sqldf, natomiast w drugim przykładzie sqldf, agregate, data.table i plyr - prawie odwrotność pierwszego. W pierwszym przykładzie sqldf jest 3x wolniejsze niż data.table, aw drugim 200x szybsze niż plyr i 100 razy szybsze niż data.table. Poniżej pokazujemy kod wejściowy, czasy wyjściowe dla scalenia i czasy wyjściowe dla agregatu. Warto również zauważyć, że sqldf jest oparty na bazie danych i dlatego może obsługiwać obiekty większe niż może obsłużyć R (jeśli użyjesz argumentu dbname funkcji sqldf), podczas gdy inne podejścia są ograniczone do przetwarzania w pamięci głównej. Zilustrowaliśmy także sqldf z sqlite, ale obsługuje on również bazy danych H2 i 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"))
)

Dane wyjściowe z dwóch wywołań porównawczych porównujących obliczenia scalania są następujące:

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

Wyniki wywołania porównawczego porównującego zagregowane obliczenia są następujące:

        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

Dziękuję, Gabor. Doskonałe uwagi, wprowadziłem pewne poprawki poprzez komentarze do pierwotnego pytania. Właściwie wydaje mi się, że kolejność może się zmienić nawet w przypadku „scalania” w zależności od względnych rozmiarów tabel, liczby kluczy itp. (Dlatego powiedziałem, że nie jestem pewien, czy mój przykład jest reprezentatywny). Niemniej miło jest zobaczyć różne rozwiązania tego problemu.
— datasmurf

Doceniam również komentarz dotyczący przypadku „agregacji”. Chociaż różni się to od ustawienia „scalania” w pytaniu, jest bardzo istotne. Właściwie zapytałbym o to w osobnym pytaniu, ale jest już jedno stackoverflow.com/questions/3685492/… . Możesz też chcieć przyczynić się do tego, ponieważ w oparciu o powyższe wyniki rozwiązanie sqldf może pokonać wszystkie istniejące tam odpowiedzi;)
— datasmurf

40

132 sekundy podane w wynikach Gabora data.tableto w rzeczywistości funkcje bazowe czasu colMeansi cbind(alokacja pamięci i kopiowanie wywołane przez użycie tych funkcji). Są też dobre i złe sposoby używania 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)

Zwróć uwagę, że nie znam dobrze Plyr, więc skonsultuj się z Hadley, zanim zaczniesz polegać na czasach plyrtutaj. Należy również pamiętać, że data.tableobejmują one czas na konwersję data.tablei ustawienie klucza, aby uzyskać cenę.


Ta odpowiedź została zaktualizowana, ponieważ pierwotnie udzielono odpowiedzi w grudniu 2010 r. Poprzednie wyniki testu porównawczego znajdują się poniżej. Zobacz historię wersji tej odpowiedzi, aby zobaczyć, co się zmieniło.

              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

Ponieważ ddply działa tylko z ramkami danych, ten przykład daje najgorszą wydajność. Mam nadzieję, że w przyszłej wersji będę miał lepszy interfejs do tego typu wspólnych operacji.
— Hadley

1
FYI: nie można używać .Internalpołączeń w pakietach CRAN, zobacz zasady dotyczące repozytoriów CRAN .
— Joshua Ulrich

@JoshuaUlrich Można by było, gdy odpowiedź została napisana prawie 2 lata temu, iirc. Zaktualizuję tę odpowiedź, ponieważ data.tableautomatycznie optymalizuje się meanteraz (bez wywoływania .Internalwewnętrznego).
— Matt Dowle

@MatthewDowle: Tak, nie jestem pewien, kiedy / czy to się zmieniło. Po prostu wiem, że teraz tak jest. Twoja odpowiedź jest w porządku, po prostu nie będzie działać w pakietach.
— Joshua Ulrich

1
@AleksandrBlekh Thanks. Połączyłem tutaj Twoje komentarze z istniejącym zgłoszeniem funkcji nr 599 . Przejdźmy tam. Twój przykładowy kod ładnie pokazuje forpętlę, to dobrze. Czy mógłbyś dodać więcej informacji o „analizie SEM” do tego problemu? Na przykład zgaduję, że SEM = skaningowy mikroskop elektronowy? Wiedza o aplikacji sprawia, że ​​jest ona dla nas bardziej interesująca i pomaga ustalić priorytety.
— Matt Dowle

16

Do prostego zadania (unikalne wartości po obu stronach połączenia) używam match:

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

To znacznie szybsze niż scalanie (na moim komputerze od 0,13 s do 3,37 s).

Moje czasy:

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

4
Dziękuję Marek. Pewne wyjaśnienie, dlaczego to jest tak szybkie (buduje indeks / tablicę skrótów) można znaleźć tutaj: tolstoy.newcastle.edu.au/R/help/01c/2739.html
— datasmurf

11

Pomyślałem, że byłoby interesujące opublikowanie testu porównawczego z dplyrem w miksie: (było wiele rzeczy uruchomionych)

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

Właśnie dodane:

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

i skonfiguruj dane dla dplyr za pomocą tabeli danych:

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

Aktualizacja: usunąłem data.tableBad i plyr i nic poza RStudio otwarte (i7, 16GB RAM).

Z tabelą danych 1.9 i dplyrem z ramką danych:

            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

Z data.table 1.9 i dplyr z tabelą danych:

            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'

Aby zachować spójność, oto oryginał z all i data.table 1.9 i dplyr przy użyciu tabeli danych:

            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

Myślę, że te dane są za małe dla nowych data.table i dplyr :)

Większy zbiór danych:

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)

Zajęło około 10-13 GB pamięci RAM, aby przechowywać dane przed uruchomieniem testu porównawczego.

Wyniki:

            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

Wypróbowałem 1 miliard, ale wysadziłem barana. 32 GB poradzi sobie z tym bez problemu.


[Edit by Arun] (dotcomken, czy mógłbyś uruchomić ten kod i wkleić wyniki testów porównawczych? Dzięki).

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

Zgodnie z prośbą Aruna, dane wyjściowe tego, co dostarczyłeś mi do uruchomienia:

        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

Przepraszam za zamieszanie, dopadła mnie późna noc.

Używanie dplyr z ramką danych wydaje się być mniej wydajnym sposobem przetwarzania podsumowań. Czy uwzględniono te metody do porównywania dokładnej funkcjonalności data.table i dplyr z ich metodami struktury danych? Prawie wolałbym to oddzielić, ponieważ większość danych będzie musiała zostać wyczyszczona przed group_by lub utworzeniem data.table. To może być kwestia gustu, ale myślę, że najważniejszą częścią jest to, jak skutecznie można modelować dane.


1
Niezła aktualizacja. Dzięki. Myślę, że twoja maszyna jest bestią w porównaniu z tym zestawem danych. Jaki jest rozmiar twojej pamięci podręcznej L2 (i L3, jeśli istnieje)?
— Arun

i7 L2 to 2x256 KB 8-drożny, L3 to 4 MB 16-drożny. 128 GB SSD, Win 7 na Dell Inspiron
— dotcomken

1
Czy mógłbyś przeformatować swój przykład. Jestem nieco zdezorientowany. Czy data.table jest lepsza (w tym przykładzie) niż dplyr? Jeśli tak, w jakich okolicznościach.
— csgillespie

1

Używając funkcji scalania i jej opcjonalnych parametrów:

Sprzężenie wewnętrzne: merge (df1, df2) zadziała w tych przykładach, ponieważ R automatycznie łączy ramki według wspólnych nazw zmiennych, ale najprawdopodobniej chcesz określić scalanie (df1, df2, by = "CustomerId"), aby upewnić się, że pasowały tylko do żądanych pól. Można również użyć parametrów by.x i by.y, jeśli pasujące zmienne mają różne nazwy w różnych ramkach danych.

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)

Pytanie dotyczyło wydajności. Podałeś tylko składnię dla złączeń. Chociaż jest pomocny, nie odpowiada na pytanie. W tej odpowiedzi brakuje danych porównawczych wykorzystujących przykłady PO, aby wykazać, że radzi sobie lepiej lub przynajmniej jest wysoce konkurencyjny.
— Michael Tuchman
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.