Claude Skill

alterlab-timesfm

Forecasts time series zero-shot with Google's TimesFM foundation models — TimesFM 2.5 (200M, Apache-2.0 weights; ForecastConfig API, XReg covariates) and TimesFM 3.0 (~330M, multivariate with native past/future covariates; non-commercial weights) — producing point forecasts and q

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alterlab-ieu-alterlab-academic-skills-skills_data-science_alterlab-timesfm-e4836c0.zip · 1768 KB
Part of alterlab-ieu/alterlab-academic-skills — 94 skills

Install

skills CLI npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/data-science/alterlab-timesfm
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
Git git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

TimesFM Forecasting

Overview

TimesFM (Time Series Foundation Model) is a pretrained decoder-only foundation model developed by Google Research for time-series forecasting. It works zero-shot — feed it a time series and it returns point forecasts with quantile prediction intervals, no training required. The timesfm package (current 3.0.2) ships two model APIs:

  • TimesFM 2.5 (200M, Apache-2.0 weights) — timesfm.TimesFM_2p5_200M_torch with ForecastConfig; univariate, optional covariates via XReg. The default in this skill and the only choice for commercial or production use.
  • TimesFM 3.0 (~330M, released Aug 2026) — timesfm3.TimesFM3Forecaster; univariate and multivariate forecasting with native past-only and past-and-future covariates. Its weights are under timesfm-non-commercial-license-v1.0, so use it only for non-commercial research and tell the user about the restriction.

This skill wraps TimesFM for safe, agent-friendly local inference. It includes a mandatory preflight system checker that verifies RAM, GPU memory, and disk space before the model is ever loaded so the agent never crashes a user's machine.

Key numbers: TimesFM 2.5 uses 200M parameters (0.93 GB safetensors); TimesFM 3.0 uses ~330M (1.3 GB). Run the system checker before the first load so an under-resourced machine fails fast instead of swapping or crashing.

When to Use This Skill

Use this skill when:

  • Forecasting any univariate time series (sales, demand, sensor, vitals, price, weather)
  • You need zero-shot forecasting without training a custom model
  • You want probabilistic forecasts with calibrated prediction intervals (quantiles)
  • You have time series of any length (the model handles 1–16,384 context points)
  • You need to batch-forecast hundreds or thousands of series efficiently
  • You want a foundation model approach instead of hand-tuning ARIMA/ETS parameters
  • You have related channels or known future drivers (TimesFM 3.0 multivariate + covariates, or TimesFM 2.5 XReg)

Does NOT Trigger

Scenario Use Instead
Classical models with interpretable coefficients (ARIMA/SARIMAX tables), VAR, or Granger causality tests alterlab-statsmodels
Time-series classification, clustering, segmentation, or similarity search alterlab-aeon
Exploring a time-series file's structure and quality before any forecasting alterlab-eda
Tabular (non-temporal) prediction alterlab-scikit-learn

Note on Anomaly Detection: TimesFM does not have built-in anomaly detection, but you can use the quantile forecasts as prediction intervals — values outside the 80% CI (q10–q90) are statistically unusual. See the examples/anomaly-detection/ directory for a full example.

Preflight: System Requirements Check

Run the system checker before loading a model for the first time on a machine: loading downloads ~1 GB of weights and allocates several GB of RAM, and the checker stops early with a clear message instead of letting the load crash or swap.

python scripts/check_system.py

This script checks:

  1. Available RAM — warns if below 4 GB, blocks if below 2 GB
  2. GPU availability — detects CUDA/MPS devices and VRAM
  3. Disk space — verifies room for the ~800 MB model download
  4. Python version — requires 3.10+
  5. Existing installation — checks if timesfm and torch are installed

Note: Model weights are NOT stored in this repository. TimesFM weights (~800 MB) download on-demand from HuggingFace on first use and cache in ~/.cache/huggingface/. The preflight checker ensures sufficient resources before any download begins.

flowchart TD
    accTitle: Preflight System Check
    accDescr: Decision flowchart showing the system requirement checks that must pass before loading TimesFM.

    start["🚀 Run check_system.py"] --> ram{"RAM ≥ 4 GB?"}
    ram -->|"Yes"| gpu{"GPU available?"}
    ram -->|"No (2-4 GB)"| warn_ram["⚠️ Warning: tight RAM<br/>CPU-only, small batches"]
    ram -->|"No (< 2 GB)"| block["🛑 BLOCKED<br/>Insufficient memory"]
    warn_ram --> disk
    gpu -->|"CUDA / MPS"| vram{"VRAM ≥ 2 GB?"}
    gpu -->|"CPU only"| cpu_ok["✅ CPU mode<br/>Slower but works"]
    vram -->|"Yes"| gpu_ok["✅ GPU mode<br/>Fast inference"]
    vram -->|"No"| cpu_ok
    gpu_ok --> disk{"Disk ≥ 2 GB free?"}
    cpu_ok --> disk
    disk -->|"Yes"| ready["✅ READY<br/>Safe to load model"]
    disk -->|"No"| block_disk["🛑 BLOCKED<br/>Need space for weights"]

    classDef ok fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
    classDef warn fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12
    classDef block fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#7f1d1d
    classDef neutral fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#1f2937

    class ready,gpu_ok,cpu_ok ok
    class warn_ram warn
    class block,block_disk block
    class start,ram,gpu,vram,disk neutral

Hardware Requirements by Model Version

Model Parameters RAM (CPU) VRAM (GPU) Disk Context
TimesFM 3.0 (non-commercial weights) ~330M ≥ 6 GB ≥ 4 GB ~1.3 GB up to 15,360
TimesFM 2.5 (default) 200M ≥ 4 GB ≥ 2 GB ~0.9 GB up to 16,384
TimesFM 2.0 (archived) 500M ≥ 16 GB ≥ 8 GB ~2 GB up to 2,048
TimesFM 1.0 (archived) 200M ≥ 8 GB ≥ 4 GB ~800 MB up to 2,048

Recommendation: Use TimesFM 2.5 by default (smallest, Apache-2.0, full ForecastConfig control). Use TimesFM 3.0 for multivariate targets or native covariates when the non-commercial license fits the project. The 1.0/2.0 checkpoints need timesfm==1.3.0 and are only worth it for reproducing old results. Measured peak CPU memory for 32 series × 1,024 context (timesfm 3.0.2): ~2.0 GB for 2.5 and ~2.7 GB for 3.0; the thresholds above leave headroom. Check 3.0 with python scripts/check_system.py --model v3.0.

🔧 Installation

Step 1: Verify System (always first)

python scripts/check_system.py

Step 2: Install TimesFM

uv pip install "timesfm[torch]"          # TimesFM 2.5 + 3.0, PyTorch backend
uv pip install "timesfm[torch,xreg]"     # + XReg covariates for TimesFM 2.5 (adds JAX, scikit-learn)
uv pip install "timesfm[mlx]"            # TimesFM 3.0 on Apple silicon without PyTorch
uv pip install "timesfm[flax]"           # TimesFM 2.5 JAX/Flax backend

Step 3: Install PyTorch for Your Hardware

# CPU-only wheels (small download, no CUDA libraries)
uv pip install torch --index-url https://download.pytorch.org/whl/cpu

# NVIDIA GPU: take the CUDA-specific index URL from https://pytorch.org/get-started/locally/
# Apple Silicon: the default PyPI wheel already includes MPS support
uv pip install torch

Step 4: Verify Installation

from importlib.metadata import version
import timesfm  # noqa: F401  (import check)
print(f"TimesFM version: {version('timesfm')}")  # the package defines no __version__
print("Installation OK")

🎯 Quick Start

Minimal Example (5 Lines)

import torch, numpy as np, timesfm

torch.set_float32_matmul_precision("high")

model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
    "google/timesfm-2.5-200m-pytorch"
)
model.compile(timesfm.ForecastConfig(
    max_context=1024, max_horizon=256, normalize_inputs=True,
    use_continuous_quantile_head=True, force_flip_invariance=True,
    infer_is_positive=True, fix_quantile_crossing=True,
))

point, quantiles = model.forecast(horizon=24, inputs=[
    np.sin(np.linspace(0, 20, 200)),  # any 1-D array
])
# point.shape == (1, 24)        — median forecast
# quantiles.shape == (1, 24, 10) — 10th–90th percentile bands

TimesFM 3.0 (multivariate, native covariates)

import numpy as np
from timesfm3 import TimesFM3Forecaster  # non-commercial weights — see license note above

forecaster = TimesFM3Forecaster.from_pretrained("google/timesfm-3.0-pytorch", device="cpu")  # or "cuda"

out = forecaster.predict(np.sin(np.linspace(0, 40, 512)).astype(np.float32),
                         horizon=24, return_quantiles=True)
# out.forecast.shape == (24,)      — median forecast
# out.quantiles.shape == (24, 9)   — q10..q90; index 4 is the median (no mean column)

# Two target channels plus one known-future covariate (context 256, horizon 32)
target = np.stack([np.sin(np.linspace(0, 24, 256)), np.cos(np.linspace(0, 24, 256))]).astype(np.float32)
future_cov = np.sin(np.linspace(0, 30, 256 + 32))[None, :].astype(np.float32)
out = forecaster.predict(target, horizon=32, past_future_covariates=future_cov, return_quantiles=True)
# out.forecast.shape == (2, 32); out.quantiles.shape == (2, 32, 9)

Full parameter list (predict_batch, past_only_covariates, make_positive, …): references/api_reference.md.

Forecast from CSV

import pandas as pd, numpy as np

df = pd.read_csv("monthly_sales.csv", parse_dates=["date"], index_col="date")

# Convert each column to a list of arrays
inputs = [df[col].dropna().values.astype(np.float32) for col in df.columns]

point, quantiles = model.forecast(horizon=12, inputs=inputs)

# Build a results DataFrame
for i, col in enumerate(df.columns):
    last_date = df[col].dropna().index[-1]
    future_dates = pd.date_range(last_date, periods=13, freq="MS")[1:]
    forecast_df = pd.DataFrame({
        "date": future_dates,
        "forecast": point[i],
        "lower_80": quantiles[i, :, 1],  # q10 — lower bound of 80% PI
        "upper_80": quantiles[i, :, 9],  # q90 — upper bound of 80% PI
    })
    print(f"\n--- {col} ---")
    print(forecast_df.to_string(index=False))

Forecast with Covariates (XReg)

TimesFM 2.5 supports exogenous variables through forecast_with_covariates(). It requires timesfm[xreg] and a model compiled with return_backcast=True (otherwise it raises ValueError). TimesFM 3.0 takes covariates directly in predict() (see above).

# Requires: uv pip install "timesfm[torch,xreg]"
model.compile(timesfm.ForecastConfig(
    max_context=1024, max_horizon=256, normalize_inputs=True,
    use_continuous_quantile_head=True, fix_quantile_crossing=True,
    return_backcast=True,
))
point, quantiles = model.forecast_with_covariates(
    inputs=inputs,
    dynamic_numerical_covariates={"price": price_arrays},
    dynamic_categorical_covariates={"holiday": holiday_arrays},
    static_categorical_covariates={"region": region_labels},
    xreg_mode="xreg + timesfm",  # or "timesfm + xreg"
)
# point / quantiles: one array per series — (horizon,) and (horizon, 10)
Covariate Type Description Example
dynamic_numerical Time-varying numeric price, temperature, promotion spend
dynamic_categorical Time-varying categorical holiday flag, day of week
static_numerical Per-series numeric store size, account age
static_categorical Per-series categorical store type, region, product category

XReg Modes:

  • "xreg + timesfm" (default): fit an in-context linear regression on the covariates first, then TimesFM forecasts the regression residuals
  • "timesfm + xreg": TimesFM forecasts first, then a linear regression on the covariates fits TimesFM's residuals

See examples/covariates-forecasting/ for a complete example with synthetic retail data.

Anomaly Detection (via Quantile Intervals)

TimesFM does not have built-in anomaly detection, but the quantile forecasts naturally provide prediction intervals that can detect anomalies:

point, q = model.forecast(horizon=H, inputs=[values])

# 80% prediction interval
lower_80 = q[0, :, 1]  # 10th percentile
upper_80 = q[0, :, 9]  # 90th percentile

# Detect anomalies: values outside the 80% CI
actual = test_values  # your holdout data
anomalies = (actual < lower_80) | (actual > upper_80)

# Severity levels
is_warning = (actual < q[0, :, 2]) | (actual > q[0, :, 8])  # outside 60% CI
is_critical = anomalies  # outside 80% CI
Severity Condition Interpretation
Normal Inside 60% CI Expected behavior
Warning Outside 60% CI Unusual but possible
Critical Outside 80% CI Statistically rare (< 20% probability)

See examples/anomaly-detection/ for a complete example with visualization.

📊 Output, Config & Workflows

The output structure and full ForecastConfig reference are in references/output_and_config.md.

Quantile layout (TimesFM 2.5): quantile_forecast has shape (batch, horizon, 10). Index 0 is the mean; q10 = index 1, q50 (median) = index 5, q90 = index 9, so the 80% PI is q[:,:,1]q[:,:,9]. TimesFM 3.0 returns 9 columns (q10–q90) with the median at index 4 — re-check indices when switching models.

Copy-paste workflows (single-series, batch, accuracy evaluation), GPU/memory performance tuning, and integration with statsmodels / matplotlib / EDA are in references/workflows.md.

📚 Scripts

  • scripts/check_system.py — mandatory preflight checker; run before first model load. Reports RAM/GPU/disk/Python/install status and a recommended per_core_batch_size.
  • scripts/forecast_csv.py — end-to-end CSV forecasting with automatic system check:
    python scripts/forecast_csv.py input.csv --horizon 24 \
        --date-col date --value-cols sales,revenue --output forecasts.csv
    

📖 Reference Documentation

Detailed guides in references/:

File Contents
references/output_and_config.md Output shapes, quantile index map, full ForecastConfig parameter reference
references/workflows.md Single/batch/eval workflows, GPU & memory tuning, statsmodels/matplotlib/EDA integration
references/pitfalls_and_validation.md Common pitfalls, quality checklist, known mistakes, regression-baseline validation
references/system_requirements.md Hardware tiers, GPU/CPU selection, memory estimation formulas
references/api_reference.md Full from_pretrained options, API surface, output shapes
references/data_preparation.md Input formats, NaN handling, CSV loading, covariate setup

Before declaring any task done, run the quality checklist and review the common pitfalls/mistakes in references/pitfalls_and_validation.md — especially the quantile index off-by-one and infer_is_positive for negative series.

Model Versions

timeline
    accTitle: TimesFM Version History
    accDescr: Timeline of TimesFM model releases showing parameter counts and key improvements.

    section 2024
        TimesFM 1.0 : 200M params, 2K context, JAX only
        TimesFM 2.0 : 500M params, 2K context, PyTorch + JAX
    section 2025
        TimesFM 2.5 : 200M params, 16K context, quantile head, no frequency indicator
    section 2026
        TimesFM 3.0 : ~330M params, 15K context, multivariate + covariates, non-commercial weights
Version Params Context Quantile Head Frequency Flag Status
3.0 ~330M 15,360 ✅ 9 deciles Latest (non-commercial weights)
2.5 200M 16,384 ✅ Continuous (30M) ❌ Removed Default (Apache-2.0)
2.0 500M 2,048 ✅ Fixed buckets ✅ Required Archived
1.0 200M 2,048 ✅ Fixed buckets ✅ Required Archived

Hugging Face checkpoints:

  • google/timesfm-3.0-pytorch (TimesFM 3.0; non-commercial license)
  • google/timesfm-2.5-200m-pytorch (default)
  • google/timesfm-2.5-200m-flax
  • google/timesfm-2.5-200m-transformers (🤗 Transformers port, TimesFm2_5ModelForPrediction)
  • google/timesfm-2.0-500m-pytorch (archived)
  • google/timesfm-1.0-200m-pytorch (archived)

Resources

Examples

Three reference examples live in examples/; all use the TimesFM 2.5 API (outputs regenerated with timesfm 3.0.2 in 2026-09). Use them as ground truth for correct API usage and expected output shape.

Example Directory What It Demonstrates When To Use It
Global Temperature Forecast examples/global-temperature/ Basic model.forecast() call, CSV -> PNG -> GIF pipeline, 36-month NOAA context, 60%/80% prediction intervals Starting point; copy-paste baseline for any univariate series
Anomaly Detection examples/anomaly-detection/ Two-phase detection: linear detrend + Z-score on context, quantile PI on forecast; 2-panel viz Any task requiring outlier detection on historical + forecasted data
Covariates (XReg) examples/covariates-forecasting/ forecast_with_covariates() API (TimesFM 2.5), covariate decomposition, 2x2 shared-axis viz Retail, energy, or any series with known exogenous drivers

Running the Examples

# Global temperature (TimesFM 2.5)
cd examples/global-temperature && python run_forecast.py && python visualize_forecast.py

# Anomaly detection (TimesFM 2.5)
cd examples/anomaly-detection && python detect_anomalies.py

# Covariates (data + API walkthrough; real inference needs timesfm[torch,xreg])
cd examples/covariates-forecasting && python demo_covariates.py

Expected Outputs

Example Key output files Acceptance criteria
global-temperature output/forecast_output.json, output/forecast_visualization.png point_forecast has 12 values; PNG shows context + forecast + PI bands
anomaly-detection output/anomaly_detection.json, output/anomaly_detection.png Sep 2023 flagged CRITICAL (z >= 3.0); >= 2 forecast CRITICAL from injected anomalies
covariates-forecasting output/sales_with_covariates.csv, output/covariates_data.png CSV has 108 rows (3 stores x 36 weeks); stores have distinct price arrays

Quality, Mistakes & Validation

Before declaring any task done, run the post-task quality checklist, review the known mistakes (quantile off-by-one, covariate-horizon coverage, residual-based anomaly detection, etc.), and run the regression-baseline verification snippets — all in references/pitfalls_and_validation.md.

Part of the AlterLab Academic Skills suite.

Files (alterlab-academic-skills)
  • evals
    • evals.json 6.5 KB
      {
        "skill": "alterlab-timesfm",
        "evals": [
          {
            "id": "single-series-forecast",
            "prompt": "I have weekly_demand.csv with about two years of weekly demand. Forecast the next 52 weeks with prediction intervals — I don't want to train anything, just get a forecast fast.",
            "expected_output": "Invokes alterlab-timesfm: runs the mandatory preflight check_system.py first, loads TimesFM 2.5 (google/timesfm-2.5-200m-pytorch) via from_pretrained, calls model.compile() with a ForecastConfig (normalize_inputs=True, use_continuous_quantile_head=True, fix_quantile_crossing=True), passes a list of 1-D arrays to model.forecast(horizon=52), and extracts the point forecast plus 80% prediction interval (q index 1 and 9).",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "behavior", "value": "Runs the system preflight check before loading, uses zero-shot TimesFM 2.5 with no training, and returns point forecasts plus calibrated prediction intervals." }
            ]
          },
          {
            "id": "batch-forecast-many-series",
            "prompt": "I have a wide CSV with one sales column per store — about 800 stores. I need a 30-step-ahead forecast for every store efficiently.",
            "expected_output": "Invokes alterlab-timesfm: after the preflight check, loads and compiles TimesFM 2.5, converts each column to a 1-D float32 array forming a list of inputs, calls model.forecast(horizon=30, inputs=inputs) to batch-forecast all series at once, tuning per_core_batch_size for the hardware, and exports per-series point/PI results.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "behavior", "value": "Forecasts all series in one batched call passing a list of 1-D arrays (not a 2-D matrix) and scales per_core_batch_size to hardware." }
            ]
          },
          {
            "id": "covariates-forecast",
            "prompt": "Forecast my retail demand but use price and a holiday flag as known future drivers — both vary over time and I know their future values for the horizon.",
            "expected_output": "Invokes alterlab-timesfm: uses TimesFM 2.5 forecast_with_covariates() (requires timesfm[xreg] and a model compiled with return_backcast=True), passing dynamic_numerical_covariates for price and dynamic_categorical_covariates for the holiday flag, notes that the covariates must span both context and the full horizon, and selects an xreg_mode. May alternatively offer TimesFM 3.0's native past_future_covariates, flagging its non-commercial weight license.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "covariates" }
            ]
          },
          {
            "id": "anomaly-via-prediction-intervals",
            "prompt": "Using my sensor time series, flag the readings that are statistically unusual relative to what the model expects — I want warning and critical severity levels.",
            "expected_output": "Invokes alterlab-timesfm: forecasts with quantile prediction intervals and flags values outside the 60% PI (q20/q80, indices 2/8) as Warning and outside the 80% PI (q10/q90, indices 1/9) as Critical, noting TimesFM has no built-in anomaly detector but the calibrated quantiles serve as the interval test (detrend before any context Z-scoring).",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "behavior", "value": "Uses the quantile prediction intervals (60% PI for warning, 80% PI for critical) to assign severity rather than claiming a built-in anomaly detector." }
            ]
          },
          {
            "id": "multivariate-timesfm3",
            "prompt": "For my research project I have hourly readings from four related air-quality sensors plus a temperature forecast for the next 48 hours. Can I forecast all four sensors jointly with a foundation model and use the known future temperature?",
            "expected_output": "Invokes alterlab-timesfm and uses TimesFM 3.0 (timesfm3.TimesFM3Forecaster.from_pretrained('google/timesfm-3.0-pytorch')) with a (4, T) target array and the temperature as past_future_covariates of length T + 48, calling predict(..., horizon=48, return_quantiles=True); explains the output shapes ((4, 48) forecast, (4, 48, 9) deciles with the median at index 4) and states that the TimesFM 3.0 weights are licensed for non-commercial, non-production use only, pointing to TimesFM 2.5 with XReg as the Apache-2.0 alternative.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "TimesFM 3" },
              { "type": "behavior", "value": "Uses TimesFM 3.0 multivariate forecasting with past-and-future covariates and flags the non-commercial weight license." }
            ]
          },
          {
            "id": "near-miss-alterlab-statsmodels",
            "prompt": "Fit a SARIMAX model to my monthly series and give me the coefficient table with significance tests and confidence intervals so I can interpret the AR and seasonal terms.",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-statsmodels. The user wants a classical statistical model with interpretable coefficients and significance tests, whereas TimesFM is a zero-shot foundation model with no coefficient interpretation.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-statsmodels" }
            ]
          },
          {
            "id": "near-miss-alterlab-eda",
            "prompt": "Before I forecast anything, just explore my time_series.csv — show me its structure, missing values, trend, and seasonality with a summary report.",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-eda. The user wants exploratory data analysis and a structural/quality report of the file, not a forecast; forecasting comes after EDA.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-eda" }
            ]
          },
          {
            "id": "near-miss-alterlab-aeon",
            "prompt": "I have 2,000 labelled ECG segments and want to train a classifier that tells normal from arrhythmic beats, comparing ROCKET and a shapelet-based method. Which time-series library should I use?",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-aeon. The task is supervised time-series classification (ROCKET, shapelets) on labelled segments, not forecasting future values with a foundation model.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-aeon" }
            ]
          }
        ]
      }
      
  • examples
    • anomaly-detection
      • output
        • anomaly_detection.json 8.8 KB
          {
            "method": "two_phase",
            "context_method": "linear_detrend_zscore",
            "forecast_method": "quantile_prediction_intervals",
            "thresholds": {
              "critical_z": 3.0,
              "warning_z": 2.0,
              "pi_critical_pct": 80,
              "pi_warning_pct": 60
            },
            "context_summary": {
              "total": 36,
              "critical": 1,
              "warning": 0,
              "normal": 35,
              "res_std": 0.11362
            },
            "forecast_summary": {
              "total": 12,
              "critical": 5,
              "warning": 1,
              "normal": 6
            },
            "context_detections": [
              {
                "date": "2022-01",
                "value": 0.89,
                "trend": 0.837,
                "residual": 0.053,
                "z_score": 0.467,
                "severity": "NORMAL"
              },
              {
                "date": "2022-02",
                "value": 0.89,
                "trend": 0.8514,
                "residual": 0.0386,
                "z_score": 0.34,
                "severity": "NORMAL"
              },
              {
                "date": "2022-03",
                "value": 1.02,
                "trend": 0.8658,
                "residual": 0.1542,
                "z_score": 1.357,
                "severity": "NORMAL"
              },
              {
                "date": "2022-04",
                "value": 0.88,
                "trend": 0.8803,
                "residual": -0.0003,
                "z_score": -0.002,
                "severity": "NORMAL"
              },
              {
                "date": "2022-05",
                "value": 0.85,
                "trend": 0.8947,
                "residual": -0.0447,
                "z_score": -0.394,
                "severity": "NORMAL"
              },
              {
                "date": "2022-06",
                "value": 0.88,
                "trend": 0.9092,
                "residual": -0.0292,
                "z_score": -0.257,
                "severity": "NORMAL"
              },
              {
                "date": "2022-07",
                "value": 0.88,
                "trend": 0.9236,
                "residual": -0.0436,
                "z_score": -0.384,
                "severity": "NORMAL"
              },
              {
                "date": "2022-08",
                "value": 0.9,
                "trend": 0.9381,
                "residual": -0.0381,
                "z_score": -0.335,
                "severity": "NORMAL"
              },
              {
                "date": "2022-09",
                "value": 0.88,
                "trend": 0.9525,
                "residual": -0.0725,
                "z_score": -0.638,
                "severity": "NORMAL"
              },
              {
                "date": "2022-10",
                "value": 0.95,
                "trend": 0.9669,
                "residual": -0.0169,
                "z_score": -0.149,
                "severity": "NORMAL"
              },
              {
                "date": "2022-11",
                "value": 0.77,
                "trend": 0.9814,
                "residual": -0.2114,
                "z_score": -1.86,
                "severity": "NORMAL"
              },
              {
                "date": "2022-12",
                "value": 0.78,
                "trend": 0.9958,
                "residual": -0.2158,
                "z_score": -1.9,
                "severity": "NORMAL"
              },
              {
                "date": "2023-01",
                "value": 0.87,
                "trend": 1.0103,
                "residual": -0.1403,
                "z_score": -1.235,
                "severity": "NORMAL"
              },
              {
                "date": "2023-02",
                "value": 0.98,
                "trend": 1.0247,
                "residual": -0.0447,
                "z_score": -0.394,
                "severity": "NORMAL"
              },
              {
                "date": "2023-03",
                "value": 1.21,
                "trend": 1.0392,
                "residual": 0.1708,
                "z_score": 1.503,
                "severity": "NORMAL"
              },
              {
                "date": "2023-04",
                "value": 1.0,
                "trend": 1.0536,
                "residual": -0.0536,
                "z_score": -0.472,
                "severity": "NORMAL"
              },
              {
                "date": "2023-05",
                "value": 0.94,
                "trend": 1.0681,
                "residual": -0.1281,
                "z_score": -1.127,
                "severity": "NORMAL"
              },
              {
                "date": "2023-06",
                "value": 1.08,
                "trend": 1.0825,
                "residual": -0.0025,
                "z_score": -0.022,
                "severity": "NORMAL"
              },
              {
                "date": "2023-07",
                "value": 1.18,
                "trend": 1.0969,
                "residual": 0.0831,
                "z_score": 0.731,
                "severity": "NORMAL"
              },
              {
                "date": "2023-08",
                "value": 1.24,
                "trend": 1.1114,
                "residual": 0.1286,
                "z_score": 1.132,
                "severity": "NORMAL"
              },
              {
                "date": "2023-09",
                "value": 1.47,
                "trend": 1.1258,
                "residual": 0.3442,
                "z_score": 3.029,
                "severity": "CRITICAL"
              },
              {
                "date": "2023-10",
                "value": 1.32,
                "trend": 1.1403,
                "residual": 0.1797,
                "z_score": 1.582,
                "severity": "NORMAL"
              },
              {
                "date": "2023-11",
                "value": 1.18,
                "trend": 1.1547,
                "residual": 0.0253,
                "z_score": 0.222,
                "severity": "NORMAL"
              },
              {
                "date": "2023-12",
                "value": 1.16,
                "trend": 1.1692,
                "residual": -0.0092,
                "z_score": -0.081,
                "severity": "NORMAL"
              },
              {
                "date": "2024-01",
                "value": 1.22,
                "trend": 1.1836,
                "residual": 0.0364,
                "z_score": 0.32,
                "severity": "NORMAL"
              },
              {
                "date": "2024-02",
                "value": 1.35,
                "trend": 1.1981,
                "residual": 0.1519,
                "z_score": 1.337,
                "severity": "NORMAL"
              },
              {
                "date": "2024-03",
                "value": 1.34,
                "trend": 1.2125,
                "residual": 0.1275,
                "z_score": 1.122,
                "severity": "NORMAL"
              },
              {
                "date": "2024-04",
                "value": 1.26,
                "trend": 1.2269,
                "residual": 0.0331,
                "z_score": 0.291,
                "severity": "NORMAL"
              },
              {
                "date": "2024-05",
                "value": 1.15,
                "trend": 1.2414,
                "residual": -0.0914,
                "z_score": -0.804,
                "severity": "NORMAL"
              },
              {
                "date": "2024-06",
                "value": 1.2,
                "trend": 1.2558,
                "residual": -0.0558,
                "z_score": -0.491,
                "severity": "NORMAL"
              },
              {
                "date": "2024-07",
                "value": 1.24,
                "trend": 1.2703,
                "residual": -0.0303,
                "z_score": -0.266,
                "severity": "NORMAL"
              },
              {
                "date": "2024-08",
                "value": 1.3,
                "trend": 1.2847,
                "residual": 0.0153,
                "z_score": 0.135,
                "severity": "NORMAL"
              },
              {
                "date": "2024-09",
                "value": 1.28,
                "trend": 1.2992,
                "residual": -0.0192,
                "z_score": -0.169,
                "severity": "NORMAL"
              },
              {
                "date": "2024-10",
                "value": 1.27,
                "trend": 1.3136,
                "residual": -0.0436,
                "z_score": -0.384,
                "severity": "NORMAL"
              },
              {
                "date": "2024-11",
                "value": 1.22,
                "trend": 1.328,
                "residual": -0.108,
                "z_score": -0.951,
                "severity": "NORMAL"
              },
              {
                "date": "2024-12",
                "value": 1.2,
                "trend": 1.3425,
                "residual": -0.1425,
                "z_score": -1.254,
                "severity": "NORMAL"
              }
            ],
            "forecast_detections": [
              {
                "date": "2025-01",
                "actual": 1.2821,
                "forecast": 1.2224,
                "q10": 1.1231,
                "q20": 1.1614,
                "q80": 1.2928,
                "q90": 1.3396,
                "severity": "NORMAL",
                "was_injected": false
              },
              {
                "date": "2025-02",
                "actual": 1.1522,
                "forecast": 1.2564,
                "q10": 1.1482,
                "q20": 1.1892,
                "q80": 1.3358,
                "q90": 1.388,
                "severity": "WARNING",
                "was_injected": false
              },
              {
                "date": "2025-03",
                "actual": 1.3358,
                "forecast": 1.2865,
                "q10": 1.1695,
                "q20": 1.2141,
                "q80": 1.3726,
                "q90": 1.4266,
                "severity": "NORMAL",
                "was_injected": false
              },
              {
                "date": "2025-04",
                "actual": 2.0594,
                "forecast": 1.2405,
                "q10": 1.1193,
                "q20": 1.1689,
                "q80": 1.3238,
                "q90": 1.3806,
                "severity": "CRITICAL",
                "was_injected": true
              },
              {
                "date": "2025-05",
                "actual": 1.0747,
                "forecast": 1.2026,
                "q10": 1.0777,
                "q20": 1.128,
                "q80": 1.2891,
                "q90": 1.347,
                "severity": "CRITICAL",
                "was_injected": false
              },
              {
                "date": "2025-06",
                "actual": 1.1442,
                "forecast": 1.21,
                "q10": 1.0811,
                "q20": 1.1352,
                "q80": 1.2938,
                "q90": 1.3532,
                "severity": "NORMAL",
                "was_injected": false
              },
              {
                "date": "2025-07",
                "actual": 1.2917,
                "forecast": 1.2253,
                "q10": 1.0918,
                "q20": 1.1475,
                "q80": 1.3113,
                "q90": 1.3728,
                "severity": "NORMAL",
                "was_injected": false
              },
              {
                "date": "2025-08",
                "actual": 1.2519,
                "forecast": 1.2422,
                "q10": 1.1043,
                "q20": 1.1599,
                "q80": 1.3305,
                "q90": 1.395,
                "severity": "NORMAL",
                "was_injected": false
              },
              {
                "date": "2025-09",
                "actual": 0.6364,
                "forecast": 1.2697,
                "q10": 1.1239,
                "q20": 1.1873,
                "q80": 1.358,
                "q90": 1.4252,
                "severity": "CRITICAL",
                "was_injected": true
              },
              {
                "date": "2025-10",
                "actual": 1.2073,
                "forecast": 1.2497,
                "q10": 1.0962,
                "q20": 1.163,
                "q80": 1.3377,
                "q90": 1.4098,
                "severity": "NORMAL",
                "was_injected": false
              },
              {
                "date": "2025-11",
                "actual": 1.3851,
                "forecast": 1.2135,
                "q10": 1.0546,
                "q20": 1.1224,
                "q80": 1.3091,
                "q90": 1.3802,
                "severity": "CRITICAL",
                "was_injected": false
              },
              {
                "date": "2025-12",
                "actual": 1.8294,
                "forecast": 1.2034,
                "q10": 1.0412,
                "q20": 1.1113,
                "q80": 1.2912,
                "q90": 1.3701,
                "severity": "CRITICAL",
                "was_injected": true
              }
            ]
          }
        • anomaly_detection.png 221.3 KB · in bundle
      • detect_anomalies.py 16.8 KB
        #!/usr/bin/env python3
        """
        TimesFM Anomaly Detection Example — Two-Phase Method
        
        Phase 1 (context): Linear detrend + Z-score on 36 months of real NOAA
          temperature anomaly data (2022-01 through 2024-12).
          Sep 2023 (1.47 C) is a known critical outlier.
        
        Phase 2 (forecast): TimesFM quantile prediction intervals on a 12-month
          synthetic future with 3 injected anomalies.
        
        Outputs:
          output/anomaly_detection.png  -- 2-panel visualization
          output/anomaly_detection.json -- structured detection records
        """
        
        from __future__ import annotations
        
        import json
        from pathlib import Path
        
        import matplotlib
        
        matplotlib.use("Agg")
        import matplotlib.patches as mpatches
        import matplotlib.pyplot as plt
        import numpy as np
        import pandas as pd
        
        HORIZON = 12
        DATA_FILE = (
            Path(__file__).parent.parent / "global-temperature" / "temperature_anomaly.csv"
        )
        OUTPUT_DIR = Path(__file__).parent / "output"
        
        CRITICAL_Z = 3.0
        WARNING_Z = 2.0
        
        # quant_fc index mapping: 0=mean, 1=q10, 2=q20, ..., 9=q90
        IDX_Q10, IDX_Q20, IDX_Q80, IDX_Q90 = 1, 2, 8, 9
        
        CLR = {"CRITICAL": "#e02020", "WARNING": "#f08030", "NORMAL": "#4a90d9"}
        
        
        # ---------------------------------------------------------------------------
        # Phase 1: context anomaly detection
        # ---------------------------------------------------------------------------
        
        
        def detect_context_anomalies(
            values: np.ndarray,
            dates: list,
        ) -> tuple[list[dict], np.ndarray, np.ndarray, float]:
            """Linear detrend + Z-score anomaly detection on context period.
        
            Returns
            -------
            records    : list of dicts, one per month
            trend_line : fitted linear trend values (same length as values)
            residuals  : actual - trend_line
            res_std    : std of residuals (used as sigma for threshold bands)
            """
            n = len(values)
            idx = np.arange(n, dtype=float)
        
            coeffs = np.polyfit(idx, values, 1)
            trend_line = np.polyval(coeffs, idx)
            residuals = values - trend_line
            res_std = residuals.std()
        
            records = []
            for i, (d, v, r) in enumerate(zip(dates, values, residuals)):
                z = r / res_std if res_std > 0 else 0.0
                if abs(z) >= CRITICAL_Z:
                    severity = "CRITICAL"
                elif abs(z) >= WARNING_Z:
                    severity = "WARNING"
                else:
                    severity = "NORMAL"
                records.append(
                    {
                        "date": str(d)[:7],
                        "value": round(float(v), 4),
                        "trend": round(float(trend_line[i]), 4),
                        "residual": round(float(r), 4),
                        "z_score": round(float(z), 3),
                        "severity": severity,
                    }
                )
            return records, trend_line, residuals, res_std
        
        
        # ---------------------------------------------------------------------------
        # Phase 2: synthetic future + forecast anomaly detection
        # ---------------------------------------------------------------------------
        
        
        def build_synthetic_future(
            context: np.ndarray,
            n: int,
            seed: int = 42,
        ) -> tuple[np.ndarray, list[int]]:
            """Build a plausible future with 3 injected anomalies.
        
            Injected months: 3, 8, 11 (0-indexed within the 12-month horizon).
            Returns (future_values, injected_indices).
            """
            rng = np.random.default_rng(seed)
            trend = np.linspace(context[-6:].mean(), context[-6:].mean() + 0.05, n)
            noise = rng.normal(0, 0.1, n)
            future = trend + noise
        
            injected = [3, 8, 11]
            future[3] += 0.7  # CRITICAL spike
            future[8] -= 0.65  # CRITICAL dip
            future[11] += 0.45  # WARNING spike
        
            return future.astype(np.float32), injected
        
        
        def detect_forecast_anomalies(
            future_values: np.ndarray,
            point: np.ndarray,
            quant_fc: np.ndarray,
            future_dates: list,
            injected_at: list[int],
        ) -> list[dict]:
            """Classify each forecast month by which PI band it falls outside.
        
            CRITICAL = outside 80% PI (q10-q90)
            WARNING  = outside 60% PI (q20-q80) but inside 80% PI
            NORMAL   = inside 60% PI
            """
            q10 = quant_fc[IDX_Q10]
            q20 = quant_fc[IDX_Q20]
            q80 = quant_fc[IDX_Q80]
            q90 = quant_fc[IDX_Q90]
        
            records = []
            for i, (d, fv, pt) in enumerate(zip(future_dates, future_values, point)):
                outside_80 = fv < q10[i] or fv > q90[i]
                outside_60 = fv < q20[i] or fv > q80[i]
        
                if outside_80:
                    severity = "CRITICAL"
                elif outside_60:
                    severity = "WARNING"
                else:
                    severity = "NORMAL"
        
                records.append(
                    {
                        "date": str(d)[:7],
                        "actual": round(float(fv), 4),
                        "forecast": round(float(pt), 4),
                        "q10": round(float(q10[i]), 4),
                        "q20": round(float(q20[i]), 4),
                        "q80": round(float(q80[i]), 4),
                        "q90": round(float(q90[i]), 4),
                        "severity": severity,
                        "was_injected": i in injected_at,
                    }
                )
            return records
        
        
        # ---------------------------------------------------------------------------
        # Visualization
        # ---------------------------------------------------------------------------
        
        
        def plot_results(
            context_dates: list,
            context_values: np.ndarray,
            ctx_records: list[dict],
            trend_line: np.ndarray,
            residuals: np.ndarray,
            res_std: float,
            future_dates: list,
            future_values: np.ndarray,
            point_fc: np.ndarray,
            quant_fc: np.ndarray,
            fc_records: list[dict],
        ) -> None:
            OUTPUT_DIR.mkdir(exist_ok=True)
        
            fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10), gridspec_kw={"hspace": 0.42})
            fig.suptitle(
                "TimesFM Anomaly Detection — Two-Phase Method", fontsize=14, fontweight="bold"
            )
        
            # -----------------------------------------------------------------------
            # Panel 1 — full timeline
            # -----------------------------------------------------------------------
            ctx_x = [pd.Timestamp(d) for d in context_dates]
            fut_x = [pd.Timestamp(d) for d in future_dates]
            divider = ctx_x[-1]
        
            # context: blue line + trend + 2sigma band
            ax1.plot(
                ctx_x,
                context_values,
                color=CLR["NORMAL"],
                lw=2,
                marker="o",
                ms=4,
                label="Observed (context)",
            )
            ax1.plot(ctx_x, trend_line, color="#aaaaaa", lw=1.5, ls="--", label="Linear trend")
            ax1.fill_between(
                ctx_x,
                trend_line - 2 * res_std,
                trend_line + 2 * res_std,
                alpha=0.15,
                color=CLR["NORMAL"],
                label="+/-2sigma band",
            )
        
            # context anomaly markers
            seen_ctx: set[str] = set()
            for rec in ctx_records:
                if rec["severity"] == "NORMAL":
                    continue
                d = pd.Timestamp(rec["date"])
                v = rec["value"]
                sev = rec["severity"]
                lbl = f"Context {sev}" if sev not in seen_ctx else None
                seen_ctx.add(sev)
                ax1.scatter(d, v, marker="D", s=90, color=CLR[sev], zorder=6, label=lbl)
                ax1.annotate(
                    f"z={rec['z_score']:+.1f}",
                    (d, v),
                    textcoords="offset points",
                    xytext=(0, 9),
                    fontsize=7.5,
                    ha="center",
                    color=CLR[sev],
                )
        
            # forecast section
            q10 = quant_fc[IDX_Q10]
            q20 = quant_fc[IDX_Q20]
            q80 = quant_fc[IDX_Q80]
            q90 = quant_fc[IDX_Q90]
        
            ax1.plot(fut_x, future_values, "k--", lw=1.5, label="Synthetic future (truth)")
            ax1.plot(
                fut_x,
                point_fc,
                color=CLR["CRITICAL"],
                lw=2,
                marker="s",
                ms=4,
                label="TimesFM point forecast",
            )
            ax1.fill_between(fut_x, q10, q90, alpha=0.15, color=CLR["CRITICAL"], label="80% PI")
            ax1.fill_between(fut_x, q20, q80, alpha=0.25, color=CLR["CRITICAL"], label="60% PI")
        
            seen_fc: set[str] = set()
            for i, rec in enumerate(fc_records):
                if rec["severity"] == "NORMAL":
                    continue
                d = pd.Timestamp(rec["date"])
                v = rec["actual"]
                sev = rec["severity"]
                mk = "X" if sev == "CRITICAL" else "^"
                lbl = f"Forecast {sev}" if sev not in seen_fc else None
                seen_fc.add(sev)
                ax1.scatter(d, v, marker=mk, s=100, color=CLR[sev], zorder=6, label=lbl)
        
            ax1.axvline(divider, color="#555555", lw=1.5, ls=":")
            ax1.text(
                divider,
                ax1.get_ylim()[1] if ax1.get_ylim()[1] != 0 else 1.5,
                "  <- Context | Forecast ->",
                fontsize=8.5,
                color="#555555",
                style="italic",
                va="top",
            )
        
            ax1.annotate(
                "Context: D = Z-score anomaly | Forecast: X = CRITICAL, ^ = WARNING",
                xy=(0.01, 0.04),
                xycoords="axes fraction",
                fontsize=8,
                bbox=dict(boxstyle="round", fc="white", ec="#cccccc", alpha=0.9),
            )
        
            ax1.set_ylabel("Temperature Anomaly (C)", fontsize=10)
            ax1.legend(ncol=2, fontsize=7.5, loc="upper left")
            ax1.grid(True, alpha=0.22)
        
            # -----------------------------------------------------------------------
            # Panel 2 — deviation bars across all 48 months
            # -----------------------------------------------------------------------
            all_labels: list[str] = []
            bar_colors: list[str] = []
            bar_heights: list[float] = []
        
            for rec in ctx_records:
                all_labels.append(rec["date"])
                bar_heights.append(rec["residual"])
                bar_colors.append(CLR[rec["severity"]])
        
            fc_deviations: list[float] = []
            for rec in fc_records:
                all_labels.append(rec["date"])
                dev = rec["actual"] - rec["forecast"]
                fc_deviations.append(dev)
                bar_heights.append(dev)
                bar_colors.append(CLR[rec["severity"]])
        
            xs = np.arange(len(all_labels))
            ax2.bar(xs[:36], bar_heights[:36], color=bar_colors[:36], alpha=0.8)
            ax2.bar(xs[36:], bar_heights[36:], color=bar_colors[36:], alpha=0.8)
        
            # threshold lines for context section only
            ax2.hlines(
                [2 * res_std, -2 * res_std], -0.5, 35.5, colors=CLR["NORMAL"], lw=1.2, ls="--"
            )
            ax2.hlines(
                [3 * res_std, -3 * res_std], -0.5, 35.5, colors=CLR["NORMAL"], lw=1.0, ls=":"
            )
        
            # PI bands for forecast section
            fc_xs = xs[36:]
            ax2.fill_between(
                fc_xs,
                q10 - point_fc,
                q90 - point_fc,
                alpha=0.12,
                color=CLR["CRITICAL"],
                step="mid",
            )
            ax2.fill_between(
                fc_xs,
                q20 - point_fc,
                q80 - point_fc,
                alpha=0.20,
                color=CLR["CRITICAL"],
                step="mid",
            )
        
            ax2.axvline(35.5, color="#555555", lw=1.5, ls="--")
            ax2.axhline(0, color="black", lw=0.8, alpha=0.6)
        
            ax2.text(
                10,
                ax2.get_ylim()[0] * 0.85 if ax2.get_ylim()[0] < 0 else -0.05,
                "<- Context: delta from linear trend",
                fontsize=8,
                style="italic",
                color="#555555",
                ha="center",
            )
            ax2.text(
                41,
                ax2.get_ylim()[0] * 0.85 if ax2.get_ylim()[0] < 0 else -0.05,
                "Forecast: delta from TimesFM ->",
                fontsize=8,
                style="italic",
                color="#555555",
                ha="center",
            )
        
            tick_every = 3
            ax2.set_xticks(xs[::tick_every])
            ax2.set_xticklabels(all_labels[::tick_every], rotation=45, ha="right", fontsize=7)
            ax2.set_ylabel("Delta from expected (C)", fontsize=10)
            ax2.grid(True, alpha=0.22, axis="y")
        
            legend_patches = [
                mpatches.Patch(color=CLR["CRITICAL"], label="CRITICAL"),
                mpatches.Patch(color=CLR["WARNING"], label="WARNING"),
                mpatches.Patch(color=CLR["NORMAL"], label="Normal"),
            ]
            ax2.legend(handles=legend_patches, fontsize=8, loc="upper right")
        
            output_path = OUTPUT_DIR / "anomaly_detection.png"
            plt.savefig(output_path, dpi=150, bbox_inches="tight")
            plt.close()
            print(f"\n  Saved: {output_path}")
        
        
        # ---------------------------------------------------------------------------
        # Main
        # ---------------------------------------------------------------------------
        
        
        def main() -> None:
            print("=" * 68)
            print("  TIMESFM ANOMALY DETECTION — TWO-PHASE METHOD")
            print("=" * 68)
        
            # --- Load context data ---------------------------------------------------
            df = pd.read_csv(DATA_FILE)
            df["date"] = pd.to_datetime(df["date"])
            df = df.sort_values("date").reset_index(drop=True)
        
            context_values = df["anomaly_c"].values.astype(np.float32)
            context_dates = [pd.Timestamp(d) for d in df["date"].tolist()]
            start_str = context_dates[0].strftime('%Y-%m') if not pd.isnull(context_dates[0]) else '?'
            end_str   = context_dates[-1].strftime('%Y-%m') if not pd.isnull(context_dates[-1]) else '?'
            print(f"\n  Context: {len(context_values)} months  ({start_str} - {end_str})")
        
            # --- Phase 1: context anomaly detection ----------------------------------
            ctx_records, trend_line, residuals, res_std = detect_context_anomalies(
                context_values, context_dates
            )
            ctx_critical = [r for r in ctx_records if r["severity"] == "CRITICAL"]
            ctx_warning = [r for r in ctx_records if r["severity"] == "WARNING"]
            print(f"\n  [Phase 1] Context anomalies (Z-score, sigma={res_std:.3f} C):")
            print(f"    CRITICAL (|Z|>={CRITICAL_Z}): {len(ctx_critical)}")
            for r in ctx_critical:
                print(f"      {r['date']}  {r['value']:+.3f} C  z={r['z_score']:+.2f}")
            print(f"    WARNING  (|Z|>={WARNING_Z}): {len(ctx_warning)}")
            for r in ctx_warning:
                print(f"      {r['date']}  {r['value']:+.3f} C  z={r['z_score']:+.2f}")
        
            # --- Load TimesFM --------------------------------------------------------
            print("\n  Loading TimesFM 2.5 ...")
            import timesfm
        
            model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
                "google/timesfm-2.5-200m-pytorch"
            )
            model.compile(
                timesfm.ForecastConfig(
                    max_context=64,
                    max_horizon=128,
                    normalize_inputs=True,
                    use_continuous_quantile_head=True,
                    fix_quantile_crossing=True,
                    infer_is_positive=False,  # temperature anomalies can be negative
                )
            )
        
            point_out, quant_out = model.forecast(horizon=HORIZON, inputs=[context_values])
            point_fc = point_out[0]  # shape (HORIZON,)
            quant_fc = quant_out[0].T  # shape (10, HORIZON)
        
            # --- Build synthetic future + Phase 2 detection --------------------------
            future_values, injected = build_synthetic_future(context_values, HORIZON)
            last_date = context_dates[-1]
            future_dates = [last_date + pd.DateOffset(months=i + 1) for i in range(HORIZON)]
        
            fc_records = detect_forecast_anomalies(
                future_values, point_fc, quant_fc, future_dates, injected
            )
            fc_critical = [r for r in fc_records if r["severity"] == "CRITICAL"]
            fc_warning = [r for r in fc_records if r["severity"] == "WARNING"]
        
            print(f"\n  [Phase 2] Forecast anomalies (quantile PI, horizon={HORIZON} months):")
            print(f"    CRITICAL (outside 80% PI): {len(fc_critical)}")
            for r in fc_critical:
                print(
                    f"      {r['date']}  actual={r['actual']:+.3f}  "
                    f"fc={r['forecast']:+.3f}  injected={r['was_injected']}"
                )
            print(f"    WARNING  (outside 60% PI): {len(fc_warning)}")
            for r in fc_warning:
                print(
                    f"      {r['date']}  actual={r['actual']:+.3f}  "
                    f"fc={r['forecast']:+.3f}  injected={r['was_injected']}"
                )
        
            # --- Plot ----------------------------------------------------------------
            print("\n  Generating 2-panel visualization...")
            plot_results(
                context_dates,
                context_values,
                ctx_records,
                trend_line,
                residuals,
                res_std,
                future_dates,
                future_values,
                point_fc,
                quant_fc,
                fc_records,
            )
        
            # --- Save JSON -----------------------------------------------------------
            OUTPUT_DIR.mkdir(exist_ok=True)
            out = {
                "method": "two_phase",
                "context_method": "linear_detrend_zscore",
                "forecast_method": "quantile_prediction_intervals",
                "thresholds": {
                    "critical_z": CRITICAL_Z,
                    "warning_z": WARNING_Z,
                    "pi_critical_pct": 80,
                    "pi_warning_pct": 60,
                },
                "context_summary": {
                    "total": len(ctx_records),
                    "critical": len(ctx_critical),
                    "warning": len(ctx_warning),
                    "normal": len([r for r in ctx_records if r["severity"] == "NORMAL"]),
                    "res_std": round(float(res_std), 5),
                },
                "forecast_summary": {
                    "total": len(fc_records),
                    "critical": len(fc_critical),
                    "warning": len(fc_warning),
                    "normal": len([r for r in fc_records if r["severity"] == "NORMAL"]),
                },
                "context_detections": ctx_records,
                "forecast_detections": fc_records,
            }
            json_path = OUTPUT_DIR / "anomaly_detection.json"
            with open(json_path, "w") as f:
                json.dump(out, f, indent=2)
            print(f"  Saved: {json_path}")
        
            print("\n" + "=" * 68)
            print("  SUMMARY")
            print("=" * 68)
            print(
                f"  Context  ({len(ctx_records)} months): "
                f"{len(ctx_critical)} CRITICAL, {len(ctx_warning)} WARNING"
            )
            print(
                f"  Forecast ({len(fc_records)} months): "
                f"{len(fc_critical)} CRITICAL, {len(fc_warning)} WARNING"
            )
            print("=" * 68)
        
        
        if __name__ == "__main__":
            main()
        
    • covariates-forecasting
      • output
        • covariates_data.png 447.8 KB · in bundle
        • covariates_metadata.json 1.5 KB
          {
            "description": "Synthetic retail sales data with covariates for TimesFM XReg demo",
            "note_on_real_data": "For real datasets (e.g., Kaggle Rossmann Store Sales), download to tempfile.mkdtemp() -- do NOT commit to this repo.",
            "stores": {
              "store_A": {
                "type": "premium",
                "region": "urban",
                "base_sales": 1000,
                "mean_sales_context": 1148.7
              },
              "store_B": {
                "type": "standard",
                "region": "suburban",
                "base_sales": 750,
                "mean_sales_context": 907.0
              },
              "store_C": {
                "type": "discount",
                "region": "rural",
                "base_sales": 500,
                "mean_sales_context": 645.3
              }
            },
            "dimensions": {
              "context_length": 24,
              "horizon_length": 12,
              "total_length": 36,
              "num_stores": 3,
              "csv_rows": 108
            },
            "covariates": {
              "dynamic_numerical": [
                "price"
              ],
              "dynamic_categorical": [
                "promotion",
                "holiday",
                "day_of_week"
              ],
              "static_categorical": [
                "store_type",
                "region"
              ]
            },
            "effect_magnitudes": {
              "holiday": "+200 units per holiday week",
              "promotion": "+150 units per promotion week",
              "price": "-20 units per $1 above base price"
            },
            "xreg_modes": {
              "xreg + timesfm": "TimesFM on regression residuals (default)",
              "timesfm + xreg": "Regression on TimesFM residuals"
            },
            "bug_fixes_history": [
              "v1: Variable-shadowing -- all stores had identical covariates",
              "v2: Fixed shadowing; CONTEXT_LEN 48->24",
              "v3: Added component decomposition (base, price/promo/holiday effects); 2x2 sharex viz"
            ]
          }
        • sales_with_covariates.csv 7.2 KB · in bundle
      • demo_covariates.py 19.6 KB
        #!/usr/bin/env python3
        """
        TimesFM Covariates (XReg) Example
        
        Demonstrates the TimesFM covariate API using synthetic retail sales data.
        TimesFM 1.0 does NOT support forecast_with_covariates(); that requires
        TimesFM 2.5 + `uv pip install "timesfm[torch,xreg]"` (the xreg extra pulls in JAX
        and scikit-learn for the in-context linear regression).
        
        This script:
          1. Generates synthetic 3-store weekly retail data (24-week context, 12-week horizon)
          2. Produces a 2x2 visualization showing WHAT each covariate contributes
             and WHY knowing them improves forecasts -- all panels share the same
             week x-axis (0 = first context week, 35 = last horizon week)
          3. Exports a compact CSV (108 rows) and metadata JSON
        
        NOTE ON REAL DATA:
          If you want to use a real retail dataset (e.g., Kaggle Rossmann Store Sales),
          download it to a TEMP location -- do NOT commit large CSVs to this repo.
        
              import tempfile, urllib.request
              tmp = tempfile.mkdtemp(prefix="timesfm_retail_")
              # urllib.request.urlretrieve("https://...store_sales.csv", f"{tmp}/store_sales.csv")
              # df = pd.read_csv(f"{tmp}/store_sales.csv")
        
          This skills directory intentionally keeps only tiny reference datasets.
        """
        
        from __future__ import annotations
        
        import json
        from pathlib import Path
        
        import matplotlib
        
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt
        import numpy as np
        import pandas as pd
        
        EXAMPLE_DIR = Path(__file__).parent
        OUTPUT_DIR = EXAMPLE_DIR / "output"
        
        N_STORES = 3
        CONTEXT_LEN = 24
        HORIZON_LEN = 12
        TOTAL_LEN = CONTEXT_LEN + HORIZON_LEN  # 36
        
        
        def generate_sales_data() -> dict:
            """Generate synthetic retail sales data with covariate components stored separately.
        
            Returns a dict with:
              stores:     {store_id: {sales, config}}
              covariates: {price, promotion, holiday, day_of_week, store_type, region}
              components: {store_id: {base, price_effect, promo_effect, holiday_effect}}
        
            Components let us show 'what would sales look like without covariates?' --
            the gap between 'base' and 'sales' IS the covariate signal.
        
            BUG FIX v3: Previous versions had variable-shadowing where inner dict
            comprehension `{store_id: ... for store_id in stores}` overwrote the outer
            loop variable causing all stores to get identical covariate arrays.
            Fixed by accumulating per-store arrays separately before building covariate dict.
            """
            rng = np.random.default_rng(42)
        
            stores = {
                "store_A": {"type": "premium", "region": "urban", "base_sales": 1000},
                "store_B": {"type": "standard", "region": "suburban", "base_sales": 750},
                "store_C": {"type": "discount", "region": "rural", "base_sales": 500},
            }
            base_prices = {"store_A": 12.0, "store_B": 10.0, "store_C": 7.5}
        
            data: dict = {"stores": {}, "covariates": {}, "components": {}}
        
            prices_by_store: dict[str, np.ndarray] = {}
            promos_by_store: dict[str, np.ndarray] = {}
            holidays_by_store: dict[str, np.ndarray] = {}
            dow_by_store: dict[str, np.ndarray] = {}
        
            for store_id, config in stores.items():
                bp = base_prices[store_id]
                weeks = np.arange(TOTAL_LEN)
        
                trend = config["base_sales"] * (1 + 0.005 * weeks)
                seasonality = 80 * np.sin(2 * np.pi * weeks / 52)
                noise = rng.normal(0, 40, TOTAL_LEN)
                base = (trend + seasonality + noise).astype(np.float32)
        
                price = (bp + rng.uniform(-0.5, 0.5, TOTAL_LEN)).astype(np.float32)
                price_effect = (-20 * (price - bp)).astype(np.float32)
        
                holidays = np.zeros(TOTAL_LEN, dtype=np.float32)
                for hw in [0, 11, 23, 35]:
                    if hw < TOTAL_LEN:
                        holidays[hw] = 1.0
                holiday_effect = (200 * holidays).astype(np.float32)
        
                promotion = rng.choice([0.0, 1.0], TOTAL_LEN, p=[0.8, 0.2]).astype(np.float32)
                promo_effect = (150 * promotion).astype(np.float32)
        
                day_of_week = np.tile(np.arange(7), TOTAL_LEN // 7 + 1)[:TOTAL_LEN].astype(
                    np.int32
                )
        
                sales = np.maximum(base + price_effect + holiday_effect + promo_effect, 50.0)
        
                data["stores"][store_id] = {"sales": sales, "config": config}
                data["components"][store_id] = {
                    "base": base,
                    "price_effect": price_effect,
                    "promo_effect": promo_effect,
                    "holiday_effect": holiday_effect,
                }
        
                prices_by_store[store_id] = price
                promos_by_store[store_id] = promotion
                holidays_by_store[store_id] = holidays
                dow_by_store[store_id] = day_of_week
        
            data["covariates"] = {
                "price": prices_by_store,
                "promotion": promos_by_store,
                "holiday": holidays_by_store,
                "day_of_week": dow_by_store,
                "store_type": {sid: stores[sid]["type"] for sid in stores},
                "region": {sid: stores[sid]["region"] for sid in stores},
            }
            return data
        
        
        def create_visualization(data: dict) -> None:
            """
            2x2 figure -- ALL panels share x-axis = weeks 0-35.
        
            (0,0) Sales by store -- context solid, horizon dashed
            (0,1) Store A: actual vs baseline (no covariates), with event overlays showing uplift
            (1,0) Price covariate for all stores -- full 36 weeks including horizon
            (1,1) Covariate effect decomposition for Store A (stacked fill_between)
        
            Each panel has a conclusion annotation box explaining what the data shows.
            """
            OUTPUT_DIR.mkdir(exist_ok=True)
        
            store_colors = {"store_A": "#1a56db", "store_B": "#057a55", "store_C": "#c03221"}
            weeks = np.arange(TOTAL_LEN)
        
            fig, axes = plt.subplots(
                2,
                2,
                figsize=(16, 11),
                sharex=True,
                gridspec_kw={"hspace": 0.42, "wspace": 0.32},
            )
            fig.suptitle(
                "TimesFM Covariates (XReg) -- Retail Sales with Exogenous Variables\n"
                "Shared x-axis: Week 0-23 = context (observed) | Week 24-35 = forecast horizon",
                fontsize=13,
                fontweight="bold",
                y=1.01,
            )
        
            def add_divider(ax, label_top=True):
                ax.axvline(CONTEXT_LEN - 0.5, color="#9ca3af", lw=1.3, ls="--", alpha=0.8)
                ax.axvspan(
                    CONTEXT_LEN - 0.5, TOTAL_LEN - 0.5, alpha=0.06, color="grey", zorder=0
                )
                if label_top:
                    ax.text(
                        CONTEXT_LEN + 0.3,
                        1.01,
                        "<- horizon ->",
                        transform=ax.get_xaxis_transform(),
                        fontsize=7.5,
                        color="#6b7280",
                        style="italic",
                    )
        
            # -- (0,0): Sales by Store ---------------------------------------------------
            ax = axes[0, 0]
            base_price_labels = {"store_A": "$12", "store_B": "$10", "store_C": "$7.50"}
            for sid, store_data in data["stores"].items():
                sales = store_data["sales"]
                c = store_colors[sid]
                lbl = f"{sid} ({store_data['config']['type']}, {base_price_labels[sid]} base)"
                ax.plot(
                    weeks[:CONTEXT_LEN],
                    sales[:CONTEXT_LEN],
                    color=c,
                    lw=2,
                    marker="o",
                    ms=3,
                    label=lbl,
                )
                ax.plot(
                    weeks[CONTEXT_LEN:],
                    sales[CONTEXT_LEN:],
                    color=c,
                    lw=1.5,
                    ls="--",
                    marker="o",
                    ms=3,
                    alpha=0.6,
                )
            add_divider(ax)
            ax.set_ylabel("Weekly Sales (units)", fontsize=10)
            ax.set_title("Sales by Store", fontsize=11, fontweight="bold")
            ax.legend(fontsize=7.5, loc="upper left")
            ax.grid(True, alpha=0.22)
            ratio = (
                data["stores"]["store_A"]["sales"][:CONTEXT_LEN].mean()
                / data["stores"]["store_C"]["sales"][:CONTEXT_LEN].mean()
            )
            ax.annotate(
                f"Store A earns {ratio:.1f}x Store C\n(premium vs discount pricing)\n"
                f"-> store_type is a useful static covariate",
                xy=(0.97, 0.05),
                xycoords="axes fraction",
                ha="right",
                fontsize=8,
                bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
            )
        
            # -- (0,1): Store A actual vs baseline ---------------------------------------
            ax = axes[0, 1]
            comp_A = data["components"]["store_A"]
            sales_A = data["stores"]["store_A"]["sales"]
            base_A = comp_A["base"]
            promo_A = data["covariates"]["promotion"]["store_A"]
            holiday_A = data["covariates"]["holiday"]["store_A"]
        
            ax.plot(
                weeks[:CONTEXT_LEN],
                base_A[:CONTEXT_LEN],
                color="#9ca3af",
                lw=1.8,
                ls="--",
                label="Baseline (no covariates)",
            )
            ax.fill_between(
                weeks[:CONTEXT_LEN],
                base_A[:CONTEXT_LEN],
                sales_A[:CONTEXT_LEN],
                where=(sales_A[:CONTEXT_LEN] > base_A[:CONTEXT_LEN]),
                alpha=0.35,
                color="#22c55e",
                label="Covariate uplift",
            )
            ax.fill_between(
                weeks[:CONTEXT_LEN],
                sales_A[:CONTEXT_LEN],
                base_A[:CONTEXT_LEN],
                where=(sales_A[:CONTEXT_LEN] < base_A[:CONTEXT_LEN]),
                alpha=0.30,
                color="#ef4444",
                label="Price suppression",
            )
            ax.plot(
                weeks[:CONTEXT_LEN],
                sales_A[:CONTEXT_LEN],
                color=store_colors["store_A"],
                lw=2,
                label="Actual sales (Store A)",
            )
        
            for w in range(CONTEXT_LEN):
                if holiday_A[w] > 0:
                    ax.axvspan(w - 0.45, w + 0.45, alpha=0.22, color="darkorange", zorder=0)
            promo_weeks = [w for w in range(CONTEXT_LEN) if promo_A[w] > 0]
            if promo_weeks:
                ax.scatter(
                    promo_weeks,
                    sales_A[promo_weeks],
                    marker="^",
                    color="#16a34a",
                    s=70,
                    zorder=6,
                    label="Promotion week",
                )
        
            add_divider(ax)
            ax.set_ylabel("Weekly Sales (units)", fontsize=10)
            ax.set_title(
                "Store A -- Actual vs Baseline (No Covariates)", fontsize=11, fontweight="bold"
            )
            ax.legend(fontsize=7.5, loc="upper left", ncol=2)
            ax.grid(True, alpha=0.22)
        
            hm = holiday_A[:CONTEXT_LEN] > 0
            pm = promo_A[:CONTEXT_LEN] > 0
            h_lift = (
                (sales_A[:CONTEXT_LEN][hm] - base_A[:CONTEXT_LEN][hm]).mean() if hm.any() else 0
            )
            p_lift = (
                (sales_A[:CONTEXT_LEN][pm] - base_A[:CONTEXT_LEN][pm]).mean() if pm.any() else 0
            )
            ax.annotate(
                f"Holiday weeks: +{h_lift:.0f} units avg\n"
                f"Promotion weeks: +{p_lift:.0f} units avg\n"
                f"Future event schedules must be known for XReg",
                xy=(0.97, 0.05),
                xycoords="axes fraction",
                ha="right",
                fontsize=8,
                bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
            )
        
            # -- (1,0): Price covariate -- full 36 weeks ---------------------------------
            ax = axes[1, 0]
            for sid in data["stores"]:
                ax.plot(
                    weeks,
                    data["covariates"]["price"][sid],
                    color=store_colors[sid],
                    lw=2,
                    label=sid,
                    alpha=0.85,
                )
            add_divider(ax, label_top=False)
            ax.set_xlabel("Week", fontsize=10)
            ax.set_ylabel("Price ($)", fontsize=10)
            ax.set_title(
                "Price Covariate -- Context + Forecast Horizon", fontsize=11, fontweight="bold"
            )
            ax.legend(fontsize=8, loc="upper right")
            ax.grid(True, alpha=0.22)
            ax.annotate(
                "Prices are planned -- known for forecast horizon\n"
                "Price elasticity: -$1 increase -> -20 units sold\n"
                "Store A ($12) consistently more expensive than C ($7.50)",
                xy=(0.97, 0.05),
                xycoords="axes fraction",
                ha="right",
                fontsize=8,
                bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
            )
        
            # -- (1,1): Covariate effect decomposition -----------------------------------
            ax = axes[1, 1]
            pe = comp_A["price_effect"]
            pre = comp_A["promo_effect"]
            he = comp_A["holiday_effect"]
        
            ax.fill_between(
                weeks,
                0,
                pe,
                alpha=0.65,
                color="steelblue",
                step="mid",
                label=f"Price effect (max +/-{np.abs(pe).max():.0f} units)",
            )
            ax.fill_between(
                weeks,
                pe,
                pe + pre,
                alpha=0.70,
                color="#22c55e",
                step="mid",
                label="Promotion effect (+150 units)",
            )
            ax.fill_between(
                weeks,
                pe + pre,
                pe + pre + he,
                alpha=0.70,
                color="darkorange",
                step="mid",
                label="Holiday effect (+200 units)",
            )
            total = pe + pre + he
            ax.plot(weeks, total, "k-", lw=1.5, alpha=0.75, label="Total covariate effect")
            ax.axhline(0, color="black", lw=0.9, alpha=0.6)
            add_divider(ax, label_top=False)
            ax.set_xlabel("Week", fontsize=10)
            ax.set_ylabel("Effect on sales (units)", fontsize=10)
            ax.set_title(
                "Store A -- Covariate Effect Decomposition", fontsize=11, fontweight="bold"
            )
            ax.legend(fontsize=7.5, loc="upper right")
            ax.grid(True, alpha=0.22, axis="y")
            ax.annotate(
                f"Holidays (+200) and promotions (+150) dominate\n"
                f"Price effect (+/-{np.abs(pe).max():.0f} units) is minor by comparison\n"
                f"-> Time-varying covariates explain most sales spikes",
                xy=(0.97, 0.55),
                xycoords="axes fraction",
                ha="right",
                fontsize=8,
                bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
            )
        
            tick_pos = list(range(0, TOTAL_LEN, 4))
            for row in [0, 1]:
                for col in [0, 1]:
                    axes[row, col].set_xticks(tick_pos)
        
            plt.tight_layout()
            output_path = OUTPUT_DIR / "covariates_data.png"
            plt.savefig(output_path, dpi=150, bbox_inches="tight")
            plt.close()
            print(f"\n Saved visualization: {output_path}")
        
        
        def demonstrate_api() -> None:
            print("\n" + "=" * 70)
            print("  TIMESFM COVARIATES API (TimesFM 2.5)")
            print("=" * 70)
            print("""
        # Installation
        uv pip install "timesfm[torch,xreg]"
        
        import timesfm
        model = timesfm.TimesFM_2p5_200M_torch.from_pretrained("google/timesfm-2.5-200m-pytorch")
        model.compile(timesfm.ForecastConfig(
            max_context=512, max_horizon=128, normalize_inputs=True,
            use_continuous_quantile_head=True, fix_quantile_crossing=True,
            return_backcast=True,   # required by forecast_with_covariates
        ))
        
        point_fc, quant_fc = model.forecast_with_covariates(
            inputs=[sales_a, sales_b, sales_c],
            dynamic_numerical_covariates={"price": [price_a, price_b, price_c]},
            dynamic_categorical_covariates={"holiday": [hol_a, hol_b, hol_c]},
            static_categorical_covariates={"store_type": ["premium","standard","discount"]},
            xreg_mode="xreg + timesfm",
            normalize_xreg_target_per_input=True,
        )
        # point_fc:  list of num_series arrays, each (horizon_len,)
        # quant_fc:  list of num_series arrays, each (horizon_len, 10)
        # horizon_len is inferred from the dynamic covariates (context + horizon values)
        """)
        
        
        def explain_xreg_modes() -> None:
            print("\n" + "=" * 70)
            print("  XREG MODES")
            print("=" * 70)
            print("""
        "xreg + timesfm" (DEFAULT)
          1. Fit a linear regression: target ~ covariates (in context)
          2. TimesFM forecasts the regression residuals
          3. Final = XReg prediction + TimesFM residual forecast
          Best when: covariates explain the main signal (e.g. temperature, price)
        
        "timesfm + xreg"
          1. TimesFM makes a baseline forecast
          2. Fit a linear regression on its residuals (actual - baseline) ~ covariates
          3. Final = TimesFM baseline + XReg adjustment
          Best when: covariates explain residual variation (e.g. promotions)
        """)
        
        
        def main() -> None:
            print("=" * 70)
            print("  TIMESFM COVARIATES (XREG) EXAMPLE")
            print("=" * 70)
        
            print("\n Generating synthetic retail sales data...")
            data = generate_sales_data()
        
            print(f"   Stores:         {list(data['stores'].keys())}")
            print(f"   Context length: {CONTEXT_LEN} weeks")
            print(f"   Horizon length: {HORIZON_LEN} weeks")
            print(f"   Covariates:     {list(data['covariates'].keys())}")
        
            demonstrate_api()
            explain_xreg_modes()
        
            print("\n Creating 2x2 visualization (shared x-axis)...")
            create_visualization(data)
        
            print("\n Saving output data...")
            OUTPUT_DIR.mkdir(exist_ok=True)
        
            records = []
            for store_id, store_data in data["stores"].items():
                for i in range(TOTAL_LEN):
                    records.append(
                        {
                            "store_id": store_id,
                            "week": i,
                            "split": "context" if i < CONTEXT_LEN else "horizon",
                            "sales": round(float(store_data["sales"][i]), 2),
                            "base_sales": round(
                                float(data["components"][store_id]["base"][i]), 2
                            ),
                            "price": round(float(data["covariates"]["price"][store_id][i]), 4),
                            "price_effect": round(
                                float(data["components"][store_id]["price_effect"][i]), 2
                            ),
                            "promotion": int(data["covariates"]["promotion"][store_id][i]),
                            "holiday": int(data["covariates"]["holiday"][store_id][i]),
                            "day_of_week": int(data["covariates"]["day_of_week"][store_id][i]),
                            "store_type": data["covariates"]["store_type"][store_id],
                            "region": data["covariates"]["region"][store_id],
                        }
                    )
        
            df = pd.DataFrame(records)
            csv_path = OUTPUT_DIR / "sales_with_covariates.csv"
            df.to_csv(csv_path, index=False)
            print(f"   Saved: {csv_path}  ({len(df)} rows x {len(df.columns)} cols)")
        
            metadata = {
                "description": "Synthetic retail sales data with covariates for TimesFM XReg demo",
                "note_on_real_data": (
                    "For real datasets (e.g., Kaggle Rossmann Store Sales), download to "
                    "tempfile.mkdtemp() -- do NOT commit to this repo."
                ),
                "stores": {
                    sid: {
                        **sdata["config"],
                        "mean_sales_context": round(
                            float(sdata["sales"][:CONTEXT_LEN].mean()), 1
                        ),
                    }
                    for sid, sdata in data["stores"].items()
                },
                "dimensions": {
                    "context_length": CONTEXT_LEN,
                    "horizon_length": HORIZON_LEN,
                    "total_length": TOTAL_LEN,
                    "num_stores": N_STORES,
                    "csv_rows": len(df),
                },
                "covariates": {
                    "dynamic_numerical": ["price"],
                    "dynamic_categorical": ["promotion", "holiday", "day_of_week"],
                    "static_categorical": ["store_type", "region"],
                },
                "effect_magnitudes": {
                    "holiday": "+200 units per holiday week",
                    "promotion": "+150 units per promotion week",
                    "price": "-20 units per $1 above base price",
                },
                "xreg_modes": {
                    "xreg + timesfm": "TimesFM on regression residuals (default)",
                    "timesfm + xreg": "Regression on TimesFM residuals",
                },
                "bug_fixes_history": [
                    "v1: Variable-shadowing -- all stores had identical covariates",
                    "v2: Fixed shadowing; CONTEXT_LEN 48->24",
                    "v3: Added component decomposition (base, price/promo/holiday effects); 2x2 sharex viz",
                ],
            }
        
            meta_path = OUTPUT_DIR / "covariates_metadata.json"
            with open(meta_path, "w") as f:
                json.dump(metadata, f, indent=2)
            print(f"   Saved: {meta_path}")
        
            print("\n" + "=" * 70)
            print("  COVARIATES EXAMPLE COMPLETE")
            print("=" * 70)
            print("""
        Key points:
          1. Requires timesfm[xreg] + TimesFM 2.5+ for actual inference
          2. Dynamic covariates need values for BOTH context AND horizon (future must be known!)
          3. Static covariates: one value per series (store_type, region)
          4. All 4 visualization panels share the same week x-axis (0-35)
          5. Effect decomposition shows holidays/promotions dominate over price variation
        
        Output files:
          output/covariates_data.png         -- 2x2 visualization with conclusions
          output/sales_with_covariates.csv   -- 108-row compact dataset
          output/covariates_metadata.json    -- metadata + effect magnitudes
        """)
        
        
        if __name__ == "__main__":
            main()
        
    • global-temperature
      • output
        • animation_data.json 130.1 KB
          {
            "metadata": {
              "model": "TimesFM 2.5 (200M) PyTorch",
              "total_steps": 25,
              "min_context": 12,
              "max_horizon": 36,
              "total_months": 48,
              "data_source": "NOAA GISTEMP Global Temperature Anomaly",
              "full_date_range": "2022-01 to 2024-12"
            },
            "actual_data": {
              "dates": [
                "2022-01",
                "2022-02",
                "2022-03",
                "2022-04",
                "2022-05",
                "2022-06",
                "2022-07",
                "2022-08",
                "2022-09",
                "2022-10",
                "2022-11",
                "2022-12",
                "2023-01",
                "2023-02",
                "2023-03",
                "2023-04",
                "2023-05",
                "2023-06",
                "2023-07",
                "2023-08",
                "2023-09",
                "2023-10",
                "2023-11",
                "2023-12",
                "2024-01",
                "2024-02",
                "2024-03",
                "2024-04",
                "2024-05",
                "2024-06",
                "2024-07",
                "2024-08",
                "2024-09",
                "2024-10",
                "2024-11",
                "2024-12"
              ],
              "values": [
                0.8899999856948853,
                0.8899999856948853,
                1.0199999809265137,
                0.8799999952316284,
                0.8500000238418579,
                0.8799999952316284,
                0.8799999952316284,
                0.8999999761581421,
                0.8799999952316284,
                0.949999988079071,
                0.7699999809265137,
                0.7799999713897705,
                0.8700000047683716,
                0.9800000190734863,
                1.2100000381469727,
                1.0,
                0.9399999976158142,
                1.0800000429153442,
                1.1799999475479126,
                1.2400000095367432,
                1.4700000286102295,
                1.3200000524520874,
                1.1799999475479126,
                1.159999966621399,
                1.2200000286102295,
                1.350000023841858,
                1.340000033378601,
                1.2599999904632568,
                1.149999976158142,
                1.2000000476837158,
                1.2400000095367432,
                1.2999999523162842,
                1.2799999713897705,
                1.2699999809265137,
                1.2200000286102295,
                1.2000000476837158
              ]
            },
            "animation_steps": [
              {
                "step": 1,
                "n_points": 12,
                "horizon": 36,
                "last_historical_date": "2022-12",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705
                ],
                "forecast_dates": [
                  "2023-01",
                  "2023-02",
                  "2023-03",
                  "2023-04",
                  "2023-05",
                  "2023-06",
                  "2023-07",
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  0.8088664412498474,
                  0.8213533163070679,
                  0.8296629786491394,
                  0.8272498846054077,
                  0.8320416808128357,
                  0.8274483680725098,
                  0.8228720426559448,
                  0.8319199085235596,
                  0.835488498210907,
                  0.8365721702575684,
                  0.8323603868484497,
                  0.8319913744926453,
                  0.8277210593223572,
                  0.8256510496139526,
                  0.8323256373405457,
                  0.8329482078552246,
                  0.8350358009338379,
                  0.8369770050048828,
                  0.8409219980239868,
                  0.8302637338638306,
                  0.8254238963127136,
                  0.8308954834938049,
                  0.8278215527534485,
                  0.825431227684021,
                  0.8326130509376526,
                  0.8362093567848206,
                  0.8295928835868835,
                  0.8252186179161072,
                  0.8292879462242126,
                  0.8271766901016235,
                  0.83133864402771,
                  0.8389358520507812,
                  0.8402608036994934,
                  0.8360776305198669,
                  0.8293388485908508,
                  0.8316023945808411
                ],
                "q10": [
                  0.7362232208251953,
                  0.7393317818641663,
                  0.740480363368988,
                  0.7334028482437134,
                  0.735645592212677,
                  0.7280339598655701,
                  0.7203625440597534,
                  0.7253413796424866,
                  0.7247218489646912,
                  0.7237515449523926,
                  0.7157658934593201,
                  0.7147862911224365,
                  0.7113339304924011,
                  0.7086962461471558,
                  0.7103153467178345,
                  0.7108928561210632,
                  0.7141188979148865,
                  0.7158302068710327,
                  0.7198674082756042,
                  0.7093445658683777,
                  0.7062948942184448,
                  0.7123644948005676,
                  0.7107348442077637,
                  0.7075759768486023,
                  0.7126845121383667,
                  0.7076839208602905,
                  0.7049165368080139,
                  0.69573575258255,
                  0.7001065015792847,
                  0.6987308263778687,
                  0.7023730278015137,
                  0.706115186214447,
                  0.7070751190185547,
                  0.7025228142738342,
                  0.6952767372131348,
                  0.6968531608581543
                ],
                "q20": [
                  0.7633756995201111,
                  0.768903374671936,
                  0.7715609669685364,
                  0.7690808176994324,
                  0.7710962891578674,
                  0.7644274830818176,
                  0.7575346827507019,
                  0.764971137046814,
                  0.7660325169563293,
                  0.7655666470527649,
                  0.758827269077301,
                  0.759317934513092,
                  0.7543391585350037,
                  0.7518272995948792,
                  0.7565937042236328,
                  0.7552905678749084,
                  0.7590928077697754,
                  0.7609377503395081,
                  0.7655678987503052,
                  0.7560577988624573,
                  0.7517439723014832,
                  0.7563831806182861,
                  0.7545374631881714,
                  0.7528203129768372,
                  0.7565345168113708,
                  0.7570515275001526,
                  0.7511169910430908,
                  0.7426859736442566,
                  0.7480732202529907,
                  0.747359573841095,
                  0.7478432655334473,
                  0.7544757723808289,
                  0.7554726600646973,
                  0.7525039315223694,
                  0.7466093301773071,
                  0.7486756443977356
                ],
                "q80": [
                  0.8619241118431091,
                  0.8804789185523987,
                  0.8957151770591736,
                  0.893984317779541,
                  0.9025715589523315,
                  0.8987082242965698,
                  0.8965424299240112,
                  0.9079017043113708,
                  0.9126548767089844,
                  0.9148302674293518,
                  0.9142650961875916,
                  0.9123477339744568,
                  0.9114906191825867,
                  0.9090692400932312,
                  0.9141737818717957,
                  0.9138196706771851,
                  0.9185777306556702,
                  0.9188478589057922,
                  0.9240081310272217,
                  0.9116800427436829,
                  0.9088319540023804,
                  0.9158249497413635,
                  0.9122939109802246,
                  0.910721480846405,
                  0.9219470620155334,
                  0.9230694770812988,
                  0.9163065552711487,
                  0.9147485494613647,
                  0.9189838171005249,
                  0.9166437983512878,
                  0.9231889843940735,
                  0.9288030862808228,
                  0.9290376305580139,
                  0.926073431968689,
                  0.9177376627922058,
                  0.9195855855941772
                ],
                "q90": [
                  0.8955454230308533,
                  0.9179462790489197,
                  0.9342303276062012,
                  0.9364297389984131,
                  0.9451982975006104,
                  0.9421089291572571,
                  0.9420742392539978,
                  0.9546166062355042,
                  0.9624627232551575,
                  0.9659777283668518,
                  0.963783323764801,
                  0.9645819067955017,
                  0.9597351551055908,
                  0.9616245031356812,
                  0.9671178460121155,
                  0.9675363898277283,
                  0.9707008600234985,
                  0.9712307453155518,
                  0.9782301187515259,
                  0.9661508202552795,
                  0.9644027352333069,
                  0.9665482044219971,
                  0.9662359952926636,
                  0.9655495882034302,
                  0.9740736484527588,
                  0.9811351299285889,
                  0.9691143035888672,
                  0.9669668078422546,
                  0.972588300704956,
                  0.9697466492652893,
                  0.9730788469314575,
                  0.9800776839256287,
                  0.97989422082901,
                  0.9777727127075195,
                  0.9716194868087769,
                  0.9716632962226868
                ]
              },
              {
                "step": 2,
                "n_points": 13,
                "horizon": 35,
                "last_historical_date": "2023-01",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716
                ],
                "forecast_dates": [
                  "2023-02",
                  "2023-03",
                  "2023-04",
                  "2023-05",
                  "2023-06",
                  "2023-07",
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  0.865241289138794,
                  0.8696656227111816,
                  0.8676479458808899,
                  0.8678996562957764,
                  0.8561481833457947,
                  0.8568556308746338,
                  0.8668228387832642,
                  0.8646612763404846,
                  0.8674001097679138,
                  0.8603436946868896,
                  0.8622444272041321,
                  0.8603477478027344,
                  0.8637131452560425,
                  0.8730989694595337,
                  0.8678280115127563,
                  0.8678702712059021,
                  0.8719590902328491,
                  0.8749479651451111,
                  0.8659812211990356,
                  0.8641831874847412,
                  0.8695651888847351,
                  0.8624573349952698,
                  0.8628767728805542,
                  0.8681024312973022,
                  0.8747267723083496,
                  0.8679366707801819,
                  0.8615780472755432,
                  0.8643974661827087,
                  0.8653913736343384,
                  0.8660670518875122,
                  0.8670449256896973,
                  0.8721159100532532,
                  0.8652377724647522,
                  0.858295738697052,
                  0.8651449680328369
                ],
                "q10": [
                  0.77837735414505,
                  0.7761418223381042,
                  0.7696338891983032,
                  0.7658881545066833,
                  0.7527641654014587,
                  0.7520480155944824,
                  0.7603661417961121,
                  0.7572698593139648,
                  0.7578057646751404,
                  0.749613344669342,
                  0.7515044212341309,
                  0.7489626407623291,
                  0.7535739541053772,
                  0.760023295879364,
                  0.7536056041717529,
                  0.7545074224472046,
                  0.7568178176879883,
                  0.758697509765625,
                  0.7471636533737183,
                  0.7434744238853455,
                  0.7482830882072449,
                  0.7425351142883301,
                  0.7433184385299683,
                  0.7470905780792236,
                  0.7536534667015076,
                  0.739619791507721,
                  0.7388734221458435,
                  0.7366622686386108,
                  0.7374582886695862,
                  0.7397369146347046,
                  0.7403240203857422,
                  0.7431556582450867,
                  0.736463725566864,
                  0.7287972569465637,
                  0.7353164553642273
                ],
                "q20": [
                  0.8112073540687561,
                  0.8106394410133362,
                  0.8047429323196411,
                  0.8052176833152771,
                  0.7919988632202148,
                  0.7921794652938843,
                  0.8010948300361633,
                  0.7990307807922363,
                  0.8013709783554077,
                  0.793975293636322,
                  0.7950073480606079,
                  0.7948554754257202,
                  0.7956244349479675,
                  0.8053519129753113,
                  0.8002361059188843,
                  0.8001100420951843,
                  0.802157998085022,
                  0.8052369356155396,
                  0.795232892036438,
                  0.7926785349845886,
                  0.7973878979682922,
                  0.7894378900527954,
                  0.7915023565292358,
                  0.7963889241218567,
                  0.8005077242851257,
                  0.7929205894470215,
                  0.7875109910964966,
                  0.7864100933074951,
                  0.7892763614654541,
                  0.7901501655578613,
                  0.7891127467155457,
                  0.7942411303520203,
                  0.7872227430343628,
                  0.7796047925949097,
                  0.788762629032135
                ],
                "q80": [
                  0.9218940138816833,
                  0.9311137795448303,
                  0.9326687455177307,
                  0.9334375858306885,
                  0.9236896634101868,
                  0.9249515533447266,
                  0.9360549449920654,
                  0.9345569610595703,
                  0.9382771849632263,
                  0.9308700561523438,
                  0.9337661266326904,
                  0.9303881525993347,
                  0.935418426990509,
                  0.9444085955619812,
                  0.9377750158309937,
                  0.9390276670455933,
                  0.9441524147987366,
                  0.9481852650642395,
                  0.9392039775848389,
                  0.9371410608291626,
                  0.9424258470535278,
                  0.9378934502601624,
                  0.9366370439529419,
                  0.9427338242530823,
                  0.9525296688079834,
                  0.9447291493415833,
                  0.936887800693512,
                  0.9421840310096741,
                  0.9419170022010803,
                  0.9432522058486938,
                  0.9461022615432739,
                  0.951067328453064,
                  0.9433810710906982,
                  0.9377023577690125,
                  0.9425805807113647
                ],
                "q90": [
                  0.9565442800521851,
                  0.9679500460624695,
                  0.9703750610351562,
                  0.9751231074333191,
                  0.965713620185852,
                  0.9682617783546448,
                  0.9807536602020264,
                  0.9801918268203735,
                  0.9850435853004456,
                  0.9780943393707275,
                  0.9800292253494263,
                  0.979936957359314,
                  0.9798586368560791,
                  0.9928213953971863,
                  0.9868407845497131,
                  0.9874966740608215,
                  0.9918557405471802,
                  0.9956948161125183,
                  0.9887789487838745,
                  0.986614465713501,
                  0.9924991726875305,
                  0.9848212599754333,
                  0.9867693781852722,
                  0.9923670887947083,
                  1.0000444650650024,
                  0.9983358979225159,
                  0.9861508011817932,
                  0.9925655722618103,
                  0.9943600296974182,
                  0.9940945506095886,
                  0.9945933222770691,
                  1.0012719631195068,
                  0.993750274181366,
                  0.9875503182411194,
                  0.994971513748169
                ]
              },
              {
                "step": 3,
                "n_points": 14,
                "horizon": 34,
                "last_historical_date": "2023-02",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863
                ],
                "forecast_dates": [
                  "2023-03",
                  "2023-04",
                  "2023-05",
                  "2023-06",
                  "2023-07",
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  0.9125930070877075,
                  0.8965274095535278,
                  0.8897064924240112,
                  0.883258581161499,
                  0.8802093267440796,
                  0.8861643075942993,
                  0.8859161138534546,
                  0.8857860565185547,
                  0.8782062530517578,
                  0.8814483880996704,
                  0.8831714391708374,
                  0.8932548761367798,
                  0.8949102163314819,
                  0.8891497850418091,
                  0.8831286430358887,
                  0.8888159990310669,
                  0.889955997467041,
                  0.8853509426116943,
                  0.8834298849105835,
                  0.8879107236862183,
                  0.8797404766082764,
                  0.8782256841659546,
                  0.8867436647415161,
                  0.8965823650360107,
                  0.8907245397567749,
                  0.8846020698547363,
                  0.8814154863357544,
                  0.8851478099822998,
                  0.8852506875991821,
                  0.8879326581954956,
                  0.8897445201873779,
                  0.8850711584091187,
                  0.8751901388168335,
                  0.8785642385482788
                ],
                "q10": [
                  0.8150502443313599,
                  0.7935221195220947,
                  0.7831082344055176,
                  0.7742792367935181,
                  0.7713347673416138,
                  0.7767363786697388,
                  0.7747073173522949,
                  0.7730660438537598,
                  0.762670636177063,
                  0.7642374038696289,
                  0.7664269208908081,
                  0.7758126258850098,
                  0.7785153388977051,
                  0.7692981958389282,
                  0.7627484798431396,
                  0.7686680555343628,
                  0.7690086364746094,
                  0.7647632360458374,
                  0.7602567672729492,
                  0.7629586458206177,
                  0.7537773847579956,
                  0.753504753112793,
                  0.7613660097122192,
                  0.7702450752258301,
                  0.7640422582626343,
                  0.7524927854537964,
                  0.7540092468261719,
                  0.7548362016677856,
                  0.7541289329528809,
                  0.7586184740066528,
                  0.7610676288604736,
                  0.7545610666275024,
                  0.7446208000183105,
                  0.7465369701385498
                ],
                "q20": [
                  0.8509358167648315,
                  0.830585241317749,
                  0.8208508491516113,
                  0.8148849010467529,
                  0.8114417791366577,
                  0.8172972202301025,
                  0.8163645267486572,
                  0.8157660961151123,
                  0.8071786165237427,
                  0.8104532957077026,
                  0.8111268281936646,
                  0.8229241371154785,
                  0.8228044509887695,
                  0.8167366981506348,
                  0.8106899261474609,
                  0.8161411285400391,
                  0.8153873682022095,
                  0.8119785785675049,
                  0.8088939189910889,
                  0.8132083415985107,
                  0.8045362234115601,
                  0.8016543388366699,
                  0.810797929763794,
                  0.8206342458724976,
                  0.8119103908538818,
                  0.8057065010070801,
                  0.8032900094985962,
                  0.8041738271713257,
                  0.8056414127349854,
                  0.8091568946838379,
                  0.8087270259857178,
                  0.8044416904449463,
                  0.7953182458877563,
                  0.7974810600280762
                ],
                "q80": [
                  0.9756828546524048,
                  0.9651288986206055,
                  0.9615590572357178,
                  0.954924464225769,
                  0.9534156322479248,
                  0.9598239660263062,
                  0.9607141017913818,
                  0.9609590768814087,
                  0.9545141458511353,
                  0.9573769569396973,
                  0.9599953889846802,
                  0.9687842130661011,
                  0.9717134237289429,
                  0.9659241437911987,
                  0.9601706266403198,
                  0.9662460088729858,
                  0.9686497449874878,
                  0.96462082862854,
                  0.9627068042755127,
                  0.9671430587768555,
                  0.9595035314559937,
                  0.9603006839752197,
                  0.9665240049362183,
                  0.9758268594741821,
                  0.9710080623626709,
                  0.9655646085739136,
                  0.9614157676696777,
                  0.9678468704223633,
                  0.9667477607727051,
                  0.969174861907959,
                  0.9732277393341064,
                  0.9688694477081299,
                  0.9580692052841187,
                  0.962073564529419
                ],
                "q90": [
                  1.014340877532959,
                  1.006066083908081,
                  1.0039570331573486,
                  1.0003474950790405,
                  0.9987590312957764,
                  1.0062971115112305,
                  1.0082859992980957,
                  1.0091193914413452,
                  1.0045729875564575,
                  1.0080937147140503,
                  1.010170817375183,
                  1.0199054479599,
                  1.0197503566741943,
                  1.0173603296279907,
                  1.0123732089996338,
                  1.018046259880066,
                  1.0193963050842285,
                  1.0155316591262817,
                  1.0155333280563354,
                  1.0208029747009277,
                  1.0138678550720215,
                  1.011103868484497,
                  1.0183448791503906,
                  1.0285290479660034,
                  1.022400975227356,
                  1.0214533805847168,
                  1.0132265090942383,
                  1.0210212469100952,
                  1.0205981731414795,
                  1.0216221809387207,
                  1.0237239599227905,
                  1.021578073501587,
                  1.0107207298278809,
                  1.0140960216522217
                ]
              },
              {
                "step": 4,
                "n_points": 15,
                "horizon": 33,
                "last_historical_date": "2023-03",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02",
                  "2023-03"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863,
                  1.2100000381469727
                ],
                "forecast_dates": [
                  "2023-04",
                  "2023-05",
                  "2023-06",
                  "2023-07",
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  1.032881736755371,
                  0.9933709502220154,
                  0.9667695760726929,
                  0.9184945821762085,
                  0.9608809351921082,
                  0.9049569964408875,
                  0.8999242782592773,
                  0.8992117047309875,
                  0.8684363961219788,
                  0.8703660368919373,
                  0.8702194094657898,
                  0.9089018702507019,
                  0.8719728589057922,
                  0.8649001717567444,
                  0.8637645244598389,
                  0.8729057312011719,
                  0.8936004638671875,
                  0.8886909484863281,
                  0.8822309374809265,
                  0.8671358823776245,
                  0.8729079365730286,
                  0.8898171186447144,
                  0.8961994051933289,
                  0.9159574508666992,
                  0.8931074738502502,
                  0.8835421204566956,
                  0.8829240798950195,
                  0.879989743232727,
                  0.8923989534378052,
                  0.895778477191925,
                  0.889941930770874,
                  0.8700044751167297,
                  0.8697490096092224
                ],
                "q10": [
                  0.8795272707939148,
                  0.8384661078453064,
                  0.8056771755218506,
                  0.7514580488204956,
                  0.792585015296936,
                  0.7346599102020264,
                  0.7255686521530151,
                  0.7200497984886169,
                  0.6834976077079773,
                  0.6833421587944031,
                  0.6763625741004944,
                  0.7177135944366455,
                  0.6827908158302307,
                  0.6729309558868408,
                  0.6689468622207642,
                  0.6839207410812378,
                  0.6997215747833252,
                  0.698107123374939,
                  0.6876631379127502,
                  0.6726993918418884,
                  0.675697922706604,
                  0.6985417008399963,
                  0.7016238570213318,
                  0.7244307994842529,
                  0.7003493905067444,
                  0.6821295022964478,
                  0.6913962364196777,
                  0.6868228316307068,
                  0.6937999129295349,
                  0.7063246369361877,
                  0.7029021978378296,
                  0.6816707253456116,
                  0.6786080002784729
                ],
                "q20": [
                  0.9346938729286194,
                  0.8934664726257324,
                  0.8590848445892334,
                  0.811957597732544,
                  0.8501499891281128,
                  0.7932955026626587,
                  0.7871618866920471,
                  0.7810631394386292,
                  0.7504785060882568,
                  0.7532151937484741,
                  0.7433745265007019,
                  0.7883854508399963,
                  0.7487297058105469,
                  0.7432934045791626,
                  0.7403719425201416,
                  0.7544263005256653,
                  0.7684431076049805,
                  0.767550528049469,
                  0.7604201436042786,
                  0.7480056881904602,
                  0.7528287172317505,
                  0.7686076164245605,
                  0.7757018804550171,
                  0.7970746159553528,
                  0.7697645425796509,
                  0.7609029412269592,
                  0.7628272771835327,
                  0.7553943395614624,
                  0.7681815028190613,
                  0.7767795920372009,
                  0.7668604254722595,
                  0.7483596205711365,
                  0.7491141557693481
                ],
                "q80": [
                  1.1406171321868896,
                  1.1145833730697632,
                  1.0979615449905396,
                  1.0489933490753174,
                  1.1014177799224854,
                  1.0495083332061768,
                  1.046041488647461,
                  1.0448428392410278,
                  1.01752769947052,
                  1.0195547342300415,
                  1.0209896564483643,
                  1.0604897737503052,
                  1.029372215270996,
                  1.0204695463180542,
                  1.0147007703781128,
                  1.0250550508499146,
                  1.0436185598373413,
                  1.0371036529541016,
                  1.0340055227279663,
                  1.0171023607254028,
                  1.0250215530395508,
                  1.0502387285232544,
                  1.0520119667053223,
                  1.068839192390442,
                  1.0476114749908447,
                  1.037192463874817,
                  1.033530354499817,
                  1.039818286895752,
                  1.0508650541305542,
                  1.0502228736877441,
                  1.0499238967895508,
                  1.0290801525115967,
                  1.030360460281372
                ],
                "q90": [
                  1.2161037921905518,
                  1.195737600326538,
                  1.181797742843628,
                  1.145155906677246,
                  1.1972581148147583,
                  1.1503835916519165,
                  1.1500006914138794,
                  1.149735450744629,
                  1.1245098114013672,
                  1.1277692317962646,
                  1.1296864748001099,
                  1.1682826280593872,
                  1.1356399059295654,
                  1.1272820234298706,
                  1.1286356449127197,
                  1.1359784603118896,
                  1.1564139127731323,
                  1.1493918895721436,
                  1.1491632461547852,
                  1.1295961141586304,
                  1.1399399042129517,
                  1.1561247110366821,
                  1.1625772714614868,
                  1.1807315349578857,
                  1.158644676208496,
                  1.1577023267745972,
                  1.1466375589370728,
                  1.1551461219787598,
                  1.162131667137146,
                  1.165910005569458,
                  1.15957772731781,
                  1.141502022743225,
                  1.1438343524932861
                ]
              },
              {
                "step": 5,
                "n_points": 16,
                "horizon": 32,
                "last_historical_date": "2023-04",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02",
                  "2023-03",
                  "2023-04"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863,
                  1.2100000381469727,
                  1.0
                ],
                "forecast_dates": [
                  "2023-05",
                  "2023-06",
                  "2023-07",
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  0.9544844627380371,
                  0.9325734376907349,
                  0.9057666063308716,
                  0.9343633651733398,
                  0.8975495100021362,
                  0.9130918979644775,
                  0.8848934173583984,
                  0.8730642795562744,
                  0.8900773525238037,
                  0.9096230268478394,
                  0.9416356086730957,
                  0.9120591878890991,
                  0.893348217010498,
                  0.8933862447738647,
                  0.8936120271682739,
                  0.8983229398727417,
                  0.8884241580963135,
                  0.9010888338088989,
                  0.8841112852096558,
                  0.8784829378128052,
                  0.8918598890304565,
                  0.9164948463439941,
                  0.9403691291809082,
                  0.9166082143783569,
                  0.8983249664306641,
                  0.8954064846038818,
                  0.8981704711914062,
                  0.8980978727340698,
                  0.8942031860351562,
                  0.8913118839263916,
                  0.8687677383422852,
                  0.8697205781936646
                ],
                "q10": [
                  0.8366692066192627,
                  0.8067655563354492,
                  0.7732248306274414,
                  0.7961797714233398,
                  0.7577214241027832,
                  0.7693302631378174,
                  0.7373653650283813,
                  0.7218222618103027,
                  0.7317745685577393,
                  0.7468284368515015,
                  0.77802574634552,
                  0.749346137046814,
                  0.7337929010391235,
                  0.7324731349945068,
                  0.7326406240463257,
                  0.7398781776428223,
                  0.7290271520614624,
                  0.742612361907959,
                  0.7206360101699829,
                  0.7094191312789917,
                  0.7211570143699646,
                  0.7474689483642578,
                  0.7693659067153931,
                  0.7469403743743896,
                  0.731969952583313,
                  0.7187391519546509,
                  0.7311376333236694,
                  0.7250378131866455,
                  0.7201212644577026,
                  0.7214102149009705,
                  0.6998387575149536,
                  0.698819100856781
                ],
                "q20": [
                  0.879749059677124,
                  0.8518922328948975,
                  0.8182146549224854,
                  0.8474950790405273,
                  0.8083600997924805,
                  0.8214960098266602,
                  0.7900927066802979,
                  0.7745316028594971,
                  0.7899496555328369,
                  0.8069479465484619,
                  0.8371009826660156,
                  0.8106673955917358,
                  0.7891895771026611,
                  0.7900681495666504,
                  0.7920819520950317,
                  0.7982132434844971,
                  0.7856446504592896,
                  0.7994391918182373,
                  0.7802129983901978,
                  0.7730735540390015,
                  0.7836601734161377,
                  0.8066291809082031,
                  0.8314725160598755,
                  0.8105231523513794,
                  0.7880481481552124,
                  0.786815881729126,
                  0.7912131547927856,
                  0.7846355438232422,
                  0.7851417064666748,
                  0.7848362922668457,
                  0.7549350261688232,
                  0.7579524517059326
                ],
                "q80": [
                  1.0351053476333618,
                  1.0242036581039429,
                  1.0053176879882812,
                  1.0349016189575195,
                  1.0042455196380615,
                  1.0217221975326538,
                  0.9962334632873535,
                  0.9877946376800537,
                  1.005587100982666,
                  1.0241262912750244,
                  1.060490608215332,
                  1.0284301042556763,
                  1.015038251876831,
                  1.0150749683380127,
                  1.0163213014602661,
                  1.0222978591918945,
                  1.0153249502182007,
                  1.0284150838851929,
                  1.012209415435791,
                  1.0036182403564453,
                  1.0173357725143433,
                  1.0460131168365479,
                  1.0672309398651123,
                  1.0454349517822266,
                  1.0309956073760986,
                  1.0304814577102661,
                  1.030550241470337,
                  1.035859227180481,
                  1.0309940576553345,
                  1.0283818244934082,
                  1.007351040840149,
                  1.009413480758667
                ],
                "q90": [
                  1.088782787322998,
                  1.0814296007156372,
                  1.0651271343231201,
                  1.103667140007019,
                  1.072096824645996,
                  1.0933531522750854,
                  1.0702825784683228,
                  1.064249873161316,
                  1.085000991821289,
                  1.1079119443893433,
                  1.1440540552139282,
                  1.1140565872192383,
                  1.0927988290786743,
                  1.098463535308838,
                  1.1019902229309082,
                  1.1066484451293945,
                  1.0999171733856201,
                  1.1141986846923828,
                  1.1008174419403076,
                  1.0935524702072144,
                  1.1083751916885376,
                  1.1316936016082764,
                  1.1556893587112427,
                  1.1337625980377197,
                  1.1194919347763062,
                  1.1272557973861694,
                  1.1212668418884277,
                  1.1273612976074219,
                  1.1243170499801636,
                  1.120854139328003,
                  1.0969750881195068,
                  1.101858377456665
                ]
              },
              {
                "step": 6,
                "n_points": 17,
                "horizon": 31,
                "last_historical_date": "2023-05",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02",
                  "2023-03",
                  "2023-04",
                  "2023-05"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863,
                  1.2100000381469727,
                  1.0,
                  0.9399999976158142
                ],
                "forecast_dates": [
                  "2023-06",
                  "2023-07",
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  0.9220501184463501,
                  0.9021075963973999,
                  0.9196691513061523,
                  0.8949684500694275,
                  0.9072319865226746,
                  0.8807746171951294,
                  0.8740841150283813,
                  0.8944268822669983,
                  0.9170041680335999,
                  0.9616637229919434,
                  0.9187473654747009,
                  0.9028078317642212,
                  0.8987056016921997,
                  0.8976238369941711,
                  0.9056991934776306,
                  0.8939219117164612,
                  0.9005017876625061,
                  0.8718598484992981,
                  0.8775186538696289,
                  0.8970916271209717,
                  0.9235289096832275,
                  0.9562622904777527,
                  0.9152999520301819,
                  0.9000718593597412,
                  0.8909444808959961,
                  0.8893237709999084,
                  0.898954451084137,
                  0.8902174234390259,
                  0.8906262516975403,
                  0.8632307052612305,
                  0.8710195422172546
                ],
                "q10": [
                  0.8193337917327881,
                  0.793817400932312,
                  0.8052181005477905,
                  0.7735587954521179,
                  0.7814819812774658,
                  0.7489681243896484,
                  0.7390453815460205,
                  0.7571810483932495,
                  0.7747156620025635,
                  0.8162355422973633,
                  0.7740774154663086,
                  0.7620123624801636,
                  0.760065495967865,
                  0.7567031383514404,
                  0.7638317346572876,
                  0.7527730464935303,
                  0.7589520215988159,
                  0.7283440828323364,
                  0.7287338972091675,
                  0.743599534034729,
                  0.7654957175254822,
                  0.8014828562736511,
                  0.7620424032211304,
                  0.7483586668968201,
                  0.7428982257843018,
                  0.7329285144805908,
                  0.7495617270469666,
                  0.7353415489196777,
                  0.735753059387207,
                  0.7101541757583618,
                  0.7168684005737305
                ],
                "q20": [
                  0.856826663017273,
                  0.830333411693573,
                  0.8440313339233398,
                  0.8159244060516357,
                  0.8258194923400879,
                  0.7958651781082153,
                  0.7871206402778625,
                  0.8049315214157104,
                  0.8265319466590881,
                  0.8703405261039734,
                  0.8262785077095032,
                  0.8130332827568054,
                  0.8070295453071594,
                  0.8089984059333801,
                  0.8178558349609375,
                  0.8055921792984009,
                  0.8103337287902832,
                  0.7796828150749207,
                  0.7834690809249878,
                  0.8010856509208679,
                  0.8264617919921875,
                  0.8545052409172058,
                  0.8183755278587341,
                  0.806572675704956,
                  0.7956568598747253,
                  0.7934994697570801,
                  0.8043015003204346,
                  0.7911402583122253,
                  0.79295814037323,
                  0.7650884389877319,
                  0.7684268355369568
                ],
                "q80": [
                  0.9995192289352417,
                  0.988165020942688,
                  1.014437198638916,
                  0.9902679324150085,
                  1.0089728832244873,
                  0.9829463362693787,
                  0.9802312254905701,
                  1.0007987022399902,
                  1.0251235961914062,
                  1.0681381225585938,
                  1.0259335041046143,
                  1.005437970161438,
                  1.004706859588623,
                  1.003812313079834,
                  1.0107853412628174,
                  1.0007336139678955,
                  1.0116283893585205,
                  0.9818609952926636,
                  0.9903008341789246,
                  1.0080190896987915,
                  1.035115122795105,
                  1.0687997341156006,
                  1.0276589393615723,
                  1.012544870376587,
                  1.0071214437484741,
                  1.0053738355636597,
                  1.0140740871429443,
                  1.0102497339248657,
                  1.0109583139419556,
                  0.9830296635627747,
                  0.9949139952659607
                ],
                "q90": [
                  1.0547047853469849,
                  1.048487663269043,
                  1.0766277313232422,
                  1.0592156648635864,
                  1.0778943300247192,
                  1.0540447235107422,
                  1.0532444715499878,
                  1.0760388374328613,
                  1.1014387607574463,
                  1.145371675491333,
                  1.1017498970031738,
                  1.0834314823150635,
                  1.0785722732543945,
                  1.0813685655593872,
                  1.0911476612091064,
                  1.0795379877090454,
                  1.088607668876648,
                  1.060987949371338,
                  1.0722323656082153,
                  1.0917062759399414,
                  1.1184828281402588,
                  1.1503663063049316,
                  1.1118690967559814,
                  1.0956041812896729,
                  1.0889523029327393,
                  1.0949807167053223,
                  1.0982210636138916,
                  1.0955382585525513,
                  1.098829984664917,
                  1.0704374313354492,
                  1.0790274143218994
                ]
              },
              {
                "step": 7,
                "n_points": 18,
                "horizon": 30,
                "last_historical_date": "2023-06",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02",
                  "2023-03",
                  "2023-04",
                  "2023-05",
                  "2023-06"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863,
                  1.2100000381469727,
                  1.0,
                  0.9399999976158142,
                  1.0800000429153442
                ],
                "forecast_dates": [
                  "2023-07",
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  0.98087078332901,
                  0.9855461716651917,
                  0.9541746973991394,
                  0.958601713180542,
                  0.9353117346763611,
                  0.9256796836853027,
                  0.9384511709213257,
                  0.9615865349769592,
                  0.9855899214744568,
                  0.9517750144004822,
                  0.9321075081825256,
                  0.9411008954048157,
                  0.939730167388916,
                  0.946065366268158,
                  0.9376538395881653,
                  0.9367172122001648,
                  0.9118211269378662,
                  0.9126870632171631,
                  0.9295854568481445,
                  0.9480547904968262,
                  0.9717631936073303,
                  0.942704439163208,
                  0.9228353500366211,
                  0.9347066879272461,
                  0.9314004778862,
                  0.9321922659873962,
                  0.9296144247055054,
                  0.930548906326294,
                  0.9068635106086731,
                  0.8996174931526184
                ],
                "q10": [
                  0.8517412543296814,
                  0.8458702564239502,
                  0.8057717680931091,
                  0.802758514881134,
                  0.7749388217926025,
                  0.7591342926025391,
                  0.7674649953842163,
                  0.7883778214454651,
                  0.8077208399772644,
                  0.7717421650886536,
                  0.7489222884178162,
                  0.7611699104309082,
                  0.7594054937362671,
                  0.7628589868545532,
                  0.751642107963562,
                  0.7514673471450806,
                  0.7251574993133545,
                  0.726761519908905,
                  0.7429383397102356,
                  0.7554072737693787,
                  0.7772794961929321,
                  0.7521500587463379,
                  0.7321100234985352,
                  0.7459696531295776,
                  0.7424036264419556,
                  0.7334294319152832,
                  0.7361115217208862,
                  0.7332212328910828,
                  0.7046105265617371,
                  0.7011197805404663
                ],
                "q20": [
                  0.8979611396789551,
                  0.8938847184181213,
                  0.8568505048751831,
                  0.8585013747215271,
                  0.8339247703552246,
                  0.8213943839073181,
                  0.8307627439498901,
                  0.8515982031822205,
                  0.8743396997451782,
                  0.8389559388160706,
                  0.8180291652679443,
                  0.8310453295707703,
                  0.8260010480880737,
                  0.8335192799568176,
                  0.8217775225639343,
                  0.8223456740379333,
                  0.7941096425056458,
                  0.7953428626060486,
                  0.8115556240081787,
                  0.8294941782951355,
                  0.8528211116790771,
                  0.8198570013046265,
                  0.8028331995010376,
                  0.8168413639068604,
                  0.8089840412139893,
                  0.8103041052818298,
                  0.8078049421310425,
                  0.803856372833252,
                  0.7817198038101196,
                  0.7741097807884216
                ],
                "q80": [
                  1.0704530477523804,
                  1.0864534378051758,
                  1.0650923252105713,
                  1.0713014602661133,
                  1.055281162261963,
                  1.045789122581482,
                  1.0600769519805908,
                  1.0834147930145264,
                  1.1089916229248047,
                  1.073487639427185,
                  1.056858777999878,
                  1.0664854049682617,
                  1.069751501083374,
                  1.0783069133758545,
                  1.0686755180358887,
                  1.0712732076644897,
                  1.050015926361084,
                  1.0523253679275513,
                  1.0724303722381592,
                  1.0870728492736816,
                  1.1110703945159912,
                  1.0836774110794067,
                  1.061486005783081,
                  1.0737686157226562,
                  1.0736602544784546,
                  1.0798463821411133,
                  1.0762066841125488,
                  1.085168480873108,
                  1.0603477954864502,
                  1.0515000820159912
                ],
                "q90": [
                  1.1324870586395264,
                  1.1551110744476318,
                  1.136146068572998,
                  1.149444818496704,
                  1.1366682052612305,
                  1.1294071674346924,
                  1.1450557708740234,
                  1.1711208820343018,
                  1.198819875717163,
                  1.1647748947143555,
                  1.1473416090011597,
                  1.1564605236053467,
                  1.158258080482483,
                  1.17169189453125,
                  1.166845679283142,
                  1.16807222366333,
                  1.1469342708587646,
                  1.1514042615890503,
                  1.1732285022735596,
                  1.191814661026001,
                  1.2145884037017822,
                  1.182774543762207,
                  1.1624226570129395,
                  1.1745063066482544,
                  1.1764562129974365,
                  1.1884748935699463,
                  1.182075023651123,
                  1.1911952495574951,
                  1.170755386352539,
                  1.1598678827285767
                ]
              },
              {
                "step": 8,
                "n_points": 19,
                "horizon": 29,
                "last_historical_date": "2023-07",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02",
                  "2023-03",
                  "2023-04",
                  "2023-05",
                  "2023-06",
                  "2023-07"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863,
                  1.2100000381469727,
                  1.0,
                  0.9399999976158142,
                  1.0800000429153442,
                  1.1799999475479126
                ],
                "forecast_dates": [
                  "2023-08",
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  1.080711007118225,
                  1.0476722717285156,
                  1.036281704902649,
                  0.989107608795166,
                  0.9940611124038696,
                  0.9996637105941772,
                  1.002669334411621,
                  1.0331133604049683,
                  0.9908102750778198,
                  0.9799997806549072,
                  0.9827334880828857,
                  0.9946244955062866,
                  0.9864649772644043,
                  0.9703959226608276,
                  0.9642912149429321,
                  0.9436467885971069,
                  0.9458589553833008,
                  0.9537132978439331,
                  0.9695243835449219,
                  0.994887113571167,
                  0.9692059755325317,
                  0.9577727317810059,
                  0.9623997211456299,
                  0.9662132263183594,
                  0.962289571762085,
                  0.953036904335022,
                  0.9478927850723267,
                  0.9287556409835815,
                  0.9364343881607056
                ],
                "q10": [
                  0.9196977615356445,
                  0.8756295442581177,
                  0.8545773029327393,
                  0.8016259670257568,
                  0.8067057132720947,
                  0.8098841905593872,
                  0.8092951774597168,
                  0.8364890813827515,
                  0.787555456161499,
                  0.7736799120903015,
                  0.7753913402557373,
                  0.792123556137085,
                  0.7859581708908081,
                  0.7645180821418762,
                  0.7579329609870911,
                  0.7393535375595093,
                  0.743321418762207,
                  0.7538007497787476,
                  0.7654019594192505,
                  0.7841991186141968,
                  0.7575802803039551,
                  0.7506732940673828,
                  0.7548415660858154,
                  0.760432243347168,
                  0.7601176500320435,
                  0.7416688799858093,
                  0.7445902228355408,
                  0.7230207920074463,
                  0.7276837825775146
                ],
                "q20": [
                  0.9750034809112549,
                  0.9337389469146729,
                  0.9136620759963989,
                  0.8658897876739502,
                  0.870386004447937,
                  0.8782131671905518,
                  0.877156138420105,
                  0.9039527177810669,
                  0.86199951171875,
                  0.8494189977645874,
                  0.8498010635375977,
                  0.8670316934585571,
                  0.8537746667861938,
                  0.8390743732452393,
                  0.8320298194885254,
                  0.8115524053573608,
                  0.8121004104614258,
                  0.8216454982757568,
                  0.8358892202377319,
                  0.8626763820648193,
                  0.8353471755981445,
                  0.8210495710372925,
                  0.8271037340164185,
                  0.8347315788269043,
                  0.8239364624023438,
                  0.818279504776001,
                  0.813523530960083,
                  0.791071891784668,
                  0.8017441034317017
                ],
                "q80": [
                  1.1896421909332275,
                  1.1762607097625732,
                  1.1758731603622437,
                  1.1297085285186768,
                  1.1438449621200562,
                  1.152435541152954,
                  1.156329870223999,
                  1.1858702898025513,
                  1.1460310220718384,
                  1.1329529285430908,
                  1.135762095451355,
                  1.1480522155761719,
                  1.146406650543213,
                  1.1305174827575684,
                  1.1246224641799927,
                  1.1080710887908936,
                  1.1052628755569458,
                  1.1148382425308228,
                  1.131864309310913,
                  1.155958890914917,
                  1.1316522359848022,
                  1.1227316856384277,
                  1.128767490386963,
                  1.131961703300476,
                  1.1339387893676758,
                  1.128844976425171,
                  1.1235500574111938,
                  1.1099764108657837,
                  1.1159868240356445
                ],
                "q90": [
                  1.2687934637069702,
                  1.2636959552764893,
                  1.2670968770980835,
                  1.2308285236358643,
                  1.247275710105896,
                  1.2591478824615479,
                  1.2652033567428589,
                  1.2975214719772339,
                  1.2586565017700195,
                  1.2465598583221436,
                  1.2493255138397217,
                  1.2631864547729492,
                  1.255868911743164,
                  1.249590516090393,
                  1.247074842453003,
                  1.2275753021240234,
                  1.2285006046295166,
                  1.2376205921173096,
                  1.2551411390304565,
                  1.2821271419525146,
                  1.2605160474777222,
                  1.247068166732788,
                  1.2528679370880127,
                  1.261684536933899,
                  1.2628902196884155,
                  1.266533613204956,
                  1.2558245658874512,
                  1.2441514730453491,
                  1.255190134048462
                ]
              },
              {
                "step": 9,
                "n_points": 20,
                "horizon": 28,
                "last_historical_date": "2023-08",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02",
                  "2023-03",
                  "2023-04",
                  "2023-05",
                  "2023-06",
                  "2023-07",
                  "2023-08"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863,
                  1.2100000381469727,
                  1.0,
                  0.9399999976158142,
                  1.0800000429153442,
                  1.1799999475479126,
                  1.2400000095367432
                ],
                "forecast_dates": [
                  "2023-09",
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  1.1922426223754883,
                  1.2098631858825684,
                  1.100298523902893,
                  1.0738366842269897,
                  1.0549321174621582,
                  1.0026826858520508,
                  1.0304441452026367,
                  1.0295737981796265,
                  0.9875823855400085,
                  0.9972972869873047,
                  1.0000556707382202,
                  0.9847385883331299,
                  0.9610478281974792,
                  0.955390989780426,
                  0.9194391369819641,
                  0.9119424223899841,
                  0.9300310015678406,
                  0.9602750539779663,
                  0.9983500242233276,
                  0.9803668260574341,
                  0.9620509147644043,
                  0.9724827408790588,
                  0.9795849919319153,
                  0.9775466918945312,
                  0.9605414271354675,
                  0.9492465257644653,
                  0.9282808899879456,
                  0.9200233817100525
                ],
                "q10": [
                  1.0217628479003906,
                  1.019641399383545,
                  0.8972402215003967,
                  0.8680728673934937,
                  0.8448814749717712,
                  0.7917778491973877,
                  0.8129575848579407,
                  0.8132039904594421,
                  0.7623622417449951,
                  0.7682194709777832,
                  0.7639622688293457,
                  0.7527958750724792,
                  0.7278140187263489,
                  0.7148745656013489,
                  0.6769647002220154,
                  0.6783226728439331,
                  0.6969042420387268,
                  0.7324048280715942,
                  0.7619717121124268,
                  0.7459046840667725,
                  0.7258397936820984,
                  0.7497656941413879,
                  0.7555427551269531,
                  0.7495037317276001,
                  0.7383043766021729,
                  0.7165833711624146,
                  0.7057961225509644,
                  0.6936802864074707
                ],
                "q20": [
                  1.0819292068481445,
                  1.0887326002120972,
                  0.9662137031555176,
                  0.93899005651474,
                  0.9153514504432678,
                  0.8668481111526489,
                  0.8892391324043274,
                  0.88505619764328,
                  0.8421231508255005,
                  0.851202666759491,
                  0.8480466604232788,
                  0.8344742655754089,
                  0.8048425912857056,
                  0.7976179122924805,
                  0.7605321407318115,
                  0.7609581351280212,
                  0.7754850387573242,
                  0.8092970848083496,
                  0.8448429703712463,
                  0.8315984606742859,
                  0.8114562034606934,
                  0.8211799263954163,
                  0.8343919515609741,
                  0.8273032307624817,
                  0.808134913444519,
                  0.8008373379707336,
                  0.7818284034729004,
                  0.7642573118209839
                ],
                "q80": [
                  1.3011014461517334,
                  1.343187928199768,
                  1.2476884126663208,
                  1.2243504524230957,
                  1.2133712768554688,
                  1.1702196598052979,
                  1.1986095905303955,
                  1.1994459629058838,
                  1.1620559692382812,
                  1.17525315284729,
                  1.1852881908416748,
                  1.1673295497894287,
                  1.1550437211990356,
                  1.149295687675476,
                  1.1117979288101196,
                  1.1154108047485352,
                  1.126950740814209,
                  1.1609079837799072,
                  1.1974833011627197,
                  1.1755201816558838,
                  1.1514952182769775,
                  1.1636412143707275,
                  1.171291470527649,
                  1.162102222442627,
                  1.1541998386383057,
                  1.1479324102401733,
                  1.1283540725708008,
                  1.1245970726013184
                ],
                "q90": [
                  1.3804570436477661,
                  1.4288644790649414,
                  1.342375636100769,
                  1.3340628147125244,
                  1.3250924348831177,
                  1.2812385559082031,
                  1.3114988803863525,
                  1.3183321952819824,
                  1.280714511871338,
                  1.295188307762146,
                  1.3041470050811768,
                  1.2911888360977173,
                  1.2716469764709473,
                  1.2746280431747437,
                  1.2428157329559326,
                  1.2415406703948975,
                  1.262570858001709,
                  1.2916929721832275,
                  1.3299453258514404,
                  1.3083765506744385,
                  1.2898699045181274,
                  1.2970939874649048,
                  1.3010331392288208,
                  1.2956008911132812,
                  1.2905380725860596,
                  1.2986949682235718,
                  1.2710200548171997,
                  1.2695214748382568
                ]
              },
              {
                "step": 10,
                "n_points": 21,
                "horizon": 27,
                "last_historical_date": "2023-09",
                "historical_dates": [
                  "2022-01",
                  "2022-02",
                  "2022-03",
                  "2022-04",
                  "2022-05",
                  "2022-06",
                  "2022-07",
                  "2022-08",
                  "2022-09",
                  "2022-10",
                  "2022-11",
                  "2022-12",
                  "2023-01",
                  "2023-02",
                  "2023-03",
                  "2023-04",
                  "2023-05",
                  "2023-06",
                  "2023-07",
                  "2023-08",
                  "2023-09"
                ],
                "historical_values": [
                  0.8899999856948853,
                  0.8899999856948853,
                  1.0199999809265137,
                  0.8799999952316284,
                  0.8500000238418579,
                  0.8799999952316284,
                  0.8799999952316284,
                  0.8999999761581421,
                  0.8799999952316284,
                  0.949999988079071,
                  0.7699999809265137,
                  0.7799999713897705,
                  0.8700000047683716,
                  0.9800000190734863,
                  1.2100000381469727,
                  1.0,
                  0.9399999976158142,
                  1.0800000429153442,
                  1.1799999475479126,
                  1.2400000095367432,
                  1.4700000286102295
                ],
                "forecast_dates": [
                  "2023-10",
                  "2023-11",
                  "2023-12",
                  "2024-01",
                  "2024-02",
                  "2024-03",
                  "2024-04",
                  "2024-05",
                  "2024-06",
                  "2024-07",
                  "2024-08",
                  "2024-09",
                  "2024-10",
                  "2024-11",
                  "2024-12",
                  "2025-01",
                  "2025-02",
                  "2025-03",
                  "2025-04",
                  "2025-05",
                  "2025-06",
                  "2025-07",
                  "2025-08",
                  "2025-09",
                  "2025-10",
                  "2025-11",
                  "2025-12"
                ],
                "point_forecast": [
                  1.3404628038406372,
                  1.217146873474121,
                  1.1326298713684082,
                  1.1711729764938354,
                  1.0787259340286255,
                  1.0912177562713623,
                  1.072016716003418,
                  1.0057430267333984,
                  1.0076451301574707,
                  0.987165093421936,
                  0.9742555022239685,
                  0.9744524955749512,
                  0.9478590488433838,
                  0.9171240329742432,
                  0.9205031394958496,
                  0.9512743353843689,
                  0.9674836993217468,
                  1.001110315322876,
                  0.9897450804710388,
                  0.9599570035934448,
                  0.9870807528495789,
                  0.9742226600646973,
                  0.964898943901062,
                  0.9835475087165833,
                  0.9640566110610962,
                  0.9044225811958313,
                  0.9052854180335999
                ],
                "q10": [
                  1.1103565692901611,
                  0.9679498076438904,
                  0.8681977987289429,
                  0.9065310955047607,
                  0.8097656965255737,
                  0.8170275688171387,
                  0.7973222136497498,
                  0.7274376153945923,
                  0.7170931100845337,
               
        • forecast_animation.gif 854.2 KB · in bundle
        • forecast_output.csv 1.5 KB · in bundle
        • forecast_output.json 4.1 KB
          {
            "model": "TimesFM 2.5 (200M) PyTorch",
            "input": {
              "source": "NOAA GISTEMP Global Temperature Anomaly",
              "n_observations": 36,
              "date_range": "2022-01 to 2024-12",
              "mean_anomaly_c": 1.09
            },
            "forecast": {
              "horizon": 12,
              "dates": [
                "2025-01",
                "2025-02",
                "2025-03",
                "2025-04",
                "2025-05",
                "2025-06",
                "2025-07",
                "2025-08",
                "2025-09",
                "2025-10",
                "2025-11",
                "2025-12"
              ],
              "point": [
                1.2223773002624512,
                1.2563585042953491,
                1.286476969718933,
                1.240488052368164,
                1.2026376724243164,
                1.210019826889038,
                1.2253108024597168,
                1.2421810626983643,
                1.2697350978851318,
                1.2496668100357056,
                1.2135264873504639,
                1.203413963317871
              ],
              "quantiles": {
                "10%": [
                  1.1230626106262207,
                  1.1482248306274414,
                  1.1694774627685547,
                  1.1192981004714966,
                  1.0776877403259277,
                  1.0811384916305542,
                  1.0917632579803467,
                  1.1043028831481934,
                  1.123927354812622,
                  1.0962445735931396,
                  1.0545521974563599,
                  1.04123055934906
                ],
                "20%": [
                  1.161399245262146,
                  1.189164638519287,
                  1.2141375541687012,
                  1.168922781944275,
                  1.1280149221420288,
                  1.1352496147155762,
                  1.1474792957305908,
                  1.1598811149597168,
                  1.1872941255569458,
                  1.163041591644287,
                  1.1223580837249756,
                  1.1113250255584717
                ],
                "30%": [
                  1.183248519897461,
                  1.2134029865264893,
                  1.2430503368377686,
                  1.1957526206970215,
                  1.1554758548736572,
                  1.168745994567871,
                  1.17641282081604,
                  1.1929150819778442,
                  1.2162872552871704,
                  1.193880558013916,
                  1.1572108268737793,
                  1.1433370113372803
                ],
                "40%": [
                  1.2030284404754639,
                  1.2355916500091553,
                  1.2636719942092896,
                  1.2162381410598755,
                  1.1801763772964478,
                  1.1853768825531006,
                  1.2029411792755127,
                  1.216418743133545,
                  1.2429509162902832,
                  1.2217118740081787,
                  1.186462163925171,
                  1.1752326488494873
                ],
                "50%": [
                  1.2223773002624512,
                  1.2563585042953491,
                  1.286476969718933,
                  1.240488052368164,
                  1.2026376724243164,
                  1.210019826889038,
                  1.2253108024597168,
                  1.2421810626983643,
                  1.2697350978851318,
                  1.2496668100357056,
                  1.2135264873504639,
                  1.203413963317871
                ],
                "60%": [
                  1.2409887313842773,
                  1.2787915468215942,
                  1.3101407289505005,
                  1.2653589248657227,
                  1.2274072170257568,
                  1.2333427667617798,
                  1.2509558200836182,
                  1.266998052597046,
                  1.2976493835449219,
                  1.2757384777069092,
                  1.244727373123169,
                  1.2308053970336914
                ],
                "70%": [
                  1.2626991271972656,
                  1.3061506748199463,
                  1.3365124464035034,
                  1.288077473640442,
                  1.2546851634979248,
                  1.2559939622879028,
                  1.2764853239059448,
                  1.2924675941467285,
                  1.323679804801941,
                  1.30413818359375,
                  1.2706527709960938,
                  1.2621955871582031
                ],
                "80%": [
                  1.2927711009979248,
                  1.3358020782470703,
                  1.372582197189331,
                  1.3237736225128174,
                  1.2890899181365967,
                  1.2938039302825928,
                  1.3113452196121216,
                  1.3304922580718994,
                  1.358002781867981,
                  1.3376691341400146,
                  1.309138536453247,
                  1.2912495136260986
                ],
                "90%": [
                  1.3396028280258179,
                  1.3880281448364258,
                  1.4265702962875366,
                  1.3806402683258057,
                  1.3469674587249756,
                  1.3532464504241943,
                  1.3728348016738892,
                  1.394975185394287,
                  1.4252182245254517,
                  1.409815788269043,
                  1.3801677227020264,
                  1.3700778484344482
                ]
              }
            },
            "summary": {
              "forecast_mean_c": 1.235,
              "forecast_max_c": 1.286,
              "forecast_min_c": 1.203,
              "vs_last_year_mean": -0.017
            }
          }
        • forecast_visualization.png 156.9 KB · in bundle
        • interactive_forecast.html 149.5 KB · in bundle
      • generate_animation_data.py 5.1 KB
        #!/usr/bin/env python3
        """
        Generate animation data for interactive forecast visualization.
        
        This script runs TimesFM forecasts incrementally, starting with minimal data
        and adding one point at a time. Each forecast extends to the final date (2025-12).
        
        Output: animation_data.json with all forecast steps
        """
        
        from __future__ import annotations
        
        import json
        from pathlib import Path
        
        import numpy as np
        import pandas as pd
        import timesfm
        
        # Configuration
        MIN_CONTEXT = 12  # Minimum points to start forecasting
        MAX_HORIZON = (
            36  # Max forecast length (when we have 12 points, forecast 36 months to 2025-12)
        )
        TOTAL_MONTHS = 48  # Total months from 2022-01 to 2025-12 (graph extent)
        INPUT_FILE = Path(__file__).parent / "temperature_anomaly.csv"
        OUTPUT_FILE = Path(__file__).parent / "output" / "animation_data.json"
        
        
        def main() -> None:
            print("=" * 60)
            print("  TIMESFM ANIMATION DATA GENERATOR")
            print("  Dynamic horizon - forecasts always reach 2025-12")
            print("=" * 60)
        
            # Load data
            df = pd.read_csv(INPUT_FILE, parse_dates=["date"])
            df = df.sort_values("date").reset_index(drop=True)
        
            all_dates = df["date"].tolist()
            all_values = df["anomaly_c"].values.astype(np.float32)
        
            print(f"\n📊 Total data: {len(all_values)} months")
            print(
                f"   Date range: {all_dates[0].strftime('%Y-%m')} to {all_dates[-1].strftime('%Y-%m')}"
            )
            print(f"   Animation steps: {len(all_values) - MIN_CONTEXT + 1}")
        
            # Load TimesFM 2.5 once; compile for the longest horizon we will request
            print(f"\n🤖 Loading TimesFM 2.5 (200M) PyTorch (max horizon={MAX_HORIZON})...")
            model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
                "google/timesfm-2.5-200m-pytorch"
            )
            model.compile(
                timesfm.ForecastConfig(
                    max_context=64,
                    max_horizon=128,  # >= MAX_HORIZON; rounded to the 128-step output patch
                    normalize_inputs=True,
                    use_continuous_quantile_head=True,
                    fix_quantile_crossing=True,
                    infer_is_positive=False,  # anomalies can be negative
                )
            )
        
            # Generate forecasts for each step
            animation_steps = []
        
            for n_points in range(MIN_CONTEXT, len(all_values) + 1):
                step_num = n_points - MIN_CONTEXT + 1
                total_steps = len(all_values) - MIN_CONTEXT + 1
        
                # Calculate dynamic horizon: forecast enough to reach 2025-12
                horizon = TOTAL_MONTHS - n_points
        
                print(
                    f"\n📈 Step {step_num}/{total_steps}: Using {n_points} points, forecasting {horizon} months..."
                )
        
                # Get historical data up to this point
                historical_values = all_values[:n_points]
                historical_dates = all_dates[:n_points]
        
                # Run forecast for exactly the remaining horizon
                point, quantiles = model.forecast(horizon=horizon, inputs=[historical_values])
                point = point[0]
                quantiles = quantiles[0]  # (horizon, 10): 0 = mean, 1..9 = q10..q90
        
                # Determine forecast dates
                last_date = historical_dates[-1]
                forecast_dates = pd.date_range(
                    start=last_date + pd.DateOffset(months=1),
                    periods=horizon,
                    freq="MS",
                )
        
                # Store step data
                step_data = {
                    "step": step_num,
                    "n_points": n_points,
                    "horizon": horizon,
                    "last_historical_date": historical_dates[-1].strftime("%Y-%m"),
                    "historical_dates": [d.strftime("%Y-%m") for d in historical_dates],
                    "historical_values": historical_values.tolist(),
                    "forecast_dates": [d.strftime("%Y-%m") for d in forecast_dates],
                    "point_forecast": point.tolist(),
                    "q10": quantiles[:, 1].tolist(),
                    "q20": quantiles[:, 2].tolist(),
                    "q80": quantiles[:, 8].tolist(),
                    "q90": quantiles[:, 9].tolist(),
                }
        
                animation_steps.append(step_data)
        
                # Show summary
                print(f"   Last date: {historical_dates[-1].strftime('%Y-%m')}")
                print(f"   Forecast to: {forecast_dates[-1].strftime('%Y-%m')}")
                print(f"   Forecast mean: {point.mean():.3f}°C")
        
            # Create output
            output = {
                "metadata": {
                    "model": "TimesFM 2.5 (200M) PyTorch",
                    "total_steps": len(animation_steps),
                    "min_context": MIN_CONTEXT,
                    "max_horizon": MAX_HORIZON,
                    "total_months": TOTAL_MONTHS,
                    "data_source": "NOAA GISTEMP Global Temperature Anomaly",
                    "full_date_range": f"{all_dates[0].strftime('%Y-%m')} to {all_dates[-1].strftime('%Y-%m')}",
                },
                "actual_data": {
                    "dates": [d.strftime("%Y-%m") for d in all_dates],
                    "values": all_values.tolist(),
                },
                "animation_steps": animation_steps,
            }
        
            # Save
            with open(OUTPUT_FILE, "w") as f:
                json.dump(output, f, indent=2)
        
            print(f"\n" + "=" * 60)
            print("  ✅ ANIMATION DATA COMPLETE")
            print("=" * 60)
            print(f"\n📁 Output: {OUTPUT_FILE}")
            print(f"   Total steps: {len(animation_steps)}")
            print(f"   Each forecast extends to 2025-12")
        
        
        if __name__ == "__main__":
            main()
        
      • generate_gif.py 6.6 KB
        #!/usr/bin/env python3
        """
        Generate animated GIF showing forecast evolution.
        
        Creates a GIF animation showing how the TimesFM forecast changes
        as more historical data points are added. Shows the full actual data as a background layer.
        """
        from __future__ import annotations
        
        import json
        from pathlib import Path
        
        import matplotlib.pyplot as plt
        import matplotlib.dates as mdates
        import numpy as np
        import pandas as pd
        from PIL import Image
        
        # Configuration
        EXAMPLE_DIR = Path(__file__).parent
        DATA_FILE = EXAMPLE_DIR / "output" / "animation_data.json"
        OUTPUT_FILE = EXAMPLE_DIR / "output" / "forecast_animation.gif"
        DURATION_MS = 500  # Time per frame in milliseconds
        
        
        def create_frame(
            ax,
            step_data: dict,
            actual_data: dict,
            final_forecast: dict,
            total_steps: int,
            x_min,
            x_max,
            y_min,
            y_max,
        ) -> None:
            """Create a single frame of the animation with fixed axes."""
            ax.clear()
        
            # Parse dates
            historical_dates = pd.to_datetime(step_data["historical_dates"])
            forecast_dates = pd.to_datetime(step_data["forecast_dates"])
            
            # Get final forecast dates for full extent
            final_forecast_dates = pd.to_datetime(final_forecast["forecast_dates"])
            
            # All actual dates for full background
            all_actual_dates = pd.to_datetime(actual_data["dates"])
            all_actual_values = np.array(actual_data["values"])
        
            # ========== BACKGROUND LAYER: Full actual data (faded) ==========
            ax.plot(
                all_actual_dates,
                all_actual_values,
                color="#9ca3af",
                linewidth=1,
                marker="o",
                markersize=2,
                alpha=0.3,
                label="All observed data",
                zorder=1,
            )
            
            # ========== BACKGROUND LAYER: Final forecast (faded) ==========
            ax.plot(
                final_forecast_dates,
                final_forecast["point_forecast"],
                color="#fca5a5",
                linewidth=1,
                linestyle="--",
                marker="s",
                markersize=2,
                alpha=0.3,
                label="Final forecast",
                zorder=2,
            )
        
            # ========== FOREGROUND LAYER: Historical data used (bright) ==========
            ax.plot(
                historical_dates,
                step_data["historical_values"],
                color="#3b82f6",
                linewidth=2.5,
                marker="o",
                markersize=5,
                label="Data used",
                zorder=10,
            )
        
            # ========== FOREGROUND LAYER: Current forecast (bright) ==========
            # 80% prediction interval, q10-q90 (outer)
            ax.fill_between(
                forecast_dates,
                step_data["q10"],
                step_data["q90"],
                alpha=0.15,
                color="#ef4444",
                zorder=5,
            )
            
            # 60% prediction interval, q20-q80 (inner)
            ax.fill_between(
                forecast_dates,
                step_data["q20"],
                step_data["q80"],
                alpha=0.25,
                color="#ef4444",
                zorder=6,
            )
            
            # Forecast line
            ax.plot(
                forecast_dates,
                step_data["point_forecast"],
                color="#ef4444",
                linewidth=2.5,
                marker="s",
                markersize=5,
                label="Forecast",
                zorder=7,
            )
        
            # ========== Vertical line at forecast boundary ==========
            ax.axvline(
                x=historical_dates[-1],
                color="#6b7280",
                linestyle="--",
                linewidth=1.5,
                alpha=0.7,
                zorder=8,
            )
        
            # ========== Formatting ==========
            ax.set_xlabel("Date", fontsize=11)
            ax.set_ylabel("Temperature Anomaly (°C)", fontsize=11)
            ax.set_title(
                f"TimesFM Forecast Evolution\n"
                f"Step {step_data['step']}/{total_steps}: {step_data['n_points']} points → "
                f"forecast from {step_data['last_historical_date']}",
                fontsize=13,
                fontweight="bold",
            )
            
            ax.grid(True, alpha=0.3, zorder=0)
            ax.legend(loc="upper left", fontsize=8)
            
            # FIXED AXES - same for all frames
            ax.set_xlim(x_min, x_max)
            ax.set_ylim(y_min, y_max)
            
            # Format x-axis
            ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
            ax.xaxis.set_major_locator(mdates.MonthLocator(interval=4))
            plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha="right")
        
        
        def main() -> None:
            print("=" * 60)
            print("  GENERATING ANIMATED GIF")
            print("=" * 60)
            
            # Load data
            with open(DATA_FILE) as f:
                data = json.load(f)
            
            total_steps = len(data["animation_steps"])
            print(f"\n📊 Total frames: {total_steps}")
            
            # Get the final forecast step for reference
            final_forecast = data["animation_steps"][-1]
            
            # Calculate fixed axis extents from ALL data
            all_actual_dates = pd.to_datetime(data["actual_data"]["dates"])
            all_actual_values = np.array(data["actual_data"]["values"])
            
            final_forecast_dates = pd.to_datetime(final_forecast["forecast_dates"])
            final_forecast_values = np.array(final_forecast["point_forecast"])
            
            # X-axis: from first actual date to last forecast date
            x_min = all_actual_dates[0]
            x_max = final_forecast_dates[-1]
            
            # Y-axis: min/max across all actual + all forecasts with CIs
            all_forecast_q10 = np.array(final_forecast["q10"])
            all_forecast_q90 = np.array(final_forecast["q90"])
            
            all_values = np.concatenate([
                all_actual_values,
                final_forecast_values,
                all_forecast_q10,
                all_forecast_q90,
            ])
            y_min = all_values.min() - 0.05
            y_max = all_values.max() + 0.05
            
            print(f"   X-axis: {x_min.strftime('%Y-%m')} to {x_max.strftime('%Y-%m')}")
            print(f"   Y-axis: {y_min:.2f}°C to {y_max:.2f}°C")
            
            # Create figure
            fig, ax = plt.subplots(figsize=(12, 6))
            
            # Generate frames
            frames = []
            
            for i, step in enumerate(data["animation_steps"]):
                print(f"   Frame {i + 1}/{total_steps}...")
                
                create_frame(
                    ax,
                    step,
                    data["actual_data"],
                    final_forecast,
                    total_steps,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                )
                
                # Save frame to buffer
                fig.canvas.draw()
                
                # Convert to PIL Image
                buf = fig.canvas.buffer_rgba()
                width, height = fig.canvas.get_width_height()
                img = Image.frombytes("RGBA", (width, height), buf)
                frames.append(img.convert("RGB"))
            
            plt.close()
            
            # Save as GIF
            print(f"\n💾 Saving GIF: {OUTPUT_FILE}")
            frames[0].save(
                OUTPUT_FILE,
                save_all=True,
                append_images=frames[1:],
                duration=DURATION_MS,
                loop=0,  # Loop forever
            )
            
            # Get file size
            size_kb = OUTPUT_FILE.stat().st_size / 1024
            print(f"   File size: {size_kb:.1f} KB")
            print(f"\n✅ Done!")
        
        
        if __name__ == "__main__":
            main()
        
      • generate_html.py 20.7 KB
        #!/usr/bin/env python3
        """
        Generate a self-contained HTML file with embedded animation data.
        
        This creates a single HTML file that can be opened directly in any browser
        without needing a server or external JSON file (CORS-safe).
        """
        
        from __future__ import annotations
        
        import json
        from pathlib import Path
        
        EXAMPLE_DIR = Path(__file__).parent
        DATA_FILE = EXAMPLE_DIR / "output" / "animation_data.json"
        OUTPUT_FILE = EXAMPLE_DIR / "output" / "interactive_forecast.html"
        
        
        HTML_TEMPLATE = """<!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>TimesFM Interactive Forecast Animation</title>
            <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
            <style>
                * {{ margin: 0; padding: 0; box-sizing: border-box; }}
                
                body {{
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
                    min-height: 100vh;
                    color: #e0e0e0;
                    padding: 20px;
                }}
                
                .container {{ max-width: 1200px; margin: 0 auto; }}
                
                header {{ text-align: center; margin-bottom: 30px; }}
                
                h1 {{
                    font-size: 2rem;
                    margin-bottom: 10px;
                    background: linear-gradient(90deg, #60a5fa, #a78bfa);
                    -webkit-background-clip: text;
                    -webkit-text-fill-color: transparent;
                }}
                
                .subtitle {{ color: #9ca3af; font-size: 1.1rem; }}
                
                .chart-container {{
                    background: rgba(255, 255, 255, 0.05);
                    border-radius: 16px;
                    padding: 20px;
                    margin-bottom: 20px;
                    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
                }}
                
                #chart {{ width: 100% !important; height: 450px !important; }}
                
                .controls {{
                    display: flex;
                    flex-direction: column;
                    gap: 20px;
                    background: rgba(255, 255, 255, 0.05);
                    border-radius: 16px;
                    padding: 20px;
                }}
                
                .slider-container {{ display: flex; flex-direction: column; gap: 10px; }}
                
                .slider-label {{ display: flex; justify-content: space-between; align-items: center; }}
                .slider-label span {{ font-size: 0.9rem; color: #9ca3af; }}
                .slider-label .value {{ font-weight: 600; color: #60a5fa; font-size: 1.1rem; }}
                
                input[type="range"] {{
                    width: 100%; height: 8px; border-radius: 4px;
                    background: #374151; outline: none; -webkit-appearance: none;
                }}
                
                input[type="range"]::-webkit-slider-thumb {{
                    -webkit-appearance: none;
                    width: 24px; height: 24px; border-radius: 50%;
                    background: linear-gradient(135deg, #60a5fa, #a78bfa);
                    cursor: pointer;
                    box-shadow: 0 2px 10px rgba(96, 165, 250, 0.5);
                }}
                
                .buttons {{ display: flex; gap: 10px; flex-wrap: wrap; }}
                
                button {{
                    flex: 1; min-width: 100px;
                    padding: 12px 20px;
                    border: none; border-radius: 8px;
                    font-size: 1rem; font-weight: 600;
                    cursor: pointer; transition: all 0.2s ease;
                }}
                
                .btn-primary {{
                    background: linear-gradient(135deg, #60a5fa, #a78bfa);
                    color: white;
                }}
                .btn-primary:hover {{ transform: translateY(-2px); box-shadow: 0 4px 15px rgba(96, 165, 250, 0.4); }}
                
                .btn-secondary {{ background: #374151; color: #e0e0e0; }}
                .btn-secondary:hover {{ background: #4b5563; }}
                
                .stats {{
                    display: grid;
                    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
                    gap: 15px;
                    margin-top: 20px;
                }}
                
                .stat-card {{
                    background: rgba(255, 255, 255, 0.05);
                    border-radius: 12px;
                    padding: 15px;
                    text-align: center;
                }}
                .stat-card .label {{ font-size: 0.8rem; color: #9ca3af; margin-bottom: 5px; }}
                .stat-card .value {{ font-size: 1.3rem; font-weight: 600; color: #60a5fa; }}
                
                .legend {{
                    display: flex;
                    justify-content: center;
                    gap: 20px;
                    flex-wrap: wrap;
                    margin-top: 15px;
                    padding-top: 15px;
                    border-top: 1px solid rgba(255, 255, 255, 0.1);
                }}
                
                .legend-item {{ display: flex; align-items: center; gap: 8px; font-size: 0.85rem; }}
                .legend-color {{ width: 16px; height: 16px; border-radius: 4px; }}
                
                footer {{
                    text-align: center;
                    margin-top: 30px;
                    color: #6b7280;
                    font-size: 0.9rem;
                }}
                footer a {{ color: #60a5fa; text-decoration: none; }}
            </style>
        </head>
        <body>
            <div class="container">
                <header>
                    <h1>TimesFM Forecast Evolution</h1>
                    <p class="subtitle">Watch the forecast evolve as more data is added — forecasts extend to 2025-12</p>
                </header>
                
                <div class="chart-container">
                    <canvas id="chart"></canvas>
                </div>
                
                <div class="controls">
                    <div class="slider-container">
                        <div class="slider-label">
                            <span>Data Points Used</span>
                            <span class="value" id="points-value">12 / 36</span>
                        </div>
                        <input type="range" id="slider" min="0" max="24" value="0" step="1">
                        <div class="slider-label">
                            <span>2022-01</span>
                            <span id="date-end">Using data through 2022-12</span>
                        </div>
                    </div>
                    
                    <div class="buttons">
                        <button class="btn-primary" id="play-btn">▶ Play</button>
                        <button class="btn-secondary" id="reset-btn">↺ Reset</button>
                    </div>
                    
                    <div class="stats">
                        <div class="stat-card">
                            <div class="label">Forecast Mean</div>
                            <div class="value" id="stat-mean">0.86°C</div>
                        </div>
                        <div class="stat-card">
                            <div class="label">Forecast Horizon</div>
                            <div class="value" id="stat-horizon">36 months</div>
                        </div>
                        <div class="stat-card">
                            <div class="label">Forecast Max</div>
                            <div class="value" id="stat-max">--</div>
                        </div>
                        <div class="stat-card">
                            <div class="label">Forecast Min</div>
                            <div class="value" id="stat-min">--</div>
                        </div>
                    </div>
                    
                    <div class="legend">
                        <div class="legend-item">
                            <div class="legend-color" style="background: #9ca3af;"></div>
                            <span>All Observed Data</span>
                        </div>
                        <div class="legend-item">
                            <div class="legend-color" style="background: #fca5a5;"></div>
                            <span>Final Forecast (reference)</span>
                        </div>
                        <div class="legend-item">
                            <div class="legend-color" style="background: #3b82f6;"></div>
                            <span>Data Used</span>
                        </div>
                        <div class="legend-item">
                            <div class="legend-color" style="background: #ef4444;"></div>
                            <span>Current Forecast</span>
                        </div>
                        <div class="legend-item">
                            <div class="legend-color" style="background: rgba(239, 68, 68, 0.25);"></div>
                            <span>60% / 80% PI</span>
                        </div>
                    </div>
                </div>
                
                <footer>
                    <p>TimesFM 2.5 (200M) PyTorch • <a href="https://github.com/google-research/timesfm">Google Research</a></p>
                </footer>
            </div>
        
            <script>
                // Embedded animation data (no external fetch needed)
                const animationData = {data_json};
                
                let chart = null;
                let isPlaying = false;
                let playInterval = null;
                let currentStep = 0;
        
                // Fixed axis extents
                let allDates = [];
                let yMin = 0.7;
                let yMax = 1.55;
        
                function initChart() {{
                    const ctx = document.getElementById('chart').getContext('2d');
                    
                    // Calculate fixed extents
                    const finalStep = animationData.animation_steps[animationData.animation_steps.length - 1];
                    allDates = [
                        ...animationData.actual_data.dates,
                        ...finalStep.forecast_dates
                    ];
                    
                    // Y extent from all values
                    const allValues = [
                        ...animationData.actual_data.values,
                        ...finalStep.point_forecast,
                        ...finalStep.q10,
                        ...finalStep.q90
                    ];
                    yMin = Math.min(...allValues) - 0.05;
                    yMax = Math.max(...allValues) + 0.05;
                    
                    chart = new Chart(ctx, {{
                        type: 'line',
                        data: {{
                            labels: allDates,
                            datasets: [
                                {{
                                    label: 'All Observed',
                                    data: animationData.actual_data.values.map((v, i) => ({{x: animationData.actual_data.dates[i], y: v}})),
                                    borderColor: '#9ca3af',
                                    borderWidth: 1,
                                    pointRadius: 2,
                                    pointBackgroundColor: '#9ca3af',
                                    fill: false,
                                    tension: 0.1,
                                    order: 1,
                                }},
                                {{
                                    label: 'Final Forecast',
                                    data: [...Array(animationData.actual_data.dates.length).fill(null), ...finalStep.point_forecast],
                                    borderColor: '#fca5a5',
                                    borderWidth: 1,
                                    borderDash: [4, 4],
                                    pointRadius: 2,
                                    pointBackgroundColor: '#fca5a5',
                                    fill: false,
                                    tension: 0.1,
                                    order: 2,
                                }},
                                {{
                                    label: 'Data Used',
                                    data: [],
                                    borderColor: '#3b82f6',
                                    backgroundColor: 'rgba(59, 130, 246, 0.1)',
                                    borderWidth: 2.5,
                                    pointRadius: 4,
                                    pointBackgroundColor: '#3b82f6',
                                    fill: false,
                                    tension: 0.1,
                                    order: 10,
                                }},
                                {{
                                    label: '80% PI Lower (q10)',
                                    data: [],
                                    borderColor: 'transparent',
                                    backgroundColor: 'rgba(239, 68, 68, 0.08)',
                                    fill: '+1',
                                    pointRadius: 0,
                                    tension: 0.1,
                                    order: 5,
                                }},
                                {{
                                    label: '80% PI Upper (q90)',
                                    data: [],
                                    borderColor: 'transparent',
                                    backgroundColor: 'rgba(239, 68, 68, 0.08)',
                                    fill: false,
                                    pointRadius: 0,
                                    tension: 0.1,
                                    order: 5,
                                }},
                                {{
                                    label: '60% PI Lower (q20)',
                                    data: [],
                                    borderColor: 'transparent',
                                    backgroundColor: 'rgba(239, 68, 68, 0.2)',
                                    fill: '+1',
                                    pointRadius: 0,
                                    tension: 0.1,
                                    order: 6,
                                }},
                                {{
                                    label: '60% PI Upper (q80)',
                                    data: [],
                                    borderColor: 'transparent',
                                    backgroundColor: 'rgba(239, 68, 68, 0.2)',
                                    fill: false,
                                    pointRadius: 0,
                                    tension: 0.1,
                                    order: 6,
                                }},
                                {{
                                    label: 'Forecast',
                                    data: [],
                                    borderColor: '#ef4444',
                                    backgroundColor: 'rgba(239, 68, 68, 0.1)',
                                    borderWidth: 2.5,
                                    pointRadius: 4,
                                    pointBackgroundColor: '#ef4444',
                                    fill: false,
                                    tension: 0.1,
                                    order: 7,
                                }},
                            ]
                        }},
                        options: {{
                            responsive: true,
                            maintainAspectRatio: false,
                            interaction: {{ intersect: false, mode: 'index' }},
                            plugins: {{
                                legend: {{ display: false }},
                                tooltip: {{
                                    backgroundColor: 'rgba(0, 0, 0, 0.8)',
                                    titleColor: '#fff',
                                    bodyColor: '#fff',
                                    padding: 12,
                                }},
                            }},
                            scales: {{
                                x: {{
                                    grid: {{ color: 'rgba(255, 255, 255, 0.05)' }},
                                    ticks: {{ color: '#9ca3af', maxRotation: 45, minRotation: 45 }},
                                }},
                                y: {{
                                    grid: {{ color: 'rgba(255, 255, 255, 0.05)' }},
                                    ticks: {{
                                        color: '#9ca3af',
                                        callback: v => v.toFixed(2) + '°C'
                                    }},
                                    min: yMin,
                                    max: yMax,
                                }},
                            }},
                            animation: {{ duration: 150 }},
                        }},
                    }});
                }}
        
                function updateChart(stepIndex) {{
                    if (!animationData || !chart) return;
                    
                    const step = animationData.animation_steps[stepIndex];
                    const finalStep = animationData.animation_steps[animationData.animation_steps.length - 1];
                    const actual = animationData.actual_data;
                    
                    // Build data arrays for each dataset
                    const nHist = step.historical_dates.length;
                    const nForecast = step.forecast_dates.length;
                    const nActual = actual.dates.length;
                    const nFinalForecast = finalStep.forecast_dates.length;
                    const totalPoints = nActual + nFinalForecast;
                    
                    // Dataset 0: All observed (always full)
                    chart.data.datasets[0].data = actual.values.map((v, i) => ({{x: actual.dates[i], y: v}}));
                    
                    // Dataset 1: Final forecast reference (always full)
                    chart.data.datasets[1].data = [
                        ...Array(nActual).fill(null),
                        ...finalStep.point_forecast
                    ];
                    
                    // Dataset 2: Data used (historical only)
                    const dataUsed = [];
                    for (let i = 0; i < totalPoints; i++) {{
                        if (i < nHist) {{
                            dataUsed.push(step.historical_values[i]);
                        }} else {{
                            dataUsed.push(null);
                        }}
                    }}
                    chart.data.datasets[2].data = dataUsed;
                    
                    // Datasets 3-6: CIs (forecast only)
                    const forecastOffset = nActual;
                    const q90Lower = [];
                    const q90Upper = [];
                    const q80Lower = [];
                    const q80Upper = [];
                    
                    for (let i = 0; i < totalPoints; i++) {{
                        const forecastIdx = i - forecastOffset;
                        if (forecastIdx >= 0 && forecastIdx < nForecast) {{
                            q90Lower.push(step.q10[forecastIdx]);
                            q90Upper.push(step.q90[forecastIdx]);
                            q80Lower.push(step.q20[forecastIdx]);
                            q80Upper.push(step.q80[forecastIdx]);
                        }} else {{
                            q90Lower.push(null);
                            q90Upper.push(null);
                            q80Lower.push(null);
                            q80Upper.push(null);
                        }}
                    }}
                    chart.data.datasets[3].data = q90Lower;
                    chart.data.datasets[4].data = q90Upper;
                    chart.data.datasets[5].data = q80Lower;
                    chart.data.datasets[6].data = q80Upper;
                    
                    // Dataset 7: Forecast line
                    const forecastData = [];
                    for (let i = 0; i < totalPoints; i++) {{
                        const forecastIdx = i - forecastOffset;
                        if (forecastIdx >= 0 && forecastIdx < nForecast) {{
                            forecastData.push(step.point_forecast[forecastIdx]);
                        }} else {{
                            forecastData.push(null);
                        }}
                    }}
                    chart.data.datasets[7].data = forecastData;
                    
                    chart.update('none');
                    
                    // Update UI
                    document.getElementById('slider').value = stepIndex;
                    document.getElementById('points-value').textContent = `${{step.n_points}} / 36`;
                    document.getElementById('date-end').textContent = `Using data through ${{step.last_historical_date}}`;
                    
                    // Stats
                    const mean = (step.point_forecast.reduce((a, b) => a + b, 0) / step.point_forecast.length).toFixed(3);
                    const max = Math.max(...step.point_forecast).toFixed(3);
                    const min = Math.min(...step.point_forecast).toFixed(3);
                    
                    document.getElementById('stat-mean').textContent = mean + '°C';
                    document.getElementById('stat-horizon').textContent = step.horizon + ' months';
                    document.getElementById('stat-max').textContent = max + '°C';
                    document.getElementById('stat-min').textContent = min + '°C';
                    
                    currentStep = stepIndex;
                }}
        
                document.getElementById('slider').addEventListener('input', e => {{
                    updateChart(parseInt(e.target.value));
                }});
        
                document.getElementById('play-btn').addEventListener('click', () => {{
                    const btn = document.getElementById('play-btn');
                    if (isPlaying) {{
                        clearInterval(playInterval);
                        btn.textContent = '▶ Play';
                        isPlaying = false;
                    }} else {{
                        btn.textContent = '⏸ Pause';
                        isPlaying = true;
                        if (currentStep >= animationData.animation_steps.length - 1) currentStep = 0;
                        playInterval = setInterval(() => {{
                            if (currentStep >= animationData.animation_steps.length - 1) {{
                                clearInterval(playInterval);
                                document.getElementById('play-btn').textContent = '▶ Play';
                                isPlaying = false;
                            }} else {{
                                currentStep++;
                                updateChart(currentStep);
                            }}
                        }}, 400);
                    }}
                }});
        
                document.getElementById('reset-btn').addEventListener('click', () => {{
                    if (isPlaying) {{
                        clearInterval(playInterval);
                        document.getElementById('play-btn').textContent = '▶ Play';
                        isPlaying = false;
                    }}
                    updateChart(0);
                }});
        
                // Initialize on load
                initChart();
                updateChart(0);
            </script>
        </body>
        </html>
        """
        
        
        def main() -> None:
            print("=" * 60)
            print("  GENERATING SELF-CONTAINED HTML")
            print("=" * 60)
        
            # Load animation data
            with open(DATA_FILE) as f:
                data = json.load(f)
        
            # Generate HTML with embedded data
            html_content = HTML_TEMPLATE.format(data_json=json.dumps(data, indent=2))
        
            # Write output
            with open(OUTPUT_FILE, "w") as f:
                f.write(html_content)
        
            size_kb = OUTPUT_FILE.stat().st_size / 1024
            print(f"\n✅ Generated: {OUTPUT_FILE}")
            print(f"   File size: {size_kb:.1f} KB")
            print(f"   Fully self-contained — no external dependencies")
        
        
        if __name__ == "__main__":
            main()
        
      • README.md 5.8 KB
        # TimesFM Forecast Report: Global Temperature Anomaly (2025)
        
        **Model:** TimesFM 2.5 (200M) PyTorch (`google/timesfm-2.5-200m-pytorch`, timesfm 3.0.2)  
        **Generated:** 2026-09-23  
        **Source:** NOAA GISTEMP Global Land-Ocean Temperature Index
        
        ---
        
        ## Executive Summary
        
        TimesFM forecasts a mean temperature anomaly of **1.24°C** for 2025, slightly below the 2024 average of 1.25°C. The model predicts continued elevated temperatures with a peak of 1.29°C in March 2025 and a minimum of 1.20°C in May and December 2025.
        
        ---
        
        ## Input Data
        
        ### Historical Temperature Anomalies (2022-2024)
        
        | Date | Anomaly (°C) | Date | Anomaly (°C) | Date | Anomaly (°C) |
        |------|-------------|------|-------------|------|-------------|
        | 2022-01 | 0.89 | 2023-01 | 0.87 | 2024-01 | 1.22 |
        | 2022-02 | 0.89 | 2023-02 | 0.98 | 2024-02 | 1.35 |
        | 2022-03 | 1.02 | 2023-03 | 1.21 | 2024-03 | 1.34 |
        | 2022-04 | 0.88 | 2023-04 | 1.00 | 2024-04 | 1.26 |
        | 2022-05 | 0.85 | 2023-05 | 0.94 | 2024-05 | 1.15 |
        | 2022-06 | 0.88 | 2023-06 | 1.08 | 2024-06 | 1.20 |
        | 2022-07 | 0.88 | 2023-07 | 1.18 | 2024-07 | 1.24 |
        | 2022-08 | 0.90 | 2023-08 | 1.24 | 2024-08 | 1.30 |
        | 2022-09 | 0.88 | 2023-09 | 1.47 | 2024-09 | 1.28 |
        | 2022-10 | 0.95 | 2023-10 | 1.32 | 2024-10 | 1.27 |
        | 2022-11 | 0.77 | 2023-11 | 1.18 | 2024-11 | 1.22 |
        | 2022-12 | 0.78 | 2023-12 | 1.16 | 2024-12 | 1.20 |
        
        **Statistics:**
        - Total observations: 36 months
        - Mean anomaly: 1.09°C
        - Trend (2022→2024): +0.37°C
        
        ---
        
        ## Raw Forecast Output
        
        ### Point Forecast and Prediction Intervals
        
        The 60% interval is q20–q80 and the 80% interval is q10–q90 (TimesFM 2.5 quantile columns
        2/8 and 1/9; column 0 is the mean).
        
        | Month | Point | 60% PI (q20–q80) | 80% PI (q10–q90) |
        |-------|-------|------------------|------------------|
        | 2025-01 | 1.222 | [1.161, 1.293] | [1.123, 1.340] |
        | 2025-02 | 1.256 | [1.189, 1.336] | [1.148, 1.388] |
        | 2025-03 | 1.286 | [1.214, 1.373] | [1.169, 1.427] |
        | 2025-04 | 1.240 | [1.169, 1.324] | [1.119, 1.381] |
        | 2025-05 | 1.203 | [1.128, 1.289] | [1.078, 1.347] |
        | 2025-06 | 1.210 | [1.135, 1.294] | [1.081, 1.353] |
        | 2025-07 | 1.225 | [1.147, 1.311] | [1.092, 1.373] |
        | 2025-08 | 1.242 | [1.160, 1.330] | [1.104, 1.395] |
        | 2025-09 | 1.270 | [1.187, 1.358] | [1.124, 1.425] |
        | 2025-10 | 1.250 | [1.163, 1.338] | [1.096, 1.410] |
        | 2025-11 | 1.214 | [1.122, 1.309] | [1.055, 1.380] |
        | 2025-12 | 1.203 | [1.111, 1.291] | [1.041, 1.370] |
        
        ### JSON Output
        
        ```json
        {
          "model": "TimesFM 2.5 (200M) PyTorch",
          "input": {
            "source": "NOAA GISTEMP Global Temperature Anomaly",
            "n_observations": 36,
            "date_range": "2022-01 to 2024-12",
            "mean_anomaly_c": 1.09
          },
          "forecast": {
            "horizon": 12,
            "dates": ["2025-01", "2025-02", "2025-03", "2025-04", "2025-05", "2025-06",
                      "2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12"],
            "point": [1.222, 1.256, 1.286, 1.240, 1.203, 1.210, 1.225, 1.242, 1.270, 1.250, 1.214, 1.203]
          },
          "summary": {
            "forecast_mean_c": 1.235,
            "forecast_max_c": 1.286,
            "forecast_min_c": 1.203,
            "vs_last_year_mean": -0.017
          }
        }
        ```
        
        (The full file, `output/forecast_output.json`, also contains the q10–q90 quantile arrays.)
        
        ---
        
        ## Visualization
        
        ![Temperature Anomaly Forecast](output/forecast_visualization.png)
        
        ---
        
        ## Findings
        
        ### Key Observations
        
        1. **Level roughly flat**: The mean 2025 forecast (1.24°C) is 0.02°C below the 2024 average (1.25°C), i.e. the model extrapolates the recent plateau rather than a continued rise.
        
        2. **Intra-year variation**: The forecast is highest in late winter (March, 1.29°C) with a secondary rise in September (1.27°C), and lowest in May and December (1.20°C).
        
        3. **Widening uncertainty**: The 80% prediction interval widens from ±0.11°C in January to ±0.16°C in December, reflecting typical forecast uncertainty growth over the horizon.
        
        4. **Peak temperature**: March 2025 is the forecast maximum at 1.29°C, below the September 2023 record of 1.47°C.
        
        ### Limitations
        
        - TimesFM is a zero-shot forecaster without physical climate model constraints
        - The 36-month training window may not capture multi-decadal climate trends
        - El Niño/La Niña cycles are not explicitly modeled
        
        ### Recommendations
        
        - Use this forecast as a baseline comparison for physics-based climate models
        - Update forecast quarterly as new observations become available
        - Consider ensemble approaches combining TimesFM with other methods
        
        ---
        
        ## Reproducibility
        
        ### Files
        
        | File | Description |
        |------|-------------|
        | `temperature_anomaly.csv` | Input data (36 months) |
        | `forecast_output.csv` | Point forecast with quantiles |
        | `forecast_output.json` | Machine-readable forecast |
        | `forecast_visualization.png` | Fan chart visualization |
        | `run_forecast.py` | Forecasting script |
        | `visualize_forecast.py` | Visualization script |
        | `run_example.sh` | One-click runner |
        
        ### How to Reproduce
        
        ```bash
        # Install dependencies
        uv pip install "timesfm[torch]" matplotlib pandas numpy
        
        # Run the complete example
        cd skills/data-science/alterlab-timesfm/examples/global-temperature
        ./run_example.sh
        ```
        
        ---
        
        ## Technical Notes
        
        ### API Used
        
        `run_forecast.py` uses the TimesFM 2.5 API from `timesfm>=2.0`:
        
        ```python
        import timesfm
        
        model = timesfm.TimesFM_2p5_200M_torch.from_pretrained("google/timesfm-2.5-200m-pytorch")
        model.compile(timesfm.ForecastConfig(
            max_context=64, max_horizon=128, normalize_inputs=True,
            use_continuous_quantile_head=True, fix_quantile_crossing=True,
            infer_is_positive=False,  # anomalies can be negative
        ))
        point, quantiles = model.forecast(horizon=12, inputs=[series])
        ```
        
        The older `TimesFmHparams` / `TimesFmCheckpoint` / `TimesFm(...)` API with `freq=[0]`
        (used by an earlier version of this example with the TimesFM 1.0 checkpoint) exists only in
        `timesfm==1.3.0`.
        
        ---
        
        *Report generated with the alterlab-timesfm skill (TimesFM 2.5, timesfm 3.0.2).*
        
      • run_example.sh 1.5 KB
        #!/bin/bash
        # run_example.sh - Run the TimesFM temperature anomaly forecasting example
        #
        # This script:
        # 1. Runs the preflight system check
        # 2. Runs the TimesFM forecast
        # 3. Generates the visualization
        #
        # Usage:
        #   ./run_example.sh
        #
        # Prerequisites:
        #   - Python 3.10+
        #   - timesfm[torch] installed: uv pip install "timesfm[torch]"
        #   - matplotlib, pandas, numpy
        
        set -e
        
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
        
        echo "============================================================"
        echo "  TimesFM Example: Global Temperature Anomaly Forecast"
        echo "============================================================"
        
        # Step 1: Preflight check
        echo ""
        echo "🔍 Step 1: Running preflight system check..."
        python3 "$SKILL_ROOT/scripts/check_system.py" || {
            echo "❌ Preflight check failed. Please fix the issues above before continuing."
            exit 1
        }
        
        # Step 2: Run forecast
        echo ""
        echo "📊 Step 2: Running TimesFM forecast..."
        cd "$SCRIPT_DIR"
        python3 run_forecast.py
        
        # Step 3: Generate visualization
        echo ""
        echo "📈 Step 3: Generating visualization..."
        python3 visualize_forecast.py
        
        echo ""
        echo "============================================================"
        echo "  ✅ Example complete!"
        echo "============================================================"
        echo ""
        echo "Output files:"
        echo "  - $SCRIPT_DIR/output/forecast_output.csv"
        echo "  - $SCRIPT_DIR/output/forecast_output.json"
        echo "  - $SCRIPT_DIR/output/forecast_visualization.png"
        
      • run_forecast.py 5.3 KB
        #!/usr/bin/env python3
        """
        Run TimesFM forecast on global temperature anomaly data.
        Generates forecast output CSV and JSON for the example.
        """
        
        from __future__ import annotations
        
        import json
        from pathlib import Path
        
        import numpy as np
        import pandas as pd
        
        # Preflight check
        print("=" * 60)
        print("  TIMeSFM FORECAST - Global Temperature Anomaly Example")
        print("=" * 60)
        
        # Load data
        data_path = Path(__file__).parent / "temperature_anomaly.csv"
        df = pd.read_csv(data_path, parse_dates=["date"])
        df = df.sort_values("date").reset_index(drop=True)
        
        print(f"\n📊 Input Data: {len(df)} months of temperature anomalies")
        print(
            f"   Date range: {df['date'].min().strftime('%Y-%m')} to {df['date'].max().strftime('%Y-%m')}"
        )
        print(f"   Mean anomaly: {df['anomaly_c'].mean():.2f}°C")
        print(
            f"   Trend: {df['anomaly_c'].iloc[-12:].mean() - df['anomaly_c'].iloc[:12].mean():.2f}°C change (first to last year)"
        )
        
        # Prepare input for TimesFM
        # TimesFM expects a list of 1D numpy arrays
        input_series = df["anomaly_c"].values.astype(np.float32)
        
        # Load TimesFM 2.5 (PyTorch) — requires timesfm>=2.0 (`uv pip install "timesfm[torch]"`)
        print("\n🤖 Loading TimesFM 2.5 (200M) PyTorch...")
        import timesfm
        
        model = timesfm.TimesFM_2p5_200M_torch.from_pretrained("google/timesfm-2.5-200m-pytorch")
        model.compile(
            timesfm.ForecastConfig(
                max_context=64,  # 36 monthly points; compile() rounds to the 32-step patch size
                max_horizon=128,  # rounded to the 128-step output patch
                normalize_inputs=True,
                use_continuous_quantile_head=True,
                fix_quantile_crossing=True,
                infer_is_positive=False,  # anomalies can be negative; do not clamp at zero
            )
        )
        
        # Forecast
        print("\n📈 Running forecast (12 months ahead)...")
        point_forecast, quantile_forecast = model.forecast(horizon=12, inputs=[input_series])
        
        print(f"   Point forecast shape: {point_forecast.shape}")
        print(f"   Quantile forecast shape: {quantile_forecast.shape}")
        
        # Extract results
        point = point_forecast[0]  # Shape: (horizon,) — the median
        quantiles = quantile_forecast[0]  # Shape: (horizon, 10)
        
        # TimesFM 2.5 quantile columns: 0 = mean, 1..9 = q10..q90 (5 = median = point forecast)
        quantile_labels = ["10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%"]
        
        # Create forecast dates (2025 monthly)
        last_date = df["date"].max()
        forecast_dates = pd.date_range(
            start=last_date + pd.DateOffset(months=1), periods=12, freq="MS"
        )
        
        # Build output DataFrame
        output_df = pd.DataFrame(
            {
                "date": forecast_dates.strftime("%Y-%m-%d"),
                "point_forecast": point,
                "mean": quantiles[:, 0],
                "q10": quantiles[:, 1],
                "q20": quantiles[:, 2],
                "q30": quantiles[:, 3],
                "q40": quantiles[:, 4],
                "q50": quantiles[:, 5],  # Median (= point forecast)
                "q60": quantiles[:, 6],
                "q70": quantiles[:, 7],
                "q80": quantiles[:, 8],
                "q90": quantiles[:, 9],
            }
        )
        
        # Save outputs
        output_dir = Path(__file__).parent / "output"
        output_dir.mkdir(exist_ok=True)
        output_df.to_csv(output_dir / "forecast_output.csv", index=False)
        
        # JSON output for the report
        output_json = {
            "model": "TimesFM 2.5 (200M) PyTorch",
            "input": {
                "source": "NOAA GISTEMP Global Temperature Anomaly",
                "n_observations": len(df),
                "date_range": f"{df['date'].min().strftime('%Y-%m')} to {df['date'].max().strftime('%Y-%m')}",
                "mean_anomaly_c": round(df["anomaly_c"].mean(), 3),
            },
            "forecast": {
                "horizon": 12,
                "dates": forecast_dates.strftime("%Y-%m").tolist(),
                "point": point.tolist(),
                "quantiles": {
                    label: quantiles[:, i + 1].tolist() for i, label in enumerate(quantile_labels)
                },
            },
            "summary": {
                "forecast_mean_c": round(float(point.mean()), 3),
                "forecast_max_c": round(float(point.max()), 3),
                "forecast_min_c": round(float(point.min()), 3),
                "vs_last_year_mean": round(
                    float(point.mean() - df["anomaly_c"].iloc[-12:].mean()), 3
                ),
            },
        }
        
        with open(output_dir / "forecast_output.json", "w") as f:
            json.dump(output_json, f, indent=2)
        
        # Print summary
        print("\n" + "=" * 60)
        print("  FORECAST RESULTS")
        print("=" * 60)
        print(
            f"\n📅 Forecast period: {forecast_dates[0].strftime('%Y-%m')} to {forecast_dates[-1].strftime('%Y-%m')}"
        )
        print(f"\n🌡️  Temperature Anomaly Forecast (°C above 1951-1980 baseline):")
        print(f"\n   {'Month':<10} {'Point':>8} {'60% PI':>18} {'80% PI':>18}")
        print(f"   {'-' * 10} {'-' * 8} {'-' * 18} {'-' * 18}")
        for date, pt, q20, q80, q10, q90 in zip(
            forecast_dates.strftime("%Y-%m"),
            point,
            quantiles[:, 2],  # q20
            quantiles[:, 8],  # q80
            quantiles[:, 1],  # q10
            quantiles[:, 9],  # q90
        ):
            print(
                f"   {date:<10} {pt:>8.3f} [{q20:>6.3f}, {q80:>6.3f}] [{q10:>6.3f}, {q90:>6.3f}]"
            )
        
        print(f"\n📊 Summary Statistics:")
        print(f"   Mean forecast:  {point.mean():.3f}°C")
        print(
            f"   Max forecast:   {point.max():.3f}°C (Month: {forecast_dates[point.argmax()].strftime('%Y-%m')})"
        )
        print(
            f"   Min forecast:   {point.min():.3f}°C (Month: {forecast_dates[point.argmin()].strftime('%Y-%m')})"
        )
        print(f"   vs 2024 mean:   {point.mean() - df['anomaly_c'].iloc[-12:].mean():+.3f}°C")
        
        print(f"\n✅ Output saved to:")
        print(f"   {output_dir / 'forecast_output.csv'}")
        print(f"   {output_dir / 'forecast_output.json'}")
        
      • temperature_anomaly.csv 591 B · in bundle
      • visualize_forecast.py 3.3 KB
        #!/usr/bin/env python3
        """
        Visualize TimesFM forecast results for global temperature anomaly.
        
        Generates a publication-quality figure showing:
        - Historical data (2022-2024)
        - Point forecast (2025)
        - 80% and 90% confidence intervals (fan chart)
        
        Usage:
            python visualize_forecast.py
        """
        
        from __future__ import annotations
        
        import json
        from pathlib import Path
        
        import matplotlib.pyplot as plt
        import numpy as np
        import pandas as pd
        
        # Configuration
        EXAMPLE_DIR = Path(__file__).parent
        INPUT_FILE = EXAMPLE_DIR / "temperature_anomaly.csv"
        FORECAST_FILE = EXAMPLE_DIR / "output" / "forecast_output.json"
        OUTPUT_FILE = EXAMPLE_DIR / "output" / "forecast_visualization.png"
        
        
        def main() -> None:
            # Load historical data
            df = pd.read_csv(INPUT_FILE, parse_dates=["date"])
        
            # Load forecast results
            with open(FORECAST_FILE) as f:
                forecast = json.load(f)
        
            # Extract forecast data
            dates = pd.to_datetime(forecast["forecast"]["dates"])
            point = np.array(forecast["forecast"]["point"])
            q10 = np.array(forecast["forecast"]["quantiles"]["10%"])
            q20 = np.array(forecast["forecast"]["quantiles"]["20%"])
            q80 = np.array(forecast["forecast"]["quantiles"]["80%"])
            q90 = np.array(forecast["forecast"]["quantiles"]["90%"])
        
            # Create figure
            fig, ax = plt.subplots(figsize=(12, 6))
        
            # Plot historical data
            ax.plot(
                df["date"],
                df["anomaly_c"],
                color="#2563eb",
                linewidth=1.5,
                marker="o",
                markersize=3,
                label="Historical (NOAA GISTEMP)",
            )
        
            # Plot 80% prediction interval (q10-q90, outer band)
            ax.fill_between(dates, q10, q90, alpha=0.2, color="#dc2626", label="80% PI (q10-q90)")
        
            # Plot 60% prediction interval (q20-q80, inner band)
            ax.fill_between(dates, q20, q80, alpha=0.3, color="#dc2626", label="60% PI (q20-q80)")
        
            # Plot point forecast
            ax.plot(
                dates,
                point,
                color="#dc2626",
                linewidth=2,
                marker="s",
                markersize=4,
                label="TimesFM Forecast",
            )
        
            # Add vertical line at forecast boundary
            ax.axvline(
                x=df["date"].max(), color="#6b7280", linestyle="--", linewidth=1, alpha=0.7
            )
        
            # Formatting
            ax.set_xlabel("Date", fontsize=12)
            ax.set_ylabel("Temperature Anomaly (°C)", fontsize=12)
            ax.set_title(
                "TimesFM Zero-Shot Forecast Example\n36-month Temperature Anomaly → 12-month Forecast",
                fontsize=14,
                fontweight="bold",
            )
        
            # Add annotations
            ax.annotate(
                f"Mean forecast: {forecast['summary']['forecast_mean_c']:.2f}°C\n"
                f"vs 2024: {forecast['summary']['vs_last_year_mean']:+.2f}°C",
                xy=(dates[6], point[6]),
                xytext=(dates[6], point[6] + 0.15),
                fontsize=10,
                arrowprops=dict(arrowstyle="->", color="#6b7280", lw=1),
                bbox=dict(boxstyle="round,pad=0.3", facecolor="white", edgecolor="#6b7280"),
            )
        
            # Grid and legend
            ax.grid(True, alpha=0.3)
            ax.legend(loc="upper left", fontsize=10)
        
            # Set y-axis limits
            ax.set_ylim(0.7, 1.5)
        
            # Rotate x-axis labels
            plt.xticks(rotation=45, ha="right")
        
            # Tight layout
            plt.tight_layout()
        
            # Save
            fig.savefig(OUTPUT_FILE, dpi=150, bbox_inches="tight")
            print(f"✅ Saved visualization to: {OUTPUT_FILE}")
        
            plt.close()
        
        
        if __name__ == "__main__":
            main()
        
  • references
    • api_reference.md 12.2 KB
      # TimesFM API Reference
      
      The `timesfm` package (>= 2.0 on PyPI; current 3.0.2 as of 2026-09) ships two APIs:
      TimesFM 2.5 (`timesfm.TimesFM_2p5_200M_torch`, below) and TimesFM 3.0 (`timesfm3`, see
      "TimesFM 3.0 API" at the end). The 1.x/2.0 `TimesFmHparams` / `TimesFmCheckpoint` /
      `TimesFm(...)` API and the `freq=` argument exist only in `timesfm==1.3.0`.
      
      ## Model Classes
      
      ### `timesfm.TimesFM_2p5_200M_torch`
      
      The primary model class for TimesFM 2.5 (200M parameters, PyTorch backend).
      
      #### `from_pretrained()`
      
      ```python
      model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
          "google/timesfm-2.5-200m-pytorch",
          cache_dir=None,          # Optional: custom cache directory
          force_download=False,    # True re-downloads the ~0.9 GB weights even if cached
      )
      ```
      
      | Parameter | Type | Default | Description |
      | --------- | ---- | ------- | ----------- |
      | `model_id` | str | `"google/timesfm-2.5-200m-pytorch"` | Hugging Face model ID |
      | `revision` | str \| None | None | Specific model revision |
      | `cache_dir` | str \| Path \| None | None | Custom cache directory |
      | `force_download` | bool | False | Force re-download of weights |
      
      **Returns**: Initialized `TimesFM_2p5_200M_torch` instance (not yet compiled).
      
      #### `compile()`
      
      Compiles the model with the given forecast configuration. **Must be called before `forecast()`.**
      
      ```python
      model.compile(
          timesfm.ForecastConfig(
              max_context=1024,
              max_horizon=256,
              normalize_inputs=True,
              per_core_batch_size=32,
              use_continuous_quantile_head=True,
              force_flip_invariance=True,
              infer_is_positive=True,
              fix_quantile_crossing=True,
          )
      )
      ```
      
      `compile()` rounds `max_context` up to a multiple of the 32-step input patch and `max_horizon`
      up to a multiple of the 128-step output patch. It raises `ValueError` if
      `max_context + max_horizon > 16384`, or if `use_continuous_quantile_head=True` with
      `max_horizon > 1024`. `forecast()` raises `RuntimeError` if the model is not compiled and
      `ValueError` if `horizon > max_horizon`.
      
      #### `forecast()`
      
      Run inference on one or more time series.
      
      ```python
      point_forecast, quantile_forecast = model.forecast(
          horizon=24,
          inputs=[array1, array2, ...],
      )
      ```
      
      | Parameter | Type | Description |
      | --------- | ---- | ----------- |
      | `horizon` | int | Number of future steps to forecast |
      | `inputs` | list[np.ndarray] | List of 1-D numpy arrays (each is a time series) |
      
      **Returns**: `tuple[np.ndarray, np.ndarray]`
      
      - `point_forecast`: shape `(batch_size, horizon)` — median (0.5 quantile)
      - `quantile_forecast`: shape `(batch_size, horizon, 10)` — [mean, q10, q20, ..., q90]
      
      **Raises**: `RuntimeError` if model is not compiled.
      
      **Key behaviors**:
      
      - Leading NaN values are stripped automatically
      - Internal NaN values are linearly interpolated
      - Series longer than `max_context` are truncated (last `max_context` points used)
      - Series shorter than `max_context` are padded
      
      #### `forecast_with_covariates()`
      
      Run inference with exogenous variables (requires `timesfm[xreg]`, which pulls in JAX and
      scikit-learn). The model must be compiled with `return_backcast=True`, otherwise the call
      raises `ValueError`.
      
      ```python
      model.compile(timesfm.ForecastConfig(
          max_context=512, max_horizon=128, normalize_inputs=True,
          use_continuous_quantile_head=True, fix_quantile_crossing=True,
          return_backcast=True,  # required for forecast_with_covariates
      ))
      
      point, quantiles = model.forecast_with_covariates(
          inputs=inputs,
          dynamic_numerical_covariates={"temp": [temp_array1, temp_array2]},
          dynamic_categorical_covariates={"dow": [dow_array1, dow_array2]},
          static_categorical_covariates={"region": ["east", "west"]},
          xreg_mode="xreg + timesfm",
      )
      # point / quantiles are LISTS with one array per series: (horizon,) and (horizon, 10)
      ```
      
      | Parameter | Type | Description |
      | --------- | ---- | ----------- |
      | `inputs` | list[np.ndarray] | Target time series |
      | `dynamic_numerical_covariates` | dict[str, list[np.ndarray]] | Time-varying numeric features |
      | `dynamic_categorical_covariates` | dict[str, list[np.ndarray]] | Time-varying categorical features |
      | `static_categorical_covariates` | dict[str, list[str]] | Fixed categorical features per series |
      | `static_numerical_covariates` | dict[str, list[float]] | Fixed numeric features per series |
      | `xreg_mode` | str | `"xreg + timesfm"` (default): fit a linear model on the target, TimesFM forecasts its residuals. `"timesfm + xreg"`: TimesFM forecasts first, a linear model fits its residuals |
      | `ridge` | float | Ridge penalty for the in-context linear model (default 0.0) |
      
      **Note**: Dynamic covariates must have length `context + horizon` for each series; the
      forecast horizon is inferred from them and must not exceed `max_horizon`.
      
      ---
      
      ## `timesfm.ForecastConfig`
      
      Immutable dataclass controlling all forecast behavior.
      
      ```python
      @dataclasses.dataclass(frozen=True)
      class ForecastConfig:
          max_context: int = 0
          max_horizon: int = 0
          normalize_inputs: bool = False
          window_size: int = 0            # reserved for decomposed forecasting (not yet implemented)
          per_core_batch_size: int = 1
          use_continuous_quantile_head: bool = False
          force_flip_invariance: bool = True
          infer_is_positive: bool = True
          fix_quantile_crossing: bool = False
          return_backcast: bool = False
      ```
      
      The quantile levels (0.1–0.9) and the median index (5) are fixed by the TimesFM 2.5 model
      definition, not by `ForecastConfig`.
      
      ### Parameter Details
      
      #### `max_context` (int, default=0)
      
      Maximum number of historical time points to use as context.
      
      - **N**: Truncate series to the last N points (rounded up to a multiple of 32); shorter series are left-padded and masked
      - **Limit**: `max_context + max_horizon` must be ≤ 16,384 for v2.5
      - **Best practice**: Set it explicitly to cover your longest series (or 512–2048 for speed); do not rely on the default of 0
      
      #### `max_horizon` (int, default=0)
      
      Maximum forecast horizon.
      
      - **N**: Forecasts up to N steps (rounded up to a multiple of 128; call `forecast(horizon=M)` with M ≤ N)
      - **Best practice**: Set it explicitly to your expected maximum forecast length
      
      #### `normalize_inputs` (bool, default=False)
      
      Whether to z-normalize each series before feeding to the model.
      
      - **True** (RECOMMENDED): Normalizes each series to zero mean, unit variance
      - **False**: Raw values are passed directly
      - **When False is OK**: Only if your series are already normalized or very close to scale 1.0
      
      #### `per_core_batch_size` (int, default=1)
      
      Number of series processed per device in each batch.
      
      - Increase for throughput, decrease if OOM
      - See `references/system_requirements.md` for recommended values by hardware
      
      #### `use_continuous_quantile_head` (bool, default=False)
      
      Use the 30M-parameter continuous quantile head for better interval calibration.
      
      - **True** (recommended): More accurate prediction intervals, especially for longer horizons; only valid for `max_horizon ≤ 1024`
      - **False**: Uses fixed quantile buckets (faster but less accurate intervals)
      
      #### `force_flip_invariance` (bool, default=True)
      
      Ensures the model satisfies `f(-x) = -f(x)`.
      
      - **True** (RECOMMENDED): Mathematical consistency — forecasts are invariant to sign flip
      - **False**: Slightly faster but may produce asymmetric forecasts
      
      #### `infer_is_positive` (bool, default=True)
      
      Automatically detect if all input values are positive and clamp forecasts ≥ 0.
      
      - **True**: Safe for sales, demand, counts, prices, volumes
      - **False**: Required for temperature, returns, PnL, any series that can be negative
      
      #### `fix_quantile_crossing` (bool, default=False)
      
      Post-process quantiles to ensure monotonicity (q10 ≤ q20 ≤ ... ≤ q90).
      
      - **True** (RECOMMENDED): Guarantees well-ordered quantiles
      - **False**: Slightly faster but quantiles may occasionally cross
      
      #### `return_backcast` (bool, default=False)
      
      Return the model's reconstruction of the input (backcast) in addition to forecast.
      
      - **True**: Required by `forecast_with_covariates()`; plain `forecast()` outputs then include the backcast steps before the horizon
      - **False**: Only return forecast
      
      ---
      
      ## Available Model Checkpoints
      
      | Model ID | Version | Params | Backend | Context |
      | -------- | ------- | ------ | ------- | ------- |
      | `google/timesfm-3.0-pytorch` | 3.0 (non-commercial weights) | ~330M | PyTorch / MLX (`timesfm3`) | 15,360 |
      | `google/timesfm-2.5-200m-pytorch` | 2.5 | 200M | PyTorch | 16,384 |
      | `google/timesfm-2.5-200m-flax` | 2.5 | 200M | JAX/Flax | 16,384 |
      | `google/timesfm-2.5-200m-transformers` | 2.5 | 200M | 🤗 Transformers (`TimesFm2_5ModelForPrediction`) | 16,384 |
      | `google/timesfm-2.0-500m-pytorch` | 2.0 | 500M | PyTorch | 2,048 |
      | `google/timesfm-2.0-500m-jax` | 2.0 | 500M | JAX | 2,048 |
      | `google/timesfm-1.0-200m-pytorch` | 1.0 | 200M | PyTorch | 2,048 |
      | `google/timesfm-1.0-200m` | 1.0 | 200M | JAX | 2,048 |
      
      ---
      
      ## Output Shape Reference
      
      | Output | Shape | Description |
      | ------ | ----- | ----------- |
      | `point_forecast` | `(B, H)` | Median forecast for B series, H steps |
      | `quantile_forecast` | `(B, H, 10)` | Full quantile distribution |
      | `quantile_forecast[:,:,0]` | `(B, H)` | Mean |
      | `quantile_forecast[:,:,1]` | `(B, H)` | 10th percentile |
      | `quantile_forecast[:,:,5]` | `(B, H)` | 50th percentile (= point_forecast) |
      | `quantile_forecast[:,:,9]` | `(B, H)` | 90th percentile |
      
      Where `B` = batch size (number of input series), `H` = forecast horizon.
      
      ---
      
      ## Error Handling
      
      | Error | Cause | Fix |
      | ----- | ----- | --- |
      | `RuntimeError: Model is not compiled` | Called `forecast()` before `compile()` | Call `model.compile(ForecastConfig(...))` first |
      | `torch.cuda.OutOfMemoryError` | Batch too large for GPU | Reduce `per_core_batch_size` |
      | `ValueError: inputs must be list` | Passed array instead of list | Wrap in list: `[array]` |
      | `HfHubHTTPError` | Download failed | Check internet, set `HF_HOME` to writable dir |
      | `ValueError: ... return_backcast must be set to True` | `forecast_with_covariates()` on a model compiled without it | Recompile with `ForecastConfig(..., return_backcast=True)` |
      | `AttributeError: module 'timesfm' has no attribute 'TimesFmHparams'` | 1.x-era code with timesfm ≥ 2.0 | Port to `TimesFM_2p5_200M_torch` (or pin `timesfm==1.3.0` for old checkpoints) |
      
      ---
      
      ## TimesFM 3.0 API (`timesfm3`)
      
      Installed with `timesfm>=3.0` (`uv pip install "timesfm[torch]"`, or `timesfm[mlx]` for the
      MLX backend on Apple silicon). The pretrained weights (`google/timesfm-3.0-pytorch`) are
      distributed under `timesfm-non-commercial-license-v1.0`: non-commercial, non-production use
      only. The source code stays Apache-2.0.
      
      ```python
      import numpy as np
      from timesfm3 import TimesFM3Forecaster   # PyTorch backend; timesfm3.mlx has the same interface
      
      forecaster = TimesFM3Forecaster.from_pretrained("google/timesfm-3.0-pytorch", device="cuda")  # or "cpu"
      
      # Univariate
      out = forecaster.predict(np.asarray(series, dtype=np.float32), horizon=24, return_quantiles=True)
      out.forecast   # (24,)    median forecast
      out.quantiles  # (24, 9)  deciles q10..q90 — index 4 is the median; there is NO mean column
      
      # Batch of series with different lengths (returns a generator of ForecastOutput)
      outs = list(forecaster.predict_batch([s1, s2], horizon=12, return_quantiles=True))
      
      # Multivariate target (num_variates, context) with covariates
      out = forecaster.predict(
          target,                                   # (V, T)
          horizon=32,
          past_only_covariates=past_only,           # (C1, T)
          past_future_covariates=past_future,       # (C2, T + horizon) — known future values
          return_quantiles=True,
      )
      out.forecast   # (V, 32);  out.quantiles  # (V, 32, 9)
      ```
      
      | Parameter | Default | Meaning |
      | --------- | ------- | ------- |
      | `horizon` | — | Steps to forecast (longer horizons are stitched from 64-step output patches) |
      | `return_quantiles` | False | Also return the 9 deciles |
      | `make_positive` | False | Clamp forecasts at zero for non-negative series |
      | `use_symmetric_averaging` | False | Average with the sign-flipped forecast |
      | `use_znorm` | False | Z-normalise inputs before decoding |
      | `padding_mode` | `"none"` | `"none"` or `"edge"` |
      
      Contexts longer than 15,360 steps are truncated to the most recent points.
      `TimesFM3Evaluator(ModelConfig(checkpoint_path=..., per_core_batch_size=..., device=...))`
      exposes the same `predict_batch` for benchmark-style evaluation.
      
    • data_preparation.md 7.5 KB
      # Data Preparation for TimesFM
      
      ## Input Format
      
      TimesFM accepts a **list of 1-D numpy arrays**. Each array represents one
      univariate time series.
      
      ```python
      inputs = [
          np.array([1.0, 2.0, 3.0, 4.0, 5.0]),       # Series 1
          np.array([10.0, 20.0, 15.0, 25.0]),          # Series 2 (different length)
          np.array([100.0, 110.0, 105.0, 115.0, 120.0, 130.0]),  # Series 3
      ]
      ```
      
      ### Key Properties
      
      - **Variable lengths**: Series in the same batch can have different lengths
      - **Float values**: Use `np.float32` or `np.float64`
      - **1-D only**: Each array must be 1-dimensional (not 2-D matrix rows)
      - **NaN handling**: Leading NaNs are stripped; internal NaNs are linearly interpolated
      
      ## Loading from Common Formats
      
      ### CSV — Single Series (Long Format)
      
      ```python
      import pandas as pd
      import numpy as np
      
      df = pd.read_csv("data.csv", parse_dates=["date"])
      values = df["value"].values.astype(np.float32)
      inputs = [values]
      ```
      
      ### CSV — Multiple Series (Wide Format)
      
      ```python
      df = pd.read_csv("data.csv", parse_dates=["date"], index_col="date")
      inputs = [df[col].dropna().values.astype(np.float32) for col in df.columns]
      ```
      
      ### CSV — Long Format with ID Column
      
      ```python
      df = pd.read_csv("data.csv", parse_dates=["date"])
      inputs = []
      for series_id, group in df.groupby("series_id"):
          values = group.sort_values("date")["value"].values.astype(np.float32)
          inputs.append(values)
      ```
      
      ### Pandas DataFrame
      
      ```python
      # Single column
      inputs = [df["temperature"].values.astype(np.float32)]
      
      # Multiple columns
      inputs = [df[col].dropna().values.astype(np.float32) for col in numeric_cols]
      ```
      
      ### Numpy Arrays
      
      ```python
      # 2-D array (rows = series, cols = time steps)
      data = np.load("timeseries.npy")  # shape (N, T)
      inputs = [data[i] for i in range(data.shape[0])]
      
      # Or from 1-D
      inputs = [np.sin(np.linspace(0, 10, 200))]
      ```
      
      ### Excel
      
      ```python
      df = pd.read_excel("data.xlsx", sheet_name="Sheet1")
      inputs = [df[col].dropna().values.astype(np.float32) for col in df.select_dtypes(include=[np.number]).columns]
      ```
      
      ### Parquet
      
      ```python
      df = pd.read_parquet("data.parquet")
      inputs = [df[col].dropna().values.astype(np.float32) for col in df.select_dtypes(include=[np.number]).columns]
      ```
      
      ### JSON
      
      ```python
      import json
      
      with open("data.json") as f:
          data = json.load(f)
      
      # Assumes {"series_name": [values...], ...}
      inputs = [np.array(values, dtype=np.float32) for values in data.values()]
      ```
      
      ## NaN Handling
      
      TimesFM handles NaN values automatically:
      
      ### Leading NaNs
      
      Stripped before feeding to the model:
      
      ```python
      # Input:  [NaN, NaN, 1.0, 2.0, 3.0]
      # Actual: [1.0, 2.0, 3.0]
      ```
      
      ### Internal NaNs
      
      Linearly interpolated:
      
      ```python
      # Input:  [1.0, NaN, 3.0, NaN, NaN, 6.0]
      # Actual: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
      ```
      
      ### Trailing NaNs
      
      **Not handled** — drop them before passing to the model:
      
      ```python
      values = df["value"].values.astype(np.float32)
      # Remove trailing NaNs
      while len(values) > 0 and np.isnan(values[-1]):
          values = values[:-1]
      inputs = [values]
      ```
      
      ### Best Practice
      
      ```python
      def clean_series(arr: np.ndarray) -> np.ndarray:
          """Clean a time series for TimesFM input."""
          arr = np.asarray(arr, dtype=np.float32)
          # Remove trailing NaNs
          while len(arr) > 0 and np.isnan(arr[-1]):
              arr = arr[:-1]
          # Replace inf with NaN (will be interpolated)
          arr[np.isinf(arr)] = np.nan
          return arr
      
      inputs = [clean_series(df[col].values) for col in cols]
      ```
      
      ## Context Length Considerations
      
      | Context Length | Use Case | Notes |
      | -------------- | -------- | ----- |
      | 64–256 | Quick prototyping | Minimal context, fast |
      | 256–512 | Daily data, ~1 year | Good balance |
      | 512–1024 | Daily data, ~2-3 years | Standard production |
      | 1024–4096 | Hourly data, weekly patterns | More context = better |
      | 4096–16384 | High-frequency, long patterns | TimesFM 2.5 maximum (context + horizon ≤ 16,384; TimesFM 3.0: 15,360) |
      
      **Rule of thumb**: Provide at least 3–5 full cycles of the dominant pattern
      (e.g., for weekly seasonality with daily data, provide at least 21–35 days).
      
      ## Covariates (XReg)
      
      TimesFM 2.5 supports exogenous variables through the `forecast_with_covariates()` API
      (requires `timesfm[xreg]` and a model compiled with `ForecastConfig(..., return_backcast=True)`).
      TimesFM 3.0 takes covariates natively instead: `predict(..., past_only_covariates=...,
      past_future_covariates=...)` — see `api_reference.md`.
      
      ### Types of Covariates
      
      | Type | Description | Example |
      | ---- | ----------- | ------- |
      | **Dynamic numerical** | Time-varying numeric features | Temperature, price, promotion spend |
      | **Dynamic categorical** | Time-varying categorical features | Day of week, holiday flag |
      | **Static categorical** | Fixed per-series features | Store ID, region, product category |
      
      ### Preparing Covariates
      
      Each covariate must have length `context + horizon` for each series:
      
      ```python
      import numpy as np
      
      context_len = 100   # length of historical data
      horizon = 24        # forecast horizon
      total_len = context_len + horizon
      
      # Dynamic numerical: temperature forecast for each series
      temp = [
          np.random.randn(total_len).astype(np.float32),  # Series 1
          np.random.randn(total_len).astype(np.float32),  # Series 2
      ]
      
      # Dynamic categorical: day of week (0-6) for each series
      dow = [
          np.tile(np.arange(7), total_len // 7 + 1)[:total_len],  # Series 1
          np.tile(np.arange(7), total_len // 7 + 1)[:total_len],  # Series 2
      ]
      
      # Static categorical: one label per series
      regions = ["east", "west"]
      
      # Forecast with covariates
      point, quantiles = model.forecast_with_covariates(
          inputs=[values1, values2],
          dynamic_numerical_covariates={"temperature": temp},
          dynamic_categorical_covariates={"day_of_week": dow},
          static_categorical_covariates={"region": regions},
          xreg_mode="xreg + timesfm",
      )
      ```
      
      ### XReg Modes
      
      | Mode | Description |
      | ---- | ----------- |
      | `"xreg + timesfm"` (default) | Fit an in-context linear regression of the target on the covariates, then TimesFM forecasts the regression residuals; final = regression + residual forecast |
      | `"timesfm + xreg"` | TimesFM forecasts first, then a linear regression on the covariates fits TimesFM's residuals; final = TimesFM forecast + regression adjustment |
      
      ## Common Data Issues
      
      ### Issue: Series too short
      
      TimesFM needs at least 1 data point, but more context = better forecasts.
      
      ```python
      MIN_LENGTH = 32  # Practical minimum for meaningful forecasts
      
      inputs = [
          arr for arr in raw_inputs
          if len(arr[~np.isnan(arr)]) >= MIN_LENGTH
      ]
      ```
      
      ### Issue: Series with constant values
      
      Constant series may produce NaN or zero-width prediction intervals:
      
      ```python
      for i, arr in enumerate(inputs):
          if np.std(arr[~np.isnan(arr)]) < 1e-10:
              print(f"⚠️ Series {i} is constant — forecast will be flat")
      ```
      
      ### Issue: Extreme outliers
      
      Large outliers can destabilize forecasts even with normalization:
      
      ```python
      def clip_outliers(arr: np.ndarray, n_sigma: float = 5.0) -> np.ndarray:
          """Clip values beyond n_sigma standard deviations."""
          mu = np.nanmean(arr)
          sigma = np.nanstd(arr)
          if sigma > 0:
              arr = np.clip(arr, mu - n_sigma * sigma, mu + n_sigma * sigma)
          return arr
      ```
      
      ### Issue: Mixed frequencies in batch
      
      TimesFM handles each series independently, so you can mix frequencies:
      
      ```python
      inputs = [
          daily_sales,      # 365 points
          weekly_revenue,   # 52 points
          monthly_users,    # 24 points
      ]
      # All forecasted in one batch — TimesFM handles different lengths
      point, q = model.forecast(horizon=12, inputs=inputs)
      ```
      
      However, the `horizon` is shared. If you need different horizons per series,
      forecast in separate calls.
      
    • output_and_config.md 3.8 KB
      # Output Structure & ForecastConfig Reference
      
      How to read TimesFM's `(point_forecast, quantile_forecast)` return and every
      `ForecastConfig` parameter. The SKILL.md body summarizes; this is the full reference.
      
      ## Understanding the Output
      
      ### Quantile Forecast Structure
      
      TimesFM returns `(point_forecast, quantile_forecast)`:
      
      - **`point_forecast`**: shape `(batch, horizon)` — the median (0.5 quantile)
      - **`quantile_forecast`**: shape `(batch, horizon, 10)` — ten slices:
      
      | Index | Quantile | Use |
      | ----- | -------- | --- |
      | 0 | Mean | Average prediction |
      | 1 | 0.1 | Lower bound of 80% PI |
      | 2 | 0.2 | Lower bound of 60% PI |
      | 3 | 0.3 | — |
      | 4 | 0.4 | — |
      | **5** | **0.5** | **Median (= `point_forecast`)** |
      | 6 | 0.6 | — |
      | 7 | 0.7 | — |
      | 8 | 0.8 | Upper bound of 60% PI |
      | 9 | 0.9 | Upper bound of 80% PI |
      
      > The most common bug: `quant_fc[..., 0]` is the **mean**, not q0. q10 = index 1, q90 = index 9.
      
      ### Extracting Prediction Intervals
      
      ```python
      point, q = model.forecast(horizon=H, inputs=data)
      
      # 80% prediction interval (most common)
      lower_80 = q[:, :, 1]  # 10th percentile
      upper_80 = q[:, :, 9]  # 90th percentile
      
      # 60% prediction interval (tighter)
      lower_60 = q[:, :, 2]  # 20th percentile
      upper_60 = q[:, :, 8]  # 80th percentile
      
      # Median (same as point forecast)
      median = q[:, :, 5]
      ```
      
      ```mermaid
      flowchart LR
          accTitle: Quantile Forecast Anatomy
          accDescr: Diagram showing how the 10-element quantile vector maps to prediction intervals.
      
          input["📈 Input Series<br/>1-D array"] --> model["🤖 TimesFM<br/>compile + forecast"]
          model --> point["📍 Point Forecast<br/>(batch, horizon)"]
          model --> quant["📊 Quantile Forecast<br/>(batch, horizon, 10)"]
          quant --> pi80["80% PI<br/>q[:,:,1] – q[:,:,9]"]
          quant --> pi60["60% PI<br/>q[:,:,2] – q[:,:,8]"]
          quant --> median["Median<br/>q[:,:,5]"]
      
          classDef data fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
          classDef model fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87
          classDef output fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
      
          class input data
          class model model
          class point,quant,pi80,pi60,median output
      ```
      
      ## ForecastConfig Reference
      
      All forecasting behavior is controlled by `timesfm.ForecastConfig`:
      
      ```python
      timesfm.ForecastConfig(
          max_context=1024,                    # Max context window (truncates longer series)
          max_horizon=256,                     # Max forecast horizon
          normalize_inputs=True,               # Normalize inputs (RECOMMENDED for stability)
          per_core_batch_size=32,              # Batch size per device (tune for memory)
          use_continuous_quantile_head=True,   # Better quantile accuracy for long horizons
          force_flip_invariance=True,          # Ensures f(-x) = -f(x) (mathematical consistency)
          infer_is_positive=True,              # Clamp forecasts ≥ 0 when all inputs > 0
          fix_quantile_crossing=True,          # Ensure q10 ≤ q20 ≤ ... ≤ q90
          return_backcast=False,               # Return backcast (for covariate workflows)
      )
      ```
      
      | Parameter | Default | When to Change |
      | --------- | ------- | -------------- |
      | `max_context` | 0 | Set to match your longest historical window (e.g., 512, 1024, 4096) |
      | `max_horizon` | 0 | Set to your maximum forecast length |
      | `normalize_inputs` | False | **Always set True** — prevents scale-dependent instability |
      | `per_core_batch_size` | 1 | Increase for throughput; decrease if OOM |
      | `use_continuous_quantile_head` | False | **Set True** for calibrated prediction intervals |
      | `force_flip_invariance` | True | Keep True unless profiling shows it hurts |
      | `infer_is_positive` | True | Set False for series that can be negative (temperature, returns) |
      | `fix_quantile_crossing` | False | **Set True** to guarantee monotonic quantiles |
      
    • pitfalls_and_validation.md 5.5 KB
      # Common Pitfalls, Quality Checklist, Mistakes & Validation
      
      The hard-won correctness rules for this skill. Read before declaring a TimesFM task done.
      
      ## Common Pitfalls
      
      1. **Not running system check** → model load can exhaust RAM on small machines. Run `check_system.py` before the first load.
      2. **Forgetting `model.compile()`** → `RuntimeError: Model is not compiled`. Must call `compile()` before `forecast()`.
      3. **Not setting `normalize_inputs=True`** → unstable forecasts for series with large values.
      4. **Using v1/v2 on machines with < 32 GB RAM** → use TimesFM 2.5 (200M params) instead.
      5. **Not setting `fix_quantile_crossing=True`** → quantiles may not be monotonic (q10 > q50).
      6. **Huge `per_core_batch_size` on small GPU** → CUDA OOM. Start small, increase.
      7. **Passing 2-D arrays** → TimesFM expects a **list of 1-D arrays**, not a 2-D matrix.
      8. **Forgetting `torch.set_float32_matmul_precision("high")`** → slower inference on Ampere+ GPUs.
      9. **Not handling NaN in output** → edge cases with very short series. Always check `np.isnan(point).any()`.
      10. **Using `infer_is_positive=True` for series that can be negative** → clamps forecasts at zero. Set False for temperature, returns, etc.
      
      ## Quality Checklist
      
      Run this checklist after every TimesFM task before declaring success:
      
      - [ ] **Output shape correct** -- `point_fc` shape is `(n_series, horizon)`, `quant_fc` is `(n_series, horizon, 10)`
      - [ ] **Quantile indices** -- index 0 = mean, 1 = q10, 2 = q20 ... 9 = q90. **NOT** 0 = q0, 1 = q10.
      - [ ] **Frequency flag** -- TimesFM 1.0/2.0: pass `freq=[0]` for monthly data. TimesFM 2.5: no freq flag.
      - [ ] **Series length** -- context must be >= 32 data points (model minimum). Warn if shorter.
      - [ ] **No NaN** -- `np.isnan(point_fc).any()` should be False. Check input series for gaps first.
      - [ ] **Visualization axes** -- if multiple panels share data, use `sharex=True`. All time axes must cover the same span.
      - [ ] **Large binary outputs** -- keep big PNG/GIF/HTML artifacts out of version control unless the project tracks them deliberately (e.g. Git LFS).
      - [ ] **No large datasets committed** -- any real dataset > 1 MB should be downloaded to `tempfile.mkdtemp()` and annotated in code.
      - [ ] **`matplotlib.use('Agg')`** -- must appear before any pyplot import when running headless.
      - [ ] **`infer_is_positive`** -- set `False` for temperature anomalies, financial returns, or any series that can be negative.
      
      ## Common Mistakes
      
      These bugs have appeared in this skill's examples. Learn from them:
      
      1. **Quantile index off-by-one** -- The most common mistake. `quant_fc[..., 0]` is the **mean**, not q0. q10 = index 1, q90 = index 9. Always define named constants: `IDX_Q10, IDX_Q20, IDX_Q80, IDX_Q90 = 1, 2, 8, 9`.
      
      2. **Variable shadowing in comprehensions** -- If you build per-series covariate dicts inside a loop, do NOT use the loop variable as the comprehension variable. Accumulate into separate `dict[str, ndarray]` outside the loop, then assign.
         ```python
         # WRONG -- outer `store_id` gets shadowed:
         covariates = {store_id: arr[store_id] for store_id in stores}  # inside outer loop over store_id
         # CORRECT -- use a different name or accumulate beforehand:
         prices_by_store: dict[str, np.ndarray] = {}
         for store_id, config in stores.items():
             prices_by_store[store_id] = compute_price(config)
         ```
      
      3. **Wrong CSV column name** -- The global-temperature CSV uses `anomaly_c`, not `anomaly`. Always `print(df.columns)` before accessing.
      
      4. **`tight_layout()` warning with `sharex=True`** -- Harmless; suppress with `plt.tight_layout(rect=[0, 0, 1, 0.97])` or ignore.
      
      5. **TimesFM 2.5 required for `forecast_with_covariates()`** -- TimesFM 1.0 does NOT have this method. Install `uv pip install "timesfm[torch,xreg]"`, use checkpoint `google/timesfm-2.5-200m-pytorch`, and compile with `return_backcast=True`. (TimesFM 3.0 takes covariates directly in `predict()`.)
      
      6. **Future covariates must span the full horizon** -- Dynamic covariates (price, promotions, holidays) must have values for BOTH the context AND the forecast horizon. You cannot pass context-only arrays.
      
      7. **Anomaly thresholds must be defined once** -- Define `CRITICAL_Z = 3.0`, `WARNING_Z = 2.0` as module-level constants. Never hardcode `3` or `2` inline.
      
      8. **Context anomaly detection uses residuals, not raw values** -- Always detrend first (`np.polyfit` linear, or seasonal decomposition), then Z-score the residuals. Raw-value Z-scores are misleading on trending data.
      
      9. **Mixing up quantile layouts** -- TimesFM 2.5 returns 10 columns (0 = mean, 1–9 = q10–q90); TimesFM 3.0 returns 9 columns (q10–q90, median at index 4). Re-check indices when switching models.
      
      ## Validation & Verification
      
      Use the example outputs as regression baselines. If you change forecasting logic, verify:
      
      ```bash
      # Anomaly detection regression check:
      python -c "
      import json
      d = json.load(open('examples/anomaly-detection/output/anomaly_detection.json'))
      ctx = d['context_summary']
      assert ctx['critical'] >= 1, 'Sep 2023 must be CRITICAL'
      assert any(r['date'] == '2023-09' and r['severity'] == 'CRITICAL'
                 for r in d['context_detections']), 'Sep 2023 not found'
      print('Anomaly detection regression: PASS')"
      
      # Covariates regression check:
      python -c "
      import pandas as pd
      df = pd.read_csv('examples/covariates-forecasting/output/sales_with_covariates.csv')
      assert len(df) == 108, f'Expected 108 rows, got {len(df)}'
      prices = df.groupby('store_id')['price'].mean()
      assert prices['store_A'] > prices['store_B'] > prices['store_C'], 'Store price ordering wrong'
      print('Covariates regression: PASS')"
      ```
      
    • system_requirements.md 5.9 KB
      # System Requirements for TimesFM
      
      ## Hardware Tiers
      
      TimesFM can run on a variety of hardware configurations. This guide helps you
      choose the right setup and tune performance for your machine.
      
      ### Tier 1: Minimal (CPU-Only, 4–8 GB RAM)
      
      - **Use case**: Light exploration, single-series forecasting, prototyping
      - **Model**: TimesFM 2.5 (200M) only
      - **Batch size**: `per_core_batch_size=4`
      - **Context**: Limit `max_context=512`
      - **Expected speed**: ~2–5 seconds per 100-point series
      
      ```python
      model.compile(timesfm.ForecastConfig(
          max_context=512,
          max_horizon=128,
          per_core_batch_size=4,
          normalize_inputs=True,
          use_continuous_quantile_head=True,
          fix_quantile_crossing=True,
      ))
      ```
      
      ### Tier 2: Standard (CPU 16 GB or GPU 4–8 GB VRAM)
      
      - **Use case**: Batch forecasting (dozens of series), evaluation, production prototypes
      - **Model**: TimesFM 2.5 (200M)
      - **Batch size**: `per_core_batch_size=32` (CPU) or `64` (GPU)
      - **Context**: `max_context=1024`
      - **Expected speed**: ~0.5–1 second per 100-point series (GPU)
      
      ```python
      model.compile(timesfm.ForecastConfig(
          max_context=1024,
          max_horizon=256,
          per_core_batch_size=64,
          normalize_inputs=True,
          use_continuous_quantile_head=True,
          fix_quantile_crossing=True,
      ))
      ```
      
      ### Tier 3: Production (GPU 16+ GB VRAM or Apple Silicon 32+ GB)
      
      - **Use case**: Large-scale batch forecasting (thousands of series), long context
      - **Model**: TimesFM 2.5 (200M)
      - **Batch size**: `per_core_batch_size=128–256`
      - **Context**: `max_context=4096` or higher
      - **Expected speed**: ~0.1–0.3 seconds per 100-point series
      
      ```python
      model.compile(timesfm.ForecastConfig(
          max_context=4096,
          max_horizon=256,
          per_core_batch_size=128,
          normalize_inputs=True,
          use_continuous_quantile_head=True,
          fix_quantile_crossing=True,
      ))
      ```
      
      ### Tier 4: Legacy Models (v1.0/v2.0 — 500M parameters)
      
      - **⚠️ WARNING**: TimesFM v2.0 (500M) requires **≥ 16 GB RAM** (CPU) or **≥ 8 GB VRAM** (GPU)
      - **⚠️ WARNING**: TimesFM v1.0 legacy JAX version may require **≥ 32 GB RAM**
      - **Recommendation**: Unless you specifically need a legacy checkpoint, use TimesFM 2.5
      
      ## Memory Estimation
      
      ### CPU Memory (RAM)
      
      Approximate RAM usage during inference:
      
      | Component | TimesFM 2.5 (200M) | TimesFM 2.0 (500M) |
      | --------- | ------------------- | ------------------- |
      | Model weights | ~0.9 GB | ~2 GB |
      | Runtime overhead | ~500 MB | ~1 GB |
      | Input/output buffers | ~200 MB per 1000 series | ~500 MB per 1000 series |
      | **Total (small batch)** | **~1.5 GB** | **~3.5 GB** |
      | **Total (large batch)** | **~3 GB** | **~6 GB** |
      
      **Formula**: `RAM ≈ model_weights + 0.5 GB + (0.2 MB × num_series × context_length / 1000)`
      
      ### GPU Memory (VRAM)
      
      | Component | TimesFM 2.5 (200M) |
      | --------- | ------------------- |
      | Model weights | ~0.9 GB |
      | KV cache + activations | ~200–500 MB (scales with context) |
      | Batch buffers | ~100 MB per 100 series at context=1024 |
      | **Total (batch=32)** | **~1.2 GB** |
      | **Total (batch=128)** | **~1.8 GB** |
      | **Total (batch=256)** | **~2.5 GB** |
      
      ### Disk Space
      
      | Item | Size |
      | ---- | ---- |
      | TimesFM 2.5 safetensors | ~0.93 GB (925 MB) |
      | TimesFM 3.0 safetensors | ~1.3 GB (fp32, ~330M params) |
      | Hugging Face cache overhead | ~200 MB |
      | **Total download (2.5 only)** | **~1.1 GB** |
      
      Model weights are downloaded once from Hugging Face Hub and cached in
      `~/.cache/huggingface/` (or `$HF_HOME`).
      
      ## GPU Selection Guide
      
      ### NVIDIA GPUs (CUDA)
      
      | GPU | VRAM | Recommended batch | Notes |
      | --- | ---- | ----------------- | ----- |
      | RTX 3060 | 12 GB | 64 | Good entry-level |
      | RTX 3090 / 4090 | 24 GB | 256 | Excellent for production |
      | A100 (40 GB) | 40 GB | 512 | Cloud/HPC |
      | A100 (80 GB) | 80 GB | 1024 | Cloud/HPC |
      | T4 | 16 GB | 128 | Cloud (Colab, AWS) |
      | V100 | 16–32 GB | 128–256 | Cloud |
      
      ### Apple Silicon (MPS)
      
      | Chip | Unified Memory | Recommended batch | Notes |
      | ---- | -------------- | ----------------- | ----- |
      | M1 | 8–16 GB | 16–32 | Works, slower than CUDA |
      | M1 Pro/Max | 16–64 GB | 32–128 | Good performance |
      | M2/M3/M4 Pro/Max | 18–128 GB | 64–256 | Excellent |
      
      ### CPU Only
      
      Works on any CPU with sufficient RAM. Expect 5–20× slower than GPU.
      
      ## Python and Package Requirements
      
      | Requirement | Minimum | Recommended |
      | ----------- | ------- | ----------- |
      | Python | 3.10 | 3.12+ |
      | numpy | 1.26.4 | latest |
      | torch | 2.0.0 | latest |
      | huggingface_hub | 0.28.0 | latest |
      | safetensors | 0.5.3 | latest |
      
      ### Optional Dependencies
      
      | Package | Purpose | Install |
      | ------- | ------- | ------- |
      | flax + jax | Flax backend for TimesFM 2.5 | `uv pip install "timesfm[flax]"` |
      | jax + scikit-learn | XReg covariates (TimesFM 2.5) | `uv pip install "timesfm[xreg]"` |
      | mlx | TimesFM 3.0 on Apple silicon without PyTorch | `uv pip install "timesfm[mlx]"` |
      
      ## Operating System Compatibility
      
      | OS | Status | Notes |
      | -- | ------ | ----- |
      | Linux (Ubuntu 20.04+) | ✅ Fully supported | Best performance with CUDA |
      | macOS 13+ (Ventura) | ✅ Fully supported | MPS acceleration on Apple Silicon |
      | Windows 11 + WSL2 | ✅ Supported | Use WSL2 for best experience |
      | Windows (native) | ⚠️ Partial | PyTorch works, some edge cases |
      
      ## Troubleshooting
      
      ### Out of Memory (OOM)
      
      ```python
      # Reduce batch size
      model.compile(timesfm.ForecastConfig(
          per_core_batch_size=4,  # Start very small
          max_context=512,        # Reduce context
          ...
      ))
      
      # Process in chunks
      for i in range(0, len(inputs), 50):
          chunk = inputs[i:i+50]
          p, q = model.forecast(horizon=H, inputs=chunk)
      ```
      
      ### Slow Inference on CPU
      
      ```python
      # Ensure matmul precision is set
      import torch
      torch.set_float32_matmul_precision("high")
      
      # Use smaller context
      model.compile(timesfm.ForecastConfig(
          max_context=256,  # Shorter context = faster
          ...
      ))
      ```
      
      ### Model Download Fails
      
      ```bash
      # Set a different cache directory
      export HF_HOME=/path/with/more/space
      
      # Or download manually
      huggingface-cli download google/timesfm-2.5-200m-pytorch
      ```
      
    • workflows.md 6.6 KB
      # Common Workflows, Performance Tuning & Integration
      
      Copy-paste workflows for single/batch/eval forecasting, GPU and memory tuning, and
      integration with statsmodels / matplotlib / EDA. All checkpoint ids and the
      `forecast_with_covariates()` API are current as of TimesFM 2.5.
      
      ## Workflow 1: Single Series Forecast
      
      ```mermaid
      flowchart TD
          accTitle: Single Series Forecast Workflow
          accDescr: Step-by-step workflow for forecasting a single time series with system checking.
      
          check["1. Run check_system.py"] --> load["2. Load model<br/>from_pretrained()"]
          load --> compile["3. Compile with ForecastConfig"]
          compile --> prep["4. Prepare data<br/>pd.read_csv → np.array"]
          prep --> forecast["5. model.forecast()<br/>horizon=N"]
          forecast --> extract["6. Extract point + PI"]
          extract --> plot["7. Plot or export results"]
      
          classDef step fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#1f2937
          class check,load,compile,prep,forecast,extract,plot step
      ```
      
      ```python
      import torch, numpy as np, pandas as pd, timesfm
      
      # 1. System check (run once)
      # python scripts/check_system.py
      
      # 2-3. Load and compile
      torch.set_float32_matmul_precision("high")
      model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
          "google/timesfm-2.5-200m-pytorch"
      )
      model.compile(timesfm.ForecastConfig(
          max_context=512, max_horizon=52, normalize_inputs=True,
          use_continuous_quantile_head=True, fix_quantile_crossing=True,
      ))
      
      # 4. Prepare data
      df = pd.read_csv("weekly_demand.csv", parse_dates=["week"])
      values = df["demand"].values.astype(np.float32)
      
      # 5. Forecast
      point, quantiles = model.forecast(horizon=52, inputs=[values])
      
      # 6. Extract prediction intervals
      forecast_df = pd.DataFrame({
          "forecast": point[0],
          "lower_80": quantiles[0, :, 1],
          "upper_80": quantiles[0, :, 9],
      })
      
      # 7. Plot
      import matplotlib.pyplot as plt
      fig, ax = plt.subplots(figsize=(12, 5))
      ax.plot(values[-104:], label="Historical")
      x_fc = range(len(values[-104:]), len(values[-104:]) + 52)
      ax.plot(x_fc, forecast_df["forecast"], label="Forecast", color="tab:orange")
      ax.fill_between(x_fc, forecast_df["lower_80"], forecast_df["upper_80"],
                      alpha=0.2, color="tab:orange", label="80% PI")
      ax.legend()
      ax.set_title("52-Week Demand Forecast")
      plt.tight_layout()
      plt.savefig("forecast.png", dpi=150)
      print("Saved forecast.png")
      ```
      
      ## Workflow 2: Batch Forecasting (Many Series)
      
      ```python
      import pandas as pd, numpy as np
      
      # Load wide-format CSV (one column per series)
      df = pd.read_csv("all_stores.csv", parse_dates=["date"], index_col="date")
      inputs = [df[col].dropna().values.astype(np.float32) for col in df.columns]
      
      # Forecast all series at once (batched internally)
      point, quantiles = model.forecast(horizon=30, inputs=inputs)
      
      # Collect results
      results = {}
      for i, col in enumerate(df.columns):
          results[col] = {
              "forecast": point[i].tolist(),
              "lower_80": quantiles[i, :, 1].tolist(),
              "upper_80": quantiles[i, :, 9].tolist(),
          }
      
      # Export
      import json
      with open("batch_forecasts.json", "w") as f:
          json.dump(results, f, indent=2)
      print(f"Forecasted {len(results)} series → batch_forecasts.json")
      ```
      
      ## Workflow 3: Evaluate Forecast Accuracy
      
      ```python
      import numpy as np
      
      # Hold out the last H points for evaluation
      H = 24
      train = values[:-H]
      actual = values[-H:]
      
      point, quantiles = model.forecast(horizon=H, inputs=[train])
      pred = point[0]
      
      # Metrics
      mae = np.mean(np.abs(actual - pred))
      rmse = np.sqrt(np.mean((actual - pred) ** 2))
      mape = np.mean(np.abs((actual - pred) / actual)) * 100
      
      # Prediction interval coverage
      lower = quantiles[0, :, 1]
      upper = quantiles[0, :, 9]
      coverage = np.mean((actual >= lower) & (actual <= upper)) * 100
      
      print(f"MAE:  {mae:.2f}")
      print(f"RMSE: {rmse:.2f}")
      print(f"MAPE: {mape:.1f}%")
      print(f"80% PI Coverage: {coverage:.1f}% (target: 80%)")
      ```
      
      ## Performance Tuning
      
      ### GPU Acceleration
      
      ```python
      import torch
      
      # Check GPU availability
      if torch.cuda.is_available():
          print(f"GPU: {torch.cuda.get_device_name(0)}")
          print(f"VRAM: {torch.cuda.get_device_properties(0).total_mem / 1e9:.1f} GB")
      elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
          print("Apple Silicon MPS available")
      else:
          print("CPU only — inference will be slower but still works")
      
      # Always set this for Ampere+ GPUs (A100, RTX 3090, etc.)
      torch.set_float32_matmul_precision("high")
      ```
      
      ### Batch Size Tuning
      
      ```python
      # Start conservative, increase until OOM
      # GPU with 8 GB VRAM:  per_core_batch_size=64
      # GPU with 16 GB VRAM: per_core_batch_size=128
      # GPU with 24 GB VRAM: per_core_batch_size=256
      # CPU with 8 GB RAM:   per_core_batch_size=8
      # CPU with 16 GB RAM:  per_core_batch_size=32
      # CPU with 32 GB RAM:  per_core_batch_size=64
      
      model.compile(timesfm.ForecastConfig(
          max_context=1024,
          max_horizon=256,
          per_core_batch_size=32,  # <-- tune this
          normalize_inputs=True,
          use_continuous_quantile_head=True,
          fix_quantile_crossing=True,
      ))
      ```
      
      ### Memory-Constrained Environments
      
      ```python
      import gc, torch
      
      # Force garbage collection before loading
      gc.collect()
      if torch.cuda.is_available():
          torch.cuda.empty_cache()
      
      # Load model
      model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
          "google/timesfm-2.5-200m-pytorch"
      )
      
      # Use small batch size on low-memory machines
      model.compile(timesfm.ForecastConfig(
          max_context=512,        # Reduce context if needed
          max_horizon=128,        # Reduce horizon if needed
          per_core_batch_size=4,  # Small batches
          normalize_inputs=True,
          use_continuous_quantile_head=True,
          fix_quantile_crossing=True,
      ))
      
      # Process series in chunks to avoid OOM
      CHUNK = 50
      all_results = []
      for i in range(0, len(inputs), CHUNK):
          chunk = inputs[i:i+CHUNK]
          p, q = model.forecast(horizon=H, inputs=chunk)
          all_results.append((p, q))
          gc.collect()  # Clean up between chunks
      ```
      
      ## Integration with Other Skills
      
      ### With `statsmodels` (`alterlab-statsmodels`)
      
      Use `statsmodels` for classical models (ARIMA, SARIMAX) as a **comparison baseline**:
      
      ```python
      # TimesFM forecast
      tfm_point, tfm_q = model.forecast(horizon=H, inputs=[values])
      
      # statsmodels ARIMA forecast
      from statsmodels.tsa.arima.model import ARIMA
      arima = ARIMA(values, order=(1,1,1)).fit()
      arima_forecast = arima.forecast(steps=H)
      
      # Compare
      print(f"TimesFM MAE: {np.mean(np.abs(actual - tfm_point[0])):.2f}")
      print(f"ARIMA MAE:   {np.mean(np.abs(actual - arima_forecast)):.2f}")
      ```
      
      ### With `matplotlib` / `alterlab-scientific-viz`
      
      Plot forecasts with prediction intervals as publication-quality figures.
      
      ### With `alterlab-eda`
      
      Run EDA on the time series before forecasting to understand trends, seasonality, and stationarity.
      
  • scripts
    • check_system.py 17.2 KB
      #!/usr/bin/env python3
      """TimesFM System Requirements Preflight Checker.
      
      MANDATORY: Run this script before loading TimesFM for the first time.
      It checks RAM, GPU/VRAM, disk space, Python version, and package
      installation so the agent never crashes a user's machine.
      
      Usage:
          python check_system.py
          python check_system.py --model v2.5   # default
          python check_system.py --model v3.0   # TimesFM 3.0 (~330M, non-commercial weights)
          python check_system.py --model v2.0   # archived 500M model
          python check_system.py --model v1.0   # archived 200M model
          python check_system.py --json         # machine-readable output
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import shutil
      import struct
      import sys
      from dataclasses import dataclass, field
      from pathlib import Path
      from typing import Any
      
      
      # ---------------------------------------------------------------------------
      # Model requirement profiles
      # ---------------------------------------------------------------------------
      
      MODEL_PROFILES: dict[str, dict[str, Any]] = {
          # TimesFM 3.0: 1.3 GB fp32 weights; measured peak RSS ~2.7 GB on CPU for
          # 32 series x 1,024 context (timesfm 3.0.2). Thresholds keep the same
          # headroom ratio as the v2.5 profile (measured ~2.0 GB).
          "v3.0": {
              "name": "TimesFM 3.0 (~330M)",
              "params": "330M",
              "min_ram_gb": 3.0,
              "recommended_ram_gb": 6.0,
              "min_vram_gb": 3.0,
              "recommended_vram_gb": 6.0,
              "disk_gb": 3.0,  # model weights + overhead
              "hf_repo": "google/timesfm-3.0-pytorch",
          },
          "v2.5": {
              "name": "TimesFM 2.5 (200M)",
              "params": "200M",
              "min_ram_gb": 2.0,
              "recommended_ram_gb": 4.0,
              "min_vram_gb": 2.0,
              "recommended_vram_gb": 4.0,
              "disk_gb": 2.0,  # model weights + overhead
              "hf_repo": "google/timesfm-2.5-200m-pytorch",
          },
          "v2.0": {
              "name": "TimesFM 2.0 (500M)",
              "params": "500M",
              "min_ram_gb": 8.0,
              "recommended_ram_gb": 16.0,
              "min_vram_gb": 4.0,
              "recommended_vram_gb": 8.0,
              "disk_gb": 4.0,
              "hf_repo": "google/timesfm-2.0-500m-pytorch",
          },
          "v1.0": {
              "name": "TimesFM 1.0 (200M)",
              "params": "200M",
              "min_ram_gb": 4.0,
              "recommended_ram_gb": 8.0,
              "min_vram_gb": 2.0,
              "recommended_vram_gb": 4.0,
              "disk_gb": 2.0,
              "hf_repo": "google/timesfm-1.0-200m-pytorch",
          },
      }
      
      
      # ---------------------------------------------------------------------------
      # Result dataclass
      # ---------------------------------------------------------------------------
      
      
      @dataclass
      class CheckResult:
          name: str
          status: str  # "pass", "warn", "fail"
          detail: str
          value: str = ""
      
          @property
          def icon(self) -> str:
              return {"pass": "✅", "warn": "⚠️", "fail": "🛑"}.get(self.status, "❓")
      
          def __str__(self) -> str:
              return f"[{self.name:<10}] {self.value:<40} {self.icon} {self.status.upper()}"
      
      
      @dataclass
      class SystemReport:
          model: str
          checks: list[CheckResult] = field(default_factory=list)
          verdict: str = ""
          verdict_detail: str = ""
          recommended_batch_size: int = 1
          mode: str = "cpu"  # "cpu", "gpu", "mps"
      
          @property
          def passed(self) -> bool:
              return all(c.status != "fail" for c in self.checks)
      
          def to_dict(self) -> dict[str, Any]:
              return {
                  "model": self.model,
                  "passed": self.passed,
                  "mode": self.mode,
                  "recommended_batch_size": self.recommended_batch_size,
                  "verdict": self.verdict,
                  "verdict_detail": self.verdict_detail,
                  "checks": [
                      {
                          "name": c.name,
                          "status": c.status,
                          "detail": c.detail,
                          "value": c.value,
                      }
                      for c in self.checks
                  ],
              }
      
      
      # ---------------------------------------------------------------------------
      # Individual checks
      # ---------------------------------------------------------------------------
      
      
      def _get_total_ram_gb() -> float:
          """Return total physical RAM in GB, cross-platform."""
          try:
              if sys.platform == "linux":
                  with open("/proc/meminfo") as f:
                      for line in f:
                          if line.startswith("MemTotal"):
                              return int(line.split()[1]) / (1024 * 1024)
              elif sys.platform == "darwin":
                  import subprocess
      
                  result = subprocess.run(
                      ["sysctl", "-n", "hw.memsize"],
                      capture_output=True,
                      text=True,
                      check=True,
                  )
                  return int(result.stdout.strip()) / (1024**3)
              elif sys.platform == "win32":
                  import ctypes
      
                  kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]
      
                  class MEMORYSTATUSEX(ctypes.Structure):
                      _fields_ = [
                          ("dwLength", ctypes.c_ulong),
                          ("dwMemoryLoad", ctypes.c_ulong),
                          ("ullTotalPhys", ctypes.c_ulonglong),
                          ("ullAvailPhys", ctypes.c_ulonglong),
                          ("ullTotalPageFile", ctypes.c_ulonglong),
                          ("ullAvailPageFile", ctypes.c_ulonglong),
                          ("ullTotalVirtual", ctypes.c_ulonglong),
                          ("ullAvailVirtual", ctypes.c_ulonglong),
                          ("sullAvailExtendedVirtual", ctypes.c_ulonglong),
                      ]
      
                  stat = MEMORYSTATUSEX()
                  stat.dwLength = ctypes.sizeof(stat)
                  kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
                  return stat.ullTotalPhys / (1024**3)
          except Exception:
              pass
      
          # Fallback: use struct to estimate (unreliable)
          return struct.calcsize("P") * 8 / 8  # placeholder
      
      
      def _get_available_ram_gb() -> float:
          """Return available RAM in GB."""
          try:
              if sys.platform == "linux":
                  with open("/proc/meminfo") as f:
                      for line in f:
                          if line.startswith("MemAvailable"):
                              return int(line.split()[1]) / (1024 * 1024)
              elif sys.platform == "darwin":
                  import subprocess
      
                  # Use vm_stat for available memory on macOS
                  result = subprocess.run(
                      ["vm_stat"], capture_output=True, text=True, check=True
                  )
                  free = 0
                  page_size = 4096
                  for line in result.stdout.split("\n"):
                      if "Pages free" in line or "Pages inactive" in line:
                          val = line.split(":")[1].strip().rstrip(".")
                          free += int(val) * page_size
                  return free / (1024**3)
              elif sys.platform == "win32":
                  import ctypes
      
                  kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]
      
                  class MEMORYSTATUSEX(ctypes.Structure):
                      _fields_ = [
                          ("dwLength", ctypes.c_ulong),
                          ("dwMemoryLoad", ctypes.c_ulong),
                          ("ullTotalPhys", ctypes.c_ulonglong),
                          ("ullAvailPhys", ctypes.c_ulonglong),
                          ("ullTotalPageFile", ctypes.c_ulonglong),
                          ("ullAvailPageFile", ctypes.c_ulonglong),
                          ("ullTotalVirtual", ctypes.c_ulonglong),
                          ("ullAvailVirtual", ctypes.c_ulonglong),
                          ("sullAvailExtendedVirtual", ctypes.c_ulonglong),
                      ]
      
                  stat = MEMORYSTATUSEX()
                  stat.dwLength = ctypes.sizeof(stat)
                  kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
                  return stat.ullAvailPhys / (1024**3)
          except Exception:
              pass
          return 0.0
      
      
      def check_ram(profile: dict[str, Any]) -> CheckResult:
          """Check if system has enough RAM."""
          total = _get_total_ram_gb()
          available = _get_available_ram_gb()
          min_ram = profile["min_ram_gb"]
          rec_ram = profile["recommended_ram_gb"]
      
          value = f"Total: {total:.1f} GB | Available: {available:.1f} GB"
      
          if total < min_ram:
              return CheckResult(
                  name="RAM",
                  status="fail",
                  detail=(
                      f"System has {total:.1f} GB RAM but {profile['name']} requires "
                      f"at least {min_ram:.0f} GB. The model will likely fail to load "
                      f"or cause the system to swap heavily and become unresponsive."
                  ),
                  value=value,
              )
          elif total < rec_ram:
              return CheckResult(
                  name="RAM",
                  status="warn",
                  detail=(
                      f"System has {total:.1f} GB RAM. {profile['name']} recommends "
                      f"{rec_ram:.0f} GB. It may work with small batch sizes but could "
                      f"be tight. Use per_core_batch_size=4 or lower."
                  ),
                  value=value,
              )
          else:
              return CheckResult(
                  name="RAM",
                  status="pass",
                  detail=f"System has {total:.1f} GB RAM, meets {rec_ram:.0f} GB recommendation.",
                  value=value,
              )
      
      
      def check_gpu() -> CheckResult:
          """Check GPU availability and VRAM."""
          # Try CUDA first
          try:
              import torch
      
              if torch.cuda.is_available():
                  name = torch.cuda.get_device_name(0)
                  vram = torch.cuda.get_device_properties(0).total_memory / (1024**3)
                  return CheckResult(
                      name="GPU",
                      status="pass",
                      detail=f"{name} with {vram:.1f} GB VRAM detected.",
                      value=f"{name} | VRAM: {vram:.1f} GB",
                  )
              elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
                  return CheckResult(
                      name="GPU",
                      status="pass",
                      detail="Apple Silicon MPS backend available. Uses unified memory.",
                      value="Apple Silicon MPS",
                  )
              else:
                  return CheckResult(
                      name="GPU",
                      status="warn",
                      detail=(
                          "No GPU detected. TimesFM will run on CPU (slower but functional). "
                          "Install CUDA-enabled PyTorch for GPU acceleration."
                      ),
                      value="None (CPU only)",
                  )
          except ImportError:
              return CheckResult(
                  name="GPU",
                  status="warn",
                  detail="PyTorch not installed — cannot check GPU. Install torch first.",
                  value="Unknown (torch not installed)",
              )
      
      
      def check_disk(profile: dict[str, Any]) -> CheckResult:
          """Check available disk space for model download."""
          # Check HuggingFace cache dir or home dir
          hf_cache = os.environ.get("HF_HOME", os.path.expanduser("~/.cache/huggingface"))
          cache_dir = Path(hf_cache)
          check_dir = cache_dir if cache_dir.exists() else Path.home()
      
          usage = shutil.disk_usage(str(check_dir))
          free_gb = usage.free / (1024**3)
          required = profile["disk_gb"]
      
          value = f"Free: {free_gb:.1f} GB (in {check_dir})"
      
          if free_gb < required:
              return CheckResult(
                  name="Disk",
                  status="fail",
                  detail=(
                      f"Only {free_gb:.1f} GB free in {check_dir}. "
                      f"Need at least {required:.0f} GB for model weights. "
                      f"Free up space or set HF_HOME to a larger volume."
                  ),
                  value=value,
              )
          else:
              return CheckResult(
                  name="Disk",
                  status="pass",
                  detail=f"{free_gb:.1f} GB available, exceeds {required:.0f} GB requirement.",
                  value=value,
              )
      
      
      def check_python() -> CheckResult:
          """Check Python version >= 3.10."""
          version = sys.version.split()[0]
          major, minor = sys.version_info[:2]
      
          if (major, minor) < (3, 10):
              return CheckResult(
                  name="Python",
                  status="fail",
                  detail=f"Python {version} detected. TimesFM requires Python >= 3.10.",
                  value=version,
              )
          else:
              return CheckResult(
                  name="Python",
                  status="pass",
                  detail=f"Python {version} meets >= 3.10 requirement.",
                  value=version,
              )
      
      
      def check_package(pkg_name: str, import_name: str | None = None) -> CheckResult:
          """Check if a Python package is installed."""
          import_name = import_name or pkg_name
          try:
              mod = __import__(import_name)
              version = getattr(mod, "__version__", None)
              if version is None:  # e.g. timesfm >= 2.0 defines no __version__
                  from importlib.metadata import PackageNotFoundError, version as dist_version
      
                  try:
                      version = dist_version(pkg_name)
                  except PackageNotFoundError:
                      version = "unknown"
              return CheckResult(
                  name=pkg_name,
                  status="pass",
                  detail=f"{pkg_name} {version} is installed.",
                  value=f"Installed ({version})",
              )
          except ImportError:
              return CheckResult(
                  name=pkg_name,
                  status="warn",
                  detail=f"{pkg_name} is not installed. Run: uv pip install {pkg_name}",
                  value="Not installed",
              )
      
      
      # ---------------------------------------------------------------------------
      # Batch size recommendation
      # ---------------------------------------------------------------------------
      
      
      def recommend_batch_size(report: SystemReport) -> int:
          """Recommend per_core_batch_size based on available resources."""
          total_ram = _get_total_ram_gb()
      
          # Check if GPU is available
          gpu_check = next((c for c in report.checks if c.name == "GPU"), None)
      
          if gpu_check and gpu_check.status == "pass" and "VRAM" in gpu_check.value:
              # Extract VRAM
              try:
                  vram_str = gpu_check.value.split("VRAM:")[1].strip().split()[0]
                  vram = float(vram_str)
                  if vram >= 24:
                      return 256
                  elif vram >= 16:
                      return 128
                  elif vram >= 8:
                      return 64
                  elif vram >= 4:
                      return 32
                  else:
                      return 16
              except (ValueError, IndexError):
                  return 32
          elif gpu_check and "MPS" in gpu_check.value:
              # Apple Silicon — use unified memory heuristic
              if total_ram >= 32:
                  return 64
              elif total_ram >= 16:
                  return 32
              else:
                  return 16
          else:
              # CPU only
              if total_ram >= 32:
                  return 64
              elif total_ram >= 16:
                  return 32
              elif total_ram >= 8:
                  return 8
              else:
                  return 4
      
      
      # ---------------------------------------------------------------------------
      # Main
      # ---------------------------------------------------------------------------
      
      
      def run_checks(model_version: str = "v2.5") -> SystemReport:
          """Run all system checks and return a report."""
          profile = MODEL_PROFILES[model_version]
          report = SystemReport(model=profile["name"])
      
          # Run checks
          report.checks.append(check_ram(profile))
          report.checks.append(check_gpu())
          report.checks.append(check_disk(profile))
          report.checks.append(check_python())
          report.checks.append(check_package("timesfm"))
          report.checks.append(check_package("torch"))
      
          # Determine mode
          gpu_check = next((c for c in report.checks if c.name == "GPU"), None)
          if gpu_check and gpu_check.status == "pass":
              if "MPS" in gpu_check.value:
                  report.mode = "mps"
              else:
                  report.mode = "gpu"
          else:
              report.mode = "cpu"
      
          # Batch size
          report.recommended_batch_size = recommend_batch_size(report)
      
          # Verdict
          if report.passed:
              report.verdict = (
                  f"✅ System is ready for {profile['name']} ({report.mode.upper()} mode)"
              )
              report.verdict_detail = (
                  f"Recommended: per_core_batch_size={report.recommended_batch_size}"
              )
          else:
              failed = [c for c in report.checks if c.status == "fail"]
              report.verdict = f"🛑 System does NOT meet requirements for {profile['name']}"
              report.verdict_detail = "; ".join(c.detail for c in failed)
      
          return report
      
      
      def print_report(report: SystemReport) -> None:
          """Print a human-readable report to stdout."""
          print(f"\n{'=' * 50}")
          print(f"  TimesFM System Requirements Check")
          print(f"  Model: {report.model}")
          print(f"{'=' * 50}\n")
      
          for check in report.checks:
              print(f"  {check}")
          print()
      
          print(f"  VERDICT: {report.verdict}")
          if report.verdict_detail:
              print(f"  {report.verdict_detail}")
          print()
      
      
      def main() -> None:
          parser = argparse.ArgumentParser(
              description="Check system requirements for TimesFM."
          )
          parser.add_argument(
              "--model",
              choices=list(MODEL_PROFILES.keys()),
              default="v2.5",
              help="Model version to check requirements for (default: v2.5)",
          )
          parser.add_argument(
              "--json",
              action="store_true",
              help="Output results as JSON (machine-readable)",
          )
          args = parser.parse_args()
      
          report = run_checks(args.model)
      
          if args.json:
              print(json.dumps(report.to_dict(), indent=2))
          else:
              print_report(report)
      
          # Exit with non-zero if any check failed
          sys.exit(0 if report.passed else 1)
      
      
      if __name__ == "__main__":
          main()
      
    • forecast_csv.py 8.9 KB
      #!/usr/bin/env python3
      """End-to-end CSV forecasting with TimesFM.
      
      Loads a CSV, runs the system preflight check, loads TimesFM, forecasts
      the requested columns, and writes results to a new CSV or JSON.
      
      Usage:
          python forecast_csv.py input.csv --horizon 24
          python forecast_csv.py input.csv --horizon 12 --date-col date --value-cols sales,revenue
          python forecast_csv.py input.csv --horizon 52 --output forecasts.csv
          python forecast_csv.py input.csv --horizon 30 --output forecasts.json --format json
      
      The script automatically:
        1. Runs the system preflight check (exits if it fails).
        2. Loads TimesFM 2.5 from Hugging Face.
        3. Reads the CSV and identifies time series columns.
        4. Forecasts each series with prediction intervals.
        5. Writes results to the specified output file.
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import sys
      from pathlib import Path
      
      import numpy as np
      import pandas as pd
      
      
      def run_preflight() -> dict:
          """Run the system preflight check and return the report."""
          # Import the check_system module from the same directory
          script_dir = Path(__file__).parent
          sys.path.insert(0, str(script_dir))
          from check_system import run_checks
      
          report = run_checks("v2.5")
          if not report.passed:
              print("\n🛑 System check FAILED. Cannot proceed with forecasting.")
              print(f"   {report.verdict_detail}")
              print("\nRun 'python scripts/check_system.py' for details.")
              sys.exit(1)
      
          return report.to_dict()
      
      
      def load_model(batch_size: int = 32, horizon: int = 256):
          """Load and compile the TimesFM model for forecasts up to ``horizon`` steps."""
          import torch
          import timesfm
      
          torch.set_float32_matmul_precision("high")
      
          print("Loading TimesFM 2.5 from Hugging Face...")
          model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
              "google/timesfm-2.5-200m-pytorch"
          )
      
          print(f"Compiling with per_core_batch_size={batch_size}...")
          model.compile(
              timesfm.ForecastConfig(
                  max_context=1024,
                  # forecast() rejects horizon > max_horizon, so size it to the request
                  max_horizon=max(256, horizon),
                  normalize_inputs=True,
                  # the continuous quantile head only supports horizons up to 1024 steps
                  use_continuous_quantile_head=horizon <= 1024,
                  force_flip_invariance=True,
                  infer_is_positive=True,
                  fix_quantile_crossing=True,
                  per_core_batch_size=batch_size,
              )
          )
      
          return model
      
      
      def load_csv(
          path: str,
          date_col: str | None = None,
          value_cols: list[str] | None = None,
      ) -> tuple[pd.DataFrame, list[str], str | None]:
          """Load CSV and identify time series columns.
      
          Returns:
              (dataframe, value_column_names, date_column_name_or_none)
          """
          df = pd.read_csv(path)
      
          # Identify date column
          if date_col and date_col in df.columns:
              df[date_col] = pd.to_datetime(df[date_col])
          elif date_col:
              print(f"⚠️ Date column '{date_col}' not found. Available: {list(df.columns)}")
              date_col = None
      
          # Identify value columns
          if value_cols:
              missing = [c for c in value_cols if c not in df.columns]
              if missing:
                  print(f"⚠️ Columns not found: {missing}. Available: {list(df.columns)}")
                  value_cols = [c for c in value_cols if c in df.columns]
          else:
              # Auto-detect numeric columns (exclude date)
              numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
              if date_col and date_col in numeric_cols:
                  numeric_cols.remove(date_col)
              value_cols = numeric_cols
      
          if not value_cols:
              print("🛑 No numeric columns found to forecast.")
              sys.exit(1)
      
          print(f"Found {len(value_cols)} series to forecast: {value_cols}")
          return df, value_cols, date_col
      
      
      def forecast_series(
          model, df: pd.DataFrame, value_cols: list[str], horizon: int
      ) -> dict[str, dict]:
          """Forecast all series and return results dict."""
          inputs = []
          for col in value_cols:
              values = df[col].dropna().values.astype(np.float32)
              inputs.append(values)
      
          print(f"Forecasting {len(inputs)} series with horizon={horizon}...")
          point, quantiles = model.forecast(horizon=horizon, inputs=inputs)
      
          results = {}
          for i, col in enumerate(value_cols):
              # Quantile index map: 0=mean, 1=q10, 2=q20, ..., 8=q80, 9=q90.
              # 80% PI = q10..q90 (index 1/9); 60% PI = q20..q80 (index 2/8).
              results[col] = {
                  "forecast": point[i].tolist(),
                  "lower_80": quantiles[i, :, 1].tolist(),  # q10 — lower bound of 80% PI
                  "lower_60": quantiles[i, :, 2].tolist(),  # q20 — lower bound of 60% PI
                  "median": quantiles[i, :, 5].tolist(),  # q50
                  "upper_60": quantiles[i, :, 8].tolist(),  # q80 — upper bound of 60% PI
                  "upper_80": quantiles[i, :, 9].tolist(),  # q90 — upper bound of 80% PI
              }
      
          return results
      
      
      def write_csv_output(
          results: dict[str, dict],
          output_path: str,
          df: pd.DataFrame,
          date_col: str | None,
          horizon: int,
      ) -> None:
          """Write forecast results to CSV."""
          rows = []
          for col, data in results.items():
              # Try to generate future dates
              future_dates = list(range(1, horizon + 1))
              if date_col and date_col in df.columns:
                  try:
                      last_date = df[date_col].dropna().iloc[-1]
                      freq = pd.infer_freq(df[date_col].dropna())
                      if freq:
                          future_dates = pd.date_range(
                              last_date, periods=horizon + 1, freq=freq
                          )[1:].tolist()
                  except Exception:
                      pass
      
              for h in range(horizon):
                  row = {
                      "series": col,
                      "step": h + 1,
                      "forecast": data["forecast"][h],
                      "lower_80": data["lower_80"][h],
                      "lower_60": data["lower_60"][h],
                      "median": data["median"][h],
                      "upper_60": data["upper_60"][h],
                      "upper_80": data["upper_80"][h],
                  }
                  if isinstance(future_dates[0], (pd.Timestamp,)):
                      row["date"] = future_dates[h]
                  rows.append(row)
      
          out_df = pd.DataFrame(rows)
          out_df.to_csv(output_path, index=False)
          print(f"✅ Wrote {len(rows)} forecast rows to {output_path}")
      
      
      def write_json_output(results: dict[str, dict], output_path: str) -> None:
          """Write forecast results to JSON."""
          with open(output_path, "w") as f:
              json.dump(results, f, indent=2)
          print(f"✅ Wrote forecasts for {len(results)} series to {output_path}")
      
      
      def main() -> None:
          parser = argparse.ArgumentParser(
              description="Forecast time series from CSV using TimesFM."
          )
          parser.add_argument("input", help="Path to input CSV file")
          parser.add_argument(
              "--horizon", type=int, required=True, help="Number of steps to forecast"
          )
          parser.add_argument("--date-col", help="Name of the date/time column")
          parser.add_argument(
              "--value-cols",
              help="Comma-separated list of value columns to forecast (default: all numeric)",
          )
          parser.add_argument(
              "--output",
              default="forecasts.csv",
              help="Output file path (default: forecasts.csv)",
          )
          parser.add_argument(
              "--format",
              choices=["csv", "json"],
              default=None,
              help="Output format (inferred from --output extension if not set)",
          )
          parser.add_argument(
              "--batch-size",
              type=int,
              default=None,
              help="Override per_core_batch_size (auto-detected from system check if omitted)",
          )
          parser.add_argument(
              "--skip-check",
              action="store_true",
              help="Skip system preflight check (not recommended)",
          )
          args = parser.parse_args()
      
          # Parse value columns
          value_cols = None
          if args.value_cols:
              value_cols = [c.strip() for c in args.value_cols.split(",")]
      
          # Determine output format
          out_format = args.format
          if not out_format:
              out_format = "json" if args.output.endswith(".json") else "csv"
      
          # 1. Preflight check
          if not args.skip_check:
              print("Running system preflight check...")
              report = run_preflight()
              batch_size = args.batch_size or report.get("recommended_batch_size", 32)
          else:
              print("⚠️ Skipping system check (--skip-check). Proceed with caution.")
              batch_size = args.batch_size or 32
      
          # 2. Load model
          model = load_model(batch_size=batch_size, horizon=args.horizon)
      
          # 3. Load CSV
          df, cols, date_col = load_csv(args.input, args.date_col, value_cols)
      
          # 4. Forecast
          results = forecast_series(model, df, cols, args.horizon)
      
          # 5. Write output
          if out_format == "json":
              write_json_output(results, args.output)
          else:
              write_csv_output(results, args.output, df, date_col, args.horizon)
      
          print("\nDone! 🎉")
      
      
      if __name__ == "__main__":
          main()
      
  • SKILL.md 20.1 KB
    ---
    name: alterlab-timesfm
    description: Forecasts time series zero-shot with Google's TimesFM foundation models — TimesFM 2.5 (200M, Apache-2.0 weights; ForecastConfig API, XReg covariates) and TimesFM 3.0 (~330M, multivariate with native past/future covariates; non-commercial weights) — producing point forecasts and quantile prediction intervals from CSV/DataFrame/array inputs, with a preflight system checker for RAM/GPU. Use when forecasting univariate or multivariate series (sales, sensors, energy, vitals, weather) without training a custom model, batch-forecasting many series, or flagging anomalies against forecast intervals. Part of the AlterLab Academic Skills suite.
    allowed-tools: Read Write Edit Bash
    license: Apache-2.0
    compatibility: No API key required. Runs locally via `uv run python`; requires timesfm >= 2.0 for the TimesFM 2.5 API and >= 3.0 for TimesFM 3.0 (current 3.0.2 as of 2026-09; Python >= 3.10; PyTorch backend, or MLX on Apple silicon). Weights download from Hugging Face on first use (~0.9 GB for 2.5, ~1.3 GB for 3.0); TimesFM 3.0 weights are licensed for non-commercial, non-production use only. GPU optional.
    metadata:
      skill-author: AlterLab
      version: "1.1.0"
      last_updated: "2026-09-23"
    ---
    
    # TimesFM Forecasting
    
    ## Overview
    
    TimesFM (Time Series Foundation Model) is a pretrained decoder-only foundation model
    developed by Google Research for time-series forecasting. It works **zero-shot** — feed it
    a time series and it returns point forecasts with quantile prediction intervals, no
    training required. The `timesfm` package (current 3.0.2) ships two model APIs:
    
    - **TimesFM 2.5** (200M, Apache-2.0 weights) — `timesfm.TimesFM_2p5_200M_torch` with
      `ForecastConfig`; univariate, optional covariates via XReg. The default in this skill and
      the only choice for commercial or production use.
    - **TimesFM 3.0** (~330M, released Aug 2026) — `timesfm3.TimesFM3Forecaster`; univariate
      *and* multivariate forecasting with native past-only and past-and-future covariates.
      Its weights are under `timesfm-non-commercial-license-v1.0`, so use it only for
      non-commercial research and tell the user about the restriction.
    
    This skill wraps TimesFM for safe, agent-friendly local inference. It includes a
    **mandatory preflight system checker** that verifies RAM, GPU memory, and disk space
    before the model is ever loaded so the agent never crashes a user's machine.
    
    > **Key numbers**: TimesFM 2.5 uses 200M parameters (0.93 GB safetensors); TimesFM 3.0 uses
    > ~330M (1.3 GB). Run the system checker before the first load so an under-resourced machine
    > fails fast instead of swapping or crashing.
    
    ## When to Use This Skill
    
    Use this skill when:
    
    - Forecasting **any univariate time series** (sales, demand, sensor, vitals, price, weather)
    - You need **zero-shot forecasting** without training a custom model
    - You want **probabilistic forecasts** with calibrated prediction intervals (quantiles)
    - You have time series of **any length** (the model handles 1–16,384 context points)
    - You need to **batch-forecast** hundreds or thousands of series efficiently
    - You want a **foundation model** approach instead of hand-tuning ARIMA/ETS parameters
    - You have **related channels or known future drivers** (TimesFM 3.0 multivariate + covariates, or TimesFM 2.5 XReg)
    
    ### Does NOT Trigger
    
    | Scenario | Use Instead |
    |----------|-------------|
    | Classical models with interpretable coefficients (ARIMA/SARIMAX tables), VAR, or Granger causality tests | `alterlab-statsmodels` |
    | Time-series classification, clustering, segmentation, or similarity search | `alterlab-aeon` |
    | Exploring a time-series file's structure and quality before any forecasting | `alterlab-eda` |
    | Tabular (non-temporal) prediction | `alterlab-scikit-learn` |
    
    > **Note on Anomaly Detection**: TimesFM does not have built-in anomaly detection, but you can
    > use the **quantile forecasts as prediction intervals** — values outside the 80% CI (q10–q90)
    > are statistically unusual. See the `examples/anomaly-detection/` directory for a full example.
    
    ## Preflight: System Requirements Check
    
    Run the system checker before loading a model for the first time on a machine: loading
    downloads ~1 GB of weights and allocates several GB of RAM, and the checker stops early with
    a clear message instead of letting the load crash or swap.
    
    ```bash
    python scripts/check_system.py
    ```
    
    This script checks:
    
    1. **Available RAM** — warns if below 4 GB, blocks if below 2 GB
    2. **GPU availability** — detects CUDA/MPS devices and VRAM
    3. **Disk space** — verifies room for the ~800 MB model download
    4. **Python version** — requires 3.10+
    5. **Existing installation** — checks if `timesfm` and `torch` are installed
    
    > **Note:** Model weights are **NOT stored in this repository**. TimesFM weights (~800 MB)
    > download on-demand from HuggingFace on first use and cache in `~/.cache/huggingface/`.
    > The preflight checker ensures sufficient resources before any download begins.
    
    ```mermaid
    flowchart TD
        accTitle: Preflight System Check
        accDescr: Decision flowchart showing the system requirement checks that must pass before loading TimesFM.
    
        start["🚀 Run check_system.py"] --> ram{"RAM ≥ 4 GB?"}
        ram -->|"Yes"| gpu{"GPU available?"}
        ram -->|"No (2-4 GB)"| warn_ram["⚠️ Warning: tight RAM<br/>CPU-only, small batches"]
        ram -->|"No (< 2 GB)"| block["🛑 BLOCKED<br/>Insufficient memory"]
        warn_ram --> disk
        gpu -->|"CUDA / MPS"| vram{"VRAM ≥ 2 GB?"}
        gpu -->|"CPU only"| cpu_ok["✅ CPU mode<br/>Slower but works"]
        vram -->|"Yes"| gpu_ok["✅ GPU mode<br/>Fast inference"]
        vram -->|"No"| cpu_ok
        gpu_ok --> disk{"Disk ≥ 2 GB free?"}
        cpu_ok --> disk
        disk -->|"Yes"| ready["✅ READY<br/>Safe to load model"]
        disk -->|"No"| block_disk["🛑 BLOCKED<br/>Need space for weights"]
    
        classDef ok fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
        classDef warn fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12
        classDef block fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#7f1d1d
        classDef neutral fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#1f2937
    
        class ready,gpu_ok,cpu_ok ok
        class warn_ram warn
        class block,block_disk block
        class start,ram,gpu,vram,disk neutral
    ```
    
    ### Hardware Requirements by Model Version
    
    | Model | Parameters | RAM (CPU) | VRAM (GPU) | Disk | Context |
    | ----- | ---------- | --------- | ---------- | ---- | ------- |
    | TimesFM 3.0 (non-commercial weights) | ~330M | ≥ 6 GB | ≥ 4 GB | ~1.3 GB | up to 15,360 |
    | **TimesFM 2.5** (default) | 200M | ≥ 4 GB | ≥ 2 GB | ~0.9 GB | up to 16,384 |
    | TimesFM 2.0 (archived) | 500M | ≥ 16 GB | ≥ 8 GB | ~2 GB | up to 2,048 |
    | TimesFM 1.0 (archived) | 200M | ≥ 8 GB | ≥ 4 GB | ~800 MB | up to 2,048 |
    
    > **Recommendation**: Use TimesFM 2.5 by default (smallest, Apache-2.0, full `ForecastConfig`
    > control). Use TimesFM 3.0 for multivariate targets or native covariates when the
    > non-commercial license fits the project. The 1.0/2.0 checkpoints need `timesfm==1.3.0`
    > and are only worth it for reproducing old results. Measured peak CPU memory for 32 series ×
    > 1,024 context (timesfm 3.0.2): ~2.0 GB for 2.5 and ~2.7 GB for 3.0; the thresholds above
    > leave headroom. Check 3.0 with `python scripts/check_system.py --model v3.0`.
    
    ## 🔧 Installation
    
    ### Step 1: Verify System (always first)
    
    ```bash
    python scripts/check_system.py
    ```
    
    ### Step 2: Install TimesFM
    
    ```bash
    uv pip install "timesfm[torch]"          # TimesFM 2.5 + 3.0, PyTorch backend
    uv pip install "timesfm[torch,xreg]"     # + XReg covariates for TimesFM 2.5 (adds JAX, scikit-learn)
    uv pip install "timesfm[mlx]"            # TimesFM 3.0 on Apple silicon without PyTorch
    uv pip install "timesfm[flax]"           # TimesFM 2.5 JAX/Flax backend
    ```
    
    ### Step 3: Install PyTorch for Your Hardware
    
    ```bash
    # CPU-only wheels (small download, no CUDA libraries)
    uv pip install torch --index-url https://download.pytorch.org/whl/cpu
    
    # NVIDIA GPU: take the CUDA-specific index URL from https://pytorch.org/get-started/locally/
    # Apple Silicon: the default PyPI wheel already includes MPS support
    uv pip install torch
    ```
    
    ### Step 4: Verify Installation
    
    ```python
    from importlib.metadata import version
    import timesfm  # noqa: F401  (import check)
    print(f"TimesFM version: {version('timesfm')}")  # the package defines no __version__
    print("Installation OK")
    ```
    
    ## 🎯 Quick Start
    
    ### Minimal Example (5 Lines)
    
    ```python
    import torch, numpy as np, timesfm
    
    torch.set_float32_matmul_precision("high")
    
    model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
        "google/timesfm-2.5-200m-pytorch"
    )
    model.compile(timesfm.ForecastConfig(
        max_context=1024, max_horizon=256, normalize_inputs=True,
        use_continuous_quantile_head=True, force_flip_invariance=True,
        infer_is_positive=True, fix_quantile_crossing=True,
    ))
    
    point, quantiles = model.forecast(horizon=24, inputs=[
        np.sin(np.linspace(0, 20, 200)),  # any 1-D array
    ])
    # point.shape == (1, 24)        — median forecast
    # quantiles.shape == (1, 24, 10) — 10th–90th percentile bands
    ```
    
    ### TimesFM 3.0 (multivariate, native covariates)
    
    ```python
    import numpy as np
    from timesfm3 import TimesFM3Forecaster  # non-commercial weights — see license note above
    
    forecaster = TimesFM3Forecaster.from_pretrained("google/timesfm-3.0-pytorch", device="cpu")  # or "cuda"
    
    out = forecaster.predict(np.sin(np.linspace(0, 40, 512)).astype(np.float32),
                             horizon=24, return_quantiles=True)
    # out.forecast.shape == (24,)      — median forecast
    # out.quantiles.shape == (24, 9)   — q10..q90; index 4 is the median (no mean column)
    
    # Two target channels plus one known-future covariate (context 256, horizon 32)
    target = np.stack([np.sin(np.linspace(0, 24, 256)), np.cos(np.linspace(0, 24, 256))]).astype(np.float32)
    future_cov = np.sin(np.linspace(0, 30, 256 + 32))[None, :].astype(np.float32)
    out = forecaster.predict(target, horizon=32, past_future_covariates=future_cov, return_quantiles=True)
    # out.forecast.shape == (2, 32); out.quantiles.shape == (2, 32, 9)
    ```
    
    Full parameter list (`predict_batch`, `past_only_covariates`, `make_positive`, …):
    `references/api_reference.md`.
    
    ### Forecast from CSV
    
    ```python
    import pandas as pd, numpy as np
    
    df = pd.read_csv("monthly_sales.csv", parse_dates=["date"], index_col="date")
    
    # Convert each column to a list of arrays
    inputs = [df[col].dropna().values.astype(np.float32) for col in df.columns]
    
    point, quantiles = model.forecast(horizon=12, inputs=inputs)
    
    # Build a results DataFrame
    for i, col in enumerate(df.columns):
        last_date = df[col].dropna().index[-1]
        future_dates = pd.date_range(last_date, periods=13, freq="MS")[1:]
        forecast_df = pd.DataFrame({
            "date": future_dates,
            "forecast": point[i],
            "lower_80": quantiles[i, :, 1],  # q10 — lower bound of 80% PI
            "upper_80": quantiles[i, :, 9],  # q90 — upper bound of 80% PI
        })
        print(f"\n--- {col} ---")
        print(forecast_df.to_string(index=False))
    ```
    
    ### Forecast with Covariates (XReg)
    
    TimesFM 2.5 supports exogenous variables through `forecast_with_covariates()`. It requires
    `timesfm[xreg]` and a model compiled with `return_backcast=True` (otherwise it raises
    `ValueError`). TimesFM 3.0 takes covariates directly in `predict()` (see above).
    
    ```python
    # Requires: uv pip install "timesfm[torch,xreg]"
    model.compile(timesfm.ForecastConfig(
        max_context=1024, max_horizon=256, normalize_inputs=True,
        use_continuous_quantile_head=True, fix_quantile_crossing=True,
        return_backcast=True,
    ))
    point, quantiles = model.forecast_with_covariates(
        inputs=inputs,
        dynamic_numerical_covariates={"price": price_arrays},
        dynamic_categorical_covariates={"holiday": holiday_arrays},
        static_categorical_covariates={"region": region_labels},
        xreg_mode="xreg + timesfm",  # or "timesfm + xreg"
    )
    # point / quantiles: one array per series — (horizon,) and (horizon, 10)
    ```
    
    | Covariate Type | Description | Example |
    | -------------- | ----------- | ------- |
    | `dynamic_numerical` | Time-varying numeric | price, temperature, promotion spend |
    | `dynamic_categorical` | Time-varying categorical | holiday flag, day of week |
    | `static_numerical` | Per-series numeric | store size, account age |
    | `static_categorical` | Per-series categorical | store type, region, product category |
    
    **XReg Modes:**
    - `"xreg + timesfm"` (default): fit an in-context linear regression on the covariates first, then TimesFM forecasts the regression residuals
    - `"timesfm + xreg"`: TimesFM forecasts first, then a linear regression on the covariates fits TimesFM's residuals
    
    > See `examples/covariates-forecasting/` for a complete example with synthetic retail data.
    
    ### Anomaly Detection (via Quantile Intervals)
    
    TimesFM does not have built-in anomaly detection, but the **quantile forecasts naturally provide
    prediction intervals** that can detect anomalies:
    
    ```python
    point, q = model.forecast(horizon=H, inputs=[values])
    
    # 80% prediction interval
    lower_80 = q[0, :, 1]  # 10th percentile
    upper_80 = q[0, :, 9]  # 90th percentile
    
    # Detect anomalies: values outside the 80% CI
    actual = test_values  # your holdout data
    anomalies = (actual < lower_80) | (actual > upper_80)
    
    # Severity levels
    is_warning = (actual < q[0, :, 2]) | (actual > q[0, :, 8])  # outside 60% CI
    is_critical = anomalies  # outside 80% CI
    ```
    
    | Severity | Condition | Interpretation |
    | -------- | --------- | -------------- |
    | **Normal** | Inside 60% CI | Expected behavior |
    | **Warning** | Outside 60% CI | Unusual but possible |
    | **Critical** | Outside 80% CI | Statistically rare (< 20% probability) |
    
    > See `examples/anomaly-detection/` for a complete example with visualization.
    
    ## 📊 Output, Config & Workflows
    
    The output structure and full `ForecastConfig` reference are in
    **[`references/output_and_config.md`](references/output_and_config.md)**.
    
    > **Quantile layout (TimesFM 2.5):** `quantile_forecast` has shape `(batch, horizon, 10)`.
    > Index 0 is the **mean**; q10 = index 1, q50 (median) = index 5, q90 = index 9, so the 80% PI
    > is `q[:,:,1]`–`q[:,:,9]`. **TimesFM 3.0** returns 9 columns (q10–q90) with the median at
    > index 4 — re-check indices when switching models.
    
    Copy-paste workflows (single-series, batch, accuracy evaluation), GPU/memory performance
    tuning, and integration with `statsmodels` / `matplotlib` / EDA are in
    **[`references/workflows.md`](references/workflows.md)**.
    
    
    ## 📚 Scripts
    
    - **`scripts/check_system.py`** — mandatory preflight checker; run before first model load. Reports RAM/GPU/disk/Python/install status and a recommended `per_core_batch_size`.
    - **`scripts/forecast_csv.py`** — end-to-end CSV forecasting with automatic system check:
      ```bash
      python scripts/forecast_csv.py input.csv --horizon 24 \
          --date-col date --value-cols sales,revenue --output forecasts.csv
      ```
    
    ## 📖 Reference Documentation
    
    Detailed guides in `references/`:
    
    | File | Contents |
    | ---- | -------- |
    | [`references/output_and_config.md`](references/output_and_config.md) | Output shapes, quantile index map, full `ForecastConfig` parameter reference |
    | [`references/workflows.md`](references/workflows.md) | Single/batch/eval workflows, GPU & memory tuning, statsmodels/matplotlib/EDA integration |
    | [`references/pitfalls_and_validation.md`](references/pitfalls_and_validation.md) | Common pitfalls, quality checklist, known mistakes, regression-baseline validation |
    | [`references/system_requirements.md`](references/system_requirements.md) | Hardware tiers, GPU/CPU selection, memory estimation formulas |
    | [`references/api_reference.md`](references/api_reference.md) | Full `from_pretrained` options, API surface, output shapes |
    | [`references/data_preparation.md`](references/data_preparation.md) | Input formats, NaN handling, CSV loading, covariate setup |
    
    > **Before declaring any task done**, run the quality checklist and review the common
    > pitfalls/mistakes in [`references/pitfalls_and_validation.md`](references/pitfalls_and_validation.md)
    > — especially the quantile index off-by-one and `infer_is_positive` for negative series.
    
    ## Model Versions
    
    ```mermaid
    timeline
        accTitle: TimesFM Version History
        accDescr: Timeline of TimesFM model releases showing parameter counts and key improvements.
    
        section 2024
            TimesFM 1.0 : 200M params, 2K context, JAX only
            TimesFM 2.0 : 500M params, 2K context, PyTorch + JAX
        section 2025
            TimesFM 2.5 : 200M params, 16K context, quantile head, no frequency indicator
        section 2026
            TimesFM 3.0 : ~330M params, 15K context, multivariate + covariates, non-commercial weights
    ```
    
    | Version | Params | Context | Quantile Head | Frequency Flag | Status |
    | ------- | ------ | ------- | ------------- | -------------- | ------ |
    | 3.0 | ~330M | 15,360 | ✅ 9 deciles | ❌ | Latest (non-commercial weights) |
    | **2.5** | 200M | 16,384 | ✅ Continuous (30M) | ❌ Removed | Default (Apache-2.0) |
    | 2.0 | 500M | 2,048 | ✅ Fixed buckets | ✅ Required | Archived |
    | 1.0 | 200M | 2,048 | ✅ Fixed buckets | ✅ Required | Archived |
    
    **Hugging Face checkpoints:**
    
    - `google/timesfm-3.0-pytorch` (TimesFM 3.0; non-commercial license)
    - `google/timesfm-2.5-200m-pytorch` (default)
    - `google/timesfm-2.5-200m-flax`
    - `google/timesfm-2.5-200m-transformers` (🤗 Transformers port, `TimesFm2_5ModelForPrediction`)
    - `google/timesfm-2.0-500m-pytorch` (archived)
    - `google/timesfm-1.0-200m-pytorch` (archived)
    
    ## Resources
    
    - **Paper**: [A Decoder-Only Foundation Model for Time-Series Forecasting](https://arxiv.org/abs/2310.10688) (ICML 2024)
    - **Repository**: https://github.com/google-research/timesfm
    - **Hugging Face**: https://huggingface.co/collections/google/timesfm-release-66e4be5fdb56e960c1e482a6
    - **Google Blog**: https://research.google/blog/a-decoder-only-foundation-model-for-time-series-forecasting/
    - **BigQuery Integration**: https://cloud.google.com/bigquery/docs/timesfm-model
    
    ## Examples
    
    Three reference examples live in `examples/`; all use the TimesFM 2.5 API (outputs regenerated with timesfm 3.0.2 in 2026-09). Use them as ground truth for correct API usage and expected output shape.
    
    | Example | Directory | What It Demonstrates | When To Use It |
    | ------- | --------- | -------------------- | -------------- |
    | **Global Temperature Forecast** | `examples/global-temperature/` | Basic `model.forecast()` call, CSV -> PNG -> GIF pipeline, 36-month NOAA context, 60%/80% prediction intervals | Starting point; copy-paste baseline for any univariate series |
    | **Anomaly Detection** | `examples/anomaly-detection/` | Two-phase detection: linear detrend + Z-score on context, quantile PI on forecast; 2-panel viz | Any task requiring outlier detection on historical + forecasted data |
    | **Covariates (XReg)** | `examples/covariates-forecasting/` | `forecast_with_covariates()` API (TimesFM 2.5), covariate decomposition, 2x2 shared-axis viz | Retail, energy, or any series with known exogenous drivers |
    
    ### Running the Examples
    
    ```bash
    # Global temperature (TimesFM 2.5)
    cd examples/global-temperature && python run_forecast.py && python visualize_forecast.py
    
    # Anomaly detection (TimesFM 2.5)
    cd examples/anomaly-detection && python detect_anomalies.py
    
    # Covariates (data + API walkthrough; real inference needs timesfm[torch,xreg])
    cd examples/covariates-forecasting && python demo_covariates.py
    ```
    
    ### Expected Outputs
    
    | Example | Key output files | Acceptance criteria |
    | ------- | ---------------- | ------------------- |
    | global-temperature | `output/forecast_output.json`, `output/forecast_visualization.png` | `point_forecast` has 12 values; PNG shows context + forecast + PI bands |
    | anomaly-detection | `output/anomaly_detection.json`, `output/anomaly_detection.png` | Sep 2023 flagged CRITICAL (z >= 3.0); >= 2 forecast CRITICAL from injected anomalies |
    | covariates-forecasting | `output/sales_with_covariates.csv`, `output/covariates_data.png` | CSV has 108 rows (3 stores x 36 weeks); stores have **distinct** price arrays |
    
    ## Quality, Mistakes & Validation
    
    Before declaring any task done, run the post-task **quality checklist**, review the
    **known mistakes** (quantile off-by-one, covariate-horizon coverage, residual-based anomaly
    detection, etc.), and run the **regression-baseline verification** snippets — all in
    **[`references/pitfalls_and_validation.md`](references/pitfalls_and_validation.md)**.
    
    Part of the AlterLab Academic Skills suite.
    
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related