Czy rodzina R jest czymś więcej niż cukrem składniowym?

...odnośnie czasu wykonania i / lub pamięci.

Jeśli to nieprawda, udowodnij to fragmentem kodu. Zauważ, że przyspieszenie przez wektoryzację się nie liczy. Speedup musi pochodzić z apply (tapply, sapply, ...).

 155
Author: Jeromy Anglim, 2010-02-16

5 answers

Funkcje apply W R nie zapewniają lepszej wydajności w porównaniu z innymi funkcjami pętli (np. for). Jednym z wyjątków jest lapply, który może być nieco szybszy, ponieważ działa więcej w kodzie C niż w R (zobacz to pytanie na przykład tego ).

Ale ogólnie, zasada jest taka, że powinieneś używać funkcji apply dla jasności, a nie dla wydajności.

Dodam jeszcze, że apply functions have no skutki uboczne, co jest ważną różnicą, jeśli chodzi o programowanie funkcyjne za pomocą R. można to zastąpić za pomocą assign lub <<-, ale może to być bardzo niebezpieczne. Skutki uboczne również utrudniają zrozumienie programu, ponieważ stan zmiennej zależy od historii.

Edit:

Aby podkreślić to banalnym przykładem, który rekurencyjnie oblicza ciąg Fibonacciego; może to być uruchomione wiele razy, aby uzyskać dokładną miary, ale chodzi o to, że żadna z metod nie ma znacząco różnej wydajności:

> fibo <- function(n) {
+   if ( n < 2 ) n
+   else fibo(n-1) + fibo(n-2)
+ }
> system.time(for(i in 0:26) fibo(i))
   user  system elapsed 
   7.48    0.00    7.52 
> system.time(sapply(0:26, fibo))
   user  system elapsed 
   7.50    0.00    7.54 
> system.time(lapply(0:26, fibo))
   user  system elapsed 
   7.48    0.04    7.54 
> library(plyr)
> system.time(ldply(0:26, fibo))
   user  system elapsed 
   7.52    0.00    7.58 

Edytuj 2:

Jeśli chodzi o użycie pakietów równoległych dla R (np. rpvm, rmpi, snow), to zazwyczaj dostarczają one funkcje rodziny apply (nawet foreach Pakiet jest zasadniczo równoważny, pomimo nazwy). Oto prosty przykład funkcji sapply W snow:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)

Ten przykład używa klastra gniazd, dla którego nie ma potrzeby dodatkowego oprogramowania zainstalowane; w przeciwnym razie będziesz potrzebował czegoś takiego jak PVM lub MPI(zobacz strona klastrów Tierney ). snow posiada następujące funkcje zastosowania:

parLapply(cl, x, fun, ...)
parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
parApply(cl, X, MARGIN, FUN, ...)
parRapply(cl, x, fun, ...)
parCapply(cl, x, fun, ...)

Ma sens, że apply funkcje powinny być używane do wykonywania równoległego, ponieważ nie mająskutki uboczne. Gdy zmienisz wartość zmiennej w pętli for, zostanie ona ustawiona globalnie. Z drugiej strony, wszystkie funkcje apply mogą być bezpiecznie używane równolegle, ponieważ zmiany są lokalne do wywołanie funkcji (chyba że spróbujesz użyć assign lub <<-, w którym to przypadku możesz wprowadzić efekty uboczne). Nie trzeba dodawać, że należy uważać na zmienne lokalne i globalne, szczególnie w przypadku wykonywania równoległego.

Edit:

Oto trywialny przykład, aby wykazać różnicę między for i *apply jeśli chodzi o skutki uboczne:

> df <- 1:10
> # *apply example
> lapply(2:3, function(i) df <- df * i)
> df
 [1]  1  2  3  4  5  6  7  8  9 10
> # for loop example
> for(i in 2:3) df <- df * i
> df
 [1]  6 12 18 24 30 36 42 48 54 60

Zauważ jak df w środowisku macierzystym jest zmieniany przez for ale nie *apply.

 154
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
2017-05-23 11:46:55

Czasami przyspieszenie może być znaczne, jak wtedy, gdy trzeba zagnieżdżać pętle for, aby uzyskać średnią opartą na grupowaniu więcej niż jednego czynnika. Tutaj masz dwa podejścia, które dają dokładnie ten sam wynik:

set.seed(1)  #for reproducability of the results

# The data
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))

# the function forloop that averages X over every combination of Y and Z
forloop <- function(x,y,z){
# These ones are for optimization, so the functions 
#levels() and length() don't have to be called more than once.
  ylev <- levels(y)
  zlev <- levels(z)
  n <- length(ylev)
  p <- length(zlev)

  out <- matrix(NA,ncol=p,nrow=n)
  for(i in 1:n){
      for(j in 1:p){
          out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
      }
  }
  rownames(out) <- ylev
  colnames(out) <- zlev
  return(out)
}

# Used on the generated data
forloop(X,Y,Z)

# The same using tapply
tapply(X,list(Y,Z),mean)

Oba dają dokładnie ten sam wynik, będąc macierzą 5 x 10 ze średnimi i nazwanymi wierszami i kolumnami. Ale:

> system.time(forloop(X,Y,Z))
   user  system elapsed 
   0.94    0.02    0.95 

> system.time(tapply(X,list(Y,Z),mean))
   user  system elapsed 
   0.06    0.00    0.06 
Proszę bardzo. Co wygrałem? ;-)
 70
Author: Joris Meys,
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-08-27 12:51:40

...i jak pisałem gdzie indziej, vapply jest twoim przyjacielem! ... to jest jak sapply, ale również określić typ zwracanej wartości, co sprawia, że znacznie szybciej.

foo <- function(x) x+1
y <- numeric(1e6)

system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
#   user  system elapsed 
#   3.54    0.00    3.53 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#   2.89    0.00    2.91 
system.time(z <- vapply(y, foo, numeric(1)))
#   user  system elapsed 
#   1.35    0.00    1.36 
[[2]] Jan. 1, 2020 aktualizacja:
system.time({z1 <- numeric(1e6); for(i in seq_along(y)) z1[i] <- foo(y[i])})
#   user  system elapsed 
#   0.52    0.00    0.53 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#   0.72    0.00    0.72 
system.time(z3 <- vapply(y, foo, numeric(1)))
#   user  system elapsed 
#    0.7     0.0     0.7 
identical(z1, z3)
# [1] TRUE
 47
Author: Tommy,
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
2020-01-01 15:56:13

Pisałem gdzie indziej, że przykład taki jak Shane ' a nie podkreśla różnicy w wydajności pomiędzy różnymi rodzajami składni pętli, ponieważ czas spędzany jest w funkcji, a nie w rzeczywistości podkreślając pętlę. Ponadto kod niesłusznie porównuje pętlę for bez pamięci z funkcjami rodziny apply, które zwracają wartość. Oto nieco inny przykład, który podkreśla punkt.

foo <- function(x) {
   x <- x+1
 }
y <- numeric(1e6)
system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
#   user  system elapsed 
#  4.967   0.049   7.293 
system.time(z <- sapply(y, foo))
#   user  system elapsed 
#  5.256   0.134   7.965 
system.time(z <- lapply(y, foo))
#   user  system elapsed 
#  2.179   0.126   3.301 

Jeśli planujesz zapisać wynik, zastosuj rodzinę funkcje mogą być znacznie więcej niż cukrem składniowym.

(prosta unlist z wynosi tylko 0,2 s, Więc lapply jest znacznie szybszy. Inicjalizacja z w pętli for jest dość szybka, ponieważ daję średnią z ostatnich 5 z 6 uruchomień tak poruszających się poza systemem.czas nie wpłynie na rzeczy)

Jeszcze jedną rzeczą do zauważenia jest to, że istnieje inny powód, aby używać funkcji apply family niezależnie od ich wydajności, jasności lub braku efektów ubocznych. A for pętla zazwyczaj Promuje umieszczenie jak najwięcej w pętli. Dzieje się tak, ponieważ każda pętla wymaga ustawienia zmiennych do przechowywania informacji (wśród innych możliwych operacji). Wyrażenia Apply są tendencyjne w drugą stronę. Często chcesz wykonać wiele operacji na swoich danych, z których kilka można wektoryzować, ale niektóre mogą nie być w stanie. W R, W przeciwieństwie do innych języków, najlepiej jest oddzielić te operacje i uruchomić te, które nie są wektoryzowane w instrukcji apply (lub wektoryzowana wersja funkcji) i te, które są wektoryzowane jako prawdziwe operacje wektorowe. To często znacznie przyspiesza wydajność.

Biorąc przykład Jorisa Meysa, gdzie zastępuje on tradycyjną pętlę for poręczną funkcją R, możemy użyć jej, aby pokazać efektywność pisania kodu w bardziej przyjazny dla R sposób dla podobnego przyspieszenia bez wyspecjalizowanej funkcji.

set.seed(1)  #for reproducability of the results

# The data - copied from Joris Meys answer
X <- rnorm(100000)
Y <- as.factor(sample(letters[1:5],100000,replace=T))
Z <- as.factor(sample(letters[1:10],100000,replace=T))

# an R way to generate tapply functionality that is fast and 
# shows more general principles about fast R coding
YZ <- interaction(Y, Z)
XS <- split(X, YZ)
m <- vapply(XS, mean, numeric(1))
m <- matrix(m, nrow = length(levels(Y)))
rownames(m) <- levels(Y)
colnames(m) <- levels(Z)
m

To kończy się znacznie szybciej niż for pętli i tylko trochę wolniej niż zbudowany w zoptymalizowanej funkcji tapply. Nie dlatego, że vapply jest o wiele szybszy niż for, ale dlatego, że wykonuje tylko jedną operację w każdej iteracji pętli. W tym kodzie Wszystko inne jest wektoryzowane. In Joris Meys traditional for loop many (7?) operacje występują w każdej iteracji i jest sporo konfiguracji tylko do jej wykonania. Zauważ również, jak bardzo jest to bardziej kompaktowe niż wersja for.

 28
Author: John,
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
2016-08-11 21:42:27

Podczas stosowania funkcji nad podzbiorami wektora, tapply może być dość szybsza niż pętla for. Przykład:

df <- data.frame(id = rep(letters[1:10], 100000),
                 value = rnorm(1000000))

f1 <- function(x)
  tapply(x$value, x$id, sum)

f2 <- function(x){
  res <- 0
  for(i in seq_along(l <- unique(x$id)))
    res[i] <- sum(x$value[x$id == l[i]])
  names(res) <- l
  res
}            

library(microbenchmark)

> microbenchmark(f1(df), f2(df), times=100)
Unit: milliseconds
   expr      min       lq   median       uq      max neval
 f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656   100
 f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273   100

apply, jednak w większości sytuacji nie zapewnia wzrostu prędkości, a w niektórych przypadkach może być nawet dużo wolniej: {]}

mat <- matrix(rnorm(1000000), nrow=1000)

f3 <- function(x)
  apply(x, 2, sum)

f4 <- function(x){
  res <- 0
  for(i in 1:ncol(x))
    res[i] <- sum(x[,i])
  res
}

> microbenchmark(f3(mat), f4(mat), times=100)
Unit: milliseconds
    expr      min       lq   median       uq      max neval
 f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975   100
 f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100   100

Ale w takich sytuacjach mamy colSums i rowSums:

f5 <- function(x)
  colSums(x) 

> microbenchmark(f5(mat), times=100)
Unit: milliseconds
    expr      min       lq   median       uq      max neval
 f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909   100
 4
Author: Michele,
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-05-30 17:54:15