Forecasting with foundation models¶
Foundation Models (FMs) have recently introduced a major paradigm shift in the field of time series forecasting. Driven by the same architectural breakthroughs that power Large Language Models (LLMs), these models offer powerful generalization capabilities out-of-the-box.
In the context of time series, a Foundation Model is a large-scale neural network (typically based on Transformer architectures) that has been pre-trained on massive and highly diverse datasets spanning multiple domains (e.g., finance, weather, web traffic, and retail).
Models such as AWS Chronos, Google TimesFM2.5, and Salesforce MOIRAI operate on a principle similar to language models: they treat time series values as a sequence of tokens. Because they have already "seen" millions of time series patterns during their pre-training phase, they can identify trends, seasonality, and complex dynamics in new, unseen data without requiring any domain-specific training.
Foundation Models vs. Machine Learning Models¶
Foundation models and traditional machine learning models approach forecasting in fundamentally different ways. Understanding these distinctions is crucial for knowing when and how to deploy each method.
Zero-Shot Prediction
Machine learning models require a training phase. You must fit the model on your historical target data (and exogenous variables) so the algorithm can learn the optimal weights and parameters for your specific time series. Foundation models are capable of zero-shot prediction. Because the model's parameters are already frozen from its massive pre-training phase, it can generate forecasts on your data immediately, without explicitly "learning" from your specific dataset first.
The Role of the fit Method
Machine learning models must be trained: calling .fit() optimizes the model's internal parameters to minimize an error metric on your data. Foundation models, by contrast, arrive pre-trained, their weights are frozen and never updated. Calling .fit() is therefore not a training step; it simply stores the recent historical context (last window of observations, frequency, and scaling factors) so that .predict() has everything it needs at inference time.
Context Window vs. Engineered Lags
Machine learning models rely on explicitly engineered features. In skforecast, you define lags or window_features to create a tabular dataset where past values are used as columns to predict the target. Foundation models rely on a context window. You pass a raw, sequential chunk of recent historical data (e.g., the last 512 observations) directly into the model at inference time. The attention mechanism inside the model automatically decides which past data points are most relevant.
✎ Note
For more details about forecasting with foundation models, visit Forecasting: Principles and Practice, the Pythonic Way.
Foundation Models in skforecast¶
Skforecast's integration is built on two layers. First, FoundationModel acts as a unified wrapper that adapts each model's native API (Chronos-2, TimesFM 2.5, Moirai-2) behind a familiar scikit-learn interface (.fit(), .predict(), .get_params()). Second, ForecasterFoundation wraps that estimator to unlock the full skforecast ecosystem (backtesting, prediction intervals, multi-series support...) with the same code you would use for any other forecaster.
Supported Foundation Models¶
Chronos |
TimesFM |
Moirai |
TabICL |
|
|---|---|---|---|---|
| Provider | Amazon | Salesforce | Soda-Inria | |
| GitHub | chronos-forecasting | timesfm | uni2ts | tabicl |
| Documentation | Chronos models | TimesFM models | Moirai-R models | TabICL Docs |
| Available model IDs | amazon/chronos-2autogluon/chronos-2-smallautogluon/chronos-2-synth |
google/timesfm-2.5-200m-pytorch |
Salesforce/moirai-2.0-R-small |
soda-inria/tabicl |
| Backend | PyTorch | PyTorch | PyTorch | PyTorch |
| Forecasting type | Zero-shot | Zero-shot | Zero-shot | Zero-shot |
Default context_length |
8192 |
512 |
2048 |
4096 |
Max context_length |
8192 |
16384 |
2048 |
4096 |
max_horizon |
No hard limit, set via steps at predict time |
512 |
No hard limit, set via steps at predict time |
No hard limit, set via steps at predict time |
| Point forecast | Median (0.5 quantile) | Mean (dedicated output array) | Median (0.5 quantile) | Mean (default, configurable to median) |
Covariate support (exog) |
Yes | No | No | Yes |
cross_learning parameter |
Yes (multi-series mode only) | No | No | No |
| Install command | pip install chronos-forecasting |
pip install git+https://github.com/google-research/timesfm.git |
pip install uni2ts |
pip install tabicl[forecast] |
💡 Tip
All three models run on the CPU. However, a CUDA GPU is recommended for faster inference, especially with long context windows. The MPS backend is also detected automatically by PyTorch and can benefit Apple Silicon users.
FoundationModel¶
Each foundation model (Chronos-2, TimesFM 2.5, Moirai-2) has its own API, input format, and inference logic. The FoundationModel class abstracts away these differences behind a single scikit-learn-compatible interface (.fit(), .predict()). Internally, it delegates to a model-specific adapter that handles all the implementation details.
ForecasterFoundation¶
The ForecasterFoundation wraps a FoundationModel and exposes the same interface as any other skforecast forecaster. This means that you can use backtesting, prediction intervals, and multi-series support with the exact same code. It also makes it easy to compare a zero-shot Chronos model with a trained ForecasterRecursive on the same dataset.
Input Data Formats¶
ForecasterFoundation accepts several data formats for both the target series and exogenous variables.
Target Series (series)
The series parameter in the .fit() method supports both single-series and multi-series (global model) configurations.
| Mode | Allowed Data Type | Description |
|---|---|---|
| Single-Series | pd.Series |
A single time series with a named index. |
| Multi-Series (Wide) | pd.DataFrame |
Each column represents a separate time series. |
| Multi-Series (Long) | pd.DataFrame |
MultiIndex (Level 0: series ID, Level 1: DatetimeIndex). |
| Multi-Series (Dict) | dict[str, pd.Series] |
Keys are series identifiers, values are pandas Series. |
💡 Tip
While Long-format DataFrames are supported, they are converted to dictionaries internally. For best performance, pass a dict[str, pd.Series] directly.
Exogenous Variables (exog)
Exogenous variables must be aligned with the target series index. Currently, only Chronos supports covariates (see the Supported Foundation Models table). TimesFM 2.5 and Moirai-2 do not accept exogenous variables.
| Mode | Allowed Data Type | Description |
|---|---|---|
| Single-Series | pd.Series or pd.DataFrame |
Aligned to the target series index. |
| Multi-Series (Dict) | dict[str, pd.Series \| pd.DataFrame \| None] |
One entry per series. |
| Multi-Series (Broadcast) | pd.Series or pd.DataFrame |
Automatically applied to all series. |
| Multi-Series (Long) | pd.DataFrame |
MultiIndex (Level 0: series ID, Level 1: DatetimeIndex). |
Libraries and data¶
# Libraries
# ==============================================================================
import numpy as np
import pandas as pd
import torch
import matplotlib.pyplot as plt
from skforecast.datasets import fetch_dataset
from skforecast.foundation import FoundationModel, ForecasterFoundation
from skforecast.model_selection import (
TimeSeriesFold,
backtesting_foundation
)
from skforecast.plot import set_dark_theme
color = '\033[1m\033[38;5;208m'
print(f"{color}torch version: {torch.__version__}")
print(f" Cuda available : {torch.cuda.is_available()}")
print(f" MPS available : {torch.backends.mps.is_available()}")
torch version: 2.4.1
Cuda available : False
MPS available : True
# Data download
# ==============================================================================
data = fetch_dataset(name='vic_electricity')
# Aggregating in 1H intervals
# ==============================================================================
# The Date column is eliminated so that it does not generate an error when aggregating.
data = data.drop(columns="Date")
data = (
data
.resample(rule="h", closed="left", label="right")
.agg({
"Demand": "mean",
"Temperature": "mean",
"Holiday": "mean",
})
)
data.head(3)
╭──────────────────────────── vic_electricity ─────────────────────────────╮ │ Description: │ │ Half-hourly electricity demand for Victoria, Australia │ │ │ │ Source: │ │ O'Hara-Wild M, Hyndman R, Wang E, Godahewa R (2022).tsibbledata: Diverse │ │ Datasets for 'tsibble'. https://tsibbledata.tidyverts.org/, │ │ https://github.com/tidyverts/tsibbledata/. │ │ https://tsibbledata.tidyverts.org/reference/vic_elec.html │ │ │ │ URL: │ │ https://raw.githubusercontent.com/skforecast/skforecast- │ │ datasets/main/data/vic_electricity.csv │ │ │ │ Shape: 52608 rows x 4 columns │ ╰──────────────────────────────────────────────────────────────────────────╯
| Demand | Temperature | Holiday | |
|---|---|---|---|
| Time | |||
| 2011-12-31 14:00:00 | 4323.095350 | 21.225 | 1.0 |
| 2011-12-31 15:00:00 | 3963.264688 | 20.625 | 1.0 |
| 2011-12-31 16:00:00 | 3950.913495 | 20.325 | 1.0 |
# Split data into train-test
# ==============================================================================
data = data.loc['2012-01-01 00:00:00':'2014-12-30 23:00:00', :].copy()
end_train = '2014-11-30 23:59:00'
data_train = data.loc[: end_train, :].copy()
data_test = data.loc[end_train:, :].copy()
print(f"Train dates: {data_train.index.min()} --- {data_train.index.max()} (n={len(data_train)})")
print(f"Test dates : {data_test.index.min()} --- {data_test.index.max()} (n={len(data_test)})")
Train dates: 2012-01-01 00:00:00 --- 2014-11-30 23:00:00 (n=25560) Test dates : 2014-12-01 00:00:00 --- 2014-12-30 23:00:00 (n=720)
⚠ Warning: Data Leakage
The following examples use a widely available public dataset for illustrative purposes. It is highly probable that the foundation models (Chronos, TimesFM, Moirai) were exposed to these data points during their pre-training phase. As a result, the predictions may be more optimistic than what would be achieved in a real-world production environment with private or novel data.
Chronos¶
A ForecasterFoundation is created using Amazon's Chronos-2-small model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="autogluon/chronos-2-small")
forecaster = ForecasterFoundation(estimator=estimator)
Each adapter accepts additional keyword arguments that control model-specific behavior (e.g., context_length, device_map, torch_dtype). These can be passed directly through the FoundationModel constructor.
For the full list of available parameters, see the API reference: ChronosAdapter, TimesFMAdapter, MoiraiAdapter.
💡 Tip
While .fit() is used here to store the historical context and metadata, it is not strictly required. Foundation models can generate forecasts by passing the context directly to .predict() via the context parameter. However, calling .fit() first simplifies subsequent calls to .predict(), .predict_interval(), and .predict_quantiles().
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
forecaster
ForecasterFoundation
General Information
- Model ID: autogluon/chronos-2-small
- Context length: 8192
- Window size: 8192
- Series names: Demand
- Exogenous included: True
- Creation date: 2026-04-23 15:33:01
- Last fit date: 2026-04-23 15:33:02
- Skforecast version: 0.22.0
- Python version: 3.12.13
- Forecaster id: None
Exogenous Variables
Temperature, Holiday
Training Information
- Context range: 'Demand': ['2012-01-01 00:00:00', '2014-11-30 23:00:00']
- Training index type: DatetimeIndex
- Training index frequency:
Model Parameters
- cross_learning: False
- context_length: 8192
- device_map: auto
- torch_dtype: None
- predict_kwargs: None
Three methods can be used to predict the next predict(), predict_interval(), and predict_quantiles(). All these methods allow for passing context and context_exog to override the historical context used by the underlying model to generate predictions.
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
steps = steps,
exog = data_test[["Temperature", "Holiday"]]
)
predictions.head(3)
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5488.940430 |
| 2014-12-01 01:00:00 | Demand | 5431.977539 |
| 2014-12-01 02:00:00 | Demand | 5347.607422 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [10, 90], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5488.940430 | 5333.779785 | 5682.048340 |
| 2014-12-01 01:00:00 | Demand | 5431.977539 | 5262.784180 | 5657.536133 |
| 2014-12-01 02:00:00 | Demand | 5347.607422 | 5161.753418 | 5592.012695 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
metrics_chronos, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
print("Backtest metrics")
display(metrics_chronos)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
0%| | 0/30 [00:00<?, ?it/s]
Backtest metrics
| mean_absolute_error | |
|---|---|
| 0 | 158.693401 |
Backtest predictions
| level | fold | pred | |
|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 5488.940430 |
| 2014-12-01 01:00:00 | Demand | 0 | 5431.977539 |
| 2014-12-01 02:00:00 | Demand | 0 | 5347.607422 |
| 2014-12-01 03:00:00 | Demand | 0 | 5256.606934 |
# Plot predictions
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(7, 3))
data_test['Demand'].plot(ax=ax, label='test')
backtest_predictions['pred'].plot(ax=ax, label='predictions')
ax.legend();
Multiple series (global model)¶
The class ForecasterFoundation allows modeling and forecasting multiple series with a single model.
# Data
# ==============================================================================
data_multiseries = fetch_dataset(name="items_sales")
display(data_multiseries.head(3))
╭─────────────────────── items_sales ───────────────────────╮ │ Description: │ │ Simulated time series for the sales of 3 different items. │ │ │ │ Source: │ │ Simulated data. │ │ │ │ URL: │ │ https://raw.githubusercontent.com/skforecast/skforecast- │ │ datasets/main/data/simulated_items_sales.csv │ │ │ │ Shape: 1097 rows x 3 columns │ ╰───────────────────────────────────────────────────────────╯
| item_1 | item_2 | item_3 | |
|---|---|---|---|
| date | |||
| 2012-01-01 | 8.253175 | 21.047727 | 19.429739 |
| 2012-01-02 | 22.777826 | 26.578125 | 28.009863 |
| 2012-01-03 | 27.549099 | 31.751042 | 32.078922 |
# Split data into train-test
# ==============================================================================
end_train = '2014-07-15 23:59:00'
data_multiseries_train = data_multiseries.loc[:end_train, :]
data_multiseries_test = data_multiseries.loc[end_train:, :]
# Plot time series
# ==============================================================================
set_dark_theme()
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 5), sharex=True)
for i, col in enumerate(data_multiseries.columns):
data_multiseries_train[col].plot(ax=axes[i], label='train')
data_multiseries_test[col].plot(ax=axes[i], label='test')
axes[i].set_title(col)
axes[i].set_ylabel('sales')
axes[i].set_xlabel('')
axes[i].legend(loc='upper left')
fig.tight_layout()
plt.show();
In this example, instead of calling fit(), the context is passed directly to the predict() method.
# Create and train ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id = "autogluon/chronos-2-small")
forecaster = ForecasterFoundation(estimator = estimator)
# fit() is optional; context is passed directly to predict()
# forecaster.fit(series=data_multiseries_train)
forecaster
ForecasterFoundation
General Information
- Model ID: autogluon/chronos-2-small
- Context length: 8192
- Window size: 8192
- Series names: None
- Exogenous included: False
- Creation date: 2026-04-23 15:33:20
- Last fit date: None
- Skforecast version: 0.22.0
- Python version: 3.12.13
- Forecaster id: None
Exogenous Variables
None
Training Information
- Context range: Not fitted
- Training index type: Not fitted
- Training index frequency: Not fitted
Model Parameters
- cross_learning: False
- context_length: 8192
- device_map: auto
- torch_dtype: None
- predict_kwargs: None
# Predictions for all series (levels)
# ==============================================================================
steps = 24
predictions_items = forecaster.predict(
steps = len(data_multiseries_test),
levels = None, # All levels are predicted
context = data_multiseries_train
)
predictions_items.head()
╭────────────────────────────────── InputTypeWarning ──────────────────────────────────╮ │ Passing a DataFrame (either wide or long format) as `series` requires additional │ │ internal transformations, which can increase computational time. It is recommended │ │ to use a dictionary of pandas Series instead. For more details, see: │ │ https://skforecast.org/latest/user_guides/independent-multi-time-series-forecasting. │ │ html#input-data │ │ │ │ Category : skforecast.exceptions.InputTypeWarning │ │ Location : │ │ /opt/homebrew/Caskroom/miniconda/base/envs/skforecast_py12/lib/python3.12/site-packa │ │ ges/skforecast/utils/utils.py:2799 │ │ Suppress : warnings.simplefilter('ignore', category=InputTypeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | |
|---|---|---|
| 2014-07-16 | item_1 | 25.478813 |
| 2014-07-16 | item_2 | 10.479328 |
| 2014-07-16 | item_3 | 11.807503 |
| 2014-07-17 | item_1 | 25.194397 |
| 2014-07-17 | item_2 | 10.714880 |
# Plot predictions
# ==============================================================================
set_dark_theme()
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 5), sharex=True)
for i, col in enumerate(data_multiseries.columns):
data_multiseries_train[col].plot(ax=axes[i], label='train')
data_multiseries_test[col].plot(ax=axes[i], label='test')
predictions_items.query(f"level == '{col}'").plot(
ax=axes[i], label='predictions', color='white'
)
axes[i].set_title(col)
axes[i].set_ylabel('sales')
axes[i].set_xlabel('')
axes[i].legend(loc='upper left')
fig.tight_layout()
plt.show();
# Interval predictions for item_1 and item_2
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = 24,
levels = ['item_1', 'item_2'],
context = data_multiseries_train,
interval = [10, 90], # 80% prediction interval
)
predictions_intervals.head()
╭────────────────────────────────── InputTypeWarning ──────────────────────────────────╮ │ Passing a DataFrame (either wide or long format) as `series` requires additional │ │ internal transformations, which can increase computational time. It is recommended │ │ to use a dictionary of pandas Series instead. For more details, see: │ │ https://skforecast.org/latest/user_guides/independent-multi-time-series-forecasting. │ │ html#input-data │ │ │ │ Category : skforecast.exceptions.InputTypeWarning │ │ Location : │ │ /opt/homebrew/Caskroom/miniconda/base/envs/skforecast_py12/lib/python3.12/site-packa │ │ ges/skforecast/utils/utils.py:2799 │ │ Suppress : warnings.simplefilter('ignore', category=InputTypeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-07-16 | item_1 | 25.393276 | 24.595732 | 26.200489 |
| 2014-07-16 | item_2 | 10.694120 | 8.742786 | 13.271328 |
| 2014-07-17 | item_1 | 25.141916 | 24.192814 | 26.036993 |
| 2014-07-17 | item_2 | 10.868244 | 8.804617 | 13.735439 |
| 2014-07-18 | item_1 | 25.119213 | 24.065229 | 26.078648 |
Other foundation models¶
The examples above use the Amazon Chronos model, but the same code structure applies to any other foundation model supported by skforecast. To use a different model, simply change the model_id parameter when instantiating the FoundationModel wrapper. The rest of the forecasting pipeline remains unchanged, allowing you to easily compare different foundation models on your dataset.
TimesFM2.5¶
A ForecasterFoundation is created using Google's TimesFM-2.5-200m model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="google/timesfm-2.5-200m-pytorch")
forecaster = ForecasterFoundation(estimator = estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(series=data_train["Demand"])
forecaster
ForecasterFoundation
General Information
- Model ID: google/timesfm-2.5-200m-pytorch
- Context length: 512
- Window size: 512
- Series names: Demand
- Exogenous included: False
- Creation date: 2026-04-23 15:33:22
- Last fit date: 2026-04-23 15:33:22
- Skforecast version: 0.22.0
- Python version: 3.12.13
- Forecaster id: None
Exogenous Variables
None
Training Information
- Context range: 'Demand': ['2012-01-01 00:00:00', '2014-11-30 23:00:00']
- Training index type: DatetimeIndex
- Training index frequency:
Model Parameters
- context_length: 512
- max_horizon: 512
- forecast_config_kwargs: None
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(steps=steps)
predictions.head(3)
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5665.909668 |
| 2014-12-01 01:00:00 | Demand | 5668.937012 |
| 2014-12-01 02:00:00 | Demand | 5760.318848 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
interval = [10, 90], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5665.909668 | 5549.081055 | 5800.553223 |
| 2014-12-01 01:00:00 | Demand | 5668.937012 | 5457.991699 | 5904.702148 |
| 2014-12-01 02:00:00 | Demand | 5760.318848 | 5443.220703 | 6093.883789 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
metrics_timesfm, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
print("Backtest metrics")
display(metrics_timesfm)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
0%| | 0/168 [00:00<?, ?it/s]
Backtest metrics
| mean_absolute_error | |
|---|---|
| 0 | 163.088465 |
Backtest predictions
| level | fold | pred | |
|---|---|---|---|
| 2014-07-16 00:00:00 | Demand | 0 | 6188.992676 |
| 2014-07-16 01:00:00 | Demand | 0 | 5991.776855 |
| 2014-07-16 02:00:00 | Demand | 0 | 5831.969727 |
| 2014-07-16 03:00:00 | Demand | 0 | 5704.315430 |
Moirai¶
A ForecasterFoundation is created using Salesforce's Moirai-2.0-R-small model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="Salesforce/moirai-2.0-R-small")
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(series=data_train["Demand"])
forecaster
ForecasterFoundation
General Information
- Model ID: Salesforce/moirai-2.0-R-small
- Context length: 2048
- Window size: 2048
- Series names: Demand
- Exogenous included: False
- Creation date: 2026-04-23 15:33:36
- Last fit date: 2026-04-23 15:33:36
- Skforecast version: 0.22.0
- Python version: 3.12.13
- Forecaster id: None
Exogenous Variables
None
Training Information
- Context range: 'Demand': ['2012-01-01 00:00:00', '2014-11-30 23:00:00']
- Training index type: DatetimeIndex
- Training index frequency:
Model Parameters
- context_length: 2048
- device: auto
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(steps=steps)
predictions.head(3)
/var/folders/wt/8tvn563d5v55nspfbydgqb9r0000gp/T/ipykernel_40162/2957951684.py:4: UserWarning: MPS device is not supported by Moirai because the uni2ts library uses float64 operations internally. Falling back to CPU. predictions = forecaster.predict(steps=steps)
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5795.251953 |
| 2014-12-01 01:00:00 | Demand | 5986.599121 |
| 2014-12-01 02:00:00 | Demand | 6149.235352 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
interval = [10, 90], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5795.251953 | 5628.000488 | 5953.445312 |
| 2014-12-01 01:00:00 | Demand | 5986.599121 | 5673.974121 | 6254.291016 |
| 2014-12-01 02:00:00 | Demand | 6149.235352 | 5728.174805 | 6538.167969 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
metrics_moirai, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
print("Backtest metrics")
display(metrics_moirai)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
0%| | 0/168 [00:00<?, ?it/s]
Backtest metrics
| mean_absolute_error | |
|---|---|
| 0 | 154.080703 |
Backtest predictions
| level | fold | pred | |
|---|---|---|---|
| 2014-07-16 00:00:00 | Demand | 0 | 6224.538086 |
| 2014-07-16 01:00:00 | Demand | 0 | 6114.214355 |
| 2014-07-16 02:00:00 | Demand | 0 | 5991.563965 |
| 2014-07-16 03:00:00 | Demand | 0 | 5959.375488 |
TabICL¶
A ForecasterFoundation is created using Soda-Inria's TabICL model.
In this case, the context_length is reduced from the default of 4096 to 72 to speed up inference. For the best performance, use as much context as possible, depending on computational capacity.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="soda-inria/tabicl", context_length=72)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
forecaster
ForecasterFoundation
General Information
- Model ID: soda-inria/tabicl
- Context length: 72
- Window size: 72
- Series names: Demand
- Exogenous included: True
- Creation date: 2026-04-23 15:33:43
- Last fit date: 2026-04-23 15:33:43
- Skforecast version: 0.22.0
- Python version: 3.12.13
- Forecaster id: None
Exogenous Variables
Temperature, Holiday
Training Information
- Context range: 'Demand': ['2012-01-01 00:00:00', '2014-11-30 23:00:00']
- Training index type: DatetimeIndex
- Training index frequency:
Model Parameters
- context_length: 72
- point_estimate: mean
- tabicl_config: None
- temporal_features: None
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
steps = steps,
exog = data_test[["Temperature", "Holiday"]]
)
predictions.head(3)
Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.28it/s]
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5672.044434 |
| 2014-12-01 01:00:00 | Demand | 5729.508301 |
| 2014-12-01 02:00:00 | Demand | 5774.244629 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [10, 90], # 80% prediction interval
)
predictions_intervals.head(3)
Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.48it/s]
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5684.678711 | 5378.824219 | 5957.540039 |
| 2014-12-01 01:00:00 | Demand | 5745.275879 | 5359.950195 | 6097.788574 |
| 2014-12-01 02:00:00 | Demand | 5808.790039 | 5236.602051 | 6276.870117 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
metrics_tabicl, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
print("Backtest metrics")
display(metrics_tabicl)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
0%| | 0/168 [00:00<?, ?it/s]
Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.87it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.47it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.24it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.32it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.06it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.32it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.32it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.56it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.57it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.50it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.53it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.33it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.24it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.53it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.56it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.42it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.36it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.38it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.65it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.85it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.46it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.38it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.49it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.47it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.33it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.22it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.31it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.44it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.53it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.37it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.10it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.21it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.38it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.05it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.14it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.03it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.37it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.18it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.29it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.02it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.36it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.17it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.88it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.31it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.53it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.27it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.27it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.46it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.50it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.29it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.44it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.22it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.21it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.63it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.47it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.46it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.32it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.38it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.73it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.04it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.77it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.29it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.70it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.77it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.94it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.12it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.22it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.19it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.77it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.94it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.21it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.23it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.30it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.31it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.42it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.07it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.16it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.39it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.34it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.34it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.29it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.29it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.28it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.44it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.32it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.50it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.44it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.95it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.01it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.13it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.36it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.42it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.21it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.15it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.23it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.17it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.36it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.28it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.24it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.38it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.21it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.28it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.31it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.35it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.34it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.14it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.23it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.46it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.38it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.26it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.47it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.27it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.21it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.33it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.41it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.13it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.64it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 1.77it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.37it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.37it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.37it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.15it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.12it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.22it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.52it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.44it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.30it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.42it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.40it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.22it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.29it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.27it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.43it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.46it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.44it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.16it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.36it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.34it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.17it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.31it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.27it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.33it/s] Predicting time series: 100%|██████████| 1/1 [00:00<00:00, 2.28it/s]
Backtest metrics
| mean_absolute_error | |
|---|---|
| 0 | 291.01068 |
Backtest predictions
| level | fold | pred | |
|---|---|---|---|
| 2014-07-16 00:00:00 | Demand | 0 | 6206.785156 |
| 2014-07-16 01:00:00 | Demand | 0 | 6153.166016 |
| 2014-07-16 02:00:00 | Demand | 0 | 6103.278809 |
| 2014-07-16 03:00:00 | Demand | 0 | 6089.614258 |
Model comparison¶
The following table summarizes the backtesting results (Mean Absolute Error) for the three foundation models on the same dataset.
✎ Note
Chronos-2 is the only model that supports exogenous variables, so its backtesting includes them. TimesFM and Moirai do not use exogenous variables. This difference should be considered when comparing results.
# Comparison of backtesting metrics
# ==============================================================================
comparison = pd.DataFrame({
"Model": [
"Chronos-2 (small)*",
"TimesFM-2.5 (200m)",
"Moirai-2.0-R (small)",
"TabICLv2"
],
"mean_absolute_error": [
metrics_chronos["mean_absolute_error"].iloc[0],
metrics_timesfm["mean_absolute_error"].iloc[0],
metrics_moirai["mean_absolute_error"].iloc[0],
metrics_tabicl["mean_absolute_error"].iloc[0] # Reduced context length
],
})
comparison.style.highlight_min(
subset="mean_absolute_error", color="green"
).format(precision=4)
| Model | mean_absolute_error | |
|---|---|---|
| 0 | Chronos-2 (small)* | 158.6934 |
| 1 | TimesFM-2.5 (200m) | 163.0885 |
| 2 | Moirai-2.0-R (small) | 154.0807 |
| 3 | TabICLv2 | 291.0107 |