Más sobre forecasting en: cienciadedatos.net
- Forecasting series temporales con machine learning
- Modelos ARIMA y SARIMAX
- Forecasting series temporales con gradient boosting: XGBoost, LightGBM y CatBoost
- Global Forecasting: Multi-series forecasting
- Forecasting de la demanda eléctrica con machine learning
- Forecasting con deep learning
- Forecasting de visitas a página web con machine learning
- Forecasting del precio de Bitcoin
- Forecasting probabilístico
- Forecasting de demanda intermitente
- Reducir el impacto del Covid en modelos de forecasting
- Modelar series temporales con tendencia utilizando modelos de árboles

Introducción
A comienzos de 2025, Kaggle lanzó la Forecasting Sticker Sales competition como parte de su serie Playground (Temporada 5, Episodio 1), proponiendo un reto práctico para los entusiastas de la predicción de series temporales.
El desafío consistía en predecir las ventas mensuales de cinco productos con la marca Kaggle en seis países y tres tipos de tiendas, lo que dio lugar a un total de 90 series temporales. El período a predecir abarcaba de 2017 a 2019, mientras que los datos históricos de ventas, correspondientes a los años 2010 a 2016, estaban disponibles para el entrenamiento de los modelos. La métrica utilizada para evaluar el rendimiento fue el Mean Absolute Percentage Error (MAPE), que mide la precisión de las predicciones en términos porcentuales.
Este documento sirve de guía introductoria sobre el uso de modelos de forecasting globales para predecir multiples series temporales usando skforecast. Además, acompañará al lector a través de las etapas fundamentales de un proyecto típico de forecasting, que incluyen: preparación de los datos, entrenamiento, predicción y evaluación del modelo. Si bien el enfoque está centrado en la simplicidad y claridad, alcanzar un rendimiento óptimo requerirá que el lector continue iterando y mejorando el modelo. Para ello, se recomienda explorar las soluciones propuestas por otros participantes en la competición, así como los tutoriales y artículos disponibles en cienciadedatos.net.
✎ Note
Este documento forma parte de una serie sobre modelos de forecasting globales:- Modelos de forecasting globales: modelado de múltiples series temporales con machine learning
- Forecasting escalable: modelado de mil de series temporales con un único modelo global
- Modelos de forecasting globales: Análisis comparativo de modelos de una y múltiples series
- Modelos de forecasting globales: Guía paso a paso con Kaggle Sticker Sales
Librerías
Las librerías utilizadas en este notebook son:
# Manipulación de datos
# ==============================================================================
import pandas as pd
import numpy as np
from itertools import product
# Gráficos
# ==============================================================================
from matplotlib import pyplot as plt
from skforecast.plot import set_dark_theme
# Modelado
# ==============================================================================
import skforecast
from skforecast.recursive import ForecasterRecursiveMultiSeries
from skforecast.model_selection import bayesian_search_forecaster_multiseries, OneStepAheadFold
from skforecast.preprocessing import reshape_series_long_to_dict, reshape_exog_long_to_dict
from sklearn.metrics import mean_absolute_percentage_error
from sklearn.compose import make_column_transformer
from sklearn.preprocessing import OrdinalEncoder
import lightgbm
from lightgbm import LGBMRegressor
import feature_engine
from feature_engine.datetime import DatetimeFeatures
from feature_engine.creation import CyclicalFeatures
import holidays
# Warnings
# ==============================================================================
import warnings
from skforecast.exceptions import MissingValuesWarning
warnings.simplefilter('ignore', category=MissingValuesWarning)
color = '\033[1m\033[38;5;208m'
print(f"{color}Versión skforecast: {skforecast.__version__}")
print(f"{color}Versión lightgbm: {lightgbm.__version__}")
print(f"{color}Versión feature_engine: {feature_engine.__version__}")
Versión skforecast: 0.17.0 Versión lightgbm: 4.6.0 Versión feature_engine: 1.8.3
Datos
Los datos utilizados en este notebook son de la Kaggle competition: Forecasting Sticker Sales" (Walter Reade and Elizabeth Park, 2025).
# Datos
# ==============================================================================
data_train = pd.read_csv('train.csv')
data_test = pd.read_csv('test.csv')
display(data_train.head())
display(data_test.head())
id | date | country | store | product | num_sold | |
---|---|---|---|---|---|---|
0 | 0 | 2010-01-01 | Canada | Discount Stickers | Holographic Goose | NaN |
1 | 1 | 2010-01-01 | Canada | Discount Stickers | Kaggle | 973.0 |
2 | 2 | 2010-01-01 | Canada | Discount Stickers | Kaggle Tiers | 906.0 |
3 | 3 | 2010-01-01 | Canada | Discount Stickers | Kerneler | 423.0 |
4 | 4 | 2010-01-01 | Canada | Discount Stickers | Kerneler Dark Mode | 491.0 |
id | date | country | store | product | |
---|---|---|---|---|---|
0 | 230130 | 2017-01-01 | Canada | Discount Stickers | Holographic Goose |
1 | 230131 | 2017-01-01 | Canada | Discount Stickers | Kaggle |
2 | 230132 | 2017-01-01 | Canada | Discount Stickers | Kaggle Tiers |
3 | 230133 | 2017-01-01 | Canada | Discount Stickers | Kerneler |
4 | 230134 | 2017-01-01 | Canada | Discount Stickers | Kerneler Dark Mode |
# Convertir la columna 'date' a tipo datetime
# ==============================================================================
data_train['date'] = pd.to_datetime(data_train['date'])
data_test['date'] = pd.to_datetime(data_test['date'])
# Crear una nueva columna 'unique_id' que identifica cada serie temporal como la combinación
# de las columnas 'country', 'store' y 'product'
# ==============================================================================
data_train['unique_id'] = (
data_train['country'] + '_' +
data_train['store'] + '_' +
data_train['product']
).replace(' ', '_')
data_test['unique_id'] = (
data_test['country'] + '_' +
data_test['store'] + '_' +
data_test['product']
).replace(' ', '_')
display(data_train.head())
display(data_test.head())
id | date | country | store | product | num_sold | unique_id | |
---|---|---|---|---|---|---|---|
0 | 0 | 2010-01-01 | Canada | Discount Stickers | Holographic Goose | NaN | Canada_Discount Stickers_Holographic Goose |
1 | 1 | 2010-01-01 | Canada | Discount Stickers | Kaggle | 973.0 | Canada_Discount Stickers_Kaggle |
2 | 2 | 2010-01-01 | Canada | Discount Stickers | Kaggle Tiers | 906.0 | Canada_Discount Stickers_Kaggle Tiers |
3 | 3 | 2010-01-01 | Canada | Discount Stickers | Kerneler | 423.0 | Canada_Discount Stickers_Kerneler |
4 | 4 | 2010-01-01 | Canada | Discount Stickers | Kerneler Dark Mode | 491.0 | Canada_Discount Stickers_Kerneler Dark Mode |
id | date | country | store | product | unique_id | |
---|---|---|---|---|---|---|
0 | 230130 | 2017-01-01 | Canada | Discount Stickers | Holographic Goose | Canada_Discount Stickers_Holographic Goose |
1 | 230131 | 2017-01-01 | Canada | Discount Stickers | Kaggle | Canada_Discount Stickers_Kaggle |
2 | 230132 | 2017-01-01 | Canada | Discount Stickers | Kaggle Tiers | Canada_Discount Stickers_Kaggle Tiers |
3 | 230133 | 2017-01-01 | Canada | Discount Stickers | Kerneler | Canada_Discount Stickers_Kerneler |
4 | 230134 | 2017-01-01 | Canada | Discount Stickers | Kerneler Dark Mode | Canada_Discount Stickers_Kerneler Dark Mode |
Rango de fechas disponibles
# Valores únicos de las columnas 'country', 'store' y 'product'
# ==============================================================================
print('Number of unique time series:', data_train['unique_id'].nunique())
print('Unique countries :', data_train['country'].unique())
print('Unique stores :', data_train['store'].unique())
print('Unique products :', data_train['product'].unique())
Number of unique time series: 90 Unique countries : ['Canada' 'Finland' 'Italy' 'Kenya' 'Norway' 'Singapore'] Unique stores : ['Discount Stickers' 'Stickers for Less' 'Premium Sticker Mart'] Unique products : ['Holographic Goose' 'Kaggle' 'Kaggle Tiers' 'Kerneler' 'Kerneler Dark Mode']
# Rango de fechas en los conjuntos de entrenamiento y test
# ==============================================================================
print('Date range in the training set :', data_train['date'].min(), 'to', data_train['date'].max())
print('Date range in the test set :', data_test['date'].min(), 'to', data_test['date'].max())
Date range in the training set : 2010-01-01 00:00:00 to 2016-12-31 00:00:00 Date range in the test set : 2017-01-01 00:00:00 to 2019-12-31 00:00:00
# Rango de fechas disponible en el conjunto de entrenamiento para cada serie temporal
# ==============================================================================
date_range = data_train.groupby('unique_id')['date'].agg(['min', 'max', 'count'])
date_range = date_range.rename(columns={'min': 'start_date', 'max': 'end_date'})
date_range
start_date | end_date | count | |
---|---|---|---|
unique_id | |||
Canada_Discount Stickers_Holographic Goose | 2010-01-01 | 2016-12-31 | 2557 |
Canada_Discount Stickers_Kaggle | 2010-01-01 | 2016-12-31 | 2557 |
Canada_Discount Stickers_Kaggle Tiers | 2010-01-01 | 2016-12-31 | 2557 |
Canada_Discount Stickers_Kerneler | 2010-01-01 | 2016-12-31 | 2557 |
Canada_Discount Stickers_Kerneler Dark Mode | 2010-01-01 | 2016-12-31 | 2557 |
... | ... | ... | ... |
Singapore_Stickers for Less_Holographic Goose | 2010-01-01 | 2016-12-31 | 2557 |
Singapore_Stickers for Less_Kaggle | 2010-01-01 | 2016-12-31 | 2557 |
Singapore_Stickers for Less_Kaggle Tiers | 2010-01-01 | 2016-12-31 | 2557 |
Singapore_Stickers for Less_Kerneler | 2010-01-01 | 2016-12-31 | 2557 |
Singapore_Stickers for Less_Kerneler Dark Mode | 2010-01-01 | 2016-12-31 | 2557 |
90 rows × 3 columns
# Gráfico de 3 series temporales
# ==============================================================================
set_dark_theme()
series_to_plot = [
'Italy_Stickers for Less_Kerneler Dark Mode',
'Singapore_Stickers for Less_Holographic Goose',
'Italy_Stickers for Less_Kaggle'
]
for series in series_to_plot:
fig, ax = plt.subplots(1, 1, figsize=(7, 2.5))
data_train.query('unique_id == @series').plot(
x='date',
y=['num_sold'],
ax=ax,
title=series,
linewidth=0.3,
legend=False,
)
plt.show()
Valores nulos en las series
Solo 9 series tienen valores nulos en la variable objetivo num_sold
, la mayoría de ellas pertenecen al producto "Holographic Goose". Dado que no se proporciona información sobre los valores nulos en la descripción de la competición, es necesario tomar una decisión sobre cómo manejarlos:
Los valores nulos significan que el producto no se vendió en ese mes, lo que implica que las ventas son 0.
Los valores nulos significan que las ventas del producto son desconocidas, por lo que podrían ser 0 o cualquier otro valor.
# Asegurar que todas las series temporales estén completas sin huecos intermedios
# ==============================================================================
data_train = (
data_train
.groupby('unique_id')
.apply(lambda group: group.set_index('date').asfreq('D', fill_value=np.nan), include_groups=False)
.reset_index()
)
data_train
unique_id | date | id | country | store | product | num_sold | |
---|---|---|---|---|---|---|---|
0 | Canada_Discount Stickers_Holographic Goose | 2010-01-01 | 0 | Canada | Discount Stickers | Holographic Goose | NaN |
1 | Canada_Discount Stickers_Holographic Goose | 2010-01-02 | 90 | Canada | Discount Stickers | Holographic Goose | NaN |
2 | Canada_Discount Stickers_Holographic Goose | 2010-01-03 | 180 | Canada | Discount Stickers | Holographic Goose | NaN |
3 | Canada_Discount Stickers_Holographic Goose | 2010-01-04 | 270 | Canada | Discount Stickers | Holographic Goose | NaN |
4 | Canada_Discount Stickers_Holographic Goose | 2010-01-05 | 360 | Canada | Discount Stickers | Holographic Goose | NaN |
... | ... | ... | ... | ... | ... | ... | ... |
230125 | Singapore_Stickers for Less_Kerneler Dark Mode | 2016-12-27 | 229764 | Singapore | Stickers for Less | Kerneler Dark Mode | 1016.0 |
230126 | Singapore_Stickers for Less_Kerneler Dark Mode | 2016-12-28 | 229854 | Singapore | Stickers for Less | Kerneler Dark Mode | 1062.0 |
230127 | Singapore_Stickers for Less_Kerneler Dark Mode | 2016-12-29 | 229944 | Singapore | Stickers for Less | Kerneler Dark Mode | 1178.0 |
230128 | Singapore_Stickers for Less_Kerneler Dark Mode | 2016-12-30 | 230034 | Singapore | Stickers for Less | Kerneler Dark Mode | 1357.0 |
230129 | Singapore_Stickers for Less_Kerneler Dark Mode | 2016-12-31 | 230124 | Singapore | Stickers for Less | Kerneler Dark Mode | 1312.0 |
230130 rows × 7 columns
# Porcentaje de valores ausentes en cada serie
# ==============================================================================
missing_pct = (
data_train
.groupby('unique_id')
.apply(lambda group: group['num_sold'].isna().mean() * 100, include_groups=False)
.sort_values(ascending=False)
.reset_index(name='missing_values_pct')
)
missing_pct.query('missing_values_pct > 0')
unique_id | missing_values_pct | |
---|---|---|
0 | Canada_Discount Stickers_Holographic Goose | 100.000000 |
1 | Kenya_Discount Stickers_Holographic Goose | 100.000000 |
2 | Kenya_Stickers for Less_Holographic Goose | 53.109112 |
3 | Canada_Stickers for Less_Holographic Goose | 51.153696 |
4 | Kenya_Premium Sticker Mart_Holographic Goose | 25.263981 |
5 | Canada_Premium Sticker Mart_Holographic Goose | 14.861165 |
6 | Kenya_Discount Stickers_Kerneler | 2.463825 |
7 | Canada_Discount Stickers_Kerneler | 0.039108 |
8 | Kenya_Discount Stickers_Kerneler Dark Mode | 0.039108 |
Las series que únicamente contienen valores nulos se excluyen del conjunto de entrenamiento, ya que no aportan información adicional. Para las series restantes, no se imputan los valores nulos, ya que el regresor elegido, LightGBM, es capaz de manejar directamente los valores NaN.
# Eliminar series con un 100% de valores ausentes
# ==============================================================================
series_to_drop = ['Canada_Discount Stickers_Holographic Goose', 'Kenya_Discount Stickers_Holographic Goose']
data_train = data_train.query("unique_id not in @series_to_drop").copy()
✎ Note
Tal y como se describe en la discusión de la competición, se pueden obtener mejores resultados rellenando los valores nulos con un número aleatorio entre 1 y el valor mínimo dentro del país (Kenya = 5, Canada = 200). Sin embargo, este enfoque parece basarse en envíos repetidos de la solución. El enfoque adoptado en este notebook es dejar los valores nulos tal como están y permitir que el modelo aprenda de ellos. Esta es una práctica común en el pronóstico de series temporales, ya que permite que el modelo aprenda de los patrones en los datos sin introducir valores artificiales. Sin embargo, el lector puede probarlo con el siguiente código:# Imputar la serie "Canada_Discount Stickers_Holographic Goose" con valores aleatorios entre 1 y 200
# ==============================================================================
# mask = data_train['unique_id'] == 'Canada_Discount Stickers_Holographic Goose'
# data_train.loc[mask, 'num_sold'] = np.random.randint(1, 200, size=sum(mask))
# Imputar la serie "Kenya_Discount Stickers_Holographic Goose" con valores aleatorios entre 1 y 5
# ==============================================================================
# mask = data_train['unique_id'] == 'Kenya_Discount Stickers_Holographic Goose'
# data_train.loc[mask, 'num_sold'] = np.random.randint(1, 5, size=sum(mask))
Feature engineering
Transformación logarítmica
Se transforman las series utilizando la función logaritmo. Esta transformación es especialmente útil para series con una marcada asimetría, ya que reduce el impacto de los valores extremos y ayuda a normalizar la distribución de los datos. Además, evita predicciones negativas, lo cual es un problema común al utilizar modelos de machine learning.
La transformación logarítmica se aplica a la variable objetivo num_sold
y los valores resultantes se almacenan en una nueva columna llamada log_num_sold
. Una vez realizadas las predicciones, se aplica la transformación inversa para obtener la escala original de los datos.
# Transformar la columna 'num_sold' a escala logarítmica
# ==============================================================================
data_train['log_num_sold'] = np.log1p(data_train['num_sold'])
data_train.head()
unique_id | date | id | country | store | product | num_sold | log_num_sold | |
---|---|---|---|---|---|---|---|---|
2557 | Canada_Discount Stickers_Kaggle | 2010-01-01 | 1 | Canada | Discount Stickers | Kaggle | 973.0 | 6.881411 |
2558 | Canada_Discount Stickers_Kaggle | 2010-01-02 | 91 | Canada | Discount Stickers | Kaggle | 881.0 | 6.782192 |
2559 | Canada_Discount Stickers_Kaggle | 2010-01-03 | 181 | Canada | Discount Stickers | Kaggle | 1003.0 | 6.911747 |
2560 | Canada_Discount Stickers_Kaggle | 2010-01-04 | 271 | Canada | Discount Stickers | Kaggle | 744.0 | 6.613384 |
2561 | Canada_Discount Stickers_Kaggle | 2010-01-05 | 361 | Canada | Discount Stickers | Kaggle | 707.0 | 6.562444 |
Variables exógenas
Además del histórico de ventas, la incorporación de variables adicionales, también conocidas como variables exógenas, puede mejorar el rendimiento del modelo. Por ejemplo, las variables relacionadas con el calendario, como el mes, el día de la semana y los días festivos.
# Generar días festivos para cada país y año
# ==============================================================================
countries = ['Canada', 'Finland', 'Italy', 'Kenya', 'Norway', 'Singapore']
years = [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019]
all_holidays = []
for country_code, year in product(countries, years):
try:
country_holidays = holidays.country_holidays(country_code, years=year)
all_holidays.extend([
{
'country': country_code,
'date': pd.to_datetime(date),
'holiday_name': name
}
for date, name in country_holidays.items()
])
except NotImplementedError:
print(f"Country '{country_code}' is not supported by the holidays library.")
df_holidays = (
pd.DataFrame(all_holidays)
.groupby(['country', 'date'], as_index=False)
.agg({'holiday_name': ', '.join})
)
df_holidays['is_holiday'] = 1
df_holidays.sort_values(by=['country', 'date'], inplace=True)
Para tener en cuenta el impacto retrasado de los días festivos en las ventas, se recomienda incluir lags de la variable de días festivos. Esto implica crear nuevos indicadores que muestren si el día anterior o el siguiente es un día festivo.
Dado que el DataFrame df_holidays
solo contiene las fechas de los días festivos, primero debe expandirse para incluir todas las fechas del calendario de cada país antes de calcular los lags.
# Crear un rango de fechas para cada país y año
date_range = pd.date_range(start=f"{min(years)}-01-01", end=f"{max(years)}-12-31")
all_dates = pd.MultiIndex.from_product([countries, date_range], names=['country', 'date']).to_frame(index=False)
# Merge con los días festivos
df_calendar = all_dates.merge(df_holidays, on=['country', 'date'], how='left')
df_calendar['is_holiday'] = df_calendar['is_holiday'].fillna(0).astype(int)
# Crear columnas adicionales para días festivos (pasados y futuros)
for i in [1, 2, 5, 7, 9]:
df_calendar[f'is_holiday_lag_{i}'] = df_calendar.groupby('country')['is_holiday'].shift(i).fillna(0).astype(int)
df_calendar[f'is_holiday_next_{i}'] = df_calendar.groupby('country')['is_holiday'].shift(-i).fillna(0).astype(int)
df_calendar.head()
country | date | holiday_name | is_holiday | is_holiday_lag_1 | is_holiday_next_1 | is_holiday_lag_2 | is_holiday_next_2 | is_holiday_lag_5 | is_holiday_next_5 | is_holiday_lag_7 | is_holiday_next_7 | is_holiday_lag_9 | is_holiday_next_9 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Canada | 2010-01-01 | New Year's Day | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | Canada | 2010-01-02 | NaN | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | Canada | 2010-01-03 | NaN | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | Canada | 2010-01-04 | NaN | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | Canada | 2010-01-05 | NaN | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
# Añadir las variables de calendario y codificarlas con codificación cíclica
# ==============================================================================
features_to_extract = [
'month',
'week',
'day_of_week',
]
calendar_transformer = DatetimeFeatures(
variables = 'date',
features_to_extract = features_to_extract,
drop_original = False,
)
df_calendar = calendar_transformer.fit_transform(df_calendar)
df_calendar.columns = df_calendar.columns.str.replace('date_', '', regex=False)
features_to_encode = [
"month",
"week",
"day_of_week",
]
max_values = {
"month": 12,
"week": 52,
"day_of_week": 6,
}
cyclical_encoder = CyclicalFeatures(
variables = features_to_encode,
max_values = max_values,
drop_original = True
)
df_calendar = cyclical_encoder.fit_transform(df_calendar)
df_calendar.head()
country | date | holiday_name | is_holiday | is_holiday_lag_1 | is_holiday_next_1 | is_holiday_lag_2 | is_holiday_next_2 | is_holiday_lag_5 | is_holiday_next_5 | is_holiday_lag_7 | is_holiday_next_7 | is_holiday_lag_9 | is_holiday_next_9 | month_sin | month_cos | week_sin | week_cos | day_of_week_sin | day_of_week_cos | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Canada | 2010-01-01 | New Year's Day | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | -8.660254e-01 | -0.5 |
1 | Canada | 2010-01-02 | NaN | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | -8.660254e-01 | 0.5 |
2 | Canada | 2010-01-03 | NaN | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | -2.449294e-16 | 1.0 |
3 | Canada | 2010-01-04 | NaN | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | 0.000000e+00 | 1.0 |
4 | Canada | 2010-01-05 | NaN | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | 8.660254e-01 | 0.5 |
# Añadir todas las variables exógenas al conjunto de entrenamiento
# ==============================================================================
exog_features = [
'is_holiday',
'is_holiday_lag_1',
'is_holiday_lag_2',
'is_holiday_lag_5',
'is_holiday_lag_7',
'is_holiday_lag_9',
'is_holiday_next_1',
'is_holiday_next_2',
'is_holiday_next_5',
'month_sin',
'month_cos',
'week_sin',
'week_cos',
'day_of_week_sin',
'day_of_week_cos',
'country',
'store',
'product',
]
data_train = data_train.merge(
right = df_calendar.drop(columns=['holiday_name']),
how = 'left',
left_on = ['country', 'date'],
right_on = ['country', 'date'],
validate = 'many_to_one'
)
data_train.head()
unique_id | date | id | country | store | product | num_sold | log_num_sold | is_holiday | is_holiday_lag_1 | ... | is_holiday_lag_7 | is_holiday_next_7 | is_holiday_lag_9 | is_holiday_next_9 | month_sin | month_cos | week_sin | week_cos | day_of_week_sin | day_of_week_cos | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Canada_Discount Stickers_Kaggle | 2010-01-01 | 1 | Canada | Discount Stickers | Kaggle | 973.0 | 6.881411 | 1 | 0 | ... | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | -8.660254e-01 | -0.5 |
1 | Canada_Discount Stickers_Kaggle | 2010-01-02 | 91 | Canada | Discount Stickers | Kaggle | 881.0 | 6.782192 | 0 | 1 | ... | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | -8.660254e-01 | 0.5 |
2 | Canada_Discount Stickers_Kaggle | 2010-01-03 | 181 | Canada | Discount Stickers | Kaggle | 1003.0 | 6.911747 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | -2.449294e-16 | 1.0 |
3 | Canada_Discount Stickers_Kaggle | 2010-01-04 | 271 | Canada | Discount Stickers | Kaggle | 744.0 | 6.613384 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | 0.000000e+00 | 1.0 |
4 | Canada_Discount Stickers_Kaggle | 2010-01-05 | 361 | Canada | Discount Stickers | Kaggle | 707.0 | 6.562444 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0.5 | 0.866025 | 0.120537 | 0.992709 | 8.660254e-01 | 0.5 |
5 rows × 25 columns
Modelado
Dado que hay 90 series temporales diferentes, se pueden adoptar dos enfoques diferentes: modelar cada serie temporal por separado, conocido como local forecasting, o modelar todas las series temporales juntas, conocido como global forecasting.
En este caso, se utiliza un modelo de forecasting global, que generalmente cumple mejor con los requisitos de las aplicaciones del mundo real, donde el número de series temporales es grande y el costo computacional de entrenar un solo modelo por serie no es factible. Además, los modelos de forecasting globales implementados en skforecast son capaces de predecir nuevas series temporales que no estaban presentes en los datos de entrenamiento, lo que será útil al predecir el producto "Holographic Goose".
Skforecast accepta diferentes estructuras de datos al crear modelos de forecasting globales (ForecasterRecursiveMultiSeries
):
Si todas las series tienen la misma longitud y comparten las mismas variables exógenas, los datos se pueden pasar como un DataFrame de pandas donde cada columna representa una serie temporal y cada fila corresponde a una fecha. El índice del DataFrame debe ser un índice de fecha y hora.
Si las series tienen diferentes longitudes, los datos deben pasarse como un diccionario. Las claves del diccionario representan los nombres de las series y los valores son las series en sí. Para facilitar esto, se puede utilizar la función
reshape_series_long_to_dict
, que toma un DataFrame en "formato largo" y devuelve un diccionario de pandas Series. De manera similar, si las variables exógenas difieren (en valores o tipo) entre las series, los datos también deben proporcionarse como un diccionario. En este caso, se utiliza la funciónreshape_exog_long_to_dict
, que convierte un DataFrame en "formato largo" en un diccionario de variables exógenas (ya sea pandas Series o pandas DataFrames).
En este caso, las variables exógenas relacionadas con los días festivos difieren para cada serie, ya que son específicas del país donde se vende el producto. Por lo tanto, los datos deben pasarse como un diccionario.
✎ Note
Desde la versión 0.17.0
, también es posible pasar los datos como un DataFrame de MultiIndex, donde el primer nivel del índice es el ID de la serie y el segundo nivel es un DatetimeIndex. Esto permite una interfaz más amigable, aunque es menos eficiente en términos de velocidad y uso de memoria.
# Transformar las series y las variables exógenas a diccionarios
# ==============================================================================
series_dict = reshape_series_long_to_dict(
data = data_train,
series_id = 'unique_id',
index = 'date',
values = 'log_num_sold',
freq = 'D'
)
exog_dict = reshape_exog_long_to_dict(
data = data_train[exog_features + ['date', 'unique_id']],
series_id = 'unique_id',
index = 'date',
freq = 'D'
)
Cuando se entrena un forecaster utilizando variables exógenas, es necesario proporcionar estas mismas variables para el período de predicción y deben seguir la misma estructura vista durante el entrenamiento. Por lo tanto, las variables exógenas para el conjunto de test también deben pasarse como un diccionario.
# Preparar las variables exógenas para el conjunto de test
# ==============================================================================
data_test = data_test.merge(
df_calendar.drop(columns=['holiday_name']),
how = 'left',
left_on = ['country', 'date'],
right_on = ['country', 'date'],
validate = 'many_to_one'
)
exog_dict_pred = reshape_exog_long_to_dict(
data = data_test[exog_features + ['date', 'unique_id']],
series_id = 'unique_id',
index = 'date',
freq = 'D'
)
Codificar variables categóricas
Las variables exógenas country
, store
y product
son categóricas. Dependiendo del regresor utilizado, puede ser necesario codificarlas. En este caso, el regresor LightGBM es capaz de manejar variables categóricas directamente. Sin embargo, para asegurar que se traten de manera consistente en las fases de entrenamiento y predicción, las variables primero se codifican como enteros y luego se especifica que sean tratadas como categoricas (Pandas category type). Para más detalles sobre cómo codificar variables exógenas, consulte la sección Feature Engineering de la guía del usuario.
# Categorical encoding
# ==============================================================================
# Se utiliza un ColumnTransformer para transformar las características categóricas
# (no numéricas) utilizando codificación ordinal. Las características numéricas
# se dejan sin cambios. Los valores faltantes se codifican como -1. Si se encuentra
# una nueva categoría en el conjunto de prueba, se codifica como -1.
categorical_features = ['country', 'store', 'product']
transformer_exog = make_column_transformer(
(
OrdinalEncoder(
dtype=int,
handle_unknown="use_encoded_value",
unknown_value=-1,
encoded_missing_value=-1
),
categorical_features
),
remainder="passthrough",
verbose_feature_names_out=False,
).set_output(transform="pandas")
El encoder se pasa al forecaster, para que pueda ser utilizado durante la fase de predicción.
Entrenamiento del forecaster
# Crear forecaster
# ==============================================================================
forecaster = ForecasterRecursiveMultiSeries(
regressor = LGBMRegressor(random_state=8520, verbose=-1),
lags = 31,
encoding = "ordinal_category",
transformer_exog = transformer_exog,
fit_kwargs = {'categorical_feature': categorical_features}
)
forecaster
ForecasterRecursiveMultiSeries
General Information
- Regressor: LGBMRegressor
- Lags: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31]
- Window features: None
- Window size: 31
- Series encoding: ordinal_category
- Exogenous included: False
- Weight function included: False
- Series weights: None
- Differentiation order: None
- Creation date: 2025-08-12 13:17:40
- Last fit date: None
- Skforecast version: 0.17.0
- Python version: 3.12.9
- Forecaster id: None
Exogenous Variables
-
None
Data Transformations
- Transformer for series: None
- Transformer for exog: ColumnTransformer(remainder='passthrough',
transformers=[('ordinalencoder',
OrdinalEncoder(dtype=
, encoded_missing_value=-1, handle_unknown='use_encoded_value', unknown_value=-1), ['country', 'store', 'product'])], verbose_feature_names_out=False)
Training Information
- Series names (levels): None
- Training range: None
- Training index type: Not fitted
- Training index frequency: Not fitted
Regressor Parameters
-
{'boosting_type': 'gbdt', 'class_weight': None, 'colsample_bytree': 1.0, 'importance_type': 'split', 'learning_rate': 0.1, 'max_depth': -1, 'min_child_samples': 20, 'min_child_weight': 0.001, 'min_split_gain': 0.0, 'n_estimators': 100, 'n_jobs': None, 'num_leaves': 31, 'objective': None, 'random_state': 8520, 'reg_alpha': 0.0, 'reg_lambda': 0.0, 'subsample': 1.0, 'subsample_for_bin': 200000, 'subsample_freq': 0, 'verbose': -1}
Fit Kwargs
-
{'categorical_feature': ['country', 'store', 'product']}
Para encontrar los mejores hiperparámetros, se utiliza la función bayesian_search_forecaster_multiseries
, que realiza una búsqueda bayesiana de hiperparámetros. Se combina con la estrategia de validación OneStepAheadFold
y utiliza el error porcentual absoluto medio (MAPE) como métrica de evaluación. Para más detalles sobre la estrategia de validación, consulte la sección Evaluación y ajuste del modelo de la documentación.
Dado que la búsqueda de hiperparámetros no debe realizarse en el conjunto de prueba, los datos de entrenamiento se dividen en dos partes: un conjunto de entrenamiento y un conjunto de validación. El conjunto de entrenamiento se utiliza para entrenar el modelo, mientras que el conjunto de validación se utiliza para evaluar su rendimiento.
# Bayesian search con OneStepAheadFold
# ==============================================================================
end_train = '2015-12-31 00:00:00'
start_validation = '2016-01-01 00:00:00'
initial_train_size = (pd.to_datetime(end_train) - pd.to_datetime(data_train['date'].min())).days
def search_space(trial):
search_space = {
'lags' : trial.suggest_categorical('lags', [1, 14, 21, 60]),
'n_estimators' : trial.suggest_int('n_estimators', 200, 800, step=100),
'max_depth' : trial.suggest_int('max_depth', 3, 8, step=1),
'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 25, 500),
'learning_rate' : trial.suggest_float('learning_rate', 0.01, 0.5),
'feature_fraction': trial.suggest_float('feature_fraction', 0.5, 0.8, step=0.1),
'max_bin' : trial.suggest_int('max_bin', 50, 100, step=25),
'reg_alpha' : trial.suggest_float('reg_alpha', 0, 1, step=0.1),
'reg_lambda' : trial.suggest_float('reg_lambda', 0, 1, step=0.1),
'linear_tree' : trial.suggest_categorical('linear_tree', [True, False]),
}
return search_space
cv = OneStepAheadFold(initial_train_size=initial_train_size)
results_search, best_trial = bayesian_search_forecaster_multiseries(
forecaster = forecaster,
series = series_dict,
exog = exog_dict,
cv = cv,
search_space = search_space,
n_trials = 20,
metric = "mean_absolute_percentage_error",
suppress_warnings = True
)
best_params = results_search.at[0, 'params']
best_lags = results_search.at[0, 'lags']
results_search.head(3)
0%| | 0/20 [00:00<?, ?it/s]
`Forecaster` refitted using the best-found lags and parameters, and the whole data set: Lags: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60] Parameters: {'n_estimators': 800, 'max_depth': 4, 'min_data_in_leaf': 190, 'learning_rate': 0.21343356904845662, 'feature_fraction': 0.7, 'max_bin': 75, 'reg_alpha': 0.1, 'reg_lambda': 1.0, 'linear_tree': True} Backtesting metric: 0.008537774021838387 Levels: ['Canada_Discount Stickers_Kaggle', 'Canada_Discount Stickers_Kaggle Tiers', 'Canada_Discount Stickers_Kerneler', 'Canada_Discount Stickers_Kerneler Dark Mode', 'Canada_Premium Sticker Mart_Holographic Goose', 'Canada_Premium Sticker Mart_Kaggle', 'Canada_Premium Sticker Mart_Kaggle Tiers', 'Canada_Premium Sticker Mart_Kerneler', 'Canada_Premium Sticker Mart_Kerneler Dark Mode', 'Canada_Stickers for Less_Holographic Goose', '...', 'Singapore_Premium Sticker Mart_Holographic Goose', 'Singapore_Premium Sticker Mart_Kaggle', 'Singapore_Premium Sticker Mart_Kaggle Tiers', 'Singapore_Premium Sticker Mart_Kerneler', 'Singapore_Premium Sticker Mart_Kerneler Dark Mode', 'Singapore_Stickers for Less_Holographic Goose', 'Singapore_Stickers for Less_Kaggle', 'Singapore_Stickers for Less_Kaggle Tiers', 'Singapore_Stickers for Less_Kerneler', 'Singapore_Stickers for Less_Kerneler Dark Mode']
levels | lags | params | mean_absolute_percentage_error__weighted_average | mean_absolute_percentage_error__average | mean_absolute_percentage_error__pooling | n_estimators | max_depth | min_data_in_leaf | learning_rate | feature_fraction | max_bin | reg_alpha | reg_lambda | linear_tree | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | [Canada_Discount Stickers_Kaggle, Canada_Disco... | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14... | {'n_estimators': 800, 'max_depth': 4, 'min_dat... | 0.008538 | 0.008490 | 0.008490 | 800 | 4 | 190 | 0.213434 | 0.7 | 75 | 0.1 | 1.0 | True |
1 | [Canada_Discount Stickers_Kaggle, Canada_Disco... | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14... | {'n_estimators': 400, 'max_depth': 4, 'min_dat... | 0.008619 | 0.008503 | 0.008503 | 400 | 4 | 158 | 0.186406 | 0.6 | 100 | 0.0 | 1.0 | True |
2 | [Canada_Discount Stickers_Kaggle, Canada_Disco... | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14... | {'n_estimators': 800, 'max_depth': 8, 'min_dat... | 0.008635 | 0.008543 | 0.008543 | 800 | 8 | 127 | 0.116468 | 0.7 | 100 | 1.0 | 0.0 | True |
Predicción
Una vez que el modelo ha sido entrenado, se puede utilizar para hacer predicciones. Hay que tener en cuenta que, cuando un forecaster se entrena utilizando variables exógenas, es necesario proporcionar estas mismas variables para el período de predicción utilizando el parámetro exog
del método predict
.
Antes de proceder con la predicción del conjunto de test final—y considerando que las ventas reales para este conjunto no están disponibles—es importante evaluar primero el rendimiento del modelo en un conjunto de validación. Esta evaluación intermedia permite entender qué tan bien generaliza el modelo y si es adecuado para el test final.
Para realizar esta evaluación, el modelo se entrena utilizando todos los datos disponibles hasta '2015-12-31 00:00:00' y se generan predicciones para el año siguiente. Estas predicciones se comparan con los datos de ventas reales para evaluar el rendimiento.
# Entrenamiento de forecaster
# ==============================================================================
forecaster.fit(
series = {k: v.loc[:end_train] for k, v in series_dict.items()},
exog = {k: v.loc[:end_train] for k, v in exog_dict.items()},
suppress_warnings = True,
)
# Predicciones para el conjunto de validación
# ==============================================================================
steps = (data_train['date'].max() - pd.to_datetime(end_train)).days
print('Number of steps to predict:', steps)
# Seleccionar las variables exógenas para las fechas de validación
exog_dict_validation = {k: v.loc[start_validation:] for k, v in exog_dict.items()}
predictions_validation = forecaster.predict(steps=steps, exog=exog_dict_validation)
predictions_validation.head(4)
Number of steps to predict: 366
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮ │ `last_window` has missing values. Most of machine learning models do not allow │ │ missing values. Prediction method may fail. │ │ │ │ Category : skforecast.exceptions.MissingValuesWarning │ │ Location : │ │ /home/joaquin/miniconda3/envs/skforecast_16_py12/lib/python3.12/site-packages/skfore │ │ cast/utils/utils.py:988 │ │ Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
level | pred | |
---|---|---|
2016-01-01 | Canada_Discount Stickers_Kaggle | 6.453703 |
2016-01-01 | Canada_Discount Stickers_Kaggle Tiers | 6.375076 |
2016-01-01 | Canada_Discount Stickers_Kerneler | 5.667374 |
2016-01-01 | Canada_Discount Stickers_Kerneler Dark Mode | 5.842492 |
Dado que el entrenamiento se realizó utilizando el logaritmo de la variable objetivo, las predicciones también están en escala logarítmica. Para obtener la escala original de los datos, se aplica la transformación inversa utilizando la función exponencial.
# Revertir la transformación logarítmica de las predicciones
# ==============================================================================
predictions_validation['pred'] = np.expm1(predictions_validation['pred'])
predictions_validation.head(4)
level | pred | |
---|---|---|
2016-01-01 | Canada_Discount Stickers_Kaggle | 634.049530 |
2016-01-01 | Canada_Discount Stickers_Kaggle Tiers | 586.030080 |
2016-01-01 | Canada_Discount Stickers_Kerneler | 288.273897 |
2016-01-01 | Canada_Discount Stickers_Kerneler Dark Mode | 343.637045 |
A continuación, se comparan las predicciones con los datos de ventas reales para evaluar el rendimiento del modelo.
# Comparar predicciones con los valores reales
# ==============================================================================
predictions_validation = predictions_validation.reset_index(names= 'date')
predictions_validation = predictions_validation.merge(
data_train[['unique_id', 'date', 'num_sold']],
left_on = ['level', 'date'],
right_on = ['unique_id', 'date'],
how = 'left',
validate = '1:1'
)
predictions_validation = predictions_validation[['date', 'unique_id', 'pred', 'num_sold']]
predictions_validation.head(4)
date | unique_id | pred | num_sold | |
---|---|---|---|---|
0 | 2016-01-01 | Canada_Discount Stickers_Kaggle | 634.049530 | 706.0 |
1 | 2016-01-01 | Canada_Discount Stickers_Kaggle Tiers | 586.030080 | 634.0 |
2 | 2016-01-01 | Canada_Discount Stickers_Kerneler | 288.273897 | 316.0 |
3 | 2016-01-01 | Canada_Discount Stickers_Kerneler Dark Mode | 343.637045 | 404.0 |
# Calcular MAPE en el conjunto de validación
# ==============================================================================
# MAPE no acepta valores 0 o NaN en el denominador (valores reales), por lo que los registros
# con 0 en `num_sold` se excluyen del cálculo.
mask_not_zero = predictions_validation['num_sold'] != 0
mask_not_nan = predictions_validation['num_sold'].notna()
mask = mask_not_zero & mask_not_nan
mape_validation = mean_absolute_percentage_error(
y_true = predictions_validation.loc[mask, 'num_sold'],
y_pred = predictions_validation.loc[mask, 'pred'],
)
print('Overall MAPE in the validation set :', mape_validation)
# MAPE para cada serie
# ==============================================================================
mape_validation_per_series = (
predictions_validation
.query('num_sold != 0 and num_sold.notna()')
.groupby('unique_id')
.apply(lambda group: mean_absolute_percentage_error(
y_true = group['num_sold'],
y_pred = group['pred'],
), include_groups=False)
.sort_values()
.reset_index(name='mape')
)
mape_validation_per_series
Overall MAPE in the validation set : 0.07483773400412347
unique_id | mape | |
---|---|---|
0 | Canada_Discount Stickers_Kaggle | 0.044088 |
1 | Norway_Discount Stickers_Kerneler | 0.044341 |
2 | Canada_Stickers for Less_Kerneler | 0.044823 |
3 | Singapore_Discount Stickers_Kaggle | 0.047717 |
4 | Canada_Stickers for Less_Kerneler Dark Mode | 0.048269 |
... | ... | ... |
83 | Norway_Premium Sticker Mart_Kaggle Tiers | 0.155127 |
84 | Kenya_Premium Sticker Mart_Holographic Goose | 0.157381 |
85 | Kenya_Premium Sticker Mart_Kaggle Tiers | 0.160968 |
86 | Norway_Discount Stickers_Holographic Goose | 0.179712 |
87 | Italy_Discount Stickers_Holographic Goose | 0.261395 |
88 rows × 2 columns
El siguiente gráfico muestra las predicciones y los datos de ventas reales para cuatro productos diferentes.
set_dark_theme()
series_to_plot = [
'Italy_Stickers for Less_Kerneler Dark Mode',
'Singapore_Stickers for Less_Holographic Goose',
'Italy_Discount Stickers_Holographic Goose',
'Kenya_Premium Sticker Mart_Holographic Goose'
]
for series in series_to_plot:
fig, ax = plt.subplots(1, 1, figsize=(7, 3))
predictions_validation.query('unique_id == @series').plot(
x='date',
y=['num_sold', 'pred'],
ax=ax,
title=series,
linewidth=0.7,
)
plt.show()
Finalmente, se entrena el modelo utilizando todos los datos disponibles y se utiliza el método predict
para generar predicciones para los próximos tres años (1094 días) para todas las series. Todas las predicciones se realizan de una sola vez, es decir, justo después de la última fecha de los datos de entrenamiento, y el modelo no se actualiza con los nuevos datos antes de realizar cada predicción.
# Entrenar el forecaster con todos los datos disponibles
# ==============================================================================
forecaster.fit(series = series_dict, exog = exog_dict)
forecaster
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮ │ NaNs detected in `y_train`. They have been dropped because the target variable │ │ cannot have NaN values. Same rows have been dropped from `X_train` to maintain │ │ alignment. This is caused by series with interspersed NaNs. │ │ │ │ Category : skforecast.exceptions.MissingValuesWarning │ │ Location : │ │ /home/joaquin/miniconda3/envs/skforecast_16_py12/lib/python3.12/site-packages/skfore │ │ cast/recursive/_forecaster_recursive_multiseries.py:1227 │ │ Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮ │ NaNs detected in `X_train`. Some regressors do not allow NaN values during training. │ │ If you want to drop them, set `forecaster.dropna_from_series = True`. │ │ │ │ Category : skforecast.exceptions.MissingValuesWarning │ │ Location : │ │ /home/joaquin/miniconda3/envs/skforecast_16_py12/lib/python3.12/site-packages/skfore │ │ cast/recursive/_forecaster_recursive_multiseries.py:1251 │ │ Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
ForecasterRecursiveMultiSeries
General Information
- Regressor: LGBMRegressor
- Lags: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60]
- Window features: None
- Window size: 60
- Series encoding: ordinal_category
- Exogenous included: True
- Weight function included: False
- Series weights: None
- Differentiation order: None
- Creation date: 2025-08-12 13:17:40
- Last fit date: 2025-08-12 13:25:47
- Skforecast version: 0.17.0
- Python version: 3.12.9
- Forecaster id: None
Exogenous Variables
-
is_holiday, is_holiday_lag_1, is_holiday_lag_2, is_holiday_lag_5, is_holiday_lag_7, is_holiday_lag_9, is_holiday_next_1, is_holiday_next_2, is_holiday_next_5, month_sin, month_cos, week_sin, week_cos, day_of_week_sin, day_of_week_cos, country, store, product
Data Transformations
- Transformer for series: None
- Transformer for exog: ColumnTransformer(remainder='passthrough',
transformers=[('ordinalencoder',
OrdinalEncoder(dtype=
, encoded_missing_value=-1, handle_unknown='use_encoded_value', unknown_value=-1), ['country', 'store', 'product'])], verbose_feature_names_out=False)
Training Information
- Series names (levels): Canada_Discount Stickers_Kaggle, Canada_Discount Stickers_Kaggle Tiers, Canada_Discount Stickers_Kerneler, Canada_Discount Stickers_Kerneler Dark Mode, Canada_Premium Sticker Mart_Holographic Goose, Canada_Premium Sticker Mart_Kaggle, Canada_Premium Sticker Mart_Kaggle Tiers, Canada_Premium Sticker Mart_Kerneler, Canada_Premium Sticker Mart_Kerneler Dark Mode, Canada_Stickers for Less_Holographic Goose, Canada_Stickers for Less_Kaggle, Canada_Stickers for Less_Kaggle Tiers, Canada_Stickers for Less_Kerneler, Canada_Stickers for Less_Kerneler Dark Mode, Finland_Discount Stickers_Holographic Goose, Finland_Discount Stickers_Kaggle, Finland_Discount Stickers_Kaggle Tiers, Finland_Discount Stickers_Kerneler, Finland_Discount Stickers_Kerneler Dark Mode, Finland_Premium Sticker Mart_Holographic Goose, Finland_Premium Sticker Mart_Kaggle, Finland_Premium Sticker Mart_Kaggle Tiers, Finland_Premium Sticker Mart_Kerneler, Finland_Premium Sticker Mart_Kerneler Dark Mode, Finland_Stickers for Less_Holographic Goose, ..., Norway_Premium Sticker Mart_Holographic Goose, Norway_Premium Sticker Mart_Kaggle, Norway_Premium Sticker Mart_Kaggle Tiers, Norway_Premium Sticker Mart_Kerneler, Norway_Premium Sticker Mart_Kerneler Dark Mode, Norway_Stickers for Less_Holographic Goose, Norway_Stickers for Less_Kaggle, Norway_Stickers for Less_Kaggle Tiers, Norway_Stickers for Less_Kerneler, Norway_Stickers for Less_Kerneler Dark Mode, Singapore_Discount Stickers_Holographic Goose, Singapore_Discount Stickers_Kaggle, Singapore_Discount Stickers_Kaggle Tiers, Singapore_Discount Stickers_Kerneler, Singapore_Discount Stickers_Kerneler Dark Mode, Singapore_Premium Sticker Mart_Holographic Goose, Singapore_Premium Sticker Mart_Kaggle, Singapore_Premium Sticker Mart_Kaggle Tiers, Singapore_Premium Sticker Mart_Kerneler, Singapore_Premium Sticker Mart_Kerneler Dark Mode, Singapore_Stickers for Less_Holographic Goose, Singapore_Stickers for Less_Kaggle, Singapore_Stickers for Less_Kaggle Tiers, Singapore_Stickers for Less_Kerneler, Singapore_Stickers for Less_Kerneler Dark Mode
- Training range: 'Canada_Discount Stickers_Kaggle': ['2010-01-01', '2016-12-31'], 'Canada_Discount Stickers_Kaggle Tiers': ['2010-01-01', '2016-12-31'], 'Canada_Discount Stickers_Kerneler': ['2010-01-01', '2016-12-31'], 'Canada_Discount Stickers_Kerneler Dark Mode': ['2010-01-01', '2016-12-31'], 'Canada_Premium Sticker Mart_Holographic Goose': ['2010-01-01', '2016-12-31'], ..., 'Singapore_Stickers for Less_Holographic Goose': ['2010-01-01', '2016-12-31'], 'Singapore_Stickers for Less_Kaggle': ['2010-01-01', '2016-12-31'], 'Singapore_Stickers for Less_Kaggle Tiers': ['2010-01-01', '2016-12-31'], 'Singapore_Stickers for Less_Kerneler': ['2010-01-01', '2016-12-31'], 'Singapore_Stickers for Less_Kerneler Dark Mode': ['2010-01-01', '2016-12-31']
- Training index type: DatetimeIndex
- Training index frequency: D
Regressor Parameters
-
{'boosting_type': 'gbdt', 'class_weight': None, 'colsample_bytree': 1.0, 'importance_type': 'split', 'learning_rate': 0.21343356904845662, 'max_depth': 4, 'min_child_samples': 20, 'min_child_weight': 0.001, 'min_split_gain': 0.0, 'n_estimators': 800, 'n_jobs': None, 'num_leaves': 31, 'objective': None, 'random_state': 8520, 'reg_alpha': 0.1, 'reg_lambda': 1.0, 'subsample': 1.0, 'subsample_for_bin': 200000, 'subsample_freq': 0, 'verbose': -1, 'min_data_in_leaf': 190, 'feature_fraction': 0.7, 'max_bin': 75, 'linear_tree': True, 'device': 'cpu'}
Fit Kwargs
-
{'categorical_feature': ['country', 'store', 'product']}
# Importancia de los predictores (top 7)
# ==============================================================================
importance = forecaster.get_feature_importances()
importance.head(7)
feature | importance | |
---|---|---|
13 | lag_14 | 581 |
6 | lag_7 | 572 |
55 | lag_56 | 428 |
75 | week_sin | 418 |
76 | week_cos | 369 |
0 | lag_1 | 319 |
1 | lag_2 | 312 |
# Predicciones para el conjunto de test
# ==============================================================================
steps = (data_test['date'].max() - data_test['date'].min()).days + 1
print('Number of steps to predict:', steps)
predictions = forecaster.predict(steps=steps, exog=exog_dict_pred, suppress_warnings=True)
# Revertir la transformación logarítmica de las predicciones
# ==============================================================================
predictions['pred'] = np.expm1(predictions['pred'])
predictions.head(4)
Number of steps to predict: 1095
level | pred | |
---|---|---|
2017-01-01 | Canada_Discount Stickers_Kaggle | 938.299735 |
2017-01-01 | Canada_Discount Stickers_Kaggle Tiers | 715.476380 |
2017-01-01 | Canada_Discount Stickers_Kerneler | 418.295782 |
2017-01-01 | Canada_Discount Stickers_Kerneler Dark Mode | 501.718288 |
Predecir nuevas series (series desconocidas)
Dos de las series fueron excluidas del conjunto de entrenamiento porque contenían solo valores nulos. Skforecast permite predecir nuevas series no vistas durante el entrenamiento del modelo, pero las predicciones no se incluyen por defecto. Para obtener predicciones de nuevas series, es necesario proporcionar el argumento last_window
en el método predict
. Se trata de un dataframe que contiene la última ventana de datos de la serie que se desea predecir. En el caso de que no se disponda de datos históricos para las nuevas series, se debe pasar un dataframe con todo NaN
.
# Predecir series no vistas durante el entrenamiento
# ==============================================================================
last_window_unseen_series = pd.DataFrame(
data = np.nan,
index = pd.date_range(end='2016-12-31', periods=forecaster.window_size, freq='D'),
columns = ['Canada_Discount Stickers_Holographic Goose', 'Kenya_Discount Stickers_Holographic Goose']
)
predictions_unseen_Series = forecaster.predict(
steps = steps,
last_window = last_window_unseen_series,
exog = exog_dict_pred,
suppress_warnings = True
)
predictions_unseen_Series['pred'] = np.expm1(predictions_unseen_Series['pred'])
predictions_unseen_Series
level | pred | |
---|---|---|
2017-01-01 | Canada_Discount Stickers_Holographic Goose | 130.095322 |
2017-01-01 | Kenya_Discount Stickers_Holographic Goose | 13.561558 |
2017-01-02 | Canada_Discount Stickers_Holographic Goose | 131.262703 |
2017-01-02 | Kenya_Discount Stickers_Holographic Goose | 15.972330 |
2017-01-03 | Canada_Discount Stickers_Holographic Goose | 163.919315 |
... | ... | ... |
2019-12-29 | Kenya_Discount Stickers_Holographic Goose | 102.435982 |
2019-12-30 | Canada_Discount Stickers_Holographic Goose | 344.756188 |
2019-12-30 | Kenya_Discount Stickers_Holographic Goose | 90.246888 |
2019-12-31 | Canada_Discount Stickers_Holographic Goose | 335.771299 |
2019-12-31 | Kenya_Discount Stickers_Holographic Goose | 91.815570 |
2190 rows × 2 columns
Enviar la solución a Kaggle
predictions_all = pd.concat([predictions, predictions_unseen_Series])
submission = data_test.merge(
predictions_all.reset_index(names=['date']),
how = 'left',
left_on = ['date', 'unique_id'],
right_on = ['date', 'level'],
validate = 'one_to_one'
)
submission = submission.loc[:, ['id', 'pred']]
submission = submission.rename(columns={'pred': 'num_sold'})
submission.to_csv('submission.csv', index=False)
submission
id | num_sold | |
---|---|---|
0 | 230130 | 130.095322 |
1 | 230131 | 938.299735 |
2 | 230132 | 715.476380 |
3 | 230133 | 418.295782 |
4 | 230134 | 501.718288 |
... | ... | ... |
98545 | 328675 | 415.009558 |
98546 | 328676 | 3036.446657 |
98547 | 328677 | 2080.632355 |
98548 | 328678 | 1366.599418 |
98549 | 328679 | 1640.340384 |
98550 rows × 2 columns
# Enviar los resultados a Kaggle
# ==============================================================================
# !pip install kaggle
# !kaggle competitions submit -c playground-series-s5e1 -f submission.csv -m "uploading submission"
Información de sesión
import session_info
session_info.show(html=False)
----- feature_engine 1.8.3 holidays 0.72 lightgbm 4.6.0 matplotlib 3.10.1 numpy 2.2.6 optuna 3.6.2 pandas 2.3.1 session_info v1.0.1 skforecast 0.17.0 sklearn 1.7.1 ----- IPython 9.1.0 jupyter_client 8.6.3 jupyter_core 5.7.2 jupyterlab 4.4.5 notebook 7.4.5 ----- Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb 6 2025, 18:56:27) [GCC 11.2.0] Linux-6.14.0-27-generic-x86_64-with-glibc2.39 ----- Session information updated at 2025-08-12 13:26
Instrucciones para citar
¿Cómo citar este documento?
Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!
Modelos de forecasting globales: Guía paso a paso con Kaggle Sticker Sales 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/py65-acelerar-models-forecasting-gpu.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.17.0). Zenodo. https://doi.org/10.5281/zenodo.8382788
APA:
Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast (Version 0.17.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.17.0}, month = {08}, year = {2025}, 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! 😊
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.