¿La familia R aplica más que el azúcar sintáctico?


152

... con respecto al tiempo de ejecución y / o memoria.

Si esto no es cierto, pruébelo con un fragmento de código. Tenga en cuenta que la aceleración por vectorización no cuenta. El aumento de velocidad debe venir de apply( tapply, sapply, ...) en sí.

Respuestas:


152

Las applyfunciones en R no proporcionan un rendimiento mejorado sobre otras funciones de bucle (por ejemplo for). Una excepción a esto es lapplyque puede ser un poco más rápido porque funciona más en código C que en R (vea esta pregunta para ver un ejemplo de esto ).

Pero, en general, la regla es que debe usar una función de aplicación para mayor claridad, no para el rendimiento .

Agregaría a esto que las funciones de aplicación no tienen efectos secundarios , lo cual es una distinción importante cuando se trata de programación funcional con R. Esto puede anularse usando assigno <<-, pero puede ser muy peligroso. Los efectos secundarios también hacen que un programa sea más difícil de entender ya que el estado de una variable depende del historial.

Editar:

Solo para enfatizar esto con un ejemplo trivial que calcula recursivamente la secuencia de Fibonacci; Esto podría ejecutarse varias veces para obtener una medida precisa, pero el punto es que ninguno de los métodos tiene un rendimiento significativamente diferente:

> 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 

Edición 2:

Con respecto al uso de paquetes paralelos para R (por ejemplo, rpvm, rmpi, snow), estos generalmente proporcionan applyfunciones familiares (incluso el foreachpaquete es esencialmente equivalente, a pesar del nombre). Aquí hay un ejemplo simple de la sapplyfunción en snow:

library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)

Este ejemplo utiliza un clúster de socket, para el que no es necesario instalar ningún software adicional; de lo contrario, necesitará algo como PVM o MPI (consulte la página de agrupación de Tierney ). snowtiene las siguientes funciones de aplicación:

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

Tiene sentido que las applyfunciones se usen para la ejecución paralela ya que no tienen efectos secundarios . Cuando cambia un valor variable dentro de un forbucle, se establece globalmente. Por otro lado, todas las applyfunciones se pueden usar de forma segura en paralelo porque los cambios son locales a la llamada a la función (a menos que intente usar assigno <<-, en cuyo caso, puede introducir efectos secundarios). No es necesario decir que es fundamental tener cuidado con las variables locales frente a las globales, especialmente cuando se trata de una ejecución paralela.

Editar:

Aquí hay un ejemplo trivial para demostrar la diferencia entre fory en *applylo que respecta a los efectos secundarios:

> 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

Tenga dfen cuenta cómo se altera en el entorno primario forpero no *apply.


30
La mayoría de los paquetes multinúcleo para R también implementan la paralelización a través de la applyfamilia de funciones. Por lo tanto, la estructuración de programas para que utilicen aplicar les permite ser paralelizados a un costo marginal muy pequeño.
Sharpie

Sharpie, ¡gracias por eso! ¿Alguna idea para un ejemplo que muestre eso (en Windows XP)?
Tal Galili

55
Sugeriría mirar el snowfallpaquete y probar los ejemplos en su viñeta. snowfallse basa en el snowpaquete y abstrae los detalles de la paralelización aún más, lo que hace que sea muy simple ejecutar applyfunciones paralelizadas .
Sharpie

1
@Sharpie, pero tenga en cuenta que foreachdesde entonces está disponible y parece ser muy cuestionado sobre SO.
Ari B. Friedman

1
@Shane, en la parte superior de su respuesta, vincula a otra pregunta como un ejemplo de un caso donde lapplyes "un poco más rápido" que un forbucle. Sin embargo, no veo nada que lo sugiera. Solo menciona que lapplyes más rápido que sapply, lo cual es un hecho bien conocido por otras razones ( sapplyintenta simplificar la salida y, por lo tanto, tiene que hacer una gran cantidad de verificación de tamaño de datos y posibles conversiones). Nada relacionado con for. ¿Me estoy perdiendo de algo?
flodel

70

A veces, la aceleración puede ser sustancial, como cuando tienes que anidar for-loops para obtener el promedio basado en una agrupación de más de un factor. Aquí tiene dos enfoques que le dan exactamente el mismo resultado:

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)

Ambos dan exactamente el mismo resultado, siendo una matriz de 5 x 10 con los promedios y las filas y columnas con nombre. Pero :

> 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 

Ahí tienes. ¿Qué gané? ;-)


aah, qué dulce :-) En realidad me preguntaba si alguien encontraría alguna vez mi respuesta tardía.
Joris Meys

1
Siempre ordeno por "activo". :) No estoy seguro de cómo generalizar su respuesta; A veces *applyes más rápido. Pero creo que el punto más importante son los efectos secundarios (actualicé mi respuesta con un ejemplo).
Shane

1
Creo que aplicar es especialmente más rápido cuando quieres aplicar una función en diferentes subconjuntos. Si hay una solución de aplicación inteligente para un bucle anidado, supongo que la solución de aplicación también será más rápida. Supongo que en la mayoría de los casos aplicar no gana mucha velocidad, pero definitivamente estoy de acuerdo con los efectos secundarios.
Joris Meys

2
Esto es un poco fuera de tema, pero para este ejemplo específico, data.tablees aún más rápido y creo que "más fácil". 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
Esta comparación es absurda. tapplyes una función especializada para una tarea específica, que es por eso que es más rápido que un bucle. No puede hacer lo que puede hacer un bucle for (mientras que la applylata regular ). Estás comparando manzanas con naranjas.
eddi

47

... y como acabo de escribir en otra parte, ¡vapply es tu amigo! ... es como sapply, pero también especifica el tipo de valor de retorno que lo hace mucho más rápido.

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 

1 de enero de 2020 actualización:

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

Los hallazgos originales ya no parecen ser ciertos. forlos bucles son más rápidos en mi computadora con Windows 10 de 2 núcleos. Hice esto con 5e6elementos: un bucle fue de 2.9 segundos frente a 3.1 segundos vapply.
Cole

27

He escrito en otra parte que un ejemplo como el de Shane realmente no enfatiza la diferencia en el rendimiento entre los diversos tipos de sintaxis de bucle porque todo el tiempo se gasta dentro de la función en lugar de realmente estresar el bucle. Además, el código compara injustamente un bucle for sin memoria con funciones familiares de aplicación que devuelven un valor. Aquí hay un ejemplo ligeramente diferente que enfatiza el 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 

Si planea guardar el resultado, aplicar las funciones familiares puede ser mucho más que azúcar sintáctica.

(la simple eliminación de la lista de z es solo 0.2s, por lo que el lapply es mucho más rápido. Inicializar la z en el bucle for es bastante rápido porque estoy dando el promedio de las últimas 5 de 6 carreras, de modo que me muevo fuera del sistema. apenas afecta las cosas)

Sin embargo, una cosa más a tener en cuenta es que hay otra razón para usar las funciones familiares independientemente de su rendimiento, claridad o falta de efectos secundarios. Un forciclo típicamente promueve poner lo más posible dentro del ciclo. Esto se debe a que cada bucle requiere la configuración de variables para almacenar información (entre otras posibles operaciones). Las declaraciones de aplicación tienden a estar sesgadas al revés. Muchas veces desea realizar múltiples operaciones en sus datos, varias de las cuales se pueden vectorizar pero otras no. En R, a diferencia de otros lenguajes, es mejor separar esas operaciones y ejecutar las que no están vectorizadas en una declaración de aplicación (o versión vectorizada de la función) y las que están vectorizadas como operaciones vectoriales verdaderas. Esto a menudo acelera enormemente el rendimiento.

Tomando el ejemplo de Joris Meys donde reemplaza un bucle for tradicional con una práctica función R, podemos usarlo para mostrar la eficiencia de escribir código de una manera más amigable R para una aceleración similar sin la función especializada.

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

Esto termina siendo mucho más rápido que el forbucle y solo un poco más lento que la tapplyfunción optimizada incorporada. No es porque vapplysea ​​mucho más rápido que forsino porque solo está realizando una operación en cada iteración del bucle. En este código, todo lo demás está vectorizado. En el forbucle tradicional de Joris Meys, se producen muchas operaciones (7?) En cada iteración y hay bastante configuración solo para que se ejecute. Tenga en cuenta también cuánto más compacto es esto que la forversión.


44
Pero el ejemplo de Shane es realista en la que la mayoría de las veces se suele gastar en la función, no en el bucle.
hadley 03 de

9
habla por ti mismo ...:) ... Tal vez el de Shane sea realista en cierto sentido, pero en ese mismo sentido el análisis es completamente inútil. Las personas se preocuparán por la velocidad del mecanismo de iteración cuando tengan que hacer muchas iteraciones, de lo contrario, sus problemas están en otra parte de todos modos. Es cierto de cualquier función. Si escribo un pecado que toma 0.001s y alguien más escribe uno que toma 0.002 ¿a quién le importa? Bueno, tan pronto como tengas que hacer un montón de ellas, te importará.
John

2
en un Intel Xeon de 12 núcleos de 3 GHz, 64 bits, obtengo números bastante diferentes para usted: el bucle for mejora considerablemente: para sus tres pruebas, obtengo 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528, y vapply es aún mejor:1.19 0.00 1.19
naught101

2
Varía con el sistema operativo y la versión R ... y en un sentido absoluto de la CPU. Acabo de correr con 2.15.2 en Mac y obtuve un sapply50% más lento fory el lapplydoble de rápido.
John

1
En su ejemplo, quiere decir establecer yen 1:1e6, no numeric(1e6)(un vector de ceros). Tratando de asignar foo(0)a z[0]una y otra no ilustra bien un típico foruso bucle. De lo contrario, el mensaje es perfecto.
Flodel

3

Al aplicar funciones sobre subconjuntos de un vector, tapplypuede ser bastante más rápido que un bucle for. Ejemplo:

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, sin embargo, en la mayoría de las situaciones no proporciona ningún aumento de velocidad, y en algunos casos puede ser mucho más 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

Pero para estas situaciones tenemos colSumsyrowSums :

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

77
Es importante tener en cuenta que (para pequeños fragmentos de código) microbenchmarkes mucho más preciso que system.time. Si intenta comparar system.time(f3(mat))y system.time(f4(mat))obtendrá resultados diferentes casi cada vez. A veces, solo una prueba de referencia adecuada puede mostrar la función más rápida.
Michele
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.