¿La familia de aplicación de R es más que azúcar sintáctico?

Resuelto steffen asked hace 54 años • 5 respuestas

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

steffen avatar Jan 01 '70 08:01 steffen
Aceptado

Las applyfunciones en R no proporcionan un rendimiento mejorado con respecto a otras funciones de bucle (p. ej. for). Una excepción a esto es lapplyque 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 assigno <<-, 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 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 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 ). 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 applylas funciones se utilicen para la ejecución paralela ya que no tienen efectos secundarios . Cuando cambia el valor de una variable dentro de un forbucle, se establece globalmente. Por otro lado, todas applylas 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 assigno <<-, 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 fory *applyen 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 dfentorno principal se ve alterado por forpero no *apply.

Shane avatar Feb 16 '2010 20:02 Shane

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

Joris Meys avatar Aug 27 '2010 12:08 Joris Meys

...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
Tommy avatar Mar 23 '2011 22:03 Tommy

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 forbucle 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 foroptimizada incorporada . tapplyNo es porque vapplysea mucho más rápido, forsino 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 forse 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 forversión anterior.

John avatar Feb 02 '2011 14:02 John

Cuando se aplican 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 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 colSumsy 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
Michele avatar Apr 10 '2013 17:04 Michele