Odpowiedzi:
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.
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 .
foreachod tego czasu stało się dostępne i wydaje się, że jest bardzo pytany w SO.
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?
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? ;-)
*applyjest szybszy. Ale myślę, że ważniejszy punkt to skutki uboczne (zaktualizowałem moją odpowiedź przykładem).
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")])
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.
... 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
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.
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.
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
sapply50% wolniejszy fori lapplydwukrotnie szybszy.
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.
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
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ę.
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.