Czy rodzina R stosuje się bardziej niż cukier syntaktyczny?

152

... pod względem czasu wykonywania i / lub pamięci.

Jeśli tak nie jest, udowodnij to za pomocą fragmentu kodu. Zauważ, że przyspieszenie przez wektoryzację się nie liczy. Przyśpieszenie musi pochodzić z apply( tapply, sapply...) sama.

Steffen
źródło

Odpowiedzi:

152

Te applyfunkcje R nie zapewniają większą wydajność w porównaniu z innymi typami pętli funkcji (na przykład for). Jedynym wyjątkiem jest sytuacja, lapplyktóra może być nieco szybsza, ponieważ wykonuje więcej pracy w kodzie C niż w języku R (zobacz to pytanie jako przykład ).

Generalnie jednak zasada jest taka, że należy używać funkcji zastosuj w celu zwiększenia przejrzystości, a nie wydajności .

Dodałbym do tego, że zastosowane funkcje nie mają skutków ubocznych , co jest ważnym rozróżnieniem, jeśli chodzi o programowanie funkcjonalne w R. Można to zmienić za pomocą assignlub <<-, ale może to być bardzo niebezpieczne. Efekty uboczne również utrudniają zrozumienie programu, ponieważ stan zmiennej zależy od historii.

Edytować:

Dla podkreślenia tego trywialnym przykładem, który rekurencyjnie oblicza ciąg Fibonacciego; można to uruchomić wiele razy, aby uzyskać dokładny pomiar, 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 

Edycja 2:

Jeśli chodzi o użycie pakietów równoległych dla R (np. Rpvm, rmpi, snow), generalnie zapewniają one applyfunkcje rodzinne (nawet foreachpakiet jest zasadniczo równoważny, pomimo nazwy). Oto prosty przykład sapplyfunkcji w snow:

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

W tym przykładzie zastosowano klaster gniazd, dla którego nie trzeba instalować dodatkowego oprogramowania; w przeciwnym razie będziesz potrzebować czegoś takiego jak PVM lub MPI (patrz strona klastrowania Tierney ). snowma następujące funkcje:

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

Sensowne jest, aby applyfunkcje były wykonywane równolegle, ponieważ nie mają one skutków ubocznych . Kiedy zmieniasz wartość zmiennej w forpętli, jest ona ustawiana globalnie. Z drugiej strony wszystkie applyfunkcje mogą być bezpiecznie używane równolegle, ponieważ zmiany są lokalne dla wywołania funkcji (chyba że spróbujesz użyć assignlub <<-w takim przypadku możesz wprowadzić efekty uboczne). Nie trzeba dodawać, że należy uważać na zmienne lokalne i globalne, zwłaszcza w przypadku wykonywania równoległego.

Edytować:

Oto trywialny przykład pokazujący różnicę między skutkami ubocznymi fori w *applytakim zakresie:

> 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

Zwróć uwagę, jak dfzmienia się środowisko w środowisku nadrzędnym, forale nie *apply.

Shane
źródło
30
Większość pakietów wielordzeniowych dla applyjęzyka R implementuje również zrównoleglenie poprzez rodzinę funkcji. Dlatego strukturyzacja programów tak, aby stosowały, pozwala na ich zrównoleglenie przy bardzo małym koszcie krańcowym.
Sharpie
Sharpie - dziękuję za to! Jakiś pomysł na przykład pokazujący to (w systemie Windows XP)?
Tal Galili
5
Proponuję spojrzeć na snowfallopakowanie i wypróbować przykłady w ich winiecie. snowfallkompiluje się na snowpakiecie i dodatkowo abstrakcyjnie wyodrębnia szczegóły równoległości, dzięki czemu wykonywanie równoległych applyfunkcji jest bardzo proste .
Sharpie,
1
@Sharpie, ale pamiętaj, że foreachod tego czasu stało się dostępne i wydaje się, że jest bardzo pytany w SO.
Ari B. Friedman
1
@Shane, na samej górze swojej odpowiedzi podajesz link do innego pytania jako przykładu przypadku, w którym lapplyjest „trochę szybszy” niż forpętla. Jednak nie widzę nic, co by to sugerowało. Wspominasz tylko, że lapplyjest szybszy niż sapply, co jest dobrze znanym faktem z innych powodów ( sapplypróbuje uprościć dane wyjściowe i dlatego musi wykonywać wiele sprawdzania rozmiaru danych i potencjalnych konwersji). Nic związanego z for. Czy coś mi brakuje?
flodela
70

Czasami przyspieszenie może być znaczne, na przykład gdy trzeba zagnieżdżać pętle for, aby uzyskać średnią na podstawie grupowania 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)

Obie dają dokładnie ten sam wynik, będący macierzą 5 x 10 ze średnimi oraz 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 ja wygrałem? ;-)

Joris Meys
źródło
aah, taki słodki :-) Właściwie zastanawiałem się, czy ktoś kiedykolwiek spotkałby się z moją dość późną odpowiedzią.
Joris Meys
1
Zawsze sortuję według „aktywnych”. :) Nie wiem, jak uogólnić swoją odpowiedź; czasami *applyjest szybszy. Ale myślę, że ważniejszy punkt to skutki uboczne (zaktualizowałem moją odpowiedź przykładem).
Shane
1
Myślę, że zastosowanie jest szczególnie szybsze, gdy chcesz zastosować funkcję do różnych podzbiorów. Jeśli istnieje inteligentne rozwiązanie do stosowania dla zagnieżdżonej pętli, myślę, że rozwiązanie do stosowania również będzie szybsze. W większości przypadków aplikacja nie przyspiesza, jak sądzę, ale zdecydowanie zgadzam się z efektami ubocznymi.
Joris Meys
2
To trochę nie na temat, ale w tym konkretnym przykładzie data.tablejest jeszcze szybsze i myślę, że „łatwiejsze”. library(data.table) dt<-data.table(X,Y,Z,key=c("Y,Z")) system.time(dt[,list(X_mean=mean(X)),by=c("Y,Z")])
dnlbrky
12
To porównanie jest absurdalne. tapplyjest wyspecjalizowaną funkcję dla określonego zadania, to dlaczego to szybciej niż pętli for. Nie może zrobić tego, co może zrobić pętla for (podczas gdy zwykła applymoże). Porównujesz jabłka z pomarańczami.
eddi
47

... i jak właśnie napisałem w innym miejscu, vapply jest twoim przyjacielem! ... to jak sapply, ale określasz również typ zwracanej wartości, co znacznie przyspiesza.

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 

Aktualizacja z 1 stycznia 2020 r .:

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
Tommy
źródło
Oryginalne ustalenia nie wydają się już prawdziwe. forpętle są szybsze na moim komputerze z 2-rdzeniowym systemem Windows 10. Zrobiłem to z 5e6elementami - pętla wynosiła 2,9 sekundy vs. 3,1 sekundy dla vapply.
Cole
27

W innym miejscu napisałem, że przykład taki jak Shane tak naprawdę nie podkreśla różnicy w wydajności między różnymi rodzajami składni zapętlonej, ponieważ cały czas spędza się w funkcji, a nie na obciążaniu pętli. Ponadto kod niesprawiedliwie 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 tę kwestię.

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, zastosowanie funkcji rodziny może być dużo więcej niż tylko cukrem syntaktycznym.

(Prosta nie na liście z wynosi tylko 0,2 s, więc okrążenie jest znacznie szybsze. Inicjalizacja z w pętli for jest dość szybka, ponieważ podaję średnią z ostatnich 5 z 6 biegów, więc poruszanie się poza systemem. prawie nie wpływają na rzeczy)

Należy jednak zauważyć, że istnieje jeszcze jeden powód, dla którego warto stosować funkcje rodzinne niezależnie od ich wydajności, przejrzystości lub braku skutków ubocznych. ZAforPętla zwykle promuje wprowadzenie jak najwięcej wewnątrz pętli. Dzieje się tak, ponieważ każda pętla wymaga ustawienia zmiennych do przechowywania informacji (wśród innych możliwych operacji). Instrukcje Zastosuj są zwykle stronnicze w drugą stronę. Często chcesz wykonać wiele operacji na danych, z których kilka można wektoryzować, ale niektóre mogą nie być w stanie tego zrobić. 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 wektoryzowanej wersji funkcji) i te, które są wektoryzowane jako prawdziwe operacje wektorowe. To często ogromnie przyspiesza wydajność.

Biorąc przykład Jorisa Meysa, w którym zastępuje tradycyjną pętlę for poręczną funkcją R, możemy jej użyć, aby pokazać efektywność pisania kodu w bardziej przyjazny dla języka R sposób przy podobnym przyspieszeniu 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ż forpętla i tylko trochę wolniej niż wbudowana tapplyfunkcja zoptymalizowana . Nie dlatego, że vapplyjest o wiele szybszy niż, forale dlatego, że wykonuje tylko jedną operację w każdej iteracji pętli. W tym kodzie wszystko inne jest wektoryzowane. W tradycyjnej forpętli Joris Meys wiele (7?) Operacji jest wykonywanych w każdej iteracji i jest sporo konfiguracji tylko po to, aby je wykonać. Zwróć także uwagę, o ile bardziej kompaktowy jest to format niż forwersja.

Jan
źródło
4
Ale przykład Shane jest realistyczne, że przez większość czasu jest zwykle spędził w funkcji, a nie w pętli.
hadley
9
mów za siebie ...:) ... Może Shane jest w pewnym sensie realistyczny, ale w tym samym sensie analiza jest kompletnie bezużyteczna. Ludziom będzie zależało na szybkości mechanizmu iteracji, kiedy będą musieli wykonać wiele iteracji, w przeciwnym razie ich problemy i tak są gdzie indziej. Dotyczy to każdej funkcji. Jeśli napiszę grzech, który zabiera 0,001 s, a ktoś inny pisze taki, który zabiera 0,002, kogo to obchodzi? Cóż, jak tylko będziesz musiał zrobić kilka z nich, będzie ci zależało.
John
2
na 12-rdzeniowym Intel Xeon 3Ghz, 64-bitowym, otrzymuję zupełnie inne wartości - pętla for znacznie się poprawia: w przypadku twoich trzech testów otrzymuję 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528, a vapply jest jeszcze lepszy:1.19 0.00 1.19
zero 101
2
Różni się w zależności od wersji systemu operacyjnego i R ... iw absolutnym sensie procesora. Właśnie uruchomiłem 2.15.2 na Macu i byłem o sapply50% wolniejszy fori lapplydwukrotnie szybszy.
John
1
W przykładzie, to znaczy zestaw ydo 1:1e6, a nie numeric(1e6)(wektorem zer). Starając się przeznaczyć foo(0)do z[0]kółko ma również nie przedstawiają typowe forużycie pętli. Poza tym wiadomość jest na miejscu.
flodela
3

Stosowanie funkcji na podzbiorach wektora tapplymoże być znacznie szybsze 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

applyjednak w większości sytuacji nie zapewnia żadnego wzrostu prędkości, aw niektórych przypadkach może być nawet dużo wolniejsza:

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 colSumsi 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
Michele
źródło
7
Należy zauważyć, że (w przypadku małych fragmentów kodu) microbenchmarkjest on znacznie dokładniejszy niż system.time. Jeśli próbujesz porównać system.time(f3(mat))i system.time(f4(mat))dostaniesz inny wynik prawie za każdym razem. Czasami tylko właściwy test porównawczy jest w stanie pokazać najszybszą funkcję.
Michele