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 con modelos fundacionales
- 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
¿Qué es skforecast-ai?¶
skforecast-ai es un asistente de forecasting de IA que empareja un motor determinista, impulsado por skforecast, con una capa de razonamiento basada en LLM. Simplemente proporciona una serie temporal y el asistente automáticamente perfila los datos, selecciona un modelo utilizando las mejores prácticas establecidas y evalúa su rendimiento. Devuelve tanto el pronóstico final como el script ejecutable de skforecast que lo produjo.
Está organizado en torno a un único objeto central, el ForecastingAssistant, que consta de dos componentes complementarios:
Motor Determinista (Basado en reglas y Reproducible): Perfila los datos, selecciona un forecaster y un estimador, deriva rezagos (lags) y pasos de preprocesamiento, ejecuta backtesting y produce el pronóstico final. Crucialmente, genera el script exacto e independiente de
skforecastque generó los resultados. Dados los mismos inputs y configuración, se garantiza que este flujo de trabajo sea reproducible.Capa de Razonamiento (Impulsada por LLM): Accesible principalmente a través del método
ask(), esta capa interpreta y explica los objetos y resultados que le pasas: perfiles de datos, planes de modelado, opciones de validación, resultados de backtesting y pronósticos. El LLM actúa estrictamente como intérprete; no vuelve a ejecutar el flujo de trabajo ni cambia silenciosamente las recomendaciones de modelado en segundo plano. Las capacidades agénticas, comorefine_plan()ocreate_cv()guiadas por el LLM, son pasos separados y explícitos donde el LLM sugiere ajustes que luego se implementan de manera transparente en código determinista.
¿Por qué skforecast-ai?¶
- 🎯 Determinista por diseño: construido como un estricto motor basado en reglas para garantizar una consistencia absoluta, el mismo input siempre significa el mismo output.
- 🔍 Código que puedes inspeccionar: el script que ves es el código que se ejecutó. Inspecciónalo, gestiónalo en control de versiones o ejecútalo de forma independiente con skforecast puro.
- ⚡ De los datos al pronóstico en una sola llamada: perfilado automático de datos, selección de modelo y estimador, ingeniería de variables/lags y evaluación mediante backtest.
- 💻 Python o terminal: dirige todo el pipeline desde unas pocas líneas de Python o desde la línea de comandos.
- 💬 Capa de razonamiento LLM: explica las decisiones del motor en lenguaje sencillo, te ayuda a mejorar la configuración y te permite pedir consejos. Esta capa es completamente opcional; el pipeline de forecasting principal puede ejecutarse de manera totalmente offline.
- 🏗️ Construido sobre skforecast: forecasters recursivos y directos, multiserie, estadísticos y modelos fundacionales (Chronos-2, TimesFM, Moirai, y más).
Inicio rápido (Python)¶
De los datos sin procesar a un pronóstico validado, y el código detrás de él, en unas pocas líneas:
import pandas as pd
from skforecast_ai import ForecastingAssistant
from skforecast.datasets import load_demo_dataset
data = load_demo_dataset(verbose=False)
assistant = ForecastingAssistant()
result = assistant.forecast(data=data, target='y', steps=12)
print(result.predictions) # pronóstico para los siguientes 12 pasos
print(result.metrics) # métricas de evaluación: MAE, MSE, MASE, MAPE
print(result.code) # el script exacto de skforecast que produjo este resultado
Esa única llamada a forecast() perfiló los datos, eligió un forecaster y estimador, generó un script de skforecast y lo ejecutó. result.code es el script que se ejecutó.
Inicio rápido (CLI)¶
El mismo pipeline se ejecuta desde la terminal. Apúntalo a un archivo CSV o URL:
# Pronóstico de principio a fin (perfil -> plan -> código -> pronóstico)
skforecast-ai forecast data.csv --target y --date-column datetime --steps 12
# Solo inspeccionar los datos
skforecast-ai profile data.csv --target y --date-column datetime
# Generar un script independiente y ejecutable sin ejecutarlo
skforecast-ai forecast-code data.csv --target y --date-column datetime --steps 12 --output forecast.py
Dos formas de usar skforecast-ai¶
skforecast-ai soporta dos flujos de trabajo distintos utilizando el mismo motor de forecasting subyacente:
La Vía Rápida: Úsala cuando desees un pronóstico o resultado de backtest en una sola llamada. El asistente perfila los datos, construye el plan de modelado, ejecuta el flujo de trabajo y devuelve los resultados junto con el código reproducible de
skforecast.La Vía Paso a Paso: Úsala cuando desees un control granular para inspeccionar o ajustar decisiones intermedias. Puedes crear un perfil manualmente, construir un plan, opcionalmente refinarlo con el LLM, definir una estrategia de validación, evaluar el modelo y luego generar el pronóstico.
Un modelo mental útil es que el forecasting y la validación son ramas separadas. Una vez que tienes un profile y un plan, puedes usar forecast() para producir predicciones futuras directamente, o backtest() para evaluar el rendimiento del modelo con datos históricos. También puedes usar compare() para evaluar varias configuraciones candidatas bajo la misma estrategia de validación cruzada y obtener una tabla de clasificación (leaderboard), de modo que la mejor configuración se elija a partir del rendimiento medido en lugar de la intuición.
El método ask() está disponible en ambos flujos de trabajo. Puede explicar un perfil, plan, configuración de validación, resultado de backtest, resultado de comparación o responder preguntas generales de forecasting, pero nunca ejecutará el flujo de trabajo ni modificará tus parámetros sin una instrucción explícita.
El resto de esta guía configura el asistente y el conjunto de datos utilizado en todo el documento, y luego recorre detalladamente la vía paso a paso. Para la vía rápida -- la forma más rápida de pasar de los datos sin procesar a un pronóstico validado con una configuración mínima -- consulta la sección de Inicio Rápido más arriba; es ideal cuando deseas resultados rápidos y confías en que el asistente tomará decisiones de modelado razonables y de referencia en tu nombre.
Inicialización del asistente¶
El primer paso es instanciar un ForecastingAssistant, que será responsable de ejecutar todo el flujo de trabajo (perfilado, planificación, backtesting y pronóstico), así como de explicar los outputs y sugerir mejoras.
Para activar el soporte opcional de LLM, los usuarios deben pasar un string en el formato 'proveedor:nombre_modelo' (por ejemplo, 'openai:gpt-5.5', 'google:gemini-3-flash-preview', 'anthropic:claude-sonnet-5', o 'ollama:qwen3:8b'). Para proveedores en la nube, la clave API (API key) correspondiente debe estar disponible como una variable de entorno o pasarse explícitamente al crear el asistente. En este tutorial, establecemos send_data_to_llm=False. Esto asegura una estricta privacidad de los datos: el LLM recibe solo metadatos y estadísticas resumen, nunca los valores crudos de las series temporales.
# Procesamiento de datos
# ==============================================================================
import os
import textwrap
import pandas as pd
from skforecast.datasets import fetch_dataset
# Gráficos
# ==============================================================================
from skforecast.plot import set_dark_theme
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.io as pio
import plotly.offline as poff
pio.templates.default = "seaborn"
poff.init_notebook_mode(connected=True)
plt.style.use('seaborn-v0_8-darkgrid')
# skforecast y skforecast-ai
# ==============================================================================
import skforecast
import skforecast_ai
import chronos # pip install chronos-forecasting
from skforecast_ai import ForecastingAssistant
from skforecast.model_selection import TimeSeriesFold
color = '\033[1m\033[38;5;208m'
print(f"{color}Versión skforecast_ai: {skforecast_ai.__version__}")
print(f"{color}Versión skforecast: {skforecast.__version__}")
print(f"{color}Versión chronos-forecasting: {chronos.__version__}")
Versión skforecast_ai: 0.2.0 Versión skforecast: 0.24.0 Versión chronos-forecasting: 2.3.1
✏️ Nota
Si no tienes acceso a un asistente LLM, aún puedes seguir el tutorial completo utilizando solo los métodos deterministas. El perfilado, la planificación, el backtesting y el forecasting se ejecutan sin un LLM. Solo las explicaciones de ask() y las variantes guiadas por LLM de refine_plan() y create_cv() requieren un LLM configurado; sus equivalentes deterministas (por ejemplo, refine_plan() con overrides explícitos y prompt=None) funcionan sin uno.
# Asistente con LLM habilitado
# ==============================================================================
LLM_MODEL = "google:gemini-3.5-flash"
api_key = os.getenv("GOOGLE_API_KEY")
assistant = ForecastingAssistant(
llm=LLM_MODEL, api_key=api_key, send_data_to_llm=False
)
# Usando AWS Bedrock
# ==============================================================================
assistant = ForecastingAssistant(
llm='bedrock:eu.anthropic.claude-sonnet-4-6',
base_url="eu-west-1"
)
# Asistente sin capa de razonamiento
# ==============================================================================
# assistant = ForecastingAssistant()
⚠️ Tus datos se mantienen privados
Por defecto, habilitar un LLM no envía tus datos de series temporales al proveedor del modelo.
El asistente solo pasa estadísticas de resumen, la frecuencia detectada,
los flags de estacionalidad y la configuración del forecaster, nunca las observaciones crudas.
Para permitirlo explícitamente, pasa send_data_to_llm=True.
Datos¶
Los datos en este documento representan el uso horario del sistema de alquiler de bicicletas en la ciudad de Washington, D.C. durante los años 2011 y 2012. Además del número de usuarios por hora, se dispone de información sobre las condiciones climáticas y días festivos (holidays).
# Descarga de datos
# ==============================================================================
data = fetch_dataset('bike_sharing', raw=True)
data = data[['date_time', 'users', 'holiday', 'weather', 'temp']]
data['date_time'] = pd.to_datetime(data['date_time'])
data.head()
╭───────────────────────────────── bike_sharing ──────────────────────────────────╮ │ Description: │ │ Hourly usage of the bike share system in the city of Washington D.C. during the │ │ years 2011 and 2012. In addition to the number of users per hour, information │ │ about weather conditions and holidays is available. │ │ │ │ Source: │ │ Fanaee-T,Hadi. (2013). Bike Sharing Dataset. UCI Machine Learning Repository. │ │ https://doi.org/10.24432/C5W894. │ │ │ │ URL: │ │ https://raw.githubusercontent.com/skforecast/skforecast- │ │ datasets/main/data/bike_sharing_dataset_clean.csv │ │ │ │ Shape: 17544 rows x 12 columns │ ╰─────────────────────────────────────────────────────────────────────────────────╯
| date_time | users | holiday | weather | temp | |
|---|---|---|---|---|---|
| 0 | 2011-01-01 00:00:00 | 16.0 | 0.0 | clear | 9.84 |
| 1 | 2011-01-01 01:00:00 | 40.0 | 0.0 | clear | 9.02 |
| 2 | 2011-01-01 02:00:00 | 32.0 | 0.0 | clear | 9.02 |
| 3 | 2011-01-01 03:00:00 | 13.0 | 0.0 | clear | 9.84 |
| 4 | 2011-01-01 04:00:00 | 1.0 | 0.0 | clear | 9.84 |
✏️ Nota
skforecast-ai está listo para preprocesar los datos, pero se recomienda que los usuarios apliquen sus propios pasos de preprocesamiento antes de utilizar el asistente. Esto garantiza que los datos estén en el formato deseado y que se hayan aplicado las transformaciones necesarias antes de continuar con el flujo de trabajo de forecasting.
# Gráfico interactivo de la serie temporal
# ==============================================================================
fig = go.Figure()
fig.add_trace(
go.Scatter(x=data['date_time'], y=data['users'], mode='lines', name='Usuarios')
)
fig.update_layout(
title = 'Número de usuarios',
xaxis_title="Tiempo",
yaxis_title="Usuarios",
legend_title="Partición:",
width=800,
height=400,
margin=dict(l=20, r=20, t=35, b=20),
legend=dict(orientation="h", yanchor="top", y=1, xanchor="left", x=0.001)
)
fig.show()
Para un recorrido más profundo del análisis exploratorio detrás de este conjunto de datos, consulta el ejemplo de skforecast: Forecasting de series temporales con skforecast, XGBoost, LightGBM y CatBoost.
Análisis Profundo: Paso a Paso¶
Si bien la vía rápida es excelente para obtener una baseline, muchos científicos de datos necesitan controlar, inspeccionar y anular decisiones intermedias. La vía paso a paso divide el proceso en fases distintas y observables: Perfilado (Profiling), Planificación y Ejecución (Forecasting o Backtesting).
Perfilar los datos¶
El método profile() es la primera etapa del flujo de trabajo paso a paso. Inspecciona el conjunto de datos y devuelve un objeto ForecastingProfile que contiene:
Metadatos de los datos: frecuencia detectada, tipo de índice, longitudes de las series, valores perdidos y roles de las columnas exógenas.
Recomendaciones de modelado: la familia de forecaster y el estimador seleccionados, junto con candidatos alternativos y el razonamiento detrás de cada elección.
Estructura de rezagos (lags): rezagos significativos en la PACF por serie, utilizados como baseline para la etapa de planificación.
Sugerencias de window features: configuraciones de estadísticas móviles (rolling statistics) apropiadas para la estacionalidad detectada.
Este es un paso puramente determinista: no hay ningún LLM involucrado. El objeto profile es un prerrequisito tanto para plan() como para el modo de explicación de ask().
| Atributo | Descripción |
|---|---|
data_profile |
Metadatos completos del dataset: frecuencia, tipo de índice, longitud de las series, valores nulos, columnas exógenas |
forecaster |
Nombre de la clase forecaster de skforecast recomendada |
forecaster_candidates |
Lista ordenada de nombres de forecasters compatibles |
estimator |
Nombre de la clase del estimador recomendado (None para modelos estadísticos) |
estimator_candidates |
Lista ordenada de nombres de estimadores compatibles |
series_pacf |
Rezagos significativos en la PACF por serie (usados por plan() para establecer los lags por defecto) |
window_features |
Configuraciones sugeridas de window features |
calendar_features |
Nombres recomendados de variables de calendario basados en la estacionalidad detectada |
explanation |
Explicación legible por humanos de por qué se seleccionaron este forecaster y estimador |
# Perfilar los datos
# ==============================================================================
profile = assistant.profile(
data = data,
target = 'users',
date_column = 'date_time'
)
# Inspeccionar el perfil
# ==============================================================================
profile
Dataset Profile ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Format │ single │ ├────────────────┼────────────────────────────────────────────────┤ │ Series │ 1 │ ├────────────────┼────────────────────────────────────────────────┤ │ Observations │ 17544 │ ├────────────────┼────────────────────────────────────────────────┤ │ Frequency │ h │ ├────────────────┼────────────────────────────────────────────────┤ │ Target │ users │ ├────────────────┼────────────────────────────────────────────────┤ │ Exog columns │ holiday, weather, temp (categorical: weather) │ ├────────────────┼────────────────────────────────────────────────┤ │ Missing values │ None │ └────────────────┴────────────────────────────────────────────────┘ Recommendation ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Forecaster candidates │ ForecasterRecursive, ForecasterDirect, ForecasterFoundation │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Estimator candidates │ LGBMRegressor, XGBRegressor, Ridge │ └───────────────────────┴─────────────────────────────────────────────────────────────┘ ╭───────────────────────────────── Profile Explanation ──────────────────────────────────╮ │ │ │ A single-series ML forecaster (ForecasterRecursive) is recommended. Data: 17544 │ │ observations, 'h' frequency. Alternative forecasters: ['ForecasterDirect', │ │ 'ForecasterFoundation']. Estimator: LGBMRegressor. A gradient boosting model is │ │ preferred for a dataset of this size (17544 observations). Alternative estimators: │ │ ['XGBRegressor', 'Ridge']. 3 exogenous variables (1 categorical) available as │ │ predictors. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Una vez que tienes un perfil, puedes pasarlo a ask() para obtener una explicación generada por el LLM de las decisiones de modelado. Ten en cuenta que el profile precalculado se pasa directamente, por lo que no se repite ningún trabajo de perfilado.
# Pedir al asistente que explique el perfil
# ==============================================================================
answer = assistant.ask(
prompt = (
"Explica por qué se recomendaron este forecaster y estimador para mis "
"datos horarios de demanda de alquiler de bicicletas, y qué aportan las variables exógenas."
),
profile = profile,
steps = 36,
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮ │ │ │ El forecaster recomendado es ForecasterRecursive con LGBMRegressor, y la elección se │ │ basa directamente en las características de tu dataset: 17.544 observaciones │ │ horarias de una sola serie, con 3 variables exógenas disponibles. │ │ │ │ Por qué ForecasterRecursive │ │ │ │ ForecasterRecursive es el punto de partida recomendado para series únicas. Entrena │ │ un solo modelo y genera predicciones multi-paso de forma recursiva, usando sus │ │ propias predicciones como entrada para los pasos siguientes. Esto lo hace eficiente │ │ en memoria y en tiempo de entrenamiento. La alternativa listada, ForecasterDirect, │ │ entrenaría un modelo independiente por cada uno de los 36 pasos del horizonte, lo │ │ cual puede ser útil cuando la relación entre los lags y el objetivo cambia mucho a │ │ lo largo del horizonte, pero no es el punto de partida recomendado aquí. │ │ │ │ Por qué LGBMRegressor │ │ │ │ Con 17.544 observaciones, el dataset tiene un tamaño suficiente para que un modelo │ │ de gradient boosting capture patrones complejos y no lineales sin sobreajustarse. │ │ LGBMRegressor es rápido, tolera valores NaN de forma nativa (por eso las filas con │ │ NaN se conservan en lugar de descartarse), y maneja variables categóricas de forma │ │ eficiente. Las alternativas listadas son XGBRegressor y Ridge. │ │ │ │ Qué aportan los lags y las window features │ │ │ │ El plan incluye 32 lags seleccionados que capturan patrones a distintas escalas: │ │ │ │ • Lags cortos (1, 2, 3) capturan dependencia inmediata entre horas consecutivas │ │ • Lags alrededor de 24 (19 a 26) capturan el ciclo diario │ │ • Lags alrededor de 168 (166, 167, 169) capturan el ciclo semanal │ │ • Lags más largos (313, 335, 337) pueden estar asociados con patrones de varias │ │ semanas │ │ │ │ Las window features complementan esto con estadísticos agregados: media y desviación │ │ estándar en ventana de 3 horas, media en ventana de 24 horas y media en ventana de │ │ 168 horas. Estos resúmenes dan al modelo una vista del comportamiento reciente, del │ │ día anterior y de la semana anterior. │ │ │ │ Las características de calendario (hora, día de la semana, fin de semana, mes) │ │ añaden estructura temporal explícita. │ │ │ │ Qué aportan las variables exógenas │ │ │ │ El plan incluye 3 variables exógenas: │ │ │ │ • holiday: indica si el día es festivo, lo que puede estar asociado con patrones de │ │ demanda distintos a los días laborables normales │ │ • weather: variable categórica (detectada automáticamente y gestionada con │ │ categorical_features='auto'), que puede estar asociada con variaciones en el uso │ │ de bicicletas según las condiciones meteorológicas │ │ • temp: la temperatura, que puede estar asociada con la comodidad percibida para el │ │ uso de bicicletas │ │ │ │ Estas variables aportan contexto externo que los lags solos no pueden capturar, │ │ especialmente para días atípicos como festivos o episodios de mal tiempo. Son │ │ conocidas en el momento de la predicción (son inputs del futuro, no del pasado), lo │ │ que las hace válidas como exógenas en un horizonte de 36 pasos. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Construir el plan¶
El método plan() convierte las decisiones preliminares de modelado en el ForecastingProfile en una configuración completamente especificada y ejecutable. Determina:
- Rezagos (Lags): derivados de los rezagos significativos en la PACF detectados en el perfil. Puedes sobrescribirlos explícitamente.
- Window features: configuraciones de estadísticas móviles apropiadas para la estacionalidad detectada.
- Pasos de preprocesamiento: lista ordenada de transformaciones (ej., diferenciación, escalado, manejo de NaNs).
- Método de intervalo de predicción:
'bootstrapping','conformal'o'native'(seleccionado en base al estimador). - Métricas: las métricas de evaluación primaria y secundaria.
Al igual que profile(), este es un paso determinista. El objeto ForecastPlan resultante es el plano (blueprint) completo que ejecutan forecast() y backtest().
| Atributo | Descripción |
|---|---|
forecaster |
Nombre de la clase del forecaster |
estimator |
Nombre de la clase del estimador |
forecaster_kwargs |
Todos los kwargs del constructor para el forecaster, incluyendo lags y window_features |
estimator_kwargs |
Kwargs del constructor para el estimador |
steps |
Horizonte de pronóstico |
interval |
Cuantiles del intervalo de predicción, ej. [0.1, 0.9] |
interval_method |
Método utilizado para producir el intervalo (bootstrapping, conformal o native) |
use_exog |
Indica si se incluyen variables exógenas |
preprocessing_steps |
Lista ordenada de acciones de preprocesamiento con fragmentos de código |
explanation |
Explicación legible por humanos de las decisiones del plan |
# Construir un plan a partir del perfil
# ==============================================================================
plan = assistant.plan(
profile = profile,
steps = 36,
interval = [0.1, 0.9] # Intervalo de predicción del 80%
)
# Inspeccionar el plan
# ==============================================================================
plan
Forecast Plan ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Steps │ 36 │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Frequency │ h │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Lags │ [1, 2, 3, 5, 8, 10, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 32, │ │ │ 33, 119, 121, 135, 136, 142, 143, 145, 160, 166, 167, 169, 313, │ │ │ 335, 337] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Window features │ [{'stats': ['mean', 'std'], 'window_size': 3}, {'stats': ['mean'], │ │ │ 'window_size': 24}, {'stats': ['mean'], 'window_size': 168}] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Calendar features │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Use exog │ True │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval │ [0.1, 0.9] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval method │ bootstrapping │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Primary metric │ mean_absolute_error │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Preprocessing │ 1 step │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ Preprocessing Steps ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Step ┃ Reason ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ handle_categorical_exog │ Categorical exogenous variables detected: ['weather']. These │ │ │ are handled automatically by skforecast │ │ │ (categorical_features='auto'). │ └─────────────────────────┴──────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── Plan Explanation ───────────────────────────────────╮ │ │ │ Plan: ForecasterRecursive + LGBMRegressor. Lags: [1, 2, 3, 5, 8, 10, 15, 17, 19, 20, │ │ 21, 22, 23, 24, 25, 26, 32, 33, 119, 121, 135, 136, 142, 143, 145, 160, 166, 167, │ │ 169, 313, 335, 337]. Window features: ['mean(window=3)', 'std(window=3)', │ │ 'mean(window=24)', 'mean(window=168)']. Calendar features: ['hour', 'day_of_week', │ │ 'weekend', 'month'] (raw ordinal encoding). Prediction intervals via bootstrapping. │ │ NaN rows kept (NaN-tolerant estimator). Exogenous variables included. MAE is │ │ interpretable, robust to outliers, and works at any scale. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Pasa tanto el profile como el plan a ask() para obtener una explicación detallada de la configuración elegida.
# Pedir al asistente que explique el plan
# ==============================================================================
answer = assistant.ask(
prompt = (
"Guíame a través de este plan. ¿Por qué estos lags y window features, "
"y cómo se producirá el intervalo de predicción del 80%?"
),
profile = profile,
plan = plan,
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮ │ │ │ El plan usa ForecasterRecursive con LGBMRegressor sobre 17 544 observaciones │ │ horarias de la variable users, con 36 pasos de horizonte (36 horas hacia adelante). │ │ A continuación te explico cada decisión. │ │ │ │ Por qué estos lags │ │ │ │ La lista de lags seleccionada es: │ │ │ │ [1, 2, 3, 5, 8, 10, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 32, 33, 119, 121, 135, │ │ 136, 142, 143, 145, 160, 166, 167, 169, 313, 335, 337] │ │ │ │ Se pueden distinguir tres grupos por su escala: │ │ │ │ • Lags cortos (1 a 33): capturan la dinámica reciente hora a hora. Los lags 23, 24 │ │ y 25 son especialmente relevantes porque se sitúan en torno al ciclo diario de 24 │ │ horas, algo esperable en datos de uso por horas. │ │ • Lags medios (119 a 169): corresponden aproximadamente a 5 a 7 días atrás. Estos │ │ lags capturan el patrón semanal: lo que ocurrió a la misma hora el mismo día de │ │ la semana anterior. │ │ • Lags largos (313, 335, 337): se acercan a las dos semanas, lo que puede ayudar a │ │ capturar ciclos bimensuales o efectos de festivos que se repiten con esa │ │ cadencia. │ │ │ │ Esta selección fue guiada por el análisis de autocorrelación parcial (PACF), que │ │ identifica qué lags tienen una relación directa con el valor actual una vez │ │ descontada la influencia de los lags intermedios. │ │ │ │ Por qué estos window features │ │ │ │ Se configuran tres objetos de ventana rodante: │ │ │ │ • Media y desviación estándar, ventana de 3 horas: recogen la tendencia y la │ │ volatilidad muy reciente, lo que ayuda al modelo a detectar cambios bruscos de │ │ nivel en el corto plazo. │ │ • Media, ventana de 24 horas: resume el nivel promedio del día anterior completo, │ │ capturando el ciclo diario sin necesidad de añadir 24 lags adicionales. │ │ • Media, ventana de 168 horas: resume el nivel promedio de la semana anterior │ │ completa (168 horas = 7 días), capturando el patrón semanal de forma compacta. │ │ │ │ Estas ventanas complementan los lags: donde los lags aportan valores puntuales en │ │ momentos específicos, las ventanas aportan estadísticas agregadas sobre rangos de │ │ tiempo relevantes. │ │ │ │ Otras fuentes de información del modelo │ │ │ │ Además de lags y ventanas, el modelo usa: │ │ │ │ • Variables exógenas: holiday, weather (categórica, gestionada automáticamente por │ │ skforecast) y temp. │ │ • Características de calendario: hour, day_of_week, weekend y month, codificadas │ │ como ordinales sin transformación adicional. │ │ │ │ Cómo se produce el intervalo de predicción del 80% │ │ │ │ El intervalo se construye mediante bootstrapping sobre los residuos del │ │ entrenamiento, siguiendo estos pasos: │ │ │ │ 1 Al ajustar el modelo con store_in_sample_residuals=True, el forecaster calcula y │ │ almacena la diferencia entre los valores reales y las predicciones sobre los │ │ datos de entrenamiento. │ │ 2 Al llamar a predict_interval, el modelo realiza múltiples simulaciones de la │ │ serie de predicción hacia adelante. En cada simulación, añade una muestra │ │ aleatoria de los residuos almacenados a cada paso, propagando la incertidumbre │ │ acumulada a lo largo del horizonte de 36 pasos. │ │ 3 Se usa use_binned_residuals=True, lo que significa que los residuos se │ │ seleccionan según el nivel de la predicción en cada paso (binning por magnitud), │ │ lo que produce intervalos mejor calibrados cuando la variabilidad del error no es │ │ constante. │ │ 4 Los percentiles 10 y 90 de la distribución de todas esas trayectorias simuladas │ │ forman los límites del intervalo, dando una cobertura nominal del 80%. │ │ │ │ El intervalo se expresa en las columnas lower_bound y upper_bound del DataFrame de │ │ predicciones, junto a la columna pred con el valor puntual. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Refinar el plan (opcional)¶
El método refine_plan() te permite ajustar el plan antes de la ejecución. Opera en dos modos distintos:
Modo Determinista (
prompt=None): pasa sobrescrituras (overrides) explícitas de configuración comolags,estimator,estimator_kwargs,forecaster,steps,interval, owindow_features. Solo los campos que especifiques explícitamente se actualizan; el resto de la configuración se vuelve a derivar de forma determinista del plan original.Modo LLM (
promptproporcionado): describe el conocimiento de tu dominio en lenguaje natural. El LLM interpreta este contexto y sugierelagsywindow_featuresapropiados. Su razonamiento se adjunta aplan.explanationy los campos modificados se registran enplan.llm_refined_fieldspara una trazabilidad completa.
⚠ Advertencia
Un plan refinado es una hipótesis, no una mejora garantizada. El LLM puede proponer lags o window features que no son útiles para la serie, o puede malinterpretar el contexto de dominio que proporcionaste. Compara siempre el plan refinado contra la baseline original utilizando un backtest adecuado a través de múltiples folds antes de adoptarlo.
Modo determinista¶
# Refinar el plan con overrides explícitos (no requiere LLM)
# ==============================================================================
plan_det = assistant.refine_plan(
profile = profile,
plan = plan,
lags = [1, 2, 3, 24, 48, 168],
estimator_kwargs = {'n_estimators': 200, 'max_depth': 6}
)
plan_det
Forecast Plan ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Steps │ 36 │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Frequency │ h │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Lags │ [1, 2, 3, 24, 48, 168] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Window features │ [{'stats': ['mean', 'std'], 'window_size': 3}, {'stats': ['mean'], │ │ │ 'window_size': 24}, {'stats': ['mean'], 'window_size': 168}] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Calendar features │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Use exog │ True │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval │ [0.1, 0.9] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval method │ bootstrapping │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Primary metric │ mean_absolute_error │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Preprocessing │ 1 step │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ Preprocessing Steps ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Step ┃ Reason ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ handle_categorical_exog │ Categorical exogenous variables detected: ['weather']. These │ │ │ are handled automatically by skforecast │ │ │ (categorical_features='auto'). │ └─────────────────────────┴──────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── Plan Explanation ───────────────────────────────────╮ │ │ │ Plan: ForecasterRecursive + LGBMRegressor. Lags: [1, 2, 3, 24, 48, 168]. Window │ │ features: ['mean(window=3)', 'std(window=3)', 'mean(window=24)', │ │ 'mean(window=168)']. Calendar features: ['hour', 'day_of_week', 'weekend', 'month'] │ │ (raw ordinal encoding). Prediction intervals via bootstrapping. NaN rows kept │ │ (NaN-tolerant estimator). Exogenous variables included. MAE is interpretable, robust │ │ to outliers, and works at any scale. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Modo LLM¶
# Refinar el plan usando conocimiento de dominio guiado por LLM
# ==============================================================================
prompt = (
"Estoy pronosticando el alquiler de bicicletas por hora. La demanda sigue un ritmo diario "
"claro con picos en horas punta, y cambia entre días de semana y fines de semana. "
"También suele ser similar a lo que ocurrió a la misma hora la semana pasada, y las últimas horas "
"dan una buena idea de la tendencia actual. Por favor, elige lags y window features que se ajusten a esto."
)
plan_refined = assistant.refine_plan(
profile = profile,
plan = plan,
prompt = prompt
)
# Plan refinado propuesto por el asistente
# ==============================================================================
plan_refined
Forecast Plan ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Steps │ 36 │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Frequency │ h │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Lags │ [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, 25, 47, 48, 49, 167, 168, 169] │ │ │ (LLM-suggested) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Window features │ [{'stats': ['mean', 'std'], 'window_size': 24}, {'stats': ['mean', │ │ │ 'std'], 'window_size': 168}] (LLM-suggested) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Calendar features │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Use exog │ True │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval │ [0.1, 0.9] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval method │ bootstrapping │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Primary metric │ mean_absolute_error │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Preprocessing │ 1 step │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ Preprocessing Steps ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Step ┃ Reason ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ handle_categorical_exog │ Categorical exogenous variables detected: ['weather']. These │ │ │ are handled automatically by skforecast │ │ │ (categorical_features='auto'). │ └─────────────────────────┴──────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── Plan Explanation ───────────────────────────────────╮ │ │ │ Plan: ForecasterRecursive + LGBMRegressor. Lags: [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, │ │ 25, 47, 48, 49, 167, 168, 169]. Window features: ['mean(window=24)', │ │ 'std(window=24)', 'mean(window=168)', 'std(window=168)']. Calendar features: │ │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding). Prediction │ │ intervals via bootstrapping. NaN rows kept (NaN-tolerant estimator). Exogenous │ │ variables included. MAE is interpretable, robust to outliers, and works at any │ │ scale. │ │ │ │ LLM Refinement Reasoning: The user describes three key dynamics for hourly bike │ │ rentals: │ │ │ │ 1 Recent trend (últimas horas): Lags 1–8 capture the immediate short-term momentum │ │ and the last few hours of riding patterns, which are strong predictors of the │ │ next step. │ │ 2 Daily seasonality with rush-hour peaks (ritmo diario): Lags 23, 24, 25 anchor the │ │ model to the same hour of the previous day (±1 h tolerance), directly encoding │ │ the 24-hour intra-day cycle and peak-hour structure. │ │ 3 Weekly seasonality and weekday/weekend differences (días de semana vs. fines de │ │ semana): Lags 47, 48, 49 cover the same hour two days ago, and lags 167, 168, 169 │ │ cover exactly the same hour last week (±1 h), which the user explicitly │ │ identifies as a strong reference point. │ │ │ │ Window features: │ │ │ │ • RollingFeatures(mean + std, window=24): Captures the average daily level and │ │ intra-day volatility over the last 24 hours — reactive to recent changes in │ │ demand. │ │ • RollingFeatures(mean + std, window=168): Captures the weekly average level and │ │ weekly volatility, reflecting the weekday/weekend regime. The std over 168 h is │ │ particularly useful for detecting whether the current week is unusually busy or │ │ quiet. │ │ │ │ This combination is lean, interpretable, and directly tied to the seasonality │ │ periods the user described (24 h and 168 h), avoiding redundant features while │ │ covering all three dynamics: recency, daily cycle, and weekly cycle. │ │ │ │ Note: the LLM-suggested lags and window_features are hypotheses, not validated │ │ improvements. Confirm any expected accuracy gain before relying on them. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Modo de explicación (plan refinado)¶
# Pedir al asistente qué cambió y por qué
# ==============================================================================
answer = assistant.ask(
prompt = (
"¿Qué cambió en el plan refinado en comparación con el original, "
"y por qué es importante para este dataset?"
),
profile = profile,
plan = plan_refined,
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮ │ │ │ El plan refinado ajusta principalmente la selección de lags y las ventanas de │ │ rolling features para alinearlas con los ciclos de estacionalidad específicos de │ │ este dataset de bicicletas por hora. │ │ │ │ Cambios en los lags │ │ │ │ El plan refinado usa un conjunto estructurado de lags en tres grupos: │ │ │ │ • Lags 1 a 8: capturan el momentum reciente de las últimas horas, importante para │ │ seguir la tendencia inmediata de uso. │ │ • Lags 23, 24, 25: anclan el modelo al mismo periodo del dia anterior (con │ │ tolerancia de mas o menos 1 hora), codificando directamente el ciclo intradiario │ │ de 24 horas y los picos de hora punta. │ │ • Lags 47, 48, 49 y 167, 168, 169: el primer grupo cubre el mismo periodo de hace │ │ dos dias, y el segundo grupo cubre exactamente el mismo periodo de la semana │ │ anterior (168 horas), capturando las diferencias entre dias laborables y fin de │ │ semana que el dataset presenta. │ │ │ │ Esto contrasta con un enfoque de lags consecutivos generico, que habria incluido │ │ lags intermedios de menor relevancia para este tipo de serie. │ │ │ │ Cambios en las window features │ │ │ │ Se definieron dos ventanas de rolling features: │ │ │ │ • Media y desviacion estandar con ventana de 24 horas: refleja el nivel promedio y │ │ la volatilidad del dia reciente, siendo reactiva a cambios rapidos en la demanda. │ │ • Media y desviacion estandar con ventana de 168 horas: refleja el nivel promedio │ │ semanal y la volatilidad del regimen semanal, permitiendo al modelo distinguir │ │ semanas mas ocupadas de semanas mas tranquilas. │ │ │ │ Por que es importante para este dataset │ │ │ │ El dataset tiene 17544 observaciones a frecuencia horaria, lo que equivale a │ │ aproximadamente dos anos de datos. Con tres fuentes de variacion principales │ │ (tendencia reciente, ciclo diario, ciclo semanal y diferencia laborable/fin de │ │ semana), un conjunto de lags no estructurado podria ignorar alguno de esos periodos │ │ o incluir lags redundantes que aumenten el ruido sin aportar senial. Los lags y │ │ ventanas elegidos estan directamente alineados con los dos periodos de │ │ estacionalidad documentados (24 h y 168 h), lo que hace el modelo mas interpretable │ │ y evita caracteristicas superfluas. │ │ │ │ Una advertencia importante: el plan indica explicitamente que los lags y window │ │ features sugeridos son hipotesis, no mejoras validadas. El impacto real sobre el MAE │ │ solo podra confirmarse tras ejecutar el backtesting. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Pronóstico (Forecast)¶
Una vez que tienes un profile y un plan, puedes llamar a forecast() o forecast_code(). Ambos aceptan el profile y el plan precalculados para que no se realice ningún perfilado adicional. El método forecast() ejecuta el script generado y devuelve un ForecastResult; forecast_code() genera solo el script, sin ejecutarlo.
La rama de forecast opera en dos modos:
Modo de evaluación (
test_sizeestablecido): el conjunto de datos se divide en conjuntos de entrenamiento (train) y test, el modelo se entrena en la parte de entrenamiento y las predicciones se comparan con los valores reales reservados para calcular las métricas.Modo de predicción (
test_size=None, por defecto): el modelo se entrena en todo el conjunto de datos y pronostica los próximosstepspuntos temporales hacia el futuro. Como no hay datos reales del futuro (ground truth), no se devuelven métricas. Si los datos tienen variables exógenas, sus valores futuros deben suministrarse a través deexog.
Modo de evaluación¶
# Pronóstico en modo evaluación, reutilizando el perfil y plan precalculados
# ==============================================================================
results_eval = assistant.forecast(
data = data,
target = 'users',
date_column = 'date_time',
steps = 36,
test_size = 36, # Últimas 36 horas como conjunto de test
profile = profile, # Reutilizar el perfil precalculado
plan = plan_refined # Reutilizar el plan refinado
)
display(results_eval.metrics)
display(results_eval.predictions.head())
| series | MAE | MSE | MASE | MAPE | |
|---|---|---|---|---|---|
| 0 | users | 38.198533 | 3128.704659 | 0.593244 | 0.464106 |
| pred | lower_bound | upper_bound | |
|---|---|---|---|
| 2012-12-30 12:00:00 | 138.604255 | 109.717473 | 168.478116 |
| 2012-12-30 13:00:00 | 134.757399 | 97.971820 | 170.428283 |
| 2012-12-30 14:00:00 | 134.030301 | 95.193056 | 172.192328 |
| 2012-12-30 15:00:00 | 132.393918 | 92.085828 | 164.241462 |
| 2012-12-30 16:00:00 | 132.248717 | 91.252356 | 169.533871 |
# Graficar las predicciones frente a los valores reales para el período de test reservado
# ==============================================================================
set_dark_theme()
predictions = results_eval.predictions
fig, ax = plt.subplots(figsize=(7, 3.5))
data.set_index('date_time').loc[predictions.index, 'users'].plot(ax=ax, label='real')
predictions['pred'].plot(ax=ax, label='predicción')
if {'lower_bound', 'upper_bound'}.issubset(predictions.columns):
ax.fill_between(
predictions.index, predictions['lower_bound'], predictions['upper_bound'],
alpha=0.3, label='Intervalo de predicción del 80%'
)
ax.set_title('Predicciones vs. demanda real de bicicletas')
ax.set_ylabel('Usuarios')
ax.legend()
plt.tight_layout()
plt.show()
# Pedir al asistente que interprete los resultados del pronóstico
# ==============================================================================
answer = assistant.ask(
prompt = "Explica los resultados de este pronóstico, incluyendo las métricas y las predicciones.",
result = results_eval
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮ │ │ │ El modelo ForecasterRecursive con LGBMRegressor produjo un pronóstico de 36 horas │ │ para la serie de usuarios, con métricas que indican un rendimiento sólido y │ │ predicciones que cubren un horizonte que va hasta el 31 de diciembre de 2012. │ │ │ │ Métricas de evaluación │ │ │ │ • MAE (Error Absoluto Medio): 38.20 usuarios -- En promedio, el modelo se equivoca │ │ en aproximadamente 38 usuarios por hora. Dado que el promedio de predicciones es │ │ de 76.68 usuarios, este nivel de error es moderado y comprensible para una serie │ │ con variabilidad diaria y semanal. │ │ • MSE (Error Cuadrático Medio): 3128.70 -- Penaliza más los errores grandes. │ │ Complementa al MAE para entender la magnitud de los desvíos más extremos. │ │ • MASE (Error Absoluto Medio Escalado): 0.59 -- Este valor es menor que 1, lo que │ │ significa que el modelo supera al pronóstico ingenuo (naive baseline) de │ │ referencia. Un MASE de 0.59 indica que el modelo comete alrededor del 59% del │ │ error que cometería un modelo ingenuo. │ │ • MAPE (Error Porcentual Absoluto Medio): 0.46 -- Expresado como porcentaje, el │ │ error promedio es del 46%. Este valor puede ser menos confiable en periodos donde │ │ el número de usuarios es muy bajo (cercano a cero), ya que el denominador se │ │ vuelve pequeño. │ │ │ │ Predicciones │ │ │ │ El horizonte de pronóstico cubre 36 horas, desde el 30 de diciembre de 2012 a las │ │ 12:00 hasta el 31 de diciembre de 2012 a las 23:00. │ │ │ │ • Rango de predicciones puntuales: entre 4.87 y 159.31 usuarios por hora, con una │ │ media de 76.68 usuarios. │ │ • Rango de límite inferior (80%): entre 0.46 y 109.72 usuarios. │ │ • Rango de límite superior (80%): entre 11.18 y 184.70 usuarios. │ │ │ │ Intervalos de prediccion │ │ │ │ El modelo genera intervalos al 80% de cobertura (percentiles 0.10 y 0.90) mediante │ │ el metodo de bootstrapping. Esto significa que, en promedio, el 80% de los valores │ │ reales deberian caer dentro de la banda definida por el limite inferior y superior. │ │ La amplitud del intervalo varia a lo largo del horizonte: los primeros pasos (por │ │ ejemplo, 12:00 del 30 de diciembre) muestran un intervalo mas estrecho (109.72 a │ │ 168.48), mientras que las ultimas horas del 31 de diciembre presentan intervalos │ │ considerablemente mas amplios en terminos relativos, lo que refleja mayor │ │ incertidumbre acumulada a medida que el horizonte se extiende. │ │ │ │ Conclusion general │ │ │ │ El modelo bate al naive baseline (MASE menor que 1), lo que confirma que las │ │ caracteristicas incorporadas (lags recientes, estacionalidad diaria y semanal, │ │ variables exogenas como clima y festivos) aportan valor predictivo real. La │ │ incertidumbre crece hacia el final del horizonte, lo cual es un comportamiento │ │ esperado y saludable en cualquier pronostico recursivo. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Modo de predicción¶
En el modo de predicción, el modelo se entrena en todo el conjunto de datos y pronostica los próximos steps puntos temporales. Como los datos incluyen variables exógenas (holiday, weather, temp), sus valores futuros deben proporcionarse mediante el argumento exog.
# Pronosticar las próximas 36 horas utilizando todo el conjunto de datos (modo predicción)
# ==============================================================================
# Simular valores futuros de variables exógenas para las próximas 36 horas
exog = data[['holiday', 'weather', 'temp']].tail(36).copy()
exog.index = pd.date_range(
start=pd.to_datetime(data['date_time'].max()) + pd.Timedelta(hours=1),
periods=36,
freq='h'
)
results_pred = assistant.forecast(
data = data,
target = 'users',
date_column = 'date_time',
steps = 36,
test_size = None, # Usar todo el conjunto de datos (modo predicción)
exog = exog, # Valores futuros de las variables exógenas
profile = profile,
plan = plan_refined
)
display(results_pred.predictions.head())
| pred | lower_bound | upper_bound | |
|---|---|---|---|
| 2013-01-01 00:00:00 | 24.195736 | 15.547905 | 33.672441 |
| 2013-01-01 01:00:00 | 13.359010 | 2.283441 | 23.108125 |
| 2013-01-01 02:00:00 | 8.672621 | 3.120421 | 15.382074 |
| 2013-01-01 03:00:00 | 5.543170 | 0.726088 | 11.122228 |
| 2013-01-01 04:00:00 | 5.161747 | 1.221141 | 10.099236 |
# Objeto de resultados completo
# ==============================================================================
results_pred
Dataset Profile ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Format │ single │ ├────────────────┼────────────────────────────────────────────────┤ │ Series │ 1 │ ├────────────────┼────────────────────────────────────────────────┤ │ Observations │ 17544 │ ├────────────────┼────────────────────────────────────────────────┤ │ Frequency │ h │ ├────────────────┼────────────────────────────────────────────────┤ │ Target │ users │ ├────────────────┼────────────────────────────────────────────────┤ │ Exog columns │ holiday, weather, temp (categorical: weather) │ ├────────────────┼────────────────────────────────────────────────┤ │ Missing values │ None │ └────────────────┴────────────────────────────────────────────────┘ Recommendation ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Forecaster candidates │ ForecasterRecursive, ForecasterDirect, ForecasterFoundation │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Estimator candidates │ LGBMRegressor, XGBRegressor, Ridge │ └───────────────────────┴─────────────────────────────────────────────────────────────┘ ╭───────────────────────────────── Profile Explanation ──────────────────────────────────╮ │ │ │ A single-series ML forecaster (ForecasterRecursive) is recommended. Data: 17544 │ │ observations, 'h' frequency. Alternative forecasters: ['ForecasterDirect', │ │ 'ForecasterFoundation']. Estimator: LGBMRegressor. A gradient boosting model is │ │ preferred for a dataset of this size (17544 observations). Alternative estimators: │ │ ['XGBRegressor', 'Ridge']. 3 exogenous variables (1 categorical) available as │ │ predictors. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ Forecast Plan ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Steps │ 36 │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Frequency │ h │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Lags │ [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, 25, 47, 48, 49, 167, 168, 169] │ │ │ (LLM-suggested) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Window features │ [{'stats': ['mean', 'std'], 'window_size': 24}, {'stats': ['mean', │ │ │ 'std'], 'window_size': 168}] (LLM-suggested) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Calendar features │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Use exog │ True │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval │ [0.1, 0.9] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval method │ bootstrapping │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Primary metric │ mean_absolute_error │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Preprocessing │ 1 step │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ Preprocessing Steps ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Step ┃ Reason ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ handle_categorical_exog │ Categorical exogenous variables detected: ['weather']. These │ │ │ are handled automatically by skforecast │ │ │ (categorical_features='auto'). │ └─────────────────────────┴──────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── Plan Explanation ───────────────────────────────────╮ │ │ │ Plan: ForecasterRecursive + LGBMRegressor. Lags: [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, │ │ 25, 47, 48, 49, 167, 168, 169]. Window features: ['mean(window=24)', │ │ 'std(window=24)', 'mean(window=168)', 'std(window=168)']. Calendar features: │ │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding). Prediction │ │ intervals via bootstrapping. NaN rows kept (NaN-tolerant estimator). Exogenous │ │ variables included. MAE is interpretable, robust to outliers, and works at any │ │ scale. │ │ │ │ LLM Refinement Reasoning: The user describes three key dynamics for hourly bike │ │ rentals: │ │ │ │ 1 Recent trend (últimas horas): Lags 1–8 capture the immediate short-term momentum │ │ and the last few hours of riding patterns, which are strong predictors of the │ │ next step. │ │ 2 Daily seasonality with rush-hour peaks (ritmo diario): Lags 23, 24, 25 anchor the │ │ model to the same hour of the previous day (±1 h tolerance), directly encoding │ │ the 24-hour intra-day cycle and peak-hour structure. │ │ 3 Weekly seasonality and weekday/weekend differences (días de semana vs. fines de │ │ semana): Lags 47, 48, 49 cover the same hour two days ago, and lags 167, 168, 169 │ │ cover exactly the same hour last week (±1 h), which the user explicitly │ │ identifies as a strong reference point. │ │ │ │ Window features: │ │ │ │ • RollingFeatures(mean + std, window=24): Captures the average daily level and │ │ intra-day volatility over the last 24 hours — reactive to recent changes in │ │ demand. │ │ • RollingFeatures(mean + std, window=168): Captures the weekly average level and │ │ weekly volatility, reflecting the weekday/weekend regime. The std over 168 h is │ │ particularly useful for detecting whether the current week is unusually busy or │ │ quiet. │ │ │ │ This combination is lean, interpretable, and directly tied to the seasonality │ │ periods the user described (24 h and 168 h), avoiding redundant features while │ │ covering all three dynamics: recency, daily cycle, and weekly cycle. │ │ │ │ Note: the LLM-suggested lags and window_features are hypotheses, not validated │ │ improvements. Confirm any expected accuracy gain before relying on them. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ Predictions (36 rows) ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓ ┃ Index ┃ pred ┃ lower_bound ┃ upper_bound ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩ │ 2013-01-01 00:00:00 │ 24.1957 │ 15.5479 │ 33.6724 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-01 01:00:00 │ 13.3590 │ 2.2834 │ 23.1081 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-01 02:00:00 │ 8.6726 │ 3.1204 │ 15.3821 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-01 03:00:00 │ 5.5432 │ 0.7261 │ 11.1222 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-01 04:00:00 │ 5.1617 │ 1.2211 │ 10.0992 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ ... │ ... │ ... │ ... │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-02 07:00:00 │ 55.7190 │ 24.5896 │ 101.6793 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-02 08:00:00 │ 88.3086 │ 47.6449 │ 177.8052 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-02 09:00:00 │ 101.6985 │ 53.3129 │ 191.3656 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-02 10:00:00 │ 143.5425 │ 46.8845 │ 166.9387 │ ├─────────────────────┼──────────┼─────────────┼─────────────┤ │ 2013-01-02 11:00:00 │ 183.1477 │ 42.5010 │ 198.5246 │ └─────────────────────┴──────────┴─────────────┴─────────────┘
Modo de solo código¶
Usa forecast_code() cuando desees previsualizar o exportar el script reproducible sin ejecutarlo. Esto es útil para revisión de código, auditoría del pipeline generado, o para ejecutar el script en un entorno separado.
# Generar el script reproducible sin ejecutarlo
# ==============================================================================
code_result = assistant.forecast_code(
data = data,
target = 'users',
date_column = 'date_time',
steps = 36,
test_size = 36,
profile = profile,
plan = plan_refined
)
code_result.show_code()
El objeto ForecastResult¶
Ambos modos de forecast() devuelven un ForecastResult, un contenedor ligero que agrupa todo lo que el asistente usó y produjo.
| Atributo | Tipo | Descripción |
|---|---|---|
predictions |
DataFrame | Valores pronosticados. Cuando se solicitan intervalos, las columnas de los límites se incluyen junto a las predicciones puntuales. |
metrics |
DataFrame o None | Métricas de evaluación (MAE, MSE, MASE, MAPE), una fila por serie. None en modo predicción. |
code |
str | El script exacto e independiente de skforecast que produjo el pronóstico, listo para ejecutarse por su cuenta. |
profile |
ForecastingProfile |
El perfil de datos detrás del pronóstico. |
plan |
ForecastPlan |
La configuración detallada que se ejecutó. |
Backtesting¶
La rama de backtesting usa los mismos profile y plan que la rama de forecast, pero evalúa el rendimiento histórico del modelo a través de validación cruzada de series temporales (time series cross-validation). La decisión clave es cómo configurar el objeto TimeSeriesFold, que controla exactamente cómo los datos históricos se dividen en ventanas sucesivas de entrenamiento y test.
skforecast-ai proporciona tres formas distintas de definir esta estrategia de validación:
Instanciación explícita (recomendada): construye manualmente un
TimeSeriesFoldy pásalo directamente abacktest(). Úsalo cuando ya conozcas tus restricciones operativas exactas.create_cv()determinista: permite que el asistente derive unTimeSeriesFoldrazonable del perfil y del plan usando valores por defecto basados en reglas. Puedes sobrescribir parámetros individuales explícitamente.create_cv()por LLM (con un prompt): describe tu caso de uso de despliegue en lenguaje natural. El LLM traduce tu descripción en un esquema deTimeSeriesFoldcompletamente configurado, acompañado de una explicación que puedes auditar.
Definir la estrategia de backtesting¶
TimeSeriesFold Manual¶
# Crea tu propio objeto TimeSeriesFold
# ==============================================================================
end_train = '2012-08-31 23:59:00'
cv = TimeSeriesFold(
steps = 36,
initial_train_size = end_train,
refit = False,
verbose = False
)
cv
TimeSeriesFold
General Information
- Initial train size: 2012-08-31 23:59:00
- Initial train size as int: None
- Steps: 36
- Fold stride: 36
- Overlapping folds: False
- Window size: None
- Differentiation: None
- Refit: False
- Fixed train size: True
- Gap: 0
- Skip folds: None
- Allow incomplete fold: True
- Return all indexes: False
create_cv() determinista¶
# Deja que el asistente derive un TimeSeriesFold con defaults basados en reglas
# ==============================================================================
cv_det, cv_det_explanation = assistant.create_cv(
profile = profile,
plan = plan_refined,
initial_train_size = end_train,
refit = False,
)
print(cv_det_explanation)
cv_det
Initial training up to 2012-08-31 23:59:00, expanding window, no refit, 36-step horizon, 82 folds.
TimeSeriesFold
General Information
- Initial train size: 2012-08-31 23:59:00
- Initial train size as int: 14616
- Steps: 36
- Fold stride: 36
- Overlapping folds: False
- Window size: None
- Differentiation: None
- Refit: False
- Fixed train size: False
- Gap: 0
- Skip folds: None
- Allow incomplete fold: True
- Return all indexes: False
create_cv() por LLM con un prompt en lenguaje natural¶
En lugar de configurar manualmente los parámetros de TimeSeriesFold, puedes describir tu estrategia de backtesting en lenguaje natural y dejar que el asistente lo traduzca a un riguroso esquema de validación cruzada.
# Deja que el asistente cree el TimeSeriesFold a partir de un prompt en lenguaje natural
# ==============================================================================
prompt = (
"Pronostico la demanda de bicicletas a 36 horas vista. "
"El modelo debe entrenarse una vez con todos los datos hasta finales de agosto de 2012, 23:59. "
"No vuelvas a entrenar (refit) el modelo a medida que la ventana avanza."
)
cv_llm, cv_llm_explanation = assistant.create_cv(
profile = profile,
plan = plan_refined,
prompt = prompt
)
# TimeSeriesFold derivado del prompt
# ==============================================================================
cv_llm
TimeSeriesFold
General Information
- Initial train size: 2012-08-31 23:59
- Initial train size as int: 14616
- Steps: 36
- Fold stride: 36
- Overlapping folds: False
- Window size: None
- Differentiation: None
- Refit: False
- Fixed train size: False
- Gap: 0
- Skip folds: None
- Allow incomplete fold: True
- Return all indexes: False
# Razonamiento del LLM detrás de la configuración del TimeSeriesFold
# ==============================================================================
print(textwrap.fill(cv_llm_explanation, width=88))
The user wants to train the model exactly once on all data up to the end of August 2012 (23:59), so initial_train_size is set to the date string "2012-08-31 23:59". Since no retraining is desired as the evaluation window rolls forward, refit=False. This simulates a single deployment where the model is trained once and then evaluated across the remaining data (September 2012 onward) using 36-hour-ahead forecasts. fixed_train_size is left at its default (True) — though it has no practical effect when refit=False — and gap=0 since no deployment delay was mentioned. With 17,544 hourly observations and a cutoff near the end of August 2012 (roughly 17,519 hours from the start of the dataset), this leaves ample observations for multiple 36-step evaluation folds. Initial training up to 2012-08-31 23:59, expanding window, no refit, 36-step horizon, 82 folds.
Como el prompt describe correctamente el corte de entrenamiento y el horizonte previstos, el objeto cv_llm devuelto por create_cv() reproduce los mismos initial_train_size y steps que el que construimos manualmente. Ten en cuenta, sin embargo, que create_cv() por defecto utiliza una ventana en expansión (fixed_train_size=False) a menos que se solicite explícitamente una fija, por lo que cv_llm y cv_det difieren del cv construido manualmente (que usa una ventana fija) en ese aspecto.
✏️ Nota
El asistente también devuelve un string cv_llm_explanation que detalla las elecciones que hizo. Inspecciónalo siempre, así como el TimeSeriesFold resultante, en lugar de asumir que una configuración derivada por LLM es equivalente a lo que pretendías.
Ejecutar el backtest¶
# Ejecutar backtesting, reutilizando el perfil y plan precalculados
# ==============================================================================
results_backtest = assistant.backtest(
data = data,
target = 'users',
date_column = 'date_time',
cv = cv, # Objeto TimeSeriesFold
profile = profile, # Reutilizar el perfil precalculado
plan = plan_refined # Reutilizar el plan refinado
)
results_backtest.show_explanation()
display(results_backtest.metrics)
display(results_backtest.predictions.head())
0%| | 0/82 [00:00<?, ?it/s]
╭───────────────────────────────── Backtest Explanation ─────────────────────────────────╮ │ │ │ Initial training up to 2012-08-31 23:59:00, fixed window, no refit, 36-step horizon, │ │ 82 folds. Results — mean_absolute_error: 52.0812, mean_squared_error: 7238.9334, │ │ mean_absolute_scaled_error: 0.8494, mean_absolute_percentage_error: 0.6039. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
| mean_absolute_error | mean_squared_error | mean_absolute_scaled_error | mean_absolute_percentage_error | |
|---|---|---|---|---|
| 0 | 52.081199 | 7238.933442 | 0.849416 | 0.603865 |
| fold | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2012-09-01 00:00:00 | 0 | 131.683979 | 103.469707 | 162.260067 |
| 2012-09-01 01:00:00 | 0 | 109.252720 | 78.937848 | 142.900301 |
| 2012-09-01 02:00:00 | 0 | 73.810825 | 51.975214 | 108.556965 |
| 2012-09-01 03:00:00 | 0 | 43.627326 | 23.415241 | 68.818569 |
| 2012-09-01 04:00:00 | 0 | 20.733795 | 6.571552 | 38.886267 |
# Graficar intervalos de predicción vs valores reales
# ==============================================================================
predictions = results_backtest.predictions
data_test = data.set_index('date_time').loc[predictions.index, :]
fig = go.Figure([
go.Scatter(name='Predicción', x=predictions.index, y=predictions['pred'], mode='lines'),
go.Scatter(
name='Valor real', x=data_test.index, y=data_test['users'], mode='lines',
),
go.Scatter(
name='Límite superior', x=predictions.index, y=predictions['upper_bound'], mode='lines',
marker=dict(color="#444"), line=dict(width=0), showlegend=False
),
go.Scatter(
name='Límite inferior', x=predictions.index, y=predictions['lower_bound'], marker=dict(color="#444"),
line=dict(width=0), mode='lines', fillcolor='rgba(68, 68, 68, 0.3)', fill='tonexty', showlegend=False
)
])
fig.update_layout(
title="Valor real vs predicho en datos de test",
xaxis_title="Fecha y hora",
yaxis_title="Usuarios",
width=800,
height=400,
margin=dict(l=20, r=20, t=35, b=20),
hovermode="x",
legend=dict(orientation="h", yanchor="top", y=1.1, xanchor="left", x=0.001),
# Zoom inicial en el eje x entre el 1 y el 10 de octubre
xaxis=dict(range=['2012-10-01', '2012-10-10'])
)
fig.show()
# Pedir al asistente que interprete los resultados del backtesting
# ==============================================================================
answer = assistant.ask(
prompt = (
"Explica los resultados de este backtesting, incluyendo la estrategia, métricas "
"y predicciones. ¿Es el modelo lo suficientemente bueno para ser desplegado?"
),
result = results_backtest
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮ │ │ │ El backtesting muestra un modelo con buen desempeño general: el MASE de 0.8494 │ │ indica que supera al modelo naive de referencia, y el MAE de 52.08 usuarios por hora │ │ da una idea concreta del error típico. Sin embargo, hay aspectos a considerar antes │ │ de decidir sobre el despliegue. │ │ │ │ Estrategia de backtesting │ │ │ │ La evaluación utilizó los siguientes parámetros: │ │ │ │ • Entrenamiento inicial hasta el 31 de agosto de 2012, con ventana fija │ │ (fixed_train_size=True) │ │ • Sin reentrenamiento durante la evaluación (refit=False) │ │ • Horizonte de predicción de 36 pasos (36 horas hacia adelante) │ │ • 82 folds evaluados, con un stride de 36 pasos (folds no solapados) │ │ • Sin gap entre el fin del entrenamiento y el inicio de cada periodo de test │ │ │ │ Esto simula un escenario en el que el modelo se entrena una sola vez y se usa para │ │ producir bloques de 36 horas consecutivas sin actualizarse, lo cual es una │ │ evaluación realista y conservadora. │ │ │ │ Métricas de rendimiento │ │ │ │ • mean_absolute_error: 52.08 usuarios por hora │ │ • mean_squared_error: 7238.93 (raíz cuadrada no disponible en contexto) │ │ • mean_absolute_scaled_error (MASE): 0.8494 │ │ • mean_absolute_percentage_error (MAPE): 0.6039 │ │ │ │ El MASE de 0.8494 es la métrica clave: al estar por debajo de 1.0, el modelo supera │ │ al baseline naive. El MAPE de 0.6039 (aproximadamente 60%) parece elevado, pero esta │ │ métrica se vuelve poco fiable cuando los valores reales se acercan a cero, lo cual │ │ ocurre en horas nocturnas con muy pocos usuarios. No debe interpretarse de forma │ │ aislada. │ │ │ │ Predicciones │ │ │ │ A lo largo de los 2928 puntos predichos en todos los folds: │ │ │ │ • Las predicciones van desde 3.63 hasta 865.61 usuarios por hora, con una media de │ │ 234.72 │ │ • Los intervalos de confianza del 80% tienen como cota inferior media 188.72 y cota │ │ superior media 280.87 │ │ • El límite inferior mínimo es ligeramente negativo (-0.088), algo esperado con │ │ bootstrapping en valores cercanos a cero; en producción conviene aplicar un clip │ │ a 0 │ │ │ │ ¿Es suficientemente bueno para despliegue? │ │ │ │ Hay señales positivas y precauciones a considerar: │ │ │ │ Señales positivas: │ │ │ │ • El MASE por debajo de 1 confirma que el modelo aporta valor real frente al naive │ │ • El modelo captura patrones de demanda horaria con un MAE de 52 usuarios, lo cual │ │ puede ser aceptable dependiendo del volumen típico de la serie │ │ • Los intervalos de predicción del 80% parecen razonablemente calibrados dado el │ │ rango de valores │ │ │ │ Precauciones antes del despliegue: │ │ │ │ • El modelo fue entrenado una sola vez y no se reentrenó (refit=False); en │ │ producción, el comportamiento puede degradarse sin reentrenamientos periódicos │ │ • El MAPE alto sugiere que el modelo tiene dificultades en horas de muy baja │ │ demanda │ │ • Conviene revisar si el error de 52 usuarios por hora es tolerable en el contexto │ │ operativo concreto (por ejemplo, planificación de flota de bicicletas) │ │ • Se recomienda comparar contra un baseline estadístico con │ │ ForecasterEquivalentDate o ForecasterStats para confirmar que la ventaja es │ │ suficiente antes de comprometerse con el despliegue │ │ │ │ En resumen, el modelo es prometedor pero la decisión final de despliegue depende del │ │ umbral de error aceptable para el negocio y de validar su comportamiento con │ │ reentrenamiento periódico. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Modo de solo código¶
Usa backtest_code() para generar el script de backtesting reproducible sin ejecutarlo.
# Generar el script de backtesting reproducible sin ejecutarlo
# ==============================================================================
code_backtest = assistant.backtest_code(
data = data,
target = 'users',
date_column = 'date_time',
cv = cv,
profile = profile,
plan = plan_refined
)
code_backtest.show_code()
El objeto BacktestResult¶
El método backtest() devuelve un BacktestResult, un contenedor ligero que agrupa todos los artefactos de backtesting.
| Atributo | Tipo | Descripción |
|---|---|---|
predictions |
DataFrame | Predicciones completas out-of-sample del backtest a través de todos los folds. |
metrics |
DataFrame | Métricas de backtesting (MAE, MSE, MASE, MAPE), una fila por serie. |
cv_config |
dict | Parámetros resueltos de TimeSeriesFold para trazabilidad completa de la estrategia de validación. |
code |
str | El script exacto e independiente de skforecast que reproduce el flujo de trabajo de backtesting. |
explanation |
str | Resumen legible por humanos de la configuración de backtesting y sus resultados. |
profile |
ForecastingProfile |
El perfil de datos detrás del backtest. |
plan |
ForecastPlan |
La configuración detallada que se ejecutó. |
# Objeto de resultados completo
# ==============================================================================
results_backtest
╭───────────────────────────────── Backtest Explanation ─────────────────────────────────╮ │ │ │ Initial training up to 2012-08-31 23:59:00, fixed window, no refit, 36-step horizon, │ │ 82 folds. Results — mean_absolute_error: 52.0812, mean_squared_error: 7238.9334, │ │ mean_absolute_scaled_error: 0.8494, mean_absolute_percentage_error: 0.6039. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ Cross-Validation Configuration ┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓ ┃ Parameter ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩ │ steps │ 36 │ ├────────────────────┼─────────────────────┤ │ initial_train_size │ 2012-08-31 23:59:00 │ ├────────────────────┼─────────────────────┤ │ refit │ False │ ├────────────────────┼─────────────────────┤ │ fixed_train_size │ True │ ├────────────────────┼─────────────────────┤ │ gap │ 0 │ ├────────────────────┼─────────────────────┤ │ fold_stride │ 36 │ ├────────────────────┼─────────────────────┤ │ differentiation │ None │ ├────────────────────┼─────────────────────┤ │ n_folds │ 82 │ └────────────────────┴─────────────────────┘ Backtest Metrics ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓ ┃ mean_absolute_error ┃ mean_squared_error ┃ mean_absolute_scale… ┃ mean_absolute_perce… ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩ │ 52.0812 │ 7238.9334 │ 0.8494 │ 0.6039 │ └─────────────────────┴────────────────────┴──────────────────────┴──────────────────────┘ Backtest Predictions (2928 rows) ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓ ┃ Index ┃ fold ┃ pred ┃ lower_bound ┃ upper_bound ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩ │ 2012-09-01 00:00:00 │ 0.0000 │ 131.6840 │ 103.4697 │ 162.2601 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-09-01 01:00:00 │ 0.0000 │ 109.2527 │ 78.9378 │ 142.9003 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-09-01 02:00:00 │ 0.0000 │ 73.8108 │ 51.9752 │ 108.5570 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-09-01 03:00:00 │ 0.0000 │ 43.6273 │ 23.4152 │ 68.8186 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-09-01 04:00:00 │ 0.0000 │ 20.7338 │ 6.5716 │ 38.8863 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ ... │ ... │ ... │ ... │ ... │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-12-31 19:00:00 │ 81.0000 │ 72.4376 │ 37.1473 │ 114.1578 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-12-31 20:00:00 │ 81.0000 │ 48.9850 │ 21.8319 │ 84.3220 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-12-31 21:00:00 │ 81.0000 │ 34.1233 │ 14.6820 │ 69.4655 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-12-31 22:00:00 │ 81.0000 │ 27.4199 │ 11.4344 │ 48.3156 │ ├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤ │ 2012-12-31 23:00:00 │ 81.0000 │ 17.7089 │ 8.8395 │ 37.0571 │ └─────────────────────┴─────────┴──────────┴─────────────┴─────────────┘ Dataset Profile ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Format │ single │ ├────────────────┼────────────────────────────────────────────────┤ │ Series │ 1 │ ├────────────────┼────────────────────────────────────────────────┤ │ Observations │ 17544 │ ├────────────────┼────────────────────────────────────────────────┤ │ Frequency │ h │ ├────────────────┼────────────────────────────────────────────────┤ │ Target │ users │ ├────────────────┼────────────────────────────────────────────────┤ │ Exog columns │ holiday, weather, temp (categorical: weather) │ ├────────────────┼────────────────────────────────────────────────┤ │ Missing values │ None │ └────────────────┴────────────────────────────────────────────────┘ Recommendation ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Forecaster candidates │ ForecasterRecursive, ForecasterDirect, ForecasterFoundation │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────────┼─────────────────────────────────────────────────────────────┤ │ Estimator candidates │ LGBMRegressor, XGBRegressor, Ridge │ └───────────────────────┴─────────────────────────────────────────────────────────────┘ ╭───────────────────────────────── Profile Explanation ──────────────────────────────────╮ │ │ │ A single-series ML forecaster (ForecasterRecursive) is recommended. Data: 17544 │ │ observations, 'h' frequency. Alternative forecasters: ['ForecasterDirect', │ │ 'ForecasterFoundation']. Estimator: LGBMRegressor. A gradient boosting model is │ │ preferred for a dataset of this size (17544 observations). Alternative estimators: │ │ ['XGBRegressor', 'Ridge']. 3 exogenous variables (1 categorical) available as │ │ predictors. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ Forecast Plan ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Steps │ 36 │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Frequency │ h │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Lags │ [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, 25, 47, 48, 49, 167, 168, 169] │ │ │ (LLM-suggested) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Window features │ [{'stats': ['mean', 'std'], 'window_size': 24}, {'stats': ['mean', │ │ │ 'std'], 'window_size': 168}] (LLM-suggested) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Calendar features │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding) │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Use exog │ True │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval │ [0.1, 0.9] │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Interval method │ bootstrapping │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Primary metric │ mean_absolute_error │ ├───────────────────┼────────────────────────────────────────────────────────────────────┤ │ Preprocessing │ 1 step │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ Preprocessing Steps ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Step ┃ Reason ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ handle_categorical_exog │ Categorical exogenous variables detected: ['weather']. These │ │ │ are handled automatically by skforecast │ │ │ (categorical_features='auto'). │ └─────────────────────────┴──────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── Plan Explanation ───────────────────────────────────╮ │ │ │ Plan: ForecasterRecursive + LGBMRegressor. Lags: [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, │ │ 25, 47, 48, 49, 167, 168, 169]. Window features: ['mean(window=24)', │ │ 'std(window=24)', 'mean(window=168)', 'std(window=168)']. Calendar features: │ │ ['hour', 'day_of_week', 'weekend', 'month'] (raw ordinal encoding). Prediction │ │ intervals via bootstrapping. NaN rows kept (NaN-tolerant estimator). Exogenous │ │ variables included. MAE is interpretable, robust to outliers, and works at any │ │ scale. │ │ │ │ LLM Refinement Reasoning: The user describes three key dynamics for hourly bike │ │ rentals: │ │ │ │ 1 Recent trend (últimas horas): Lags 1–8 capture the immediate short-term momentum │ │ and the last few hours of riding patterns, which are strong predictors of the │ │ next step. │ │ 2 Daily seasonality with rush-hour peaks (ritmo diario): Lags 23, 24, 25 anchor the │ │ model to the same hour of the previous day (±1 h tolerance), directly encoding │ │ the 24-hour intra-day cycle and peak-hour structure. │ │ 3 Weekly seasonality and weekday/weekend differences (días de semana vs. fines de │ │ semana): Lags 47, 48, 49 cover the same hour two days ago, and lags 167, 168, 169 │ │ cover exactly the same hour last week (±1 h), which the user explicitly │ │ identifies as a strong reference point. │ │ │ │ Window features: │ │ │ │ • RollingFeatures(mean + std, window=24): Captures the average daily level and │ │ intra-day volatility over the last 24 hours — reactive to recent changes in │ │ demand. │ │ • RollingFeatures(mean + std, window=168): Captures the weekly average level and │ │ weekly volatility, reflecting the weekday/weekend regime. The std over 168 h is │ │ particularly useful for detecting whether the current week is unusually busy or │ │ quiet. │ │ │ │ This combination is lean, interpretable, and directly tied to the seasonality │ │ periods the user described (24 h and 168 h), avoiding redundant features while │ │ covering all three dynamics: recency, daily cycle, and weekly cycle. │ │ │ │ Note: the LLM-suggested lags and window_features are hypotheses, not validated │ │ improvements. Confirm any expected accuracy gain before relying on them. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Comparando forecasters¶
Elegir un modelo de forecasting no debería basarse únicamente en la intuición. Dos configuraciones que parecen igualmente razonables pueden tener un rendimiento muy diferente una vez que se evalúan en datos temporales reales. El enfoque más fiable es probar cada candidato bajo condiciones idénticas y comparar sus métricas.
El método compare() hace exactamente eso. Recibe una lista de configuraciones candidatas, somete a backtest cada una utilizando la misma estrategia TimeSeriesFold y devuelve una tabla de clasificación (leaderboard) ordenada por la métrica seleccionada.
En la vía paso a paso, el argumento clave es profile. Pasar el perfil calculado al principio de este tutorial omite el perfilado por completo y garantiza que cada candidato se evalúe con el mismo perfil de datos. Nota que compare() no acepta un plan: cada candidato deriva su propio plan del perfil compartido, que es precisamente lo que hace que los candidatos difieran.
Los candidatos se pueden proporcionar de dos formas:
Candidatos automáticos (
candidates=None): el asistente construye el conjunto de comparación a partir deprofile.forecaster_candidates, utilizando los tipos de forecaster identificados como adecuados durante el perfilado. Esto es útil al explorar un conjunto de datos nuevo sin una lista predefinida.Candidatos explícitos (recomendado): pasa una lista de tuplas
(name, config), dondenameetiqueta la fila en el leaderboard yconfigcontiene las mismas claves de override entendidas porplan():'forecaster','estimator','estimator_kwargs','lags'y'window_features'. Esto proporciona un control total y facilita la interpretación de la tabla resultante.
Un candidato fallido no detiene la comparación. En su lugar, se emite una advertencia CandidateFailedWarning, la fila registra el error y se coloca en último lugar.
💡 Consejo
Todos los candidatos utilizan la misma estrategia de validación cruzada, garantizando una comparación justa. Sin embargo, los resultados solo son significativos si la configuración de cv refleja el caso de uso real donde el modelo será desplegado. Por ejemplo, si el sistema de producción se reentrena semanalmente, el backtest también debería hacer refit semanalmente. Si se espera que el modelo pronostique 24 horas hacia adelante, el backtest debe usar un horizonte de 24 horas.
La ventana de evaluación también debe ser representativa. Un período demasiado corto o dominado por eventos inusuales (días festivos, interrupciones o picos excepcionales) puede favorecer a un candidato que en realidad rinde de manera deficiente a lo largo del tiempo. Define la configuración de validación con cuidado antes de comparar modelos para que el ranking final sea fiable.
Candidatos automáticos¶
# Comparar los forecasters candidatos sugeridos por el perfil
# ==============================================================================
results_compare = assistant.compare(
data = data,
target = 'users',
date_column = 'date_time',
cv = cv, # El mismo TimeSeriesFold usado en el backtest anterior
candidates = None, # Candidatos sugeridos por el asistente
profile = profile # Reutilizar el perfil precalculado
)
Comparing forecasters: 0%| | 0/3 [00:00<?, ?it/s]
Loading weights: 0%| | 0/92 [00:00<?, ?it/s]
# Tabla de clasificación (leaderboard)
# ==============================================================================
results_compare.results
| rank | name | forecaster | estimator | mean_absolute_error | mean_squared_error | mean_absolute_scaled_error | mean_absolute_percentage_error | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | ForecasterFoundation | ForecasterFoundation | Chronos-2 | 38.436156 | 4231.326355 | 0.597467 | 0.611349 |
| 1 | 2 | ForecasterRecursive | ForecasterRecursive | LGBMRegressor | 46.584343 | 5443.171132 | 0.754003 | 0.470311 |
| 2 | 3 | ForecasterDirect | ForecasterDirect | LGBMRegressor | 49.020298 | 5848.934447 | 0.793430 | 0.497920 |
# Resumen determinista de la comparación
# ==============================================================================
results_compare.show_explanation()
╭──────────────────────────────── Comparison Explanation ────────────────────────────────╮ │ │ │ Compared 3 configurations, ranked ascending by mean_absolute_error. Shared │ │ cross-validation strategy: Initial training up to 2012-08-31 23:59:00, fixed window, │ │ no refit, 36-step horizon, 82 folds. Best: 'ForecasterFoundation' │ │ (ForecasterFoundation / Chronos-2) = 38.4362, 17.5% ahead of 'ForecasterRecursive' │ │ (46.5843). │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Candidatos explícitos¶
En la práctica, a menudo ya tendrás una lista de preseleccionados (shortlist) en mente: un baseline rápido, un modelo de gradient boosting o una variante con un conjunto de variables más rico. Pasar tuplas (name, config) explícitas mantiene la comparación enfocada y hace que el leaderboard resultante sea fácil de entender de un vistazo.
El diccionario config acepta los mismos overrides que plan(). Cualquier opción omitida recurre a la recomendación determinista derivada del perfil compartido, por lo que los candidatos pueden mantenerse concisos. Por ejemplo, {'forecaster': 'ForecasterDirect'} cambia solo el forecaster manteniendo el estimador, los lags y las variables recomendadas.
⚠️ Coste computacional
Cada candidato se somete a backtest de forma independiente en todos los folds, por lo que el tiempo de ejecución aumenta tanto con el número como con la complejidad de las configuraciones. Comparar cuatro candidatos llevará aproximadamente cuatro veces más tiempo que ejecutar un solo backtest. Empieza con un conjunto pequeño de opciones claramente diferentes, revisa los resultados y refina a partir de ahí. Probar muchas variantes casi idénticas es costoso y rara vez es útil.
# Comparar una lista preseleccionada de configuraciones explícitas
# ==============================================================================
candidates = [
(
"ridge_baseline",
{
"forecaster": "ForecasterRecursive",
"estimator" : "Ridge",
"lags" : 24,
}
),
(
"lgbm_daily_lags",
{
"forecaster": "ForecasterRecursive",
"estimator" : "LGBMRegressor",
"lags" : 24,
}
),
(
"refined_plan",
{
"forecaster" : plan_refined.forecaster,
"estimator" : plan_refined.estimator,
"lags" : plan_refined.forecaster_kwargs.get("lags"),
"window_features" : plan_refined.forecaster_kwargs.get("window_features"),
}
),
(
"lgbm_direct",
{
"forecaster": "ForecasterDirect",
"estimator" : "LGBMRegressor",
"lags" : 24,
}
),
(
"foundation_model",
{
"forecaster": "ForecasterFoundation"
}
),
]
results_compare = assistant.compare(
data = data,
target = 'users',
date_column = 'date_time',
cv = cv,
candidates = candidates, # Candidatos específicos a comparar
metric = ['mean_absolute_error', 'mean_absolute_scaled_error'],
profile = profile
)
Comparing forecasters: 0%| | 0/5 [00:00<?, ?it/s]
Loading weights: 0%| | 0/92 [00:00<?, ?it/s]
El candidato refined_plan reutiliza el forecaster, estimador, lags y window features del plan producido por refine_plan(). Esta es la forma recomendada de validar un plan refinado: el leaderboard muestra si el conocimiento del dominio adicional realmente mejora las métricas en comparación con los baselines deterministas.
Cuando se solicitan varias métricas, todas ellas se muestran como columnas pero solo la primera de ellas guía el ranking.
# Tabla de clasificación, ordenada por la primera métrica solicitada
# ==============================================================================
results_compare.results
| rank | name | forecaster | estimator | mean_absolute_error | mean_absolute_scaled_error | |
|---|---|---|---|---|---|---|
| 0 | 1 | foundation_model | ForecasterFoundation | Chronos-2 | 38.436156 | 0.597467 |
| 1 | 2 | lgbm_direct | ForecasterDirect | LGBMRegressor | 50.380925 | 0.821734 |
| 2 | 3 | refined_plan | ForecasterRecursive | LGBMRegressor | 52.081199 | 0.849416 |
| 3 | 4 | lgbm_daily_lags | ForecasterRecursive | LGBMRegressor | 54.953374 | 0.896312 |
| 4 | 5 | ridge_baseline | ForecasterRecursive | Ridge | 93.145620 | 1.519244 |
Inspeccionar candidatos individuales¶
Como cada candidato es un BacktestResult completo, los detalles de cualquier configuración individual permanecen disponibles, incluyendo sus métricas, sus predicciones y el script independiente que las generó.
# Inspeccionar un candidato específico
# ==============================================================================
candidate = results_compare.candidates['foundation_model']
display(candidate.metrics)
display(candidate.predictions.head())
candidate.show_code()
| mean_absolute_error | mean_absolute_scaled_error | |
|---|---|---|
| 0 | 38.436156 | 0.597467 |
| level | fold | pred | |
|---|---|---|---|
| 2012-09-01 00:00:00 | users | 0 | 148.059479 |
| 2012-09-01 01:00:00 | users | 0 | 102.715004 |
| 2012-09-01 02:00:00 | users | 0 | 66.084412 |
| 2012-09-01 03:00:00 | users | 0 | 40.878601 |
| 2012-09-01 04:00:00 | users | 0 | 29.577911 |
Reutilizar la configuración ganadora¶
El resultado más útil de una comparación a menudo no es el leaderboard, sino best_candidate. Es un BacktestResult completo que lleva tanto el profile como el plan ganadores, por lo que puede retroalimentarse en el flujo de trabajo paso a paso sin tener que reconstruir manualmente la configuración.
# Configuración ganadora
# ==============================================================================
print(f"Mejor candidato: {results_compare.best_name}")
results_compare.best_candidate.plan
Mejor candidato: foundation_model
Forecast Plan ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ foundation │ ├────────────────┼──────────────────────┤ │ Forecaster │ ForecasterFoundation │ ├────────────────┼──────────────────────┤ │ Estimator │ Chronos-2 │ ├────────────────┼──────────────────────┤ │ Steps │ 36 │ ├────────────────┼──────────────────────┤ │ Frequency │ h │ ├────────────────┼──────────────────────┤ │ Use exog │ True │ ├────────────────┼──────────────────────┤ │ Interval │ None │ ├────────────────┼──────────────────────┤ │ Primary metric │ mean_absolute_error │ ├────────────────┼──────────────────────┤ │ Preprocessing │ 1 step │ └────────────────┴──────────────────────┘ Preprocessing Steps ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Step ┃ Reason ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ handle_categorical_exog │ Categorical exogenous variables detected: ['weather']. │ │ │ Chronos-2 consumes categorical covariates natively, so no │ │ │ encoding is needed. │ └─────────────────────────┴──────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── Plan Explanation ───────────────────────────────────╮ │ │ │ Plan: ForecasterFoundation + Chronos-2. No lag or window features: the foundation │ │ model forecasts directly from the raw context window. Exogenous variables included. │ │ MAE is interpretable, robust to outliers, and works at any scale. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
# Producir el pronóstico final con la configuración ganadora
# ==============================================================================
results_pred_best = assistant.forecast(
data = data,
target = 'users',
date_column = 'date_time',
steps = 36,
interval = [0.1, 0.9],
test_size = None, # Modo de predicción
exog = exog, # Valores futuros de las variables exógenas
profile = results_compare.profile, # Perfil compartido
plan = results_compare.best_candidate.plan # Plan ganador
)
results_pred_best.show_code()
╭─────────────────────────────── IgnoredArgumentWarning ───────────────────────────────╮ │ A pre-built `plan` was provided, so the following argument(s) are ignored: │ │ ['interval']. To change these, refine the plan with `refine_plan()` before calling. │ │ │ │ Category : skforecast.exceptions.IgnoredArgumentWarning │ │ Location : │ │ /home/ubuntu/miniconda3/envs/skforecast_24_py13/lib/python3.13/site-packages/skforec │ │ ast_ai/_utils.py:402 │ │ Suppress : warnings.simplefilter('ignore', category=IgnoredArgumentWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
Loading weights: 0%| | 0/92 [00:00<?, ?it/s]
Modo de explicación (comparación)¶
Al igual que cualquier otro resultado, un ComparisonResult puede pasarse a ask() para explicar por qué el ranking tiene el aspecto que tiene. Sin embargo, el LLM no puede cambiar el resultado: todas las métricas y clasificaciones se calculan de manera determinista antes de que vea el resultado.
# Pedir al asistente que interprete la comparación
# ==============================================================================
answer = assistant.ask(
prompt = (
"Explica los resultados de la comparación. ¿Es significativo el margen entre "
"los mejores candidatos, o son prácticamente equivalentes?"
),
result = results_compare
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮ │ │ │ El modelo ganador, foundation_model (ForecasterFoundation con Chronos-2), obtuvo un │ │ MAE de 38.44, claramente por delante del segundo clasificado, lgbm_direct │ │ (ForecasterDirect con LGBMRegressor), con un MAE de 50.38. La diferencia no es │ │ marginal: el contexto determinista la describe como un margen del 23.7%, lo que │ │ indica una separación sustancial entre el primero y el resto. │ │ │ │ Resultados por candidato │ │ │ │ • foundation_model (ForecasterFoundation / Chronos-2): MAE 38.44, MASE 0.597 │ │ • lgbm_direct (ForecasterDirect / LGBMRegressor): MAE 50.38, MASE 0.821 │ │ • refined_plan (ForecasterRecursive / LGBMRegressor): MAE 52.08, MASE 0.849 │ │ • lgbm_daily_lags (ForecasterRecursive / LGBMRegressor): MAE 54.95, MASE 0.896 │ │ • ridge_baseline (ForecasterRecursive / Ridge): MAE 93.15, MASE 1.519 │ │ │ │ Interpretacion del margen entre candidatos │ │ │ │ Entre el 1er y 2do puesto: el margen es notable. Un MAE de 38.44 frente a 50.38 │ │ representa una diferencia visible en terminos absolutos de prediccion de usuarios. │ │ No son equivalentes. │ │ │ │ Entre el 2do, 3er y 4to puesto (los tres modelos LGBM): los valores de MAE son │ │ 50.38, 52.08 y 54.95 respectivamente. Estos tres candidatos estan bastante proximos │ │ entre si. Dependiendo del contexto operativo, podrian considerarse practicamente │ │ equivalentes, aunque lgbm_direct mantiene una ventaja consistente frente a las dos │ │ variantes recursivas. │ │ │ │ El 5to puesto (Ridge): se separa claramente de todos los demas, con un MAE de 93.15 │ │ y un MASE de 1.519. Un MASE superior a 1 indica que Ridge no supera al modelo naive │ │ de referencia, lo que lo descarta como opcion competitiva. │ │ │ │ Conclusion sobre significancia │ │ │ │ El modelo foundation_model se distingue de forma clara del resto. Los tres modelos │ │ LGBM forman un grupo intermedio cohesionado. Ridge queda fuera de competencia al no │ │ superar el baseline naive. La evaluacion fue identica para todos: 82 pliegues, │ │ horizonte de 36 pasos y ventana de entrenamiento fija, lo que garantiza que la │ │ comparacion es justa y los resultados son fiables. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
El objeto ComparisonResult¶
El método compare() devuelve un ComparisonResult, que agrupa la configuración compartida, el leaderboard clasificado y los backtests individuales en un solo objeto.
| Atributo | Tipo | Descripción |
|---|---|---|
results |
DataFrame | Leaderboard clasificado, una fila por candidato, ordenado del mejor al peor. Columnas: rank, name, forecaster, estimator, las columnas de métricas y error cuando al menos un candidato falló. |
candidates |
dict | Mapeo del nombre del candidato a su objeto BacktestResult completo. |
failures |
dict | Mapeo del nombre del candidato a un CandidateFailure describiendo por qué falló. Vacío cuando todos los candidatos tienen éxito. |
ranking_metric |
str | Nombre de la métrica utilizada para ordenar results. |
cv_config |
dict | Parámetros resueltos del TimeSeriesFold más los n_folds resultantes, aplicados idénticamente a cada candidato. |
profile |
ForecastingProfile |
El perfil de datos compartido detrás de cada candidato. |
explanation |
str | Resumen determinista y legible por humanos de la comparación. |
best_name |
str | Nombre del candidato mejor clasificado (top-ranked). |
best_candidate |
BacktestResult |
El candidato mejor clasificado como un BacktestResult completo. |
# Objeto de resultados completo
# ==============================================================================
results_compare
╭──────────────────────────────── Comparison Explanation ────────────────────────────────╮ │ │ │ Compared 5 configurations, ranked ascending by mean_absolute_error. Shared │ │ cross-validation strategy: Initial training up to 2012-08-31 23:59:00, fixed window, │ │ no refit, 36-step horizon, 82 folds. Best: 'foundation_model' (ForecasterFoundation │ │ / Chronos-2) = 38.4362, 23.7% ahead of 'lgbm_direct' (50.3809). │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ Comparison Results ┏━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓ ┃ Index ┃ rank ┃ name ┃ forecaster ┃ estimator ┃ mean_absol… ┃ mean_absolu… ┃ ┡━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩ │ 0 │ 1 │ foundation_… │ ForecasterF… │ Chronos-2 │ 38.4362 │ 0.5975 │ ├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤ │ 1 │ 2 │ lgbm_direct │ ForecasterD… │ LGBMRegress… │ 50.3809 │ 0.8217 │ ├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤ │ 2 │ 3 │ refined_plan │ ForecasterR… │ LGBMRegress… │ 52.0812 │ 0.8494 │ ├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤ │ 3 │ 4 │ lgbm_daily_… │ ForecasterR… │ LGBMRegress… │ 54.9534 │ 0.8963 │ ├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤ │ 4 │ 5 │ ridge_basel… │ ForecasterR… │ Ridge │ 93.1456 │ 1.5192 │ └───────┴──────┴──────────────┴──────────────┴──────────────┴─────────────┴──────────────┘
Código reproducible¶
Cada flujo de trabajo de pronóstico (forecast) o backtest expone el script independiente de skforecast utilizado para producir sus resultados. Este script es estrictamente determinista, asegurando salidas idénticas para un conjunto dado de entradas y configuraciones. Puedes acceder al código en cualquier momento a través del método show_code().
Q&A en formato libre¶
El método ask() no se limita a interpretar objetos del flujo de trabajo. Sin ningún profile, plan o resultado adjunto, funciona como un asistente general de conocimiento de forecasting, útil para aclarar la metodología, elegir entre enfoques o comprender los compromisos (trade-offs) entre métricas.
# Hacer una pregunta general de forecasting (no se requieren datos ni resultados)
# ==============================================================================
answer = assistant.ask(
prompt = (
"Para una demanda horaria con fuerte estacionalidad diaria y semanal, ¿cuándo debería "
"preferir una estrategia de forecasting directo frente a una recursiva?"
)
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮ │ │ │ La estrategia directa es preferible cuando los patrones predictivos cambian │ │ significativamente a lo largo del horizonte de prediccion. En demanda horaria con │ │ estacionalidad diaria y semanal, esto ocurre tipicamente cuando predecir la hora │ │ pico del dia siguiente requiere relaciones distintas a las necesarias para predecir │ │ la hora inmediata siguiente. │ │ │ │ Cuando preferir ForecasterDirect │ │ │ │ • El horizonte es largo en relacion a la frecuencia de los datos. Por ejemplo, │ │ predecir las proximas 24 o 168 horas (una semana completa) implica que los │ │ errores se acumularian paso a paso en una estrategia recursiva, degradando la │ │ calidad al final del horizonte. │ │ • Los patrones por paso son distintos. En demanda horaria, la relacion entre │ │ variables de entrada y la demanda a las 3:00 AM puede ser muy diferente a la │ │ relacion para las 6:00 PM. ForecasterDirect entrena un modelo independiente por │ │ cada paso, capturando esas diferencias. │ │ • La propagacion de errores es un problema critico. En la estrategia recursiva, las │ │ predicciones de pasos anteriores se usan como input para los siguientes. Si los │ │ primeros pasos tienen error, ese error se arrastra y amplifica en el horizonte. │ │ │ │ Cuando preferir ForecasterRecursive │ │ │ │ • El horizonte es corto (pocas horas hacia adelante), donde la propagacion de │ │ errores tiene poco tiempo para acumularse. │ │ • Los recursos computacionales son limitados. ForecasterRecursive entrena un solo │ │ modelo; ForecasterDirect entrena N modelos, uno por cada paso del horizonte, lo │ │ que puede ser costoso para horizontes de 24 o 168 pasos. │ │ • El dataset es relativamente pequeno. Con pocos datos, entrenar N modelos │ │ independientes puede llevar a sobreajuste en cada modelo individual. │ │ │ │ Consideracion practica para series horarias con estacionalidad multiple │ │ │ │ • Los lags mas relevantes para capturar estacionalidad diaria (lag 24) y semanal │ │ (lag 168) son los mismos para ambas estrategias; la eleccion de estrategia no │ │ afecta que lags se puedan incluir. │ │ • ForecasterDirect admite el parametro n_jobs para paralelizar el entrenamiento de │ │ los N modelos, lo que mitiga el costo computacional. │ │ • Ambas estrategias admiten RollingFeatures (medias moviles, desviaciones estandar) │ │ y variables exogenas, por lo que las capacidades de ingenieria de caracteristicas │ │ son equivalentes. │ │ • La forma mas rigurosa de decidir es comparar ambos forecasters mediante │ │ backtesting_forecaster con TimeSeriesFold, usando la misma metrica de evaluacion, │ │ y elegir el que produzca mejores resultados en el periodo de validacion. │ │ │ │ Resumen de la decision │ │ │ │ • Horizonte largo (24h o mas) con patrones cambiantes por hora del dia: │ │ ForecasterDirect. │ │ • Horizonte corto o recursos limitados: ForecasterRecursive como punto de partida. │ │ • Incertidumbre sobre cual es mejor: evaluar ambos con backtesting_forecaster y │ │ dejar que los datos decidan. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
Resumen¶
Este tutorial cubrió la vía paso a paso de skforecast-ai. Aquí hay un resumen de lo que hace cada etapa y cuándo usarla:
| Paso | Método | Cuándo usar |
|---|---|---|
| 1. Perfilado | profile() |
Siempre: produce el ForecastingProfile requerido por todos los métodos posteriores. |
| 2. Plan | plan() |
Siempre: convierte el perfil en una configuración ejecutable. |
| 3. Refinar plan | refine_plan() |
Opcional: usa cuando desees anular decisiones específicas (determinista) o inyectar conocimiento del dominio (LLM). Evalúa siempre el resultado. |
| 4a. Forecast | forecast() |
Cuando desees predicciones futuras o una evaluación de datos reservados (held-out) en una sola ejecución. |
| 4a. Solo código | forecast_code() |
Cuando desees previsualizar o exportar el script sin ejecutarlo. |
| 4b. Estrategia CV | create_cv() |
Cuando desees que el asistente derive o traduzca un TimeSeriesFold por ti. |
| 4b. Backtest | backtest() |
Cuando desees evaluar el modelo sobre múltiples ventanas históricas. |
| 4b. Solo código | backtest_code() |
Cuando desees previsualizar o exportar el script de backtesting sin ejecutarlo. |
| 4c. Comparar | compare() |
Cuando desees clasificar varias configuraciones bajo una estrategia de validación cruzada idéntica y reutilizar al ganador. |
| En cualquier momento | ask() |
Cuando desees una explicación del LLM de cualquier objeto o resultado intermedio, o un Q&A general de forecasting. |
La ventaja clave de esta vía es que el profile y el plan se construyen una vez y se reutilizan tanto en la rama de forecast como en la de backtest. Esto evita el perfilado redundante y asegura que ambas ramas utilicen la misma configuración de modelado. El mismo perfil también puede pasarse a compare(), para que cada candidato se clasifique usando el mismo perfil de datos.
Para una alternativa más rápida que ejecuta todo el pipeline en una sola llamada, vuelve a visitar la sección de Inicio Rápido en la parte superior de esta guía. Para obtener una descripción completa de la mecánica del backtesting, consulta la guía de usuario de backtesting de skforecast.
Información de sesión¶
import session_info
session_info.show(html=False)
----- chronos 2.3.1 matplotlib 3.10.9 pandas 2.3.3 plotly 6.9.0 session_info v1.0.1 skforecast 0.24.0 skforecast_ai 0.2.0 ----- IPython 9.16.1 jupyter_client 8.9.1 jupyter_core 5.9.1 ----- Python 3.13.15 | packaged by conda-forge | (main, Aug 10 2026, 13:05:01) [GCC 14.4.0] Linux-7.0.0-1010-aws-x86_64-with-glibc2.43 ----- Session information updated at 2026-08-24 09:46
Citación¶
Cómo citar este documento
Si utilizas este documento o alguna parte del mismo, te agradeceríamos que citaras la fuente. ¡Muchas gracias!
Forecasting agéntico con skforecast-AI 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/py80-forecasting-agentico-skforecast-ai.html
Cómo citar skforecast-ai
Si utilizas skforecast-ai en una publicación, te agradeceríamos que citaras el software publicado.
Zenodo:
Amat Rodrigo, Joaquin, & Escobar Ortiz, Javier. (2024). skforecast-ai (v0.2.0). Zenodo. https://doi.org/10.5281/zenodo.21338159
APA:
Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast-ai (Versión 0.2.0) [Software de computadora]. https://doi.org/10.5281/zenodo.21338159
BibTeX:
@software{skforecast-ai, author = {Amat Rodrigo, Joaquin and Escobar Ortiz, Javier}, title = {skforecast-ai}, version = {0.2.0}, month = {09}, year = {2026}, license = {BSD-3-Clause}, url = {https://ai.skforecast.org/}, doi = {10.5281/zenodo.21338159} }
¿Te ha gustado el artículo? Tu apoyo es importante
Tu aportación me ayudará a seguir generando contenido educativo gratuito. ¡Muchas gracias! 😊
Este trabajo de Joaquín Amat Rodrigo y Javier Escobar Ortiz está bajo una licencia Attribution-NonCommercial-ShareAlike 4.0 International.
Permitido:
-
Compartir: copiar y redistribuir el material en cualquier medio o formato.
-
Adaptar: remezclar, transformar y construir a partir del material.
Bajo los siguientes términos:
-
Atribución: Usted debe dar el crédito apropiado, proveer un enlace a la licencia, e indicar si se realizaron cambios. Puede hacerlo de cualquier manera razonable, pero no de una manera que sugiera que el licenciante lo respalda a usted o a su uso.
-
No Comercial: Usted no puede usar el material con propósitos comerciales.
-
Compartir Igual: Si usted remezcla, transforma o construye a partir del material, debe distribuir sus contribuciones bajo la misma licencia que el original.
