10.1 apply

apply aplica una función a todos los elementos de una matriz.

La estructura de esta función es la siguiente.

apply(X, MARGIN, FUN)

apply tiene tres argumentos:

  • X: Una matriz o un objeto que pueda coercionarse a una matriz, generalmente, un data frame.
  • MARGIN: La dimensión (margen) que agrupará los elementos de la matriz X, para aplicarles una función. Son identificadas con números, 1 son renglones y 2 son columnas.
  • FUN: La función que aplicaremos a la matriz X en su dimensión MARGIN.

10.1.1 ¿Qué es X

X es una matriz o cualquier otro objeto que sea posible coercionar a una matriz. Esto es, principalmente, vectores y data frames.

Recuerda que puedes coercionar objetos a matriz usando as.matrix() y puedes comprobar si un objeto es de esta clase con is.matrix().

# Creamos un data frame
mi_df <- data.frame(v1 = 1:3, v2 = 4:6)

mi_df
##   v1 v2
## 1  1  4
## 2  2  5
## 3  3  6
# Coerción a matriz
mi_matriz <- as.matrix(mi_df)

# Verificamos que sea matriz
is.matrix(mi_matriz)
## [1] TRUE
# Resultado
mi_matriz
##      v1 v2
## [1,]  1  4
## [2,]  2  5
## [3,]  3  6

Aunque también podemos coercionar listas y arrays a matrices, los resultados que obtenemos no siempre son apropiados para apply(), por lo que no es recomendable usar estos objetos como argumentos.

10.1.2 ¿Qué es MARGIN?

Recuerda que las matrices y los data frames están formadas por vectores y que estas estructuras tienen dos dimensiones, ordenadas en renglones y columnas. Esto lo vimos en en Matrices y arrays y Data frames.

Para MARGIN:

  • 1 es renglones.
  • 2 es columnas.

Por ejemplo, podemos usar apply() para obtener la sumatoria de los elementos de una matriz, por renglón.

Creamos una matriz de cuatro renglones.

matriz <- matrix(1:14, nrow = 4) 
## Warning in matrix(1:14, nrow = 4): data length [14] is not a sub-multiple
## or multiple of the number of rows [4]

Aplicamos apply(), dando la función sum() el argumento FUN, nota que sólo necesitamos el nombre de la función, sin paréntesis.

Por último, damos el argumento MARGIN = 1, para aplicar la función por renglón.

apply(X = matriz, MARGIN = 1, FUN = sum)
## [1] 28 32 22 26

Esto es equivalente a hacer lo siguiente.

sum(matriz[1, ])
## [1] 28
sum(matriz[2, ])
## [1] 32
sum(matriz[3, ])
## [1] 22
sum(matriz[4, ])
## [1] 26

Y naturalmente, es equivalente a hacer lo siguiente.

sum(vector_1)
## [1] NA
sum(vector_2)
## [1] NA
sum(vector_3)
## [1] 15
sum(vector_4)
## [1] 58

Estamos aplicando una función a cada elemento de nuestra matriz. Los elementos son los renglones. Cada renglón es un vector. Cada vector es usado como argumento de la función.

Si cambiamos el argumento MARGIN de MARGIN = 1 a MARGIN = 2, entonces la función se aplicará por columna.

apply(X = matriz, MARGIN = 2, FUN = sum)
## [1] 10 26 42 30

En este caso, la función sum() ha sido aplicado a cada elementos de nuestra matriz, los elementos son las columnas, y cada columna es un vector.

10.1.3 ¿Qué es FUN?

FUN es un argumento que nos pide el nombre de una función que se se aplicarla a todos los elementos de nuestra matriz.

El ejemplo de la sección anterior aplicamos las funciones mean() y sum() usando sus nombres, sin paréntesis, esto es, sin especificar argumentos.

Podemos dar como argumento cualquier nombre de función, siempre y cuando ésta acepte vectores como argumentos.

Probemos cambiando el argumento FUN. Usaremos la función mean() para obtener la media de cada renglón y de cada columna.

Aplicado a los renglones.

apply(matriz, 1, mean)
## [1] 7.0 8.0 5.5 6.5

Aplicado a las columnas

apply(matriz, 2, mean)
## [1]  2.5  6.5 10.5  7.5

Las siguientes llamadas a sd(), max() y quantile() se ejecutan sin necesidad de especificar argumentos.

# Desviación estándar
apply(matriz, 1, FUN = sd)
## [1] 5.163978 5.163978 4.434712 4.434712
# Máximo
apply(matriz, 1, FUN = max)
## [1] 13 14 11 12
# Cuantiles
apply(matriz, 1, FUN = quantile)
##      [,1] [,2] [,3] [,4]
## 0%      1    2  1.0  2.0
## 25%     4    5  2.5  3.5
## 50%     7    8  5.0  6.0
## 75%    10   11  8.0  9.0
## 100%   13   14 11.0 12.0

10.1.4 ¿Cómo sabe FUN cuáles son sus argumentos?

Recuerda que podemos llamar una función y proporcionar sus argumentos en orden, tal como fueron establecidos en su definición.

Por lo tanto, el primer argumento que espera la función, será la X del apply().

Para ilustrar esto, usaremos la función quantile(). Llama ?quantile en la consola para ver su documentación.

?quantile

quantile() espera siempre un argumento x, que debe ser un vector numérico, además tener varios argumentos adicionales.

  • probs es un vector numérico con las probabilidades de las que queremos extraer cuantiles.
  • na.rm, si le asignamos TRUE quitará de x los NA y NaN antes de realizar operaciones.
  • names, si le asignamos TRUE, hará que el objeto resultado de la función tenga nombres.
  • type espera un valor entre 1 y 9, para determinar el algoritmo usado para el cálculo de los cuantiles.

En orden, el primer argumento es x, el segundo probs, y así sucesivamente.

Cuando usamos quantile() en un apply(), el argumento x de la función será cada elemento de nuestra matriz. Es decir, los vectores como renglones o columnas de los que está constituida la matriz.

Esto funcionará siempre y cuando los argumentos sean apropiados para la función. Si proporcionamos un argumento inválido, la función no se ejecutará y apply fallará.

Por ejemplo, intentamos obtener cuantiles de las columnas de una matriz, en la que una de ellas es de tipo carácter.

Creamos una matriz.

matriz2 <- matrix(c(1:2, "a", "b"), nrow = 2)

# Resultado

Aplicamos la función y obtenemos un error.

apply(matriz2, 2, quantile)
## Error in (1 - h) * qs[i]: non-numeric argument to binary operator

Por lo tanto, apply sólo puede ser usado con funciones que esperan vectores como argumentos.

10.1.5 ¿Qué pasa si deseamos utilizar los demás argumentos de una función con apply?

En los casos en los que una función tiene recibe más de un argumento, asignamos los valores de estos del nombre de la función, separados por comas, usando sus propios nombres (a este procedimiento es al que se refiere el argumento ... descrito en la documentación de apply).

Supongamos que deseamos encontrar los cuantiles de un vector, correspondientes a las probabilidades .33 y .66. Esto es definido con el argumento probs de esta función.

Para ello, usamos quantile() y después de haber escrito el nombre de la función, escribimos el nombre del argumento probs y los valores que deseamos para este.

apply(X = matriz, MARGIN = 2, FUN = quantile, probs = c(.33, .66))
##     [,1] [,2]  [,3]  [,4]
## 33% 1.99 5.99  9.99  1.99
## 66% 2.98 6.98 10.98 12.78

Como podrás ver, hemos obtenido los resultados esperados.

Si además deseamos que el resultado aparezca sin nombres, entonces definimos el valor del argumento names de la misma manera.

apply(matriz, 2, quantile, probs = c(.33, .66), names = FALSE)
##      [,1] [,2]  [,3]  [,4]
## [1,] 1.99 5.99  9.99  1.99
## [2,] 2.98 6.98 10.98 12.78

De este modo es posible aplicar funciones complejas que aceptan múltiples argumentos, con la ventaja que usamos pocas líneas de código.

10.1.6 ¿Qué tipo de resultados devuelve apply?

En los ejemplos anteriores, el resultado de apply() en algunas ocasiones fue un vector y en otros fue una matriz.

Si aplicamos mean(), obtenemos como resultado un vector.

mat_media <- apply(matriz, 1, mean)

class(mat_media)
## [1] "numeric"

Pero si aplicamos quantile(), obtenemos una matriz.

mat_cuant <- apply(matriz, 1, quantile)

class(mat_cuant)
## [1] "matrix"

Este comportamiento se debe a que apply() nos devolverá objetos del mismo tipo que la función aplicada devuelve. Dependiendo de la función, será el tipo de objeto que obtengamos.

Sin embargo, este comportamiento puede causarte algunos problemas. En primer lugar, anterior te obliga a conocer de antemano el tipo del resultado que obtendrás, lo cual no siempre es fácil de determinar, en particular si las funciones que estás utilizando son poco comunes o tienen comportamientos poco convencionales.

Cuando estás trabajando en proyectos en los que el resultado de una operación será usado en operaciones posteriores, corres el riesgo de que en alguna parte del proceso, un apply() te devuelva un resultado que te impida continuar adelante.

Con algo de práctica es más o menos sencillo identificar problemas posibles con los resultados de apply(), pero es algo que debes tener en cuenta, pues puede explicar por qué tu código no funciona como esperabas.

En este sentido, lapply() tiene la ventaja de que siempre devuelve una lista.