Parte II — Análisis multidimensional con xarray

En esta parte aplicaremos xarray a un problema real combinando:

El objetivo es calcular un indicador de riesgo asociado a la movilidad:

risk(t, i, j)

donde: - t → tiempo - i → región de origen - j → región de destino

Motivación

Hasta ahora hemos trabajado con dataframes.

Sin embargo, este tipo de problema tiene una estructura naturalmente multidimensional:

  • movilidad → (t, origen, destino)
  • casos → (t, región)

Con xarray podemos representar esta estructura directamente.

import pandas as pd
import numpy as np
import xarray as xr
covid_df = pd.read_parquet("data/processed/cnig_provincias_covid_cases.parquet")
trips_df = pd.read_parquet("data/processed/cnig_provincias_daily_mobility.parquet")
pop_df   = pd.read_parquet("data/processed/cnig_provincias_zone_movements.parquet")

fechas = pop_df.index.get_level_values('date').unique()
covid_df = covid_df.loc[fechas]

Ejercicio 1 — Exploración

  • ¿Qué columnas tiene cada dataset?
  • ¿Qué representa cada variable?
  • ¿Qué dimensiones implícitas puedes identificar?
  • ¿Qué rango de fechas tiene cada dataset?

Ejercicio 2 — Conversión a xarray (*)

Convierte los tres dataframes a estructuras xarray adecuadas.

Primero vamos a intentar hacerlo de forma manual, es decir: 1. creando las coordinadas 2. estructurando los datos

De esta forma vamos a entender mejor el funcionamiento. Para la segunda parte del ejercicio, vamos a volver a crear los xarray pero empleando funciones específicas Pistas: - usa .set_index(...) - luego .to_xarray()

# crear los xarray a partir de los dataframes de forma manual
# covid_df = covid_df[['date','id','new_cases']]

fechas = covid_df['date'].unique()
provincias = covid_df['id'].unique()
m = fechas.shape[0]
n = provincias.shape[0]

tot = covid_df['new_cases'].values.shape[0]
print(m,n,tot)

a = covid_df['new_cases'].values
data = a.reshape(m,n)

np.abs(data[:,0] - covid_df.loc[covid_df['id']=='01','new_cases'].values).max()

coords = {'fecha': fechas, 'provincia': provincias}
xr.DataArray(data=data, coords=coords)
pop_ds = pop_df.set_index(['date', 'id']).to_xarray()

pop_ds['total_population'] - pop_ds['moving_population'] 

pop_xa = pop_ds['total_population']
# crear los xarray a partir de los dataframes de empleando las funciones especializadas de pandas
covid_ds = covid_df.to_xarray()

covid_ds['total_population'] = pop_df['total_population'].to_xarray()
covid_ds["IA10"] = covid_ds['new_cases'].rolling(date=10, min_periods=1).sum()
covid_ds["prob_inf"] = covid_ds["IA10"] / covid_ds['total_population']

Ejercicio 3 — Dimensiones y coordenadas

Para cada dataset:

  • identifica dimensiones
  • identifica coordenadas
  • comprueba que están alineadas

Ejercicio 4 — Incidencia acumulada (*)

Calcula una incidencia acumulada a 10 días usando rolling sum.

Pregunta:

  • ¿por qué usamos esto como proxy?

Ejercicio 5 — Normalización (*)

Calcula la densidad de infectados:

density(t, i) = casos / población

Pregunta:

  • ¿qué dimensiones tiene el resultado?

Ejercicio 6 — Cálculo del riesgo (*)

Queremos calcular:

risk(t, i, j)

a partir de:

  • trips(t, i, j)
  • density(t, i)

👉 intenta hacerlo directamente

trips_xa = trips_df['trips'].to_xarray()

trips_xa * covid_ds['prob_inf'].rename({'id': 'source'})

Ejercicio 7 — Análisis

Calcula:

  • riesgo total saliente por región
  • riesgo total entrante

Pregunta:

  • ¿qué regiones destacan?

Ejercicio 8 — Riesgo y propagación de la epidemia

En este ejercicio analizaremos si el riesgo asociado a la movilidad entre regiones puede estar relacionado con la aparición de nuevos casos en la región de destino.

La idea es explorar si:

  • un aumento en el riesgo desde i → j
  • precede a un aumento en la incidencia en j

Esto podría indicar que los viajes desde i han contribuido a la propagación hacia j.

Selecciona dos provincias (i, j) y analiza:

  • el riesgo asociado a los viajes desde i → j
  • la incidencia en la región j

Ejemplo

Madrid → Asturias

Paso 1

Selecciona una pareja de regiones (origen, destino).

origin = "28"   # Madrid
target = "33"   # Asturias

Paso 2

Extrae las siguientes series temporales:

  • riesgo desde i → j
  • incidencia en j

Paso 3

Representa ambas series en una misma figura.

Pregunta:

  • ¿Cómo evolucionan en el tiempo?
  • ¿Hay relación entre ellas?
  • ¿observas patrones similares?
  • ¿hay desfase temporal entre ambas?

Paso 4 — Interpretación

Reflexiona:

  • ¿el aumento del riesgo ocurre antes que el aumento de casos en j?
  • ¿podría existir una relación causal?

Parte III — Operaciones avanzadas con xarray

Ejercicio 1 — Construcción de un Dataset

Crea un objeto xarray.Dataset que contenga:

  • incidencia: casos y densidad
  • población
  • movilidad (trips)
  • riesgo

Asegúrate de que:

  • comparten coordenadas
  • las dimensiones están alineadas

Pregunta:

  • ¿qué ventajas tiene agrupar variables en un Dataset?

Ejercicio 2 — Validación

Comprueba:

  • que las coordenadas de tiempo coinciden
  • que las regiones están alineadas

Pregunta:

  • ¿qué problemas podrían surgir si no lo estuvieran?

Ejercicio 3 — Guardado (*)

Guarda el dataset en formato NetCDF.

Luego:

  • vuelve a cargarlo
  • verifica que es igual al original

Pregunta:

  • ¿qué ventajas tiene NetCDF frente a CSV o Parquet?

Ejercicio 4 — Reducción

Calcula:

  • riesgo total saliente por región
  • riesgo total entrante por región
  • riesgo medio en el tiempo

Pregunta:

  • ¿cómo interpretas cada uno?

Ejercicio 5 — Subconjuntos

Selecciona:

  • un subconjunto de fechas
  • un subconjunto de regiones

Pregunta:

  • ¿cómo cambia la estructura del dataset?

Ejercicio 6 — Agregación temporal

Agrupa los datos por:

  • semana o
  • mes

y calcula el riesgo medio

Pregunta:

  • ¿qué patrones emergen?

Ejercicio — Matriz de riesgo neto

Para una fecha concreta:

  • representa la matriz risk(i, j)

Pregunta:

  • ¿qué estructura observas?

Ejercicio avanzado — Correlación temporal

Queremos estudiar la relación entre:

  • riesgo desde i → j
  • incidencia en j

a lo largo del tiempo.


Objetivo

Calcular la correlación en ventanas deslizantes.


Paso 1: Selecciona una pareja (i, j)

Paso 2: Calcula la correlación usando ventanas móviles (rolling window)

Hint

xr.corr(risk_ij.rolling(date=7), incidence_j.rolling(date=7))

De matrices a redes

La matriz de riesgo risk(i, j) puede interpretarse como una red dirigida:

  • nodos → regiones
  • aristas → flujo de riesgo entre regiones

Riesgo neto

Definimos el riesgo neto como (usar transpose):

net(i, j) = risk(i, j) - risk(j, i)


Interpretación:

  • net(i, j) > 0 → flujo neto de riesgo de i hacia j
  • net(i, j) < 0 → flujo neto de riesgo de j hacia i

Para construir una red dirigida, nos quedamos con:

net(i, j) > 0

Ejercicio Avanzado — Red de riesgo dirigida (*)

A partir de la matriz risk(i, j), construye una red dirigida basada en el riesgo neto.


Paso 1 — Riesgo neto

Selecciona una fecha concreta.

Calcula:

net(i, j) = risk(i, j) - risk(j, i)

y conserva solo los valores positivos.

date = "2020-03-15"

risk_day = risk.sel(date=date)

net = risk_day - risk_day.transpose("target", "source")
net_pos = net.where(net > 0)

Paso 2 — Construcción de la red

Convierte la matriz en un grafo dirigido.

import networkx as nx

edges = (
    net_pos.to_dataframe(name="weight")
    .dropna()
    .reset_index()
)

G = nx.DiGraph()

for _, row in edges.iterrows():
    G.add_edge(row["source"], row["target"], weight=row["weight"])

Paso 3 — Posición de nodos

Usa los centroides de las regiones como posiciones.

coord_dict = {
    idx: (geom.centroid.x, geom.centroid.y)
    for idx, geom in gdf["geometry"].items()
}

Paso 4 — plot con flechas

  • Leemos las geometrías de las provincias
  • calculamos los centroides para poder dibujar las fechas
import geopandas as gpd

gdf = gpd.read_parquet("data/covid19/provincias.parquet")
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10,8))

# mapa base
gdf.plot(ax=ax, color="lightgrey", edgecolor="black")

# nodos
nx.draw_networkx_nodes(
    G,
    pos=coord_dict,
    node_size=10,
    node_color="black",
    ax=ax
)

# edges con flechas
nx.draw_networkx_edges(
    G,
    pos=coord_dict,
    arrowstyle='-|>',
    arrowsize=8,
    connectionstyle="arc3,rad=0.2",
    width=0.5,
    alpha=0.7,
    ax=ax
)

ax.set_title(f"Directed risk network — {date}")
ax.set_axis_off()

plt.show()

Paso 5 — Filtrado

Filtra las conexiones más relevantes.

threshold = net_pos.quantile(0.95)
edges = edges[edges["weight"] > threshold]

Interpretación

  • ¿Qué regiones son emisores netos?
  • ¿Qué regiones reciben más riesgo?
  • ¿Hay estructura geográfica?