Risposte:
Le applyfunzioni in R non forniscono prestazioni migliorate rispetto ad altre funzioni di loop (ad es for.). Un'eccezione a ciò è lapplyche può essere un po 'più veloce perché funziona più nel codice C che in R (vedi questa domanda per un esempio di questo ).
Ma in generale, la regola è che dovresti usare una funzione apply per chiarezza, non per prestazioni .
Aggiungo a ciò che applicare le funzioni non ha effetti collaterali , il che è una distinzione importante quando si tratta di programmazione funzionale con R. Questo può essere ignorato usando assigno <<-, ma può essere molto pericoloso. Gli effetti collaterali rendono anche un programma più difficile da capire poiché lo stato di una variabile dipende dalla storia.
Modificare:
Giusto per enfatizzare questo con un banale esempio che calcola ricorsivamente la sequenza di Fibonacci; questo potrebbe essere eseguito più volte per ottenere una misura accurata, ma il punto è che nessuno dei metodi ha prestazioni significativamente diverse:
> 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
Modifica 2:
Per quanto riguarda l'uso di pacchetti paralleli per R (ad es. Rpvm, rmpi, snow), questi generalmente forniscono applyfunzioni familiari (anche il foreachpacchetto è sostanzialmente equivalente, nonostante il nome). Ecco un semplice esempio della sapplyfunzione in snow:
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
In questo esempio viene utilizzato un cluster di socket, per il quale non è necessario installare alcun software aggiuntivo; altrimenti avrai bisogno di qualcosa come PVM o MPI (vedi la pagina di clustering di Tierney ). snowha le seguenti funzioni di applicazione:
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, ...)
Ha senso che le applyfunzioni debbano essere usate per l'esecuzione parallela poiché non hanno effetti collaterali . Quando si modifica un valore variabile all'interno di un forciclo, viene impostato a livello globale. D'altra parte, tutte le applyfunzioni possono essere tranquillamente utilizzate in parallelo perché le modifiche sono locali alla chiamata della funzione (a meno che non si tenti di utilizzare assigno <<-, nel qual caso è possibile introdurre effetti collaterali). Inutile dire che è fondamentale fare attenzione alle variabili locali rispetto a quelle globali, specialmente quando si ha a che fare con l'esecuzione parallela.
Modificare:
Ecco un esempio banale per dimostrare la differenza tra fore *applyper quanto riguarda gli effetti collaterali:
> 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
Nota come l' dfambiente genitore viene modificato forma non *apply.
snowfallpacchetto e provare gli esempi nella loro vignetta. snowfallsi basa sul snowpacchetto e estrae i dettagli della parallelizzazione rendendo ancora più semplice l'esecuzione di applyfunzioni parallelizzate .
foreachda allora è diventato disponibile e sembra essere molto indagato su SO.
lapplyè "un po 'più veloce" di un forciclo. Tuttavia, lì, non vedo nulla che lo suggerisca. Hai solo detto che lapplyè più veloce di sapply, il che è un fatto ben noto per altri motivi ( sapplycerca di semplificare l'output e quindi deve fare un sacco di controllo della dimensione dei dati e potenziali conversioni). Niente a che fare con for. Mi sto perdendo qualcosa?
A volte l'accelerazione può essere sostanziale, come quando devi annidare i for-loop per ottenere la media basata su un raggruppamento di più di un fattore. Qui hai due approcci che ti danno esattamente lo stesso risultato:
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)
Entrambi danno esattamente lo stesso risultato, essendo una matrice 5 x 10 con le medie e le righe e le colonne denominate. Ma :
> 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
Ecco qua Cosa ho vinto? ;-)
*applyè più veloce. Ma penso che il punto più importante siano gli effetti collaterali (ho aggiornato la mia risposta con un esempio).
data.tableè ancora più veloce e penso "più facile". 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è una funzione specializzata per un'attività specifica, ecco perché è più veloce di un ciclo for. Non può fare ciò che può fare un ciclo for (mentre il normale applypuò). Stai confrontando le mele con le arance.
... e come ho appena scritto altrove, Vapply è tuo amico! ... è come sapply, ma si specifica anche il tipo di valore restituito che lo rende molto più veloce.
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
Aggiornamento del 1 gennaio 2020:
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
fori loop sono più veloci sul mio computer Windows 10, a 2 core. L'ho fatto con gli 5e6elementi: un ciclo era di 2,9 secondi contro 3,1 secondi per vapply.
Ho scritto altrove che un esempio come quello di Shane non sottolinea realmente la differenza nelle prestazioni tra i vari tipi di sintassi del loop perché il tempo è tutto trascorso all'interno della funzione piuttosto che stressare il loop. Inoltre, il codice confronta ingiustamente un ciclo for senza memoria con le funzioni della famiglia apply che restituiscono un valore. Ecco un esempio leggermente diverso che sottolinea il punto.
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
Se hai intenzione di salvare il risultato, applicare le funzioni familiari può essere molto più che uno zucchero sintattico.
(la semplice unlist di z è di soli 0,2 secondi, quindi lapply è molto più veloce. L'inizializzazione di z nel ciclo for è piuttosto veloce perché sto dando la media delle ultime 5 corse su 6 così in movimento che al di fuori del sistema. difficilmente influenzano le cose)
Un'altra cosa da notare è che c'è un altro motivo per usare le funzioni familiari applicate indipendentemente dalle loro prestazioni, chiarezza o mancanza di effetti collaterali. Un forloop in genere promuove l'inserimento il più possibile all'interno del loop. Questo perché ogni ciclo richiede la configurazione di variabili per memorizzare informazioni (tra le altre possibili operazioni). Le dichiarazioni di applicazione tendono ad essere distorte nell'altro modo. Spesso si desidera eseguire più operazioni sui dati, molti dei quali possono essere vettorializzati ma alcuni potrebbero non essere in grado di esserlo. In R, a differenza di altre lingue, è meglio separare quelle operazioni ed eseguire quelle che non sono vettorializzate in un'istruzione apply (o versione vettorializzata della funzione) e quelle che sono vettorializzate come vere operazioni vettoriali. Questo spesso accelera notevolmente le prestazioni.
Prendendo l'esempio di Joris Meys in cui sostituisce un tradizionale ciclo per con una pratica funzione R, possiamo usarlo per mostrare l'efficienza della scrittura del codice in un modo più R amichevole per una velocità simile senza la funzione specializzata.
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
Questo finisce per essere molto più veloce del forloop e solo un po 'più lento della tapplyfunzione ottimizzata integrata. Non è perché vapplyè molto più veloce di forma perché sta eseguendo solo un'operazione in ogni iterazione del ciclo. In questo codice tutto il resto è vettorializzato. Nel forciclo tradizionale di Joris Meys si verificano molte (7?) Operazioni in ogni iterazione e c'è un bel po 'di configurazione solo per l'esecuzione. Nota anche quanto è più compatto rispetto alla forversione.
2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528, e vapply è ancora meglio:1.19 0.00 1.19
sapply50% più lentamente rispetto fore lapplydue volte più veloce.
ysu 1:1e6, non numeric(1e6)(un vettore di zero). Cercando di assegnare foo(0)a z[0]più e più non illustra bene un tipico forutilizzo loop. In caso contrario, il messaggio è perfetto.
Quando si applicano funzioni su sottoinsiemi di un vettore, tapplypuò essere più veloce di un ciclo for. Esempio:
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, tuttavia, nella maggior parte delle situazioni non fornisce alcun aumento di velocità e in alcuni casi può essere anche molto più lento:
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
Ma per queste situazioni abbiamo colSumse 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è molto più preciso di system.time. Se provi a confrontare system.time(f3(mat))e system.time(f4(mat))otterrai risultati diversi quasi ogni volta. A volte solo un test benchmark adeguato è in grado di mostrare la funzione più veloce.
applyfamiglia di funzioni. Pertanto, strutturare i programmi in modo che utilizzino si applica consente loro di essere parallelizzati a un costo marginale molto piccolo.