R: ускорение операций "group by"
У меня есть симуляция, которая имеет огромный агрегат и объединяет шаг прямо посередине. Я прототипировал этот процесс, используя функцию pdr 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)
)
)
Все советы или предложения приветствуются!
6 ответов
Вместо обычного фрейма данных 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
Это намного быстрее, потому что он избегает копирования данных, извлекая только подмножество, необходимое для каждого вычисления, когда оно вычисляется. Переключение данных в матричную форму дает еще один прирост скорости, поскольку поднабор матриц выполняется намного быстрее, чем поднабор кадров данных.
Далее 2-кратное ускорение и более краткий код:
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
,
Я бы профиль с базой 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 секунды!
Используете ли вы последнюю версию 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))))
}
)
Я обычно использую индексный вектор с 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)
}
Вероятно, самое быстрое решение - использоватьcollapse::fgroup_by
. Это в 8 раз быстрее, чемdata.table
:
library(collapse)
myDF %>%
fgroup_by(year, state, group1, group2) %>%
fsummarise(myFact = fmean(myFact, weights))
bm <- bench::mark(
collapse = myDF %>%
fgroup_by(year, state, group1, group2) %>%
fsummarise(myFact = fmean(myFact, weights)),
data.table = dtb[, weighted.mean(myFact, weights), by=list(year, state, group1, group2)],
check = FALSE)
#> bm
# expression min median itr/se…¹ mem_a…² gc/se…³ n_itr n_gc total…⁴
#1 collapse 101ms 105ms 9.10 8.84MB 0 5 0 549ms
#2 data.table 852ms 852ms 1.17 24.22MB 2.35 1 2 852ms