Forecasting series temporales incompletas

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

Forecasting series temporales incompletas

Joaquin Amat Rodrigo, Javier Escobar Ortiz
Agosto, 2024

Series temporales con valores faltantes

En muchos casos reales de forecasting, aunque se disponga de datos históricos, es habitual que las series temporales estén incompletas. La presencia de valores faltantes (missing) en los datos es un problema importante, ya que la mayoría de los algoritmos de forecasting requieren que las series temporales estén completas para poder entrenar un modelo.

Una estrategia comúnmente empleada para evitar este problema consiste en imputar los valores que faltan antes de entrenar el modelo, por ejemplo, utilizando una media móvil. Sin embargo, la calidad de las imputaciones puede no ser buena, lo que perjudica el entrenamiento del modelo. Una forma de mejorar la estrategia de imputación es combinarla con weighted time series forecasting. Esta última consiste en reducir el peso de las observaciones imputadas y, por tanto, su influencia durante el entrenamiento del modelo.

Este documento muestra dos ejemplos de cómo skforecast facilita la aplicación de esta estrategia.

Librarías

In [22]:
# Manipulación de datos
# ==============================================================================
import numpy as np
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 y Forecasting
# ==============================================================================
import sklearn
import skforecast
from lightgbm import LGBMRegressor
from skforecast.ForecasterAutoreg import ForecasterAutoreg
from skforecast.model_selection import backtesting_forecaster

# Warnings
# ==============================================================================
import warnings

color = '\033[1m\033[38;5;208m' 
print(f"{color}Version skforecast: {skforecast.__version__}")
print(f"{color}Version scikit-learn: {sklearn.__version__}")
print(f"{color}Version pandas: {pd.__version__}")
print(f"{color}Version numpy: {np.__version__}")
Version skforecast: 0.13.0
Version scikit-learn: 1.4.2
Version pandas: 2.2.2
Version numpy: 2.0.1

Datos

In [23]:
# Descarga de datos
# ==============================================================================
data = fetch_dataset('bicimad')
data
bicimad
-------
This dataset contains the daily users of the bicycle rental service (BiciMad) in
the city of Madrid (Spain) from 2014-06-23 to 2022-09-30.
The original data was obtained from: Portal de datos abiertos del Ayuntamiento
de Madrid https://datos.madrid.es/portal/site/egob
Shape of the dataset: (3022, 1)
Out[23]:
users
date
2014-06-23 99
2014-06-24 72
2014-06-25 119
2014-06-26 135
2014-06-27 149
... ...
2022-09-26 12340
2022-09-27 13888
2022-09-28 14239
2022-09-29 11574
2022-09-30 12957

3022 rows × 1 columns

In [24]:
# Crear periodos sin datos (gaps) sobreescibiendo los valores con NaN
# ==============================================================================
gaps = [
    ['2020-09-01', '2020-10-10'],
    ['2020-11-08', '2020-12-15'],
]

for gap in gaps:
    data.loc[gap[0]:gap[1]] = np.nan
In [25]:
# División datos en train-test
# ==============================================================================
data = data.loc['2020-06-01':'2021-06-01'].copy()
end_train = '2021-03-01'
data_train = data.loc[: end_train, :]
data_test  = data.loc[end_train:, :]

print(f"Fechas train : {data_train.index.min()} --- {data_train.index.max()}  (n={len(data_train)})")
print(f"Fechas test  : {data_test.index.min()} --- {data_test.index.max()}  (n={len(data_test)})")
Fechas train : 2020-06-01 00:00:00 --- 2021-03-01 00:00:00  (n=274)
Fechas test  : 2021-03-01 00:00:00 --- 2021-06-01 00:00:00  (n=93)
In [26]:
# Gráfico con periodos sin datos
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(7, 3))
data_train.users.plot(ax=ax, label='train', linewidth=1)
data_test.users.plot(ax=ax, label='test', linewidth=1)

for gap in gaps:
    ax.plot(
        [pd.to_datetime(gap[0]), pd.to_datetime(gap[1])],
        [data.users[pd.to_datetime(gap[0]) - pd.Timedelta(days=1)],
         data.users[pd.to_datetime(gap[1]) + pd.Timedelta(days=1)]],
        color = 'red',
        linestyle = '--',
        label = 'gap'
        )

ax.set_title('Número de usuarios BiciMAD')
ax.set_xlabel('')
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
ax.legend(by_label.values(), by_label.keys(), loc='lower right');

Imputación de valores faltantes

In [27]:
# Imputación de valores mediante interpolación lineal
# ======================================================================================
data['users_imputed'] = data['users'].interpolate(method='linear')
data_train = data.loc[: end_train, :]
data_test  = data.loc[end_train:, :]

Modelo con valores imputados

In [28]:
# Crear forecaster (ForecasterAutoreg) con 14 lags
# ==============================================================================
forecaster = ForecasterAutoreg(
                 regressor   = LGBMRegressor(random_state=123, verbose=-1),
                 lags        = 14
             )

# Backtesting: se predicen en cada iteración 7 días
# ==============================================================================
metrica, predicciones = backtesting_forecaster(
                            forecaster         = forecaster,
                            y                  = data.users_imputed,
                            initial_train_size = len(data.loc[:end_train]),
                            fixed_train_size   = False,
                            steps              = 7,
                            metric             = 'mean_absolute_error',
                            refit              = True,
                            verbose            = False
                        )
display(metrica)
predicciones.head(4)
mean_absolute_error
0 2151.339364
Out[28]:
pred
2021-03-02 9679.561409
2021-03-03 10556.841280
2021-03-04 8922.423792
2021-03-05 8874.277159

Dar peso de cero a los valores imputados

Para minimizar la influencia en el modelo de los valores imputados, se define una función que crea pesos acorde a las siguientes reglas:

  • Peso de 0 si la fecha del índice al periodo imputado o está a ménos de 14 días de él.

  • Peso de 1 en caso contrario.

Si una observación tiene un peso de 0, no influye en el entrenamiento del modelo.

✎ Nota

Los valores imputados no deben participar en el proceso de entrenamiento ni como *target* ni como predictor (lags). Por lo tanto, también deben excluirse los valores dentro de una ventana de tamaño tan grande como los lags utilizados.
In [29]:
# Función para asignar pesos a las observaciones
# ==============================================================================
def custom_weights(index):
    """
    Devuelve 0 si el índice se encuentra en algún gap.
    """
    gaps = [
        ['2020-09-01', '2020-10-10'],
        ['2020-11-08', '2020-12-15'],
    ]
    
    missing_dates = [pd.date_range(
                        start = pd.to_datetime(gap[0]) + pd.Timedelta('14d'),
                        end   = pd.to_datetime(gap[1]) + pd.Timedelta('14d'),
                        freq  = 'D'
                    ) for gap in gaps]
    missing_dates = pd.DatetimeIndex(np.concatenate(missing_dates))   
    weights = np.where(index.isin(missing_dates), 0, 1)

    return weights

Se entrena de nuevo un ForecasterAutoreg pero esta vez incluyendo la función custom_weights.

In [30]:
# Crear forecaster (ForecasterAutoreg) con 14 lags
# ==============================================================================
forecaster = ForecasterAutoreg(
                 regressor   = LGBMRegressor(random_state=123, verbose=-1),
                 lags        = 14,
                 weight_func = custom_weights
             )

# Backtesting: se predicen en cada iteración 7 días
# ==============================================================================
metrica, predicciones = backtesting_forecaster(
                            forecaster         = forecaster,
                            y                  = data.users_imputed,
                            initial_train_size = len(data.loc[:end_train]),
                            fixed_train_size   = False,
                            steps              = 7,
                            metric             = 'mean_absolute_error',
                            refit              = True,
                            verbose            = False
                        )
display(metrica)
predicciones.head(4)
mean_absolute_error
0 1904.830714
Out[30]:
pred
2021-03-02 10524.159747
2021-03-03 10087.283682
2021-03-04 8882.926166
2021-03-05 9474.810215

Asignando un peso de 0 a los valores imputados (excluyéndolos del entrenamiento del modelo) se mejora el rendimiento del forecasting.

Información de sesión

In [31]:
import session_info
session_info.show(html=False)
-----
lightgbm            4.4.0
matplotlib          3.9.0
numpy               2.0.1
pandas              2.2.2
session_info        1.0.0
skforecast          0.13.0
sklearn             1.4.2
-----
IPython             8.25.0
jupyter_client      8.6.2
jupyter_core        5.7.2
-----
Python 3.12.4 | packaged by Anaconda, Inc. | (main, Jun 18 2024, 15:03:56) [MSC v.1929 64 bit (AMD64)]
Windows-11-10.0.22631-SP0
-----
Session information updated at 2024-08-11 21:18

Instrucciones para citar

¿Cómo citar este documento?

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

Forecasting series temporales incompletas 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://www.cienciadedatos.net/documentos/py36-forecasting-series-temporales-incompletas.html

¿Cómo citar skforecast?

Si utilizas skforecast en tu investigación o publicación, te lo agradeceríamos mucho que lo cites. ¡Muchas gracias!

Zenodo:

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

APA:

Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast (Version 0.13.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.13.0}, month = {8}, 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

Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊


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.

  • NoComercial: No puedes utilizar el material para fines comerciales.

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