End-to-End Forecasting with TimesFM 2.5: Backtesting, Covariates, Anomaly Detection, and Scalable Colab Deployment
In this tutorial, we build an advanced end-to-end time-series forecasting workflow with TimesFM 2.5. We begin by configuring the runtime, installing the required dependencies, detecting available hardware, and generating a realistic multi-store retail dataset with trend, seasonality, pricing, promotions, holidays, temperature effects, and random variation. We then load and compile the TimesFM 2.5 model, examine its forecast configuration, and use it for zero-shot point and probabilistic forecasting. As we progress, we evaluate forecast quality with metrics such as MAE, RMSE, sMAPE, MASE, pinball loss, and prediction-interval coverage, while also testing batched inference, rolling-origin backtesting, context-length sensitivity, covariate integration through XReg, anomaly detection, long-horizon forecasting, throughput tuning, and input robustness. By working through these stages, we develop a practical understanding of how we configure, validate, benchmark, and deploy TimesFM for realistic forecasting tasks. Copy CodeCopiedUse a different Browser 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 We configure the Google Colab environment, install TimesFM and its supporting libraries, detect the available CPU or GPU, and initialize reproducible random seeds. We generate a realistic multi-store retail dataset containing trends, weekly and yearly seasonality, pricing effects, promotions, holidays, temperature variations, and random demand noise. We then load the TimesFM 2.5 model, define its baseline forecast configuration, compile it, and create a reusable function for changing model settings in later experiments. Copy CodeCopiedUse a different Browser 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”npoint {point.shape} # (n_series, horizon)”) print(f”quantile {quant.shape} # (n_series, horizon, 10)”) fig, ax = plt.subplots(figsize=(14, 5)) hist_n = 180 ax.plot(dates[-HORIZON – hist_n:-HORIZON], train[-hist_n:], color=”#334155″, lw=1.2, label=”history”) ax.plot(dates[-HORIZON:], actual, color=”#0f172a”, lw=1.6, label=”actual”) ax.plot(dates[-HORIZON:], point[0], color=”#ea580c”, lw=2, label=”TimesFM median”) for lo, hi, a in [(1, 9, .12), (2, 8, .16), (3, 7, .20), (4, 6, .24)]: ax.fill_between(dates[-HORIZON:], quant[0, :, lo], quant[0, :, hi], color=”#ea580c”, alpha=a, lw=0) ax.axvline(dates[-HORIZON], color=”#94a3b8″, ls=”–“, lw=1) ax.set_title(f”TimesFM 2.5 zero-shot — {target_store}, {HORIZON}-day horizon ” f”(fan = q10…q90)”) ax.legend(loc=”upper left”) ax.xaxis.set_major_formatter(mdates.DateFormatter(“%b %Y”)) plt.tight_layout() plt.show() print(“n— output anatomy —“) print(“index 0 = MEAN (not q0!). indices 1..9 = q10..q90. index 5 = median.”) print(“point_forecast is literally quantile[…, 5]:”, np.allclose(point, quant[…, IDX_Q50])) print(“monotone quantiles (fix_quantile_crossing):”, bool(np.all(np.diff(quant[0, :, 1:], axis=-1) >= -1e-4))) row = pd.DataFrame({ “index”: range(10), “meaning”: [“mean”] + [f”q{int(q*100)}” for q in QUANTILES], “day+1″: quant[0, 0].round(1), f”day+{HORIZON}”: quant[0, -1].round(1), }) print(row.to_string(index=False)) print(“Interval width grows with horizon — day+1 q10..q90 span ” f”{quant[0,0,9]-quant[0,0,1]:.1f}, day+{HORIZON} span ” f”{quant[0,-1,9]-quant[0,-1,1]:.1f}”) def seasonal_naive(history, horizon, season=SEASON): “””Repeat the last full season forward — the baseline you must beat.””” reps = int(np.ceil(horizon / season)) return np.tile(history[-season:], reps)[:horizon] def pinball(actual, q, quantiles=QUANTILES): “””Mean pinball (quantile) loss over q10..q90 — the probabilistic metric.””” out = [] for i, tau in enumerate(quantiles, start=1): e = actual – q[:, i] out.append(np.mean(np.maximum(tau * e, (tau – 1) * e))) return float(np.mean(out)) def evaluate(actual, pred, history, q=None, season=SEASON): actual, pred = np.asarray(actual, float), np.asarray(pred, float) err = actual – pred scale = np.mean(np.abs(history[season:] – history[:-season])) + 1e-9 m = { “MAE”: float(np.mean(np.abs(err))), “RMSE”: float(np.sqrt(np.mean(err ** 2))), “MAPE%”: float( np.mean(np.abs(err / np.maximum(np.abs(actual), 1e-9))) * 100 ), “sMAPE%”: float( np.mean( 2 * np.abs(err) / (np.abs(actual) + np.abs(pred) + 1e-9) ) * 100 ), “MASE”: float(np.mean(np.abs(err)) / scale), } if q is not None: m[“pinball”] = pinball(actual, q) m[“cov80%”] = float( np.mean( (actual >= q[:, IDX_Q10]) & (actual <= q[:, IDX_Q90]) ) * 100 ) return m base_pred =

