More about forecasting in cienciadedatos.net


What is skforecast-ai?

skforecast-ai is an AI forecasting assistant that pairs a deterministic engine, powered by skforecast, with an LLM reasoning layer. Simply provide a time series, and the assistant automatically profiles the data, selects a model using established best practices, and evaluates its performance. It returns both the final forecast and the runnable skforecast script that produced it.

It is organized around a single core object, the ForecastingAssistant, which consists of two complementary components:

  • Deterministic Engine (Rule-based and Reproducible): Profiles the data, selects a forecaster and estimator, derives lags and preprocessing steps, runs backtesting, and produces the final forecast. Crucially, it outputs the exact standalone skforecast script that generated the results. Given the same inputs and configuration, this workflow is guaranteed to be reproducible.

  • Reasoning Layer (LLM-powered): Accessed primarily via the ask() method, this layer interprets and explains the objects and results you pass to it: data profiles, modeling plans, validation choices, backtesting outputs, and forecasts. The LLM acts strictly as an interpreter; it does not rerun the workflow or silently change modeling recommendations behind the scenes. Agentic features, such as the LLM-guided refine_plan() or create_cv(), are separate, explicit steps where the LLM suggests adjustments that are then implemented transparently in deterministic code.

Why skforecast-ai?

  • 🎯 Deterministic by design: built as a strict rule-based engine to guarantee absolute consistency, same input always means the same output.
  • 🔍 Code you can inspect: the script you see is the code that ran. Inspect it, version it, or run it standalone with plain skforecast.
  • From data to forecast in one call: automatic data profiling, model and estimator selection, lag/feature engineering, and backtest evaluation.
  • 💻 Python or terminal: drive the full pipeline from a few lines of Python or from the command line.
  • 💬 LLM reasoning layer: explains the engine's decisions in plain language, helps you improve the configuration, and lets you ask for advice. This layer is entirely optional; the core forecasting pipeline can run fully offline.
  • 🏗️ Built on skforecast: recursive & direct forecasters, multi-series, statistical, and foundation models (Chronos-2, TimesFM, Moirai, and more).

Quickstart (Python)

From raw data to a validated forecast, and the code behind it, in a few lines:

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)   # forecast for the next 12 steps
print(result.metrics)       # evaluation metrics: MAE, MSE, MASE, MAPE
print(result.code)          # the exact skforecast script that produced this result

That single forecast() call profiled the data, chose a forecaster and estimator, generated a skforecast script, and executed it. result.code is the script that ran.

Quickstart (CLI)

The same pipeline runs from the terminal. Point it at a CSV file or URL:

# End-to-end forecast (profile -> plan -> code -> forecast)
skforecast-ai forecast data.csv --target y --date-column datetime --steps 12

# Just inspect the data
skforecast-ai profile data.csv --target y --date-column datetime

# Generate a standalone, runnable script without executing it
skforecast-ai forecast-code data.csv --target y --date-column datetime --steps 12 --output forecast.py

Two ways to use skforecast-ai

skforecast-ai supports two distinct workflows using the same underlying forecasting engine:

  • The Fast Path: Use this when you want a forecast or backtest result in a single call. The assistant profiles the data, builds the modeling plan, executes the workflow, and returns the results alongside the reproducible skforecast code.

  • The Step-by-Step Path: Use this when you want granular control to inspect or adjust intermediate decisions. You can manually create a profile, build a plan, optionally refine it with the LLM, define a validation strategy, evaluate the model, and then generate the forecast.

A useful mental model is that forecasting and validation are separate branches. Once you have a profile and a plan, you can use forecast() to produce future predictions directly, or backtest() to evaluate the model's performance on historical data. You can also use compare() to evaluate several candidate configurations under the same cross-validation strategy and obtain a ranked leaderboard, so the best configuration is chosen from measured performance rather than intuition.

The ask() method is available in both workflows. It can explain a profile, plan, validation setup, backtest result, comparison result, or answer general forecasting questions, but it will never execute the workflow or modify your parameters without explicit instruction.

Fast path: one call

Profiling, planning and execution happen internally.

data
Forecast
forecast()
or forecast_code()
predictions + code
Backtesting (validation)
create_cv()
Deterministic, Agentic mode
or pass a skforecast TimeSeriesFold object
backtest()
or backtest_code()
metrics + predictions + code
Step-by-step path: full control

Build a profile and a plan from your data, then branch into forecasting and backtesting.

data
profile()
plan()
refine_plan(), optional (Deterministic or Agentic mode)
Forecast
forecast()
or forecast_code()
predictions + code
Backtesting (validation)
create_cv()
Deterministic, Agentic mode
or pass a skforecast TimeSeriesFold object
backtest()
or backtest_code()
metrics + predictions + code
Model selection: which forecaster should you use?

compare() answers the question every forecasting project starts with: Among several reasonable models, which one actually performs best on my data? Every candidate is evaluated using the same data and cross-validation strategy. Therefore, the differences you see come from the models, not from the setup.

Candidates
A handful of configurations worth testing: different forecasters, estimators, lags or window features. Supply your own, or let the data profile propose them.
compare()
Runs a full backtest for each candidate under identical conditions, and scores them with the metrics you care about.
A ranked answer
A leaderboard sorted best to worst, the reproducible code behind every row, and the winner ready to be used for forecasting or further tuning.

The ranking is a plain sort of the metric column: fully deterministic and auditable. The LLM plays no part in choosing the winner.

LLM reasoning: available at any moment, in any workflow
Call ask() before, during or after either path. It can take a profile, a plan, a forecast_result, a backtest_result, or nothing at all (pure Q&A).

The rest of this guide sets up the assistant and the dataset used throughout, then walks through the step-by-step path in detail. For the fast path -- the quickest way to go from raw data to a validated forecast with minimal setup -- see the Quickstart section above; it is ideal when you want rapid results and trust the assistant to make sensible, baseline modeling decisions on your behalf.

Assistant initialization

The first step is to instantiate a ForecastingAssistant, which will be responsible for executing the entire workflow (profiling, planning, backtesting, and forecasting), as well as explaining the outputs and suggesting improvements.

To activate the optional LLM support, users must pass a string in the format 'provider:model_name' (for example, 'openai:gpt-5.5', 'google:gemini-3-flash-preview', 'anthropic:claude-sonnet-5', or 'ollama:qwen3:8b'). For hosted providers, the corresponding API key must be available as an environment variable or passed explicitly when creating the assistant. In this tutorial, we set send_data_to_llm=False. This ensures strict data privacy: the LLM receives only metadata and summary statistics, never the raw time series values.

# Data processing
# ==============================================================================
import os
import textwrap
import pandas as pd
from skforecast.datasets import fetch_dataset

# Plots
# ==============================================================================
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 and 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}Version skforecast_ai: {skforecast_ai.__version__}")
print(f"{color}Version skforecast: {skforecast.__version__}")
print(f"{color}Version chronos-forecasting: {chronos.__version__}")
Version skforecast_ai: 0.2.0
Version skforecast: 0.24.0
Version chronos-forecasting: 2.3.1

✏️ Note

If you do not have access to an LLM assistant, you can still follow the full tutorial using only the deterministic methods. Profiling, planning, backtesting, and forecasting all run without an LLM. Only the ask() explanations and the LLM-guided variants of refine_plan() and create_cv() require a configured LLM; their deterministic counterparts (for example, refine_plan() with explicit overrides and prompt=None) work without one.

# LLM-enabled assistant
# ==============================================================================
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
)

# Using aws bedrock
# ==============================================================================
assistant = ForecastingAssistant(
    llm='bedrock:eu.anthropic.claude-sonnet-4-6',
    base_url="eu-west-1"
)

# Assistant without reasoning layer
# ==============================================================================
# assistant = ForecastingAssistant()

⚠️ Your data stays private

By default, enabling an LLM does not send your time-series data to the model provider. The assistant passes only summary statistics, detected frequency, seasonality flags and the forecaster configuration, never the raw observations. To explicitly allow it, pass send_data_to_llm=True.

Data

The data in this document represent the 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.

# Downloading data
# ==============================================================================
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

✏️ Note

skforecast-ai is ready to preprocess the data, but it is recommended that users apply their own preprocessing steps before using the assistant. This ensures the data is in the desired format and any necessary transformations have been applied before proceeding with the forecasting workflow.

# Interactive plot of time series
# ==============================================================================
fig = go.Figure()
fig.add_trace(
    go.Scatter(x=data['date_time'], y=data['users'], mode='lines', name='Users')
)
fig.update_layout(
    title  = 'Number of users',
    xaxis_title="Time",
    yaxis_title="Users",
    legend_title="Partition:",
    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()

For a deeper walkthrough of the exploratory analysis behind this dataset, see the skforecast example: Forecasting time series with skforecast, XGBoost, LightGBM and CatBoost.

Deep Dive: The Step-by-Step

While the fast path is great for getting a baseline, many data scientists need to control, inspect, and override intermediate decisions. The step-by-step path breaks the process into distinct, observable phases: Profiling, Planning, and Execution (Forecasting or Backtesting).

Profile the data

The profile() method is the first stage of the step-by-step workflow. It inspects the dataset and returns a ForecastingProfile object that contains:

  • Data metadata: detected frequency, index type, series lengths, missing values, and exogenous column roles.

  • Modeling recommendations: the selected forecaster family and estimator, along with alternative candidates and the reasoning behind each choice.

  • Lag structure: PACF-significant lags per series, used as a baseline for the planning stage.

  • Window feature suggestions: rolling statistics configurations appropriate for the detected seasonality.

This is a purely deterministic step: no LLM is involved. The profile object is a prerequisite for both plan() and ask() explain mode.

Attribute Description
data_profile Full dataset metadata: frequency, index type, series lengths, missing values, exog columns
forecaster Recommended skforecast forecaster class name
forecaster_candidates Ordered list of compatible forecaster names
estimator Recommended estimator class name (None for statistical models)
estimator_candidates Ordered list of compatible estimator names
series_pacf Per-series PACF-significant lags (used by plan() to set default lags)
window_features Suggested window feature configurations
calendar_features Recommended calendar feature names based on detected seasonality
explanation Human-readable explanation of why this forecaster and estimator were selected
# Profile the data
# ==============================================================================
profile = assistant.profile(
    data        = data,
    target      = 'users',
    date_column = 'date_time'
)
# Inspect the profile
# ==============================================================================
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.                                                                           
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Once you have a profile, you can pass it to ask() to get an LLM-generated explanation of the modeling decisions. Note that the pre-computed profile is passed directly, so no profiling work is repeated.

# Ask the assistant to explain the profile
# ==============================================================================
answer = assistant.ask(
    prompt  = (
        "Explain why this forecaster and estimator were recommended for my "
        "hourly bike-sharing demand data, and what the exogenous variables add."
    ),
    profile = profile,
    steps   = 36,
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮
                                                                                        
  The forecaster and estimator were chosen based on your data size, frequency, and      
  available features. With 17,544 hourly observations, a single-series ML approach      
  using ForecasterRecursive with LGBMRegressor is the recommended starting point, and   
  the three exogenous variables (holiday, weather, temp) directly enrich each           
  prediction with real-world context.                                                   
                                                                                        
  Why ForecasterRecursive                                                               
                                                                                        
  ForecasterRecursive trains a single model and uses its own predictions as inputs      
  when projecting multiple steps ahead. For a 36-step horizon on hourly data, this is   
  efficient and flexible. It avoids the overhead of training one model per step, and    
  the selected lags and window features are designed to capture the cycling patterns    
  in your series.                                                                       
                                                                                        
  The alternative ForecasterDirect would train 36 separate models, one per forecast     
  step, which may be useful if the relationship between past demand and future demand   
  changes substantially across the horizon, but the recursive approach is the           
  recommended default here.                                                             
                                                                                        
  Why LGBMRegressor                                                                     
                                                                                        
  LightGBM (gradient boosting) is preferred for a dataset of this size. It handles:     
                                                                                        
  A large number of observations efficiently                                         
  Mixed feature types, including the categorical weather variable, natively via      
     skforecast's categorical_features='auto' setting                                   
  Non-linear interactions between lags, calendar signals, and exogenous drivers      
     without manual feature engineering                                                 
                                                                                        
  Alternative estimators listed were XGBRegressor and Ridge, but gradient boosting is   
  the primary recommendation at this data scale.                                        
                                                                                        
  What the Lags and Window Features Capture                                             
                                                                                        
  The lag set includes short-range lags (1, 2, 3) for immediate autocorrelation, daily  
  lags around 24 for the hour-of-day cycle, weekly lags around 168 for the day-of-week  
  cycle, and longer lags (313, 335, 337) that may associate with patterns further back  
  in time. The window features add rolling summaries:                                   
                                                                                        
  Rolling mean and std over 3 hours, capturing very recent local trends              
  Rolling mean over 24 hours, capturing the daily average level                      
  Rolling mean over 168 hours, capturing the weekly average level                    
                                                                                        
  What the Exogenous Variables Add                                                      
                                                                                        
  The three exogenous columns extend what the model can explain beyond pure             
  autocorrelation:                                                                      
                                                                                        
   • holiday: flags days when commuter or leisure cycling patterns shift, which may     
     not be predictable from lags alone                                                 
   • weather: a categorical variable (handled automatically) that may be associated     
     with riders choosing to cycle or stay home                                         
   • temp: a continuous variable that likely co-varies with demand, as temperature is   
     commonly associated with outdoor activity levels                                   
                                                                                        
  Together, these variables give the model information about conditions at the          
  forecast horizon that the target series itself cannot provide, making predictions     
  more context-aware than a lags-only model would be.                                   
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Build the plan

The plan() method converts the coarse modeling decisions in the ForecastingProfile into a fully-specified, executable configuration. It determines:

  • Lags: derived from the PACF-significant lags detected in the profile. You can override these explicitly.
  • Window features: rolling statistics configurations appropriate for the detected seasonality.
  • Preprocessing steps: ordered list of transformations (e.g., differencing, scaling, NaN handling).
  • Prediction interval method: 'bootstrapping', 'conformal', or 'native' (selected based on the estimator).
  • Metrics: the primary and secondary evaluation metrics.

Like profile(), this is a deterministic step. The resulting ForecastPlan object is the complete blueprint that forecast() and backtest() execute.

Attribute Description
forecaster Forecaster class name
estimator Estimator class name
forecaster_kwargs All constructor kwargs for the forecaster, including lags and window_features
estimator_kwargs Constructor kwargs for the estimator
steps Forecast horizon
interval Prediction interval quantiles, e.g. [0.1, 0.9]
interval_method Method used to produce the interval (bootstrapping, conformal, or native)
use_exog Whether exogenous variables are included
preprocessing_steps Ordered list of preprocessing actions with code snippets
explanation Human-readable explanation of plan decisions
# Build a plan from the profile
# ==============================================================================
plan = assistant.plan(
    profile  = profile,
    steps    = 36,
    interval = [0.1, 0.9]  # 80% prediction interval
)
# Inspect the 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.                            
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Pass both the profile and the plan to ask() for a detailed explanation of the chosen configuration.

# Ask the assistant to explain the plan
# ==============================================================================
answer = assistant.ask(
    prompt  = (
        "Walk me through this plan. Why these lags and window features, "
        "and how will the 80% prediction interval be produced?"
    ),
    profile = profile,
    plan    = plan,
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮
                                                                                        
  The plan builds a ForecasterRecursive with LGBMRegressor to forecast 36 steps ahead   
  on hourly bike-sharing demand, using a rich set of lags, rolling window statistics,   
  calendar features, and three exogenous variables. Here is a walkthrough of each       
  design decision.                                                                      
                                                                                        
  Lag Selection                                                                         
                                                                                        
  The lag list contains 32 lags drawn from several distinct ranges:                     
                                                                                        
   • Short lags (1, 2, 3, 5, 8, 10, 15): capture very recent momentum and short         
     autocorrelation structure, meaning the last few hours of demand feed directly      
     into the next prediction.                                                          
   • Daily lags (17 through 26, 32, 33): cluster around the 24-hour mark, capturing     
     the same-hour pattern from yesterday. Lags like 23, 24, and 25 specifically        
     bracket the exact daily repeat.                                                    
   • Multi-day lags (119, 121, 135, 136, 142, 143, 145, 160, 166, 167, 169): these are  
     near the 5-day and 7-day marks (168 hours is one week). Lags 166, 167, and 169     
     bracket the weekly seasonal repeat, capturing the same hour from the same day      
     last week.                                                                         
   • Longer lags (313, 335, 337): these reach back roughly 13 days, picking up any      
     bi-weekly or fortnightly patterns present in the series.                           
                                                                                        
  The overall structure reflects that hourly ridership is likely governed by daily      
  commuting rhythms and weekly cycles, so the lags are concentrated at those seasonal   
  offsets rather than being a simple consecutive range.                                 
                                                                                        
  Window Features                                                                       
                                                                                        
  Four rolling statistics complement the lags:                                          
                                                                                        
   • Mean and standard deviation over 3 hours: captures very recent local level and     
     volatility, useful for detecting sudden demand shifts.                             
   • Mean over 24 hours: summarises the typical demand over the past day, providing a   
     smoothed daily-level signal without requiring 24 individual lag columns.           
   • Mean over 168 hours: represents the average demand over the past week, giving the  
     model a stable weekly baseline to anchor predictions.                              
                                                                                        
  Together these summaries let the model distinguish whether current demand is above    
  or below recent norms at three different timescales, with only four additional        
  features.                                                                             
                                                                                        
  Calendar and Exogenous Features                                                       
                                                                                        
  Calendar features (hour, day of week, weekend flag, month) are added with raw         
  ordinal encoding, giving the model direct signals about time-of-day and day-of-week   
  patterns. The three exogenous variables, namely holiday, weather (categorical,        
  handled automatically via categorical_features='auto'), and temp, allow the model to  
  adjust forecasts for conditions that affect ridership beyond historical patterns      
  alone.                                                                                
                                                                                        
  How the 80% Prediction Interval Is Produced                                           
                                                                                        
  The interval method is bootstrapping, which works as follows:                         
                                                                                        
   1 During fit, in-sample residuals (the differences between fitted and actual values  
     on the training set) are stored, and they are binned by prediction level using a   
     KBinsDiscretizer. This means residuals are grouped according to the magnitude of   
     the prediction they correspond to.                                                 
   2 When predict_interval is called, the forecaster generates many bootstrap sample    
     paths (controlled by the n_boot parameter). For each path, residuals are drawn     
     from the bin matching the current predicted level, then added to the point         
     forecast to simulate one possible future trajectory.                               
   3 After all bootstrap paths are collected for each of the 36 steps, the 10th and     
     90th percentiles of those simulated values are taken as the lower and upper        
     bounds.                                                                            
                                                                                        
  The result is an 80% interval, meaning that roughly 80% of actual future              
  observations are expected to fall within those bounds if the residual distribution    
  remains stable. The use of binned residuals (rather than a single global residual     
  pool) improves calibration when forecast uncertainty varies with the predicted        
  demand level, which is common in count-like series such as ridership.                 
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Refine the plan (optional)

The refine_plan() method lets you adjust the plan before execution. It operates in two distinct modes:

  • Deterministic mode (prompt=None): pass explicit configuration overrides such as lags, estimator, estimator_kwargs, forecaster, steps, interval, or window_features. Only the fields you explicitly specify are updated; the rest of the configuration is deterministically re-derived from the original plan.

  • LLM mode (prompt provided): describe your domain knowledge in natural language. The LLM interprets this context and suggests appropriate lags and window_features. Its reasoning is appended to plan.explanation and the changed fields are recorded in plan.llm_refined_fields for full traceability.

Warning

A refined plan is a hypothesis, not a guaranteed improvement. The LLM may propose lags or window features that are not helpful for the series, or it may misread the domain context you provided. Always compare the refined plan against the original baseline using a proper backtest over multiple folds before adopting it.

Deterministic mode

# Refine the plan with explicit overrides (no LLM required)
# ==============================================================================
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.                                                  
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

LLM mode

# Refine the plan using LLM-guided domain knowledge
# ==============================================================================
prompt = (
    "I'm forecasting hourly bike rentals. Demand follows a clear daily rhythm with "
    "rush-hour peaks, and it changes between weekdays and weekends. It's also usually "
    "similar to what happened at the same time last week, and the last few hours give "
    "a good sense of the current trend. Please pick lags and rolling features that fit this."
)

plan_refined = assistant.refine_plan(
    profile = profile,
    plan    = plan,
    prompt  = prompt
)
# Refined plan proposed by the assistant
# ==============================================================================
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, 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, 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 described three key dynamics for hourly bike       
  rentals:                                                                              
                                                                                        
   1 Recent trend (last few hours): Lags 1–6 capture the immediate short-term momentum  
     and trend in rentals, which is especially useful for modelling rush-hour ramp-ups  
     and cool-downs.                                                                    
   2 Daily rhythm with rush-hour peaks: Lags 23, 24, 25 anchor the forecast to the      
     same hour yesterday (±1 hour tolerance). This directly encodes the strong 24-hour  
     daily seasonality. A 24-hour rolling mean (window_size=24) smooths out the daily   
     level signal, and std over 24 hours captures intra-day volatility (e.g. the        
     spread between peak and off-peak hours).                                           
   3 Weekly pattern (weekday vs. weekend): Lags 47, 48, 49 capture the same hour two    
     days ago (helpful for tracking weekday/weekend transitions), and lags 167, 168,    
     169 directly anchor to the same hour last week (±1 hour), capturing the strong     
     7-day seasonality. A 168-hour rolling mean summarises the weekly baseline level,   
     while its std captures week-to-week variability and regime changes (e.g. a         
     holiday week vs. a regular week).                                                  
                                                                                        
  Together, this is a compact but well-targeted set: short lags for trend, seasonal     
  lags at 24 h and 168 h for daily and weekly cycles, and rolling stats at the same     
  two horizons for smoothed level and volatility — without inflating the feature space  
  with redundant intermediate lags.                                                     
                                                                                        
  Note: the LLM-suggested lags and window_features are hypotheses, not validated        
  improvements. Confirm any expected accuracy gain before relying on them.              
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Explain mode (refined plan)

# Ask the assistant what changed and why
# ==============================================================================
answer = assistant.ask(
    prompt  = (
        "What changed in the refined plan compared to the original, "
        "and why does it matter for this dataset?"
    ),
    profile = profile,
    plan    = plan_refined,
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮
                                                                                        
  The refined plan introduced a targeted set of lags and window features specifically   
  chosen to reflect the known dynamics of hourly bike rental data, rather than using a  
  generic default configuration. Here is what changed and why it is relevant.           
                                                                                        
  Lag Selection                                                                         
                                                                                        
  The original starting point would typically use a simple contiguous range of recent   
  lags. The refined plan uses a compact, non-contiguous set: [1, 2, 3, 4, 5, 6, 23,     
  24, 25, 47, 48, 49, 167, 168, 169].                                                   
                                                                                        
  Lags 1 through 6 capture short-term momentum, such as a rental surge building up   
     over several consecutive hours during a morning rush.                              
  Lags 23, 24, and 25 anchor each forecast to the same hour yesterday, plus or       
     minus one hour, directly encoding the 24-hour daily cycle.                         
  Lags 47, 48, and 49 capture the same hour two days ago, which may help the model   
     navigate weekday-to-weekend transitions.                                           
  Lags 167, 168, and 169 anchor to the same hour last week, directly encoding the    
     7-day weekly seasonality.                                                          
                                                                                        
  The plus-or-minus-one-hour tolerance around each seasonal anchor (24, 48, 168)        
  matters because real rental patterns can shift slightly around their peak times.      
                                                                                        
  Window Features                                                                       
                                                                                        
  Two pairs of rolling statistics were added: mean and standard deviation over a        
  24-hour window, and mean and standard deviation over a 168-hour window. The means     
  summarise the smoothed daily and weekly baseline level of rentals. The standard       
  deviations capture intra-day volatility and week-to-week variability, for example     
  distinguishing a holiday week from a regular commuting week.                          
                                                                                        
  Why This Matters for This Dataset                                                     
                                                                                        
  The dataset has 17,544 hourly observations with known exogenous context (holiday,     
  weather, temperature). Hourly bike rentals exhibit a strong 24-hour daily cycle and   
  a strong 7-day weekly cycle. A generic lag set might include many intermediate lags   
  that add noise without adding signal. The refined set is compact by design,           
  targeting only the three dynamics described: recent trend, daily rhythm, and weekly   
  pattern. This keeps the feature space smaller while directly representing the         
  seasonality structures most relevant to this series.                                  
                                                                                        
  One important caveat: the plan notes explicitly that the suggested lags and window    
  features are hypotheses, not validated improvements. Any expected accuracy gain       
  should be confirmed through backtesting before relying on it.                         
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Forecast

Once you have a profile and a plan, you can call forecast() or forecast_code(). Both accept the pre-computed profile and plan so no additional profiling is performed. The forecast() method executes the generated script and returns a ForecastResult; forecast_code() generates the script only, without running it.

The forecast branch operates in two modes:

  • Evaluation mode (test_size is set): the dataset is split into train and test sets, the model is trained on the train portion, and predictions are compared against the held-out actuals to compute metrics.

  • Prediction mode (test_size=None, the default): the model is trained on the entire dataset and forecasts the next steps time points into the future. Because there is no ground truth, no metrics are returned. If the data has exogenous variables, their future values must be supplied via exog.

Evaluation mode

# Forecast in evaluation mode, reusing the pre-computed profile and plan
# ==============================================================================
results_eval = assistant.forecast(
    data        = data,
    target      = 'users',
    date_column = 'date_time',
    steps       = 36,
    test_size   = 36,          # Last 36 hours as test set
    profile     = profile,     # Reuse the pre-computed profile
    plan        = plan_refined # Reuse the refined plan
)

display(results_eval.metrics)
display(results_eval.predictions.head())
series MAE MSE MASE MAPE
0 users 44.509038 4491.272806 0.69125 0.509689
pred lower_bound upper_bound
2012-12-30 12:00:00 143.367904 109.261080 174.845666
2012-12-30 13:00:00 130.565596 86.144130 166.581911
2012-12-30 14:00:00 121.586466 85.988106 163.576222
2012-12-30 15:00:00 125.681532 81.114751 161.331299
2012-12-30 16:00:00 128.594036 81.989952 165.070426
# Plot predictions vs. actual values for the held-out test period
# ==============================================================================
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='actual')
predictions['pred'].plot(ax=ax, label='prediction')
if {'lower_bound', 'upper_bound'}.issubset(predictions.columns):
    ax.fill_between(
        predictions.index, predictions['lower_bound'], predictions['upper_bound'],
        alpha=0.3, label='80% prediction interval'
    )
ax.set_title('Predictions vs. actual bike demand')
ax.set_ylabel('Users')
ax.legend()
plt.tight_layout()
plt.show()
# Ask the assistant to interpret the forecast results
# ==============================================================================
answer = assistant.ask(
    prompt = "Explain the results of this forecast, including the metrics and predictions.",
    result = results_eval
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮
                                                                                        
  The forecast predicts hourly bike rental counts 36 steps ahead, covering the final    
  hours of December 30 through December 31, 2012. The model uses a ForecasterRecursive  
  with LGBMRegressor and beats the naive baseline on the primary metric. Here is a      
  full breakdown.                                                                       
                                                                                        
  Model and Configuration                                                               
                                                                                        
  The forecaster combines several feature types to capture the known dynamics of        
  hourly rentals:                                                                       
                                                                                        
  Lags 1 through 6 for short-term momentum                                           
  Lags 23, 24, and 25 to anchor to the same hour yesterday, capturing the daily      
     24-hour cycle                                                                      
  Lags 47, 48, and 49 for the same hour two days ago, helping with                   
     weekday-to-weekend transitions                                                     
  Lags 167, 168, and 169 to anchor to the same hour last week, capturing the weekly  
     7-day cycle                                                                        
  Rolling mean and standard deviation over windows of 24 hours and 168 hours,        
     providing smoothed level and volatility signals at both the daily and weekly       
     scale                                                                              
  Calendar features (hour, day of week, weekend flag, month) as exogenous inputs     
  Three additional exogenous variables: holiday, weather (categorical, handled       
     automatically), and temp                                                           
                                                                                        
  Evaluation Metrics                                                                    
                                                                                        
  The backtesting results are:                                                          
                                                                                        
  MAE of 44.51, meaning predictions deviate from actual rental counts by about 44.5  
     users on average                                                                   
  MSE of 4491.27                                                                     
  MASE of 0.691, which is below 1.0, meaning the model outperforms a naive baseline  
     forecast                                                                           
  MAPE of approximately 51%, which should be interpreted cautiously since rental     
     counts can be low (near zero during overnight hours), causing percentage errors    
     to inflate                                                                         
                                                                                        
  The MASE is the most informative summary here. A value of 0.691 confirms the model    
  is genuinely adding predictive value beyond simply repeating the last observed        
  value.                                                                                
                                                                                        
  Predictions                                                                           
                                                                                        
  The 36-step forecast spans from 12:00 on December 30 to 23:00 on December 31. Key     
  characteristics of the full set of 36 predictions:                                    
                                                                                        
  Point forecasts range from a minimum of about 5.0 to a maximum of about 158.6      
     users, with a mean of about 70.4 users across the horizon                          
  Lower bounds (10th percentile) range from approximately -0.3 to 109.3, with a      
     mean of about 39.5                                                                 
  Upper bounds (90th percentile) range from approximately 8.6 to 184.2, with a mean  
     of about 106.1                                                                     
                                                                                        
  The 80% prediction intervals (bootstrapping method, covering the 10th to 90th         
  percentile) are noticeably wide relative to the point forecasts. For example, at      
  12:00 on December 30, the point forecast is about 143.4 users with an interval of     
  roughly 109.3 to 174.8. By late on December 31, forecasts drop to the mid-to-low      
  teens, reflecting typical overnight patterns, with tighter absolute interval widths   
  at lower count levels.                                                                
                                                                                        
  Summary                                                                               
                                                                                        
  The model performs well relative to the naive baseline (MASE of 0.691). The wide      
  prediction intervals acknowledge genuine uncertainty in the recursive multi-step      
  setup, where prediction error accumulates across steps. The low overnight rental      
  counts are also a known driver of high MAPE values, so that metric alone should not   
  be used to judge performance here.                                                    
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Prediction mode

In prediction mode, the model trains on the entire dataset and forecasts the next steps time points. Because the data includes exogenous variables (holiday, weather, temp), their future values must be supplied via the exog argument.

# Forecast the next 36 hours using the entire dataset (prediction mode)
# ==============================================================================
# Simulate future values of exogenous variables for the next 36 hours
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,        # Use the entire dataset (prediction mode)
    exog        = exog,        # Future values of exogenous variables
    profile     = profile,
    plan        = plan_refined
)

display(results_pred.predictions.head())
pred lower_bound upper_bound
2013-01-01 00:00:00 24.309643 16.027705 35.219552
2013-01-01 01:00:00 13.915313 3.844538 23.919623
2013-01-01 02:00:00 8.877284 2.506829 15.059061
2013-01-01 03:00:00 6.143251 1.555860 12.018677
2013-01-01 04:00:00 6.188143 1.538990 11.670134
# Full results object
# ==============================================================================
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, 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, 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 described three key dynamics for hourly bike       
  rentals:                                                                              
                                                                                        
   1 Recent trend (last few hours): Lags 1–6 capture the immediate short-term momentum  
     and trend in rentals, which is especially useful for modelling rush-hour ramp-ups  
     and cool-downs.                                                                    
   2 Daily rhythm with rush-hour peaks: Lags 23, 24, 25 anchor the forecast to the      
     same hour yesterday (±1 hour tolerance). This directly encodes the strong 24-hour  
     daily seasonality. A 24-hour rolling mean (window_size=24) smooths out the daily   
     level signal, and std over 24 hours captures intra-day volatility (e.g. the        
     spread between peak and off-peak hours).                                           
   3 Weekly pattern (weekday vs. weekend): Lags 47, 48, 49 capture the same hour two    
     days ago (helpful for tracking weekday/weekend transitions), and lags 167, 168,    
     169 directly anchor to the same hour last week (±1 hour), capturing the strong     
     7-day seasonality. A 168-hour rolling mean summarises the weekly baseline level,   
     while its std captures week-to-week variability and regime changes (e.g. a         
     holiday week vs. a regular week).                                                  
                                                                                        
  Together, this is a compact but well-targeted set: short lags for trend, seasonal     
  lags at 24 h and 168 h for daily and weekly cycles, and rolling stats at the same     
  two horizons for smoothed level and volatility — without inflating the feature space  
  with redundant intermediate lags.                                                     
                                                                                        
  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.3096 │     16.0277 │     35.2196 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 01:00:00 │ 13.9153 │      3.8445 │     23.9196 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 02:00:00 │  8.8773 │      2.5068 │     15.0591 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 03:00:00 │  6.1433 │      1.5559 │     12.0187 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 04:00:00 │  6.1881 │      1.5390 │     11.6701 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ ...                 │     ... │         ... │         ... │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 07:00:00 │ 32.6998 │     20.6468 │     73.6151 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 08:00:00 │ 54.4638 │     40.3162 │    128.4668 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 09:00:00 │ 63.5735 │     40.4755 │    167.7687 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 10:00:00 │ 75.5977 │     34.4001 │    164.2217 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 11:00:00 │ 81.6627 │     34.0974 │    178.6108 │
└─────────────────────┴─────────┴─────────────┴─────────────┘
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, 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)                                                                        

Code-only mode

Use forecast_code() when you want to preview or export the reproducible script without executing it. This is useful for code review, auditing the generated pipeline, or running the script in a separate environment.

# Generate the reproducible script without executing it
# ==============================================================================
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, 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.                      

The ForecastResult object

Both forecast() modes return a ForecastResult, a lightweight container that bundles everything the assistant used and produced.

Attribute Type Description
predictions DataFrame Forecasted values. When intervals are requested, the bound columns are included alongside the point predictions.
metrics DataFrame or None Evaluation metrics (MAE, MSE, MASE, MAPE), one row per series. None in prediction mode.
code str The exact standalone skforecast script that produced the forecast, ready to run on its own.
profile ForecastingProfile The data profile behind the forecast.
plan ForecastPlan The detailed configuration that was executed.

Backtesting

The backtesting branch uses the same profile and plan as the forecast branch but evaluates the model's historical performance through time series cross-validation. The key decision is how to configure the TimeSeriesFold object, which controls exactly how the historical data is partitioned into successive training and test windows.

skforecast-ai provides three distinct ways to define this validation strategy:

  1. Explicit instantiation (recommended): manually construct a TimeSeriesFold and pass it directly to backtest(). Use this when you already know your exact operational constraints.

  2. Deterministic create_cv(): allow the assistant to derive a sensible TimeSeriesFold from the profile and plan using rule-based defaults. You can override individual parameters explicitly.

  3. LLM create_cv() (with a prompt): describe your deployment use case in natural language. The LLM translates your description into a fully-configured TimeSeriesFold, accompanied by an explanation you can audit.

Define the backtesting strategy

Manual TimeSeriesFold

# Create your own TimeSeriesFold object
# ==============================================================================
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

Deterministic create_cv()

# Let the assistant derive a TimeSeriesFold with rule-based defaults
# ==============================================================================
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

LLM create_cv() with a natural-language prompt

Rather than manually configuring TimeSeriesFold parameters, you can describe your backtesting strategy in natural language and let the assistant translate it into a rigorous cross-validation schema.

# Let the assistant create the TimeSeriesFold from a natural-language prompt
# ==============================================================================
prompt = (
    "I forecast bike demand 36 hours ahead. "
    "The model should be trained once on all data up to the end of August 2012, 23:59. "
    "Do not refit the model as the window rolls forward."
)
cv_llm, cv_llm_explanation = assistant.create_cv(
    profile = profile,
    plan    = plan_refined,
    prompt  = prompt
)
# TimeSeriesFold derived from the 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

# LLM reasoning behind the TimeSeriesFold configuration
# ==============================================================================
print(textwrap.fill(cv_llm_explanation, width=88))
The user wants to train once on all data up to the end of August 2012 (2012-08-31
23:59), so initial_train_size is set to that date string. Since the model should never
be refitted as the evaluation window rolls forward, refit=False (train once). The
forecast horizon is 36 hours (steps=36, already set in the dataset context).
fixed_train_size has no effect when refit=False (the window doesn't move), so it is left
at its default. No deployment gap was mentioned, so gap=0 (default). All other
parameters are left at their defaults. Initial training up to 2012-08-31 23:59,
expanding window, no refit, 36-step horizon, 82 folds.

Since the prompt correctly describes the intended training cutoff and horizon, the cv_llm object returned by create_cv() reproduces the same initial_train_size and steps as the one we built manually. Note, however, that create_cv() defaults to an expanding window (fixed_train_size=False) unless a fixed one is explicitly requested, so cv_llm and cv_det differ from the manually built cv (which uses a fixed window) in that respect.

✏️ Note

The assistant also returns a cv_llm_explanation string that details the choices it made. Always inspect it, and the resulting TimeSeriesFold, rather than assuming an LLM-derived configuration is equivalent to what you intended.

Run the backtest

# Run backtesting, reusing the pre-computed profile and plan
# ==============================================================================
results_backtest = assistant.backtest(
    data        = data,
    target      = 'users',
    date_column = 'date_time',
    cv          = cv,           # TimeSeriesFold object
    profile     = profile,      # Reuse the pre-computed profile
    plan        = plan_refined  # Reuse the refined plan
)

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: 51.5109, mean_squared_error: 7214.6293,      
  mean_absolute_scaled_error: 0.8401, mean_absolute_percentage_error: 0.5802.           
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯
mean_absolute_error mean_squared_error mean_absolute_scaled_error mean_absolute_percentage_error
0 51.510902 7214.62932 0.840115 0.580167
fold pred lower_bound upper_bound
2012-09-01 00:00:00 0 130.211859 99.968511 156.251372
2012-09-01 01:00:00 0 109.513132 72.983995 140.265692
2012-09-01 02:00:00 0 71.058720 47.705659 102.381454
2012-09-01 03:00:00 0 45.074954 22.850053 69.603788
2012-09-01 04:00:00 0 21.052789 7.211254 37.476825
# Plot prediction intervals vs real value
# ==============================================================================
predictions = results_backtest.predictions
data_test = data.set_index('date_time').loc[predictions.index, :]

fig = go.Figure([
    go.Scatter(name='Prediction', x=predictions.index, y=predictions['pred'], mode='lines'),
    go.Scatter(
        name='Real value', x=data_test.index, y=data_test['users'], mode='lines',
    ),
    go.Scatter(
        name='Upper Bound', x=predictions.index, y=predictions['upper_bound'], mode='lines',
        marker=dict(color="#444"), line=dict(width=0), showlegend=False
    ),
    go.Scatter(
        name='Lower Bound', 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="Real value vs predicted in test data",
    xaxis_title="Date time",
    yaxis_title="Users",
    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),
    # Initial zoom on the x axis between Oct 1 and Oct 10
    xaxis=dict(range=['2012-10-01', '2012-10-10'])
)
fig.show()
# Ask the assistant to interpret the backtesting results
# ==============================================================================
answer = assistant.ask(
    prompt = (
        "Explain the results of this backtesting, including the strategy, metrics, "
        "and predictions. Is the model good enough to deploy?"
    ),
    result = results_backtest
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮
                                                                                        
  The backtesting evaluated a ForecasterRecursive with LGBMRegressor across 82 folds    
  covering September through December 2012, forecasting 36 hours ahead each time. The   
  model achieved a Mean Absolute Error of 51.51 users and a MASE of 0.84, meaning it    
  beats the naive baseline. Whether it is good enough to deploy depends on your         
  tolerance for roughly 51-user average errors and the context of your use case.        
                                                                                        
  Backtesting Strategy                                                                  
                                                                                        
  The cross-validation used a fixed training window ending at 2012-08-31 23:59, with    
  no refit across the 82 folds. Key parameters:                                         
                                                                                        
   • Steps: 36 (one forecast covers the next 36 hours)                                  
   • Fold stride: 36 (non-overlapping test windows, each fold starts exactly where the  
     previous one ended)                                                                
   • Fixed train size: True (the training window rolls forward, always the same         
     length, discarding older data)                                                     
   • Refit: False (the model was trained once on the initial window and applied to all  
     82 folds without retraining)                                                       
   • Gap: 0 (no delay between training end and forecast start)                          
   • Differentiation: None                                                              
                                                                                        
  This setup simulates a realistic deployment where the model is trained once and used  
  to forecast rolling 36-hour windows without updates.                                  
                                                                                        
  Feature Design                                                                        
                                                                                        
  The model used a carefully targeted set of inputs:                                    
                                                                                        
   • Short lags (1 to 6): capture recent momentum and short-term trends                 
   • Daily seasonal lags (23, 24, 25): anchor the forecast to the same hour yesterday,  
     plus or minus one hour                                                             
   • Two-day and weekly lags (47, 48, 49 and 167, 168, 169): capture the same hour two  
     days ago and the same hour last week, encoding the 7-day cycle                     
   • Rolling window features: mean and standard deviation over 24-hour and 168-hour     
     windows, capturing daily and weekly baseline level and volatility                  
   • Calendar features: hour, day of week, weekend flag, and month                      
   • Exogenous variables: holiday, weather (categorical, handled automatically), and    
     temperature                                                                        
                                                                                        
  Evaluation Metrics                                                                    
                                                                                        
   • MAE: 51.51 users - On average, the point forecast is off by about 51 users per     
     hour. Whether this is acceptable depends on the typical scale of hourly rentals.   
     The prediction summary shows values ranging from roughly 4 to 858 users, so in     
     high-demand periods the relative error may be small, while in low-demand periods   
     it could be proportionally larger.                                                 
   • MSE: 7214.63 - The squared metric is notably higher relative to the MAE,           
     suggesting some folds or hours have larger errors that inflate the squared loss.   
   • MASE: 0.84 - This is the most important metric for deployment readiness. A MASE    
     below 1.0 means the model outperforms the naive seasonal baseline. A value of      
     0.84 represents a meaningful improvement.                                          
   • MAPE: 0.58 - The model's percentage errors average around 58%. This figure should  
     be interpreted cautiously, as MAPE becomes unreliable when true values approach    
     zero, which is likely during overnight low-demand hours.                           
                                                                                        
  Prediction Intervals                                                                  
                                                                                        
  The 80% bootstrapped prediction intervals (lower bound at the 10th percentile, upper  
  bound at the 90th) show a reasonable spread. Across all 2,928 predictions, the mean   
  lower bound is about 186 users and the mean upper bound is about 280 users,           
  bracketing the mean point forecast of about 231 users. The lower bound minimum dips   
  just below zero (approximately -0.45), which is a known artifact of bootstrapping on  
  a count series and worth noting if downstream systems expect non-negative values.     
                                                                                        
  Deployment Readiness Assessment                                                       
                                                                                        
  The model clears the most important threshold: a MASE of 0.84 confirms it adds        
  genuine predictive value beyond a naive approach. Points to weigh before deploying:   
                                                                                        
   • In favour: MASE below 1, consistent evaluation across 82 non-overlapping folds,    
     rich feature set capturing daily and weekly patterns, exogenous variables          
     included                                                                           
   • Worth investigating: The MAPE of 0.58 suggests meaningful percentage errors,       
     particularly in low-demand hours. The near-zero lower bound may need clipping for  
     operational use.                                                                   
   • Refit consideration: The backtest used refit=False. If data patterns shift over    
     time, enabling periodic retraining via refit=True or refit=<int> in                
     TimeSeriesFold could improve production performance.                               
   • Comparison baseline: It would be worth confirming the MASE improvement against a   
     ForecasterEquivalentDate or ForecasterStats benchmark before committing to         
     deployment.                                                                        
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Code-only mode

Use backtest_code() to generate the reproducible backtesting script without executing it.

# Generate the reproducible backtest script without executing it
# ==============================================================================
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, 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())                                                                 

The BacktestResult object

The backtest() method returns a BacktestResult, a lightweight container that bundles all the backtesting artifacts.

Attribute Type Description
predictions DataFrame Full out-of-sample backtest predictions across all folds.
metrics DataFrame Backtesting metrics (MAE, MSE, MASE, MAPE), one row per series.
cv_config dict Resolved TimeSeriesFold parameters for full traceability of the validation strategy.
code str The exact standalone skforecast script that reproduces the backtesting workflow.
explanation str Human-readable summary of the backtesting configuration and results.
profile ForecastingProfile The data profile behind the backtest.
plan ForecastPlan The detailed configuration that was executed.
# Full results object
# ==============================================================================
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: 51.5109, mean_squared_error: 7214.6293,      
  mean_absolute_scaled_error: 0.8401, mean_absolute_percentage_error: 0.5802.           
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯
       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… ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│             51.5109 │          7214.6293 │               0.8401 │               0.5802 │
└─────────────────────┴────────────────────┴──────────────────────┴──────────────────────┘
                    Backtest Predictions (2928 rows)                    
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Index                   fold      pred  lower_bound  upper_bound ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ 2012-09-01 00:00:00 │  0.0000 │ 130.2119 │     99.9685 │    156.2514 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 01:00:00 │  0.0000 │ 109.5131 │     72.9840 │    140.2657 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 02:00:00 │  0.0000 │  71.0587 │     47.7057 │    102.3815 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 03:00:00 │  0.0000 │  45.0750 │     22.8501 │     69.6038 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 04:00:00 │  0.0000 │  21.0528 │      7.2113 │     37.4768 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ ...                 │     ... │      ... │         ... │         ... │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 19:00:00 │ 81.0000 │  70.1810 │     32.3445 │    121.4605 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 20:00:00 │ 81.0000 │  49.7231 │     18.8142 │     82.6254 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 21:00:00 │ 81.0000 │  33.5835 │     11.7511 │     54.1651 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 22:00:00 │ 81.0000 │  22.8176 │      9.4612 │     46.5829 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 23:00:00 │ 81.0000 │  15.6257 │      7.3005 │     33.3873 │
└─────────────────────┴─────────┴──────────┴─────────────┴─────────────┘
                          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, 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, 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 described three key dynamics for hourly bike       
  rentals:                                                                              
                                                                                        
   1 Recent trend (last few hours): Lags 1–6 capture the immediate short-term momentum  
     and trend in rentals, which is especially useful for modelling rush-hour ramp-ups  
     and cool-downs.                                                                    
   2 Daily rhythm with rush-hour peaks: Lags 23, 24, 25 anchor the forecast to the      
     same hour yesterday (±1 hour tolerance). This directly encodes the strong 24-hour  
     daily seasonality. A 24-hour rolling mean (window_size=24) smooths out the daily   
     level signal, and std over 24 hours captures intra-day volatility (e.g. the        
     spread between peak and off-peak hours).                                           
   3 Weekly pattern (weekday vs. weekend): Lags 47, 48, 49 capture the same hour two    
     days ago (helpful for tracking weekday/weekend transitions), and lags 167, 168,    
     169 directly anchor to the same hour last week (±1 hour), capturing the strong     
     7-day seasonality. A 168-hour rolling mean summarises the weekly baseline level,   
     while its std captures week-to-week variability and regime changes (e.g. a         
     holiday week vs. a regular week).                                                  
                                                                                        
  Together, this is a compact but well-targeted set: short lags for trend, seasonal     
  lags at 24 h and 168 h for daily and weekly cycles, and rolling stats at the same     
  two horizons for smoothed level and volatility — without inflating the feature space  
  with redundant intermediate lags.                                                     
                                                                                        
  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, 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())                                                                 

Comparing forecasters

Choosing a forecasting model should not rely on intuition alone. Two configurations that look equally reasonable can perform very differently once evaluated on real temporal data. The most reliable approach is to test every candidate under identical conditions and compare their metrics.

The compare() method does exactly that. It receives a list of candidate configurations, backtests each one using the same TimeSeriesFold strategy, and returns a leaderboard ranked by the selected metric.

In the step-by-step path, the key argument is profile. Passing the profile computed at the beginning of this tutorial skips profiling entirely and guarantees that every candidate is evaluated against the same data profile. Note that compare() does not accept a plan: each candidate derives its own plan from the shared profile, which is precisely what makes the candidates differ.

Candidates can be provided in two ways:

  • Automatic candidates (candidates=None): the assistant builds the comparison set from profile.forecaster_candidates, using the forecaster types identified as suitable during profiling. This is useful when exploring a new dataset without a predefined shortlist.

  • Explicit candidates (recommended): pass a list of (name, config) tuples, where name labels the row in the leaderboard and config holds the same override keys understood by plan(): 'forecaster', 'estimator', 'estimator_kwargs', 'lags' and 'window_features'. This provides full control and makes the resulting table easier to interpret.

A failed candidate does not stop the comparison. Instead, a CandidateFailedWarning is issued, the row records the error and is placed last.

💡 Tip

All candidates use the same cross-validation strategy, ensuring a fair comparison. However, the results are only meaningful if the cv setup reflects the real use case where the model will be deployed. For example, if the production system retrains weekly, the backtest should also refit weekly. If the model is expected to forecast 24 hours ahead, the backtest should use a 24-hour horizon. The evaluation window must also be representative. A period that is too short or dominated by unusual events (holidays, outages, or exceptional peaks) may favor a candidate that performs poorly over time. Define the validation setup carefully before comparing models so the final ranking is reliable.

Automatic candidates

# Compare the forecaster candidates suggested by the profile
# ==============================================================================
results_compare = assistant.compare(
    data        = data,
    target      = 'users',
    date_column = 'date_time',
    cv          = cv,       # Same TimeSeriesFold used in the backtest above
    candidates  = None,     # Candidates suggested by the assistant 
    profile     = profile   # Reuse the pre-computed profile
)
Comparing forecasters:   0%|          | 0/3 [00:00<?, ?it/s]
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
# Ranked 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
# Deterministic summary of the comparison
# ==============================================================================
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).                                                                            
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Explicit candidates

In practice, you will often already have a shortlist in mind: a fast baseline, a gradient boosting model, or a variant with a richer feature set. Passing explicit (name, config) tuples keeps the comparison focused and makes the resulting leaderboard easy to understand at a glance.

The config dictionary accepts the same overrides as plan(). Any omitted option falls back to the deterministic recommendation derived from the shared profile, so candidates can remain concise. For example, {'forecaster': 'ForecasterDirect'} changes only the forecaster while keeping the recommended estimator, lags, and features.

⚠️ Computational cost

Each candidate is backtested independently across all folds, so runtime increases with both the number and complexity of the configurations. Comparing four candidates will take roughly four times as long as running one backtest. Start with a small set of clearly different options, review the results, and refine from there. Testing many near-identical variants is costly and rarely useful.

# Compare an explicit shortlist of configurations
# ==============================================================================
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,  # Specific candidates to compare
    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]

The refined_plan candidate reuses the forecaster, estimator, lags and window features of the plan produced by refine_plan(). This is the recommended way to validate a refined plan: the leaderboard shows whether the extra domain knowledge actually improves the metrics compared to the deterministic baselines.

When several metrics are requested, all of them are shown as columns but only the first one drives the ranking.

# Ranked leaderboard, sorted by the first metric requested
# ==============================================================================
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 51.510902 0.840115
3 4 lgbm_daily_lags ForecasterRecursive LGBMRegressor 54.953374 0.896312
4 5 ridge_baseline ForecasterRecursive Ridge 93.145620 1.519244

Inspect individual candidates

Because every candidate is a full BacktestResult, the details of any individual configuration remain available, including its metrics, its predictions and the standalone script that generated them.

# Inspect a specific candidate
# ==============================================================================
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())                                                                 

Reuse the winning configuration

The most useful result of a comparison is often not the leaderboard, but best_candidate. It is a complete BacktestResult carrying both the winning profile and plan, so it can be fed back into the step-by-step workflow without manually rebuilding the configuration.

# Winning configuration
# ==============================================================================
print(f"Best candidate: {results_compare.best_name}")
results_compare.best_candidate.plan
Best candidate: 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.                     
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯
# Produce the final forecast with the winning configuration
# ==============================================================================
results_pred_best = assistant.forecast(
    data        = data,
    target      = 'users',
    date_column = 'date_time',
    steps       = 36,
    interval    = [0.1, 0.9],
    test_size   = None,                                # Prediction mode
    exog        = exog,                                # Future values of exogenous variables
    profile     = results_compare.profile,             # Shared profile
    plan        = results_compare.best_candidate.plan  # Winning plan
)

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)                                                                        

Explain mode (comparison)

Like any other result, a ComparisonResult can be passed to ask() to explain why the ranking looks the way it does. However, the LLM cannot change the outcome: all metrics and rankings are computed deterministically before it sees the result.

# Ask the assistant to interpret the comparison
# ==============================================================================
answer = assistant.ask(
    prompt = (
        "Explain the comparison results. Is the margin between the top candidates "
        "meaningful, or are they practically equivalent?"
    ),
    result = results_compare
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮
                                                                                        
  The foundation model (Chronos-2) clearly won this comparison, and the margin over     
  the runner-up is meaningful rather than negligible. The best candidate posted a mean  
  absolute error of 38.44 users, while the second-place candidate (lgbm_direct) posted  
  50.38 users, a gap that the deterministic summary describes as 23.7% ahead. That is   
  a substantive difference in practical terms, not a borderline result.                 
                                                                                        
  Leaderboard Summary                                                                   
                                                                                        
   • foundation_model (ForecasterFoundation / Chronos-2): MAE 38.44, MASE 0.597         
   • lgbm_direct (ForecasterDirect / LGBMRegressor): MAE 50.38, MASE 0.822              
   • refined_plan (ForecasterRecursive / LGBMRegressor): MAE 51.51, MASE 0.840          
   • lgbm_daily_lags (ForecasterRecursive / LGBMRegressor): MAE 54.95, MASE 0.896       
   • ridge_baseline (ForecasterRecursive / Ridge): MAE 93.15, MASE 1.519                
                                                                                        
  Interpreting the Margins                                                              
                                                                                        
  The gap between positions 2, 3, and 4 is comparatively narrow. The three              
  LightGBM-based candidates span a MAE range from roughly 50 to 55 users, and their     
  MASE values (0.822, 0.840, 0.896) are all fairly close together. Calling those three  
  practically equivalent would be reasonable, since the differences among them are      
  small relative to the larger gap separating them from Chronos-2.                      
                                                                                        
  The Ridge baseline is in a different category altogether. Its MASE of 1.519 means it  
  performs worse than a naive seasonal baseline, while every other candidate has a      
  MASE below 1.0, meaning they all beat the naive baseline.                             
                                                                                        
  Key Takeaway on MASE                                                                  
                                                                                        
  All four non-Ridge candidates have MASE below 1.0, confirming they each outperform    
  the naive baseline. Chronos-2 at 0.597 has the most comfortable margin below 1.0,     
  while lgbm_direct and refined_plan at 0.822 and 0.840 respectively are solid but      
  closer to the naive threshold.                                                        
                                                                                        
  Evaluation Robustness                                                                 
                                                                                        
  The comparison was run over 82 folds with a fixed 36-step horizon, which is a         
  thorough evaluation. Results based on that many folds carry more weight than a        
  single held-out window, so the ranking is unlikely to reverse due to a lucky or       
  unlucky evaluation window. The gap between Chronos-2 and the LightGBM cluster is      
  consistent enough across that many folds to be considered meaningful.                 
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

The ComparisonResult object

The compare() method returns a ComparisonResult, which groups the shared setup, the ranked leaderboard, and the individual backtests in a single object.

Attribute Type Description
results DataFrame Ranked leaderboard, one row per candidate, sorted best to worst. Columns: rank, name, forecaster, estimator, the metric columns, and error when at least one candidate failed.
candidates dict Mapping of candidate name to the full BacktestResult object.
failures dict Mapping of candidate name to a CandidateFailure describing why it failed. Empty when every candidate succeeds.
ranking_metric str Name of the metric used to sort results.
cv_config dict Resolved TimeSeriesFold parameters plus the resulting n_folds, applied identically to every candidate.
profile ForecastingProfile The shared data profile behind every candidate.
explanation str Deterministic, human-readable summary of the comparison.
best_name str Name of the top-ranked candidate.
best_candidate BacktestResult Top-ranked candidate as a complete BacktestResult.
# Full results object
# ==============================================================================
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… │     51.5109 │       0.8401 │
├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤
│ 3     │    4 │ lgbm_daily_… │ ForecasterR… │ LGBMRegress… │     54.9534 │       0.8963 │
├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤
│ 4     │    5 │ ridge_basel… │ ForecasterR… │        Ridge │     93.1456 │       1.5192 │
└───────┴──────┴──────────────┴──────────────┴──────────────┴─────────────┴──────────────┘

Reproducible code

Every forecast or backtest workflow exposes the standalone skforecast script used to produce its results. This script is strictly deterministic, ensuring identical outputs for a given set of inputs and configurations. You can access the code at any time via the show_code() method.

Free-form Q&A

The ask() method is not limited to interpreting workflow objects. Without any profile, plan, or result attached, it works as a general forecasting knowledge assistant, useful for clarifying methodology, choosing between approaches, or understanding metric trade-offs.

# Ask a general forecasting question (no data or result required)
# ==============================================================================
answer = assistant.ask(
    prompt = (
        "For hourly demand with strong daily and weekly seasonality, when should "
        "I prefer a direct forecasting strategy over a recursive one?"
    )
)
answer.show_explanation()
╭────────────────────────────────── Assistant Response ──────────────────────────────────╮
                                                                                        
  Direct forecasting is worth considering when your forecast horizon is long enough     
  that recursive error propagation becomes a real concern, and when the relationship    
  between past observations and future values changes meaningfully across steps. For    
  hourly data with daily and weekly seasonality, those conditions arise in specific     
  ways.                                                                                 
                                                                                        
  When Direct Is the Better Choice                                                      
                                                                                        
   • Long horizons relative to your seasonality cycles. For hourly data, a 24-step or   
     168-step horizon means your recursive forecaster must feed its own predictions     
     back as inputs many times before reaching the end of the horizon. Each step        
     compounds any bias or error introduced by the model. A ForecasterDirect trains     
     one independent model per step, so step 168 never sees the predicted values from   
     steps 1 through 167.                                                               
   • Step-specific patterns across the horizon. Hourly demand often behaves             
     differently at step 1 (one hour ahead) versus step 24 (tomorrow at the same hour)  
     or step 168 (same hour next week). If the features that explain demand at 2 AM     
     differ from those that explain demand at 5 PM, a single recursive model must       
     represent all of those relationships at once. ForecasterDirect lets each step      
     learn its own predictive relationship independently.                               
   • When you want to eliminate recursive error propagation entirely. ForecasterDirect  
     always uses only observed lag values at prediction time, never its own prior       
     predictions. This is the clearest structural advantage over ForecasterRecursive.   
                                                                                        
  When Recursive Is Still Preferable                                                    
                                                                                        
   • Short horizons. If you only need 1 to 6 steps ahead, error propagation is minimal  
     and ForecasterRecursive trains faster with a single model.                         
   • Limited training data. Training N separate models (one per step) with              
     ForecasterDirect can lead to overfitting when data is scarce, since each model     
     sees the same number of rows but must generalise to a specific future offset.      
   • Computational budget. ForecasterDirect stores N estimators in its estimators_      
     dictionary and can be parallelised with n_jobs, but it is still slower and more    
     memory-intensive than training one model.                                          
                                                                                        
  Practical Skforecast Guidance                                                         
                                                                                        
  With ForecasterDirect, set steps at initialisation to match your full horizon      
     (for example, steps=168 for one week ahead at hourly frequency).                   
  Use n_jobs=-1 to parallelise training across steps.                                
  Add calendar features such as hour-of-day and day-of-week as exogenous variables   
     in either forecaster; these may be especially valuable for capturing seasonality   
     patterns.                                                                          
  Use backtesting_forecaster with a TimeSeriesFold configured to your actual         
     operational horizon to compare the two strategies on your specific data before     
     committing to one.                                                                 
  Compare both against ForecasterEquivalentDate (same hour, same weekday, prior      
     week) as a naive baseline to confirm either ML approach adds value.                
                                                                                        
  The core trade-off is: ForecasterRecursive is simpler and faster; ForecasterDirect    
  avoids error propagation and allows step-specific modelling, which may be beneficial  
  at horizons of 24 or more steps on hourly data with strong seasonality patterns.      
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

Summary

This tutorial covered the step-by-step path of skforecast-ai. Here is a recap of what each stage does and when to use it:

Step Method When to use
1. Profile profile() Always: produces the ForecastingProfile required by all downstream methods.
2. Plan plan() Always: converts the profile into an executable configuration.
3. Refine plan refine_plan() Optional: use when you want to override specific decisions (deterministic) or inject domain knowledge (LLM). Always evaluate the result.
4a. Forecast forecast() When you want future predictions or a held-out evaluation in a single execution.
4a. Code only forecast_code() When you want to preview or export the script without running it.
4b. CV strategy create_cv() When you want the assistant to derive or translate a TimeSeriesFold for you.
4b. Backtest backtest() When you want to evaluate the model over multiple historical folds.
4b. Code only backtest_code() When you want to preview or export the backtesting script without running it.
4c. Compare compare() When you want to rank several configurations under an identical cross-validation strategy and reuse the winner.
Any time ask() When you want an LLM explanation of any intermediate object or result, or a general forecasting Q&A.

The key advantage of this path is that the profile and plan are built once and reused across both the forecast and backtest branches. This avoids redundant profiling and ensures that both branches use the same modeling configuration. The same profile can also be handed to compare(), so every candidate is ranked against the very same data profile.

For a faster alternative that runs the entire pipeline in a single call, revisit the Quickstart section at the top of this guide. For a comprehensive overview of backtesting mechanics, see the skforecast backtesting user guide.

Session information

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 08:56

Citation

How to cite this document

If you use this document or any part of it, please acknowledge the source, thank you!

Agentic forecasting with skforecast-AI by Joaquín Amat Rodrigo and Javier Escobar Ortiz available under Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) at https://cienciadedatos.net/documentos/py80-agentic-forecasting-skforecast-ai.ipynb

How to cite skforecast-ai

If you use skforecast-ai for a publication, we would appreciate if you cite the published software.

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 (Version 0.2.0) [Computer software]. 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} }


Did you like the article? Your support is important

Your contribution will help me to continue generating free educational content. Many thanks! 😊

Become a GitHub Sponsor Become a GitHub Sponsor

Creative Commons Licence

This work by Joaquín Amat Rodrigo, Javier Escobar Ortiz is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International.

Allowed:

  • Share: copy and redistribute the material in any medium or format.

  • Adapt: remix, transform, and build upon the material.

Under the following terms:

  • Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.

  • NonCommercial: You may not use the material for commercial purposes.

  • ShareAlike: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.