Агрегирование значений во фрейме данных на основе ключа

У меня есть фрагмент кода агрегации, который работает достаточно хорошо, но работает немного медленнее для фрейма данных с 10e6 строками. Я не настолько опытен в R, поэтому извиняюсь за мой код, достойный съеживания!

Я просто хочу сделать базовую сводку и сумму значений для общего ключа...

например перейти от...

  key val
1   a   5
2   b   7
3   a   6

to...

  key val
1   a   11
2   b   7

лучшее, что я могу сделать, это...

keys = unique(inp$key)
vals = sapply(keys, function(x) { sum(inp[inp$key==x,]$val) })
out = data.frame(key=keys, val=vals)

Я нутром чувствую, что inp[inp$key==x,] не лучший способ. Есть ли очевидное ускорение, которое мне не хватает? Я могу сделать это в Hadoop (поскольку набор данных 10e6 на самом деле уже представляет собой набор данных из набора строк 2e9), но я пытаюсь улучшить свой R.

Привет, Мэт


person mat kelcey    schedule 25.07.2011    source источник


Ответы (4)


Другой вариант с использованием tapply:

dat <- data.frame(key = c('a', 'b', 'a'), val = c(5,7,6))

> with(dat, tapply(val, key, FUN = sum))
 a  b 
11  7

Мои тесты показывают, что это самый быстрый метод для этого конкретного упражнения, очевидно, что ваши мили могут отличаться:

fn.tapply <- function(daters) with(daters, tapply(val, key, FUN = sum))
fn.aggregate <- function(daters) aggregate(val~key, sum, data = daters)
fn.ddply <- function(daters) ddply(daters, .(key), summarize, val = sum(val))


library(rbenchmark)

benchmark(fn.tapply(dat), fn.aggregate(dat), fn.ddply(dat)
          , columns = c("test", "elapsed", "relative")
          , order = "relative"
          , replications = 100
          )


               test elapsed  relative
1    fn.tapply(dat)    0.03  1.000000
2 fn.aggregate(dat)    0.20  6.666667
3     fn.ddply(dat)    0.30 10.000000

Обратите внимание, что преобразование решения tapply в data.frame сократило эту разницу вдвое примерно на 40% для истинного сравнения яблок с яблоками по сравнению с первыми двумя.

Использование набора данных 1M строк, как указано в комментариях, немного меняет ситуацию:

 dat2 <- data.frame(key = rep(letters[1:5], each = 200000), val = runif(1e6))
> benchmark(fn.tapply(dat2), fn.aggregate(dat2), fn.ddply(dat2)
+           , columns = c("test", "elapsed", "relative")
+           , order = "relative"
+           , replications = 100
+           )
               test elapsed relative
1   fn.tapply(dat2)  39.114 1.000000
3     fn.ddply(dat2)  62.178 1.589661
2 fn.aggregate(dat2) 157.463 4.025745
person Chase    schedule 25.07.2011
comment
Если вы попытаетесь использовать более крупный фрейм данных (например, с 1 миллионом строк), тесты окажутся удивительно разными: tapply по-прежнему самый быстрый, но ddply всего на 25% медленнее, а aggregate займет примерно в 5 раз больше времени. - person grautur; 25.07.2011
comment
@grautur - Действительно интересно. Спасибо что подметил это. Я добавил еще один тест, который в целом подтверждает вашу гипотезу. - person Chase; 25.07.2011
comment
Вот это да! спасибо за все подробности. я дам вам знать, как это идет против моего большего набора данных. - person mat kelcey; 25.07.2011
comment
@Chase: вы можете немного ускорить свою агрегатную функцию, избегая метода формулы. - person Joshua Ulrich; 26.07.2011

вы можете использовать aggregate:

> d
  key val
1   a   5
2   b   7
3   a   6
> aggregate(val~key, sum, data=d)
  key val
1   a  11
2   b   7

вы также можете использовать ddply из пакета plyr от Hadley:

> ddply(d, .(key), summarize, val=sum(val))
  key val
1   a  11
2   b   7
person kohske    schedule 25.07.2011
comment
Я провел быстрое тестирование всех трех методов, и (несколько неожиданно) самым быстрым оказался sapply мата, за ним ddply, а затем aggregate. (Усреднение пары прогонов по фрейму данных с 300 тыс. строк, sapply заняло около 0,15 с, ddply заняло около 0,2 с, а aggregate заняло около 0,9 с.) Однако не знаю, получится ли по-другому с другим распределением ключей. - person grautur; 25.07.2011
comment
Как бы вы это сделали, если бы у вас было несколько столбцов для группировки и суммирования? Используя первый вариант, второй вариант работает как шарм, но его решение занимает слишком много времени.... - person Zombraz; 08.05.2019

Использование sapply и split — еще один вариант. Я расскажу о данных и тестах из отличного ответа @Chase.

fn.tapply <- function(daters) with(daters, tapply(val, key, FUN = sum))
fn.split <- function(daters) with(daters, sapply(split(val, key), sum))

str(dat)
# 'data.frame': 1000000 obs. of  2 variables:
#  $ key: Factor w/ 5 levels "a","b","c","d",..: 1 1 1 1 1 1 1 1 1 1 ...
#  $ val: num  0.186 0.875 0.42 0.294 0.878 ...

benchmark(fn.tapply(dat), fn.split(dat)
          , columns = c("test", "elapsed", "relative")
          , order = "relative"
          , replications = 100
          )
#             test elapsed relative
# 2  fn.split(dat)   4.106  1.00000
# 1 fn.tapply(dat)  69.982 17.04384
person Joshua Ulrich    schedule 25.07.2011
comment
спасибо за дополнительную информацию, я обязательно включу split в свои тесты - person mat kelcey; 25.07.2011

Технически вы сказали «фрейм данных», но другой гораздо более быстрый вариант (более чем в 22 раза быстрее) — использовать пакет data.table.

Вот эталон для 10 повторений по 10-6 рядов.

library(rbenchmark)
library(plyr)
library(data.table)

key <- sample(letters,10e6, replace = T)
val <- sample.int(10,10e6, replace = T)
big_df <- data.frame(key,val)
rm(key)
rm(val)
big_dtable <- data.table(big_df)
setkey(big_dtable,key)

fn.data_table <- function(mydata) mydata[,list(sum = sum(val)), by = 'key']
fn.tapply <- function(mydata) with(mydata, tapply(val, key, FUN = sum))
fn.aggregate <- function(mydata) aggregate(val~key, sum, data = mydata)
fn.ddply <- function(mydata) ddply(mydata, .(key), summarize, val = sum(val))

А теперь эталон....

benchmark(fn.data_table(big_dtable)
          , fn.tapply(big_df)
          , fn.aggregate(big_df)
          , fn.ddply(big_df)
          , fn.ddply(big_dtable)
          , columns = c("test","elapsed","relative")
          , order = "relative"
          , replications = 10         
          )

А результат....

                       test elapsed  relative
1 fn.data_table(big_dtable)    1.98   1.00000
5      fn.ddply(big_dtable)   37.59  18.98485
4          fn.ddply(big_df)   44.36  22.40404
2         fn.tapply(big_df)   51.03  25.77273
3      fn.aggregate(big_df)  238.52 120.46465
person Tommy O'Dell    schedule 27.11.2011