Respuestas:
Las apply
funciones en R no proporcionan un rendimiento mejorado sobre otras funciones de bucle (por ejemplo for
). Una excepción a esto es lapply
que 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 assign
o <<-
, 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 apply
funciones familiares (incluso el foreach
paquete es esencialmente equivalente, a pesar del nombre). Aquí hay un ejemplo simple de la sapply
funció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 ). snow
tiene 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 apply
funciones se usen para la ejecución paralela ya que no tienen efectos secundarios . Cuando cambia un valor variable dentro de un for
bucle, se establece globalmente. Por otro lado, todas las apply
funciones 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 assign
o <<-
, 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 for
y en *apply
lo 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 df
en cuenta cómo se altera en el entorno primario for
pero no *apply
.
snowfall
paquete y probar los ejemplos en su viñeta. snowfall
se basa en el snow
paquete y abstrae los detalles de la paralelización aún más, lo que hace que sea muy simple ejecutar apply
funciones paralelizadas .
foreach
desde entonces está disponible y parece ser muy cuestionado sobre SO.
lapply
es "un poco más rápido" que un for
bucle. Sin embargo, no veo nada que lo sugiera. Solo menciona que lapply
es más rápido que sapply
, lo cual es un hecho bien conocido por otras razones ( sapply
intenta 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?
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é? ;-)
*apply
es más rápido. Pero creo que el punto más importante son los efectos secundarios (actualicé mi respuesta con un ejemplo).
data.table
es 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")])
tapply
es 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 apply
lata regular ). Estás comparando manzanas con naranjas.
... 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
for
los bucles son más rápidos en mi computadora con Windows 10 de 2 núcleos. Hice esto con 5e6
elementos: un bucle fue de 2.9 segundos frente a 3.1 segundos vapply
.
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 for
ciclo 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 for
bucle y solo un poco más lento que la tapply
función optimizada incorporada. No es porque vapply
sea mucho más rápido que for
sino 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 for
bucle 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 for
versión.
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
sapply
50% más lento for
y el lapply
doble de rápido.
y
en 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 for
uso bucle. De lo contrario, el mensaje es perfecto.
Al aplicar funciones sobre subconjuntos de un vector, tapply
puede 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 colSums
yrowSums
:
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
es 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.
apply
familia 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.