4  Listas

Las tablas son contenedores de información estructurada: las columnas son del mismo tipo, todas tienen la misma longitud, etc. Gran parte de los datos con los que se trabaja habitualmente son estructurados, palabra que, en la jerga, significa que admiten una representación tabular.

Sin embargo, cada vez es más habitual trabajar directamente con información desestructurada. Particularmente, en ciencia de datos. Eso justifica el uso de las listas, que pueden definirse como contenedores genéricos de información desestructurada.

SQL, SAS, Matlab y otros entornos y lenguajes de programación usados en el análisis de datos utilizan fundamentalmente estructuras de datos pensados para información estructurada: tablas de distintos tipos, matrices, etc. Por eso, solo pueden utilizarse para procesar información desestructurada utilizando artificios ad hoc. Una de las ventajas (compartida con Python) de R sobre ellos es, precisamente, que no está sometido a esa restricción. Por eso puede aplicarse de manera natural en un rango más extenso de aplicaciones.

En esta sección investigaremos algunos de los usos de las listas y aprenderemos a crearlas, consultarlas y manipularlas.

4.1 Algunos usos de las listas

Las listas son objetos de R que ya hemos usado antes implícitamente. Por ejemplo, iris, como todas las tablas en R, es una lista.

is.list(iris)
[1] TRUE

Es conveniente recordar que en R las tablas son esencialmente listas de vectores de la misma longitud1. Aunque el anterior es el principal uso de las listas en R, también se emplean como contenedores genéricos de información. Por ejemplo, en la sección anterior hemos construido un modelo,

datos <- as.data.frame(UCBAdmissions)
datos$Admit <- datos$Admit == "Admitted"
modelo_con_dept <- glm(Admit ~ Gender + Dept,
                       data = datos, weights = Freq,
                       family = binomial())

La representación interna en R de este modelo es una lista:

is.list(modelo_con_dept)
[1] TRUE
length(modelo_con_dept)
[1] 30

Ejercicio 4.1 Explora modelo_con_dept con las funciones names y str.

En efecto, un modelo estadístico en R es una colección de objetos diversos (residuos, coeficientes, etc. representados en forma de valores atómicos, vectores o tablas) que lo resumen. Las listas son estructuras de datos que encapsulan toda esa información heterogénea.

Pero una lista también puede contener otras listas que, a su vez, pudieran contener otras, etc. Es decir, las listas permiten almacenar árboles de información. Los árboles de información son cada vez más importantes en las aplicaciones. De hecho, los ficheros .xml o .json disponen su información en árboles, como muestra el siguiente ejemplo de un fichero .json muy simple simple:

{
    "id": "0001",
    "type": "tarta",
    "name": "Tarta de zanahoria",
    "image":
        {
            "url": "images/tarta0001.jpg",
            "width": 200,
            "height": 200
        },
    "thumbnail":
        {
            "url": "images/thumbnails/tarta0001.jpg",
            "width": 32,
            "height": 32
        }
}

El fichero anterior tiene cinco elementos, dos de los cuales, image y thumbnail, contienen a su vez sendas listas de tres elementos (con identificadores url, width y height).

La representación natural en R de esta información es una lista con cinco elementos, dos de los cuales serán sublistas con tres elementos.

Ejercicio 4.2 Guarda la salida del test de Student del capítulo anterior como un objeto. ¿De qué clase es? Examínalo con las funciones anteriores.

4.2 Exploración y manipulación de listas

La función length aplicada a una lista devuelve el número de elementos que contiene. Por eso

length(iris)

es el número de columnas de iris.

Esto es un poco contraintuitivo para quienes se han acostumbrado a considerar la longitud de una tabla como su número de filas.

Es el momento de introducir formalmente el operador $, que ya utilizamos en capítulos previos para extraer columnas de una tabla. Ahora vamos a reconocerlo propiamente como un operador de listas.

modelo_con_dept$coefficients
 (Intercept) GenderFemale        DeptB        DeptC        DeptD        DeptE 
  0.58205140   0.09987009  -0.04339793  -1.26259802  -1.29460647  -1.73930574 
       DeptF 
 -3.30648006 

Además de $, las listas disponen de otro operador para extraer elementos, los dobles corchetes, [[]], que funcionan de manera parecida a $. Se parecen en que solo pueden extraer un único elemento y lo complementan porque:

  • Admite un número como argumento (y $ no); es decir, permite extraer el elemento n-ésimo de una lista. P.e., iris[[3]].
  • Permite extraer elementos cuyo nombre contiene un espacio (como ocurre en ocasiones con los nombres de columnas de tablas importadas de fuentes externas). Por ejemplo (hipotético), iris[["Petal Length"]].

La función list permite crear listas. Una vez creada una lista se le pueden añadir campos adicionales2.

mi_lista <- list(a = 1:3, b = c("hola", "adiós"))
mi_lista$z <- matrix(1:4, 2, 2)

Ejercicio 4.3 ¿Qué función serviría para concatenar dos listas? ¿Puedes poner un ejemplo?

Ejercicio 4.4 ¿Cómo borrarías un elemento de una lista? Recuerda cómo se hacía con tablas (que no dejan de ser listas).

Ejercicio 4.5 ¿Qué crees que pasaría si haces mi_lista[1:2]?

Ejercicio 4.6 Compara mi_lista[2] y mi_lista[[2]]. ¿Cuál es la diferencia? Pista: puede ayudarte a entender la diferencia el aplicarles la función is.list.

Ejercicio 4.7 ¿Cómo se crea una lista vacía?

Ejercicio 4.8 Si escribes lm en la consola de R, se muestra el código de dicha función. Hazlo y examina la última parte. Comprueba que no hace otra cosa que añadir progresivamente elementos a una lista, la que define el modelo, para devolverla al final.

4.3 Las funciones lapply, sapply y split

La función lapply está estrechamente relacionada con las listas. Aunque después, en la sección de programación, la trataremos de nuevo, es conveniente familiarizarse con ella cuanto antes.

La función lapply es una función de orden superior, como tapply, que aplica una función a cada elemento de una lista o vector. Por ejemplo,

lapply(airquality[, 1:4], mean, na.rm = TRUE)

calcula la media de cada una de las cuatro primeras columnas de airquality. El argumento adicional, na.rm = TRUE, igual que como ocurría con tapply, se pasa a la función mean.

La función lapply opera sobre una lista o vector y devuelve, siempre, una lista. Está relacionada con la función sapply, que es una versión simplificada de lapply. Es decir, sapply aplica la función lapply y estudia la salida. Cuando entiende que dicha salida admite una representación menos aparatosa que una lista, la simplifica. Por ejemplo,

sapply(airquality[, 1:4], mean, na.rm = TRUE)
     Ozone    Solar.R       Wind       Temp 
 42.129310 185.931507   9.957516  77.882353 

En el caso anterior, en lugar de una lista, como hace lapply, sapply devuelve una representación más natural: un vector de números.

Tanto lapply como sapply son maps, i.e., funciones que aplican una función a cada elemento de una lista o vector. En muchos casos habituales, no hace falta usar estas funciones porque muchas, como ya hemos visto, están vectorizadas. Por ejemplo,

sqrt(1:5)
[1] 1.000000 1.414214 1.732051 2.000000 2.236068

Pero si sqrt no estuviese vectorizada, aún podríamos aplicársela a un vector haciendo, por ejemplo,

sapply(1:5, sqrt)
[1] 1.000000 1.414214 1.732051 2.000000 2.236068

Obviamente, lapply y sapply permiten aplicar cualquier función, incluidas las definidas por los usuarios.

Finalmente, también es muy útil la función split, que permite partir una tabla en trozos de acuerdo con un vector que define los grupos. Por ejemplo,

tmp <- split(iris, iris$Species)

Ejercicio 4.9 Investiga el objeto tmp: ¿qué longitud tiene? ¿qué contiene cada uno de sus componentes?

Ejercicio 4.10 Usa las funciones lapply y sapply para mostrar la dimensión de cada una de las tablas que contiene tmp.

La función split puede servir para operar en subtablas en combinación con lapply. Sin embargo, paquetes como plyr, que introduciremos más tarde, son más prácticos para este tipo de operaciones.

Ejercicio 4.11 Usa split para partir iris en dos subtablas al azar con el mismo número de filas.

Transformaciones como las que solicita el ejercicio anterior son comunes en ciencia de datos: partir un conjunto de datos en dos al azar para construir un modelo sobre una parte de los datos y evaluar su rendimiento sobre el complementario. El ejercicio siguiente está relacionado con una técnica similar, la de la validación cruzada, que es una alternativa más sofisticada a la anterior.

Ejercicio 4.12 Usa split para partir iris cinco subtablas, cada una de ellas con 30 filas distintas.

4.4 Resumen y referencias

Las listas permiten el almacenamiento, manipulación y análisis de información no estructurada en R. Los datos estructurados son un caso particular de los desestructurados. De ahí que las tablas sean también listas de columnas.

Las tablas de los sistemas de bases de datos han sido siempre colecciones de filas. Solo recientemente se están considerando las bases de datos columnares, en las que las columnas de una tabla se almacenan por separado. Esta arquitectura ofrece determinadas ventajas en algunos usos y desventajas en otros. Por lo tanto, como almacén de datos, R podría considerarse un ejemplo de una base de datos columnar.

Las listas están también muy relacionadas con la programación funcional. De hecho, las funciones lapply y sapply son ejemplos versiones de la operación map. La programación funcional admite muchas definiciones e interpretaciones, pero tal vez la más útil (a la vez que inconcreta) es que se trata de aquella que mejor separa el qué del cómo, es decir, qué operaciones queremos realizar para obtener un resultado del cómo realmente implementarlas. Eso lo consigue a través de conceptos y construcciones de orden superior, como map (y también reduce, filter, etc.) que se corresponden muy naturalmente a la manera en que las personas ideamos y construimos los algoritmos.

4.5 Ejercicios adicionales

Ejercicio 4.13 La función strsplit parte una cadena de texto por un caracter o conjunto de caracteres. Explora la salida de strsplit(c("un día vi", "una vaca vestida de uniforme"), " ").

Ejercicio 4.14 Razona por qué la salida de la función strsplit tiene que ser una lista.

Ejercicio 4.15 Dada la lista mi_lista <- list(a = 1:3, b = c("hola", "adiós")),

  • suma los elementos de su primera componente
  • añade a su segundo elemento el valor hasta luego
  • usa lapply o sapply para calcular la longitud de cada elemento de la lista
  • añade a la lista un tercer elemento (llamado iris) que contenga la tabla iris
  • borra el elemento a

  1. En esto, R difiere de otras herramientas (p.e., SAS) que consideran las tablas una lista de filas.↩︎

  2. En el fondo, una lista en R es lo que en otros lenguajes se denominaría un namespace, i.e., una especie de entorno dentro del cual se pueden crear variables.↩︎