Más sobre forecasting en: cienciadedatos.net


¿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 skforecast que 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, como refine_plan() o create_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.

Vía rápida: una sola llamada

El perfilado, la planificación y la ejecución ocurren internamente.

datos
Pronóstico
forecast()
o forecast_code()
predicciones + código
Backtesting (validación)
create_cv()
Modo Determinista, Agéntico
o pasa un objeto TimeSeriesFold de skforecast
backtest()
o backtest_code()
métricas + predicciones + código
Vía paso a paso: control total

Construye un profile y un plan a partir de tus datos, luego divídelo en forecasting y backtesting.

datos
profile()
plan()
refine_plan(), opcional (Modo Determinista o Agéntico)
Pronóstico
forecast()
o forecast_code()
predicciones + código
Backtesting (validación)
create_cv()
Modo Determinista, Agéntico
o pasa un objeto TimeSeriesFold de skforecast
backtest()
o backtest_code()
métricas + predicciones + código
Selección de modelo: ¿qué forecaster deberías usar?

compare() responde a la pregunta con la que empieza todo proyecto de forecasting: Entre varios modelos razonables, ¿cuál rinde mejor realmente con mis datos? Cada candidato se evalúa usando los mismos datos y estrategia de validación cruzada. Por lo tanto, las diferencias que ves provienen de los modelos, no de la configuración.

Candidatos
Un puñado de configuraciones que vale la pena probar: diferentes forecasters, estimadores, rezagos o window features. Proporciona los tuyos o deja que el perfil de datos los proponga.
compare()
Ejecuta un backtest completo para cada candidato bajo condiciones idénticas y los califica con las métricas que te importan.
Una respuesta clasificada
Una tabla de clasificación (leaderboard) ordenada del mejor al peor, el código reproducible detrás de cada fila y el ganador listo para ser utilizado para forecasting o ajustes adicionales.

El ranking es una simple ordenación de la columna de la métrica: completamente determinista y auditable. El LLM no participa en la elección del ganador.

Razonamiento LLM: disponible en cualquier momento, en cualquier flujo de trabajo
Llama a ask() antes, durante o después de cualquier vía. Puede recibir un profile, un plan, un forecast_result, un backtest_result, o nada en absoluto (Q&A puro).

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 como lags, estimator, estimator_kwargs, forecaster, steps, interval, o window_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 (prompt proporcionado): describe el conocimiento de tu dominio en lenguaje natural. El LLM interpreta este contexto y sugiere lags y window_features apropiados. Su razonamiento se adjunta a plan.explanation y los campos modificados se registran en plan.llm_refined_fields para 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_size establecido): 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óximos steps puntos 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 de exog.

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 │
└─────────────────────┴──────────┴─────────────┴─────────────┘
Generated code
import pandas as pd                                                                       
from lightgbm import LGBMRegressor                                                        
from skforecast.preprocessing import RollingFeatures, CalendarFeatures                    
from skforecast.recursive import ForecasterRecursive                                      
                                                                                          
# Load data                                                                               
data = pd.read_csv('data.csv')                                                            
                                                                                          
# Load future exogenous variables covering the forecast horizon                           
exog_future = pd.read_csv('exog_future.csv')                                              
                                                                                          
data['date_time'] = pd.to_datetime(data['date_time'])                                     
data = data.set_index('date_time')                                                        
data = data.asfreq('h')                                                                   
data = data.sort_index()                                                                  
                                                                                          
exog_future['date_time'] = pd.to_datetime(exog_future['date_time'])                       
exog_future = exog_future.set_index('date_time')                                          
exog_future = exog_future.asfreq('h')                                                     
exog_future = exog_future.sort_index()                                                    
                                                                                          
exog_features = ['holiday', 'weather', 'temp']                                            
                                                                                          
window_features = RollingFeatures(                                                        
    stats        = ['mean', 'std', 'mean', 'std'],                                        
    window_sizes = [24, 24, 168, 168],                                                    
)                                                                                         
                                                                                          
calendar_features = CalendarFeatures(                                                     
    features = ['hour', 'day_of_week', 'weekend', 'month'],                               
    encoding = None,                                                                      
)                                                                                         
                                                                                          
# Create forecaster                                                                       
forecaster = ForecasterRecursive(                                                         
    estimator            = LGBMRegressor(random_state=123, verbose=-1),                   
    lags                 = [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, 25, 47, 48, 49, 167, 168,     
169],                                                                                     
    window_features      = window_features,                                               
    calendar_features    = calendar_features,                                             
    categorical_features = 'auto',                                                        
    dropna_from_series   = False,                                                         
)                                                                                         
                                                                                          
# Fit                                                                                     
forecaster.fit(                                                                           
    y                         = data['users'],                                            
    exog                      = data[exog_features],                                      
    store_in_sample_residuals = True,                                                     
)                                                                                         
                                                                                          
# Predict intervals                                                                       
steps = 36                                                                                
predictions = forecaster.predict_interval(                                                
    steps    = steps,                                                                     
    exog     = exog_future[exog_features],                                                
    method   = 'bootstrapping',                                                           
    interval = [0.1, 0.9],                                                                
)                                                                                         
print(predictions)                                                                        

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()
Generated code
import pandas as pd                                                                       
from sklearn.metrics import mean_absolute_error, mean_squared_error,                      
mean_absolute_percentage_error                                                            
from skforecast.metrics import mean_absolute_scaled_error                                 
from lightgbm import LGBMRegressor                                                        
from skforecast.preprocessing import RollingFeatures, CalendarFeatures                    
from skforecast.recursive import ForecasterRecursive                                      
                                                                                          
# Load data                                                                               
data = pd.read_csv('data.csv')                                                            
                                                                                          
data['date_time'] = pd.to_datetime(data['date_time'])                                     
data = data.set_index('date_time')                                                        
data = data.asfreq('h')                                                                   
data = data.sort_index()                                                                  
                                                                                          
# Train/test split                                                                        
end_train = '2012-12-30 11:00:00'  # last training date, adjust to change the split point 
data_train = data.loc[:end_train]                                                         
data_test  = data.loc[data.index > end_train]                                             
exog_features = ['holiday', 'weather', 'temp']                                            
                                                                                          
print(                                                                                    
    f"Train dates : {data_train.index.min()} --- {data_train.index.max()}                 
(n={len(data_train)})"                                                                    
)                                                                                         
print(                                                                                    
    f"Test dates  : {data_test.index.min()} --- {data_test.index.max()}                   
(n={len(data_test)})"                                                                     
)                                                                                         
                                                                                          
window_features = RollingFeatures(                                                        
    stats        = ['mean', 'std', 'mean', 'std'],                                        
    window_sizes = [24, 24, 168, 168],                                                    
)                                                                                         
                                                                                          
calendar_features = CalendarFeatures(                                                     
    features = ['hour', 'day_of_week', 'weekend', 'month'],                               
    encoding = None,                                                                      
)                                                                                         
                                                                                          
# Create forecaster                                                                       
forecaster = ForecasterRecursive(                                                         
    estimator            = LGBMRegressor(random_state=123, verbose=-1),                   
    lags                 = [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, 25, 47, 48, 49, 167, 168,     
169],                                                                                     
    window_features      = window_features,                                               
    calendar_features    = calendar_features,                                             
    categorical_features = 'auto',                                                        
    dropna_from_series   = False,                                                         
)                                                                                         
                                                                                          
# Fit                                                                                     
forecaster.fit(                                                                           
    y                         = data_train['users'],                                      
    exog                      = data_train[exog_features],                                
    store_in_sample_residuals = True,                                                     
)                                                                                         
                                                                                          
# Predict intervals                                                                       
steps = 36                                                                                
predictions = forecaster.predict_interval(                                                
    steps    = steps,                                                                     
    exog     = data_test[exog_features],                                                  
    method   = 'bootstrapping',                                                           
    interval = [0.1, 0.9],                                                                
)                                                                                         
print(predictions)                                                                        
                                                                                          
# Evaluate on test set                                                                    
actual = data_test['users'].iloc[:steps]                                                  
mae = mean_absolute_error(actual, predictions['pred'])                                    
mse = mean_squared_error(actual, predictions['pred'])                                     
mase = mean_absolute_scaled_error(                                                        
    y_true  = actual,                                                                     
    y_pred  = predictions['pred'],                                                        
    y_train = data_train['users'],                                                        
)                                                                                         
mape = mean_absolute_percentage_error(actual, predictions['pred'])                        
                                                                                          
print(f"MAE  : {mae:.4f}")                                                                
print(f"MSE  : {mse:.4f}")                                                                
print(f"MASE : {mase:.4f}")                                                               
print(f"MAPE : {mape:.4f}")                                                               
                                                                                          
# NOTE: This script uses a train/test split for demonstration purposes.                   
# For production forecasting, retrain with all available data                             
# and provide future exogenous values covering the forecast horizon.                      

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:

  1. Instanciación explícita (recomendada): construye manualmente un TimeSeriesFold y pásalo directamente a backtest(). Úsalo cuando ya conozcas tus restricciones operativas exactas.

  2. create_cv() determinista: permite que el asistente derive un TimeSeriesFold razonable del perfil y del plan usando valores por defecto basados en reglas. Puedes sobrescribir parámetros individuales explícitamente.

  3. 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 de TimeSeriesFold completamente 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

📖 API Reference    📝 User Guide

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

📖 API Reference    📝 User Guide

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

📖 API Reference    📝 User Guide

# 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()
Generated code
import pandas as pd                                                                       
from lightgbm import LGBMRegressor                                                        
from skforecast.preprocessing import RollingFeatures, CalendarFeatures                    
from skforecast.recursive import ForecasterRecursive                                      
from skforecast.model_selection import TimeSeriesFold, backtesting_forecaster             
                                                                                          
# Load data                                                                               
data = pd.read_csv('data.csv')                                                            
                                                                                          
data['date_time'] = pd.to_datetime(data['date_time'])                                     
data = data.set_index('date_time')                                                        
data = data.asfreq('h')                                                                   
data = data.sort_index()                                                                  
                                                                                          
window_features = RollingFeatures(                                                        
    stats        = ['mean', 'std', 'mean', 'std'],                                        
    window_sizes = [24, 24, 168, 168],                                                    
)                                                                                         
                                                                                          
calendar_features = CalendarFeatures(                                                     
    features = ['hour', 'day_of_week', 'weekend', 'month'],                               
    encoding = None,                                                                      
)                                                                                         
                                                                                          
# Create forecaster                                                                       
forecaster = ForecasterRecursive(                                                         
    estimator            = LGBMRegressor(random_state=123, verbose=-1),                   
    lags                 = [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, 25, 47, 48, 49, 167, 168,     
169],                                                                                     
    window_features      = window_features,                                               
    calendar_features    = calendar_features,                                             
    categorical_features = 'auto',                                                        
    dropna_from_series   = False,                                                         
)                                                                                         
                                                                                          
# Time series cross-validation configuration                                              
cv = TimeSeriesFold(                                                                      
    steps              = 36,                                                              
    initial_train_size = '2012-08-31 23:59:00',                                           
    refit              = False,                                                           
)                                                                                         
                                                                                          
# Run backtesting                                                                         
exog_features = ['holiday', 'weather', 'temp']                                            
                                                                                          
metrics, predictions = backtesting_forecaster(                                            
    forecaster        = forecaster,                                                       
    y                 = data['users'],                                                    
    exog              = data[exog_features],                                              
    cv                = cv,                                                               
    metric            = ['mean_absolute_error', 'mean_squared_error',                     
'mean_absolute_scaled_error', 'mean_absolute_percentage_error'],                          
    interval          = [0.1, 0.9],                                                       
    n_jobs            = 'auto',                                                           
    verbose           = False,                                                            
    show_progress     = True,                                                             
    suppress_warnings = True,                                                             
)                                                                                         
                                                                                          
print(metrics)                                                                            
print(predictions.head())                                                                 

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.              
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯
Generated code
import pandas as pd                                                                       
from lightgbm import LGBMRegressor                                                        
from skforecast.preprocessing import RollingFeatures, CalendarFeatures                    
from skforecast.recursive import ForecasterRecursive                                      
from skforecast.model_selection import TimeSeriesFold, backtesting_forecaster             
                                                                                          
# Load data                                                                               
data = pd.read_csv('data.csv')                                                            
                                                                                          
data['date_time'] = pd.to_datetime(data['date_time'])                                     
data = data.set_index('date_time')                                                        
data = data.asfreq('h')                                                                   
data = data.sort_index()                                                                  
                                                                                          
window_features = RollingFeatures(                                                        
    stats        = ['mean', 'std', 'mean', 'std'],                                        
    window_sizes = [24, 24, 168, 168],                                                    
)                                                                                         
                                                                                          
calendar_features = CalendarFeatures(                                                     
    features = ['hour', 'day_of_week', 'weekend', 'month'],                               
    encoding = None,                                                                      
)                                                                                         
                                                                                          
# Create forecaster                                                                       
forecaster = ForecasterRecursive(                                                         
    estimator            = LGBMRegressor(random_state=123, verbose=-1),                   
    lags                 = [1, 2, 3, 4, 5, 6, 7, 8, 23, 24, 25, 47, 48, 49, 167, 168,     
169],                                                                                     
    window_features      = window_features,                                               
    calendar_features    = calendar_features,                                             
    categorical_features = 'auto',                                                        
    dropna_from_series   = False,                                                         
)                                                                                         
                                                                                          
# Time series cross-validation configuration                                              
cv = TimeSeriesFold(                                                                      
    steps              = 36,                                                              
    initial_train_size = '2012-08-31 23:59:00',                                           
    refit              = False,                                                           
)                                                                                         
                                                                                          
# Run backtesting                                                                         
exog_features = ['holiday', 'weather', 'temp']                                            
                                                                                          
metrics, predictions = backtesting_forecaster(                                            
    forecaster        = forecaster,                                                       
    y                 = data['users'],                                                    
    exog              = data[exog_features],                                              
    cv                = cv,                                                               
    metric            = ['mean_absolute_error', 'mean_squared_error',                     
'mean_absolute_scaled_error', 'mean_absolute_percentage_error'],                          
    interval          = [0.1, 0.9],                                                       
    n_jobs            = 'auto',                                                           
    verbose           = False,                                                            
    show_progress     = True,                                                             
    suppress_warnings = True,                                                             
)                                                                                         
                                                                                          
print(metrics)                                                                            
print(predictions.head())                                                                 

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 de profile.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), donde name etiqueta la fila en el leaderboard y config contiene las mismas claves de override entendidas por plan(): '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
Generated code
import pandas as pd                                                                       
from skforecast.foundation import FoundationModel, ForecasterFoundation                   
from skforecast.model_selection import TimeSeriesFold, backtesting_foundation             
                                                                                          
# Load data                                                                               
data = pd.read_csv('data.csv')                                                            
                                                                                          
data['date_time'] = pd.to_datetime(data['date_time'])                                     
data = data.set_index('date_time')                                                        
data = data.asfreq('h')                                                                   
data = data.sort_index()                                                                  
                                                                                          
exog_features = ['holiday', 'weather', 'temp']                                            
                                                                                          
# Create foundation model (chronos-2-small)                                               
estimator = FoundationModel(                                                              
    model_id       = 'autogluon/chronos-2-small',                                         
    context_length = 8192,                                                                
)                                                                                         
                                                                                          
# Create forecaster                                                                       
forecaster = ForecasterFoundation(estimator=estimator)                                    
                                                                                          
# Time series cross-validation configuration                                              
cv = TimeSeriesFold(                                                                      
    steps              = 36,                                                              
    initial_train_size = '2012-08-31 23:59:00',                                           
    refit              = False,                                                           
)                                                                                         
                                                                                          
# Run backtesting                                                                         
metrics, predictions = backtesting_foundation(                                            
    forecaster        = forecaster,                                                       
    series            = data['users'],                                                    
    cv                = cv,                                                               
    metric            = ['mean_absolute_error', 'mean_absolute_scaled_error'],            
    exog              = data[exog_features],                                              
    verbose           = False,                                                            
    show_progress     = True,                                                             
    suppress_warnings = True,                                                             
)                                                                                         
                                                                                          
print(metrics)                                                                            
print(predictions.head())                                                                 

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]
Generated code
import pandas as pd                                                                       
from skforecast.foundation import FoundationModel, ForecasterFoundation                   
                                                                                          
# Load data                                                                               
data = pd.read_csv('data.csv')                                                            
                                                                                          
# Load future exogenous variables covering the forecast horizon                           
exog_future = pd.read_csv('exog_future.csv')                                              
                                                                                          
data['date_time'] = pd.to_datetime(data['date_time'])                                     
data = data.set_index('date_time')                                                        
data = data.asfreq('h')                                                                   
data = data.sort_index()                                                                  
                                                                                          
exog_future['date_time'] = pd.to_datetime(exog_future['date_time'])                       
exog_future = exog_future.set_index('date_time')                                          
exog_future = exog_future.asfreq('h')                                                     
exog_future = exog_future.sort_index()                                                    
                                                                                          
series = data['users']                                                                    
exog = data[['holiday', 'weather', 'temp']]                                               
                                                                                          
# Create foundation model (chronos-2-small)                                               
estimator = FoundationModel(                                                              
    model_id       = 'autogluon/chronos-2-small',                                         
    context_length = 8192,                                                                
)                                                                                         
                                                                                          
# Create forecaster                                                                       
forecaster = ForecasterFoundation(estimator=estimator)                                    
                                                                                          
# Fit (stores context only — no training)                                                 
forecaster.fit(series=series, exog=exog)                                                  
                                                                                          
# Predict                                                                                 
steps = 36                                                                                
predictions = forecaster.predict(steps=steps, exog=exog_future)                           
print(predictions)                                                                        

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

Become a GitHub Sponsor Become a GitHub Sponsor

Licencia Creative Commons

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.