More about forecasting in cienciadedatos.net
- Time series forecasting with machine learning
- ARIMA and SARIMAX models
- Time series forecasting with gradient boosting
- Exponential smoothing models
- ARAR forecasting models
- Visualizing time series data
- Electricity demand forecasting
- Global forecasting models I
- Global forecasting models II
- Global forecasting models III
- Clustering time series
- Forecasting with deep learning models
- Probabilistic forecasting I
- Probabilistic forecasting II
- Probabilistic forecasting III
- Data drift detection in time series forecasting models
- Forecasting categorical time series
- Interpretability and explainability in forecasting models
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
skforecastscript 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-guidedrefine_plan()orcreate_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
skforecastcode.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.
The ask() method is available in both workflows. It can explain a profile, plan, validation setup, backtest result, or answer general forecasting questions, but it will never execute the workflow or modify your parameters without explicit instruction.
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
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__}")
Version skforecast_ai: 0.1.0 Version skforecast: 0.23.0
✏️ 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-flash-preview"
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 Path¶
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 │ ├────────────────┼────────────────────────┤ │ Missing target │ none │ └────────────────┴────────────────────────┘ Recommendation ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Forecaster candidates │ ForecasterRecursive, ForecasterDirect, ForecasterFoundation, │ │ │ ForecasterStats │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Estimator candidates │ LGBMRegressor, XGBRegressor, Ridge │ └───────────────────────┴────────────────────────────────────────────────────────────────┘ ╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ A single-series ML forecaster (ForecasterRecursive) is recommended. Data: 17544 │ │ observations, 'h' frequency. Alternative forecasters: ['ForecasterDirect', │ │ 'ForecasterFoundation', 'ForecasterStats']. 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()
╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ Forecaster & Estimator Recommendation Explained │ │ │ │ Why ForecasterRecursive? │ │ │ │ For your 17,544-observation hourly bike-sharing series, ForecasterRecursive is the │ │ natural starting point. It trains a single model that learns to predict demand one │ │ step ahead, then feeds its own predictions forward to cover all 36 future hours. Key │ │ reasons it fits here: │ │ │ │ • Sufficient data: With ~2 years of hourly observations, there's ample history to │ │ train a robust lag-based model without overfitting. │ │ • Flexibility: You don't need step-specific models — demand patterns at hour +1 and │ │ hour +36 can reasonably be captured by the same underlying relationships (recent │ │ usage, time of day, weather). │ │ • Full feature support: Rolling window features, calendar features, and exogenous │ │ variables all work natively, which is important given your data's clear daily and │ │ weekly seasonality. │ │ │ │ ForecasterDirect remains a listed alternative if you later find that short-horizon │ │ and long-horizon predictions benefit from fundamentally different feature │ │ relationships, but that adds training complexity without a guaranteed gain here. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Why LGBMRegressor? │ │ │ │ LightGBM (gradient boosting on decision trees) is the preferred estimator at this │ │ scale for several practical reasons: │ │ │ │ • Handles 17,544 rows efficiently: LightGBM is specifically designed for fast │ │ training on medium-to-large tabular datasets, far outpacing Random Forest or deep │ │ learning in wall-clock time here. │ │ • Native categorical support: Your weather variable is categorical. LightGBM │ │ handles this internally without one-hot expansion, which reduces noise and speeds │ │ up training — this is why categorical_features='auto' is set in the plan. │ │ • NaN tolerance: LightGBM natively handles missing values in lag/window features │ │ that arise at the start of the training window. This is why rows with NaN are │ │ kept rather than dropped. │ │ • Strong out-of-the-box performance: Gradient boosting captures nonlinear │ │ interactions (e.g., temperature matters more on weekends, or holidays suppress │ │ demand regardless of weather) without manual feature crossing. │ │ │ │ XGBoost and Ridge are listed as alternatives — Ridge would serve as a simpler linear │ │ baseline, while XGBoost is broadly comparable to LightGBM. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ What the Exogenous Variables Add │ │ │ │ Your three exogenous variables — holiday, weather, and temp — contribute information │ │ that pure lag history cannot capture: │ │ │ │ │ │ Variable Type What it contributes │ │ ────────────────────────────────────────────────────────────────────────────────── │ │ holiday Likely binary Demand on public holidays may collapse or shift even when │ │ recent usage was high — the model can't infer this from │ │ yesterday's counts alone │ │ weather Categorical Rainy or stormy conditions are associated with suppressed │ │ ridership regardless of the usual hourly pattern │ │ temp Numeric Temperature has a well-known nonlinear relationship with │ │ cycling demand (too cold or too hot both reduce usage) │ │ │ │ │ │ Together, they act as contextual signals that explain demand departures from what │ │ the lag/seasonal pattern alone would predict. Importantly, for predict() to use │ │ these, you must supply their known or forecast values across all 36 future steps — │ │ they cannot be inferred from the series itself. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ How the Features Work Together │ │ │ │ The plan combines three complementary information sources: │ │ │ │ 1 Lags (including lags at 24, 168 hours): Capture the immediate recent trend, the │ │ same hour yesterday, and the same hour last week — directly encoding daily and │ │ weekly seasonality. │ │ 2 Rolling window features (3h, 24h, 168h mean/std): Summarise short-term momentum, │ │ daily average level, and weekly average level as smoothed context signals. │ │ 3 Calendar features (hour, day_of_week, weekend, month): Give the model explicit │ │ awareness of when it is, complementing what the lags imply implicitly. │ │ 4 Exogenous variables: Add external context (weather, holidays, temperature) that │ │ is unknowable from the series history. │ │ │ │ LightGBM's tree structure is particularly well-suited to learning interactions │ │ across all these feature types — for example, automatically discovering that hour=8 │ │ AND day_of_week=weekday AND weather=clear predicts a morning commute surge. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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 ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ 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 │ - handle_categorical_exog: Categorical exogenous variables │ │ │ detected: ['weather']. These are handled automatically by │ │ │ skforecast (categorical_features='auto'). (optional) │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── 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()
╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ Forecasting Plan Walkthrough │ │ │ │ Overview │ │ │ │ You have 17,544 hourly observations of a users series — roughly two years of data. │ │ The plan uses a ForecasterRecursive with LightGBM to predict 36 steps ahead (36 │ │ hours), with an 80% prediction interval around each point forecast. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Why These Lags? │ │ │ │ The lag set [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] was selected via │ │ autocorrelation analysis (PACF). Each lag represents a past hour that carries │ │ statistically meaningful partial autocorrelation with the target. There are three │ │ natural clusters: │ │ │ │ Short-range lags (1–33 hours) │ │ │ │ • Lags 1–3: Immediate recent behaviour — what happened in the last 1–3 hours is │ │ almost always predictive. │ │ • Lags 5, 8, 10, 15: Shorter within-day dependencies, possibly capturing shift │ │ patterns or gradual trends. │ │ • Lags 19–26: These bracket the 24-hour daily cycle. Users at 2pm today are likely │ │ similar to users at 2pm yesterday. The cluster around lag 24 is the most │ │ important daily seasonal anchor. │ │ • Lags 32–33: Just over a day back — capturing day-over-day momentum with a slight │ │ offset (e.g., yesterday plus the previous partial day). │ │ │ │ Weekly seasonal lags (~119–169 hours) │ │ │ │ • Lags 119–169 cluster around 168 hours (7 days). User behaviour on Monday at 9am │ │ is strongly predictive of Monday at 9am next week. The spread around 168 accounts │ │ for slightly asymmetric weekly patterns (e.g., Friday afternoons behave │ │ differently from Monday mornings at the same hour). │ │ │ │ Bi-weekly lags (~313–337 hours) │ │ │ │ • Lags 313–337 sit around 336 hours (two weeks). These capture fortnightly cycles — │ │ common in usage data driven by pay cycles, bi-weekly meetings, or recurring │ │ events. With 17,544 observations (~2 years), there is enough data for the model │ │ to reliably estimate these long-range dependencies. │ │ │ │ ▌ LightGBM handles a large, sparse lag set efficiently — it will assign near-zero │ │ ▌ importance to any lags that turn out not to be useful, so including candidate │ │ ▌ lags is low-risk. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Why These Window Features? │ │ │ │ Rolling (window) features summarise the recent history of the series into a small │ │ number of meaningful statistics. Three scales are used: │ │ │ │ │ │ Feature What it captures │ │ ────────────────────────────────────────────────────────────────────────────────── │ │ mean(window=3) The very recent local level — has usage been rising or falling │ │ in the last 3 hours? │ │ std(window=3) Short-term volatility — is usage fluctuating or stable right │ │ now? │ │ mean(window=24) The typical level over the last full day — a smooth daily │ │ baseline that absorbs noise from individual lags. │ │ mean(window=168) The typical level over the last full week — the long-run weekly │ │ baseline, useful for detecting week-over-week shifts. │ │ │ │ │ │ These four features complement the lags in an important way: the lags capture │ │ specific past time points, while the rolling means capture smoothed levels across a │ │ range. Together they give LightGBM both fine-grained and coarse-grained views of the │ │ series history. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ The Exogenous Variables │ │ │ │ Three external variables are included: │ │ │ │ • holiday: Whether the current hour falls on a holiday. User patterns on holidays │ │ often resemble weekends but deviate from the day-of-week calendar feature. │ │ • weather: A categorical variable (e.g., sunny/rainy/snowy). Skforecast detects │ │ this automatically via categorical_features='auto' and passes it to LightGBM as a │ │ native categorical — no one-hot encoding needed. │ │ • temp: Temperature, a continuous variable that likely has a nonlinear relationship │ │ with users (e.g., very cold or very hot temperatures suppressing outdoor │ │ activity). │ │ │ │ ▌ These variables must be provided for all 36 future hours when calling predict(). │ │ ▌ The plan assumes you have forecasts or known values for these variables at │ │ ▌ prediction time. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Calendar Features │ │ │ │ Four calendar features are added using raw ordinal encoding (no cyclical sin/cos │ │ transformation): hour, day_of_week, weekend, and month. LightGBM's tree-based splits │ │ handle ordinal integers well and can discover the natural boundaries (e.g., hour 8 │ │ vs. hour 9, weekday vs. weekend) directly from data — cyclical encoding is more │ │ important for linear models. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ How the 80% Prediction Interval Is Produced │ │ │ │ The interval [0.1, 0.9] targets the 10th and 90th percentiles of the forecast │ │ distribution — meaning approximately 80% of future observations should fall within │ │ the band. │ │ │ │ The method is bootstrapping, which works as follows: │ │ │ │ Step 1 — Store in-sample residuals │ │ │ │ During fit(), the model predicts each training observation and stores the errors │ │ (residuals). These residuals represent the distribution of mistakes the model makes, │ │ indexed by the magnitude of the prediction (using use_binned_residuals=True). │ │ Binning by prediction level is important: if the model makes larger errors when │ │ users is high (heteroscedasticity), the intervals will be wider at those levels and │ │ narrower when usage is low. │ │ │ │ Step 2 — Bootstrap multi-step paths │ │ │ │ At prediction time, n_boot=500 simulated forecast trajectories are generated. For │ │ each trajectory: │ │ │ │ 1 A residual is drawn (at random, from the appropriate bin for the current │ │ predicted level). │ │ 2 That residual is added to the point forecast to create a perturbed value. │ │ 3 The perturbed value is fed back as if it were an observation to produce the next │ │ step. │ │ │ │ This propagates uncertainty across all 36 steps: errors compound over the horizon, │ │ so the intervals naturally widen the further out you forecast. │ │ │ │ Step 3 — Derive quantiles │ │ │ │ After 500 trajectories, each future hour has a distribution of 500 simulated values. │ │ The 10th percentile of that distribution becomes lower_bound and the 90th percentile │ │ becomes upper_bound. │ │ │ │ What the output looks like │ │ │ │ Each of the 36 forecast rows will contain three columns: │ │ │ │ • pred — the point forecast (LightGBM's direct prediction) │ │ • lower_bound — the 10th percentile (80% interval lower edge) │ │ • upper_bound — the 90th percentile (80% interval upper edge) │ │ │ │ A note on calibration │ │ │ │ An 80% interval should contain the true value roughly 80% of the time. In practice │ │ this depends on whether the stored residuals are representative of future errors. │ │ Using use_binned_residuals=True improves calibration by matching residual size to │ │ prediction magnitude, but it is worth checking empirically — the backtesting step in │ │ the script will compute coverage so you can verify this directly. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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 aslags,estimator,estimator_kwargs,forecaster,steps,interval, orwindow_features. Only the fields you explicitly specify are updated; the rest of the configuration is deterministically re-derived from the original plan.LLM mode (
promptprovided): describe your domain knowledge in natural language. The LLM interprets this context and suggests appropriatelagsandwindow_features. Its reasoning is appended toplan.explanationand the changed fields are recorded inplan.llm_refined_fieldsfor 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 ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ 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 │ - handle_categorical_exog: Categorical exogenous variables │ │ │ detected: ['weather']. These are handled automatically by │ │ │ skforecast (categorical_features='auto'). (optional) │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── 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 ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ 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 │ - handle_categorical_exog: Categorical exogenous variables │ │ │ detected: ['weather']. These are handled automatically by │ │ │ skforecast (categorical_features='auto'). (optional) │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── 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 key dynamics for hourly bike │ │ rentals: │ │ │ │ 1 "The last few hours give a good sense of the current trend" → Short lags [1, 2, │ │ 3, 4, 5, 6] capture the immediate momentum and direction of demand (e.g., a │ │ commute ramp-up in progress). │ │ 2 "Clear daily rhythm with rush-hour peaks" → Lags around 24 h [23, 24, 25] and 48 │ │ h [47, 48, 49] anchor the model to the same hour on the previous one and two │ │ days, directly encoding the intra-day cycle. The ±1 neighbours (23/25, 47/49) add │ │ robustness for slight timing shifts in peaks. │ │ 3 "Usually similar to what happened at the same time last week" → Lags around 168 h │ │ [167, 168, 169] capture the same hour from exactly one week ago, accounting for │ │ the weekday/weekend rhythm the user highlighted. Again, ±1 neighbours provide │ │ robustness. │ │ 4 Rolling window — 24 h mean + std: The 24-hour mean smooths over the full daily │ │ cycle and gives the model a sense of the current day's overall demand level. The │ │ 24-hour std captures intra-day volatility (e.g., a rainy day suppresses peaks), │ │ which is useful for a demand series driven by weather and events. │ │ 5 Rolling window — 168 h mean + max: The weekly mean gives the baseline level for │ │ the current week (distinguishing a typical weekday week from a holiday week), │ │ while the weekly max captures whether any unusually high peak occurred recently — │ │ a signal relevant for capacity and planning in bike-share systems. Together these │ │ two window sizes (one short/reactive, one long/trend) follow the multi-scale best │ │ practice without over-engineering the feature set. │ │ │ │ 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()
╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ Refined Plan vs. Original: What Changed and Why It Matters │ │ │ │ Summary of Changes │ │ │ │ The refinement moved from a generic default configuration to one tailored │ │ specifically to the dynamics of hourly bike-rental demand. Here are the key │ │ differences: │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ 1. Lags: Generic → Seasonality-Aware │ │ │ │ │ │ Original (default) Refined │ │ ────────────────────────────────────────────────────────────────────────────────── │ │ Lags Typically a simple contiguous block [1, 2, 3, 4, 5, 6, 23, 24, 25, 47, 48, │ │ (e.g., 1–24) 49, 167, 168, 169] │ │ │ │ │ │ Why it matters: A contiguous block like 1–24 wastes capacity on lags 7–22 (mid-day │ │ history) that carry little signal compared to the structural anchors of bike demand: │ │ the same hour yesterday, two days ago, and last week. │ │ │ │ The refined set targets three distinct temporal patterns you described: │ │ │ │ • Lags 1–6: Capture the current trend/momentum — whether demand is ramping up │ │ (morning commute) or tapering off. │ │ • Lags 23–25 and 47–49: Anchor the model to the same clock-hour from the previous │ │ one and two days, directly encoding the daily commute rhythm. The ±1 neighbours │ │ (e.g., 23 and 25 alongside 24) make the model robust to slight timing shifts in │ │ peaks. │ │ • Lags 167–169: Encode the same hour from exactly one week ago, capturing the │ │ weekday/weekend contrast you highlighted. This is the single most important │ │ structural feature for a weekly-seasonal series. │ │ │ │ The net effect is fewer lags but higher information density — the model sees the │ │ right history rather than redundant intermediate history. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ 2. Window Features: Simple → Multi-Scale │ │ │ │ │ │ Original (default) Refined │ │ ────────────────────────────────────────────────────────────────────────────── │ │ Windows Possibly a single rolling mean 24-h mean + std, and 168-h mean + max │ │ │ │ │ │ Why it matters: Two scales working together: │ │ │ │ • 24-h mean: Tells the model the overall demand level for the current day — is │ │ today a busy day or a quiet one? This smooths over individual hour noise. │ │ • 24-h std: Captures intra-day volatility. A high std suggests strong peak/trough │ │ contrast (typical dry weekday); a low std suggests suppressed, flat demand (rainy │ │ day, holiday). This interacts naturally with your weather and holiday exogenous │ │ variables. │ │ • 168-h mean: Gives the model the baseline demand level for the current week as a │ │ whole, helping distinguish a normal weekday week from a holiday-affected week. │ │ • 168-h max: Flags whether an unusually high peak occurred recently — relevant for │ │ understanding capacity constraints and demand surges in bike-share contexts. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ 3. Everything Else Stayed the Same (and That's Intentional) │ │ │ │ • LGBMRegressor: Correct choice for 17,544 observations. Handles the categorical │ │ weather variable natively via categorical_features='auto'. Fast to train and │ │ tune. │ │ • Exogenous variables (holiday, weather, temp): All three are included. weather is │ │ automatically encoded as a categorical feature. │ │ • Calendar features (hour, day_of_week, weekend, month): Added as raw ordinal │ │ features — these are critical for a series with strong time-of-day and │ │ day-of-week patterns. │ │ • 80% prediction interval (0.1–0.9) via bootstrapping: Appropriate for operational │ │ planning where you want to communicate a credible demand range, not just a point │ │ estimate. │ │ • MAE as the primary metric: Interpretable in the same units as users, robust to │ │ the demand spikes that are common in bike-rental data. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Important Caveat │ │ │ │ ▌ The lag and window feature choices are hypotheses grounded in domain reasoning, │ │ ▌ not validated improvements. The actual accuracy gain over a simpler default │ │ ▌ configuration will only be confirmed once backtesting results are in hand. If │ │ ▌ MAE from backtesting does not improve over a simpler lag set, consider running a │ │ ▌ hyperparameter search that includes lags in the search space. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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_sizeis 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 nextstepstime 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 viaexog.
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.",
forecast_result = results_eval
)
answer.show_explanation()
╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ Forecast Results: Hourly Bike Rentals │ │ │ │ Overall Model Quality │ │ │ │ The model performs well above the naive baseline, which is the most important │ │ finding. The key metrics from backtesting are: │ │ │ │ │ │ Metric Value Interpretation │ │ ────────────────────────────────────────────────────────────────────────────────── │ │ MAE 39.99 users On average, predictions are off by ~40 users per hour │ │ MASE 0.621 62% of the naive forecast error — meaningfully better than │ │ simply repeating the last observed value │ │ MSE 3,680 Reflects occasional larger errors (e.g., during unusual │ │ demand spikes) │ │ MAPE 48.2% Should be interpreted cautiously — high percentage errors │ │ often arise during low-demand hours (e.g., 2 AM with 5 actual │ │ users vs. 8 predicted = 60% error) │ │ │ │ │ │ The MASE of 0.621 is the headline result: any value below 1.0 means the model beats │ │ the naive "repeat yesterday's value" benchmark. A score of 0.62 represents a solid, │ │ practically useful model for an hourly bike-share demand series. │ │ │ │ The gap between MAE (~40) and the square root of MSE (~61) suggests the model makes │ │ occasional larger errors, likely during rush-hour peaks or weather-driven demand │ │ swings that are harder to predict precisely. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Prediction Interval Quality │ │ │ │ An 80% prediction interval was generated using bootstrapping ([0.1, 0.9] quantiles). │ │ This means roughly 8 out of 10 actual observations should fall between the lower and │ │ upper bounds. │ │ │ │ Looking at the interval widths: │ │ │ │ • Average interval width: ~66 users (105.9 − 39.3 mean bounds) │ │ • At peak hours (e.g., 12:00–16:00 on Dec 30): bounds span roughly 75 users (105 to │ │ 180), reflecting genuine uncertainty during high-demand periods │ │ • At low-demand hours (e.g., 23:00 on Dec 31): bounds narrow to ~33 users (8 to │ │ 41), which makes intuitive sense — late-night demand is more predictable and │ │ lower in magnitude │ │ │ │ The lower bound never goes below ~0.5, which is appropriate for a count-like │ │ variable. The bootstrapping method correctly captures asymmetric uncertainty — the │ │ upper bound tends to extend further above the point forecast than the lower bound │ │ does below it, reflecting that demand can spike upward more easily than it can go │ │ negative. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Forecast Pattern (Dec 30–31) │ │ │ │ The predictions tell a coherent story that aligns with known bike-share behaviour: │ │ │ │ December 30 (Sunday afternoon) │ │ │ │ • Forecasts start at ~146 users at noon and hold relatively stable through the │ │ afternoon (133–137 range from 13:00–16:00), consistent with leisure weekend │ │ riding patterns rather than sharp commuter peaks. │ │ │ │ December 31 (New Year's Eve) │ │ │ │ • Demand drops sharply in the evening: from a moderate daytime level down to ~62 │ │ users at 19:00, then rapidly declining to ~43 → 32 → 21 → 17 users at 23:00. │ │ • This pattern is plausible: New Year's Eve riders likely shift to other transport │ │ modes for celebrations, and cold late-December conditions (captured via temp and │ │ weather exogenous variables) may additionally suppress demand. │ │ │ │ Summary statistics across all 36 steps: │ │ │ │ • Mean forecast: 74 users/hour │ │ • Range: 5 to 153 users/hour │ │ • This spread is consistent with a series transitioning from an active afternoon to │ │ a quiet New Year's Eve night. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Feature Design and Its Effect │ │ │ │ The lag and window structure directly shaped these results: │ │ │ │ • Short lags [1–6] allowed the model to track the current demand trend in real │ │ time, which matters most during transitions (e.g., the evening decline on Dec │ │ 31). │ │ • 24h and 48h lags [23–25, 47–49] anchored predictions to the same hour on prior │ │ days, capturing the daily rhythm without which MAPE would be far higher. │ │ • 168h lags [167–169] encoded the weekday/weekend distinction — critical for a │ │ series where Sunday ridership differs structurally from Monday. │ │ • Rolling mean/std (24h) provided the model with a sense of the current day's │ │ overall demand level and volatility, which is especially valuable given the │ │ weather and holiday exogenous variables. │ │ • Rolling mean/max (168h) gave the model a weekly baseline and a signal for whether │ │ any unusual peak occurred recently. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Caveats │ │ │ │ • MAPE (48%) appears high but is largely an artifact of low-demand hours where │ │ small absolute errors become large percentages. MAE and MASE are more reliable │ │ guides here. │ │ • The forecasts cover only Dec 30–31, which is a holiday period. Model performance │ │ on typical weekdays may differ. │ │ • The 80% interval calibration should be verified: if actual coverage deviates │ │ significantly from 80%, the intervals may be over- or under-confident. This can │ │ be checked using calculate_coverage on the backtesting predictions. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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 │ ├────────────────┼────────────────────────┤ │ Missing target │ none │ └────────────────┴────────────────────────┘ Recommendation ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Forecaster candidates │ ForecasterRecursive, ForecasterDirect, ForecasterFoundation, │ │ │ ForecasterStats │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Estimator candidates │ LGBMRegressor, XGBRegressor, Ridge │ └───────────────────────┴────────────────────────────────────────────────────────────────┘ ╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ A single-series ML forecaster (ForecasterRecursive) is recommended. Data: 17544 │ │ observations, 'h' frequency. Alternative forecasters: ['ForecasterDirect', │ │ 'ForecasterFoundation', 'ForecasterStats']. 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 ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ 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 │ - handle_categorical_exog: Categorical exogenous variables │ │ │ detected: ['weather']. These are handled automatically by │ │ │ skforecast (categorical_features='auto'). (optional) │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── 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 key dynamics for hourly bike │ │ rentals: │ │ │ │ 1 "The last few hours give a good sense of the current trend" → Short lags [1, 2, │ │ 3, 4, 5, 6] capture the immediate momentum and direction of demand (e.g., a │ │ commute ramp-up in progress). │ │ 2 "Clear daily rhythm with rush-hour peaks" → Lags around 24 h [23, 24, 25] and 48 │ │ h [47, 48, 49] anchor the model to the same hour on the previous one and two │ │ days, directly encoding the intra-day cycle. The ±1 neighbours (23/25, 47/49) add │ │ robustness for slight timing shifts in peaks. │ │ 3 "Usually similar to what happened at the same time last week" → Lags around 168 h │ │ [167, 168, 169] capture the same hour from exactly one week ago, accounting for │ │ the weekday/weekend rhythm the user highlighted. Again, ±1 neighbours provide │ │ robustness. │ │ 4 Rolling window — 24 h mean + std: The 24-hour mean smooths over the full daily │ │ cycle and gives the model a sense of the current day's overall demand level. The │ │ 24-hour std captures intra-day volatility (e.g., a rainy day suppresses peaks), │ │ which is useful for a demand series driven by weather and events. │ │ 5 Rolling window — 168 h mean + max: The weekly mean gives the baseline level for │ │ the current week (distinguishing a typical weekday week from a holiday week), │ │ while the weekly max captures whether any unusually high peak occurred recently — │ │ a signal relevant for capacity and planning in bike-share systems. Together these │ │ two window sizes (one short/reactive, one long/trend) follow the multi-scale best │ │ practice without over-engineering the feature set. │ │ │ │ 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 │ └─────────────────────┴─────────┴─────────────┴─────────────┘
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()
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:
Explicit instantiation (recommended): manually construct a
TimeSeriesFoldand pass it directly tobacktest(). Use this when you already know your exact operational constraints.Deterministic
create_cv(): allow the assistant to derive a sensibleTimeSeriesFoldfrom the profile and plan using rule-based defaults. You can override individual parameters explicitly.LLM
create_cv()(with a prompt): describe your deployment use case in natural language. The LLM translates your description into a fully-configuredTimeSeriesFold, 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
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
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
# LLM reasoning behind the TimeSeriesFold configuration
# ==============================================================================
print(textwrap.fill(cv_llm_explanation, width=88))
The user wants to forecast bike demand 36 hours ahead with a single, fixed training run. - **initial_train_size = "2012-08-31 23:59"**: The model is trained once on all data up to the end of August 2012, exactly as specified by the user. A date string is used to pin the cutoff precisely. - **refit = False**: The user explicitly stated the model should NOT be retrained as the window rolls forward. A single fit on the initial training set is used for all subsequent test folds. - All other parameters are left at their defaults (expanding window, no gap, no stride override, etc.), as the user did not specify any additional constraints. 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]
╭───────────────────────────────────── 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?"
),
backtest_result = results_backtest
)
answer.show_explanation()
╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ Backtesting Results: Hourly Bike Rental Demand Forecasting │ │ │ │ Strategy Overview │ │ │ │ The backtesting simulated how the model would perform in production by walking │ │ forward through the data from September 1 to December 31, 2012, producing │ │ 36-hour-ahead forecasts at each step (82 folds in total, fold 0 through fold 81). │ │ │ │ Key configuration choices: │ │ │ │ • refit=False: The model was trained once on all data up to August 31, 2012, and │ │ then evaluated without retraining. This tests how well a single trained model │ │ generalises over a ~4-month deployment window — a realistic "deploy once" │ │ scenario. │ │ • fixed_train_size=True: The training window was fixed (not expanding), consistent │ │ with refit=False having no effect on window growth when the model is never │ │ retrained. │ │ • gap=0: Forecasts begin immediately after the last observed value — no ingestion │ │ delay is assumed. │ │ • fold_stride=36: Each 36-step forecast block is non-overlapping, so every test │ │ observation is evaluated exactly once. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Metric Interpretation │ │ │ │ │ │ Metric Value Interpretation │ │ ───────────────────────────────────────────────────────────────────────── │ │ MAE 50.3 users On average, predictions are off by ~50 users per hour │ │ MSE 6,911 Sensitive to large errors; RMSE ≈ 83 users │ │ MASE 0.820 Better than a naïve seasonal forecast (< 1.0) ✅ │ │ MAPE 55.5% High — caution warranted (see note below) │ │ │ │ │ │ What stands out │ │ │ │ MASE of 0.82 is the most meaningful number here. It tells you the model makes 18% │ │ fewer errors than simply repeating the value from the same hour the previous day. │ │ For an hourly demand series with complex weather and event effects, this is a solid │ │ result. │ │ │ │ MAPE of 55.5% is misleadingly high. This inflated figure almost certainly comes from │ │ the overnight/early-morning hours (1am–5am) where true demand is very low (near │ │ zero). Small absolute errors become enormous percentage errors when the denominator │ │ is tiny — for example, predicting 5 users when 3 actually showed up gives a 67% │ │ error despite a 2-user absolute miss. This is a known pathology of MAPE on │ │ intermittent or low-count data. Do not use MAPE as the primary decision metric here. │ │ │ │ MAE of ~50 users should be judged against the demand range. The predictions span │ │ roughly 3 to 867 users (mean ~234), so a 50-user average error represents about 21% │ │ of the mean demand — reasonable for a 36-hour horizon with real-world variability in │ │ weather and holidays. │ │ │ │ The negative lower bound (min = −0.94) in a handful of predictions is a minor │ │ artefact of the bootstrapping interval method. In production, clip lower bounds to 0 │ │ for bike-rental demand. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Prediction Interval Quality │ │ │ │ The 80% prediction intervals [0.1, 0.9] show sensible behaviour: │ │ │ │ • Mean interval width: 280 − 185 ≈ 95 users on average │ │ • Intervals widen appropriately during high-demand periods (upper bound reaches │ │ 909) and narrow at low-demand times (overnight intervals are tight) │ │ • The bootstrapping method uses in-sample residuals, so interval quality is tied to │ │ how representative those residuals are of future errors │ │ │ │ To formally validate calibration, you would compute calculate_coverage on the │ │ backtest predictions. A well-calibrated 80% interval should contain the true value │ │ approximately 80% of the time. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Feature Design Rationale │ │ │ │ The lag and window structure directly encodes the three dynamics described for this │ │ dataset: │ │ │ │ • Short lags [1–6 h] → current momentum (e.g., commute ramp-up) │ │ • Daily lags [23, 24, 25] and [47, 48, 49] → same hour yesterday and two days ago, │ │ with ±1 buffer for timing shifts in peaks │ │ • Weekly lags [167, 168, 169] → same hour last week, capturing the weekday/weekend │ │ rhythm │ │ • 24 h rolling mean + std → today's overall demand level and intra-day volatility │ │ • 168 h rolling mean + max → this week's baseline and whether any unusual peak │ │ occurred recently │ │ │ │ The three exogenous variables (holiday, weather, temp) add contextual signal that │ │ the lag structure alone cannot capture. The categorical weather column is handled │ │ automatically. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Deployment Readiness Assessment │ │ │ │ │ │ Factor Assessment │ │ ────────────────────────────────────────────────────────────────────────────────── │ │ MASE < 1.0 ✅ Meaningfully beats the naïve baseline │ │ 36-hour horizon ✅ Covered with appropriate lag structure │ │ Prediction intervals available ✅ 80% intervals for uncertainty communication │ │ refit=False holds up over 4 months ✅ No evidence of severe degradation │ │ Overnight MAPE inflation ⚠️ Not a real problem, but avoid MAPE in │ │ reporting │ │ Negative lower bounds ⚠️ Minor — clip to 0 in production │ │ Calibration not yet verified ⚠️ Run calculate_coverage before relying on │ │ intervals │ │ │ │ │ │ Recommendation │ │ │ │ The model is a reasonable candidate for deployment, with three caveats before going │ │ live: │ │ │ │ 1 Verify interval calibration: Compute the actual coverage rate on the backtest │ │ predictions. If it falls significantly below 80%, consider switching to │ │ method='conformal_intervals' which provides formal coverage guarantees. │ │ 2 Plan a retraining cadence: The model was tested with refit=False over 4 months. │ │ In production, schedule periodic retraining (e.g., monthly) and consider setting │ │ up a RangeDriftDetector to flag when incoming data drifts outside the training │ │ distribution. │ │ 3 Clip negative predictions: Apply max(pred, 0) and max(lower_bound, 0) on the │ │ output before serving. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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()
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
╭───────────────────────────────────── 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 │ └────────────────────┴─────────────────────┘ 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 │ ├────────────────┼────────────────────────┤ │ Missing target │ none │ └────────────────┴────────────────────────┘ Recommendation ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Task type │ single_series │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Forecaster │ ForecasterRecursive │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Forecaster candidates │ ForecasterRecursive, ForecasterDirect, ForecasterFoundation, │ │ │ ForecasterStats │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Estimator │ LGBMRegressor │ ├───────────────────────┼────────────────────────────────────────────────────────────────┤ │ Estimator candidates │ LGBMRegressor, XGBRegressor, Ridge │ └───────────────────────┴────────────────────────────────────────────────────────────────┘ ╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ A single-series ML forecaster (ForecasterRecursive) is recommended. Data: 17544 │ │ observations, 'h' frequency. Alternative forecasters: ['ForecasterDirect', │ │ 'ForecasterFoundation', 'ForecasterStats']. 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 ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ 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 │ - handle_categorical_exog: Categorical exogenous variables │ │ │ detected: ['weather']. These are handled automatically by │ │ │ skforecast (categorical_features='auto'). (optional) │ └───────────────────┴────────────────────────────────────────────────────────────────────┘ ╭─────────────────────────────────── 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 key dynamics for hourly bike │ │ rentals: │ │ │ │ 1 "The last few hours give a good sense of the current trend" → Short lags [1, 2, │ │ 3, 4, 5, 6] capture the immediate momentum and direction of demand (e.g., a │ │ commute ramp-up in progress). │ │ 2 "Clear daily rhythm with rush-hour peaks" → Lags around 24 h [23, 24, 25] and 48 │ │ h [47, 48, 49] anchor the model to the same hour on the previous one and two │ │ days, directly encoding the intra-day cycle. The ±1 neighbours (23/25, 47/49) add │ │ robustness for slight timing shifts in peaks. │ │ 3 "Usually similar to what happened at the same time last week" → Lags around 168 h │ │ [167, 168, 169] capture the same hour from exactly one week ago, accounting for │ │ the weekday/weekend rhythm the user highlighted. Again, ±1 neighbours provide │ │ robustness. │ │ 4 Rolling window — 24 h mean + std: The 24-hour mean smooths over the full daily │ │ cycle and gives the model a sense of the current day's overall demand level. The │ │ 24-hour std captures intra-day volatility (e.g., a rainy day suppresses peaks), │ │ which is useful for a demand series driven by weather and events. │ │ 5 Rolling window — 168 h mean + max: The weekly mean gives the baseline level for │ │ the current week (distinguishing a typical weekday week from a holiday week), │ │ while the weekly max captures whether any unusually high peak occurred recently — │ │ a signal relevant for capacity and planning in bike-share systems. Together these │ │ two window sizes (one short/reactive, one long/trend) follow the multi-scale best │ │ practice without over-engineering the feature set. │ │ │ │ Note: the LLM-suggested lags and window_features are hypotheses, not validated │ │ improvements. Confirm any expected accuracy gain before relying on them. │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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()
╭───────────────────────────────────── Explanation ──────────────────────────────────────╮ │ │ │ Direct vs Recursive Forecasting for Hourly Demand with Seasonality │ │ │ │ When Direct Beats Recursive │ │ │ │ For hourly data with strong daily (24-step) and weekly (168-step) cycles, error │ │ propagation is the core issue with recursive forecasting. Here's when that matters │ │ enough to prefer direct: │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Key Decision Factors │ │ │ │ 1. Forecast Horizon Length │ │ │ │ │ │ Horizon Recommendation │ │ ──────────────────────────────────────────────────────────────────────────────── │ │ Short (1–6 hours) ForecasterRecursive — error propagation is minimal │ │ Medium (6–24 hours) Either; benchmark both │ │ Long (24–168+ hours) ForecasterDirect — recursive errors compound significantly │ │ │ │ │ │ The 24-step and 168-step seasonality lags are critical inputs. In recursive mode, │ │ steps 2 through 24 rely on predicted (not observed) values as lag features — │ │ degrading the quality of those seasonal signals progressively. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ 2. Horizon-Dependent Patterns │ │ │ │ Direct trains one model per step, so each step can learn its own relationship. This │ │ matters when: │ │ │ │ • Peak demand at hour 18 has different predictors than off-peak at hour 3 │ │ • Weekend vs weekday transitions affect specific hours differently │ │ • The marginal importance of lags shifts across the horizon (e.g., lag-168 matters │ │ more for step-168 than step-1) │ │ │ │ Recursive uses a single model that must generalise across all steps simultaneously — │ │ it cannot specialise per step. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ 3. Series Properties │ │ │ │ │ │ Property Favours │ │ ────────────────────────────────────────────────────────────────────────────────── │ │ High autocorrelation, smooth series Recursive (predictions feed back cleanly) │ │ Noisy, irregular demand spikes Direct (no error amplification) │ │ Strong, stable seasonality Recursive handles it well with seasonal lags │ │ Weak or shifting seasonality Direct (each step learns its own seasonal │ │ structure) │ │ │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Practical Considerations for Hourly Data │ │ │ │ Lag Configuration │ │ │ │ With ForecasterDirect(steps=168), you typically use: │ │ │ │ • Short lags: [1, 2, 3, 24] │ │ • Seasonal lags: [168] (same hour last week) │ │ │ │ Because direct never feeds predictions back, lag-1 at step-24 is still an observed │ │ value, not a 23-step-ahead prediction. This is the primary advantage. │ │ │ │ Computational Cost │ │ │ │ ForecasterDirect trains N = steps separate models. For steps=168, that is 168 models │ │ — significantly more expensive than the single model in ForecasterRecursive. You can │ │ mitigate this with n_jobs=-1 to parallelise fitting. │ │ │ │ Fixed Horizon Requirement │ │ │ │ ForecasterDirect requires steps to be set at initialisation, not at predict time. If │ │ your operational need varies (sometimes 24h, sometimes 48h), you would need separate │ │ forecasters or to retrain. │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Recommended Approach │ │ │ │ ▌ Always benchmark both using backtesting_forecaster with a realistic │ │ ▌ TimeSeriesFold. Do not choose based on theory alone — the winning strategy is │ │ ▌ data-dependent. │ │ │ │ A typical benchmark setup would: │ │ │ │ 1 Use the same estimator (e.g., LGBMRegressor) for both │ │ 2 Use the same lag set and exogenous features │ │ 3 Evaluate with mean_absolute_error and mean_absolute_percentage_error over a │ │ held-out period covering at least several weeks │ │ 4 Check performance per step (direct often wins at steps > 24, recursive may win at │ │ steps 1–6) │ │ │ │ ------------------------------------------------------------------------------------ │ │ │ │ Summary │ │ │ │ Prefer ForecasterDirect when: │ │ │ │ • Your horizon is ≥ 24 hours (one full daily cycle) │ │ • Demand patterns differ meaningfully by hour-of-day or day-of-week across the │ │ horizon │ │ • The series has significant noise or irregular spikes │ │ • You can afford the computational cost of 24–168 models │ │ │ │ Stick with ForecasterRecursive when: │ │ │ │ • Your horizon is short (< 12 hours) │ │ • Computational speed or memory is a constraint │ │ • You need a flexible horizon at predict time │ │ • The series is smooth and highly autocorrelated │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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. |
| 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.
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.1.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-20 19:22
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! 😊
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.
