Capítulo 10 Listas
Scripts usados:
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
: undata.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.
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).
## [[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