Close Menu
NCIJ Network NCIJ Network
    What's Hot

    Can Democratic Socialists Agree on Anything? They Say That’s Not the Point.

    August 2, 2026

    Uber is building an autonomous vehicle empire, and here’s every company it’s using to do it

    August 2, 2026

    Coldcard Hack Expands as Bitcoin Losses Reach $88.6M

    August 2, 2026
    Facebook X (Twitter) Instagram
    Trending
    • Can Democratic Socialists Agree on Anything? They Say That’s Not the Point.
    • Uber is building an autonomous vehicle empire, and here’s every company it’s using to do it
    • Coldcard Hack Expands as Bitcoin Losses Reach $88.6M
    • Scientists found two mysterious ‘ghost’ ancestors hiding in our DNA
    • At least three killed in Moscow restaurant bombing, Russian officials say
    • This $9 key physically locks your most addictive apps
    • End-to-End Forecasting with TimesFM 2.5: Backtesting, Covariates, Anomaly Detection, and Scalable Colab Deployment
    • Coinkite Releases Fixed Firmware After Coldcard Bug; AI Likely Involved In The Breach
    • About
      • Our Team
      • Editorial Policy
      • Editorial Independence
      • International Support
    • Trust & Standards
      • AI Usage Policy
      • Conflict of Interest Policy
      • Corrections Policy
      • Ethics Policy
      • Fact-Checking Policy
      • Source Protection
    • Get Involved
      • Guide for Sources
      • Support Independent Journalism
    • Legal
      • Cookie Policy
      • Privacy Policy
      • Terms of Use
    Facebook X (Twitter) Instagram
    NCIJ Network NCIJ Network
    Sunday, August 2
    • Home
    • World
    • Ai
    • Business
    • Politics
    • Health
    • Crypto
    • Science
    • Technology
    • Cybersecurity
    • Defense & Security
    • Economy
    • Energy
    • Europe
    • More
      • Fact Check
      • Investigations
      • Opinion & Analysis
      • Environment
    NCIJ Network NCIJ Network
    Home»Artificial Intelligence

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

    NCIJ NETWNCIJ NETWORKBy NCIJ NETWNCIJ NETWORKAugust 2, 2026 Artificial Intelligence No Comments18 Mins Read
    Share
    Facebook Twitter LinkedIn Pinterest Email

    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.

    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.

    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 = seasonal_naive(train, HORIZON)
    cmp = pd.DataFrame({
       "TimesFM 2.5": evaluate(actual, point[0], train, quant[0]),
       "seasonal naive": evaluate(actual, base_pred, train),
       "last value": evaluate(
           actual,
           np.repeat(train[-1], HORIZON),
           train
       ),
    }).T
    print("n--- single-series accuracy ---")
    print(cmp.round(3).to_string())
    print("MASE < 1 means you beat seasonal naive. cov80% should sit near 80.")
    inputs = [
       wide[s].values[:-HORIZON].astype(np.float32).copy()
       for s in STORES
    ]
    t0 = time.time()
    b_point, b_quant = model.forecast(
       horizon=HORIZON,
       inputs=list(inputs)
    )
    elapsed = time.time() - t0
    print(f"n{len(STORES)} series forecast in {elapsed:.2f}s "
         f"({elapsed/len(STORES)*1000:.0f} ms/series)")
    per_series = {}
    for i, s in enumerate(STORES):
       hist = wide[s].values[:-HORIZON]
       act = wide[s].values[-HORIZON:]
       per_series[s] = evaluate(
           act,
           b_point[i],
           hist,
           b_quant[i]
       )
    tbl = pd.DataFrame(per_series).T
    print(tbl.round(3).to_string())
    print("mean MASE:", round(tbl["MASE"].mean(), 3),
         "| mean 80% coverage:", round(tbl["cov80%"].mean(), 1))
    fig, axes = plt.subplots(2, 3, figsize=(16, 7), sharex=True)
    for ax, s, i in zip(
       axes.ravel(),
       STORES,
       range(len(STORES))
    ):
       hist = wide[s].values[:-HORIZON]
       ax.plot(
           dates[-HORIZON-90:-HORIZON],
           hist[-90:],
           color="#475569",
           lw=1
       )
       ax.plot(
           dates[-HORIZON:],
           wide[s].values[-HORIZON:],
           color="#0f172a",
           lw=1.4
       )
       ax.plot(
           dates[-HORIZON:],
           b_point[i],
           color="#ea580c",
           lw=1.8
       )
       ax.fill_between(
           dates[-HORIZON:],
           b_quant[i, :, IDX_Q10],
           b_quant[i, :, IDX_Q90],
           color="#ea580c",
           alpha=.18,
           lw=0
       )
       ax.set_title(
           f"{s}  MASE={per_series[s]['MASE']:.2f}",
           fontsize=10
       )
       ax.tick_params(labelsize=8)
    fig.suptitle(
       "Batched zero-shot forecasts "
       "(orange = median, band = 80% PI)"
    )
    plt.tight_layout()
    plt.show()
    

    We generate our first zero-shot forecast and visualize the historical data, actual observations, median predictions, and probabilistic quantile bands in a fan chart. We inspect the structure of the point and quantile outputs, define evaluation functions for MAE, RMSE, MAPE, sMAPE, MASE, pinball loss, and prediction-interval coverage, and compare TimesFM against simple forecasting baselines. We also forecast all store series in a single batch, calculate store-level performance metrics, and plot the resulting forecasts and uncertainty intervals across the complete retail dataset.

    N_FOLDS = 3 if FAST_MODE else 6
    STEP = HORIZON
    print(f"n--- rolling-origin backtest: {N_FOLDS} folds x {HORIZON}d x "
         f"{len(STORES)} series ---")
    fold_rows = []
    for k in range(N_FOLDS):
       end = N_DAYS - k * STEP
       cut = end - HORIZON
       fold_inputs, fold_actuals, fold_hists = [], [], []
       for s in STORES:
           v = wide[s].values.astype(np.float32)
           fold_inputs.append(v[:cut].copy())
           fold_actuals.append(v[cut:end])
           fold_hists.append(v[:cut])
       fp, fq = model.forecast(
           horizon=HORIZON,
           inputs=list(fold_inputs)
       )
       for i, s in enumerate(STORES):
           m_tfm = evaluate(
               fold_actuals[i],
               fp[i],
               fold_hists[i],
               fq[i]
           )
           m_snv = evaluate(
               fold_actuals[i],
               seasonal_naive(fold_hists[i], HORIZON),
               fold_hists[i]
           )
           fold_rows.append({
               "fold": k,
               "store": s,
               "model": "TimesFM",
               **m_tfm
           })
           fold_rows.append({
               "fold": k,
               "store": s,
               "model": "snaive",
               **m_snv
           })
    bt = pd.DataFrame(fold_rows)
    summary = bt.groupby("model")[
       ["MAE", "RMSE", "sMAPE%", "MASE"]
    ].mean()
    print(summary.round(3).to_string())
    lift = (
       1 -
       summary.loc["TimesFM", "MASE"] /
       summary.loc["snaive", "MASE"]
    )
    print(
       f"TimesFM reduces MASE by "
       f"{lift*100:.1f}% vs seasonal naive, zero-shot."
    )
    fig, ax = plt.subplots(figsize=(10, 4))
    piv = bt.pivot_table(
       index="fold",
       columns="model",
       values="MASE"
    )
    piv.plot(
       kind="bar",
       ax=ax,
       color=["#ea580c", "#94a3b8"],
       width=.8
    )
    ax.set_ylabel("MASE (lower is better)")
    ax.axhline(1.0, color="k", ls=":", lw=1)
    ax.set_title("Rolling-origin backtest — MASE per fold")
    plt.tight_layout()
    plt.show()
    ctx_grid = (
       [128, 512]
       if FAST_MODE
       else [64, 128, 256, 512, 1024]
    )
    print(f"n--- context ablation {ctx_grid} ---")
    abl = []
    for ctx in ctx_grid:
       recompile(max_context=ctx)
       ins = [
           wide[s].values[:-HORIZON][-ctx:]
           .astype(np.float32)
           .copy()
           for s in STORES
       ]
       t0 = time.time()
       p, q = model.forecast(
           horizon=HORIZON,
           inputs=list(ins)
       )
       dt = time.time() - t0
       ms = [
           evaluate(
               wide[s].values[-HORIZON:],
               p[i],
               wide[s].values[:-HORIZON],
               q[i]
           )
           for i, s in enumerate(STORES)
       ]
       abl.append({
           "context": ctx,
           "MASE": np.mean([m["MASE"] for m in ms]),
           "sMAPE%": np.mean([m["sMAPE%"] for m in ms]),
           "cov80%": np.mean([m["cov80%"] for m in ms]),
           "sec": round(dt, 2)
       })
    abl = pd.DataFrame(abl)
    print(abl.round(3).to_string(index=False))
    fig, ax1 = plt.subplots(figsize=(9, 4))
    ax1.plot(
       abl["context"],
       abl["MASE"],
       "o-",
       color="#ea580c"
    )
    ax1.set_xscale("log", base=2)
    ax1.set_xlabel("context length (days)")
    ax1.set_ylabel("MASE", color="#ea580c")
    ax2 = ax1.twinx()
    ax2.bar(
       abl["context"],
       abl["sec"],
       width=abl["context"] * 0.35,
       alpha=.25,
       color="#0369a1"
    )
    ax2.set_ylabel("wall-clock (s)", color="#0369a1")
    ax1.set_title("Accuracy vs. context length vs. cost")
    plt.tight_layout()
    plt.show()
    recompile()
    

    We perform rolling-origin backtesting across multiple historical cutoffs to evaluate TimesFM under several realistic forecasting periods instead of relying on one holdout window. We compare the model with a seasonal-naive baseline, summarize the average error metrics, calculate the improvement in MASE, and visualize performance across backtest folds. We then run a context-length ablation study to determine how different amounts of historical data influence forecast accuracy, interval coverage, and inference time.

    if HAS_XREG_DEPS:
       CTX_X, H_X = 512, 56
       recompile(
           max_context=CTX_X,
           max_horizon=128,
           return_backcast=True
       )
       x_inputs = []
       dyn_num = {"price": [], "temp": []}
       dyn_cat = {"promo": [], "holiday": [], "dow": []}
       stat_cat = {"region": [], "store": []}
       for s in STORES:
           g = df[df["store"] == s].reset_index(drop=True)
           n = len(g)
           ctx_lo, ctx_hi = n - H_X - CTX_X, n - H_X
           x_inputs.append(
               g["sales"]
               .values[ctx_lo:ctx_hi]
               .astype(np.float32)
           )
           for name, col in [
               ("price", "price"),
               ("temp", "temp")
           ]:
               dyn_num[name].append(
                   g[col]
                   .values[ctx_lo:n]
                   .astype(np.float32)
               )
           for name, col in [
               ("promo", "promo"),
               ("holiday", "holiday"),
               ("dow", "dow")
           ]:
               dyn_cat[name].append(
                   g[col]
                   .values[ctx_lo:n]
                   .astype(np.int32)
                   .tolist()
               )
           stat_cat["region"].append(g["region"].iloc[0])
           stat_cat["store"].append(s)
       x_actuals = [
           wide[s].values[-H_X:]
           for s in STORES
       ]
       x_hists = [
           wide[s].values[:-H_X]
           for s in STORES
       ]
       print(
           f"n--- XReg: inputs len={len(x_inputs[0])}, "
           f"covariate len={len(dyn_num['price'][0])} "
           f"=> inferred horizon "
           f"{len(dyn_num['price'][0]) - len(x_inputs[0])} ---"
       )
       xreg_results = {}
       for mode in ["xreg + timesfm", "timesfm + xreg"]:
           t0 = time.time()
           p_list, q_list = model.forecast_with_covariates(
               inputs=[a.copy() for a in x_inputs],
               dynamic_numerical_covariates=dyn_num,
               dynamic_categorical_covariates=dyn_cat,
               static_categorical_covariates=stat_cat,
               xreg_mode=mode,
               normalize_xreg_target_per_input=True,
               ridge=1.0,
               force_on_cpu=False,
           )
           dt = time.time() - t0
           ms = [
               evaluate(
                   x_actuals[i],
                   np.asarray(p_list[i]),
                   x_hists[i],
                   np.asarray(q_list[i])
               )
               for i in range(len(STORES))
           ]
           xreg_results[mode] = (
               np.asarray(p_list),
               np.asarray(q_list),
               ms,
               dt
           )
           print(
               f"{mode:>16}: "
               f"MASE={np.mean([m['MASE'] for m in ms]):.3f} "
               f"sMAPE={np.mean([m['sMAPE%'] for m in ms]):.2f}% "
               f"({dt:.1f}s)"
           )
       recompile(
           max_context=CTX_X,
           max_horizon=128,
           return_backcast=False
       )
       p_uni, q_uni = model.forecast(
           horizon=H_X,
           inputs=[a.copy() for a in x_inputs]
       )
       ms_uni = [
           evaluate(
               x_actuals[i],
               p_uni[i],
               x_hists[i],
               q_uni[i]
           )
           for i in range(len(STORES))
       ]
       print(
           f"{'univariate':>16}: "
           f"MASE={np.mean([m['MASE'] for m in ms_uni]):.3f} "
           f"sMAPE={np.mean([m['sMAPE%'] for m in ms_uni]):.2f}%"
       )
       comp = pd.DataFrame({
           "univariate": pd.DataFrame(ms_uni).mean(),
           "xreg + timesfm": pd.DataFrame(
               xreg_results["xreg + timesfm"][2]
           ).mean(),
           "timesfm + xreg": pd.DataFrame(
               xreg_results["timesfm + xreg"][2]
           ).mean(),
       }).T
       print("n", comp.round(3).to_string())
       best_mode = min(
           xreg_results,
           key=lambda m: np.mean(
               [x["MASE"] for x in xreg_results[m][2]]
           )
       )
       p_best = xreg_results[best_mode][0]
       fig, axes = plt.subplots(
           2,
           2,
           figsize=(15, 7),
           sharex=True
       )
       for ax, i in zip(axes.ravel(), range(4)):
           ax.plot(
               dates[-H_X - 60:-H_X],
               x_hists[i][-60:],
               color="#475569",
               lw=1
           )
           ax.plot(
               dates[-H_X:],
               x_actuals[i],
               color="#0f172a",
               lw=1.5,
               label="actual"
           )
           ax.plot(
               dates[-H_X:],
               p_uni[i],
               color="#94a3b8",
               lw=1.6,
               ls="--",
               label="univariate"
           )
           ax.plot(
               dates[-H_X:],
               p_best[i],
               color="#ea580c",
               lw=1.8,
               label=f"{best_mode}"
           )
           pr = df[
               df["store"] == STORES[i]
           ]["promo"].values[-H_X:]
           for j in np.where(pr == 1)[0]:
               d = dates[-H_X + j]
               ax.axvspan(
                   d - pd.Timedelta(hours=12),
                   d + pd.Timedelta(hours=12),
                   color="#16a34a",
                   alpha=.25,
                   lw=0
               )
           ax.set_title(STORES[i], fontsize=10)
           ax.tick_params(labelsize=8)
       axes[0, 0].legend(fontsize=8)
       fig.suptitle(
           "Covariate forecasting "
           "(green = promotion days known in advance)"
       )
       plt.tight_layout()
       plt.show()
    else:
       print(
           "n[skip] section 10 — "
           "install scikit-learn + jax to run XReg."
       )
    recompile()
    

    We incorporate numerical, categorical, and static covariates into the forecasting workflow through TimesFM’s XReg functionality. We compare the xreg + timesfm and timesfm + xreg fusion modes against a univariate forecast while using price, temperature, promotion, holiday, weekday, region, and store information. We evaluate the competing approaches, identify the best-performing covariate mode, and visualize how its predictions respond to known future promotion periods.

    sig = wide[STORES[1]].values.astype(np.float32).copy()
    truth_idx = [
       N_DAYS - 210,
       N_DAYS - 128,
       N_DAYS - 61,
       N_DAYS - 20
    ]
    sig[truth_idx[0]] *= 1.9
    sig[truth_idx[1]] *= 0.35
    sig[truth_idx[2]:truth_idx[2] + 5] *= 1.5
    sig[truth_idx[3]] *= 0.45
    DET_WIN, DET_START = 14, N_DAYS - 280
    flags = []
    for start in range(
       DET_START,
       N_DAYS - DET_WIN + 1,
       DET_WIN
    ):
       ctx = sig[max(0, start - 512):start]
       p, q = model.forecast(
           horizon=DET_WIN,
           inputs=[ctx.copy()]
       )
       act = sig[start:start + DET_WIN]
       lo90 = q[0, :, IDX_Q10]
       hi90 = q[0, :, IDX_Q90]
       lo_ext = (
           q[0, :, IDX_Q10] -
           1.5 * (
               q[0, :, IDX_Q50] -
               q[0, :, IDX_Q10]
           )
       )
       hi_ext = (
           q[0, :, IDX_Q90] +
           1.5 * (
               q[0, :, IDX_Q90] -
               q[0, :, IDX_Q50]
           )
       )
       for j in range(DET_WIN):
           sev = (
               "CRITICAL"
               if (
                   act[j] < lo_ext[j] or
                   act[j] > hi_ext[j]
               )
               else "WARNING"
               if (
                   act[j] < lo90[j] or
                   act[j] > hi90[j]
               )
               else "ok"
           )
           flags.append({
               "i": start + j,
               "date": dates[start + j],
               "actual": act[j],
               "lo": lo90[j],
               "hi": hi90[j],
               "sev": sev,
               "z": (
                   act[j] -
                   q[0, j, IDX_Q50]
               ) / max(
                   (hi90[j] - lo90[j]) / 2.563,
                   1e-6
               )
           })
    fl = pd.DataFrame(flags)
    alerts = fl[fl.sev != "ok"]
    print(
       f"n--- anomaly detection: "
       f"{len(alerts)} flags in {len(fl)} points "
       f"({len(alerts)/len(fl)*100:.1f}% — "
       f"expect ~20% by construction) ---"
    )
    print(
       alerts[
           alerts.sev == "CRITICAL"
       ][
           ["date", "actual", "lo", "hi", "z", "sev"]
       ]
       .round(2)
       .to_string(index=False)
    )
    print(
       "injected anomalies at:",
       [str(dates[i].date()) for i in truth_idx]
    )
    fig, ax = plt.subplots(figsize=(14, 4.5))
    ax.plot(
       fl["date"],
       fl["actual"],
       color="#0f172a",
       lw=1.1,
       label="observed"
    )
    ax.fill_between(
       fl["date"],
       fl["lo"],
       fl["hi"],
       color="#0369a1",
       alpha=.15,
       lw=0,
       label="80% predictive interval"
    )
    w = fl[fl.sev == "WARNING"]
    c = fl[fl.sev == "CRITICAL"]
    ax.scatter(
       w["date"],
       w["actual"],
       s=28,
       color="#f59e0b",
       zorder=3,
       label="warning"
    )
    ax.scatter(
       c["date"],
       c["actual"],
       s=60,
       color="#dc2626",
       zorder=4,
       marker="X",
       label="critical"
    )
    ax.set_title(
       "Anomaly detection from TimesFM predictive intervals"
    )
    ax.legend(loc="upper left", fontsize=8)
    plt.tight_layout()
    plt.show()
    LONG_H = 256
    long_train = (
       wide[STORES[2]]
       .values[:-LONG_H]
       .astype(np.float32)
    )
    long_act = wide[STORES[2]].values[-LONG_H:]
    recompile(max_horizon=256)
    p_direct, q_direct = model.forecast(
       horizon=LONG_H,
       inputs=[long_train.copy()]
    )
    recompile(max_horizon=128)
    CHUNK = 64
    ctx_roll = long_train.copy()
    rec = []
    for _ in range(LONG_H // CHUNK):
       p_c, _ = model.forecast(
           horizon=CHUNK,
           inputs=[ctx_roll[-1024:].copy()]
       )
       rec.append(p_c[0])
       ctx_roll = np.concatenate([
           ctx_roll,
           p_c[0]
       ])
    p_rec = np.concatenate(rec)
    print(f"n--- long horizon ({LONG_H} days) ---")
    print(
       pd.DataFrame({
           "direct": evaluate(
               long_act,
               p_direct[0],
               long_train,
               q_direct[0]
           ),
           f"recursive x{CHUNK}": evaluate(
               long_act,
               p_rec,
               long_train
           ),
       })
       .T
       .round(3)
       .to_string()
    )
    fig, ax = plt.subplots(figsize=(14, 4.5))
    ax.plot(
       dates[-LONG_H-120:-LONG_H],
       long_train[-120:],
       color="#475569",
       lw=1
    )
    ax.plot(
       dates[-LONG_H:],
       long_act,
       color="#0f172a",
       lw=1.4,
       label="actual"
    )
    ax.plot(
       dates[-LONG_H:],
       p_direct[0],
       color="#ea580c",
       lw=1.8,
       label="direct"
    )
    ax.plot(
       dates[-LONG_H:],
       p_rec,
       color="#7c3aed",
       lw=1.5,
       ls="--",
       label=f"recursive ({CHUNK}-day chunks)"
    )
    ax.fill_between(
       dates[-LONG_H:],
       q_direct[0, :, IDX_Q10],
       q_direct[0, :, IDX_Q90],
       color="#ea580c",
       alpha=.15,
       lw=0
    )
    ax.legend()
    ax.set_title(
       "Direct vs recursive long-horizon forecasting"
    )
    plt.tight_layout()
    plt.show()
    recompile()
    

    We create an anomaly-detection workflow by comparing observed values against TimesFM’s predictive quantile intervals and assigning warning or critical severity levels to unusual observations. We inject spikes, outages, and sustained shifts into a retail series, calculate interval-based anomaly scores, and visualize the detected events alongside the expected forecasting range. We also compare direct and recursive long-horizon forecasting to examine how each strategy affects accuracy, uncertainty, and error accumulation over an extended prediction period.

    many = [
       wide[STORES[i % len(STORES)]]
       .values[:-HORIZON]
       .astype(np.float32)
       .copy()
       for i in range(48)
    ]
    print("n--- throughput (48 series, 1024 context) ---")
    bench = []
    for bs in (
       [8, 32]
       if FAST_MODE
       else [1, 4, 16, 32, 48]
    ):
       recompile(per_core_batch_size=bs)
       model.forecast(
           horizon=32,
           inputs=[many[0].copy()]
       )
       t0 = time.time()
       model.forecast(
           horizon=HORIZON,
           inputs=list(many)
       )
       dt = time.time() - t0
       bench.append({
           "per_core_batch_size": bs,
           "sec": round(dt, 2),
           "series/sec": round(len(many) / dt, 1)
       })
    bench = pd.DataFrame(bench)
    print(bench.to_string(index=False))
    print(
       "Tip: also try force_flip_invariance=False "
       "for a ~2x speedup when your series are "
       "strictly positive — it removes the second "
       "forward pass."
    )
    recompile()
    print("n--- robustness checks ---")
    dirty = (
       wide[STORES[0]]
       .values[:-HORIZON]
       .astype(np.float32)
       .copy()
    )
    dirty[:20] = np.nan
    dirty[400:415] = np.nan
    p_nan, _ = model.forecast(
       horizon=HORIZON,
       inputs=[dirty.copy()]
    )
    print(
       f"(a) NaN input  -> finite output: "
       f"{np.isfinite(p_nan).all()}, "
       f"MASE "
       f"{evaluate(actual, p_nan[0], train)['MASE']:.3f} "
       f"(clean: "
       f"{evaluate(actual, point[0], train)['MASE']:.3f})"
    )
    def trim_trailing_nans(a):
       a = np.asarray(a, dtype=np.float32)
       return (
           a[:len(a) - np.argmax(~np.isnan(a[::-1]))]
           if np.isnan(a[-1])
           else a
       )
    for L in [8, 32, 128]:
       ps, _ = model.forecast(
           horizon=14,
           inputs=[train[-L:].copy()]
       )
       print(
           f"(b) context={L:>4} -> 14d MASE "
           f"{evaluate("
           f"series[-HORIZON:-HORIZON+14], "
           f"ps[0], "
           f"train"
           f")['MASE']:.3f}"
       )
    decay = np.clip(
       np.linspace(400, 8, 400) +
       np.random.normal(0, 6, 400),
       0,
       None
    ).astype(np.float32)
    recompile(infer_is_positive=True)
    p_clip, _ = model.forecast(
       horizon=64,
       inputs=[decay.copy()]
    )
    recompile(infer_is_positive=False)
    p_free, _ = model.forecast(
       horizon=64,
       inputs=[decay.copy()]
    )
    print(
       f"(c) decaying series: "
       f"infer_is_positive=True "
       f"min={p_clip.min():+.1f} "
       f"(floored at 0) | "
       f"False min={p_free.min():+.1f}"
    )
    recompile()
    probe = [
       train.copy(),
       train.copy(),
       train.copy()
    ]
    before = len(probe)
    model.forecast(
       horizon=16,
       inputs=probe
    )
    print(
       f"(d) list length before={before} "
       f"after={len(probe)} "
       f"<-- pass list(inputs)!"
    )
    r1, _ = model.forecast(
       horizon=32,
       inputs=[train.copy()]
    )
    r2, _ = model.forecast(
       horizon=32,
       inputs=[train.copy()]
    )
    print(
       f"(e) deterministic: "
       f"{np.allclose(r1, r2)}"
    )
    fc_dates = pd.date_range(
       dates[-1] + pd.Timedelta(days=1),
       periods=HORIZON,
       freq="D"
    )
    final_in = [
       wide[s]
       .values
       .astype(np.float32)
       .copy()
       for s in STORES
    ]
    fp, fq = model.forecast(
       horizon=HORIZON,
       inputs=list(final_in)
    )
    out = []
    for i, s in enumerate(STORES):
       out.append(pd.DataFrame({
           "date": fc_dates,
           "store": s,
           "forecast": fp[i],
           "q10": fq[i, :, IDX_Q10],
           "q25": fq[i, :, 3],
           "q50": fq[i, :, IDX_Q50],
           "q75": fq[i, :, 7],
           "q90": fq[i, :, IDX_Q90],
           "mean": fq[i, :, IDX_MEAN],
       }))
    out = pd.concat(out, ignore_index=True)
    out.to_csv(
       "timesfm_forecasts.csv",
       index=False
    )
    with open("timesfm_backtest.json", "w") as f:
       json.dump({
           "backtest": summary.round(4).to_dict(),
           "context_ablation": abl.round(4).to_dict(
               orient="records"
           ),
           "throughput": bench.to_dict(
               orient="records"
           )
       }, f, indent=2)
    print(
       "nWrote timesfm_forecasts.csv "
       "and timesfm_backtest.json"
    )
    print(
       out.head(4)
       .round(1)
       .to_string(index=False)
    )
    print("""
    ============================ USE IT ON YOUR OWN CSV ==========================
    df   = pd.read_csv("mydata.csv", parse_dates=["date"]).sort_values("date")
    vals = df["value"].astype("float32").to_numpy()          # 1-D, evenly spaced!
    model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
               "google/timesfm-2.5-200m-pytorch")
    model.compile(timesfm.ForecastConfig(
       max_context=1024, max_horizon=128, normalize_inputs=True,
       per_core_batch_size=32, use_continuous_quantile_head=True,
       fix_quantile_crossing=True, infer_is_positive=True))
    point, quant = model.forecast(horizon=30, inputs=[vals])
    lower, median, upper = quant[0,:,1], point[0], quant[0,:,9]
    CHECKLIST
     [ ] regular spacing, one row per period, gaps as NaN (not dropped rows)
     [ ] trim trailing NaNs yourself
     [ ] max_context multiple of 32; max_horizon multiple of 128
     [ ] max_context + max_horizon <= 16384; horizon <= 1024 w/ quantile head
     [ ] quantile index 0 = MEAN, 1..9 = q10..q90, 5 = median
     [ ] infer_is_positive=False for anything that can go negative
     [ ] return_backcast=True only for forecast_with_covariates()
     [ ] dynamic covariates must cover context + horizon
     [ ] pass list(inputs) — forecast() mutates the list it is given
     [ ] backtest with rolling origins and compare against seasonal naive
    =============================================================================""")
    

    We benchmark forecasting throughput across different per-core batch sizes and measure how many time series we process per second. We test model robustness with leading and interior missing values, very short histories, positive-value clipping, mutable input lists, and deterministic repeated inference. We finally generate future forecasts for every store, export the results and experiment summaries to CSV and JSON files, and provide a reusable template for applying TimesFM 2.5 to our own time-series dataset.

    In conclusion, we completed a comprehensive TimesFM 2.5 forecasting pipeline that extends well beyond generating a basic prediction. We used probabilistic quantiles to measure uncertainty, compare the model against seasonal-naive and last-value baselines, and apply rolling-origin backtesting to obtain a more reliable view of real-world performance. We also explored how context length, batch size, exogenous covariates, compilation flags, and direct or recursive forecasting strategies influence accuracy and computational cost. Through anomaly detection and robustness tests, we examined how the model behaves with missing values, short histories, positive-value constraints, mutable inputs, and long forecast horizons. Finally, we exported forecast, backtest, ablation, and throughput results into reusable files and provided a template for applying the workflow to our own evenly spaced time-series data. We finished with a structured and production-oriented foundation for adapting TimesFM 2.5 to demand forecasting, operational planning, anomaly monitoring, and other large-scale forecasting applications.


    Check out the Full Codes here. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.

    Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us


    Sana Hassan, a consulting intern at Marktechpost and dual-degree student at IIT Madras, is passionate about applying technology and AI to address real-world challenges. With a keen interest in solving practical problems, he brings a fresh perspective to the intersection of AI and real-life solutions.

    Anomaly Backtesting Colab Covariates Deployment Detection EndtoEnd Forecasting Scalable TimesFM
    NCIJ NETWNCIJ NETWORK
    • Website

    Keep Reading

    NVIDIA AI Releases Molt: A PyTorch-Native Agentic Reinforcement Learning Framework

    Accelerating Transformer Training with NVIDIA Transformer Engine, Fused Kernels, BF16, FP8, and GPU Benchmarking

    AMD Releases Instella-MoE-16B-A3B: A Fully Open Mixture-of-Experts LLM With 2.8B Active Parameters Trained On Instinct GPUs

    MiniMax Releases MiniMax H3: An Omni-Modal Video Model That Generates 15-Second 2K Clips With Native Stereo Audio

    Supabase Releases Evals: an Open Source Benchmark That Scores Claude Code, Codex and OpenCode on Real Supabase Tasks

    DeepSeek Upgrades DeepSeek-V4-Flash-0731 with Major Agentic and Coding Gains

    Add A Comment
    Leave A Reply Cancel Reply

    Editors Picks

    Can Democratic Socialists Agree on Anything? They Say That’s Not the Point.

    August 2, 2026

    Uber is building an autonomous vehicle empire, and here’s every company it’s using to do it

    August 2, 2026

    Coldcard Hack Expands as Bitcoin Losses Reach $88.6M

    August 2, 2026

    Scientists found two mysterious ‘ghost’ ancestors hiding in our DNA

    August 2, 2026
    Latest Posts

    Wildfires ravage Spain, France and Italy, killing three firefighters

    July 23, 2026

    Three speeches on a single day signaled a dying American democracy | Robert B Shpiner

    July 23, 2026

    Keystone clashes: Millions pour into three Pennsylvania races that could decide control of the House • OpenSecrets

    July 23, 2026

    Subscribe to News

    Get the latest sports news from NewsSite about world, sports and politics.

    NCIJ Network is an independent digital news platform delivering trusted investigative journalism, European and global news, in-depth analysis, and fact-based reporting with accuracy, transparency, and integrity.

    Facebook X (Twitter) Instagram Pinterest YouTube

    Can Democratic Socialists Agree on Anything? They Say That’s Not the Point.

    August 2, 2026

    Uber is building an autonomous vehicle empire, and here’s every company it’s using to do it

    August 2, 2026

    Coldcard Hack Expands as Bitcoin Losses Reach $88.6M

    August 2, 2026

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    Type above and press Enter to search. Press Esc to cancel.