functions - r apply function to each row
¿Los R''s aplican a la familia más que el azúcar sintáctico? (5)
... y como acabo de escribir en otro lado, ¡es tu amigo! ... es como sapply, pero también especifica el tipo de valor de retorno que lo hace mucho más rápido.
> 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
... con respecto 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 acelerar por vectorización no cuenta. La aceleración debe provenir de apply
( tapply
, sapply
, ...) en sí.
A veces, la aceleración puede ser considerable, como cuando tiene que anidar bucles for 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é? ;-)
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
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
He escrito en otro lugar que un ejemplo como el de Shane realmente no enfatiza la diferencia en el rendimiento entre los diversos tipos de sintaxis de bucle porque el tiempo se pasa todo dentro de la función en lugar de estresar el ciclo. Además, el código compara injustamente un bucle for sin memoria con funciones de familia 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, entonces aplicar las funciones familiares puede ser mucho más que azúcar sintáctica.
(la simple lista de z es solo 0.2s, entonces la aplicación es mucho más rápida. Inicializar la z en el ciclo for es bastante rápido porque estoy dando el promedio de las últimas 5 de 6 ejecuciones moviéndolo así fuera del sistema. apenas afecta las cosas)
Sin embargo, una cosa más a tener en cuenta es que existe otra razón para usar las funciones familiares de aplicación independientemente de su rendimiento, claridad o falta de efectos secundarios. Un bucle for
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 operaciones posibles). Las declaraciones de solicitud tienden a estar sesgadas de la otra manera. Muchas veces desea realizar múltiples operaciones en sus datos, varios de los cuales pueden ser vectorizados, pero algunos podrían no serlo. En R, a diferencia de otros lenguajes, es mejor separar esas operaciones y ejecutar las que no están vectorizadas en una sentencia de aplicación (o versión vectorizada de la función) y las que están vectorizadas como verdaderas operaciones de vector. Esto a menudo acelera enormemente el rendimiento.
Tomando el ejemplo de Joris Meys donde reemplaza un bucle for tradicional con una función R práctica, podemos usarlo para mostrar la eficiencia de escribir código de una manera más amigable para 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 for
y solo un poco más lento que la función optimizada tapply
. No es porque vapply
es mucho más rápido que for
sino porque solo está realizando una operación en cada iteración del ciclo. En este código, todo lo demás está vectorizado. En el bucle tradicional for
Joris Meys, muchas operaciones (7?) Se producen en cada iteración y hay bastante configuración para que se ejecute. Tenga en cuenta también cuánto más compacto es esto que for
versión.
Las funciones de apply
en R no proporcionan un rendimiento mejorado en comparación con 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 el código C que en R (ver esta pregunta para un ejemplo de esto ).
Pero, en general, la regla es que debe usar una función de aplicar para mayor claridad, no para el rendimiento .
Añadirí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 ser anulado usando assign
o <<-
, pero eso puede ser muy peligroso. Los efectos secundarios también dificultan la comprensión de un programa, 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
Editar 2:
En cuanto al uso de paquetes paralelos para R (p. Ej., Rpvm, rmpi, snow), estos generalmente proporcionan funciones familiares de apply
(incluso el paquete foreach
es esencialmente equivalente, a pesar del nombre). Aquí hay un ejemplo simple de la función sapply
en la snow
:
library(snow)
cl <- makeSOCKcluster(c("localhost","localhost"))
parSapply(cl, 1:20, get("+"), 3)
Este ejemplo utiliza un clúster de socket, para el cual no se necesita instalar ningún software adicional; de lo contrario, necesitará algo como PVM o MPI (vea la página de clustering de Tierney ). snow
tiene las siguientes funciones de aplicar:
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 funciones de apply
se usen para la ejecución en paralelo ya que no tienen efectos secundarios . Cuando cambia un valor de variable dentro de un bucle for
, se establece globalmente. Por otro lado, todas las funciones de apply
se pueden usar de forma segura en paralelo porque los cambios son locales a la llamada a la función (a menos que intente utilizar assign
o <<-
, en cuyo caso puede introducir efectos secundarios). Huelga 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
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 se modifica el df
en el entorno primario por for
pero no *apply
.