Si te gusta Skforecast , ayúdanos dándonos una estrella en GitHub! ⭐️
Más sobre forecasting
En el forecasting de series temporales univariantes, una única serie temporal se modela como una combinación lineal o no lineal de sus lags. Es decir, los valores pasados de la serie se utilizan para predecir su comportamiento futuro. En el forecasting multiserie, dos o más series temporales se modelan conjuntamente mediante un único modelo. Se pueden distinguir dos estrategias:
Múltiples series temporales independientes
En esta situación, cada serie temporal es independiente de las demás o, dicho de otro modo, los valores pasados de una serie no se utilizan como predictores de las otras series. ¿Por qué es útil entonces modelar todo junto? Aunque las series no dependen unas de otras, pueden seguir el mismo patrón intrínseco en cuanto a sus valores pasados y futuros. Por ejemplo, en una misma tienda, las ventas de los productos A y B pueden no estar relacionadas, pero siguen la misma dinámica, la de la tienda.
Para predecir los siguientes n steps, se sigue una estrategia recurisva, recursive multi-step forecasting.
Múltiples series temporales dependientes
Todas las series se modelan teniendo en cuenta que cada serie temporal depende no sólo de sus valores pasados, sino también de los valores pasados de las demás series. Se espera que el modelo no sólo aprenda la información de cada serie por separado, sino que también las relacione. Por ejemplo, las mediciones realizadas por todos los sensores (caudal, temperatura, presión...) instalados en una máquina industrial como un compresor. Series temporales multivariantes user guide.
  Note
La claseForecasterAutoregMultiSeries
y ForecasterAutoregMultiSeriesCustom
permite modelar series temporales independientes. API Reference
La clase ForecasterAutoregMultivariate
permite modelar series temporales dependientes. API Reference
El forecasting multiserie no siempre superan al modelado de la serie individual. Cuál de ellas funciona mejor depende en gran medida de las características del caso de uso al que se aplican. Sin embargo, conviene tener en cuenta la siguiente heurística:
Ventajas de los modelos multiseries:
Es más fácil mantener y controlar un solo modelo que varios.
Dado que todas las series temporales se combinan durante el entrenamiento, cuando las series sean cortas (pocos datos) el modelo tendrá una mayor capacidad de aprendizaje al disponer de más observaciones.
Al combinar múltiples series temporales, el modelo puede aprender patrones más generalizables.
Desventajas de los modelos multiseries:
Si las series no siguen la misma dinámica interna, el modelo puede aprender un patrón que no represente a ninguna de ellas.
Las series pueden enmascararse unas a otras, por lo que el modelo puede no predecirlas todas con el mismo rendimiento.
Es más exigente desde el punto de vista computacional (tiempo y recursos) entrenar y realizar backtesting de un modelo grande que de varios pequeños.
El objetivo de este estudio es comparar los resultados obtenidos por un modelo multiserie frente a la utilización de un modelo diferente para cada serie.
Los datos se han obtenido de Store Item Demand Forecasting Challenge. Este dataset contiene 913.000 transacciones de ventas desde el 2013-01-01 hasta el 2017-12-31 para 50 productos (SKU) en 10 tiendas. El objetivo es predecir las ventas de los próximos 7 días de 50 artículos diferentes en una tienda utilizando el historial disponible de 5 años.
# Librerías
# ======================================================================================
import numpy as np
import pandas as pd
from tqdm import tqdm
import matplotlib.pyplot as plt
plt.style.use('seaborn-v0_8-darkgrid')
from statsmodels.graphics.tsaplots import plot_acf
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import HistGradientBoostingRegressor
from skforecast.ForecasterAutoregMultiSeries import ForecasterAutoregMultiSeries
from skforecast.ForecasterAutoreg import ForecasterAutoreg
from skforecast.model_selection import backtesting_forecaster
from skforecast.model_selection import grid_search_forecaster
from skforecast.model_selection_multiseries import backtesting_forecaster_multiseries
from skforecast.model_selection_multiseries import grid_search_forecaster_multiseries
# Descarga de datos
# ======================================================================================
data = pd.read_csv('./train_stores_kaggle.csv')
display(data)
print(f"Shape: {data.shape}")
# Preparación del dato
# ======================================================================================
selected_store = 2 # Seleccionar una tienda
selected_items = data.item.unique() # Todos los items
#selected_items = [1, 2, 3, 4, 5] # Selección de items para reducir el tiempo de ejecución
data = data[(data['store'] == selected_store) & (data['item'].isin(selected_items))].copy()
data['date'] = pd.to_datetime(data['date'], format='%Y-%m-%d')
data = pd.pivot_table(
data = data,
values = 'sales',
index = 'date',
columns = 'item'
)
data.columns.name = None
data.columns = [f"item_{col}" for col in data.columns]
data = data.asfreq('1D')
data = data.sort_index()
data.head(4)
⚠ Warning
El modelado de 50 *items* puede requerir un tiempo computacional significativo. Siéntase libre de seleccionar sólo un subconjunto de *items* para acelerar la ejecución.El dataset se divide en 3 particiones: una para el entrenamiento, otra para la validación y otra para test.
# Separación datos train-validation-test
# ======================================================================================
end_train = '2016-05-31 23:59:00'
end_val = '2017-05-31 23:59:00'
data_train = data.loc[:end_train, :].copy()
data_val = data.loc[end_train:end_val, :].copy()
data_test = data.loc[end_val:, :].copy()
print(f"Fechas train : {data_train.index.min()} --- {data_train.index.max()} (n={len(data_train)})")
print(f"Fechas validación : {data_val.index.min()} --- {data_val.index.max()} (n={len(data_val)})")
print(f"Fechas test : {data_test.index.min()} --- {data_test.index.max()} (n={len(data_test)})")
Se dibujan cuatro de las series para comprender sus tendencias y patrones. Se recomienda encarecidamente al lector que grafique más de ellas para comprenderlas en profundidad.
# Gráfico series temporales
# ======================================================================================
fig, ax = plt.subplots(figsize=(8, 5))
data.iloc[:, :4].plot(
legend = True,
subplots = True,
sharex = True,
title = 'Ventas de la tienda 2',
ax = ax,
);
# Gráficos de autocorrelación
# ======================================================================================
fig, axes = plt.subplots(nrows=4, ncols=1, figsize=(8, 5), sharex=True)
axes = axes.flat
for i, col in enumerate(data.columns[:4]):
plot_acf(data[col], ax=axes[i], lags=7*5)
axes[i].set_title(f'{col}')
fig.tight_layout()
plt.show()
Los gráficos de autocorrelación muestran una clara asociación entre las ventas de un día y las del mismo día una semana antes. Este tipo de correlación es un indicio de que los modelos autorregresivos pueden funcionar bien.
También hay una estacionalidad semanal común entre las series. Cuanto más similar sea la dinámica entre las series, más probable será que el modelo multiseries aprenda patrones útiles.
Se entrena un modelo diferente para cada artículo de la tienda y se estima su error medio absoluto mediante backtesting.
# Entrenar y realizar backtesting de un modelo para cada item
# ======================================================================================
items = []
mae_values = []
predictions = {}
for i, item in enumerate(tqdm(data.columns)):
# Definir el forecaster
forecaster = ForecasterAutoreg(
regressor = HistGradientBoostingRegressor(random_state=123),
lags = 14,
transformer_y = StandardScaler()
)
# Backtesting forecaster
metric, preds = backtesting_forecaster(
forecaster = forecaster,
y = data[item],
initial_train_size = len(data_train) + len(data_val),
steps = 7,
metric = 'mean_absolute_error',
refit = False,
fixed_train_size = False,
verbose = False,
show_progress = False
)
items.append(item)
mae_values.append(metric)
predictions[item] = preds
# Resultados
uni_series_mae = pd.Series(
data = mae_values,
index = items,
name = 'uni_series_mae'
)
Se entrena un único modelo multiserie para predecir las ventas de cada producto en los próximos 7 días. En este caso, se debe indicar a qué item, level
, se desea realizar el backtesting.
# Entrenar y realizar backtesting con un único modelo para todos los items
# ======================================================================================
items = list(data.columns)
# Definir el forecaster
forecaster_ms = ForecasterAutoregMultiSeries(
regressor = HistGradientBoostingRegressor(random_state=123),
lags = 14,
transformer_series = StandardScaler(),
)
# Backtesting forecaster para todos los items
multi_series_mae, predictions_ms = backtesting_forecaster_multiseries(
forecaster = forecaster_ms,
series = data,
levels = items,
steps = 7,
metric = 'mean_absolute_error',
initial_train_size = len(data_train) + len(data_val),
refit = False,
fixed_train_size = False,
verbose = False,
show_progress = True
)
# Resultados
display(multi_series_mae.head(3))
print('')
display(predictions_ms.head(3))
# Diferencia de la métrica de backtesting para cada item
# ======================================================================================
multi_series_mae = multi_series_mae.set_index('levels')
multi_series_mae.columns = ['multi_series_mae']
results = pd.concat((uni_series_mae, multi_series_mae), axis = 1)
results['improvement'] = results.eval('uni_series_mae - multi_series_mae')
results['improvement_(%)'] = 100 * results.eval('(uni_series_mae - multi_series_mae) / uni_series_mae')
results = results.round(2)
results.style.bar(subset=['improvement_(%)'], align='mid', color=['#d65f5f', '#5fba7d'])
# Mejora media de todos los items
# ======================================================================================
results[['improvement', 'improvement_(%)']].agg(['mean', 'min', 'max'])
# Número de series con mejora positiva y negativa
# ======================================================================================
pd.Series(np.where(results['improvement_(%)'] < 0, 'negative', 'positive')).value_counts()
# Gráfico del item con la máxima mejora
# ======================================================================================
fig, ax=plt.subplots(figsize=(8, 4))
data_test['item_3'].tail(60).plot(ax=ax)
predictions['item_3'].tail(60).plot(ax=ax)
predictions_ms['item_3'].tail(60).plot(ax=ax)
ax.legend(['real', 'predicción forecaster individual', 'predicción multiseries forecaster'])
ax.set_xlabel('')
ax.set_title('Forecasting for Item 3');
# Gráfico del item con la menor mejora
# ======================================================================================
fig, ax=plt.subplots(figsize=(8, 4))
data_test['item_16'].tail(60).plot(ax=ax)
predictions['item_16'].tail(60).plot(ax=ax)
predictions_ms['item_16'].tail(60).plot(ax=ax)
ax.legend(['real', 'predicción forecaster individual', 'predicción multiseries forecaster'])
ax.set_xlabel('')
ax.set_title('Forecasting Item 16');
Si una serie temporal tiene pocas observaciones, la cantidad de información disponible para que el modelo aprenda es limitada. Este es un problema común en casos reales en los que no hay muchos datos históricos disponibles. Los forecasters multiseries no multivariantes combinan todas las series durante el entrenamiento, por lo que pueden acceder a más datos.
En esta sección, se realiza la misma comparación entre el forecasting de una sola serie y el forecasting multiserie, pero, esta vez, la longitud de las series es mucho menor.
# Separación datos train-validation-test
# ======================================================================================
start_train = '2017-01-01 00:00:00'
end_train = '2017-05-01 00:00:00'
end_val = '2017-07-31 23:59:00'
end_test = '2017-09-30 23:59:00'
data = data.loc[start_train:, :].copy()
data_train = data.loc[:end_train, :].copy()
data_val = data.loc[end_train:end_val, :].copy()
data_test = data.loc[end_val:end_test, :].copy()
print(f"Fechas train : {data_train.index.min()} --- {data_train.index.max()} (n={len(data_train)})")
print(f"Fechas validación : {data_val.index.min()} --- {data_val.index.max()} (n={len(data_val)})")
print(f"Fechas test : {data_test.index.min()} --- {data_test.index.max()} (n={len(data_test)})")
# Entrenar y realizar backtesting de un modelo para cada item
# ======================================================================================
items = []
mae_values = []
predictions = {}
for i, item in enumerate(tqdm(data.columns)):
forecaster = ForecasterAutoreg(
regressor = HistGradientBoostingRegressor(random_state=123),
lags = 14,
transformer_y = StandardScaler()
)
metric, preds = backtesting_forecaster(
forecaster = forecaster,
y = data[item],
initial_train_size = len(data_train) + len(data_val),
steps = 7,
metric = 'mean_absolute_error',
refit = False,
fixed_train_size = False,
verbose = False,
show_progress = False
)
items.append(item)
mae_values.append(metric)
predictions[item] = preds
uni_series_mae = pd.Series(
data = mae_values,
index = items,
name = 'uni_series_mae'
)
# Entrenar y realizar backtesting con un único modelo para todos los items
# ======================================================================================
# Definir el forecaster
forecaster_ms = ForecasterAutoregMultiSeries(
regressor = HistGradientBoostingRegressor(random_state=123),
lags = 14,
transformer_series = StandardScaler(),
)
# Backtesting forecaster para todos los items
multi_series_mae, predictions_ms = backtesting_forecaster_multiseries(
forecaster = forecaster_ms,
series = data,
levels = None, # Si es None se seleccionan todos los niveles
steps = 7,
metric = 'mean_absolute_error',
initial_train_size = len(data_train) + len(data_val),
refit = False,
fixed_train_size = False,
verbose = False
)
# Results
display(multi_series_mae.head(3))
print('')
display(predictions_ms.head(3))
# Diferencia de la métrica de backtesting para cada item
# ======================================================================================
multi_series_mae = multi_series_mae.set_index('levels')
multi_series_mae.columns = ['multi_series_mae']
results = pd.concat((uni_series_mae, multi_series_mae), axis = 1)
results['improvement'] = results.eval('uni_series_mae - multi_series_mae')
results['improvement_(%)'] = 100 * results.eval('(uni_series_mae - multi_series_mae) / uni_series_mae')
results = results.round(2)
results
# Mejora media de todos los items
# ======================================================================================
results[['improvement', 'improvement_(%)']].agg(['mean', 'min', 'max'])
# Número de series con mejora positiva y negativa
# ======================================================================================
pd.Series(np.where(results['improvement_(%)'] < 0, 'negative', 'positive')).value_counts()
La mejora media ha pasado del 6.6 al 8.2%. La ventaja de utilizar un forecaster multiserie parece crecer a medida que se reduce la longitud de las series disponibles.
En las secciones anteriores, la comparación entre forecaster se ha realizado sin optimizar los hiperparámetros de los regresores. Para una comparación justa, se utiliza una estrategia de grid search con el fin de seleccionar la mejor configuración para cada forecaster. Véase más información en hyperparameter tuning and lags selection.
⚠ Warning
La sección siguiente puede requerir un tiempo de ejecución considerable (alrededor de 45 minutos). Siéntase libre de seleccionar sólo un subconjunto de items para acelerar la ejecución.# Ocultar progress bar tqdm
# ======================================================================================
from tqdm import tqdm
from functools import partialmethod
tqdm.__init__ = partialmethod(tqdm.__init__, disable=True)
# Búsqueda de hiperparámetros y backtesting de un modelo para cada item
# ======================================================================================
items = []
mae_values = []
lags_grid = [7, 14, 21]
param_grid = {
'max_iter': [100, 500],
'max_depth': [3, 5, 10, None],
'learning_rate': [0.01, 0.1]
}
for i, item in enumerate(data.columns):
forecaster = ForecasterAutoreg(
regressor = HistGradientBoostingRegressor(random_state=123),
lags = 14,
transformer_y = StandardScaler()
)
results_grid = grid_search_forecaster(
forecaster = forecaster,
y = data.loc[:end_val, item],
lags_grid = lags_grid,
param_grid = param_grid,
steps = 7,
metric = 'mean_absolute_error',
initial_train_size = len(data_train),
refit = False,
fixed_train_size = False,
return_best = True,
verbose = False,
show_progress = False
)
metric, preds = backtesting_forecaster(
forecaster = forecaster,
y = data[item],
initial_train_size = len(data_train) + len(data_val),
steps = 7,
metric = 'mean_absolute_error',
refit = False,
fixed_train_size = False,
verbose = False,
show_progress = False
)
items.append(item)
mae_values.append(metric)
uni_series_mae = pd.Series(
data = mae_values,
index = items,
name = 'uni_series_mae'
)
# Búsqueda de hiperparámetros y backtesting para un modelo multiserie
# ======================================================================================
lags_grid = [7, 14, 21]
param_grid = {
'max_iter': [100, 500],
'max_depth': [3, 5, 10, None],
'learning_rate': [0.01, 0.1]
}
forecaster_ms = ForecasterAutoregMultiSeries(
regressor = HistGradientBoostingRegressor(random_state=123),
lags = 14,
transformer_series = StandardScaler(),
)
results_grid_ms = grid_search_forecaster_multiseries(
forecaster = forecaster_ms,
series = data.loc[:end_val, :],
levels = None, # Si es None se seleccionan todos los niveles
lags_grid = lags_grid,
param_grid = param_grid,
steps = 7,
metric = 'mean_absolute_error',
initial_train_size = len(data_train),
refit = False,
fixed_train_size = False,
return_best = True,
verbose = False
)
multi_series_mae, predictions_ms = backtesting_forecaster_multiseries(
forecaster = forecaster_ms,
series = data,
levels = None, # Si es None se seleccionan todos los niveles
steps = 7,
metric = 'mean_absolute_error',
initial_train_size = len(data_train) + len(data_val),
refit = False,
fixed_train_size = False,
verbose = False
)
# Diferencia de la métrica de backtesting para cada item
# ======================================================================================
multi_series_mae = multi_series_mae.set_index('levels')
multi_series_mae.columns = ['multi_series_mae']
results = pd.concat((uni_series_mae, multi_series_mae), axis = 1)
results['improvement'] = results.eval('uni_series_mae - multi_series_mae')
results['improvement_(%)'] = 100 * results.eval('(uni_series_mae - multi_series_mae) / uni_series_mae')
results = results.round(2)
results.style.bar(subset=['improvement_(%)'], align='mid', color=['#d65f5f', '#5fba7d'])
# Mejora media de todos los items
# ======================================================================================
results[['improvement', 'improvement_(%)']].agg(['mean', 'min', 'max'])
# Número de series con mejora positiva y negativa
# ======================================================================================
pd.Series(np.where(results['improvement_(%)'] < 0, 'negative', 'positive')).value_counts()
Tras identificar la combinación de lags e hiperparámetros que logran el mejor rendimiento predictivo para cada forecaster, un número superior de modelos univariantes han logrado una mayor capacidad predictiva al generalizar mejor sus propios datos (un item). Aun así, el modelo multiserie proporciona mejores resultados para la mayoría de los items.
Los pesos se utilizan para controlar la influencia que tiene cada observación en el entrenamiento del modelo. ForecasterAutoregMultiseries
acepta dos tipos de pesos:
series_weights
controla la importancia relativa de cada serie. Si una serie tiene el doble de peso que las demás, las observaciones de esa serie influyen el doble en el entrenamiento. Cuanto mayor sea el peso de una serie en relación con las demás, más se centrará el modelo en intentar aprender esa serie.
weight_func
controla la importancia relativa de cada observación en función del índice. Por ejemplo, una función que asigna un peso menor a ciertas fechas.
Si se indican los dos tipos de pesos, estos se multiplican para crear los pesos finales como se muestra en la figura. El sample_weight
resultante no puede contener valores negativos.
Más información sobre weights in multi-series forecasting y weighted time series forecasting con skforecast.
En este ejemplo, item_1
tiene una mayor importancia relativa entre series (pesa 3 veces más que el resto de series), y las observaciones entre '2013-12-01' y '2014-01-31' se consideran no representativas y se les aplica un peso de 0.
# Pesos en forecasting multiseries
# ======================================================================================
# `series_weights`
series_weights = {'item_1': 3.} # Las series que no aparezcan en el dict tienen un peso de 1
# Se puede pasar un diccionario a `weight_func` para aplicar diferentes funciones para cada serie
# Las series que no aparezcan en el dict tienen un peso de 1
def custom_weights(index):
"""
Devuelve 0 si el índice está entre '2013-12-01' y '2014-01-31', 1 en caso contrario.
"""
weights = np.where(
(index >= '2013-12-01') & (index <= '2014-01-31'),
0,
1
)
return weights
forecaster = ForecasterAutoregMultiSeries(
regressor = HistGradientBoostingRegressor(random_state=123),
lags = 14,
transformer_series = StandardScaler(),
transformer_exog = None,
weight_func = custom_weights,
series_weights = series_weights
)
forecaster.fit(series=data)
forecaster.predict(steps=7).head(3)
Este caso de uso muestra como un modelo multiserie puede presentar ventajas sobre varios modelos individuales cuando se predicen series temporales con una dinámica similar.
Más allá de las posibles mejoras en la predicción, también es importante tener en cuenta la ventaja de tener un solo modelo que mantener.
import session_info
session_info.show(html=False)
¿Cómo citar este documento?
Multi-series forecasting with python and skforecast by Joaquín Amat Rodrigo and Javier Escobar Ortiz, available under a Attribution 4.0 International (CC BY 4.0) at https://www.cienciadedatos.net/documentos/py44-multi-series-forecasting-skforecast-español.html
¿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! 😊
This work by Joaquín Amat Rodrigo and Javier Escobar Ortiz is licensed under a Creative Commons Attribution 4.0 International License.