Какой самый быстрый способ объединить / объединить 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 01

Оба ответа содержат ценную информацию, стоит прочитать оба (хотя только один может быть «принят»).
datasmurf 03

в своем вопросе вы сравниваете левое соединение с внутренним соединением
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, aggregate, 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 01

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

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
К вашему сведению: вы не можете использовать .Internalвызовы в пакетах CRAN, см. Политику репозитория CRAN .
Джошуа Ульрих,

@JoshuaUlrich Вы могли, когда ответ был написан почти 2 года назад, iirc. Я обновлю этот ответ, поскольку теперь он data.tableавтоматически оптимизируется mean(без .Internalвнутреннего вызова ).
Мэтт Доул

@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 01

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, не было открыто (i7, 16 ГБ оперативной памяти).

С таблицей данных 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

С таблицей данных 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 ГБ справятся без проблем.


[Edit by Arun] (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 с включенными в них методами структуры данных? Я почти предпочел бы разделить это, так как большинство данных нужно будет очистить, прежде чем мы group_by или создадим data.table. Это может быть дело вкуса, но я думаю, что самая важная часть - насколько эффективно можно моделировать данные.


1
Хорошее обновление. Спасибо. Я думаю, что ваша машина - зверь по сравнению с этим набором данных .. Каков размер вашего кэша L2 (и L3, если он существует)?
Арун

i7 L2 - 2x256 КБ 8-канальный, L3 - 4 МБ 16-канальный. SSD 128 ГБ, Win 7 на Dell inspiron
dotcomken

1
Не могли бы вы переформатировать свой пример. Я немного запутался. Data.table лучше (в этом примере), чем dplyr? Если да, то при каких обстоятельствах.
csgillespie 08

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)

Вопрос был в производительности. Вы просто предоставили синтаксис для объединений. Хотя это полезно, это не отвечает на вопрос. В этом ответе отсутствуют тестовые данные с использованием примеров OP, чтобы показать, что он работает лучше или, по крайней мере, конкурентоспособно.
Майкл Такман,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.