Capítulo 10 Listas

Veamos un pequeño resumen de los datos que conocemos:

  • vectores: colección de elementos de igual tipo. Pueden ser números, caracteres o valores lógicos, entre otros.

  • matrices: colección BIDIMENSIONAL de elementos de igual tipo e igual longitud.

  • data.frame: colección BIDIMENSIONAL de elementos de igual longitud pero de cualquier tipo, lo más parecido a lo que conocemos como una tabla en Excel.

Con todos estos ingredientes estamos preparados/as para ver el que probablemente sea el tipo de dato más importante en R: las listas.

Las listas son colecciones de variables de diferente tipo (ya lo teníamos con data.frame) pero además también de diferente longitud, con estructuras totalmente heterógeneas, todo guardado en la misma variable (incluso una lista puede tener dentro a su vez otra lista).

Vamos a crear nuestra primera lista con tres elementos: el nombre de nuestros padres/madres, nuestro lugar de nacimiento y edades de nuestros hermanos.

variable_1 <- c("Paloma", "Gregorio")
variable_2 <- "Madrid"
variable_3 <- c(25, 30, 26)

lista <- list("progenitores" = variable_1,
              "lugar_nacimiento" = variable_2,
              "edades_hermanos" = variable_3)
lista
## $progenitores
## [1] "Paloma"   "Gregorio"
## 
## $lugar_nacimiento
## [1] "Madrid"
## 
## $edades_hermanos
## [1] 25 30 26
length(lista)
## [1] 3

Si observas el objeto que hemos definido como lista, su longitud del es de 3 ya que tenemos guardados tres elementos

  • un vector de caracteres (de longitud 2)
  • un caracter (vector de longitud 1)
  • un vector de números (de longitud 3)

Tenemos guardados elementos de distinto tipo (algo que ya podíamos con los data.frame pero, además, de longitudes dispares).

dim(lista) # devolverá NULL al no tener dos dimensiones
## NULL
length(lista)
## [1] 3
class(lista) # de tipo lista
## [1] "list"

Si los juntásemos con un data.frame, al tener distinta longitud, obtendríamos un error: arguments imply differing number of rows.

data.frame("progenitores" = variable_1,
           "lugar_nacimiento" = variable_2,
           "edades_hermanos" = variable_3)
## Error in data.frame(progenitores = variable_1, lugar_nacimiento = variable_2, : arguments imply differing number of rows: 2, 1, 3

Para acceder a un elemento de la lista tenemos dos opciones:

  • Acceder por índice: con el operador [[i]] accedemos al elemento i-ésimo de la lista.

  • Acceder por nombre: con el operador $nombre_elemento accedemos al elemento por su nombre

# Accedemos por índice
lista[[1]]
## [1] "Paloma"   "Gregorio"
# Accedemos por nombre
lista$progenitores
## [1] "Paloma"   "Gregorio"

Dada su heterogeneidad y flexibilidad, para acceder a un elemento particular, las listas tienen una forma peculiar de acceder (con el corchete doble, en contraposición con el corchete simple que nos permite acceder a varios elementos a la vez)

# Varios elementos
lista[1:2]
## $progenitores
## [1] "Paloma"   "Gregorio"
## 
## $lugar_nacimiento
## [1] "Madrid"

Las listas nos dan tanta flexibilidad que es el formato de dato natural para guardar datos que no están estructurados, como pueden ser los datos almacenados en el registro de una persona.

Vamos a definir, por ejemplo, los datos que tendría un instituto de un alumno.

  • nacimiento: una fecha.
  • notas_insti: un data.frame.
  • teléfonos: vector de números.
  • nombre_padres: vector de texto.
# Fecha de nacimiento
fecha_nacimiento <- as_date("1989-09-10")

# Notas de asignaturas en primer y segundo parcial
notas <- data.frame("biología" = c(5, 7), "física" = c(4, 5),
                    "matemáticas" = c(8, 9.5))
row.names(notas) <- # Nombre a las filas
  c("primer_parcial", "segundo_parcial")

# Números de teléfono
tlf <- c("914719567", "617920765", "716505013")

# Nombres
padres <- c("Juan", "Julia")

# Guardamos TODO en una lista (con nombres de cada elemento)
datos <- list("nacimiento" = fecha_nacimiento,
              "notas_insti" = notas, "teléfonos" = tlf,
              "nombre_padres" = padres)
datos
## $nacimiento
## [1] "1989-09-10"
## 
## $notas_insti
##                 biología física matemáticas
## primer_parcial         5      4         8.0
## segundo_parcial        7      5         9.5
## 
## $teléfonos
## [1] "914719567" "617920765" "716505013"
## 
## $nombre_padres
## [1] "Juan"  "Julia"

Hemos creado una lista algo más compleja de 4 elementos, a los cuales podemos acceder por índice o por nombre.

datos[[1]]
## [1] "1989-09-10"
datos$nacimiento
## [1] "1989-09-10"
datos[[2]]
##                 biología física matemáticas
## primer_parcial         5      4         8.0
## segundo_parcial        7      5         9.5
datos$notas_insti
##                 biología física matemáticas
## primer_parcial         5      4         8.0
## segundo_parcial        7      5         9.5

Como hemos comentado, también podemos hacer listas con otras listas dentro, de forma que para acceder a cada nivel deberemos usar el operador [[]].

lista_de_listas <- list("lista_1" = datos[3:4], "lista_2" = datos[1:2])
names(lista_de_listas) # Nombres de los elementos del primer nivel
## [1] "lista_1" "lista_2"
names(lista_de_listas[[1]]) # Nombres de los elementos guardados en el primer elemento, que es a su vez una lista
## [1] "teléfonos"     "nombre_padres"
lista_de_listas[[1]][[1]] # Elemento 1 de la lista guardada como elemento 1 de la lista superior
## [1] "914719567" "617920765" "716505013"

¡Nos permiten guardar «datos n-dimensionales»!.

Es un formato muy habitual para devolver argumentos en funciones. Imagina que la función igualdad_nombres que hemos definido en el Ejercicio 4

 

WARNING: operaciones aritméticas con listas

Una lista no se puede vectorizar de forma inmediata, por lo cualquier operación aritmética aplicada a una lista dará error (para ello está disponible la función lapply(), o con las funciones del paquete purrr, cuyo uso avanzado corresponderá a otro manual pero que veremos a continuación una pequeña introducción).

datos <- list("a" = 1:5, "b" = 10:20)
datos / 2
## Error in datos/2: argumento no-numérico para operador binario
lapply(datos, FUN = function(x) { x / 2})
## $a
## [1] 0.5 1.0 1.5 2.0 2.5
## 
## $b
##  [1]  5.0  5.5  6.0  6.5  7.0  7.5  8.0  8.5  9.0  9.5 10.0

 

10.1 Introducción a purrr

Dada su heterogeneidad, una lista no se puede vectorizar de forma inmediata, por lo cualquier operación aritmética aplicada a una lista dará error, como vemos a continuación con un ejemplo sencillo.

datos <- list("a" = 1:5, "b" = 10:20)
datos / 2
## Error in datos/2: argumento no-numérico para operador binario

Para operar con listas, una de las opciones más habituales es hacer uso de la familia lapply(), con un funcionamiento similar a la familia apply() que ya hemos visto con matrices. Dicha función lapply() necesita como primer argumento la lista a la que aplicar la operación, y como segundo argumento FUN = ... la función que querramos aplicar a cada elemento de la lista.

lapply(datos, FUN = function(x) { x / 2})
## $a
## [1] 0.5 1.0 1.5 2.0 2.5
## 
## $b
##  [1]  5.0  5.5  6.0  6.5  7.0  7.5  8.0  8.5  9.0  9.5 10.0

Fíjate que la salida de lapply(), por defecto, siempre será otra lista de igual longitud (cada elemento será la función aplicada a cada elemento original de la lista).

 

Una opción más flexible y versatil de aparición «reciente» es hacer uso del paquete purrr del entorno tidyverse.

# install.packages("purrr")
library(purrr)

Dicho paquete contiene diversa funciones que pretenden imitar la programación funcional de otros lenguajes como Scala o la estrategia map-reduce de Hadoop (de Google).

La función más simple del paquete purrr es la función map(), que nos aplica una función vectorizada a cada uno de los elementos de una lista.

library(microbenchmark)
x <- 1:1000
y <- sqrt(x) # vectorizado

# bucle
for (i in 1:1000) { y[i] <- sqrt(x[i]) }

microbenchmark(sqrt(x), for (i in 1:1000) { y[i] <- sqrt(x[i]) }, times = 1e3)
## Unit: microseconds
##                                          expr      min        lq        mean
##                                       sqrt(x)    2.209    2.6315    3.541066
##  for (i in 1:1000) {     y[i] <- sqrt(x[i]) } 1462.342 1558.2770 1876.473790
##     median        uq        max neval cld
##     3.0585    3.5125     33.828  1000  a 
##  1616.7650 1784.7465 116439.979  1000   b

En vectores disponemos de una vectorización por defecto porque R realiza operaciones elemento a elemento. Con map() podemos «mapear» cada lista y aplicar la función elemento a elemento (si fuese el caso).

library(purrr)
x <- rep(list(1:2), 3)
x
## [[1]]
## [1] 1 2
## 
## [[2]]
## [1] 1 2
## 
## [[3]]
## [1] 1 2
# purrr
map(x, sqrt) 
## [[1]]
## [1] 1.000000 1.414214
## 
## [[2]]
## [1] 1.000000 1.414214
## 
## [[3]]
## [1] 1.000000 1.414214
# otro ejemplo
x <- list(rnorm(n = 1e3, mean = 0, sd = 1),
          rnorm(n = 1e3, mean = 2, sd = 1))
map(x, mean)
## [[1]]
## [1] -0.007485607
## 
## [[2]]
## [1] 2.012836
x <- rep(list(rnorm(n = 1e3, mean = 0, sd = 1)), 1000)

# Medimos tiempos entre map y lapply
microbenchmark(map(x, .f = function(x) { mean(x^2) }),
               lapply(x, FUN = function(x) { mean(x^2) }),
               times = 1e3)
## Unit: milliseconds
##                                            expr      min       lq     mean
##      map(x, .f = function(x) {     mean(x^2) }) 5.205882 6.333234 7.973593
##  lapply(x, FUN = function(x) {     mean(x^2) }) 4.999136 6.231217 7.868020
##    median       uq      max neval cld
##  7.098439 8.021562 124.1761  1000   a
##  6.963646 8.122574 120.8657  1000   a

 

Además de ser más legible y eficiente, el la vectorización de las listas con el paquete purrr nos permitirá decidir el formato de salida tras la operación (por ejemplo, en formato de vector con map_dbl() para números - en general - y map_int() para enteros), sin necesidad de hacer uso de unlist() (deshace el formato de lista original).

# lista de 1000 valores de dos normales
x <- list(rnorm(n = 1e3, mean = 0, sd = 1),
          rnorm(n = 1e3, mean = 2, sd = 1))

# media de cada una, devuelto en formato vector
map_dbl(x, mean)
## [1] -0.02453606  1.96544088

Una de las opciones más habituales, y una de las principales ventajas, es pasar como argumento un número en lugar de una función, lo cual nos devolverá el elemento i-ésimo de cada lista de forma inmediata.

c(x[[1]][3], x[[2]][3])
## [1] -1.1511723  0.8225336
map_dbl(x, 3) # equivalente a lo anterior 
## [1] -1.1511723  0.8225336

 

Aunque no es el objetivo de este manual introductorio profundizar en dicho paquete (te lo recomiendo), mencionar que además nos permite la opción de pasar más de un argumento, realizando operaciones binarias, con la función map2()

x <- list("a" = 1:3, "b" = 4:7)
y <- list("c" = c(-1, 4, 0), "b" = c(5, -4, -1, 2))

# dos listas como argumentos
map2(x, y, .f = function(x, y) { x^2 + y^2})
## $a
## [1]  2 20  9
## 
## $b
## [1] 41 41 37 53

10.2 📝 Ejercicios

Ejercicio 1: define una lista de 4 elementos de tipos distintos y accede al segundo de ellos (yo incluiré uno que sea un data.frame para que veas que en una lista cabe de todo).

  • Solución:
# Ejemplo: lista con texto, numérico, lógico y un data.frame
lista_ejemplo <- list("nombre" = "Javier", "cp" = 28019,
                      "soltero" = TRUE,
                      "notas" = data.frame("mates" = c(7.5, 8, 9),
                                           "lengua" = c(10, 5, 6)))
lista_ejemplo
## $nombre
## [1] "Javier"
## 
## $cp
## [1] 28019
## 
## $soltero
## [1] TRUE
## 
## $notas
##   mates lengua
## 1   7.5     10
## 2   8.0      5
## 3   9.0      6
# Longitud
length(lista_ejemplo)
## [1] 4
# Accedemos al elemento dos
lista_ejemplo[[2]]
## [1] 28019

 

Ejercicio 2: accede a los elementos que ocupan los lugares 1 y 4 de la lista definida anteriormente.

  • Solución:
# Accedemos al 1 y al 4
lista_ejemplo[c(1, 4)]
## $nombre
## [1] "Javier"
## 
## $notas
##   mates lengua
## 1   7.5     10
## 2   8.0      5
## 3   9.0      6

Otra opción es acceder con los nombres

# Accedemos al 1 y al 4
lista_ejemplo$nombre
## [1] "Javier"
lista_ejemplo$notas
##   mates lengua
## 1   7.5     10
## 2   8.0      5
## 3   9.0      6
lista_ejemplo[c("nombre", "notas")]
## $nombre
## [1] "Javier"
## 
## $notas
##   mates lengua
## 1   7.5     10
## 2   8.0      5
## 3   9.0      6

 

Ejercicio 3: define una lista de 4 elementos que contenga, en una sola variable, tu nombre, apellido, edad y si estás soltero/a.

  • Solución:
# Creamos lista: con lubridate calculamos la diferencia de años desde la fecha de nuestro nacimiento hasta hoy (sea cuando sea hoy)
lista_personal <- list("nombre" = "Javier",
                       "apellidos" = "Álvarez Liébana",
                       "edad" = 32,
                       "soltero" = TRUE)
lista_personal
## $nombre
## [1] "Javier"
## 
## $apellidos
## [1] "Álvarez Liébana"
## 
## $edad
## [1] 32
## 
## $soltero
## [1] TRUE