• Introducción
  • Librerías
  • Datos
    • Rango de fechas disponibles
    • Valores nulos en las series
  • Feature engineering
    • Transformación logarítmica
    • Variables exógenas
  • Modelado
    • Codificar variables categóricas
    • Entrenamiento del forecaster
    • ForecasterRecursiveMultiSeries
    • Predicción
    • ForecasterRecursiveMultiSeries
  • Predecir nuevas series (series desconocidas)
    • Enviar la solución a Kaggle
  • Información de sesión
  • Instrucciones para citar


Más sobre forecasting en: cienciadedatos.net


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.

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ón reshape_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']}

🛈 API Reference    🗎 User Guide

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']}

🛈 API Reference    🗎 User Guide

# 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! 😊

Become a GitHub Sponsor Become a GitHub Sponsor

Creative Commons Licence

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

Se permite:

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

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

Bajo los siguientes términos:

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

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

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