Odpowiedzi:
Te apply
funkcje R nie zapewniają większą wydajność w porównaniu z innymi typami pętli funkcji (na przykład for
). Jedynym wyjątkiem jest sytuacja, lapply
któ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ą assign
lub <<-
, 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 apply
funkcje rodzinne (nawet foreach
pakiet jest zasadniczo równoważny, pomimo nazwy). Oto prosty przykład sapply
funkcji 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 ). snow
ma 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 apply
funkcje były wykonywane równolegle, ponieważ nie mają one skutków ubocznych . Kiedy zmieniasz wartość zmiennej w for
pętli, jest ona ustawiana globalnie. Z drugiej strony wszystkie apply
funkcje mogą być bezpiecznie używane równolegle, ponieważ zmiany są lokalne dla wywołania funkcji (chyba że spróbujesz użyć assign
lub <<-
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 for
i w *apply
takim 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 df
zmienia się środowisko w środowisku nadrzędnym, for
ale nie *apply
.
snowfall
opakowanie i wypróbować przykłady w ich winiecie. snowfall
kompiluje się na snow
pakiecie i dodatkowo abstrakcyjnie wyodrębnia szczegóły równoległości, dzięki czemu wykonywanie równoległych apply
funkcji jest bardzo proste .
foreach
od tego czasu stało się dostępne i wydaje się, że jest bardzo pytany w SO.
lapply
jest „trochę szybszy” niż for
pętla. Jednak nie widzę nic, co by to sugerowało. Wspominasz tylko, że lapply
jest szybszy niż sapply
, co jest dobrze znanym faktem z innych powodów ( sapply
pró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?
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? ;-)
*apply
jest szybszy. Ale myślę, że ważniejszy punkt to skutki uboczne (zaktualizowałem moją odpowiedź przykładem).
data.table
jest 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")])
tapply
jest 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 apply
może). Porównujesz jabłka z pomarańczami.
... 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
for
pętle są szybsze na moim komputerze z 2-rdzeniowym systemem Windows 10. Zrobiłem to z 5e6
elementami - pętla wynosiła 2,9 sekundy vs. 3,1 sekundy dla vapply
.
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. ZAfor
Pę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ż for
pętla i tylko trochę wolniej niż wbudowana tapply
funkcja zoptymalizowana . 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. W tradycyjnej for
pę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ż for
wersja.
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
sapply
50% wolniejszy for
i lapply
dwukrotnie szybszy.
y
do 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 for
użycie pętli. Poza tym wiadomość jest na miejscu.
Stosowanie funkcji na podzbiorach wektora tapply
moż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
apply
jednak 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 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
microbenchmark
jest 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ę.
apply
ję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.