Data leakage en modelos de forecasting preentrenados

Si te gusta  Skforecast , ayúdanos dándonos una estrella en  GitHub! ⭐️

Data leakage en modelos de forecasting preentrenados

Joaquin Amat Rodrigo, Javier Escobar Ortiz
Diciembre, 2024

Introducción

El data leakage, también conocido como information leakage, ocurre cuando datos externos al conjunto de entrenamiento influyen inadvertidamente en el modelo durante el proceso de entrenamiento. Este problema genera estimaciones de rendimiento demasiado optimistas durante la evaluación del modelo y dificulta su capacidad de generalización a nuevos datos. En la predicción de series temporales, el data leakage suele ocurrir cuando el modelo tiene acceso a valores futuros que no estarían disponibles en el momento de la predicción.

Este problema es especialmente crítico al trabajar con modelos preentrenados, como los modelos fundacionales para series temporales, ya que los usuarios generalmente no participan en el proceso de entrenamiento. Para mitigar el data leakage, la mayoría de los autores informan sobre las series utilizadas para preentrenar el modelo, lo que permite a los usuarios verificar que los datos empleados en la evaluación no hayan sido vistos durante el entrenamiento. Sin embargo, garantizar la separación de datos en la fase de preentrenamiento no es suficiente para evitar el data leakage. En la predicción de series temporales, también es crucial impedir que el modelo acceda a datos más recientes que el período designado para la validación. Esto se debe a que, si una serie utilizada en el entrenamiento está altamente correlacionada con la serie empleada en la evaluación, el modelo podría acceder de manera indirecta a información que no debería utilizar.

El riesgo de este tipo data leakage es especialmente alto en modelos entrenados con miles de series temporales, ya que es probable que algunas de ellas presenten una fuerte correlación con la serie que el usuario desea predecir.

Para ilustrar este fenómeno, se lleva a cabo un experimento en el que se entrenan dos modelos con varias series temporales que presentan un alto grado de correlación. Luego, se evalúa un modelo con una serie que ha sido excluida del conjunto de entrenamiento, pero que corresponde a un período de tiempo ya observado durante el entrenamiento. Los resultados se comparan con aquellos obtenidos cuando el período de test se excluye por completo de los datos de entrenamiento. Si el modelo muestra un desempeño significativamente mejor en el primer escenario, esto indica que está aprovechando información a la que no debería tener acceso, lo que confirma la presencia data leakage.

Para evitar el data leakage y garantizar una evaluación justa, el modelo no debe tener acceso a ningún dato del período de test designado.

Librerías

Las librerías utilizadas en este documento son:

In [1]:
# Tratamiento de datos
# ==============================================================================
import pandas as pd
from skforecast.datasets import fetch_dataset

# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
from skforecast.plot import set_dark_theme

# Modelado
# ==============================================================================
import skforecast
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from skforecast.recursive import ForecasterRecursiveMultiSeries

# Configuración
# ==============================================================================
import warnings
warnings.filterwarnings('once')

color = '\033[1m\033[38;5;208m' 
print(f"{color}Versión skforecast: {skforecast.__version__}")
Versión skforecast: 0.14.0

Datos

Los datos utilizados en este documento contienen el número diario de pasajeros de varios servicios de transporte público en Madrid.

In [2]:
# Descarga de datos
# ==============================================================================
data = pd.read_csv(
    "https://raw.githubusercontent.com/skforecast/skforecast-datasets/refs/heads/"
    "main/data/public-transport-madrid.csv"
)
data["date"] = pd.to_datetime(data["date"])
data = data.set_index("date")
data = data.asfreq("D")
data = data.drop(columns=["total"])
data.head()
Out[2]:
metro bus road train
date
2023-01-01 685684 319488 155714 174991
2023-01-02 1581661 1024836 588003 446467
2023-01-03 1781186 1151845 662751 510268
2023-01-04 1846531 1160892 681347 517539
2023-01-05 1842966 1087828 615698 487856
In [3]:
# Gráfico de las series
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(7, 3.5))
data.plot(ax=ax, legend=True)
ax.set_title('Users of public transport in Madrid')
ax.set_xlabel('')
ax.legend(loc='upper left');
In [4]:
# Matriz de correlación entre series
# ==============================================================================
correlation = data.corr()
correlation
Out[4]:
metro bus road train
metro 1.000000 0.962036 0.963832 0.972725
bus 0.962036 1.000000 0.988552 0.982577
road 0.963832 0.988552 1.000000 0.980971
train 0.972725 0.982577 0.980971 1.000000

El gráfico y la matriz de correlación de las series muestran que estas están altamente correlacionadas.

In [5]:
# Variables exógenas basadas en calendario
# ==============================================================================
exog = pd.DataFrame(index=data.index)
exog['day'] = exog.index.day
exog['month'] = exog.index.month
exog['quarter'] = exog.index.quarter
exog['dayofweek'] = exog.index.dayofweek
exog['dayofyear'] = exog.index.dayofyear
exog['year'] = exog.index.year
exog.head(4)
Out[5]:
day month quarter dayofweek dayofyear year
date
2023-01-01 1 1 1 6 1 2023
2023-01-02 2 1 1 0 2 2023
2023-01-03 3 1 1 1 3 2023
2023-01-04 4 1 1 2 4 2023

Modelado

Se entrenan dos modelos de predicción utilizando las series disponibles, excluyendo metro. Luego, se evalúa el rendimiento de los modelos en la serie excluida metro en dos escenarios diferentes:

In [6]:
# División datos: train-test
# ==============================================================================
end_train = '2024-07-31 23:59:00'
data_train = data.loc[:end_train, :].copy()
data_test  = data.loc[end_train:, :].copy()
exog_train = exog.loc[:end_train, :].copy()
exog_test  = exog.loc[end_train:, :].copy()

print(f"Train dates : {data_train.index.min()} --- {data_train.index.max()}  (n={len(data_train)})")
print(f"Test dates  : {data_test.index.min()} --- {data_test.index.max()}  (n={len(data_test)})")
Train dates : 2023-01-01 00:00:00 --- 2024-07-31 00:00:00  (n=578)
Test dates  : 2024-08-01 00:00:00 --- 2024-12-15 00:00:00  (n=137)

Modelo que no ha visto la serie objetivo ni ninguna otra serie en el período de test

In [7]:
# Entrenar forecaster
# ==============================================================================
lags = [1, 7, 14, 364]
forecaster_1 = ForecasterRecursiveMultiSeries(
                   regressor          = Ridge(random_state=951),
                   lags               = lags,
                   transformer_series = StandardScaler(),
                   encoding           = None,
                   forecaster_id      = 'forecaster_no_leakage'
               )

forecaster_1.fit(series=data_train.drop(columns='metro'), exog=exog_train)
In [8]:
# Predicciones
# ==============================================================================
predictions_1 = forecaster_1.predict(
                    steps       = len(data_test),
                    last_window = data_train[['metro']],
                    exog        = exog_test,
                    levels      = ["metro"]
                )
predictions_1 = predictions_1.rename(columns={'metro': 'pred_model_1'})
predictions_1.head(3)
Out[8]:
pred_model_1
2024-08-01 1.451714e+06
2024-08-02 1.478629e+06
2024-08-03 1.141787e+06

Modelo que no ha visto la serie objetivo, pero sí ha visto el período de prueba de las otras series

In [9]:
# Entrenar forecaster
# ==============================================================================
lags = [1, 7, 14, 364]
forecaster_2 = ForecasterRecursiveMultiSeries(
                   regressor          = Ridge(random_state=951),
                   lags               = lags,
                   transformer_series = StandardScaler(),
                   encoding           = None,
                   forecaster_id      = 'forecaster_with_leakage'
               )

forecaster_2.fit(series=data.drop(columns='metro'), exog=exog)
In [10]:
# Predicciones
# ==============================================================================
predictions_2 = forecaster_2.predict(
                    steps       = len(data_test),
                    last_window = data_train[['metro']],
                    exog        = exog_test,
                    levels      = ["metro"]
                )
predictions_2 = predictions_2.rename(columns={'metro': 'pred_model_2'})
predictions_2.head(3)
Out[10]:
pred_model_2
2024-08-01 1.474107e+06
2024-08-02 1.511256e+06
2024-08-03 1.164568e+06
In [11]:
# Resultados
# ==============================================================================
results = pd.concat([data_test['metro'], predictions_1, predictions_2], axis=1)
fig, ax = plt.subplots(figsize=(7, 3.5))
data_test['metro'].plot(ax=ax, label='True')
results['pred_model_1'].plot(ax=ax, label='No leakage')
results['pred_model_2'].plot(ax=ax, label='With leakage')
ax.set_title('Users of public transport in Madrid')
ax.set_xlabel('')
ax.legend()
plt.show();
In [12]:
# Prediction error for each model
# ==============================================================================
mae_1 = (results['metro'] - results['pred_model_1']).abs().mean()
mae_2 = (results['metro'] - results['pred_model_2']).abs().mean()
improvement = (mae_1 - mae_2) / mae_1

print(f"MAE model {forecaster_1.forecaster_id}: {mae_1}")
print(f"MAE model {forecaster_2.forecaster_id}: {mae_2}")
print(f"Improvement: {100 * improvement:.2f}%")
MAE model forecaster_no_leakage: 439142.0060668145
MAE model forecaster_with_leakage: 248235.84805849835
Improvement: 43.47%

Aunque ninguno de los modelos ha visto directamente la serie objetivo, el segundo modelo ha estado expuesto al período de test de otras series. Dado que algunas de estas series están altamente correlacionadas con la serie objetivo, el modelo ha accedido indirectamente a información futura, lo que le permite hacer predicciones más precisas que el primer modelo.

Este ejemplo ilustra cómo puede producirse data leakage incluso cuando la serie objetivo no forma parte del conjunto de entrenamiento. Para evitar la filtración de información y garantizar una evaluación justa, el modelo no debe tener acceso a ningún dato del período de test designado.

Información de sesión

In [13]:
import session_info
session_info.show(html=False)
-----
matplotlib          3.9.2
pandas              2.2.3
session_info        1.0.0
skforecast          0.14.0
sklearn             1.5.1
-----
IPython             8.27.0
jupyter_client      8.6.3
jupyter_core        5.7.2
notebook            6.4.12
-----
Python 3.12.5 | packaged by Anaconda, Inc. | (main, Sep 12 2024, 18:27:27) [GCC 11.2.0]
Linux-5.15.0-1075-aws-x86_64-with-glibc2.31
-----
Session information updated at 2025-02-04 11:01

Instrucciones para citar

¿Cómo citar este documento?

Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!

Data leakage en modelos de forecasting preentrenados por Joaquín Amat Rodrigo y Javier Escobar Ortiz, disponible bajo una licencia Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) en https://cienciadedatos.net/documentos/py63-data-leakage-modelos-forecasting-preentrenados.html

¿Cómo citar skforecast?

Si utilizas skforecast, te agradeceríamos mucho que lo cites. ¡Muchas gracias!

Zenodo:

Amat Rodrigo, Joaquin, & Escobar Ortiz, Javier. (2024). skforecast (v0.14.0). Zenodo. https://doi.org/10.5281/zenodo.8382788

APA:

Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast (Version 0.14.0) [Computer software]. https://doi.org/10.5281/zenodo.8382788

BibTeX:

@software{skforecast, author = {Amat Rodrigo, Joaquin and Escobar Ortiz, Javier}, title = {skforecast}, version = {0.14.0}, month = {11}, year = {2024}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382788} }

¿Te ha gustado el artículo? Tu ayuda es importante

Tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊

Become a GitHub Sponsor Become a GitHub Sponsor

Creative Commons Licence

Este documento creado por Joaquín Amat Rodrigo y Javier Escobar Ortiz tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.

Se permite:

  • Compartir: copiar y redistribuir el material en cualquier medio o formato.

  • Adaptar: remezclar, transformar y crear a partir del material.

Bajo los siguientes términos:

  • Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.

  • No-Comercial: No puedes utilizar el material para fines comerciales.

  • Compartir-Igual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.