is.list(iris)
[1] TRUE
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.
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,
<- as.data.frame(UCBAdmissions)
datos $Admit <- datos$Admit == "Admitted"
datos<- glm(Admit ~ Gender + Dept,
modelo_con_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.
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.
$coefficients modelo_con_dept
(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:
$
no); es decir, permite extraer el elemento n-ésimo de una lista. P.e., iris[[3]]
.iris[["Petal Length"]]
.La función list
permite crear listas. Una vez creada una lista se le pueden añadir campos adicionales2.
<- list(a = 1:3, b = c("hola", "adiós"))
mi_lista $z <- matrix(1:4, 2, 2) mi_lista
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.
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,
<- split(iris, iris$Species) tmp
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.
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.
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"))
,
hasta luego
lapply
o sapply
para calcular la longitud de cada elemento de la listairis
) que contenga la tabla iris
a