Tema 7 Tipos de datos: listas

En este tema seguimos analizando los principales tipos de datos de R, estudiando las listas. Una lista es similar a un vector, en el sentido de que puede almacenar una secuencia ordenada de elementos. La diferencia es que los elementos de un vector tienen que ser del mismo tipo, mientras que los de una lista pueden ser de distinto tipo. Esta diferencia hace que realmente sean dos tipos de datos con distintas aplicaciones. Las listas son muy flexibles, pues permiten almacenar cualquier cosa. Los vectores son más específicos, lo que los hace más eficientes y permite que ciertas operaciones, como la suma o la aplicación de operaciones relacionales, tengan sentido.

Las listas se usan principalmente en R para almacenar dos tipos de datos. En primer lugar, colecciones de elementos de un mismo tipo, pero de un tipo tal que no es posible almacenarlo en un vector. En segundo lugar, un conjunto de información heterogénea (de distinto tipo) en un único objeto. Un ejemplo de este segundo uso son los datos de una persona, que incluyen su nombre (cadena de caracteres), edad (entero), altura en metros (real), etcétera.

Las listas se implementan como ilustra la Figura 7.1. Una lista se representa como un vector para que el acceso a sus elementos sea eficiente. Para poder almacenar algo en un vector, todos sus elementos deben ser del mismo tipo, para que ocupen lo mismo en la memoria del ordenador. Puesto que los componentes de una lista pueden ser de distinto tipo, el vector almacena un puntero (una dirección de memoria) a donde realmente se guarda un componente. Los punteros son un tipo de dato y tienen un tamaño fijo. En la figura los punteros se representan mediante flechas azules. Observa cómo el tercer componente de la lista es una lista.

Implementación de una lista.
Implementación de una lista.

Para crear una lista se usa la función list, especificando los elementos o componentes de la lista como argumentos de la función:

l <- list("Juan", 23, FALSE, c(8, 6.5, 9))
l
## [[1]]
## [1] "Juan"
## 
## [[2]]
## [1] 23
## 
## [[3]]
## [1] FALSE
## 
## [[4]]
## [1] 8.0 6.5 9.0

Como ilustra el ejemplo, los componentes pueden ser de distinto tipo. Para algunas listas resulta más legible hacer que los distintos componentes tengan un nombre. Esto se puede realizar directamente al crear la lista o, a posteriori, con la función names:

names(l) <- c("Nombre", "Edad", "Casado", "Notas") # añade nombres
l
## $Nombre
## [1] "Juan"
## 
## $Edad
## [1] 23
## 
## $Casado
## [1] FALSE
## 
## $Notas
## [1] 8.0 6.5 9.0
punto2d <- list(x = 5, y = 6) # crea lista con nombres
punto2d
## $x
## [1] 5
## 
## $y
## [1] 6

Dado que una lista puede contener componentes de distintos tipos (incluso de tipo lista), no resulta fácil visualizarla en la consola. La función str produce una visualización algo más estructurada en la consola:

str(l)
## List of 4
##  $ Nombre: chr "Juan"
##  $ Edad  : num 23
##  $ Casado: logi FALSE
##  $ Notas : num [1:3] 8 6.5 9
str(punto2d)
## List of 2
##  $ x: num 5
##  $ y: num 6

La función str es genérica, por lo que puede aplicarse a cualquier objeto, no sólo a listas. Otra función útil para visualizar el contenido de objetos es View, que abre un panel con información del objeto consultado en la zona del editor de texto de RStudio (pruébala).

Para obtener el número de componentes de una lista se usa la función length:

length(l)
## [1] 4
length(punto2d)
## [1] 2

7.1 Indexación

Existen varias formas de indexar una lista, así que usaremos la que nos guste más, nos parezca más legible o sea apropiada al cálculo que queramos realizar. Los operadores para indexar una lista son tres: [], [[]] y $.

7.1.1 Indexación con []

La indexación de listas con [] admite las mismas posibilidades que con vectores. Recordemos las posibilidades:

l[c(1, 4)]  # indexación con un vector de números positivos
## $Nombre
## [1] "Juan"
## 
## $Notas
## [1] 8.0 6.5 9.0
l[-c(1, 4)] # indexación con un vector de números negativos
## $Edad
## [1] 23
## 
## $Casado
## [1] FALSE
l[c("Nombre", "Edad")] # indexación con vector de cadenas
## $Nombre
## [1] "Juan"
## 
## $Edad
## [1] 23
l[c(TRUE, FALSE, FALSE, TRUE)] # indexación con vector lógico
## $Nombre
## [1] "Juan"
## 
## $Notas
## [1] 8.0 6.5 9.0

Al indexar con [] se obtiene una lista con los componentes seleccionados.

7.1.2 Indexación con [[]]

Es importante observar que al indexar con [] se obtiene una lista, aunque sólo accedamos a un componente de la lista. Por ejemplo, supongamos que queremos sumarle 2 al componente x del objeto punto2d:

punto2d <- list(x = 5, y = 6)
punto2d["x"]
## $x
## [1] 5
typeof(punto2d["x"])
## [1] "list"
# punto2d["x"] + 2 # pruébalo en la consola y obtendrás un error

No es posible realizar la suma con [], porque no se puede aplicar el operador + a una lista. Cuando queramos acceder al contenido de un único componente de una lista para trabajar con él hay que usar el operador [[]] o el operador $:

punto2d[["x"]]
## [1] 5
typeof(punto2d[["x"]])
## [1] "double"
punto2d[["x"]] + 2
## [1] 7

Cuando usemos el operador [[]] habrá que utilizar un único índice:

l[[4]]          # accedemos al componente de índice 4
## [1] 8.0 6.5 9.0
l[["Notas"]]    # accedemos al componente de nombre Notas
## [1] 8.0 6.5 9.0
l[["Notas"]][1] # primera nota
## [1] 8

7.1.3 Indexación con $

Cuando una lista l tiene nombres asociados a sus componentes, las expresiones l[["Nombre"]] y l$Nombre son equivalentes:

l[["Notas"]]
## [1] 8.0 6.5 9.0
l$Notas
## [1] 8.0 6.5 9.0

Cuando tengamos un componente de una lista con un nombre muy largo, es posible especificarlo con el operador $ escribiendo únicamente los tres primeros caracteres del nombre (salvo que haya ambigüedad porque más de un nombre de componente empiece por esos tres caracteres):

l$Not  # equivale a l$Notas
## [1] 8.0 6.5 9.0

El operador $ es muy cómodo para acceder al contenido de un componente por su nombre.

7.2 Creación y eliminación de componentes

Es posible crear y eliminar componentes de una lista, veamos un ejemplo:

punto2d
## $x
## [1] 5
## 
## $y
## [1] 6
punto2d$z <- 10   # se añade un componente de nombre z
punto2d
## $x
## [1] 5
## 
## $y
## [1] 6
## 
## $z
## [1] 10
punto2d$z <- NULL # se elimina el componente de nombre z
punto2d
## $x
## [1] 5
## 
## $y
## [1] 6

A continuación se añade un componente usando indexación numérica:

punto2d[[3]] <- 9
punto2d
## $x
## [1] 5
## 
## $y
## [1] 6
## 
## [[3]]
## [1] 9

7.3 Las listas son un tipo de dato recursivo

Los componentes de una lista pueden ser de cualquier tipo, incluido el propio tipo lista. Un tipo de dato, como las listas, que admite elementos de su propio tipo se dice que es recursivo. Veamos un ejemplo de una lista con un componente de tipo lista:

fecha <- list(dia = 4, mes = "febrero", año = 2002)
alumno <- list(nombre = "Marta", nacimiento = fecha)
str(alumno)
## List of 2
##  $ nombre    : chr "Marta"
##  $ nacimiento:List of 3
##   ..$ dia: num 4
##   ..$ mes: chr "febrero"
##   ..$ año: num 2002

En este caso el componente nacimiento de la lista alumno es una lista. Veamos cómo acceder a sus campos:

alumno$nacimiento$año
## [1] 2002
alumno[["nacimiento"]][["año"]] # otra posibilidad
## [1] 2002

7.4 Funciones que devuelven listas

Muchas funciones de R devuelven información diversa sobre los cálculos que realizan. Como en R una función sólo puede devolver un objeto, la forma usual de devolver esta información es en una lista con distintos componentes, cada componente almacena algún tipo de dato sobre el cómputo realizado por la función. Veamos un ejemplo: la función hist calcula un histograma sobre una muestra y lo visualiza en la pantalla:

set.seed(10)
hist(rnorm(1000, 5, 3))

Pero hist no sólo visualiza el histograma, también devuelve una lista con información sobre sus características:

set.seed(10)
salida <- hist(rnorm(1000, 5, 3))

typeof(salida)
## [1] "list"
str(salida)
## List of 6
##  $ breaks  : num [1:12] -6 -4 -2 0 2 4 6 8 10 12 ...
##  $ counts  : int [1:11] 2 5 38 116 212 241 220 127 29 8 ...
##  $ density : num [1:11] 0.001 0.0025 0.019 0.058 0.106 ...
##  $ mids    : num [1:11] -5 -3 -1 1 3 5 7 9 11 13 ...
##  $ xname   : chr "rnorm(1000, 5, 3)"
##  $ equidist: logi TRUE
##  - attr(*, "class")= chr "histogram"

Los distintos componentes de la lista salida nos aportan información sobre el histograma construido. Así, el componente breaks nos indica que las cajas tienen rangos \([-6, -4]\), \([-4, -2]\), etc. El componente equidist nos indica que todas las cajas tienen la misma anchura. counts indica cuántos números caen en cada caja.

7.5 La función lapply

La función lapply permite aplicar una función a los distintos componentes de una lista. La salida de lapply es una lista. Por cada componente de la lista de entrada se genera un componente en la lista de salida con el resultado de aplicar la función al componente de la lista de entrada:

str(l)
## List of 4
##  $ Nombre: chr "Juan"
##  $ Edad  : num 23
##  $ Casado: logi FALSE
##  $ Notas : num [1:3] 8 6.5 9
lapply(l, length) # longitud de los distintos componentes de l
## $Nombre
## [1] 1
## 
## $Edad
## [1] 1
## 
## $Casado
## [1] 1
## 
## $Notas
## [1] 3

Si, como en este caso, la aplicación de la función produce elementos del mismo tipo (en concreto enteros), se puede usar la función sapply para obtener la salida en un vector:

sapply(l, length)
## Nombre   Edad Casado  Notas 
##      1      1      1      3

La función lapply también se puede aplicar a vectores:

v <- c(25, 4, 60)
lapply(v, sqrt)
## [[1]]
## [1] 5
## 
## [[2]]
## [1] 2
## 
## [[3]]
## [1] 7.745967
sapply(v, sqrt)
## [1] 5.000000 2.000000 7.745967

Para este ejemplo, sin embargo, sería más sencillo usar:

sqrt(v)
## [1] 5.000000 2.000000 7.745967

7.6 Listas multidimensionales

Por defecto las listas son unidimensionales, como los vectores. No obstante, se puede cambiar el atributo dim para convertir una lista unidimensional en multidimensional. En la práctica esta característica se usa muy pocas veces.

l <- list(1:3, "x", TRUE, 2.0)
dim(l) <-  c(2, 2)
l
##      [,1]      [,2]
## [1,] integer,3 TRUE
## [2,] "x"       2
l[[1, 1]]
## [1] 1 2 3

7.7 Ejercicios

  1. Crea una lista que almacene la dirección de una casa, el número de cuartos de baño que tiene, y la superficie en metros cuadrados de los distintos dormitorios

  2. Dada la siguiente lista, crea una lista que contenga el primer y último componente de la lista:

    l <- list(1, 2, "a", "b")
  3. Dada la siguiente lista, modifica su tercer componente para que valga TRUE, en lugar de "x":

    l <- list(a = 1, b = 2, c = "x", d = "y")
  4. Dada la siguiente lista:

    l <- list(TRUE, 3:1)

    añade un componente al final de la lista. Sugerencia: usa el índice length(l)+1

  5. Dada la lista: list(rbinom(10, 30, 0.5), runif(100, -8, -4)) usa lapply o sapply para calcular las medias de los componentes de la lista.

  6. Dada la lista:

    l <- list(-2, c(4, -1))

    aplícale la función abs a todos sus elementos.

  7. Dada la lista:

    l <- list(1:5, 3:11, 3:1, 2:9)

    selecciona aquellos componentes cuya longitud es mayor o igual a 5. Sugerencia: aplica primero length y luego usa indexado lógico.

7.8 Soluciones a los ejercicios

# Creación de una lista
list(dir = "Calle Mayor, 11, 1ºC", ncb = 2, sup = c(10, 9.7, 17.2))
## $dir
## [1] "Calle Mayor, 11, 1ºC"
## 
## $ncb
## [1] 2
## 
## $sup
## [1] 10.0  9.7 17.2
# Dada una lista obtener una lista con su primer y último componente
l <- list(1, 2, "a", "b")
l[c(1, length(l))] # una solución menos general es l[c(1, 4)]
## [[1]]
## [1] 1
## 
## [[2]]
## [1] "b"
# Modificar el tercer componente de una lista para que valga TRUE
l <- list(a = 1, b = 2, c = "x", d = "y")
l[[3]] <- TRUE # también vale l$c <- TRUE
l
## $a
## [1] 1
## 
## $b
## [1] 2
## 
## $c
## [1] TRUE
## 
## $d
## [1] "y"
# Añadir un componente al final de una lista
l <- list(TRUE, 3:1)
l[[length(l)+1]] <- "Mi lista"
l
## [[1]]
## [1] TRUE
## 
## [[2]]
## [1] 3 2 1
## 
## [[3]]
## [1] "Mi lista"
# Media de los componentes de una lista
l <- list(rbinom(10, 30, 0.5), runif(100, -8, -4))
sapply(l, mean)
## [1] 14.900000 -6.143121
# Aplicar la función abs a todos los elementos de una lista
l <- list(-2, c(4, -1))
lapply(l, abs)
## [[1]]
## [1] 2
## 
## [[2]]
## [1] 4 1
# Seleccionar aquellos componentes de una lista con longitud >= 5
l <- list(1:5, 3:11, 3:1, 2:9)
longitud <- sapply(l, length)
l[longitud >= 5]
## [[1]]
## [1] 1 2 3 4 5
## 
## [[2]]
## [1]  3  4  5  6  7  8  9 10 11
## 
## [[3]]
## [1] 2 3 4 5 6 7 8 9