End-to-End Forecasting with TimesFM 2.5: Backtesting, Covariates, Anomaly Detection, and Scalable Colab Deployment

anomaly detectionbacktestingcolab deploymentcovariatesprobabilistic forecastingrolling-origin evaluationtime-series forecastingtimesfm 2.5xreg

Introduction


This tutorial presents an advanced end-to-end time-series forecasting workflow using TimesFM 2.5, a state-of-the-art foundation model for time-series from Google Research. We start by configuring the runtime, installing dependencies, and generating a realistic multi-store retail dataset that incorporates trend, seasonality, pricing, promotions, holidays, temperature effects, and random noise. We then load and compile the TimesFM 2.5 model and perform both zero-shot point and probabilistic forecasting.


As we progress, we evaluate forecast quality using metrics such as MAE, RMSE, sMAPE, MASE, pinball loss, and prediction-interval coverage. We also test batched inference, rolling-origin backtesting, context-length sensitivity, covariate integration via XReg, anomaly detection, long-horizon forecasting, throughput tuning, and input robustness. By the end, you will have a practical framework for configuring, validating, benchmarking, and deploying TimesFM in real-world forecasting scenarios.


Environment Setup


We begin by installing the required libraries and checking the available hardware. The code below ensures all dependencies are present and sets up a reproducible environment.


FAST_MODE = False
SEED = 7
import subprocess, sys, os, time, json, math, warnings
warnings.filterwarnings("ignore")
def _pip(*args):
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *args])
try:
    import timesfm
except ImportError:
    print("Installing timesfm[torch] ... (~1-2 min)")
    _pip("timesfm[torch]")
    import timesfm
import numpy as np
import pandas as pd
import torch
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.set_float32_matmul_precision("high")
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
print("=" * 78)
print(f"timesfm  : {getattr(timesfm, '__version__', 'n/a')}")
print(f"torch    : {torch.__version__}")
print(f"device   : {DEVICE}")
if DEVICE == "cuda":
    print(f"gpu      : {torch.cuda.get_device_name(0)} "
          f"({torch.cuda.get_device_properties(0).total_memory/1e9:.1f} GB)")
print("=" * 78)

Data Generation


We simulate a retail environment with six stores across three regions. Each store’s daily sales are influenced by trend, weekly seasonality, holidays, promotions, price, and temperature. The following code generates the dataset.


N_DAYS = 1200
N_STORES = 6
REGIONS = ["north", "north", "south", "south", "coast", "coast"]
dates = pd.date_range("2021-01-01", periods=N_DAYS, freq="D")
# Generate base features
df = pd.DataFrame({"date": dates})
for i in range(N_STORES):
    store_name = f"store_{i+1}"
    region = REGIONS[i]
    # Trend + seasonality + noise
    trend = np.linspace(100, 180, N_DAYS) if region == "north" else \
            np.linspace(110, 200, N_DAYS) if region == "south" else \
            np.linspace(120, 220, N_DAYS)
    weekly = 10 * np.sin(2 * np.pi * np.arange(N_DAYS) / 7)
    noise = np.random.normal(0, 5, N_DAYS)
    sales = trend + weekly + noise
    # Add holiday effect (e.g., New Year, Christmas)
    holidays = pd.to_datetime(["2021-01-01", "2021-12-25", "2022-01-01", "2022-12-25", "2023-01-01"])
    holiday_effect = 20 * (df["date"].isin(holidays).astype(int))
    sales += holiday_effect
    # Promotion effect
    promo = np.random.choice([0, 1], size=N_DAYS, p=[0.95, 0.05])
    sales += promo * 30
    # Price effect
    price = np.random.normal(10, 2, N_DAYS)
    sales -= (price - 10) * 5
    # Temperature effect
    temp = 15 + 10 * np.sin(2 * np.pi * np.arange(N_DAYS) / 365) + np.random.normal(0, 2, N_DAYS)
    sales += (temp - 15) * 0.5
    df[f"sales_{store_name}"] = sales
    df[f"price_{store_name}"] = price
    df[f"promo_{store_name}"] = promo
    df[f"temp_{store_name}"] = temp

Model Loading and Configuration


TimesFM 2.5 can be used with several backends (PyTorch, JAX). We show loading the model with PyTorch and inspecting its default forecast configuration.


import timesfm
tfm = timesfm.TimesFm(
    hparams=timesfm.TimesFmHparams(
        backend="gpu",
        per_core_batch_size=32,
        horizon_len=128,
        num_layers=20,
        model_dims=1280,
    ),
    checkpoint=timesfm.TimesFmCheckpoint(
        huggingface_repo_id="google/timesfm-2.5-200m-pytorch"
    ),
)
tfm.load_from_checkpoint()
print("Model loaded. Forecast config:", tfm.hparams)

Zero-Shot Forecasting


TimesFM performs zero-shot forecasting, meaning it can generate forecasts without additional training. We demonstrate both point and probabilistic forecasts.


import numpy as np
# Prepare input: shape (batch, context_len)
context = df["sales_store_1"].values[-tfm.hparams.context_len:]
context = context.reshape(1, -1)
# Point forecast
point_forecast = tfm.forecast(context)
print("Point forecast shape:", point_forecast.shape)
# Probabilistic forecast (quantiles)
mean, quantiles = tfm.forecast(context, return_quantiles=True)
print("Quantiles shape:", quantiles.shape)

The model outputs predictions for the next horizon_len steps (default 128). You can use these to plot and compare against actuals if available.


Evaluation Metrics


We define functions to compute MAE, RMSE, sMAPE, MASE, pinball loss, and prediction-interval coverage.


def mae(y_true, y_pred):
    return np.mean(np.abs(y_true - y_pred))
def rmse(y_true, y_pred):
    return np.sqrt(np.mean((y_true - y_pred)**2))
def smape(y_true, y_pred):
    denom = (np.abs(y_true) + np.abs(y_pred)) / 2
    return 100 * np.mean(np.abs(y_true - y_pred) / np.where(denom == 0, 1, denom))
def mase(y_true, y_pred, y_train):
    n = len(y_train)
    naive_errors = np.abs(y_train[1:] - y_train[:-1])
    scale = np.mean(naive_errors)
    return np.mean(np.abs(y_true - y_pred)) / scale if scale > 0 else np.nan
def pinball_loss(y_true, y_pred, q):
    e = y_true - y_pred
    return np.mean(np.where(e >= 0, q * e, (q - 1) * e))
def interval_coverage(y_true, lower, upper):
    return np.mean((y_true >= lower) & (y_true <= upper))

Rolling-Origin Backtesting


To validate forecast stability, we implement rolling-origin backtesting: we train (or use the pretrained model) and evaluate over multiple origins.


def backtest(model, data, horizon, n_origins=5):
    results = []
    total_len = len(data)
    step = (total_len - horizon) // (n_origins + 1)
    for i in range(1, n_origins + 1):
        cutoff = total_len - i * step
        train = data[:cutoff]
        test = data[cutoff:cutoff + horizon]
        # Zero-shot: use model directly
        forecast = model.forecast(train[-model.hparams.context_len:].reshape(1, -1))
        results.append((test, forecast))
    return results

Evaluate each backtest fold with the metrics defined above.


Covariate Integration with XReg


TimesFM supports external regressors via XReg. This is useful for incorporating promotions, holidays, or other exogenous variables.


# Prepare covariates: shape (batch, context_len, num_covariates)
# For simplicity, use price and promo as covariates
context_covariates = np.column_stack([price[-context_len:], promo[-context_len:]]).reshape(1, -1, 2)
# Forecast with covariates
mean, quantiles = tfm.forecast(
    context,
    xreg=context_covariates,
    forecast_xreg=some_future_covariates,  # shape (1, horizon, num_covariates)
    return_quantiles=True,
)

Anomaly Detection


We can detect anomalies by comparing actual values to the forecast distribution. Points outside a high-confidence interval are flagged.


def detect_anomalies(y_true, lower, upper, threshold=0.05):
    anomalies = (y_true < lower) | (y_true > upper)
    return anomalies
# Example
lower = quantiles[0, :, 0]  # 5th percentile
upper = quantiles[0, :, -1]  # 95th percentile
anomalies = detect_anomalies(actual_values, lower, upper)
print(f"Found {np.sum(anomalies)} anomalies")

Long-Horizon Forecasting and Throughput Tuning


For long horizons, you can increase horizon_len (e.g., 512) and adjust batch size for throughput. TimesFM supports batching multiple series at once.


# Batch inference
contexts = np.stack([df[f"sales_{s}"].values[-context_len:] for s in range(1, N_STORES+1)])
forecasts = tfm.forecast(contexts, batch_size=32)  # shape (batch, horizon)
# Throughput test
start = time.time()
for _ in range(100):
    tfm.forecast(contexts[:1])
print(f"Throughput: {100 / (time.time() - start):.2f} inferences/sec")

Input Robustness


TimesFM is robust to input length variations and missing values (to some extent). We test by truncating context and adding small noise.


# Truncated context
short_context = context[:, -50:]
forecast_short = tfm.forecast(short_context)
# Noisy input
noisy_context = context + np.random.normal(0, 0.1, context.shape)
forecast_noisy = tfm.forecast(noisy_context)

Deployment on Google Colab


TimesFM can be deployed on Google Colab (free or Pro) with a single GPU. The notebook should:

  • Use a T4 or V100 GPU runtime
  • Install timesfm from the repository
  • Set environment variables for PyTorch caching
  • Use the --cpu flag if GPU is not available (slower)

A typical Colab cell:


!pip install -q timesfm[torch]
import timesfm
# ... rest of the script

For persistent deployment, consider saving the model to Google Drive and loading it later.


Conclusion


We have covered a complete forecasting pipeline using TimesFM 2.5, from data generation to advanced evaluation and deployment. The model’s zero-shot capability, combined with its support for covariates and probabilistic outputs, makes it a powerful tool for real-world forecasting. By following the steps in this tutorial, you can adapt TimesFM to your own time-series problems, validate its performance with robust backtesting, and scale your deployment into production environments.


References



Article written by Sana Hassan, published on August 1, 2026.

via MarkTechPost

Related