¿La familia de aplicación de R es más que azúcar sintáctico?
...en cuanto al tiempo de ejecución y/o memoria.
Si esto no es cierto, pruébalo con un fragmento de código. Tenga en cuenta que la aceleración por vectorización no cuenta. La aceleración debe provenir del propio apply
( tapply
,, sapply
...).
Las apply
funciones en R no proporcionan un rendimiento mejorado con respecto a otras funciones de bucle (p. ej. for
). Una excepción a esto es lapply
que puede ser un poco más rápido porque hace más trabajo en código C que en R (consulte esta pregunta para ver un ejemplo de esto ).
Pero, en general, la regla es que se debe utilizar una función de aplicación por motivos de claridad, no por rendimiento .
Yo 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 se puede anular usando assign
o <<-
, pero eso 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:
Sólo 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:
En cuanto 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 sockets, para el cual no es necesario instalar software adicional; de lo contrario, necesitará algo como PVM o MPI (consulte la página de agrupación en clústeres 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 apply
las funciones se utilicen para la ejecución paralela ya que no tienen efectos secundarios . Cuando cambia el valor de una variable dentro de un for
bucle, se establece globalmente. Por otro lado, todas apply
las funciones se pueden usar en paralelo de manera segura porque los cambios son locales en la llamada a la función (a menos que intente usar assign
o <<-
, en cuyo caso puede introducir efectos secundarios). No hace falta decir que es fundamental tener cuidado con las variables locales frente a las globales, especialmente cuando se trata de ejecución paralela.
Editar:
Aquí hay un ejemplo trivial para demostrar la diferencia entre for
y *apply
en 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
Observe cómo el df
entorno principal se ve alterado por for
pero no *apply
.
A veces, la aceleración puede ser sustancial, como cuando hay que anidar bucles for para obtener el promedio basado en una agrupación de más de un factor. Aquí tienes dos enfoques que te 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 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é? ;-)
...y como acabo de escribir en otra parte, ¡vapply es tu amigo! ...es como sapply, pero también especificas el tipo de valor de retorno, lo 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
Actualización del 1 de enero de 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
He escrito en otra parte que un ejemplo como el de Shane realmente no enfatiza la diferencia en el rendimiento entre los distintos tipos de sintaxis de bucle porque todo el tiempo se emplea dentro de la función en lugar de enfatizar realmente el bucle. Además, el código compara injustamente un bucle for sin memoria con funciones de la familia de aplicación que devuelven un valor. Aquí hay un ejemplo ligeramente diferente que enfatiza este 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, puede aplicar funciones familiares mucho más que azúcar sintáctica.
(la simple eliminación de la lista de z es solo 0,2 s, por lo que la aplicación es mucho más rápida. Inicializar z en el bucle for es bastante rápido porque estoy dando el promedio de las últimas 5 de 6 ejecuciones, por lo que me muevo fuera del sistema. El tiempo sería apenas afecta las cosas)
Sin embargo, una cosa más a tener en cuenta es que existe otra razón para aplicar funciones familiares independientemente de su rendimiento, claridad o falta de efectos secundarios. Un for
bucle normalmente promueve poner tanto como sea posible dentro del bucle. 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 en sentido contrario. Muchas veces desea realizar múltiples operaciones con 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 una 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 con 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 bucle y un poco más lento que la función for
optimizada incorporada . tapply
No es porque vapply
sea mucho más rápido, for
sino porque solo realiza una operación en cada iteración del ciclo. En este código todo lo demás está vectorizado. En el bucle tradicional de Joris Meys for
se producen muchas operaciones (¿7?) en cada iteración y hay bastante configuración solo para que se ejecute. Tenga en cuenta también que es mucho más compacto que la for
versión anterior.
Cuando se aplican 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 incluso 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
y 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