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
Forecasting with foundation models¶
Foundation models (FMs) have triggered a fundamental paradigm shift in time series forecasting, moving the field away from modelling for each dataset and towards generalised representation learning. Driven by the same architectural breakthroughs that power Large Language Models (LLMs), FMs bring zero-shot and in-context learning capabilities to temporal data.
In the context of forecasting, a foundation model is a massively scaled neural network (typically Transformer-based) that has been pre-trained on highly diverse, cross-domain datasets spanning finance, weather, web traffic, retail and more.
Models such as AWS Chronos, Google TimesFM 2.5 and Salesforce Moirai frame temporal forecasting as a sequence modelling problem and process temporal data either as quantized discrete tokens (as in Chronos, which applies scalar quantization) or as continuous patch embeddings (as in TimesFM and Moirai, which group consecutive time steps into fixed-length patches before encoding them). Having already internalised the structural priors of millions of series during pre-training, they can instantly infer trends, seasonality and complex dynamics in completely unseen data, eliminating the need for any domain-specific weight updates.
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 so the algorithm can learn the optimal weights and parameters for your specific time series. Foundation models, however, are capable of zero-shot inference. Because their highly generalized weights are already frozen from the massive pre-training phase, they can generate accurate forecasts on your data immediately, leveraging their pre-existing latent representations rather than learning your dataset from scratch.
The Role of the fit Method
Machine learning models must be trained: calling .fit() optimizes the model's internal parameters by minimizing a loss function on your historical data. Foundation models, by contrast, arrive pre-trained: their weights are fixed and are never updated. Calling .fit() on a foundation model is not a training step; it simply stores the historical context (observations, frequency, and any scaling factors) needed at inference time. In some implementations, calling .fit() is entirely optional before prediction.
Context Window vs. Engineered Lags
Machine learning models rely on explicitly engineered features; they require creating 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.
In summary, foundation models represent a fundamental paradigm shift, replacing the traditional train → predict pipeline with a pre-train → (context + predict) approach. As major research institutions with access to millions of diverse time series carry out the computationally intensive pre-training phase, end users are completely freed from model training.
However, there is no such thing as a free lunch in machine learning. Skipping the training phase results in a heavier burden during the inference phase. Because their weights are frozen, these models cannot adapt to your data through training. Instead, they adapt implicitly at inference time by processing the historical context through their attention mechanism. Each prediction therefore requires ingesting and attending over a large sequence of raw observations in real time. Consequently, the main drawback of zero-shot forecasting is that the inference process is significantly slower, more computationally expensive and requires your data pipeline to continuously provide large amounts of historical context at runtime.
| - | ML Model | Foundation Model |
|---|---|---|
| fit | Trains model, updates weights | Stores context & metadata |
| predict | Uses learned weights | Processes context via attention |
| Data required at train time | Full history | Not required |
| Data required at predict time | Last lags observations | Full context window |
| Computational cost | At train time | At inference time |
✏️ 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 and 3.0, Moirai-2, TabICL, TabPFN-TS, TFC T0, Nori, TS-ICL, Timestral and TiRex-2) behind a familiar scikit-learn interface (fit, predict, get_params). Second, ForecasterFoundation wraps that estimator to unlock the full skforecast ecosystem. It exposes the same interface as any other skforecast forecaster, meaning users can use backtesting, prediction intervals, and multi-series support with the exact same code.
Supported Foundation Models¶
| Model | Provider | GitHub | Documentation | Available model IDs | Backend | Default context length | Max context length | Max horizon | Point forecast | Covariate support (exog) | Cross learning | Install command |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Chronos | Amazon | GitHub | Docs | amazon/chronos-2 autogluon/chronos-2-small autogluon/chronos-2-synth |
PyTorch | 8192 | 8192 | No hard limit, set via steps |
Median (0.5 quantile) | Yes | Multi-series only |
pip install chronos-forecasting
|
| TimesFM 2.5 | GitHub | Docs | google/timesfm-2.5-200m-pytorch | PyTorch | 512 | 16384 | 512 | Median (0.5 quantile) | No | No |
pip install timesfm[torch]
|
|
| TimesFM 3.0 | GitHub | Docs | google/timesfm-3.0-pytorch | PyTorch | 2048 | ~15,360 | No hard limit, set via steps |
Median (0.5 quantile) | Yes | No |
pip install timesfm[torch]
|
|
| Moirai | Salesforce | GitHub | Docs | Salesforce/moirai-2.0-R-small | PyTorch | 2048 | 2048 | No hard limit, set via steps |
Median (0.5 quantile) | No | No |
pip install uni2ts
|
| TabICL | Soda-Inria | GitHub | Docs | soda-inria/tabicl | PyTorch | 4096 | 4096 | No hard limit, set via steps |
Mean (default) | Yes | No |
pip install tabicl[forecast]
|
| TabPFN-TS | Prior Labs | GitHub | Docs | priorlabs/tabpfn-ts | PyTorch | 32768 | No hard limit (~65536) | No hard limit, set via steps |
Median (default) | Yes | No |
pip install tabpfn-time-series
|
| T0 | The Forecasting Company | GitHub | Docs | theforecastingcompany/t0-alpha | PyTorch | 8192 | No hard limit | No hard limit, set via steps |
Median (0.5 quantile) | Yes | No |
pip install tfc-t0
|
| Nori | Synthefy | GitHub | Docs | Synthefy/Nori | PyTorch | 4096 | No hard limit | No hard limit, set via steps |
Mean (default) | Yes | No |
pip install synthefy-nori
|
| TS-ICL | EDF Lab | GitHub | Docs | taharnbl/TS-ICL | PyTorch | 4096 | No hard limit | No hard limit, set via steps |
Median (0.5) | Yes | No |
pip install tsicl
|
| Timestral | Mistral AI | GitHub | Docs | mistralai/Timestral-5M mistralai/Timestral-28M mistralai/Timestral-123M mistralai/Timestral-209M |
PyTorch | 4096 | 32768 | No hard limit, set via steps |
Median (0.5 quantile) | Yes | No |
pip install "timestral @ git+https://github.com/mistralai/timestral.git"
|
| TiRex-2 | NX-AI | GitHub | Docs | NX-AI/TiRex-2 | PyTorch | 2048 | 2048 | 320 steps | Median (0.5 quantile) | Yes | No |
pip install tirex-2
|
The information in this table can also be obtained from code, without installing the backend libraries or downloading the weights. list_adapters returns one row per adapter, and get_model_info returns the same information for a specific model_id.
# Capabilities of the supported foundation models
# ==============================================================================
from skforecast.foundation import list_adapters
list_adapters(as_frame=True)
| model_id | model_id_prefixes | default_model_id | default_context_length | backend_package | allow_exog | supports_past_only_covariates | supports_categorical_covariates | supports_heterogeneous_covariates | supports_nan_in_series | supported_quantiles | requires_hf_auth | requires_provider_auth | weights_repo_id | weights_in_hf_cache | license | license_url | commercial_use_restricted | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| adapter | ||||||||||||||||||
| ChronosAdapter | autogluon/chronos-2-small | (amazon/chronos-2, autogluon/chronos-2) | autogluon/chronos-2-small | 8192 | chronos-forecasting | True | True | True | False | True | None | False | False | autogluon/chronos-2-small | True | Apache-2.0 | https://huggingface.co/autogluon/chronos-2-small | False |
| TimesFM25Adapter | google/timesfm-2.5-200m-pytorch | (google/timesfm-2.5,) | google/timesfm-2.5-200m-pytorch | 512 | timesfm[torch] | False | False | False | True | True | (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9) | False | False | google/timesfm-2.5-200m-pytorch | True | Apache-2.0 | https://huggingface.co/google/timesfm-2.5-200m... | False |
| TimesFM3Adapter | google/timesfm-3.0-pytorch | (google/timesfm-3.0,) | google/timesfm-3.0-pytorch | 2048 | timesfm[torch] | True | True | False | False | True | (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9) | False | False | google/timesfm-3.0-pytorch | True | timesfm-non-commercial-license-v1.0 | https://huggingface.co/google/timesfm-3.0-pyto... | True |
| TiRex2Adapter | NX-AI/TiRex-2 | (NX-AI/TiRex-2,) | NX-AI/TiRex-2 | 2048 | tirex-2 | True | True | False | True | True | (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9) | False | False | NX-AI/TiRex-2 | True | Apache-2.0 | https://huggingface.co/NX-AI/TiRex-2 | False |
| MoiraiAdapter | Salesforce/moirai-2.0-R-small | (Salesforce/moirai-2,) | Salesforce/moirai-2.0-R-small | 2048 | uni2ts | False | False | False | True | True | (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9) | False | False | Salesforce/moirai-2.0-R-small | True | CC-BY-NC-4.0 | https://huggingface.co/Salesforce/moirai-2.0-R... | True |
| TabICLAdapter | soda-inria/tabicl | (soda-inria/tabicl,) | soda-inria/tabicl | 4096 | tabicl[forecast] | True | False | False | False | True | None | False | False | jingang/TabICL | True | BSD-3-Clause | https://huggingface.co/jingang/TabICL | False |
| TabPFNAdapter | priorlabs/tabpfn-ts | (priorlabs/tabpfn,) | priorlabs/tabpfn-ts | 32768 | tabpfn-time-series>=1.3 | True | False | False | True | True | None | False | True | Prior-Labs/tabpfn_3_5 | False | tabpfn-3-5-license-v1.0 | https://huggingface.co/Prior-Labs/tabpfn_3_5/b... | True |
| T0Adapter | theforecastingcompany/t0-alpha | (theforecastingcompany/t0,) | theforecastingcompany/t0-alpha | 8192 | tfc-t0[torch]>=0.6 | True | False | False | False | True | None | False | False | theforecastingcompany/t0-alpha | True | Apache-2.0 | https://huggingface.co/theforecastingcompany/t... | False |
| NoriAdapter | Synthefy/Nori | (Synthefy/Nori,) | Synthefy/Nori | 4096 | synthefy-nori | True | False | False | True | True | None | False | False | Synthefy/Nori | True | Apache-2.0 | https://huggingface.co/Synthefy/Nori | False |
| TSICLAdapter | taharnbl/TS-ICL | (taharnbl/TS-ICL,) | taharnbl/TS-ICL | 4096 | tsicl | True | True | False | False | True | (0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.0... | False | False | taharnbl/TS-ICL | True | tsicl-v1-license-v1.0 | https://huggingface.co/taharnbl/TS-ICL/blob/ma... | True |
| TimestralAdapter | mistralai/Timestral-28M | (mistralai/Timestral,) | mistralai/Timestral-28M | 4096 | timestral @ git+https://github.com/mistralai/t... | True | True | True | False | True | (0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, ... | False | False | mistralai/Timestral-28M | True | other | https://huggingface.co/mistralai/Timestral-209... | True |
💡 Tip
All 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.
It is important to note that context length significantly impacts inference speed. Larger contexts provide the models with more information, but they increase processing time. Although these models boast massive context capacities, shorter contexts often achieve similar results much faster for most use cases.
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. All the supported models except TimesFM 2.5 and Moirai-2 accept covariates (see the Supported Foundation Models table).
| 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 os
import pandas as pd
import torch
import time
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,
bayesian_search_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()}\033[0m")
torch version: 2.14.1+cu130
Cuda available : True
MPS available : False
# 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. `Time` is in UTC (suffix Z, parsed as a naive │ │ datetime); `Date` and `Holiday` refer to local time in Melbourne (UTC+10, UTC+11 in summer). │ │ │ │ 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)
Single series forecasting¶
A ForecasterFoundation is created using Amazon's Chronos-2-small model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="autogluon/chronos-2-small", context_length=500)
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 of each adapter, see the FoundationModel API reference.
💡 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: 500
- Window size: 500
- Series names: Demand
- Exogenous included: True
- Creation date: 2026-11-12 15:47:43
- Last fit date: 2026-11-12 15:47:43
- Skforecast version: 0.27.0
- Python version: 3.13.18
- 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: h
Model Parameters
- cross_learning: False
- context_length: 500
- device_map: auto
- torch_dtype: None
- predict_kwargs: None
Three methods can be used to predict the next $n$ steps ahead: 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.
In predict_interval(), the pred column is always the median (0.5 quantile), so it may differ from the output of predict() for models whose point forecast is the mean (TabICL and Nori). In addition, some models only produce a fixed set of quantiles: TimesFM 2.5, TimesFM 3.0, Moirai and TiRex-2 return the deciles 0.1 to 0.9, Timestral 21 levels and TS-ICL the percentiles 0.01 to 0.99. With these models, both bounds of interval (and every quantile requested in predict_quantiles()) must belong to that set; other levels raise a ValueError, since quantiles are not interpolated. For example, a 95% interval ([0.025, 0.975]) cannot be obtained with the models restricted to deciles.
# 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 | 5527.678711 |
| 2014-12-01 01:00:00 | Demand | 5511.500977 |
| 2014-12-01 02:00:00 | Demand | 5457.791992 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5527.678711 | 5372.815430 | 5689.793457 |
| 2014-12-01 01:00:00 | Demand | 5511.500977 | 5318.045410 | 5733.491699 |
| 2014-12-01 02:00:00 | Demand | 5457.791992 | 5241.040039 | 5717.424805 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_chronos, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_chronos = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_chronos:.4f} seconds.")
print("Backtest metrics")
display(metrics_chronos)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 1.2366 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 171.266951 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5527.678711 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5511.500977 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5457.791992 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5402.819336 |
# 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_items = '2014-07-15 23:59:00'
data_multiseries_train = data_multiseries.loc[:end_train_items, :]
data_multiseries_test = data_multiseries.loc[end_train_items:, :]
# Plot time series
# ==============================================================================
set_dark_theme()
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(7, 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", context_length=500)
forecaster = ForecasterFoundation(estimator = estimator)
# fit() is optional; context is passed directly to predict()
# forecaster.fit(series=data_multiseries_train)
# Predictions for all series (levels)
# ==============================================================================
steps = len(data_multiseries_test)
predictions_items = forecaster.predict(
steps = steps,
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 : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_27_gpu\Lib\site-packages\skforecast\util │ │ s\utils.py:4425 │ │ Suppress : warnings.simplefilter('ignore', category=InputTypeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | |
|---|---|---|
| 2014-07-16 | item_1 | 25.523064 |
| 2014-07-16 | item_2 | 10.456666 |
| 2014-07-16 | item_3 | 11.862235 |
| 2014-07-17 | item_1 | 25.296782 |
| 2014-07-17 | item_2 | 10.701234 |
# Plot predictions
# ==============================================================================
set_dark_theme()
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(7, 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 = [0.1, 0.9], # 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 : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_27_gpu\Lib\site-packages\skforecast\util │ │ s\utils.py:4425 │ │ Suppress : warnings.simplefilter('ignore', category=InputTypeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-07-16 | item_1 | 25.464174 | 24.582005 | 26.430853 |
| 2014-07-16 | item_2 | 10.649371 | 8.679634 | 13.302807 |
| 2014-07-17 | item_1 | 25.270247 | 24.255964 | 26.327971 |
| 2014-07-17 | item_2 | 10.834244 | 8.717453 | 13.826941 |
| 2014-07-18 | item_1 | 25.175861 | 24.079086 | 26.286255 |
Impact of the context length¶
Because foundation forecasting models are highly generalized, they lack intrinsic knowledge of your specific dataset. To compensate, they rely on a "context window", a specific period of recent historical data, to adapt to your unique scenario in real-time. This context acts as the model's short-term memory, allowing it to calculate the current trajectory of your data and identify whether the series is trending upward, accelerating, or flattening out.
The length of this context window is absolutely critical for capturing seasonality and recurring events. To accurately predict a pattern, such as a weekly sales spike or a yearly cycle, the model must actually observe that pattern within the provided history. For instance, if your data has a 365-day seasonality, providing 400 days of context allows the model to recognize and project the cycle, whereas a 30-day window would cause the model to miss the pattern entirely, resulting in a flat or inaccurate forecast.
However, increasing the context length to improve accuracy introduces a significant computational trade-off. Because most foundation models are built on Transformer architectures, the computational complexity of their attention mechanism scales quadratically ($O(N^2)$) with the length of the input sequence. Consequently, doubling the context window can quadruple the required memory and processing power. This quadratic growth means that pushing context lengths to their theoretical maximum often yields diminishing accuracy gains at rapidly increasing computational cost.
Ultimately, effectively utilizing foundation models requires carefully evaluating this trade-off. The best practice is to analyze the context size and select the shortest possible window that still achieves high predictive performance. Striking this balance ensures accurate, pattern-aware forecasts while preventing the unnecessary waste of computational resources and data transfer bandwidth.
The following code shows how to evaluate the impact of the context length on the predictive accuracy and inference time of a foundation model. The example uses the Amazon Chronos-2-small model, but the same approach can be applied to any other foundation model supported by skforecast.
# Influence of the context length in the forecasting accuracy and speed
# ==============================================================================
model_id = 'autogluon/chronos-2-small'
context_lengths = [100, 500, 1000, 5000]
model_ids_allow_exog = {
'autogluon/chronos-2-small', 'soda-inria/tabicl',
'priorlabs/tabpfn-ts', 'theforecastingcompany/t0-alpha',
'Synthefy/Nori', 'taharnbl/TS-ICL', 'google/timesfm-3.0-pytorch',
'mistralai/Timestral-209M', 'NX-AI/TiRex-2'
}
if model_id in model_ids_allow_exog:
exog = data[["Temperature", "Holiday"]]
else:
exog = None
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
results_metrics = []
results_elapsed_time = []
for context_length in context_lengths:
estimator = FoundationModel(model_id=model_id, context_length=context_length)
forecaster = ForecasterFoundation(estimator=estimator)
start = time.perf_counter()
metrics, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = exog,
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True,
show_progress = False
)
elapsed_time = time.perf_counter() - start
results_metrics.append(metrics.at[0, 'mean_absolute_error'])
results_elapsed_time.append(elapsed_time)
results = pd.DataFrame(
{
'metric': results_metrics,
'elapsed_time': results_elapsed_time
},
index=context_lengths
)
results
| metric | elapsed_time | |
|---|---|---|
| 100 | 214.318938 | 2.066310 |
| 500 | 171.266951 | 2.148275 |
| 1000 | 155.492166 | 2.119351 |
| 5000 | 158.398184 | 1.949581 |
# Plot results
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(6, 2.5))
results['metric'].plot(ax = ax, marker = 'o')
ax.set_title("Prediction error vs context length")
ax.set_xlabel("context length")
ax.set_ylabel("mean absolute error")
fig, ax = plt.subplots(figsize=(6, 2.5))
results['elapsed_time'].plot(ax = ax, marker = 'o')
ax.set_title("Elapsed time vs context length")
ax.set_xlabel("context length")
ax.set_ylabel("Elapsed time");
With a context of 100 observations (about four days of hourly data), the error is high (MAE 214.3). It drops to 171.3 with 500 observations and to 155.5 with 1,000, but does not improve further with 5,000 (158.4). The inference time barely changes (a few seconds in every case): on a GPU and with a single series, the cost of a longer context is small in this example. The quadratic growth described above may matter more with many series or on the CPU.
The previous example was illustrative, showing how context length can affect results, but it is not the best search strategy. Skforecast allows optimizing context length along with other FoundationModel parameters using Bayesian search.
# Split data into train-validation-test
# ==============================================================================
data = data.loc['2012-01-01 00:00:00':'2014-12-30 23:00:00', :].copy()
end_train = '2014-10-30 23:59:00'
end_validation = '2014-11-30 23:59:00'
data_train = data.loc[: end_train, :].copy()
data_val = data.loc[end_train : end_validation, :].copy()
data_test = data.loc[end_validation:, :].copy()
print(f"Train dates : {data_train.index.min()} --- {data_train.index.max()} (n={len(data_train)})")
print(f"Validation dates: {data_val.index.min()} --- {data_val.index.max()} (n={len(data_val)})")
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-10-30 23:00:00 (n=24816) Validation dates: 2014-10-31 00:00:00 --- 2014-11-30 23:00:00 (n=744) Test dates : 2014-12-01 00:00:00 --- 2014-12-30 23:00:00 (n=720)
# Bayesian search of optimal context length
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
estimator = FoundationModel(model_id="autogluon/chronos-2-small", context_length=150)
forecaster = ForecasterFoundation(estimator=estimator)
# Search space
def search_space(trial):
search_space = {
'context_length': trial.suggest_int('context_length', 100, 5000, step=100),
}
return search_space
results, study = bayesian_search_foundation(
forecaster = forecaster,
series = data.loc[:end_validation, 'Demand'],
exog = data.loc[:end_validation, ['Temperature', 'Holiday']],
search_space = search_space,
cv = cv,
metric = 'mean_absolute_error',
n_trials = 20
)
results
| trial_number | levels | params | mean_absolute_error | n_evaluated | context_length | |
|---|---|---|---|---|---|---|
| 0 | 4 | [Demand] | {'context_length': 3600} | 139.470452 | 744 | 3600 |
| 1 | 18 | [Demand] | {'context_length': 3700} | 139.835770 | 744 | 3700 |
| 2 | 12 | [Demand] | {'context_length': 5000} | 139.935638 | 744 | 5000 |
| 3 | 6 | [Demand] | {'context_length': 5000} | 139.935638 | 744 | 5000 |
| 4 | 11 | [Demand] | {'context_length': 5000} | 139.935638 | 744 | 5000 |
| 5 | 13 | [Demand] | {'context_length': 4100} | 140.149165 | 744 | 4100 |
| 6 | 17 | [Demand] | {'context_length': 4500} | 140.165838 | 744 | 4500 |
| 7 | 14 | [Demand] | {'context_length': 4200} | 140.180651 | 744 | 4200 |
| 8 | 15 | [Demand] | {'context_length': 4200} | 140.180651 | 744 | 4200 |
| 9 | 2 | [Demand] | {'context_length': 1200} | 142.167338 | 744 | 1200 |
| 10 | 5 | [Demand] | {'context_length': 2200} | 142.248174 | 744 | 2200 |
| 11 | 1 | [Demand] | {'context_length': 1500} | 142.699090 | 744 | 1500 |
| 12 | 9 | [Demand] | {'context_length': 2000} | 143.176423 | 744 | 2000 |
| 13 | 3 | [Demand] | {'context_length': 2800} | 143.253423 | 744 | 2800 |
| 14 | 7 | [Demand] | {'context_length': 3500} | 143.339501 | 744 | 3500 |
| 15 | 0 | [Demand] | {'context_length': 3500} | 143.339501 | 744 | 3500 |
| 16 | 8 | [Demand] | {'context_length': 2500} | 143.521976 | 744 | 2500 |
| 17 | 16 | [Demand] | {'context_length': 3200} | 143.582051 | 744 | 3200 |
| 18 | 19 | [Demand] | {'context_length': 3100} | 143.681774 | 744 | 3100 |
| 19 | 10 | [Demand] | {'context_length': 500} | 149.195011 | 744 | 500 |
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. The following subsections demonstrate that the pipeline is identical regardless of the underlying model; only the model_id changes. To use a different model, simply pass it when instantiating the FoundationModel wrapper.
# Split data into train-test
# ==============================================================================
# The same partition is used to compare all the models in this section
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)
TimesFM 2.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", context_length=500)
forecaster = ForecasterFoundation(estimator = estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(series=data_train["Demand"])
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(steps=steps)
predictions.head(3)
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5658.415039 |
| 2014-12-01 01:00:00 | Demand | 5671.861816 |
| 2014-12-01 02:00:00 | Demand | 5747.938477 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5658.415039 | 5541.004883 | 5790.344727 |
| 2014-12-01 01:00:00 | Demand | 5671.861816 | 5470.189453 | 5899.113281 |
| 2014-12-01 02:00:00 | Demand | 5747.938477 | 5450.534180 | 6064.879883 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_timesfm, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_timesfm = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_timesfm:.4f} seconds.")
print("Backtest metrics")
display(metrics_timesfm)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 5.8102 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 191.928338 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5658.415039 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5671.861816 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5747.938477 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5754.575684 |
TimesFM 3.0¶
A ForecasterFoundation is created using Google's TimesFM-3.0 model. Unlike TimesFM 2.5, this version natively supports exogenous variables.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="google/timesfm-3.0-pytorch", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
steps = steps,
exog = data_test[["Temperature", "Holiday"]]
)
predictions.head(3)
╭─────────────────────────────────── LicenseWarning ───────────────────────────────────╮ │ The weights for 'google/timesfm-3.0-pytorch' are released under TimesFM │ │ Non-Commercial License v1.0. Review the license terms before commercial or │ │ production use. See │ │ https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE. │ │ │ │ Category : skforecast.exceptions.LicenseWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_27_gpu\Lib\site-packages\skforecast\foun │ │ dation\_adapters.py:1465 │ │ Suppress : warnings.simplefilter('ignore', category=LicenseWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5634.526367 |
| 2014-12-01 01:00:00 | Demand | 5622.936523 |
| 2014-12-01 02:00:00 | Demand | 5611.826172 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5634.526367 | 5476.729004 | 5773.681641 |
| 2014-12-01 01:00:00 | Demand | 5622.936523 | 5372.011719 | 5859.936035 |
| 2014-12-01 02:00:00 | Demand | 5611.826172 | 5294.573242 | 5965.582520 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_timesfm3, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_timesfm3 = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_timesfm3:.4f} seconds.")
print("Backtest metrics")
display(metrics_timesfm3)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 16.1568 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 162.435154 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5634.526367 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5622.936523 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5611.826172 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5588.210449 |
Moirai¶
A ForecasterFoundation is created using Salesforce's Moirai-2.0-R-small model.
The cells of this section are not executed in this document: the uni2ts library, which provides Moirai, requires older versions of Python and NumPy than the rest of the models. The code is the same as for any other model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="Salesforce/moirai-2.0-R-small", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(series=data_train["Demand"])
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(steps=steps)
predictions.head(3)
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_moirai, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_moirai = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_moirai:.4f} seconds.")
print("Backtest metrics")
display(metrics_moirai)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
TabICL¶
A ForecasterFoundation is created using Soda-Inria's TabICL model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="soda-inria/tabicl", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# 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 | 5494.205566 |
| 2014-12-01 01:00:00 | Demand | 5516.348145 |
| 2014-12-01 02:00:00 | Demand | 5488.497559 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5508.785156 | 4984.156738 | 5957.627930 |
| 2014-12-01 01:00:00 | Demand | 5529.266113 | 4982.987305 | 6008.017578 |
| 2014-12-01 02:00:00 | Demand | 5496.240723 | 4920.764648 | 6029.109375 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_tabicl, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_tabicl = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_tabicl:.4f} seconds.")
print("Backtest metrics")
display(metrics_tabicl)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 32.7755 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 215.802606 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5494.205566 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5516.348145 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5488.497559 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5470.052246 |
TabPFN-TS¶
A ForecasterFoundation is created using Prior Labs' TabPFN-TS model. TabPFN-TS requires a free Prior Labs API key, which you can obtain by signing up at https://priorlabs.ai. By default, inference runs locally (mode='local', CUDA > MPS > CPU) and the key is used to accept the license and download the weights; pass mode='client' to use the Prior Labs cloud API instead (no GPU needed), which also authenticates with the key.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="priorlabs/tabpfn-ts", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
steps = steps,
exog = data_test[["Temperature", "Holiday"]]
)
predictions.head(3)
╭─────────────────────────────────── LicenseWarning ───────────────────────────────────╮ │ The weights for 'priorlabs/tabpfn-ts' are released under TabPFN-3.5 License v1.0 │ │ (non-commercial without an enterprise license). Review the license terms before │ │ commercial or production use. See │ │ https://huggingface.co/Prior-Labs/tabpfn_3_5/blob/main/LICENSE. │ │ │ │ Category : skforecast.exceptions.LicenseWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_27_gpu\Lib\site-packages\skforecast\foun │ │ dation\_adapters.py:3165 │ │ Suppress : warnings.simplefilter('ignore', category=LicenseWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5653.850420 |
| 2014-12-01 01:00:00 | Demand | 5724.181737 |
| 2014-12-01 02:00:00 | Demand | 5782.585122 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5653.850420 | 5542.568024 | 5795.258160 |
| 2014-12-01 01:00:00 | Demand | 5724.181737 | 5542.035999 | 5992.667085 |
| 2014-12-01 02:00:00 | Demand | 5782.585122 | 5520.490918 | 6164.288275 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_tabpfn, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_tabpfn = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_tabpfn:.4f} seconds.")
print("Backtest metrics")
display(metrics_tabpfn)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 39.0282 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 185.661662 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5653.850420 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5724.181737 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5782.585122 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5829.590193 |
The Forecasting Company T0¶
A ForecasterFoundation is created using The Forecasting Company's T0 model. Covariates must be numeric, so encode any categorical features as numbers beforehand.
⚠ Warning
theforecastingcompany/t0* checkpoints are gated on the Hugging Face Hub. Before running the cells below, log in at the model page (e.g. huggingface.co/theforecastingcompany/t0-alpha) to accept its license, then authenticate locally, for example with hf auth login or by setting the HF_TOKEN environment variable.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="theforecastingcompany/t0-alpha", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# 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 | 5720.463867 |
| 2014-12-01 01:00:00 | Demand | 5811.913086 |
| 2014-12-01 02:00:00 | Demand | 5905.423828 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5720.463867 | 5568.763184 | 5892.993164 |
| 2014-12-01 01:00:00 | Demand | 5811.913086 | 5575.719727 | 6079.401367 |
| 2014-12-01 02:00:00 | Demand | 5905.423828 | 5562.162598 | 6246.908691 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_t0, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_t0 = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_t0:.4f} seconds.")
print("Backtest metrics")
display(metrics_t0)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 2.9476 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 172.266263 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5720.463867 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5811.913086 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5905.423828 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5954.536621 |
Synthefy Nori¶
A ForecasterFoundation is created using Synthefy Nori model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="Synthefy/Nori", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# 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 | 5114.587752 |
| 2014-12-01 01:00:00 | Demand | 5174.259389 |
| 2014-12-01 02:00:00 | Demand | 5190.578951 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_nori, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_nori = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_nori:.4f} seconds.")
print("Backtest metrics")
display(metrics_nori)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 39.2342 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 185.634038 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5114.587752 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5174.259389 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5190.578951 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5176.669940 |
EDF-Lab TS-ICL¶
A ForecasterFoundation is created using EDF-Lab TS-ICL model.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="taharnbl/TS-ICL", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
steps = steps,
exog = data_test[["Temperature", "Holiday"]]
)
predictions.head(3)
╭─────────────────────────────────── LicenseWarning ───────────────────────────────────╮ │ The weights for 'taharnbl/TS-ICL' are released under tsicl-v1-license-v1.0 │ │ (non-commercial). Review the license terms before commercial or production use. See │ │ https://huggingface.co/taharnbl/TS-ICL/blob/main/LICENSE. │ │ │ │ Category : skforecast.exceptions.LicenseWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_27_gpu\Lib\site-packages\skforecast\foun │ │ dation\_adapters.py:4329 │ │ Suppress : warnings.simplefilter('ignore', category=LicenseWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5785.186523 |
| 2014-12-01 01:00:00 | Demand | 5816.175293 |
| 2014-12-01 02:00:00 | Demand | 5784.619629 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_tsicl, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_tsicl = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_tsicl:.4f} seconds.")
print("Backtest metrics")
display(metrics_tsicl)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 1.8123 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 140.877087 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5785.186523 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5816.175293 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5784.619629 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5782.970215 |
Mistral AI Timestral¶
A ForecasterFoundation is created using Mistral AI Timestral. Timestral is published in four sizes (mistralai/Timestral-5M, mistralai/Timestral-28M, mistralai/Timestral-123M and mistralai/Timestral-209M); this example uses the largest one. It accepts past-only, known-future and categorical covariates, and returns 21 fixed quantile levels (0.01, 0.05, every multiple of 0.05 up to 0.95, and 0.99). The backend is not on PyPI yet: install it from GitHub with pip install "timestral @ git+https://github.com/mistralai/timestral.git". The weights are released under a license that grants no rights to companies with a monthly revenue above USD 20 million, so loading them raises a LicenseWarning.
⚠️ Warning
With Timestral 1.0.0, a known covariate that is constant over the context and changes over the forecast horizon (for example, a 0/1 indicator that is 0 over the whole context) can make the upper quantiles, and sometimes the median, very large (mistralai/timestral#7). In this example, it can happen with Holiday in the backtesting folds that include Christmas. skforecast warns with a FeatureOutOfRangeWarning naming the affected series and columns; setting the historical values of those columns to NaN avoids it.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="mistralai/Timestral-209M", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
steps = steps,
exog = data_test[["Temperature", "Holiday"]]
)
predictions.head(3)
╭─────────────────────────────────── LicenseWarning ───────────────────────────────────╮ │ The weights for 'mistralai/Timestral-209M' are released under Timestral Modified MIT │ │ License (no rights for companies with a monthly revenue above USD 20 million). │ │ Review the license terms before commercial or production use. See │ │ https://huggingface.co/mistralai/Timestral-209M/blob/main/LICENSE. │ │ │ │ Category : skforecast.exceptions.LicenseWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_27_gpu\Lib\site-packages\skforecast\foun │ │ dation\_adapters.py:5588 │ │ Suppress : warnings.simplefilter('ignore', category=LicenseWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| level | pred | |
|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5720.498535 |
| 2014-12-01 01:00:00 | Demand | 5793.178711 |
| 2014-12-01 02:00:00 | Demand | 5792.962402 |
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
| level | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 5720.498535 | 5639.606445 | 5811.215332 |
| 2014-12-01 01:00:00 | Demand | 5793.178711 | 5653.979004 | 5947.561523 |
| 2014-12-01 02:00:00 | Demand | 5792.962402 | 5594.829102 | 6019.723633 |
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_timestral, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_timestral = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_timestral:.4f} seconds.")
print("Backtest metrics")
display(metrics_timestral)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Backtesting completed in 4.4476 seconds. Backtest metrics
| mean_absolute_error | n_evaluated | |
|---|---|---|
| 0 | 135.670497 | 720 |
Backtest predictions
| level | fold | step | y_true | pred | |
|---|---|---|---|---|---|
| 2014-12-01 00:00:00 | Demand | 0 | 1 | 5581.380952 | 5720.498535 |
| 2014-12-01 01:00:00 | Demand | 0 | 2 | 5543.858127 | 5793.178711 |
| 2014-12-01 02:00:00 | Demand | 0 | 3 | 5594.417046 | 5792.962402 |
| 2014-12-01 03:00:00 | Demand | 0 | 4 | 5738.964024 | 5811.826172 |
NX-AI TiRex-2¶
A ForecasterFoundation is created using NX-AI TiRex-2 (NX-AI/TiRex-2). It accepts past-only and known-future numeric covariates and returns the quantiles 0.1, 0.2, ..., 0.9, so both bounds of the interval passed to predict_interval must be one of these levels. The checkpoint forecasts at most 320 steps ahead; a longer steps raises a ValueError. The backend is installed with pip install tirex-2 and requires Python >= 3.11. The weights are released under the Apache-2.0 license.
The cells of this section are not executed in this document: on Windows, TiRex-2 requires the CUDA Toolkit and a C++ compiler (Microsoft Visual C++), because its xLSTM kernels are compiled the first time they are used. On Linux, or with these tools installed, the code runs as shown.
# Create ForecasterFoundation
# ==============================================================================
estimator = FoundationModel(model_id="NX-AI/TiRex-2", context_length=500)
forecaster = ForecasterFoundation(estimator=estimator)
# Train ForecasterFoundation
# ==============================================================================
forecaster.fit(
series = data_train["Demand"],
exog = data_train[["Temperature", "Holiday"]]
)
# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
steps = steps,
exog = data_test[["Temperature", "Holiday"]]
)
predictions.head(3)
# Predictions: intervals
# ==============================================================================
predictions_intervals = forecaster.predict_interval(
steps = steps,
exog = data_test[["Temperature", "Holiday"]],
interval = [0.1, 0.9], # 80% prediction interval
)
predictions_intervals.head(3)
# Backtesting
# ==============================================================================
cv = TimeSeriesFold(
steps = 24,
initial_train_size = len(data.loc[:end_train]),
refit = False
)
start = time.perf_counter()
metrics_tirex2, backtest_predictions = backtesting_foundation(
forecaster = forecaster,
series = data['Demand'],
exog = data[["Temperature", "Holiday"]],
cv = cv,
metric = 'mean_absolute_error',
suppress_warnings = True
)
elapsed_time_tirex2 = time.perf_counter() - start
print(f"Backtesting completed in {elapsed_time_tirex2:.4f} seconds.")
print("Backtest metrics")
display(metrics_tirex2)
print("")
print("Backtest predictions")
backtest_predictions.head(4)
Model comparison¶
The following table summarizes the backtesting results (Mean Absolute Error) for the foundation models on the same dataset. Moirai and TiRex-2 are not included because their cells are not executed in this document.
# Comparison of backtesting metrics
# ==============================================================================
comparison = pd.DataFrame({
"Model": [
"Chronos-2 (small)",
"TimesFM-2.5 (200m)*",
"TimesFM-3.0",
"TabICLv2",
"TabPFN-TS",
"T0",
"Nori",
"TS-ICL",
"Timestral (209M)",
],
"mean_absolute_error": [
metrics_chronos["mean_absolute_error"].iloc[0],
metrics_timesfm["mean_absolute_error"].iloc[0],
metrics_timesfm3["mean_absolute_error"].iloc[0],
metrics_tabicl["mean_absolute_error"].iloc[0],
metrics_tabpfn["mean_absolute_error"].iloc[0],
metrics_t0["mean_absolute_error"].iloc[0],
metrics_nori["mean_absolute_error"].iloc[0],
metrics_tsicl["mean_absolute_error"].iloc[0],
metrics_timestral["mean_absolute_error"].iloc[0],
],
"Elapsed time": [
elapsed_time_chronos,
elapsed_time_timesfm,
elapsed_time_timesfm3,
elapsed_time_tabicl,
elapsed_time_tabpfn,
elapsed_time_t0,
elapsed_time_nori,
elapsed_time_tsicl,
elapsed_time_timestral,
]
}).sort_values(by="mean_absolute_error")
display(
comparison.style.highlight_min(
subset="mean_absolute_error", color="green"
).format(precision=4)
)
print("* TimesFM-2.5 (200m) does not allow the inclusion of exogenous features.")
| Model | mean_absolute_error | Elapsed time | |
|---|---|---|---|
| 8 | Timestral (209M) | 135.6705 | 4.4476 |
| 7 | TS-ICL | 140.8771 | 1.8123 |
| 2 | TimesFM-3.0 | 162.4352 | 16.1568 |
| 0 | Chronos-2 (small) | 171.2670 | 1.2366 |
| 5 | T0 | 172.2663 | 2.9476 |
| 6 | Nori | 185.6340 | 39.2342 |
| 4 | TabPFN-TS | 185.6617 | 39.0282 |
| 1 | TimesFM-2.5 (200m)* | 191.9283 | 5.8102 |
| 3 | TabICLv2 | 215.8026 | 32.7755 |
* TimesFM-2.5 (200m) does not allow the inclusion of exogenous features.
Timestral achieves the lowest error (MAE 135.7), followed by TS-ICL (140.9) and TimesFM 3.0 (162.4). All models use the same context of 500 observations and their default parameters, so the table compares them under the same conditions, not in their best configuration. For example, Chronos-2 improves from 171.3 to 155.5 with a context of 1,000 observations (see the section on the impact of the context length), which would move it to third place. With other datasets, horizons or context lengths, the ranking may change. The times correspond to a single run on a GPU and vary from one run to another.
⚠️ Warning
This example uses 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.
Session information¶
import session_info
session_info.show(html=False)
----- matplotlib 3.11.2 optuna 5.0.0 pandas 2.3.3 session_info v1.0.1 skforecast 0.27.0 torch 2.14.1+cu130 ----- IPython 9.17.1 jupyter_client 8.10.0 jupyter_core 5.9.1 ----- Python 3.13.18 | packaged by conda-forge | (main, Oct 26 2026, 11:30:40) [MSC v.1944 64 bit (AMD64)] Windows-11-10.0.26200-SP0 ----- Session information updated at 2026-11-12 15:51
Citation¶
How to cite this document
If you use this document or any part of it, please acknowledge the source, thank you!
Forecasting with foundation models 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/py79-forecasting-with-foundation-models.html
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. (2026). skforecast (v0.27.0). Zenodo. https://doi.org/10.5281/zenodo.8382787
APA:
Amat Rodrigo, J., & Escobar Ortiz, J. (2026). skforecast (Version 0.27.0) [Computer software]. https://doi.org/10.5281/zenodo.8382787
BibTeX:
@software{skforecast, author = {Amat Rodrigo, Joaquin and Escobar Ortiz, Javier}, title = {skforecast}, version = {0.27.0}, month = {11}, year = {2026}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382787} }
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.
