The TimesFM 2.5 model now supports rolling-origin backtesting, covariate integration, and anomaly detection alongside its standard forecasting capabilities. Google has released a Colab-ready implementation that handles these tasks within a single workflow.
Setup and dataset generation
The tutorial begins by configuring the runtime environment. It checks for CUDA availability and sets random seeds for reproducibility. The code then generates a synthetic retail dataset covering 1,200 days across six stores in different regions. This data includes trends, weekly and yearly seasonality, pricing elasticity, promotional lifts, holiday effects, and temperature influences.
Users load the google/timesfm-2.5-200m-pytorch model and compile it with a baseline configuration. Key settings include a maximum context of 1,024 steps and a maximum horizon of 256 steps. The model uses continuous quantile heads and enforces flip invariance to ensure consistent probabilistic outputs.
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)
try:
import jax
from sklearn import preprocessing
HAS_XREG_DEPS = True
except Exception as e:
HAS_XREG_DEPS = False
print(f"[warn] XReg deps missing ({e}); section 10 will be skipped.")
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")
t = np.arange(N_DAYS)
dow = dates.dayofweek.values
doy = dates.dayofyear.values
temp_base = 18 + 12 * np.sin(2 * np.pi * (doy - 105) / 365.25)
temp = temp_base + np.cumsum(np.random.normal(0, 0.6, N_DAYS)) * 0.15
temp = temp - np.linspace(0, temp[-1] - temp_base[-1], N_DAYS)
holiday_doy = {1, 2, 45, 100, 120, 185, 240, 300, 358, 359, 360, 361, 362, 363, 364, 365}
is_holiday = np.isin(doy, list(holiday_doy)).astype(int)
rows = []
for s in range(N_STORES):
level = 180 + 60 * s
slope = np.random.uniform(0.02, 0.09)
week_amp = np.random.uniform(15, 35)
year_amp = np.random.uniform(20, 45)
elasticity = np.random.uniform(18, 32)
promo_lift = np.random.uniform(35, 70)
temp_beta = np.random.uniform(0.8, 2.2)
phase = np.random.uniform(0, 2 * np.pi)
base_price = np.random.uniform(9.0, 13.0)
price = base_price + np.random.normal(0, 0.25, N_DAYS)
promo = (np.random.rand(N_DAYS) < 0.09).astype(int)
price = price - promo * np.random.uniform(1.2, 2.2)
weekly = week_amp * np.array([0.9, 0.7, 0.7, 0.85, 1.25, 1.8, 1.5])[dow]
yearly = year_amp * np.sin(2 * np.pi * doy / 365.25 + phase)
sales = (level
+ slope * t
+ weekly
+ yearly
- elasticity * (price - base_price)
+ promo_lift * promo
+ 55 * is_holiday
+ temp_beta * (temp - 18)
+ np.random.normal(0, 14, N_DAYS))
sales = np.clip(sales, 5, None)
rows.append(pd.DataFrame({
"date": dates,
"store": f"store_{s}",
"region": REGIONS[s],
"sales": sales.astype(np.float32),
"price": price.astype(np.float32),
"promo": promo.astype(np.int32),
"holiday": is_holiday.astype(np.int32),
"dow": dow.astype(np.int32),
"temp": temp.astype(np.float32),
}))
df = pd.concat(rows, ignore_index=True)
STORES = sorted(df["store"].unique())
print(f"\nDataset: {df.shape[0]:,} rows | {len(STORES)} stores | "
f"{dates[0].date()} → {dates[-1].date()}")
print(df.head(3).to_string(index=False))
wide = df.pivot(index="date", columns="store", values="sales")
SEASON = 7
HORIZON = 56
print("\nLoading google/timesfm-2.5-200m-pytorch ...")
t0 = time.time()
model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
"google/timesfm-2.5-200m-pytorch"
)
print(f"loaded in {time.time() - t0:.1f}s")
BASE_CFG = dict(
max_context=1024,
max_horizon=256,
normalize_inputs=True,
per_core_batch_size=16,
use_continuous_quantile_head=True,
force_flip_invariance=True,
infer_is_positive=True,
fix_quantile_crossing=True,
return_backcast=False,
)
model.compile(timesfm.ForecastConfig(**BASE_CFG))
print("compiled:", {k: v for k, v in BASE_CFG.items() if k in
("max_context", "max_horizon", "per_core_batch_size")})
def recompile(**overrides):
cfg = {**BASE_CFG, **overrides}
model.compile(timesfm.ForecastConfig(**cfg))
return cfg
Zero-shot forecasting and output structure
The model produces two distinct outputs: a point forecast representing the median prediction and a set of quantile forecasts. These quantiles allow users to construct prediction intervals directly from the model output.
For a single store, the code plots historical data against actual values and the model’s median forecast. It overlays the 10th to 90th percentile bands to visualise uncertainty. The output array contains ten indices. Index zero is the mean, while indices one through nine correspond to the 10th, 20th, 30th, 40th, 50th (median), 60th, 70th, 80th, and 90th percentiles.
The tutorial includes a function to calculate pinball loss, which measures the accuracy of the probabilistic forecasts. It also compares the TimesFM results against a seasonal naive baseline and a last-value baseline.
IDX_MEAN, IDX_Q10, IDX_Q50, IDX_Q90 = 0, 1, 5, 9
QUANTILES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
target_store = STORES[0]
series = wide[target_store].values.astype(np.float32)
train, actual = series[:-HORIZON], series[-HORIZON:]
point, quant = model.forecast(horizon=HORIZON, inputs=[train.copy()])
print(f"\nSource Read original →



