R: przyspieszenie operacji "grupowo"

Mam symulację, która ma ogromny agregat i połączyć krok w samym środku. Prototypowałem ten proces za pomocą funkcji ddply () plyra, która działa świetnie dla ogromnego odsetka moich potrzeb. Ale ten etap agregacji musi być szybszy, ponieważ muszę uruchomić symulacje 10K. Jestem już skalowanie symulacji równolegle, ale jeśli ten jeden krok były szybsze mógłbym znacznie zmniejszyć liczbę węzłów, których potrzebuję.

Oto rozsądne uproszczenie tego, co próbuję do:

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

Wszystkie wskazówki lub sugestie są mile widziane!

Author: Matt Dowle, 2010-09-10

5 answers

Zamiast zwykłej ramki danych R, można użyć niezmiennej ramki danych, która zwraca wskaźniki do oryginalnej, gdy jest ustawiona i może być znacznie szybsza:

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 

Gdybym miał napisać funkcję plyr dostosowaną dokładnie do tej sytuacji, zrobiłbym coś takiego:

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 

Jest o wiele szybszy, ponieważ unika kopiowania danych, wyodrębniając tylko podzbiór potrzebny do każdego obliczenia, gdy jest obliczany. Przełączenie danych na matrycę daje kolejny wzrost prędkości, ponieważ podsiewanie macierzy jest znacznie szybsze niż podsiewanie ramek danych.

 37
Author: hadley,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-10 17:32:16

Kolejne 2x speedup i bardziej zwięzły kod:

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 

Mój pierwszy post, więc proszę bądź miły;)


From data.table v1.9.2, setDT wyeksportowana zostanie funkcja, która przekonwertuje data.frame na data.table przez odniesienie (zgodnie z data.table - wszystkie funkcje set* modyfikują obiekt przez odniesienie). Oznacza to, bez zbędnego kopiowania, a zatem jest szybki. Możesz to zmierzyć, ale to będzie niedbałe.

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 

Jest to w przeciwieństwie do 1.264 sekundy z rozwiązaniem OP powyżej, gdzie {[8] } jest używany do tworzenia dtb.

 25
Author: datasmurf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-06 16:21:42

Chciałbym profilować z bazą 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

Na mojej maszynie zajmuje 5sec w porównaniu do 67sec z oryginalnym kodem.

EDIT Właśnie znalazłem kolejne przyspieszenie z rowsum funkcją:

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

It takes 3sec!

 8
Author: Marek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-10 16:14:44

Czy używasz najnowszej wersji plyr (uwaga: to jeszcze nie dotarło do wszystkich luster CRAN)? Jeśli tak, możesz po prostu uruchomić to równolegle.

Oto przykład llply, ale to samo powinno dotyczyć 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 

Edit:

Cóż, inne podejścia do pętli są gorsze, więc prawdopodobnie wymaga to albo (a) kodu C / C++ lub (b) bardziej fundamentalnego przemyślenia tego, jak to robisz. Nawet nie próbowałem używać by(), bo to bardzo powolne w moim doświadczenie.

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))))
}
)
 7
Author: Shane,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-10 15:20:38

Zwykle używam wektora indeksowego z tapply, gdy stosowana funkcja ma wiele wektorów args:

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 

Używam prostego wrappera, który jest równoważny, ale ukrywa bałagan:

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

Edytowano, aby dodać tmapply do komentarza poniżej:

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)
}
 5
Author: Charles,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-29 19:27:16