list_example = [10, 1.23, "like this", True, None]
print(list_example)[10, 1.23, 'like this', True, None]
En este capítulo aprenderás sobre los datos anidados: cómo trabajar con datos que tienen, en esencia, forma de árbol y cómo (a menudo) convertirlos en dataframes rectangulares formados por filas y columnas. Esto es importante porque los datos anidados son sorprendentemente comunes, sobre todo cuando trabajas con datos que provienen de una API web (como verás en Web scraping y API).
Para aprender sobre la rectangularización, primero conocerás las listas, los diccionarios y el formato JSON, ya que son las estructuras de datos que más se usan para trabajar con datos jerárquicos en Python. Después aprenderás algunas funciones que te ayudarán a convertir datos jerárquicos en datos ‘tidy’ organizados en columnas y filas. Luego te mostraremos algunos casos prácticos en los que aplicamos estas funciones sencillas varias veces para resolver problemas reales complejos.
En este capítulo usaremos el paquete de análisis de datos pandas.
Las listas son una forma muy útil de trabajar con muchos datos a la vez. Se definen con corchetes y sus elementos se separan con comas.
list_example = [10, 1.23, "like this", True, None]
print(list_example)[10, 1.23, 'like this', True, None]
También puedes construirlas añadiendo elementos:
list_example.append("one more entry")
print(list_example)[10, 1.23, 'like this', True, None, 'one more entry']
Y puedes acceder a los elementos mediante un índice, que empieza en 0 y termina en la longitud de la lista menos uno (esta es la convención en muchos lenguajes de programación). Por ejemplo, para imprimir elementos concretos del principio, con 0, y del final, con -1:
print(list_example[0])
print(list_example[-1])10
one more entry
¿Cómo podrías acceder a la penúltima entrada de un objeto lista si no supieras cuántos elementos tiene?
Además de acceder a posiciones de las listas mediante índices, puedes usar slices (rebanadas) sobre ellas. Para ello se usa el carácter de dos puntos, :, que representa ‘desde el principio’ o ‘hasta el final’ (cuando aparece una sola vez). Por ejemplo, para imprimir solo los dos últimos elementos, usaríamos el índice -2:, que significa desde el penúltimo en adelante. Aquí tienes dos ejemplos distintos: obtener los tres primeros y los tres últimos elementos, que se imprimen sucesivamente:
print(list_example[:3])
print(list_example[-3:])[10, 1.23, 'like this']
[True, None, 'one more entry']
El slicing puede ser aún más elaborado, porque podemos saltar elementos usando un segundo signo de dos puntos. Aquí tienes un ejemplo completo que empieza en el segundo elemento (recuerda que el índice empieza en 0), llega hasta el penúltimo elemento (sin incluirlo) y salta un elemento de cada dos entre medias (range simplemente produce una lista de enteros desde el valor inicial hasta uno menos que el último):
list_of_numbers = list(range(1, 11))
start = 1
stop = -1
step = 2
print(list_of_numbers[start:stop:step])[2, 4, 6, 8]
Un truco práctico es que puedes imprimir una lista completa invertida usando dos signos de dos puntos:
print(list_of_numbers[::-1])[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Haz un slice de la list_example de antes para obtener solo las primeras cinco entradas.
Lo increíble de las listas es que pueden contener cualquier tipo, ¡incluidas otras listas! Aquí tienes un ejemplo válido de una lista bastante cargada:
wacky_list = [
3.1415,
16,
["five", 4, 3],
(91, 93, 90),
"Hello World!",
True,
None,
{"key": "value", "key2": "value2"},
]
wacky_list[3.1415,
16,
['five', 4, 3],
(91, 93, 90),
'Hello World!',
True,
None,
{'key': 'value', 'key2': 'value2'}]
Como las listas pueden contener más listas (y así sucesivamente), pueden usarse para almacenar datos jerárquicos. Veamos un ejemplo:
multilayer_list = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]
multilayer_list[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]
Ahora, supongamos que quisiéramos reducir esto a una sola lista. Podemos hacerlo con una list comprehension (comprensión de listas):
[x for little_list in multilayer_list for x in little_list][1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
Lo que estamos diciendo aquí es: toma todos los valores de cada pequeña lista y ponlos en una única lista.
En ocasiones tendrás datos en listas que querrás convertir en un dataframe. Por ejemplo, quizá tengas una lista de listas como esta:
list_of_lists = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]Puedes pasarla directamente al constructor de un dataframe mediante el argumento de palabra clave data= (añadiendo otra información si es necesario). Ten en cuenta que son cuatro listas de tres elementos, así que el bucle interno tiene elementos del 0 al 2… es este bucle interno el que se usará como las filas de cualquier dataframe, y el número de elementos de cada lista interna será igual al número de columnas.
import pandas as pd
pd.DataFrame(data=list_of_lists, columns=["a", "b", "c"])| a | b | c | |
|---|---|---|---|
| 0 | 1 | 2 | 3 |
| 1 | 4 | 5 | 6 |
| 2 | 7 | 8 | 9 |
| 3 | 10 | 11 | 12 |
Hay un truco más que mostrarte: explode. Es útil cuando tienes datos con más de un nivel de profundidad de listas. Supongamos que lees unos datos con una estructura jerárquica compleja como esta:
df = pd.DataFrame(
{
"alpha": [[0, 1, 2], "foo", [], [3, 4]],
"beta": 1,
"gamma": [["a", "b", "c"], pd.NA, [], ["d", "e"]],
}
)
df| alpha | beta | gamma | |
|---|---|---|---|
| 0 | [0, 1, 2] | 1 | [a, b, c] |
| 1 | foo | 1 | <NA> |
| 2 | [] | 1 | [] |
| 3 | [3, 4] | 1 | [d, e] |
Tenemos varias filas y columnas que contienen listas. En algunas situaciones no pasa nada por tener una lista en una columna, pero aquí probablemente no sea buena idea, ya que está mezclada con otros tipos de datos. Podemos usar explode() para desplegar las columnas a lo largo
df.explode("alpha")| alpha | beta | gamma | |
|---|---|---|---|
| 0 | 0 | 1 | [a, b, c] |
| 0 | 1 | 1 | [a, b, c] |
| 0 | 2 | 1 | [a, b, c] |
| 1 | foo | 1 | <NA> |
| 2 | NaN | 1 | [] |
| 3 | 3 | 1 | [d, e] |
| 3 | 4 | 1 | [d, e] |
¡Ninguna discusión sobre programación y objetos de datos jerárquicos estaría completa sin mencionar JSON (Java Script Object Notation)! Te encontrarás con este formato de datos de texto constantemente al trabajar con datos de la web, especialmente de APIs (servicios de datos automatizados basados en la web). JSON representa los objetos como pares nombre/valor, igual que un diccionario de Python (que tiene el formato {key1: value1, key2: value2}).
La siguiente tabla compara los distintos tipos de datos de Python y JSON.
| OBJETO JSON | OBJETO PYTHON |
|---|---|
| object | dict |
| array | list |
| string | str |
| null | None |
| number (int) | int |
| number (real) | float |
| true | True |
| false | False |
Normalmente hay dos operaciones que querrás hacer con datos JSON: 1) convertir datos JSON en un objeto de Python (p. ej., de JSON a diccionario de Python) o viceversa (lo que se conoce como deserialización y serialización, respectivamente); y 2) convertir un objeto deserializado en un tipo diferente de objeto de Python.
Veamos cada una por separado.
Veamos un ejemplo típico de lectura de datos JSON.
Obtendremos datos JSON de una API. Tomemos los datos más recientes de desempleo del Reino Unido (código de serie temporal “MGSX” y código de dataset “LMS”).
import requests
url = "https://api.beta.ons.gov.uk/v1/data?uri=employmentandlabourmarket/peoplenotinwork/unemployment/timeseries/mgsx/lms/previous/v106"
# Get the data from the ONS API:
json_data = requests.get(url).json()Comprobemos qué tipo hemos obtenido:
type(json_data)dict
Como era de esperar, los datos JSON se han leído automáticamente como un diccionario, pero ten cuidado: los campos se han leído como texto en lugar de como números, fechas y horas u otros tipos de datos específicos.
Podríamos imprimir el objeto completo, pero ocuparía mucho espacio; en su lugar, veamos un par de elementos bajo la clave “months”.
json_data["months"][:2][{'date': '1971 FEB',
'value': '3.8',
'label': '1971 JAN-MAR',
'year': '1971',
'month': 'February',
'quarter': '',
'sourceDataset': 'LMS',
'updateDate': '2015-10-13T23:00:00.000Z'},
{'date': '1971 MAR',
'value': '3.9',
'label': '1971 FEB-APR',
'year': '1971',
'month': 'March',
'quarter': '',
'sourceDataset': 'LMS',
'updateDate': '2015-10-13T23:00:00.000Z'}]
Para este ejercicio, necesitarás descargar el archivo JSON ‘cakes.json’ de la carpeta de datos del repositorio asociado a este libro y guardarlo en una subcarpeta llamada “data”. Podemos echar un vistazo a los datos usando la terminal (eso es lo que significa el signo de exclamación inicial):
json_string = """
{
"food": "doughnut",
"good_with": ["coffee", "tea"],
"flavour": null,
"toppings": [{"id": 0, "type": "glazed"},
{"id": 1, "type": "sugar"}]
}
"""Usamos la librería integrada json para leer esto en Python (también podrías usar una ruta de archivo aquí; enseguida veremos cómo):
import json
result = json.loads(json_string)
result{'food': 'doughnut',
'good_with': ['coffee', 'tea'],
'flavour': None,
'toppings': [{'id': 0, 'type': 'glazed'}, {'id': 1, 'type': 'sugar'}]}
Ten en cuenta que no todo es igual al pasar de texto JSON a un diccionario de Python: JSON usa null en lugar de None, no acepta comas finales al final de las listas y sus tipos básicos son listas, strings (y todas las claves deben ser strings), números, booleans y nulos. Veamos ahora cómo convertir un diccionario de Python de nuevo a JSON, por ejemplo para escribirlo en un archivo:
json_stream = json.dumps(result)
json_stream'{"food": "doughnut", "good_with": ["coffee", "tea"], "flavour": null, "toppings": [{"id": 0, "type": "glazed"}, {"id": 1, "type": "sugar"}]}'
Para escribir en un archivo, usarías este patrón:
with open('data/json_data_output.json', 'w') as outfile:
json.dump(json_stream, outfile)Para leer un archivo del disco, por ejemplo “data/json_data_output.json”, se hace así
json.load(open("data/json_data_output.json"))pandas tiene muchas opciones para convertir datos JSON o de diccionario en un dataframe. Eso sí, necesitas pensar un poco en la estructura subyacente de los datos:
import pandas as pd
pd.DataFrame(result["toppings"], columns=["id", "type"])| id | type | |
|---|---|---|
| 0 | 0 | glazed |
| 1 | 1 | sugar |
Los datos obtenidos de la web que descargamos antes tenían una estructura más complicada, pero pandas tiene una función json_normalize() que puede manejarla. Por ejemplo, con los siguientes datos faltan muchos elementos, pero json_normalize() aun así puede convertirlos en un dataframe.
data = [
{"id": 1, "name": {"first": "Coleen", "last": "Volk"}},
{"name": {"given": "Mark", "family": "Regner"}},
{"id": 2, "name": "Faye Raker"},
]
pd.json_normalize(data)| id | name.first | name.last | name.given | name.family | name | |
|---|---|---|---|---|---|---|
| 0 | 1.0 | Coleen | Volk | NaN | NaN | NaN |
| 1 | NaN | NaN | NaN | Mark | Regner | NaN |
| 2 | 2.0 | NaN | NaN | NaN | NaN | Faye Raker |
Y también podemos controlar el nivel hasta el que se desglosan propiedades como ‘name’ (puedes consultar más opciones en la documentación de pandas)
pd.json_normalize(data, max_level=0)| id | name | |
|---|---|---|
| 0 | 1.0 | {'first': 'Coleen', 'last': 'Volk'} |
| 1 | NaN | {'given': 'Mark', 'family': 'Regner'} |
| 2 | 2.0 | Faye Raker |
Además de la función de normalización de JSON, pandas tiene un método from_dict() para trabajar con objetos de diccionario más sencillos.