La famiglia di applicare R è più dello zucchero sintattico?


152

... per quanto riguarda i tempi di esecuzione e / o la memoria.

Se ciò non è vero, dimostralo con uno snippet di codice. Si noti che l'accelerazione mediante vettorializzazione non conta. L'aumento di velocità deve venire da apply( tapply, sapply, ...) stesso.

Risposte:


152

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.


30
La maggior parte dei pacchetti multi core per R implementa anche il parallelismo attraverso la applyfamiglia di funzioni. Pertanto, strutturare i programmi in modo che utilizzino si applica consente loro di essere parallelizzati a un costo marginale molto piccolo.
Sharpie,

Sharpie - grazie per quello! Qualche idea per un esempio che lo mostri (su Windows XP)?
Tal Galili,

5
Suggerirei di guardare il 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 .
Sharpie,

1
@Sharpie ma nota che foreachda allora è diventato disponibile e sembra essere molto indagato su SO.
Ari B. Friedman,

1
@Shane, nella parte superiore della tua risposta, ti colleghi a un'altra domanda come esempio di un caso in cui 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?
flodel

70

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? ;-)


aah, così dolce :-) In realtà mi chiedevo se qualcuno avrebbe mai trovato la mia risposta piuttosto tardi.
Joris Meys,

1
Ordino sempre per "attivo". :) Non sono sicuro di come generalizzare la tua risposta; a volte *applyè più veloce. Ma penso che il punto più importante siano gli effetti collaterali (ho aggiornato la mia risposta con un esempio).
Shane,

1
Penso che applicare sia particolarmente più veloce quando si desidera applicare una funzione su diversi sottoinsiemi. Se esiste una soluzione di applicazione intelligente per un ciclo nidificato, immagino che anche la soluzione di applicazione sarà più veloce. Nella maggior parte dei casi, applicare non guadagna molta velocità, immagino, ma sono sicuramente d'accordo sugli effetti collaterali.
Joris Meys,

2
Questo è un po 'fuori tema, ma per questo esempio specifico, 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")])
dnlbrky,

12
Questo confronto è assurdo. 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.
eddi

47

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

Le scoperte originali non sembrano più essere vere. 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.
Cole

27

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.


4
Ma l'esempio di Shane è realistico in quanto la maggior parte del tempo viene normalmente impiegata nella funzione, non nel ciclo.
Hadley,

9
parla da solo ...:) ... Forse Shane è realistico in un certo senso, ma in quello stesso senso l'analisi è completamente inutile. Le persone si preoccuperanno della velocità del meccanismo di iterazione quando devono fare molte iterazioni, altrimenti i loro problemi sono comunque altrove. È vero per qualsiasi funzione. Se scrivo un peccato che impiega 0.001s e qualcun altro ne scrive uno che prende 0.002 a chi importa ?? Bene, non appena ne devi fare un mucchio, ti importa.
Giovanni

2
su un Xelon Intel a 12 core a 3 MHz, 64 bit, ottengo numeri abbastanza diversi da te - il ciclo for migliora considerevolmente: per i tuoi tre test, ottengo 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
naught101

2
Varia con OS e versione R ... e in senso assoluto CPU. Ho appena corso con 2.15.2 su Mac e ho ottenuto il sapply50% più lentamente rispetto fore lapplydue volte più veloce.
Giovanni

1
Nel tuo esempio, intendi impostare 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.
flodel

3

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

7
È importante notare che (per piccoli pezzi di codice) 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.
Michele,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.