10.2 lapply

lapply() es un caso especial de apply(), diseñado para aplicar funciones a todos los elementos de una lista. La l de su nombre se refiere, precisamente, a lista.

lapply() intentará coercionar a una lista el objeto que demos como argumento y después aplicará una función a todos sus elementos.

lapply siempre nos devolverá una lista como resultado. A diferencia de apply, sabemos que siempre obtendremos un objeto de tipo lista después de aplicar una función, sin importar cuál función sea.

Dado que en R todas las estructuras de datos pueden coercionarse a una lista, lapply() puede usarse en un número más amplio de casos que apply(), además de que esto nos permite utilizar funciones que aceptan argumentos distintos a vectores.

La estructura de esta función es:

lapply(X, FUN)

En donde:

  • X es una lista o un objeto coercionable a una lista.
  • FUN es la función a aplicar.

Estos argumentos son idéntico a los de apply(), pero a diferencia aquí no especificamos MARGIN, pues las listas son estructuras con una unidimensionales, que sólo tienen largo.

10.2.1 Usando lapply()

Probemos lapply() aplicando una función a un data frame. Usaremos el conjunto de datos trees, incluido por defecto en R base.

trees contiene datos sobre el grueso, alto y volumen de distinto árboles de cerezo negro. Cada una de estas variables está almacenada en una columna del data frame.

Veamos los primeros cinco renglones de trees.

trees[1:5, ]
##   Girth Height Volume
## 1   8.3     70   10.3
## 2   8.6     65   10.3
## 3   8.8     63   10.2
## 4  10.5     72   16.4
## 5  10.7     81   18.8

Aplicamos la función mean(), usando su nombre.

lapply(X = trees, FUN = mean)
## $Girth
## [1] 13.24839
## 
## $Height
## [1] 76
## 
## $Volume
## [1] 30.17097

Dado que un data frame está formado por columnas y cada columna es un vector atómico, cuando usamos lapply() , la función es aplicada a cada columna. lapply(), a diferencia de apply() no puede aplicarse a renglones.

En este ejemplo, obtuvimos la media de grueso (Girth), alto (Height) y volumen (Volume), como una lista.

Verificamos que la clase de nuestro resultado es una lista con class().

arboles <- lapply(X = trees, FUN = mean)

class(arboles)
## [1] "list"

Esto es muy conveniente, pues la recomendación para almacenar datos en un data frame es que cada columna represente una variable y cada renglón un caso (por ejemplo, el enfoque tidy de Wickham (2014)). Por lo tanto, con lapply() podemos manipular y transformar datos, por variable.

Al igual que con apply(), podemos definir argumentos adicionales a las funciones que usemos, usando sus nombres, después del nombre de la función.

lapply(X = trees, FUN = quantile, probs = .8)
## $Girth
##  80% 
## 16.3 
## 
## $Height
## 80% 
##  81 
## 
## $Volume
##  80% 
## 42.6

Si usamos lapply con una matriz, la función se aplicará a cada celda de la matriz, no a cada columna.

Creamos una matriz.

matriz <- matrix(1:9, ncol = 3)

# Resultado
matriz
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9

Llamamos a lapply().

lapply(matriz, quantile, probs = .8)
## [[1]]
## 80% 
##   1 
## 
## [[2]]
## 80% 
##   2 
## 
## [[3]]
## 80% 
##   3 
## 
## [[4]]
## 80% 
##   4 
## 
## [[5]]
## 80% 
##   5 
## 
## [[6]]
## 80% 
##   6 
## 
## [[7]]
## 80% 
##   7 
## 
## [[8]]
## 80% 
##   8 
## 
## [[9]]
## 80% 
##   9

Para usar una matriz con lapply() y que la función se aplique a cada columna, primero la coercionamos a un data frame con la función as.data.frame()

lapply(as.data.frame(matriz), quantile, probs = .8)
## $V1
## 80% 
## 2.6 
## 
## $V2
## 80% 
## 5.6 
## 
## $V3
## 80% 
## 8.6

Si deseamos aplicar una función a los renglones de una matriz, una manera de lograr es transponer la matriz con t() y después coercionar a un data frame.

matriz_t <- t(matriz)

lapply(as.data.frame(matriz_t), quantile, probs = .8)
## $V1
## 80% 
## 5.8 
## 
## $V2
## 80% 
## 6.8 
## 
## $V3
## 80% 
## 7.8

Con vectores como argumento, lapply() aplicará la función a cada elementos del vector, de manera similar a una vectorización de operaciones.

Por ejemplo, usamos lapply() para obtener la raíz cuadrada de un vector numérico del 1 al 4, con la función sqrt().

mi_vector <- 1:4

lapply(mi_vector, sqrt)
## [[1]]
## [1] 1
## 
## [[2]]
## [1] 1.414214
## 
## [[3]]
## [1] 1.732051
## 
## [[4]]
## [1] 2

10.2.2 Usando lapply() en lugar de un bucle for

En muchos casos es posible reemplazar un bucle for() por un lapply().

De hecho, lapply() está haciendo lo mismo que un for(), está iterando una operación en todos los elementos de una estructura de datos.

Por lo tanto, el siguiente código con un for()

mi_vector <- 6:12
resultado <- NULL
posicion <- 1

for(numero in mi_vector) {
  resultado[posicion] <- sqrt(numero)
  posicion <- posicion + 1
}

resultado
## [1] 2.449490 2.645751 2.828427 3.000000 3.162278 3.316625 3.464102

… nos dará los mismos resultados que el siguiente código con lapply().

resultado <- NULL

resultado <- lapply(mi_vector, sqrt)

resultado
## [[1]]
## [1] 2.44949
## 
## [[2]]
## [1] 2.645751
## 
## [[3]]
## [1] 2.828427
## 
## [[4]]
## [1] 3
## 
## [[5]]
## [1] 3.162278
## 
## [[6]]
## [1] 3.316625
## 
## [[7]]
## [1] 3.464102

El código con lapply() es mucho más breve y más sencillo de entender, al menos para otros usuarios de R.

El inconveniente es que obtenemos una lista como resultado en lugar de un vector, pero eso es fácil de resolver usando la función as.numeric() para hacer coerción a tipo numérico.

as.numeric(resultado)
## [1] 2.449490 2.645751 2.828427 3.000000 3.162278 3.316625 3.464102

El siguiente código es la manera en la que usamos for() si deseamos aplicar una función a todas sus columnas, tiene algunas partes que no hemos discutido, pero es sólo para ilustrar la diferencia simplemente usar trees_max <- lapply(trees, max).

trees_max <- NULL
i <- 1
columnas <- ncol(trees)

for(i in 1:columnas) {
  trees_max[i] <- max(trees[, i])
  i <- i +1
}

trees_max
## [1] 20.6 87.0 77.0

10.2.3 Usando lapply con listas

Hasta hora hemos hablado de usar lapply() con objetos que pueden coercionarse a una lista, pero ¿qué pasa si usamos esta función con una lista que contiene a otros objetos?

Pues la función se aplicará a cada uno de ellos. Por lo tanto, así podemos utilizar funciones que acepten todo tipo de objetos como argumento. Incluso podemos aplicar funciones a listas recursivas, es decir, listas de listas.

Por ejemplo, obtendremos el coeficiente de correlación de cuatro data frames contenidos en una sola lista. Esto no es posible con apply(), porque sólo podemos usar funciones que aceptan vectores como argumentos, pero con lapply() no es ningún problema.

Empezaremos creando una lista de data frames. Para esto, usaremos las función rnorm(), que genera números al azar y set.seed(), para que obtengas los mismos resultados aquí mostrados.

rnorm() creara n números al azar (pseudoaleatorios, en realidad), sacados de una distribución normal con media 0 y desviación estándar 1. set.seed() es una función que “fija” los resultados de una generación de valores al azar. Cada que ejecutas rnorm() obtienes resultados diferentes, pero si das un número como argumento seed a set.seed(), siempre obtendrás los mismos números.

# Fijamos seed
set.seed(seed = 2018)

# Creamos una lista con tres data frames dentro
tablas <- list(
  df1 = data.frame(a = rnorm(n = 5), b = rnorm(n = 5), c = rnorm(n = 5)),
  df2 = data.frame(d = rnorm(n = 5), e = rnorm(n = 5), f = rnorm(n = 5)),
  df3 = data.frame(g = rnorm(n = 5), h = rnorm(n = 5), i = rnorm(n = 5))
)

# Resultado
tablas
## $df1
##             a          b          c
## 1 -0.42298398 -0.2647112 -0.6430347
## 2 -1.54987816  2.0994707 -1.0300287
## 3 -0.06442932  0.8633512  0.7124813
## 4  0.27088135 -0.6105871 -0.4457721
## 5  1.73528367  0.6370556  0.2489796
## 
## $df2
##            d          e          f
## 1 -1.0741940  1.2638637 -0.2401222
## 2 -1.8272617  0.2501979 -1.0586618
## 3  0.0154919  0.2581954  0.4194091
## 4 -1.6843613  1.7855342 -0.2709566
## 5  0.2044675 -1.2197058 -0.6318248
## 
## $df3
##            g          h          i
## 1 -0.2284119 -0.4897908 -0.3594423
## 2  1.1786797  1.4105216 -1.2995363
## 3 -0.2662727 -1.0752636 -0.8698701
## 4  0.5281408  0.2923947  1.0543623
## 5 -1.7686592 -0.2066645 -0.1486396

Para obtener el coeficiente de correlación usaremos la función cor().

Esta función acepta como argumento una data frame o una matriz. Con este objeto, calculará el coeficiente de correlación R de Pearson existente entre cada una de sus columnas. Como resultado obtendremos una matriz de correlación.

Por ejemplo, este es el resultado de aplicar cor() a iris.

cor(iris[1:4])
##              Sepal.Length Sepal.Width Petal.Length Petal.Width
## Sepal.Length    1.0000000  -0.1175698    0.8717538   0.8179411
## Sepal.Width    -0.1175698   1.0000000   -0.4284401  -0.3661259
## Petal.Length    0.8717538  -0.4284401    1.0000000   0.9628654
## Petal.Width     0.8179411  -0.3661259    0.9628654   1.0000000

Con lapply aplicaremos cor() a cada uno de los data frames contenidos en nuestra lista. El resultado será una lista de matrices de correlaciones.

Esto lo logramos con una línea de código.

lapply(X = tablas, FUN = cor)
## $df1
##            a          b          c
## a  1.0000000 -0.4427336  0.6355358
## b -0.4427336  1.0000000 -0.1057007
## c  0.6355358 -0.1057007  1.0000000
## 
## $df2
##            d          e         f
## d  1.0000000 -0.6960942 0.4709283
## e -0.6960942  1.0000000 0.2624429
## f  0.4709283  0.2624429 1.0000000
## 
## $df3
##            g          h          i
## g  1.0000000  0.6228793 -0.1472657
## h  0.6228793  1.0000000 -0.1211321
## i -0.1472657 -0.1211321  1.0000000

De esta manera puedes manipular información de múltiples data frames, matrices o listas con muy pocas líneas de código y, en muchos casos, más rápidamente que con las alternativas existentes.

Finalmente, si asignamos los resultados de las última operación a un objeto, podemos usarlos y manipularlos de la misma manera que cualquier otra lista.

correlaciones <- lapply(tablas, cor)

# Extraemos el primer elemento de la lista
correlaciones[[1]]
##            a          b          c
## a  1.0000000 -0.4427336  0.6355358
## b -0.4427336  1.0000000 -0.1057007
## c  0.6355358 -0.1057007  1.0000000