Risposte:
Le apply
funzioni in R non forniscono prestazioni migliorate rispetto ad altre funzioni di loop (ad es for
.). Un'eccezione a ciò è lapply
che 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 assign
o <<-
, 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 apply
funzioni familiari (anche il foreach
pacchetto è sostanzialmente equivalente, nonostante il nome). Ecco un semplice esempio della sapply
funzione 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 ). snow
ha 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 apply
funzioni debbano essere usate per l'esecuzione parallela poiché non hanno effetti collaterali . Quando si modifica un valore variabile all'interno di un for
ciclo, viene impostato a livello globale. D'altra parte, tutte le apply
funzioni possono essere tranquillamente utilizzate in parallelo perché le modifiche sono locali alla chiamata della funzione (a meno che non si tenti di utilizzare assign
o <<-
, 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 for
e *apply
per 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' df
ambiente genitore viene modificato for
ma non *apply
.
snowfall
pacchetto e provare gli esempi nella loro vignetta. snowfall
si basa sul snow
pacchetto e estrae i dettagli della parallelizzazione rendendo ancora più semplice l'esecuzione di apply
funzioni parallelizzate .
foreach
da allora è diventato disponibile e sembra essere molto indagato su SO.
lapply
è "un po 'più veloce" di un for
ciclo. 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 ( sapply
cerca 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 apply
può). 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
for
i loop sono più veloci sul mio computer Windows 10, a 2 core. L'ho fatto con gli 5e6
elementi: 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 for
loop 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 for
loop e solo un po 'più lento della tapply
funzione ottimizzata integrata. Non è perché vapply
è molto più veloce di for
ma perché sta eseguendo solo un'operazione in ogni iterazione del ciclo. In questo codice tutto il resto è vettorializzato. Nel for
ciclo 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 for
versione.
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
sapply
50% più lentamente rispetto for
e lapply
due volte più veloce.
y
su 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 for
utilizzo loop. In caso contrario, il messaggio è perfetto.
Quando si applicano funzioni su sottoinsiemi di un vettore, tapply
può 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 colSums
e 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.
apply
famiglia di funzioni. Pertanto, strutturare i programmi in modo che utilizzino si applica consente loro di essere parallelizzati a un costo marginale molto piccolo.