Який найшвидший спосіб об’єднати / об’єднати data.frames у R?


97

Наприклад (не впевнений, що найбільш репрезентативний приклад):

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

Це те, що я маю на даний момент:

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

Правильний спосіб зробити спосіб sqldf вказаний нижче Габором: створіть лише один індекс (скажімо на d1) і використовуйте d1.main замість d1 у операторі select (інакше він не використовуватиме індекс). Час у цьому випадку становить 13,6 сек. Побудова індексів для обох таблиць насправді також не потрібна у випадку data.table, просто виконайте "dt2 <- data.table (d2)", і час буде 3,9 сек.
datasmurf

Обидва відповіді дають цінну інформацію, яку варто прочитати обидві (хоча лише одну можна прийняти).
datasmurf

Ви порівнюєте ліве приєднання до внутрішнього приєднання у своєму питанні
jangorecki

Відповіді:


46

Підхід збігів працює, коли у другому кадрі даних є унікальний ключ для кожного значення ключа в першому. Якщо у другому кадрі даних є дублікати, то підходи збігу та злиття неоднакові. Звичайно, матч швидший, оскільки він робить не так багато. Зокрема, він ніколи не шукає дублікатів ключів. (продовження після коду)

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

У коді sqldf, який був опублікований у питанні, може здатися, що індекси використовувались у двох таблицях, але насправді вони розміщені в таблицях, які були перезаписані до того, як sql select коли-небудь запуститься, і це, частково, пояснює, чому це так повільно. Ідея sqldf полягає в тому, що кадри даних у вашій сесії R складають базу даних, а не таблиці в sqlite. Таким чином, кожного разу, коли код посилається на некваліфіковане ім'я таблиці, він буде шукати його у вашій робочій області R, а не в головній базі даних sqlite. Таким чином, показаний оператор select читає d1 і d2 з робочої області в основну базу даних sqlite, обробляючи ті, що були там, за допомогою індексів. В результаті він робить об'єднання без індексів. Якщо ви хочете скористатися версіями d1 і d2, які були в основній базі даних sqlite, вам доведеться називати їх як main.d1 та main. d2, а не як d1 і d2. Крім того, якщо ви намагаєтеся зробити його якнайшвидшим, зауважте, що просте об’єднання не може використовувати індекси в обох таблицях, щоб ви могли заощадити час створення одного з індексів. У наведеному нижче коді ми ілюструємо ці моменти.

Варто зауважити, що точне обчислення може суттєво змінити, який пакет є найшвидшим. Наприклад, ми робимо злиття та сукупність нижче. Ми бачимо, що результати для цих двох майже зворотні. У першому прикладі від найшвидшого до найповільнішого ми отримуємо: data.table, plyr, merge та sqldf, тоді як у другому прикладі sqldf, agregate, data.table та plyr - майже зворотне значення першого. У першому прикладі sqldf в 3 рази повільніший за data.table, а у другому - у 200 разів швидший, ніж plyr, і в 100 разів швидший, ніж data.table. Нижче ми показуємо вхідний код, вихідні таймінги для злиття та вихідні таймінги для сукупності. Також варто зазначити, що sqldf базується на базі даних і, отже, може обробляти об'єкти, більші за R (якщо ви використовуєте аргумент dbname sqldf), тоді як інші підходи обмежені обробкою в основній пам'яті. Також ми проілюстрували sqldf за допомогою sqlite, але він також підтримує бази даних H2 та 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"))
)

Результати двох викликів порівняльної оцінки для порівняння обчислень злиття:

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

Результатом виклику тесту для порівняння сукупних розрахунків є:

        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

Дякую, Габоре. Чудові моменти, я вніс деякі корективи за допомогою коментарів до початкового питання. Насправді я припускаю, що порядок може змінитися навіть у випадку злиття залежно від відносних розмірів таблиць, множинності ключів тощо (саме тому я сказав, що не впевнений, що мій приклад репрезентативний). Тим не менше, приємно бачити всі різні шляхи вирішення проблеми.
datasmurf

Я також ціную коментар щодо справи "агрегації". Хоча це і відрізняється від налаштування "злиття" у питанні, воно є дуже актуальним. Я б справді запитав про це в окремому питанні, але тут уже є один stackoverflow.com/questions/3685492/… . Можливо, ви захочете зробити свій внесок і в цей, оскільки на підставі наведених вище результатів рішення sqldf може перемогти всі існуючі там відповіді;)
datasmurf

40

132 секунди, про які повідомляється в результатах роботи Габора, data.tableнасправді є базовими функціями хронометражу colMeansта cbind(виділення та копіювання пам'яті, викликані використанням цих функцій). Існують також хороші і погані способи використання 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)

Зверніть увагу, що я погано знаю Plyr, тому, будь ласка, проконсультуйтеся з Хадлі, перш ніж покладатися на plyrтерміни тут. Також зауважте, що у вартість data.tableвходить час перетворення data.tableта встановлення ключа для тарифу.


Ця відповідь була оновлена ​​з моменту первинної відповіді у грудні 2010 р. Попередні результати порівняння наведені нижче. Будь ласка, перегляньте історію редагувань цієї відповіді, щоб побачити, що змінилося.

              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

Оскільки ddply працює лише з фреймами даних, цей приклад дає найгіршу продуктивність. Я сподіваюся, що в майбутній версії буде покращений інтерфейс для цього типу загальних операцій.
Хедлі

1
FYI: ви не можете використовувати .Internalдзвінки в пакетах CRAN, див. Політику сховищ CRAN .
Джошуа Ульріх,

@JoshuaUlrich Ви могли б, коли відповідь була написана майже 2 роки тому, iirc. Я оновлю цю відповідь, оскільки зараз data.tableавтоматично оптимізується mean(без .Internalвнутрішнього дзвінка ).
Matt Dowle,

@MatthewDowle: Так, я не впевнений, коли / якщо це змінилося. Я просто знаю, що це зараз так. І це чудово у вашій відповіді, просто не буде працювати в пакетах.
Джошуа Ульріх,

1
@AleksandrBlekh Дякую. Ваші коментарі я зв’язав із існуючим запитом на функцію № 599 . Переїдемо туди. Ваш прикладний код добре показує forцикл, це добре. Не могли б ви додати більше інформації про "SEM-аналіз" до цього випуску? Наприклад, я здогадуюсь, що SEM = скануючий електронний мікроскоп? Більше знання про програму робить нас цікавішими та допомагає визначити пріоритети.
Метт Доул,

16

Для простого завдання (унікальні значення з обох боків об'єднання) я використовую match:

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

Це набагато швидше, ніж злиття (на моїй машині від 0,13 до 3,37 с).

Мої терміни:

  • merge: 3.32с
  • plyr: 0,84 с
  • match: 0,12 с

4
Дякую, Марек. Деяке пояснення, чому це так швидко (будує індекс / хеш-таблицю), можна знайти тут: tolstoy.newcastle.edu.au/R/help/01c/2739.html
datasmurf

11

Думав, було б цікаво опублікувати контрольний показник з dplyr у суміші: (було багато речей, що працюють)

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

Щойно додано:

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

та налаштуйте дані для dplyr за допомогою таблиці даних:

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

Оновлено: я видалив data.tableBad і plyr і нічого, крім RStudio open (i7, 16 Гб оперативної пам'яті).

З data.table 1.9 і dplyr з фреймом даних:

            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

З data.table 1.9 і dplyr з таблицею даних:

            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'

Для узгодженості ось оригінал із усіма та data.table 1.9 та dplyr за допомогою таблиці даних:

            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

Я думаю, що ці дані замалі для нових data.table та dplyr :)

Більший набір даних:

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)

Взяв близько 10-13 ГБ оперативної пам'яті, лише щоб утримати дані перед запуском тесту.

Результати:

            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

Спробував 1 мільярд, але підірвав таран. 32 Гб це впорається без проблем.


[Редагувати Арун] (dotcomken, не могли б ви запустити цей код та вставити результати порівняльного тестування? Дякую).

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

Відповідно до запиту Аруна, висновок того, що ви надали мені для запуску:

        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

Вибачте за плутанину, пізня ніч дійшла до мене.

Використання dplyr з фреймом даних представляється менш ефективним способом обробки резюме. Це методи для порівняння точної функціональності data.table та dplyr із їх методами структури даних? Я б майже віддав перевагу відокремити це, оскільки більшість даних потрібно буде очистити перед тим, як згрупувати за допомогою або створити data.table. Це може бути питанням смаку, але я думаю, що найважливішим є те, наскільки ефективно можна моделювати дані.


1
Приємне оновлення. Дякую. Я думаю, що ваша машина - звір порівняно з цим набором даних .. Який розмір вашого кешу L2 (і L3, якщо він існує)?
Арун

i7 L2 - 2x256 КБ 8-смуговий, L3 - 4 МБ 16-смуговий. 128 Гб SSD, виграй 7 на Dell Inspiron
dotcomken

1
Не могли б ви переформатувати свій приклад. Я трохи розгублений. Чи є data.table кращим (у цьому прикладі), ніж dplyr? Якщо так, то за яких обставин.
csgillespie

1

За допомогою функції злиття та її необов’язкових параметрів:

Внутрішнє приєднання: злиття (df1, df2) працюватиме для цих прикладів, оскільки R автоматично приєднує кадри за загальними іменами змінних, але ви, швидше за все, хочете вказати злиття (df1, df2, by = "CustomerId"), щоб переконатися, що ви відповідали лише полям, які ви бажали. Ви також можете використовувати параметри by.x та by.y, якщо відповідні змінні мають різні імена в різних кадрах даних.

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)

Питання було про продуктивність. Ви просто надали синтаксис об’єднань. Хоча це корисно, воно не відповідає на запитання. У цій відповіді бракує еталонних даних на прикладах ОП, щоб показати, що вона працює ефективніше, або, принаймні, дуже конкурентноздатна.
Майкл Тухман,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.