Si te gusta Skforecast , ayúdanos dándonos una estrella en GitHub! ⭐️
Más sobre forecasting en: cienciadedatos.net
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.
# 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.recursive import ForecasterRecursive
from skforecast.model_selection import TimeSeriesFold
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__}")
# Descarga de datos
# ==============================================================================
data = fetch_dataset('bicimad')
data
# 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
# 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)})")
# 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 mediante interpolación lineal
# ======================================================================================
data['users_imputed'] = data['users'].interpolate(method='linear')
data_train = data.loc[: end_train, :]
data_test = data.loc[end_train:, :]
# Crear forecaster (ForecasterAutoreg) con 14 lags
# ==============================================================================
forecaster = ForecasterRecursive(
regressor = LGBMRegressor(random_state=123, verbose=-1),
lags = 14
)
# Backtesting: se predicen en cada iteración 7 días
# ==============================================================================
cv = TimeSeriesFold(
steps = 7,
initial_train_size = len(data.loc[:end_train]),
refit = True,
fixed_train_size = False
)
metrica, predicciones = backtesting_forecaster(
forecaster = forecaster,
y = data.users_imputed,
cv = cv,
metric = 'mean_absolute_error',
verbose = False
)
display(metrica)
predicciones.head(4)
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.# 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
.
# Crear forecaster (ForecasterAutoreg) con 14 lags
# ==============================================================================
forecaster = ForecasterRecursive(
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,
cv = cv,
metric = 'mean_absolute_error',
verbose = False
)
display(metrica)
predicciones.head(4)
Asignando un peso de 0 a los valores imputados (excluyéndolos del entrenamiento del modelo) se mejora el rendimiento del forecasting.
import session_info
session_info.show(html=False)
¿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.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
Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊
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.