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.23.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 primarily because your dataset has 17,544    
  hourly observations and a single target series ("users"), which makes                 
  ForecasterRecursive with LGBMRegressor the natural starting point: one global model   
  trained recursively across the full series, using a gradient boosting estimator       
  well-suited to datasets of this size.                                                 
                                                                                        
  Why ForecasterRecursive                                                               
                                                                                        
  ForecasterRecursive trains a single model that reuses its own predictions as inputs   
  when generating multi-step forecasts. For a 36-step ahead horizon on hourly data,     
  this is efficient and flexible. The plan notes that ForecasterDirect and              
  ForecasterFoundation are alternatives, but the recursive approach is the recommended  
  default for this kind of single-series regression problem.                            
                                                                                        
  Why LGBMRegressor                                                                     
                                                                                        
  The profile decision explicitly states that a gradient boosting model is preferred    
  for a dataset of this size. LightGBM handles the scale well, trains quickly, and      
  natively tolerates missing values, which is why the plan keeps NaN rows rather than   
  dropping them. XGBRegressor and Ridge are listed as alternatives but were not         
  selected as the primary recommendation.                                               
                                                                                        
  What the lags and window features capture                                             
                                                                                        
  The plan uses a rich set of 32 lags, including short-term lags (1, 2, 3), daily       
  cycle lags (around 24), weekly cycle lags (around 168), and longer lags reaching      
  back to 337 hours. This directly encodes recent demand, the same hour yesterday, and  
  the same hour last week. The window features add rolling summaries: a short 3-hour    
  mean and standard deviation, a 24-hour mean capturing the daily average, and a        
  168-hour mean capturing the weekly average. Together these give the model a layered   
  picture of demand at multiple time scales.                                            
                                                                                        
  What the exogenous variables add                                                      
                                                                                        
  Your three exogenous variables, "holiday", "weather", and "temp", provide             
  information the lags and window features cannot reconstruct on their own.             
                                                                                        
  "holiday" signals days when demand patterns are structurally different from        
     normal workdays, something a lag of 24 or 168 hours may not reliably capture if    
     holidays fall irregularly.                                                         
  "weather" is categorical (detected automatically by skforecast via                 
     categorical_features='auto') and may associate with distinct demand regimes, such  
     as rain suppressing ridership.                                                     
  "temp" is a continuous variable that may be associated with comfort thresholds     
     influencing whether people choose to bike.                                         
                                                                                        
  Together these three variables give the model context about conditions at the         
  forecast horizon that the historical series alone cannot supply.                      
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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 hours ahead,  
  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 carefully chosen lags rather than a dense consecutive        
  block. A few groupings stand out:                                                     
                                                                                        
   • Short-term lags (1, 2, 3, 5, 8, 10, 15): capture immediate autocorrelation and     
     intra-hour dynamics from the last several hours.                                   
   • Daily cycle lags (17 through 26, 32, 33): bracket the 24-hour mark, letting the    
     model learn from approximately the same hour yesterday plus a few hours on either  
     side.                                                                              
   • Weekly cycle lags (119, 121, 135, 136, 142, 143, 145, 160, 166, 167, 169):         
     cluster around 120 hours (5 days) and 168 hours (exactly one week ago), capturing  
     the strong day-of-week pattern typical of hourly user data.                        
   • Two-week lags (313, 335, 337): reach back roughly two weeks, picking up any        
     fortnightly seasonality or calendar regularities.                                  
                                                                                        
  This sparse but deliberate selection avoids loading the model with hundreds of        
  redundant consecutive lags while still covering the dominant seasonal frequencies     
  present in an hourly series.                                                          
                                                                                        
  Window Features                                                                       
                                                                                        
  Three RollingFeatures objects complement the lags by summarising history at           
  different scales:                                                                     
                                                                                        
   • Mean and std over the last 3 hours: a very short smoothing window that captures    
     the immediate local level and variability, useful for detecting sudden spikes or   
     drops.                                                                             
   • Mean over the last 24 hours: represents the recent daily average level, giving     
     the model a smoothed sense of today's baseline without adding 24 separate lag      
     columns.                                                                           
   • Mean over the last 168 hours: the rolling weekly average, encoding the typical     
     weekly rhythm as a single feature rather than a collection of week-ago lags.       
                                                                                        
  Together these four statistics (two short, one daily, one weekly) provide             
  multi-scale context that the point-in-time lags alone cannot fully capture.           
                                                                                        
  Calendar and Exogenous Features                                                       
                                                                                        
  Calendar features (hour, day_of_week, weekend, month) are added with raw ordinal      
  encoding, giving LightGBM direct access to time-of-day and day-of-week signals. The   
  three exogenous variables, holiday, temp, and weather (which is categorical and       
  handled automatically via categorical_features='auto'), supply real-world context     
  that is not derivable from the series history alone.                                  
                                                                                        
  How the 80% Prediction Interval Is Produced                                           
                                                                                        
  The interval method is bootstrapping, which works in these steps:                     
                                                                                        
   1 During fit, in-sample residuals (the differences between fitted and actual values  
     on the training set) are stored. With use_binned_residuals=True, these residuals   
     are grouped by the predicted value level, so that the interval width can vary      
     depending on whether the forecast is high or low, rather than applying a single    
     uniform spread.                                                                    
   2 At prediction time, the forecaster runs n_boot simulated forecast paths, each      
     time drawing residuals from the appropriate bin and adding them to the recursive   
     predictions step by step.                                                          
   3 After all bootstrap paths are collected, the 10th and 90th percentiles of those    
     paths at each horizon step form the lower and upper bounds, producing an interval  
     with nominal 80% coverage.                                                         
                                                                                        
  The result is a DataFrame with columns for the point forecast, lower_bound, and       
  upper_bound across all 36 steps.                                                      
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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', │
│                   │ 'max'], '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)', 'max(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 core dynamics for hourly bike      
  rentals:                                                                              
                                                                                        
   1 Short-term trend (last few hours): Lags 1–6 capture the immediate recent trend —   
     the last few hours of demand, including the build-up and decay of rush-hour        
     peaks.                                                                             
   2 Daily rhythm (24-hour cycle): Lags 23, 24, 25 capture the same hour yesterday and  
     its immediate neighbours, directly encoding the strong daily seasonality. A        
     rolling mean and std over a 24-hour window also summarise yesterday's average      
     level and variability (e.g., how volatile demand was over the past day), which     
     helps the model adapt to different day types.                                      
   3 Weekly rhythm (weekday vs. weekend): Lags 47, 48, 49 (±1 around 48 h = 2 days      
     ago) and lags 167, 168, 169 (±1 around 168 h = exactly one week ago) let the       
     model directly compare the same hour from the previous week and the day before     
     yesterday. A rolling mean and max over the full 168-hour week summarise the        
     weekly baseline level and the peak demand observed in the past week — critical     
     for distinguishing weekday commuter spikes from weekend leisure patterns.          
                                                                                        
  This set avoids redundancy (no consecutive lags beyond the short-term window) while   
  covering all three seasonality scales the user identified. The window features        
  complement the lags by providing smooth, compact summaries at the daily and weekly    
  scales without requiring dozens of additional lag columns.                            
                                                                                        
  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 extends the feature set beyond a simple set of recent lags by        
  explicitly encoding three seasonality scales that are characteristic of hourly bike   
  rental demand. Here is what changed and why each change is relevant.                  
                                                                                        
  Original vs Refined Feature Set                                                       
                                                                                        
  The profile decision identified a ForecasterRecursive with LGBMRegressor and noted    
  the exogenous variables. The refinement step specified a concrete, structured set of  
  lags and window features rather than leaving them at a generic default.               
                                                                                        
  What Changed                                                                          
                                                                                        
  Lags                                                                                  
                                                                                        
  The refined plan uses lags [1, 2, 3, 4, 5, 6, 23, 24, 25, 47, 48, 49, 167, 168, 169]  
  instead of a simple consecutive range. The key additions are:                         
                                                                                        
  Lags 1 through 6 capture the immediate short-term trend, including the build-up    
     and decay of rush-hour peaks over the preceding few hours.                         
  Lags 23, 24, 25 target the same hour yesterday and its immediate neighbours,       
     directly encoding the strong 24-hour daily cycle.                                  
  Lags 47, 48, 49 reference two days ago at the same hour, giving the model a        
     second daily anchor point.                                                         
  Lags 167, 168, 169 reference exactly one week ago plus or minus one hour,          
     encoding the weekly rhythm and the contrast between weekday commuter patterns and  
     weekend leisure patterns.                                                          
                                                                                        
  This avoids adding dozens of consecutive lag columns while still covering all three   
  seasonality scales.                                                                   
                                                                                        
  Window Features                                                                       
                                                                                        
  Four rolling summary features are added across two window sizes:                      
                                                                                        
  Rolling mean and standard deviation over 24 hours summarise yesterday's average    
     demand level and its variability, helping the model adapt to different day types.  
  Rolling mean and maximum over 168 hours (one full week) capture the weekly         
     baseline level and the peak demand observed in the past week.                      
                                                                                        
  These complement the point lags by providing smooth, compact signals at the daily     
  and weekly scales.                                                                    
                                                                                        
  Calendar Features                                                                     
                                                                                        
  Hour of day, day of week, a weekend indicator, and month are added as raw ordinal     
  features. For bike rentals, the hour of day alone can account for large swings        
  between off-peak and peak periods, and the weekend flag directly encodes the          
  commuter-versus-leisure split.                                                        
                                                                                        
  Why It Matters for This Dataset                                                       
                                                                                        
  Hourly bike rental demand is strongly structured by time. Without lags and window     
  features that explicitly span 24-hour and 168-hour horizons, the model would need to  
  infer those cycles indirectly from recent lags alone, which becomes unreliable for a  
  36-step forecast horizon. By anchoring features at those exact periodicity points,    
  the model has direct access to the relevant historical context at each seasonality    
  scale.                                                                                
                                                                                        
  One important caveat: as noted in the plan, the lag and window feature choices are    
  hypotheses and not yet validated improvements. The accuracy gain over a simpler lag   
  set should be confirmed through backtesting before relying on them in production.     
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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 39.992629 3680.134351 0.621108 0.4819
pred lower_bound upper_bound
2012-12-30 12:00:00 146.169439 105.096817 180.289260
2012-12-30 13:00:00 135.193183 90.746536 170.332678
2012-12-30 14:00:00 133.087911 82.962582 167.156476
2012-12-30 15:00:00 133.307877 91.917838 163.950205
2012-12-30 16:00:00 137.103069 90.414013 171.935524
# 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 covers 36 hourly steps from 2012-12-30 12:00 through 2012-12-31 23:00,   
  predicting bike rental demand using a ForecasterRecursive model with LGBMRegressor.   
  The model achieved a MASE of 0.621, which is below 1.0, meaning it outperforms the    
  naive baseline forecast. Here is a full breakdown.                                    
                                                                                        
  Model Setup                                                                           
                                                                                        
  The forecaster was built with these key components:                                   
                                                                                        
  Estimator: LGBMRegressor, chosen as a gradient boosting model well suited to the   
     dataset size of 17,544 hourly observations                                         
  Lags: 15 selected lags covering short-term recency (hours 1 through 6), the daily  
     cycle (hours 23, 24, 25), two-day history (hours 47, 48, 49), and the weekly       
     cycle (hours 167, 168, 169)                                                        
  Window features: rolling mean and standard deviation over 24 hours, plus rolling   
     mean and maximum over 168 hours (one full week)                                    
  Exogenous variables: holiday, weather (categorical, handled automatically), and    
     temp                                                                               
  Calendar features: hour, day of week, weekend indicator, and month                 
                                                                                        
  Evaluation Metrics                                                                    
                                                                                        
  The backtesting results across the full series are:                                   
                                                                                        
  MAE of 39.99 means the model's point forecasts are off by about 40 users per hour  
     on average                                                                         
  MASE of 0.621 confirms the model beats the naive seasonal baseline, since any      
     value below 1.0 represents an improvement over that reference                      
  MAPE of 0.4819 (roughly 48%) is a percentage-based measure; because bike rental    
     counts can approach low values at night or quiet periods, this figure may be       
     inflated by near-zero denominators and should be interpreted with caution          
  MSE of 3,680 reflects the average squared error, which penalizes larger            
     individual mistakes more heavily than MAE does                                     
                                                                                        
  Predictions                                                                           
                                                                                        
  Across all 36 predicted hours, the point forecasts range from a minimum of about 4.9  
  users to a maximum of about 152.8 users, with a mean prediction of about 74.3 users   
  per hour.                                                                             
                                                                                        
  The 80% prediction intervals (lower bound at the 10th percentile, upper bound at the  
  90th percentile, computed via bootstrapping) show:                                    
                                                                                        
  Lower bounds range from about 0.5 to 105.1, with a mean of about 39.3              
  Upper bounds range from about 13.6 to 187.3, with a mean of about 105.9            
                                                                                        
  The interval width varies across the horizon, which is expected with bootstrapping:   
  uncertainty tends to grow as predictions feed back into later steps in a recursive    
  forecaster. The visible rows at the end of the horizon (late evening on 2012-12-31)   
  show notably lower predicted demand, consistent with nighttime hours, and             
  correspondingly narrower intervals.                                                   
                                                                                        
  Summary                                                                               
                                                                                        
  The model performs well relative to the naive baseline, with a MASE of 0.621 and a    
  MAE of roughly 40 users per hour. The 80% intervals provide a useful uncertainty      
  band around each point forecast. The MAPE should be treated as supplementary given    
  the potential for low rental counts in off-peak hours to distort percentage-based     
  measures.                                                                             
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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.184796 14.907624 34.497458
2013-01-01 01:00:00 13.733646 3.659032 21.379483
2013-01-01 02:00:00 7.976549 2.642388 13.898093
2013-01-01 03:00:00 5.635198 1.515536 9.686748
2013-01-01 04:00:00 5.568613 1.659878 9.801700
# 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', │
│                   │ 'max'], '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)', 'max(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 core dynamics for hourly bike      
  rentals:                                                                              
                                                                                        
   1 Short-term trend (last few hours): Lags 1–6 capture the immediate recent trend —   
     the last few hours of demand, including the build-up and decay of rush-hour        
     peaks.                                                                             
   2 Daily rhythm (24-hour cycle): Lags 23, 24, 25 capture the same hour yesterday and  
     its immediate neighbours, directly encoding the strong daily seasonality. A        
     rolling mean and std over a 24-hour window also summarise yesterday's average      
     level and variability (e.g., how volatile demand was over the past day), which     
     helps the model adapt to different day types.                                      
   3 Weekly rhythm (weekday vs. weekend): Lags 47, 48, 49 (±1 around 48 h = 2 days      
     ago) and lags 167, 168, 169 (±1 around 168 h = exactly one week ago) let the       
     model directly compare the same hour from the previous week and the day before     
     yesterday. A rolling mean and max over the full 168-hour week summarise the        
     weekly baseline level and the peak demand observed in the past week — critical     
     for distinguishing weekday commuter spikes from weekend leisure patterns.          
                                                                                        
  This set avoids redundancy (no consecutive lags beyond the short-term window) while   
  covering all three seasonality scales the user identified. The window features        
  complement the lags by providing smooth, compact summaries at the daily and weekly    
  scales without requiring dozens of additional lag columns.                            
                                                                                        
  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.1848 │     14.9076 │     34.4975 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 01:00:00 │ 13.7336 │      3.6590 │     21.3795 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 02:00:00 │  7.9765 │      2.6424 │     13.8981 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 03:00:00 │  5.6352 │      1.5155 │      9.6867 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-01 04:00:00 │  5.5686 │      1.6599 │      9.8017 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ ...                 │     ... │         ... │         ... │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 07:00:00 │ 55.5648 │     20.9451 │     92.3618 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 08:00:00 │ 69.1623 │     43.4470 │    182.4530 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 09:00:00 │ 75.4641 │     43.4894 │    184.2271 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 10:00:00 │ 65.8258 │     37.8858 │    163.2721 │
├─────────────────────┼─────────┼─────────────┼─────────────┤
│ 2013-01-02 11:00:00 │ 64.8625 │     39.0137 │    182.4295 │
└─────────────────────┴─────────┴─────────────┴─────────────┘
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', 'max'],                                        
    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', 'max'],                                        
    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 the model 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 retrained as the evaluation window rolls forward, refit=False (train
once). This means fixed_train_size has no effect (as documented: fixed_train_size is
irrelevant without refit), so it is left at its default. The forecast horizon is 36
hours (steps=36), matching the dataset metadata. No deployment gap was mentioned, so
gap=0 (default). With 17,544 hourly observations and training ending in August 2012
(roughly 8 months × ~730 h ≈ 5,840 observations), there is ample remaining data for well
over 2 folds of 36-step evaluation, satisfying the minimum-folds constraint. 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: 50.3025, mean_squared_error: 6910.7727,      
  mean_absolute_scaled_error: 0.8204, mean_absolute_percentage_error: 0.5551.           
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯
mean_absolute_error mean_squared_error mean_absolute_scaled_error mean_absolute_percentage_error
0 50.302476 6910.772709 0.820406 0.555051
fold pred lower_bound upper_bound
2012-09-01 00:00:00 0 129.141506 100.578964 153.657475
2012-09-01 01:00:00 0 106.534982 73.902908 136.019721
2012-09-01 02:00:00 0 67.848374 38.599536 105.805277
2012-09-01 03:00:00 0 39.658499 19.872419 69.489409
2012-09-01 04:00:00 0 19.804622 5.966910 36.093576
# 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 LightGBM across 82 folds on      
  hourly bike rental data, producing a mean absolute error of 50.30 users. The MASE of  
  0.82 confirms the model beats the naive baseline, which is an encouraging sign.       
  Whether it is ready to deploy depends on the tolerance for roughly 50 users of        
  average error in context.                                                             
                                                                                        
  Backtesting Strategy                                                                  
                                                                                        
  The cross-validation used a fixed-window, no-refit design with the following          
  configuration:                                                                        
                                                                                        
   • Initial training cutoff: 2012-08-31 23:59 (everything before that date used for    
     training)                                                                          
   • Forecast horizon: 36 steps (36 hours ahead per fold)                               
   • Refit: False, meaning one model was trained once and evaluated across all 82       
     folds without updating                                                             
   • Fixed training size: True, so the training window does not expand as the           
     evaluation moves forward                                                           
   • Gap: 0, meaning forecasts begin immediately after the last training observation    
   • Fold stride: 36, so each fold's test window starts exactly where the previous one  
     ended, covering September through December 2012 without overlap                    
                                                                                        
  This setup simulates deploying a single trained model and measuring how it holds up   
  over four months of unseen hourly data.                                               
                                                                                        
  Evaluation Metrics                                                                    
                                                                                        
   • MAE: 50.30 users — on average, predictions deviate from actual rentals by about    
     50 users per hour. This is the most interpretable number for operational           
     decisions.                                                                         
   • MSE: 6910.77 — the squared error is sensitive to occasional large misses. The gap  
     between MAE and the square root of MSE (roughly 83) suggests some hours have       
     substantially larger errors than the average.                                      
   • MASE: 0.82 — this is the most important metric here. A value below 1.0 means the   
     model outperforms the naive seasonal baseline. At 0.82, it beats that baseline by  
     a meaningful margin.                                                               
   • MAPE: 0.56 (55.6%) — this is high and should be interpreted cautiously. MAPE       
     inflates when actual rental counts are near zero, which happens in overnight       
     hours. It likely overstates the practical error during the hours that matter most  
     operationally.                                                                     
                                                                                        
  Predictions and Intervals                                                             
                                                                                        
  Across all 2,928 predicted hours, point forecasts ranged from roughly 3 to 867        
  users, with a mean of about 234. The 80% prediction intervals (lower bound mean       
  ~185, upper bound mean ~280) provide a reasonable spread around those point           
  forecasts. A small number of lower bound values dip slightly below zero, which is     
  physically impossible for rental counts and is worth noting as a minor calibration    
  imperfection.                                                                         
                                                                                        
  Is the Model Ready to Deploy?                                                         
                                                                                        
  The evidence is moderately positive but not conclusive on its own. Points in favour:  
                                                                                        
  MASE of 0.82 confirms it genuinely improves on the naive baseline across 82        
     non-overlapping evaluation folds                                                   
  The fixed-window no-refit strategy is a conservative test, meaning real            
     deployment with periodic retraining would likely perform at least as well          
  The model covers all three seasonality scales identified for this data (hourly,    
     daily, weekly) through carefully chosen lags and rolling features                  
                                                                                        
  Points to consider before deploying:                                                  
                                                                                        
  A MAE of 50 users may or may not be acceptable depending on the operational        
     context (fleet sizing, rebalancing decisions, etc.). That threshold is a business  
     judgement, not a statistical one.                                                  
  The MASE of 0.82 is solid but leaves room for improvement. Hyperparameter tuning   
     via bayesian_search_forecaster or feature selection via select_features could      
     close that gap.                                                                    
  The model was trained once and never retrained across four months. Running a       
     refit=True or periodic-refit backtest would reveal whether drift over time is a    
     concern.                                                                           
  The 80% prediction intervals have not been formally calibrated. Checking coverage  
     with calculate_coverage against a holdout set would confirm whether the intervals  
     are trustworthy for operational uncertainty management.                            
                                                                                        
  A reasonable next step before deployment would be to tune the model and verify        
  interval coverage, but the current results justify continued development rather than  
  rejection.                                                                            
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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', 'max'],                                        
    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: 50.3025, mean_squared_error: 6910.7727,      
  mean_absolute_scaled_error: 0.8204, mean_absolute_percentage_error: 0.5551.           
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯
       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… ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│             50.3025 │          6910.7727 │               0.8204 │               0.5551 │
└─────────────────────┴────────────────────┴──────────────────────┴──────────────────────┘
                    Backtest Predictions (2928 rows)                    
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Index                   fold      pred  lower_bound  upper_bound ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ 2012-09-01 00:00:00 │  0.0000 │ 129.1415 │    100.5790 │    153.6575 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 01:00:00 │  0.0000 │ 106.5350 │     73.9029 │    136.0197 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 02:00:00 │  0.0000 │  67.8484 │     38.5995 │    105.8053 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 03:00:00 │  0.0000 │  39.6585 │     19.8724 │     69.4894 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-09-01 04:00:00 │  0.0000 │  19.8046 │      5.9669 │     36.0936 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ ...                 │     ... │      ... │         ... │         ... │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 19:00:00 │ 81.0000 │  67.3204 │     31.5294 │    108.5078 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 20:00:00 │ 81.0000 │  40.3452 │     20.0169 │     82.1894 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 21:00:00 │ 81.0000 │  29.3831 │     13.5137 │     61.1140 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 22:00:00 │ 81.0000 │  19.5810 │     11.1901 │     42.4820 │
├─────────────────────┼─────────┼──────────┼─────────────┼─────────────┤
│ 2012-12-31 23:00:00 │ 81.0000 │  16.1096 │      8.8776 │     31.6928 │
└─────────────────────┴─────────┴──────────┴─────────────┴─────────────┘
                          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', │
│                   │ 'max'], '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)', 'max(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 core dynamics for hourly bike      
  rentals:                                                                              
                                                                                        
   1 Short-term trend (last few hours): Lags 1–6 capture the immediate recent trend —   
     the last few hours of demand, including the build-up and decay of rush-hour        
     peaks.                                                                             
   2 Daily rhythm (24-hour cycle): Lags 23, 24, 25 capture the same hour yesterday and  
     its immediate neighbours, directly encoding the strong daily seasonality. A        
     rolling mean and std over a 24-hour window also summarise yesterday's average      
     level and variability (e.g., how volatile demand was over the past day), which     
     helps the model adapt to different day types.                                      
   3 Weekly rhythm (weekday vs. weekend): Lags 47, 48, 49 (±1 around 48 h = 2 days      
     ago) and lags 167, 168, 169 (±1 around 168 h = exactly one week ago) let the       
     model directly compare the same hour from the previous week and the day before     
     yesterday. A rolling mean and max over the full 168-hour week summarise the        
     weekly baseline level and the peak demand observed in the past week — critical     
     for distinguishing weekday commuter spikes from weekend leisure patterns.          
                                                                                        
  This set avoids redundancy (no consecutive lags beyond the short-term window) while   
  covering all three seasonality scales the user identified. The window features        
  complement the lags by providing smooth, compact summaries at the daily and weekly    
  scales without requiring dozens of additional lag columns.                            
                                                                                        
  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', 'max'],                                        
    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]
# Ranked leaderboard
# ==============================================================================
results_compare.results
rank name forecaster estimator mean_absolute_error mean_squared_error mean_absolute_scaled_error mean_absolute_percentage_error error
0 1 ForecasterRecursive ForecasterRecursive LGBMRegressor 46.584343 5443.171132 0.754003 0.470311 None
1 2 ForecasterDirect ForecasterDirect LGBMRegressor 49.020298 5848.934447 0.793430 0.497920 None
2 3 ForecasterFoundation ForecasterFoundation Chronos-2 NaN NaN NaN NaN ImportError: chronos-forecasting >=2.0 is requ...
# 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: 'ForecasterRecursive'                      
  (ForecasterRecursive / LGBMRegressor) = 46.5843, 5.0% ahead of 'ForecasterDirect'     
  (49.0203). 1 configuration failed to run and is ranked last.                          
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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]
config.json:   0%|          | 0.00/969 [00:00<?, ?B/s]
model.safetensors:   0%|          | 0.00/112M [00:00<?, ?B/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 refined_plan ForecasterRecursive LGBMRegressor 50.302476 0.820406
2 3 lgbm_direct ForecasterDirect LGBMRegressor 50.380925 0.821734
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_ai/lib/python3.13/site-packages/skforecast_a 
 i/_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) is the clear winner, and the margin over the         
  second-place candidate is meaningful rather than negligible. With a MAE of 38.44      
  versus 50.30 for the refined_plan (ForecasterRecursive with LGBMRegressor), the gap   
  is substantial in absolute terms. The MASE values reinforce this: Chronos-2 scores    
  0.597, while refined_plan scores 0.820.                                               
                                                                                        
  Key observations from the leaderboard                                                 
                                                                                        
  foundation_model (Chronos-2): MAE 38.44, MASE 0.597                                
  refined_plan (LGBMRegressor, recursive): MAE 50.30, MASE 0.821                     
  lgbm_direct (LGBMRegressor, direct): MAE 50.38, MASE 0.822                         
  lgbm_daily_lags (LGBMRegressor, recursive): MAE 54.95, MASE 0.896                  
  ridge_baseline (Ridge, recursive): MAE 93.15, MASE 1.519                           
                                                                                        
  On the gap between positions 1 and 2                                                  
                                                                                        
  The separation between Chronos-2 and the best ML candidate is notable. Both           
  candidates were evaluated under identical cross-validation conditions: 82 folds, a    
  fixed training window, a 36-step horizon, and no refit. That controlled setup makes   
  the comparison fair and the gap credible, not an artifact of different evaluation     
  conditions.                                                                           
                                                                                        
  Between positions 2, 3, and 4                                                         
                                                                                        
  The two LGBMRegressor candidates in second and third place (refined_plan and          
  lgbm_direct) are practically equivalent. Their MAE values are 50.30 and 50.38, and    
  their MASE values are 0.820 and 0.822. Those differences are negligible, and no       
  meaningful practical distinction can be drawn between the recursive and direct        
  strategies here.                                                                      
                                                                                        
  The lgbm_daily_lags variant (MAE 54.95, MASE 0.896) sits a bit further back from      
  those two, but all three LGBM candidates beat the naive baseline because their MASE   
  values are below 1.0.                                                                 
                                                                                        
  On the ridge baseline                                                                 
                                                                                        
  The Ridge model (MASE 1.519) fails to beat the naive baseline, since a MASE above     
  1.0 means the naive baseline would have produced smaller errors. It serves its        
  intended role as a sanity-check lower bound, confirming that the more capable models  
  are adding real value.                                                                
                                                                                        
  Summary                                                                               
                                                                                        
  The foundation model leads by a meaningful margin. The two top LGBM variants are      
  essentially tied with each other. All ML candidates except Ridge beat the naive       
  baseline, and Ridge confirms the floor below which no useful model should fall.       
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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.6% ahead of 'refined_plan' (50.3025).                      
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯
                                    Comparison Results                                    
┏━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Index  rank          name    forecaster     estimator  mean_absol…  mean_absolu… ┃
┡━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ 0     │    1 │ foundation_… │ ForecasterF… │    Chronos-2 │     38.4362 │       0.5975 │
├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤
│ 1     │    2 │ refined_plan │ ForecasterR… │ LGBMRegress… │     50.3025 │       0.8204 │
├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤
│ 2     │    3 │  lgbm_direct │ ForecasterD… │ LGBMRegress… │     50.3809 │       0.8217 │
├───────┼──────┼──────────────┼──────────────┼──────────────┼─────────────┼──────────────┤
│ 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 ──────────────────────────────────╮
                                                                                        
  The direct strategy trains one independent model per forecast step, so it avoids the  
  error propagation that builds up when a recursive model feeds its own predictions     
  back as inputs. For hourly demand with strong daily and weekly seasonality, the       
  direct approach becomes preferable when the predictive relationship changes           
  meaningfully across the horizon, for example when the features that best explain      
  demand at hour 1 ahead differ substantially from those that explain demand at hour    
  24 or hour 168 ahead.                                                                 
                                                                                        
  Specific situations that favour ForecasterDirect                                      
                                                                                        
   • Long horizons relative to the seasonal cycle. When you need to forecast 24 or 48   
     hours ahead, recursive predictions accumulate errors over many steps. A direct     
     model at step 24 is trained directly against the 24-hour-ahead target, so it is    
     not penalised by errors at steps 1 through 23.                                     
   • Step-specific seasonal patterns. Peak morning demand and late-evening troughs may  
     be driven by different lag relationships. A direct model at each hour can learn    
     those hour-specific dynamics independently, whereas a single recursive model must  
     compromise across all steps.                                                       
   • Horizon-dependent exogenous features. If you have exogenous variables such as      
     weather forecasts or calendar flags whose predictive value varies by how far       
     ahead you are forecasting, direct models can weight them differently at each       
     step.                                                                              
   • When recursive error propagation is measurable. The practical test is backtesting  
     with backtesting_forecaster using TimeSeriesFold. If the recursive model's         
     accuracy degrades notably at the far end of the horizon but the direct model       
     holds up, that is the empirical signal to prefer direct.                           
                                                                                        
  Situations where recursive is still competitive                                       
                                                                                        
   • Short horizons (a few steps). Error propagation is small when you only need a few  
     steps ahead, so the simplicity and speed of a single model often wins.             
   • Limited training data. Fitting N separate models (one per step) multiplies the     
     effective model count. With scarce data, a single recursive model may generalise   
     better.                                                                            
   • When strong lag structure dominates. If the 24-lag and 168-lag values are          
     overwhelmingly predictive at every horizon, a single recursive model captures      
     that structure efficiently.                                                        
                                                                                        
  Practical skforecast guidance                                                         
                                                                                        
  Use ForecasterDirect with the steps parameter set to your full horizon (for        
     example 24 or 48) at initialisation.                                               
  Enable n_jobs=-1 to parallelise the N model fits, which offsets the training time  
     cost.                                                                              
  Per-series lag dicts are not available in ForecasterDirect, but you can pass       
     different lag arrays to the single lags parameter.                                 
  Run backtesting_forecaster with both ForecasterRecursive and ForecasterDirect      
     under the same TimeSeriesFold configuration and compare MASE or RMSSE values. A    
     MASE below 1 confirms the model beats the naive baseline; the leaderboard result   
     tells you which strategy is relatively stronger.                                   
                                                                                        
  The bottom line is: for hourly demand at horizons of 24 hours or longer, the direct   
  strategy is worth testing, and the empirical comparison via backtesting is the most   
  reliable way to decide.                                                               
                                                                                        
╰────────────────────────────────────────────────────────────────────────────────────────╯

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)
-----
matplotlib          3.11.0
pandas              2.3.3
plotly              6.8.0
session_info        v1.0.1
skforecast          0.23.0
skforecast_ai       0.2.0
-----
IPython             9.15.0
jupyter_client      8.9.1
jupyter_core        5.9.1
-----
Python 3.13.14 | packaged by conda-forge | (main, Jun 12 2026, 09:50:25) [GCC 14.3.0]
Linux-7.0.0-1010-aws-x86_64-with-glibc2.43
-----
Session information updated at 2026-08-21 08:44

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

If you use skforecast 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.