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
datasmurf
źródło
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
G. Grothendieck
źródło
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
Matt Dowle
źródło
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
Marek
źródło
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.

dotcomken
źródło
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)
Amarjeet
źródło
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