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__}")

✏️ 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()

✏️ 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

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()

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

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()

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

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

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()

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())
# 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()

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())
# Objeto de resultados completo
# ==============================================================================
results_pred

Modo de solo código

Usa forecast_code() cuando desees previsualizar o exportar el script reproducible sin ejecutarlo. Esto es útil para revisión de código, auditoría del pipeline generado, o para ejecutar el script en un entorno separado.

# Generar el script reproducible sin ejecutarlo
# ==============================================================================
code_result = assistant.forecast_code(
    data        = data,
    target      = 'users',
    date_column = 'date_time',
    steps       = 36,
    test_size   = 36,
    profile     = profile,
    plan        = plan_refined
)
code_result.show_code()

El objeto ForecastResult

Ambos modos de forecast() devuelven un ForecastResult, un contenedor ligero que agrupa todo lo que el asistente usó y produjo.

Atributo Tipo Descripción
predictions DataFrame Valores pronosticados. Cuando se solicitan intervalos, las columnas de los límites se incluyen junto a las predicciones puntuales.
metrics DataFrame o None Métricas de evaluación (MAE, MSE, MASE, MAPE), una fila por serie. None en modo predicción.
code str El script exacto e independiente de skforecast que produjo el pronóstico, listo para ejecutarse por su cuenta.
profile ForecastingProfile El perfil de datos detrás del pronóstico.
plan ForecastPlan La configuración detallada que se ejecutó.

Backtesting

La rama de backtesting usa los mismos profile y plan que la rama de forecast, pero evalúa el rendimiento histórico del modelo a través de validación cruzada de series temporales (time series cross-validation). La decisión clave es cómo configurar el objeto TimeSeriesFold, que controla exactamente cómo los datos históricos se dividen en ventanas sucesivas de entrenamiento y test.

skforecast-ai proporciona tres formas distintas de definir esta estrategia de validación:

  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

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

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
# Razonamiento del LLM detrás de la configuración del TimeSeriesFold
# ==============================================================================
print(textwrap.fill(cv_llm_explanation, width=88))

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())
# 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()

Modo de solo código

Usa backtest_code() para generar el script de backtesting reproducible sin ejecutarlo.

# Generar el script de backtesting reproducible sin ejecutarlo
# ==============================================================================
code_backtest = assistant.backtest_code(
    data        = data,
    target      = 'users',
    date_column = 'date_time',
    cv          = cv,
    profile     = profile,
    plan        = plan_refined
)
code_backtest.show_code()

El objeto BacktestResult

El método backtest() devuelve un BacktestResult, un contenedor ligero que agrupa todos los artefactos de backtesting.

Atributo Tipo Descripción
predictions DataFrame Predicciones completas out-of-sample del backtest a través de todos los folds.
metrics DataFrame Métricas de backtesting (MAE, MSE, MASE, MAPE), una fila por serie.
cv_config dict Parámetros resueltos de TimeSeriesFold para trazabilidad completa de la estrategia de validación.
code str El script exacto e independiente de skforecast que reproduce el flujo de trabajo de backtesting.
explanation str Resumen legible por humanos de la configuración de backtesting y sus resultados.
profile ForecastingProfile El perfil de datos detrás del backtest.
plan ForecastPlan La configuración detallada que se ejecutó.
# Objeto de resultados completo
# ==============================================================================
results_backtest

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
)
# Tabla de clasificación (leaderboard)
# ==============================================================================
results_compare.results
# Resumen determinista de la comparación
# ==============================================================================
results_compare.show_explanation()

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
)

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

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()

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
# 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()

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()

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

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()

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)

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.