More about forecasting in cienciadedatos.net


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, Moirai-2, TabICL, TabPNF) 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.

skforecast ecosystem

ForecasterFoundation (forecasting)

FoundationModel (skforecast Wrapper)

.fit() (Store Context)
</div>
.predict() (Zero-Shot Run)
</div> </div>

Internal Model Adapters
Chronos
TimesFM
Moirai
TabICL
TabPFN
TFC-T0
Nori
TS-ICL
</div> </div>

</div>

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 Google GitHub Docs google/timesfm-2.5-200m-pytorch PyTorch 512 16384 512 Mean (dedicated output) No No
pip install timesfm
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

💡 Tip

All four 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. Currently, only Chronos and TabICL support 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 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.8.0+cu128
  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                   │
│                                                                          │
│ 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, see the API reference: ChronosAdapter, TimesFMAdapter, MoiraiAdapter, TabICLAdapter.

💡 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-07-28 13:55:38
  • Last fit date: 2026-07-28 13:55:38
  • Skforecast version: 0.24.0
  • Python version: 3.13.14
  • 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

📖 API Reference    📝 User Guide

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.

# Predictions: point forecast
# ==============================================================================
steps = 24
predictions = forecaster.predict(
                  steps = steps,
                  exog  = data_test[["Temperature", "Holiday"]]
              )

predictions.head(3)
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
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.815918 5689.793457
2014-12-01 01:00:00 Demand 5511.500977 5318.045410 5733.492188
2014-12-01 02:00:00 Demand 5457.791992 5241.040527 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)
  0%|          | 0/30 [00:00<?, ?it/s]
Backtesting completed in 0.8370 seconds.
Backtest metrics
mean_absolute_error
0 171.266953
Backtest predictions
level fold pred
2014-12-01 00:00:00 Demand 0 5527.678711
2014-12-01 01:00:00 Demand 0 5511.500977
2014-12-01 02:00:00 Demand 0 5457.791992
2014-12-01 03:00:00 Demand 0 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 = '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=(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_24_py13\Lib\site-packages\skforecast\uti 
 ls\utils.py:3388                                                                     
 Suppress : warnings.simplefilter('ignore', category=InputTypeWarning)                
╰──────────────────────────────────────────────────────────────────────────────────────╯
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
level pred
2014-07-16 item_1 25.523064
2014-07-16 item_2 10.456666
2014-07-16 item_3 11.862236
2014-07-17 item_1 25.296782
2014-07-17 item_2 10.701235
# 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_24_py13\Lib\site-packages\skforecast\uti 
 ls\utils.py:3388                                                                     
 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.649370 8.679634 13.302807
2014-07-17 item_1 25.270247 24.255964 26.327969
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'
}
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
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
metric elapsed_time
100 213.935341 4.997886
500 132.459974 5.062328
1000 127.277556 4.783594
5000 123.635550 4.627170
# 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");

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"Train val  : {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)
Train val  : 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
  0%|          | 0/20 [00:00<?, ?it/s]
Loading weights:   0%|          | 0/92 [00:00<?, ?it/s]
trial_number levels params mean_absolute_error context_length
0 4 [Demand] {'context_length': 3600} 139.470447 3600
1 6 [Demand] {'context_length': 5000} 139.935649 5000
2 12 [Demand] {'context_length': 5000} 139.935649 5000
3 11 [Demand] {'context_length': 5000} 139.935649 5000
4 18 [Demand] {'context_length': 5000} 139.935649 5000
5 13 [Demand] {'context_length': 4000} 140.103792 4000
6 14 [Demand] {'context_length': 4100} 140.149167 4100
7 10 [Demand] {'context_length': 4300} 140.168050 4300
8 16 [Demand] {'context_length': 4900} 140.202668 4900
9 2 [Demand] {'context_length': 1200} 142.167335 1200
10 5 [Demand] {'context_length': 2200} 142.248174 2200
11 1 [Demand] {'context_length': 1500} 142.699084 1500
12 9 [Demand] {'context_length': 2000} 143.176399 2000
13 3 [Demand] {'context_length': 2800} 143.253429 2800
14 7 [Demand] {'context_length': 3500} 143.339500 3500
15 0 [Demand] {'context_length': 3500} 143.339500 3500
16 17 [Demand] {'context_length': 3500} 143.339500 3500
17 19 [Demand] {'context_length': 3500} 143.339500 3500
18 8 [Demand] {'context_length': 2500} 143.521964 2500
19 15 [Demand] {'context_length': 100} 252.934367 100

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.

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-10-31 00:00:00 Demand 4934.027344
2014-10-31 01:00:00 Demand 4881.851074
2014-10-31 02:00:00 Demand 4884.708008
# 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-10-31 00:00:00 Demand 4934.027344 4849.165527 5009.802734
2014-10-31 01:00:00 Demand 4881.851074 4765.966309 4972.802734
2014-10-31 02:00:00 Demand 4884.708008 4743.789062 5000.303711
# 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)
  0%|          | 0/61 [00:00<?, ?it/s]
Backtesting completed in 13.5872 seconds.
Backtest metrics
mean_absolute_error
0 188.277363
Backtest predictions
level fold pred
2014-10-31 00:00:00 Demand 0 4934.027344
2014-10-31 01:00:00 Demand 0 4881.851074
2014-10-31 02:00:00 Demand 0 4884.708008
2014-10-31 03:00:00 Demand 0 4844.118164

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", 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)
level pred lower_bound upper_bound
2014-12-01 00:00:00 Demand 5731.725098 5517.737793 5940.646484
2014-12-01 01:00:00 Demand 5870.827148 5548.743164 6176.801270
2014-12-01 02:00:00 Demand 5959.207031 5599.376953 6323.206055
# 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)
  0%|          | 0/168 [00:00<?, ?it/s]
Backtesting completed in 9.3907 seconds.
Backtest metrics
mean_absolute_error
0 161.691106
Backtest predictions
level fold pred
2014-07-16 00:00:00 Demand 0 6222.097656
2014-07-16 01:00:00 Demand 0 6114.366699
2014-07-16 02:00:00 Demand 0 5969.839844
2014-07-16 03:00:00 Demand 0 5920.479492

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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location : C:\Users\Joaquin\AppData\Local\Temp\ipykernel_38512\248161195.py:4        
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred
2014-10-31 00:00:00 Demand 4921.321289
2014-10-31 01:00:00 Demand 4944.915527
2014-10-31 02:00:00 Demand 4953.985352
# 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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location :                                                                           
 c:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\fou 
 ndation\_forecaster_foundation.py:868                                                
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred lower_bound upper_bound
2014-10-31 00:00:00 Demand 4914.860352 4690.616211 5158.764160
2014-10-31 01:00:00 Demand 4931.264160 4649.990723 5255.370117
2014-10-31 02:00:00 Demand 4930.381348 4608.160156 5325.879883
# 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)
  0%|          | 0/61 [00:00<?, ?it/s]
Backtesting completed in 51.5142 seconds.
Backtest metrics
mean_absolute_error
0 210.75038
Backtest predictions
level fold pred
2014-10-31 00:00:00 Demand 0 4977.681152
2014-10-31 01:00:00 Demand 0 5165.072266
2014-10-31 02:00:00 Demand 0 5276.810059
2014-10-31 03:00:00 Demand 0 5313.650391

TabPFN-TS

A [ForecasterFoundation](../api/forecasterfoundation.html) is created using Prior Labs' TabPFN-TS model. TabPFN-TS requires a Free Prior Labs API key to run inference. You can obtain one by signing up at https://priorlabs.ai. By default inference runs locally (mode='local', CUDA > MPS > CPU); pass mode='client' to use the Prior Labs cloud API instead (no GPU needed, requires an API 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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location : C:\Users\Joaquin\AppData\Local\Temp\ipykernel_38512\248161195.py:4        
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred
2014-10-31 00:00:00 Demand 4999.521973
2014-10-31 01:00:00 Demand 4974.923340
2014-10-31 02:00:00 Demand 4976.432617
# 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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location :                                                                           
 c:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\fou 
 ndation\_forecaster_foundation.py:868                                                
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred lower_bound upper_bound
2014-10-31 00:00:00 Demand 4999.521973 4685.389160 5354.020508
2014-10-31 01:00:00 Demand 4974.923340 4640.789551 5340.963867
2014-10-31 02:00:00 Demand 4976.432617 4610.401367 5358.028320
# 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)
  0%|          | 0/61 [00:00<?, ?it/s]
Backtesting completed in 109.4941 seconds.
Backtest metrics
mean_absolute_error
0 190.96687
Backtest predictions
level fold pred
2014-10-31 00:00:00 Demand 0 4958.276855
2014-10-31 01:00:00 Demand 0 5120.209473
2014-10-31 02:00:00 Demand 0 5215.486816
2014-10-31 03:00:00 Demand 0 5241.886719

The Forecasting Company T0

A [ForecasterFoundation](../api/forecasterfoundation.html) 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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location : C:\Users\Joaquin\AppData\Local\Temp\ipykernel_38512\248161195.py:4        
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred
2014-10-31 00:00:00 Demand 4892.875488
2014-10-31 01:00:00 Demand 4826.296387
2014-10-31 02:00:00 Demand 4808.840820
# 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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location :                                                                           
 c:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\fou 
 ndation\_forecaster_foundation.py:868                                                
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred lower_bound upper_bound
2014-10-31 00:00:00 Demand 4892.875488 4820.239746 4965.842773
2014-10-31 01:00:00 Demand 4826.296387 4700.557617 4962.449219
2014-10-31 02:00:00 Demand 4808.840820 4646.588867 4980.415527
# 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)
  0%|          | 0/61 [00:00<?, ?it/s]
Backtesting completed in 5.1474 seconds.
Backtest metrics
mean_absolute_error
0 144.131346
Backtest predictions
level fold pred
2014-10-31 00:00:00 Demand 0 4904.634766
2014-10-31 01:00:00 Demand 0 4869.083496
2014-10-31 02:00:00 Demand 0 4887.306641
2014-10-31 03:00:00 Demand 0 4923.372559

Synthefy Nori

A [ForecasterFoundation](../api/forecasterfoundation.html) 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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location : C:\Users\Joaquin\AppData\Local\Temp\ipykernel_38512\248161195.py:4        
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred
2014-10-31 00:00:00 Demand 5068.465582
2014-10-31 01:00:00 Demand 5017.020701
2014-10-31 02:00:00 Demand 4990.685821
# 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)
  0%|          | 0/61 [00:00<?, ?it/s]
Backtesting completed in 80.9644 seconds.
Backtest metrics
mean_absolute_error
0 193.843538
Backtest predictions
level fold pred
2014-10-31 00:00:00 Demand 0 4867.585570
2014-10-31 01:00:00 Demand 0 5005.078139
2014-10-31 02:00:00 Demand 0 5091.125827
2014-10-31 03:00:00 Demand 0 5124.809975

EDF-Lab TS-ICL

A [ForecasterFoundation](../api/forecasterfoundation.html) 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)
╭──────────────────────────────── MissingValuesWarning ────────────────────────────────╮
 `exog` for series ['Demand'] has been reindexed to match the expected forecast       
 horizon. Missing timestamps were filled with NaN.                                    
                                                                                      
 Category : skforecast.exceptions.MissingValuesWarning                                
 Location : C:\Users\Joaquin\AppData\Local\Temp\ipykernel_38512\248161195.py:4        
 Suppress : warnings.simplefilter('ignore', category=MissingValuesWarning)            
╰──────────────────────────────────────────────────────────────────────────────────────╯
level pred
2014-10-31 00:00:00 Demand 4871.514648
2014-10-31 01:00:00 Demand 4836.692383
2014-10-31 02:00:00 Demand 4801.071777
# 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)
  0%|          | 0/61 [00:00<?, ?it/s]
Backtesting completed in 3.6687 seconds.
Backtest metrics
mean_absolute_error
0 139.935565
Backtest predictions
level fold pred
2014-10-31 00:00:00 Demand 0 4934.014648
2014-10-31 01:00:00 Demand 0 4937.680176
2014-10-31 02:00:00 Demand 0 4976.268555
2014-10-31 03:00:00 Demand 0 5069.974121

Model comparison

The following table summarizes the backtesting results (Mean Absolute Error) for the four foundation models on the same dataset.

# Comparison of backtesting metrics
# ==============================================================================
comparison = pd.DataFrame({
    "Model": [
        "Chronos-2 (small)*",
        "TimesFM-2.5 (200m)",
        "Moirai-2.0-R (small)",
        "TabICLv2*",
        "TabPFN-TS*",
        "T0",
        "Nori",
        "TS-ICL"
    ],
    "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],
        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],
    ],
    "Elapsed time": [
        elapsed_time_chronos,
        elapsed_time_timesfm,
        elapsed_time_moirai,
        elapsed_time_tabicl,
        elapsed_time_tabpfn,
        elapsed_time_t0,
        elapsed_time_nori,
        elapsed_time_tsicl
        

    ]
}).sort_values(by="mean_absolute_error")

display(
    comparison.style.highlight_min(
        subset="mean_absolute_error", color="green"
    ).format(precision=4)
)
print("* Chronos-2 (small), TabICL, TabPFN-TS, TFC T0, Nori and TS-ICL allow the inclusion of exogenous features.")
  Model mean_absolute_error Elapsed time
7 TS-ICL 139.9356 3.6687
5 T0 144.1313 5.1474
2 Moirai-2.0-R (small) 161.6911 9.3907
0 Chronos-2 (small)* 171.2670 0.8370
1 TimesFM-2.5 (200m) 188.2774 13.5872
4 TabPFN-TS* 190.9669 109.4941
6 Nori 193.8435 80.9644
3 TabICLv2* 210.7504 51.5142
* Chronos-2 (small), TabICL, TabPFN-TS, TFC T0, Nori and TS-ICL allow the inclusion of exogenous features.

⚠️ 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.1
optuna              4.9.0
pandas              2.3.3
session_info        v1.0.1
skforecast          0.24.0
torch               2.8.0+cu128
-----
IPython             9.15.0
jupyter_client      8.9.1
jupyter_core        5.9.1
-----
Python 3.13.14 | packaged by conda-forge | (main, Jun 12 2026, 09:44:26) [MSC v.1944 64 bit (AMD64)]
Windows-11-10.0.26200-SP0
-----
Session information updated at 2026-07-28 14:06

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. (2024). skforecast (v0.24.0). Zenodo. https://doi.org/10.5281/zenodo.8382787

APA:

Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast (Version 0.24.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.24.0}, month = {07}, year = {2026}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382788} }


Did you like the article? Your support is important

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

Become a GitHub Sponsor Become a GitHub Sponsor

Creative Commons Licence

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

Allowed:

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

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

Under the following terms:

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

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

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