Antes de empezar
Varios ejercicios usan las variables y funciones de la teoría: temp, tabla_ventana, X e y, X_ent, y_ent, X_prueba, y_prueba, mae_hoy, con_huecos, faltan, tss, modelo, clics y los import. Abre un cuaderno nuevo en Colab, pega y ejecuta en orden los bloques de la ficha Código (la primera celda descarga los datos, así que necesitas internet) y después trabaja debajo. Intenta cada ejercicio antes de abrir la respuesta.
Los datos de temperatura son de Weather data by Open-Meteo.com (CC BY 4.0), calculados con el reanálisis ERA5-Land; las cifras de las respuestas son cálculos nuestros a partir de ellos.
Ejercicios
1. Tendencia, estacionalidad y ruido
Para cada serie, di si esperas una tendencia, una estacionalidad (y con qué período) y mucho o poco ruido:
- La temperatura media diaria de La Paz.
- Los pasajeros mensuales de vuelos internacionales de 1949 a 1960 (tema 4).
- La cantidad de personas que suben a un teleférico, hora por hora, durante un mes.
- El precio de cierre diario de una acción en la bolsa.
Respuesta
- Estacionalidad anual clara (frío a mitad de año) y mucho ruido de un día a otro; casi nada de tendencia en diez años.
- Tendencia fuerte hacia arriba (se multiplican por más de cuatro) y estacionalidad anual, con un pico a mitad de año que crece con los años.
- Dos estacionalidades a la vez: la del día (horas pico de mañana y de tarde, nada de madrugada) y la de la semana (los días hábiles no se parecen al domingo). La tendencia, en un mes, será pequeña.
- Sobre todo ruido, con tendencias que cambian sin aviso y ninguna estacionalidad fuerte. Por eso, para el precio de mañana, «igual que hoy» es una línea base muy difícil de superar.
2. Rezagos a mano
Una serie de cinco días vale 10, 12, 11, 15 y 14. Sin computadora, escribe para cada día el valor de shift(1), de shift(-2) y de rolling(3).mean(). ¿Por qué algunos quedan vacíos? Después compruébalo.
Respuesta
shift(1) le da a cada día el valor del día anterior; shift(-2), el de dos días después; rolling(3).mean(), el promedio de ese día y los dos anteriores. Quedan vacíos (NaN) cuando no existe ese día: el primero no tiene ayer, los dos últimos no tienen «dentro de dos días» y los dos primeros no tienen tres días de historia.
s = pd.Series([10, 12, 11, 15, 14],
index=pd.date_range("2024-03-01", periods=5))
print(pd.DataFrame({"s": s, "shift(1)": s.shift(1),
"shift(-2)": s.shift(-2),
"media 3": s.rolling(3).mean().round(2)}))
s shift(1) shift(-2) media 3
2024-03-01 10 NaN 11.0 NaN
2024-03-02 12 10.0 15.0 NaN
2024-03-03 11 12.0 14.0 11.00
2024-03-04 15 11.0 NaN 12.67
2024-03-05 14 15.0 NaN 13.33
3. Variables de calendario
La estacionalidad depende de la fecha, y la fecha es algo que sí se sabe de antemano. Agrega a la tabla el mes y la posición del día dentro del año. Para la posición, en lugar del número de día (de 1 a 366), usa su seno y su coseno: así el 31 de diciembre y el 1 de enero quedan cerca, como en un reloj, en vez de en los dos extremos. ¿Mejora la validación por tiempo del modelo?
Respuesta
def con_calendario(X):
"""Agrega el mes y la posición del día dentro del año."""
X = X.copy()
dia = X.index.day_of_year
X["mes"] = X.index.month
# Seno y coseno: el 31 de diciembre queda pegado al 1 de enero
X["dia_sin"] = np.sin(2 * np.pi * dia / 365.25)
X["dia_cos"] = np.cos(2 * np.pi * dia / 365.25)
return X
X_cal = con_calendario(X_ent)
print(X_cal[["mes", "dia_sin", "dia_cos"]].head(3).round(3))
for nombre, tabla_x in [("Sin calendario", X_ent),
("Con calendario", X_cal)]:
errores = -cross_val_score(modelo, tabla_x, y_ent, cv=tss,
scoring="neg_mean_absolute_error")
print(f"{nombre}: MAE = {errores.mean():.3f} °C")
mes dia_sin dia_cos
fecha
2015-01-07 1 0.120 0.993
2015-01-08 1 0.137 0.991
2015-01-09 1 0.154 0.988
Sin calendario: MAE = 0.633 °C
Con calendario: MAE = 0.620 °C
Mejora un poco, de 0,633 a 0,620 °C. Con solo la temperatura de los últimos días, el modelo no sabe si va hacia el invierno o hacia el verano; la fecha se lo dice. Fíjate que el calendario no es fuga: la fecha de mañana se conoce hoy. Lo que no se puede usar es la temperatura de mañana.
4. Encuentra la fuga
Alguien te pasa este cuaderno, orgulloso de su MAE. Encuentra los problemas antes de abrir la respuesta. Una pista: hay más de uno.
# El cuaderno de otra persona: ¿dónde está la fuga?
semana = temp.groupby(temp.index.to_period("W")).transform("mean")
X_mal = X.copy()
X_mal["media_semana"] = semana[X.index]
cv = KFold(n_splits=5, shuffle=True, random_state=0)
errores = -cross_val_score(modelo, X_mal, y, cv=cv,
scoring="neg_mean_absolute_error")
print(f"MAE del cuaderno con fugas: {errores.mean():.3f} °C")
MAE del cuaderno con fugas: 0.538 °C
Respuesta
Hay tres fugas:
media_semanapromedia la semana de calendario completa (de lunes a domingo) a la que pertenece cada día. Para un martes incluye el miércoles (el objetivo) y los días siguientes: datos del futuro.KFold(shuffle=True)baraja los días: el modelo entrena con días posteriores a los que valida.- Usa
Xeycompletos, incluidos 2023 y 2024, que habíamos guardado como prueba. Si eliges el modelo mirando la prueba, ya no es una prueba.
Corregido: sin media_semana, solo con el entrenamiento y con TimeSeriesSplit:
errores = -cross_val_score(modelo, X_ent, y_ent, cv=tss,
scoring="neg_mean_absolute_error")
print(f"MAE corregido: {errores.mean():.3f} °C")
MAE corregido: 0.633 °C
El error «sube» de 0,538 a 0,633 °C. No es que el modelo empeorara: 0,633 es lo que de verdad se puede esperar, apenas mejor que la línea base.
5. Forward-fill frente a la media
Con con_huecos de la teoría, rellena los faltantes de tres formas: con forward-fill, con la media de toda la serie y con interpolate(), que traza una recta entre el último valor conocido y el siguiente. Compara el error en los días rellenados. ¿Cuál usarías para predecir en tiempo real, día a día?
Respuesta
rellenos = {
"ffill": con_huecos.ffill(),
"media de toda la serie": con_huecos.fillna(con_huecos.mean()),
"interpolación": con_huecos.interpolate(),
}
for nombre, serie in rellenos.items():
error = (serie[faltan] - temp[faltan]).abs().mean()
print(f"{nombre:24s} error medio = {error:.2f} °C")
ffill error medio = 0.64 °C
media de toda la serie error medio = 1.08 °C
interpolación error medio = 0.50 °C
- La media de toda la serie es la peor (1,08 °C): un hueco en julio recibe el promedio del año, que es de otra estación. Además usa el futuro: el promedio incluye días posteriores al hueco.
- La interpolación es la mejor (0,50 °C), porque usa el valor de antes y el de después. Justamente por eso no sirve para predecir en tiempo real: el día del hueco todavía no conoces el valor siguiente. Sirve para limpiar datos históricos que no vas a usar para evaluar un pronóstico.
- El forward-fill (0,64 °C) solo usa el pasado: es lo que tendrías de verdad ese día. Es la opción segura para un modelo que predice el futuro.
6. Otra línea base: la climatología
Además de «mañana igual que hoy», hay otra línea base clásica: la climatología, predecir para cada fecha el promedio de ese mismo día del año en los años de entrenamiento. Calcúlala para 2023 y 2024 con horizontes de 1 y de 7 días y compárala con la persistencia. ¿Cuál conviene en cada caso?
Respuesta
# Climatología: el promedio de ese mismo día del año, 2015 a 2022
pasado = temp[:"2022-12-31"]
clima_dia = pasado.groupby(pasado.index.day_of_year).mean()
print("Horizonte climatología igual que hoy (MAE en °C)")
for h in [1, 7]:
X_h, y_h = tabla_ventana(temp, horizonte=h)
prueba_h = X_h.index >= "2023-01-01"
# El día que se predice, como número de día del año
dias_obj = (X_h.index[prueba_h] + pd.Timedelta(days=h)).day_of_year
pred_clima = clima_dia.loc[dias_obj].to_numpy()
c = mean_absolute_error(y_h[prueba_h], pred_clima)
b = mean_absolute_error(y_h[prueba_h], X_h["hoy"][prueba_h])
print(f"h = {h} {c:.3f} {b:.3f}")
Horizonte climatología igual que hoy (MAE en °C)
h = 1 1.034 0.632
h = 7 1.030 1.107
A un día, la persistencia gana con claridad (0,632 frente a 1,034 °C): ayer dice mucho más que el calendario. A siete días se invierte: «igual que hoy» se equivoca 1,107 °C y la climatología, que no depende del horizonte, 1,030 °C. Por eso un modelo con buenas variables mezcla las dos ideas: la ventana reciente y la época del año. En la teoría, a 7 días el modelo llegó a 0,937 °C, mejor que las dos líneas base.
7. Remuestrear
¿Cuál fue la semana más fría de los diez años? ¿Cuántos días con una media de menos de 5 °C hubo cada año? Usa resample.
Respuesta
semanal = temp.resample("W").mean()
print("Semana más fría, la que termina el",
semanal.idxmin().date(), f"con {semanal.min():.1f} °C")
# Días con una media de menos de 5 °C, año por año
frios = (temp < 5).resample("YS").sum()
print(frios.set_axis(frios.index.year).to_string())
Semana más fría, la que termina el 2015-08-16 con 3.5 °C
fecha
2015 15
2016 5
2017 4
2018 15
2019 10
2020 0
2021 6
2022 6
2023 4
2024 2
resample("W") agrupa por semanas que terminan en domingo, y rotula cada grupo con ese domingo. Para contar días, temp < 5 da una serie de True y False, y al sumar, cada True cuenta 1. Los años con más días fríos fueron 2015 y 2018 (15 cada uno); en 2020 no hubo ninguno.
8. Más variables para secuencias de largo variable
A partir de clics, agrega dos variables por visita: cuántas páginas distintas visitó y la pausa más larga entre dos clics. ¿Qué visita parece la de alguien que se distrajo?
Respuesta
resumen_2 = clics.groupby("visita").agg(
paginas_distintas=("pagina", "nunique"),
pausa_maxima=("segundo", lambda s: s.diff().max()),
)
print(resumen_2)
paginas_distintas pausa_maxima
visita
1 3 55.0
2 2 300.0
3 3 29.0
La visita 2: solo dos clics y una pausa de 300 segundos entre ellos, sin llegar a pagar. La visita 3 tiene muchos clics seguidos en la tienda y termina pagando. Cada visita, de cualquier largo, queda como una fila con las mismas columnas.
Mini-proyecto: la temperatura de mañana en Cochabamba
Repite todo el tema con otra ciudad, ahora con tres variables por día, y supera a la línea base.
- Escribe una función
descargar(latitud, longitud, variables)que pida a Open-Meteo la temperatura media, máxima y mínima diarias de Cochabamba (latitud −17,39, longitud −66,16) de 2015 a 2024. Revisa los faltantes y, si los hay, rellénalos con forward-fill. Grafica la media con su media móvil de 30 días (la referencia no incluye el gráfico). - Arma la tabla con
tabla_ventanasobre la media y agrega la máxima y la mínima de hoy y las variables de calendario del ejercicio 3. - Deja 2023 y 2024 como prueba. Con
TimeSeriesSplit, compara la línea base, el modelo con solo la ventana y el modelo con todo. - Mide una sola vez con la prueba y reporta cuánto mejora el modelo a la línea base.
- Escribe tres frases: qué variables ayudaron, por qué no barajaste y qué información le falta al modelo para predecir mejor.
Resultados de referencia, para comprobar los tuyos
def descargar(latitud, longitud, variables):
"""Temperatura diaria de 2015 a 2024 desde Open-Meteo."""
parametros = {
"latitude": latitud, "longitude": longitud,
"start_date": "2015-01-01", "end_date": "2024-12-31",
"daily": ",".join(variables), "models": "era5_land",
"timezone": "America/La_Paz", "format": "csv",
}
url = API + "?" + urlencode(parametros)
tabla = pd.read_csv(url, skiprows=3, index_col=0,
parse_dates=True)
tabla.columns = variables
tabla.index.name = "fecha"
return tabla
# Cochabamba
cbba = descargar(-17.39, -66.16, ["temperature_2m_mean",
"temperature_2m_max",
"temperature_2m_min"])
cbba.columns = ["media", "maxima", "minima"]
print(cbba.describe().round(1))
print("Faltantes:", cbba.isna().sum().sum())
cbba = cbba.ffill() # por si hubiera huecos
media maxima minima
count 3653.0 3653.0 3653.0
mean 15.9 21.7 10.7
std 1.7 2.3 2.8
min 7.9 10.7 -0.6
25% 14.8 20.4 8.8
50% 16.0 21.8 11.2
75% 17.1 23.2 12.9
max 20.9 29.5 16.2
Faltantes: 0
# Paso 2: la tabla de la ventana y dos variables más de hoy
Xc, yc = tabla_ventana(cbba["media"])
Xc["maxima_hoy"] = cbba["maxima"][Xc.index]
Xc["minima_hoy"] = cbba["minima"][Xc.index]
Xc = con_calendario(Xc)
ent_c = Xc.index < "2023-01-01"
Xc_ent, yc_ent = Xc[ent_c], yc[ent_c]
Xc_prueba, yc_prueba = Xc[~ent_c], yc[~ent_c]
# Paso 3: validación por bloques de tiempo
for nombre, columnas in [("Solo la ventana", X.columns),
("Todo", Xc.columns)]:
errores = -cross_val_score(modelo, Xc_ent[columnas], yc_ent,
cv=tss,
scoring="neg_mean_absolute_error")
print(f"{nombre:16s} MAE de validación = {errores.mean():.3f}")
base_c = [mean_absolute_error(yc_ent.iloc[v], Xc_ent["hoy"].iloc[v])
for _, v in tss.split(Xc_ent)]
print(f"{'Línea base':16s} MAE de validación = {np.mean(base_c):.3f}")
# Paso 4: una sola vez, la prueba con 2023 y 2024
modelo.fit(Xc_ent, yc_ent)
mae_c = mean_absolute_error(yc_prueba, modelo.predict(Xc_prueba))
mae_base_c = mean_absolute_error(yc_prueba, Xc_prueba["hoy"])
print(f"Prueba: modelo {mae_c:.3f} °C "
f"igual que hoy {mae_base_c:.3f} °C")
Solo la ventana MAE de validación = 0.853
Todo MAE de validación = 0.808
Línea base MAE de validación = 0.888
Prueba: modelo 0.782 °C igual que hoy 0.803 °C
- Cochabamba, en estos datos, promedia 15,9 °C, con máximas de unos 22 °C y mínimas de unos 11 °C. Recuerda que es el promedio de una celda de unos 11 km que incluye cerros alrededor del valle, así que puede ser más fresco que la ciudad. Tampoco hay faltantes, así que el forward-fill no cambia nada; la línea queda en el código por si otra ciudad los tiene.
- En la validación por tiempo, la línea base se equivoca 0,888 °C; el modelo con solo la ventana, 0,853, y con la máxima, la mínima y el calendario, 0,808: un 9 % menos que la línea base.
- En la prueba (2023 y 2024), el modelo se equivoca 0,782 °C y la línea base 0,803: la mejora se achica a un 2,6 %. Es normal que la prueba dé distinto que la validación: son otros años, con otro clima.
- Al modelo le falta lo que mueve el clima de un día a otro y no está en la temperatura: nubes, humedad, viento, lo que viene de otras regiones.
Errores comunes
- Barajar una serie de tiempo.
train_test_splitbaraja por defecto, yKFold, conshuffle=True. Para una serie, divide por fecha y valida conTimeSeriesSplit. - Ventanas que miran adelante.
rolling(..., center=True), los promedios de la semana o del mes completos y cualquiershiftnegativo en una entrada meten el futuro en la fila. Pregúntate por cada columna: ¿la sabría el día de la predicción? - Olvidar la línea base. Un MAE de 0,6 °C suena muy bien hasta que ves que «mañana igual que hoy» saca casi lo mismo. Compara siempre, y en los mismos días.
- Confundir el sentido de
shift.shift(1)trae el pasado (ayer);shift(-1)trae el futuro (mañana). El objetivo se construye conshift(-h); las entradas, nunca. - Imputar con la media de toda la serie o interpolar antes de evaluar. Las dos usan datos posteriores al hueco. Para un modelo que predice, usa forward-fill.
- Usar el índice como texto. Si las fechas no son de tipo fecha,
resample,rollingcon fechas y los cortes comotemp["2024-06"]no funcionan. Convierte conparse_dates=Trueopd.to_datetime. - Medir con la prueba varias veces. Elige con la validación por tiempo y usa 2023 y 2024 una sola vez, al final.
- Descargar un rango que todavía no terminó. Si pides datos hasta hoy, cada ejecución da otros números. Para estudiar y comparar, usa un rango fijo y pasado.
¿Lo lograste?
Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.
- Cargar y graficar una serie con índice de fechas en pandas, y remuestrearla (ejercicio 7 y paso 1 del mini-proyecto).
- Crear variables de ventana deslizante: rezagos, media y desviación móviles (ejercicios 2 y 3 y paso 2 del mini-proyecto).
- Dividir respetando el tiempo con
TimeSeriesSplity explicar por qué no se baraja (ejercicio 4 y pasos 3 y 5 del mini-proyecto). - Imputar faltantes en secuencias con forward-fill y explicar por qué la media y la interpolación usan el futuro (ejercicio 5).
- Comparar un modelo con una línea base ingenua (ejercicio 6 y paso 4 del mini-proyecto).
- Resumir secuencias de largo variable en variables fijas (ejercicio 8).