R: групповое ускорение по операциям

У меня есть симуляция, которая имеет огромный агрегат и комбинирует шаг прямо посередине. Я прототипировал этот процесс, используя функцию plyr ddply (), которая отлично подходит для огромного процента моих потребностей. Но мне нужно, чтобы этот этап агрегации был быстрее, так как мне нужно запускать моделирование 10K. Я уже параллельно масштабирую симуляции, но если бы этот шаг был быстрее, я мог бы значительно уменьшить количество необходимых мне узлов.

Вот разумное упрощение того, что я пытаюсь сделать:

library(Hmisc)

# Set up some example data
year <-    sample(1970:2008, 1e6, rep=T)
state <-   sample(1:50, 1e6, rep=T)
group1 <-  sample(1:6, 1e6, rep=T)
group2 <-  sample(1:3, 1e6, rep=T)
myFact <-  rnorm(100, 15, 1e6)
weights <- rnorm(1e6)
myDF <- data.frame(year, state, group1, group2, myFact, weights)

# this is the step I want to make faster
system.time(aggregateDF <- ddply(myDF, c("year", "state", "group1", "group2"),
                     function(df) wtd.mean(df$myFact, weights=df$weights)
                                 )
           )

Мы ценим все советы и предложения!


person JD Long    schedule 10.09.2010    source источник
comment
Не связано с производительностью, но оформление заказа weighted.mean в базе   -  person hadley    schedule 10.09.2010
comment
О, это удобно. Вы можете видеть, что я изучил R, поискав в Google то, что мне нужно сделать;)   -  person JD Long    schedule 10.09.2010


Ответы (5)


Вместо обычного фрейма данных R вы можете использовать неизменяемый фрейм данных, который возвращает указатели на оригинал при подмножестве и может быть намного быстрее:

idf <- idata.frame(myDF)
system.time(aggregateDF <- ddply(idf, c("year", "state", "group1", "group2"),
   function(df) wtd.mean(df$myFact, weights=df$weights)))

#    user  system elapsed 
# 18.032   0.416  19.250 

Если бы мне пришлось написать функцию plyr, адаптированную именно к этой ситуации, я бы сделал что-то вроде этого:

system.time({
  ids <- id(myDF[c("year", "state", "group1", "group2")], drop = TRUE)
  data <- as.matrix(myDF[c("myFact", "weights")])
  indices <- plyr:::split_indices(seq_len(nrow(data)), ids, n = attr(ids, "n"))

  fun <- function(rows) {
    weighted.mean(data[rows, 1], data[rows, 2])
  }
  values <- vapply(indices, fun, numeric(1))

  labels <- myDF[match(seq_len(attr(ids, "n")), ids), 
    c("year", "state", "group1", "group2")]
  aggregateDF <- cbind(labels, values)
})

# user  system elapsed 
# 2.04    0.29    2.33 

Это намного быстрее, потому что он избегает копирования данных, извлекая только подмножество, необходимое для каждого вычисления при его вычислении. Переключение данных в матричную форму дает еще один прирост скорости, потому что поднабор матрицы происходит намного быстрее, чем поднабор фрейма данных.

person hadley    schedule 10.09.2010
comment
idata.frame был добавлен в plyr 1.0. - person hadley; 10.09.2010
comment
Я возился с созданием индексов и тому подобным с помощью data.table и почти отказался от этой идеи. Я надеялся на улучшение на 50%. Это намного превосходит мои ожидания. - person JD Long; 10.09.2010
comment
у меня небольшие проблемы с правильным запуском ... Но я учусь по ходу ... Я изменил данные в myDF, но не уверен, в чем проблема ... - person JD Long; 10.09.2010
comment
в приведенном выше коде, похоже, отсутствует создание матрицы 'data' (если я правильно это понимаю), возможно, data ‹- as.matrix (myDF [5: 6]) вверху? - person JD Long; 10.09.2010
comment
Я знаю, что это было 4 года назад, так что это длинный снимок, но есть этот вопрос, и теперь код indices, похоже, не запускается. У кого-нибудь еще есть эта проблема? Изменили ли обновления plyr принцип plyr:::split_indices работы? Получение этой ошибки Error in plyr:::split_indices(seq_len(nrow(data)), ids, n = attr(ids, : unused argument (ids) - person lamecicle; 05.11.2014
comment
@lamecicle, теперь тебе намного лучше использовать dplyr - person hadley; 05.11.2014
comment
@hadley Спасибо за это, трудно не отставать от новейших методов управления данными и манипуляций, есть какие-то предложения хорошей формы или что-то в этом роде? - person lamecicle; 05.11.2014
comment
@lamecicle #rstats hashtag в твиттере, возможно, - person hadley; 05.11.2014

Дальнейшее двукратное ускорение и более лаконичный код:

library(data.table)
dtb <- data.table(myDF, key="year,state,group1,group2")
system.time( 
  res <- dtb[, weighted.mean(myFact, weights), by=list(year, state, group1, group2)] 
)
#   user  system elapsed 
#  0.950   0.050   1.007 

Мой первый пост, так что будьте любезны;)


Из data.table v1.9.2 экспортируется setDT функция, которая преобразует data.frame в data.table по ссылке (в соответствии с языком data.table - все set* функции изменяют объект по ссылке). Это означает отсутствие ненужного копирования и, следовательно, быстрое. Вы можете рассчитать время, но это будет небрежно.

require(data.table)
system.time({
  setDT(myDF)
  res <- myDF[, weighted.mean(myFact, weights), 
             by=list(year, state, group1, group2)] 
})
#   user  system elapsed 
#  0.970   0.024   1.015 

Это в отличие от 1,264 секунды в решении OP выше, где data.table(.) используется для создания dtb.

person datasmurf    schedule 29.10.2010
comment
Хороший пост! Спасибо за ответ. Однако для согласованности с другими методами шаг, на котором создается таблица данных и индекс, должен находиться внутри шага system.time (). - person JD Long; 30.10.2010
comment
Действительно, но он все же остается самым быстрым. Было бы неплохо иметь возможность в ddply работать с data.tables или использовать data.tables под капотом (я только что обнаружил data.table, ища решения той же проблемы, но я бы предпочел более ddply-like синтаксис для этого случая). - person datasmurf; 31.10.2010

Я бы профиль с базой R

g <- with(myDF, paste(year, state, group1, group2))
x <- with(myDF, c(tapply(weights * myFact, g, sum) / tapply(weights, g, sum)))
aggregateDF <- myDF[match(names(x), g), c("year", "state", "group1", "group2")]
aggregateDF$V1 <- x

На моей машине это занимает 5 секунд по сравнению с 67 секундами с исходным кодом.

РЕДАКТИРОВАТЬ Только что нашел еще одно ускорение с функцией rowsum:

g <- with(myDF, paste(year, state, group1, group2))
X <- with(myDF, rowsum(data.frame(a=weights*myFact, b=weights), g))
x <- X$a/X$b
aggregateDF2 <- myDF[match(rownames(X), g), c("year", "state", "group1", "group2")]
aggregateDF2$V1 <- x

Это занимает 3 секунды!

person Marek    schedule 10.09.2010
comment
Второй занимает 5 секунд на моем компьютере, так что plyr все еще немного превосходит базу;) (Плюс он правильно упорядочивает строки) - person hadley; 10.09.2010
comment
Но спасибо за указатель на rowsum - так сложно угнаться за множеством функций агрегирования в базе R. - person hadley; 10.09.2010
comment
Я знал, что это должен быть простой способ сделать это, но я изо всех сил пытался понять это. У меня вообще есть эта борьба с семьей заявок. - person JD Long; 10.09.2010
comment
@hadley Согласен. Некоторое время назад я нашел замену apply(X,1,which.max) в col.max. Интересно, сколько еще существует функций этого типа (например, _3 _ / _ 4_), оптимизированных для объектов матриц с помощью уровня .Internal. - person Marek; 13.09.2010
comment
@Marek: см. 4dpiecharts.com / 2010/09/14 / - person Richie Cotton; 15.09.2010
comment
@Richie Это именно то, чем я планирую заняться на выходных :) И простота этого кода выдающаяся - person Marek; 15.09.2010

Вы используете последнюю версию plyr (примечание: она еще не попала на все зеркала CRAN)? Если это так, вы можете просто запустить это параллельно.

Вот пример llply, но то же самое должно относиться и к ddply:

  x <- seq_len(20)
  wait <- function(i) Sys.sleep(0.1)
  system.time(llply(x, wait))
  #  user  system elapsed 
  # 0.007   0.005   2.005 

  library(doMC)
  registerDoMC(2) 
  system.time(llply(x, wait, .parallel = TRUE))
  #  user  system elapsed 
  # 0.020   0.011   1.038 

Изменить:

Что ж, другие подходы к циклам хуже, поэтому для этого, вероятно, потребуется либо (а) код C / C ++, либо (б) более фундаментальное переосмысление того, как вы это делаете. Я даже не пробовал использовать by(), потому что, по моему опыту, это очень медленно.

groups <- unique(myDF[,c("year", "state", "group1", "group2")])
system.time(
aggregateDF <- do.call("rbind", lapply(1:nrow(groups), function(i) {
   df.tmp <- myDF[myDF$year==groups[i,"year"] & myDF$state==groups[i,"state"] & myDF$group1==groups[i,"group1"] & myDF$group2==groups[i,"group2"],]
   cbind(groups[i,], wtd.mean(df.tmp$myFact, weights=df.tmp$weights))
}))
)

aggregateDF <- data.frame()
system.time(
for(i in 1:nrow(groups)) {
   df.tmp <- myDF[myDF$year==groups[i,"year"] & myDF$state==groups[i,"state"] & myDF$group1==groups[i,"group1"] & myDF$group2==groups[i,"group2"],]
   aggregateDF <- rbind(aggregateDF, data.frame(cbind(groups[i,], wtd.mean(df.tmp$myFact, weights=df.tmp$weights))))
}
)
person Shane    schedule 10.09.2010
comment
это помогает мне в случае с одной машиной, но я уже сдуваю это с Hadoop и превышаю подписку на каждый узел (больше процессов, чем процессоров). Но мне очень приятно, что распараллеливание превратилось в plyr! - person JD Long; 10.09.2010

Я обычно использую индексный вектор с tapply, когда применяемая функция имеет несколько векторных аргументов:

system.time(tapply(1:nrow(myDF), myDF[c('year', 'state', 'group1', 'group2')], function(s) weighted.mean(myDF$myFact[s], myDF$weights[s])))
# user  system elapsed 
# 1.36    0.08    1.44 

Я использую простую оболочку, которая эквивалентна, но скрывает беспорядок:

tmapply(list(myDF$myFact, myDF$weights), myDF[c('year', 'state', 'group1', 'group2')], weighted.mean)

Отредактировано, чтобы включить tmapply для комментария ниже:

tmapply = function(XS, INDEX, FUN, ..., simplify=T) {
  FUN = match.fun(FUN)
  if (!is.list(XS))
    XS = list(XS)
  tapply(1:length(XS[[1L]]), INDEX, function(s, ...)
    do.call(FUN, c(lapply(XS, `[`, s), list(...))), ..., simplify=simplify)
}
person Charles    schedule 14.09.2010
comment
Просто добавлю: as.data.frame(as.table(RESULTS)) это простой способ создать data.frame из вывода. - person Marek; 15.09.2010
comment
Это tmapply, который вы используете? stat.ethz.ch/pipermail/r-help/2002- Октябрь / 025773.html - person Shane; 29.12.2010
comment
Здесь m используется для обозначения матрицы. В моем случае это означало больше как гибрид tapply и mapply, т.е. когда вы хотите вычислить сгруппированный агрегат с использованием нескольких входных данных (cor, weighted.mean и т. Д.). - person Charles; 29.12.2010