|
Run in Colab Enterprise |
Run in Colab Enterprise: click the badge to import this notebook, pick a runtime, and Run all. The Terraform-deployed templates already carry the
SF_*run identity in their env, so there's no environment cell to fill in. Runs on thesf-mainruntime template (Python 3.11). Seedocs/notebook_runtimes.mdfor the per-notebook template mapping and the headless acceptance harness.
05 · Three-Tier Exogenous Covariates & Global Cross-Series Models¶
Real-world enterprise demand and financial forecasting relies on three distinct tiers of exogenous information (features.py), each with strict causal availability:
- Static Covariates (
features.static_covariates): Time-invariant series metadata (region,category). Target-encoded and one-hot encoded across global panels so cross-series models learn segment-specific elasticities. - Known-Future Covariates (
features.future_covariates): Planned promotions (promo_flag), scheduled pricing (price_index), and calendar holidays known across both historical training and the future forecast horizon $t+1 \dots t+H$. - Observed-Past Covariates (
features.past_covariates): Historical weather (temperature), foot traffic, or macro indicators observed only up to the forecast origin $t$. To prevent future leakage while forecasting step $h \in [1, H]$, the feature engine automatically shiftspast_covariatesbyexog_lags$\ge h$.
At a glance¶
| Dimension | Details |
|---|---|
| Learning outcomes | Configure 3-tier exogenous features (static_covariates, future_covariates, lag-shifted past_covariates), test on_unsupported_covariates policies, and inspect Two-Tier global & per-horizon-step feature attributions. |
| Prerequisites | Deployed scale-forecasting Google Cloud project (SF_* env vars) + scale-forecasting[gcp]. |
| Estimated time | ~5–8 minutes. |
| Estimated run cost | Estimated ~\$0.10–\$0.40 USD (Vertex AI / Compute Engine / BigQuery ML compute for the 20-series panel + BigQuery Storage API). |
| Ongoing cost | Zero compute when idle (ephemeral job compute tears down automatically on completion). |
flowchart TD
subgraph Tiers["Three-Tier Exogenous Feature Contract (features.py)"]
T1["static_covariates
region · category
(Time-invariant metadata)"]
T2["future_covariates
promo_flag · price_index · holidays
(Known in history & future horizon)"]
T3["past_covariates
temperature + exog_lags {temperature: [7, 14]}
(Observed in history only — lag-shifted)"]
end
subgraph Policy["on_unsupported_covariates Policy"]
P1["fallback (default): strip unsupported tiers & log"]
P2["error: fail fast at config validation"]
end
T1 & T2 & T3 --> Policy --> FIT["Global ML (lightgbm · xgboost · catboost)
& Local Exog (sarimax · prophet)"]
Act 1 · Cloud Bootstrap & Deployment Resolution¶
On a managed cloud notebook (Colab Enterprise, Vertex AI Workbench) the bootstrap cell below clones or updates the repository and installs the locked dependency set (uv.lock) into an isolated .colab-deps prefix so import scale_forecasting resolves against the exact package versions every cloud worker uses. In a local clone with .venv, this cell is a fast no-op.
# Cloud bootstrap: clone + install the LOCKED dependency set (uv.lock) so
# `import scale_forecasting` resolves against the exact versions every other surface runs.
import importlib.util
import os
import shutil
import subprocess
import sys
REPO_URL = os.environ.get("SF_REPO_URL", "https://github.com/statmike/scale-forecasting.git")
REPO_DIR = os.environ.get("SF_REPO_DIR", "scale-forecasting")
EXTRAS = ['gcp']
if importlib.util.find_spec("scale_forecasting") is None:
if not os.path.isdir(REPO_DIR):
subprocess.run(["git", "clone", "--depth", "1", REPO_URL, REPO_DIR], check=True)
uv = shutil.which("uv")
if uv is None:
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "uv"], check=True)
uv = shutil.which("uv") or "uv"
reqs = os.path.abspath(os.path.join(REPO_DIR, "colab-requirements.txt"))
extra_flags = [f for e in EXTRAS for f in ("--extra", e)]
subprocess.run(
[uv, "export", "--frozen", "--no-emit-project", "--no-hashes", "--no-dev", *extra_flags, "-o", reqs],
cwd=REPO_DIR,
check=True,
)
target = os.path.abspath(os.path.join(REPO_DIR, ".colab-deps"))
subprocess.run(
[uv, "pip", "install", "--python", sys.executable, "--target", target, "-r", reqs],
check=True,
)
sys.path.insert(0, target)
sys.path.insert(0, os.path.join(REPO_DIR, "src"))
print("installed scale_forecasting from", REPO_DIR)
else:
print("scale_forecasting already importable — skipping bootstrap")
scale_forecasting already importable — skipping bootstrap
Resolve the Live GCP Deployment (Settings.resolve())¶
Settings.resolve() reads the SF_* environment variables (SF_PROJECT_ID, SF_REGION, SF_DATASET_ID, SF_WAREHOUSE_URI, SF_CONNECTION, SF_COMPUTE_SA) pre-populated by Terraform in the sf-main runtime template (or your local shell).
from google.cloud import bigquery
from scale_forecasting.settings import Settings
settings = Settings.resolve()
client = bigquery.Client(project=settings.project_id)
DATASET = settings.dataset_ref
print(f"Deployment: {DATASET} | Region: {settings.region} | Bucket: {settings.warehouse_uri}")
Deployment: statmike-scale-forecasting.scale_forecasting | Region: us-central1 | Bucket: gs://statmike-scale-forecasting-warehouse/warehouse
Act 2 · Inspect Model Covariate Capabilities & Configure a 3-Tier Global Run¶
First, let's filter model_catalog() to inspect which models support future_covariates, past_covariates, static_covariates, and global_mode.
from scale_forecasting import Forecaster, RunConfig, model_catalog
cat = model_catalog()
display(cat[cat["future_covariates"] | cat["past_covariates"] | cat["static_covariates"]][[
"model", "family", "future_covariates", "past_covariates", "static_covariates", "explainability", "global_mode", "hybrid_mode"
]])
| model | family | future_covariates | past_covariates | static_covariates | explainability | global_mode | hybrid_mode | |
|---|---|---|---|---|---|---|---|---|
| 4 | tft | deep_learning | True | True | True | False | True | False |
| 5 | tide | deep_learning | True | True | True | False | True | False |
| 6 | tsmixer | deep_learning | True | True | True | False | True | False |
| 7 | catboost | ml | True | True | False | True | False | False |
| 8 | lightgbm | ml | True | True | False | True | False | False |
| 9 | random_forest | ml | True | True | False | True | False | False |
| 10 | regression_lags | ml | True | True | False | True | False | False |
| 11 | xgboost | ml | True | True | False | True | False | False |
| 12 | auto_arima | statistical | True | True | False | False | False | False |
| 19 | kalman | statistical | True | True | False | False | False | False |
| 24 | prophet | statistical | True | True | False | False | False | False |
| 25 | sarimax | statistical | True | True | False | False | False | False |
| 29 | ucm | statistical | True | True | False | False | False | False |
| 30 | vertex_l2l | automl | True | True | True | True | True | False |
| 31 | vertex_seq2seq | automl | True | True | True | True | True | False |
| 32 | vertex_tft | automl | True | True | True | True | True | False |
| 33 | vertex_tide | automl | True | True | True | True | True | False |
import time
# === Parameters — edit me ===============================================
RUN_NAME = f"nb05 covariates global {int(time.time())}"
SOURCE_TABLE = "source_series_covariates_native" # or "source_series_covariates_iceberg"
MODELS = ["tide", "tsmixer", "lightgbm", "xgboost", "theta"]
HORIZON = 14
SERIES_LIMIT = 20
# ========================================================================
cfg = RunConfig(
run_name=RUN_NAME,
python_runtime="vertex",
data={
"source_table": SOURCE_TABLE,
"horizon": HORIZON,
"series_limit": SERIES_LIMIT,
},
models=MODELS,
model_params={
"tide": {"training_mode": "global", "max_epochs": 15},
"tsmixer": {"training_mode": "global", "max_epochs": 15},
},
compute={
"families": {
"deep_learning": {"runtime": "vertex", "hardware": "cpu"},
"ml": {"runtime": "vertex"},
"statistical": {"runtime": "vertex"},
}
},
features={
"holidays": ["US"],
"static_covariates": ["region", "category"],
"future_covariates": ["promo_flag", "price_index"],
"past_covariates": ["temperature"],
"exog_lags": {"promo_flag": [1, 7], "temperature": [7, 14]},
"on_unsupported_covariates": "fallback",
},
backtest={"enabled": True, "n_folds": 2, "horizon": HORIZON, "step": HORIZON},
ensemble={"enabled": True, "strategies": ["inverse_error", "nnls"]},
)
forecaster = Forecaster(cfg, settings=settings)
print("run_id:", forecaster.run_id)
display(forecaster.explain(validate_data=False))
run_id: nb05-covariates-global-1791391135-dfcbfde016ec
| run_id | family | job_key | runtime | machine_type | workers | hardware | gpu_type | models | training_modes | n_series | n_folds | expected_cells | covariates | depends_on | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | nb05-covariates-global-1791391135-dfcbfde016ec | statistical | sf-nb05-covariates-global-1791391135-dfcbfde01... | vertex | n2-standard-8 | 1 | cpu | None | theta | theta:local | 20 | 2 | 20 | future(2), past(1), static(2) | |
| 1 | nb05-covariates-global-1791391135-dfcbfde016ec | ml | sf-nb05-covariates-global-1791391135-dfcbfde01... | vertex | n2-standard-8 | 1 | cpu | None | lightgbm, xgboost | lightgbm:local, xgboost:local | 20 | 2 | 40 | future(2), past(1), static(2) | |
| 2 | nb05-covariates-global-1791391135-dfcbfde016ec | deep_learning | sf-nb05-covariates-global-1791391135-dfcbfde01... | vertex | n2-standard-8 | 2 | cpu | None | tide, tsmixer | tide:global, tsmixer:global | 20 | 2 | 40 | future(2), past(1), static(2) | |
| 3 | nb05-covariates-global-1791391135-dfcbfde016ec | ensemble | sf-nb05-covariates-global-1791391135-dfcbfde01... | bigquery | sql | 1 | sql | None | ensemble_inverse_error, ensemble_nnls | barrier | 20 | 2 | 40 | future(2), past(1), static(2) | sf-nb05-covariates-global-1791391135-dfcbfde01... |
Act 3 · Launch & Monitor Live (forecaster.run_live())¶
Notice in the explain() table above that:
- The
deep_learningfamily (tide,tsmixer) runs inglobalcross-series mode using all three covariate tiers (future,past,static). - The
mlfamily (lightgbm,xgboost) runs inlocalmode usingfuture_covariatesand lag-shiftedpast_covariates. - The
statisticalfamily (theta) automatically strips unsupported covariates underon_unsupported_covariates='fallback'.
result = forecaster.run_live(poll_s=15.0, probe=True)
print(f"Completed {result.run_id}: status={result.status}, runtime={result.runtime_seconds:.1f}s")
2026-10-07 16:38:55,890 WARNING scale_forecasting.dag: model 'lightgbm' does not support configured covariate tier(s) ['static_covariates'] and will fall back to its supported tiers ['future_covariates', 'past_covariates'] (set features.on_unsupported_covariates='error' to refuse instead).
2026-10-07 16:38:55,892 WARNING scale_forecasting.dag: model 'xgboost' does not support configured covariate tier(s) ['static_covariates'] and will fall back to its supported tiers ['future_covariates', 'past_covariates'] (set features.on_unsupported_covariates='error' to refuse instead).
2026-10-07 16:38:55,894 WARNING scale_forecasting.dag: model 'theta' does not support configured covariate tier(s) ['future_covariates', 'past_covariates', 'static_covariates'] and will fall back to univariate forecasting (set features.on_unsupported_covariates='error' to refuse instead).
Completed nb05-covariates-global-1791391135-dfcbfde016ec: status=COMPLETED, runtime=456.2s
Act 4 · Review Leaderboard, Interval Calibration, Two-Tier Feature Attributions & Structural Decomposition¶
Let's compare the global and covariate-aware models against the univariate baseline on the leaderboard, inspect conformal interval calibration by horizon step (forecaster.plot_calibration()), examine Two-Tier Feature Attributions (forecaster.attributions_df(model_type="lightgbm") and forecaster.plot_attributions(model_type="lightgbm") — Tier 1 global feature importance and Tier 2 per-horizon-step TreeSHAP attributions across recursive target lags, calendar features, and exogenous covariates), and decompose the lightgbm forecast into its structural trend, seasonality, and exogenous covariate contributions (promo_flag, price_index, temperature) via forecaster.plot_forecast_explanation().
import matplotlib.pyplot as plt
from scale_forecasting import plot_leaderboard, plot_metric_distribution
display(forecaster.leaderboard_df())
review = forecaster.review_run()
plot_leaderboard(review)
plt.show()
plot_metric_distribution(review)
plt.show()
forecaster.plot_forecasts(history_tail=90)
plt.show()
forecaster.plot_calibration()
plt.show()
print("--- Two-Tier TreeSHAP Feature Attributions (forecaster.attributions_df & plot_attributions) ---")
attr_df = forecaster.attributions_df(model_type="lightgbm", level="global")
display(attr_df.head(10))
forecaster.plot_attributions(model_type="lightgbm", level="global", title="Tier 1 Global TreeSHAP Importance (lightgbm)")
plt.show()
forecaster.plot_attributions(model_type="lightgbm", level="local", title="Tier 2 Local Per-Step TreeSHAP Attributions (lightgbm)")
plt.show()
exp_df = forecaster.explain_forecast(model_type="lightgbm")
display(exp_df.tail(10))
forecaster.plot_forecast_explanation(model_type="lightgbm", history_tail=90)
plt.show()
| rank | model_type | family | is_ensemble | ensemble_id | compute_engine | n_series | decision_metric | score | pooled_wape | ... | mean_mae | mean_rmse | mean_coverage | mean_interval_score | mean_fit_seconds | median_fit_seconds | no_artifact_rate | n_predictions | lift_vs_best_base | lift_pct | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | ensemble_inverse_error | ensemble | True | 34432fdde9fa | ensemble | 20 | wape | 0.406179 | 0.137957 | ... | 12.511065 | 17.542903 | NaN | NaN | NaN | NaN | 1.0 | 280 | 0.024027 | 0.055851 |
| 1 | 2 | theta | statistical | False | None | vertex | 20 | wape | 0.430206 | 0.155773 | ... | 14.755309 | 19.577359 | 0.991071 | 170.476561 | 4.247834 | 2.654362 | 1.0 | 280 | NaN | NaN |
| 2 | 3 | ensemble_nnls | ensemble | True | 34432fdde9fa | ensemble | 20 | wape | 0.432475 | 0.145220 | ... | 12.810496 | 18.003265 | NaN | NaN | NaN | NaN | 0.0 | 280 | -0.002269 | -0.005274 |
| 3 | 4 | tsmixer | deep_learning | False | None | vertex | 20 | wape | 0.441231 | 0.214435 | ... | 20.504270 | 26.229074 | 0.816071 | 110.581714 | 1.507077 | 1.507077 | 1.0 | 280 | NaN | NaN |
| 4 | 5 | lightgbm | ml | False | None | vertex | 20 | wape | 0.444321 | 0.144800 | ... | 13.657062 | 18.708318 | 0.237500 | 104.295213 | 4.469882 | 2.508540 | 1.0 | 280 | NaN | NaN |
| 5 | 6 | xgboost | ml | False | None | vertex | 20 | wape | 0.446640 | 0.151259 | ... | 13.162259 | 18.580552 | 0.164286 | 108.298528 | 4.192350 | 4.344461 | 1.0 | 280 | NaN | NaN |
| 6 | 7 | tide | deep_learning | False | None | vertex | 20 | wape | 0.461806 | 0.169715 | ... | 15.458432 | 20.967358 | 0.841071 | 94.894177 | 2.581071 | 2.581071 | 1.0 | 280 | NaN | NaN |
7 rows × 25 columns
--- Two-Tier TreeSHAP Feature Attributions (forecaster.attributions_df & plot_attributions) ---
| ts_id | model_type | compute_engine | feature | importance | |
|---|---|---|---|---|---|
| 0 | s_000000 | lightgbm | vertex | lag_7 | 0.117444 |
| 1 | s_000000 | lightgbm | vertex | lag_1 | 0.099000 |
| 2 | s_000000 | lightgbm | vertex | doy | 0.088111 |
| 3 | s_000000 | lightgbm | vertex | temperature_lag_7 | 0.081000 |
| 4 | s_000000 | lightgbm | vertex | lag_14 | 0.077333 |
| 5 | s_000000 | lightgbm | vertex | lag_3 | 0.075667 |
| 6 | s_000000 | lightgbm | vertex | temperature_lag_14 | 0.074889 |
| 7 | s_000000 | lightgbm | vertex | lag_28 | 0.072556 |
| 8 | s_000000 | lightgbm | vertex | price_index | 0.071111 |
| 9 | s_000000 | lightgbm | vertex | temperature | 0.069111 |
| ts_id | model_type | segment | ds | y_true | yhat | yhat_lower | yhat_upper | trend_baseline | level_shift | seasonal_effect | covariate_effect | oof_residual | interval_width | cov_promo_flag | cov_price_index | cov_temperature | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 124 | s_000000 | lightgbm | forecast | 2025-01-04 | NaN | 275.075040 | 244.507923 | 287.328154 | 249.533191 | 72.779333 | 32.545727 | -3.705789 | NaN | 42.820230 | -0.669871 | -1.338342 | -1.697576 |
| 125 | s_000000 | lightgbm | forecast | 2025-01-05 | NaN | 278.684157 | 247.957379 | 301.089794 | 244.116069 | 72.779333 | 39.063253 | -3.705789 | NaN | 53.132415 | -0.669871 | -1.338342 | -1.697576 |
| 126 | s_000000 | lightgbm | forecast | 2025-01-06 | NaN | 259.804869 | 229.611275 | 282.743690 | 245.970480 | 72.779333 | 20.402427 | -3.705789 | NaN | 53.132415 | -0.669871 | -1.338342 | -1.697576 |
| 127 | s_000000 | lightgbm | forecast | 2025-01-07 | NaN | 223.414274 | 198.289640 | 247.311521 | 248.326396 | 72.779333 | -19.018635 | -3.705789 | NaN | 49.021881 | -0.669871 | -1.338342 | -1.697576 |
| 128 | s_000000 | lightgbm | forecast | 2025-01-08 | NaN | 228.002887 | 202.878253 | 251.900134 | 250.304848 | 72.779333 | -40.607435 | -3.705789 | NaN | 49.021881 | -0.669871 | -1.338342 | -1.697576 |
| 129 | s_000000 | lightgbm | forecast | 2025-01-09 | NaN | 217.971142 | 202.830404 | 241.970415 | 250.507421 | 72.779333 | -35.352640 | -3.705789 | NaN | 39.140011 | -0.669871 | -1.338342 | -1.697576 |
| 130 | s_000000 | lightgbm | forecast | 2025-01-10 | NaN | 255.332401 | 240.918954 | 280.058965 | 250.267611 | 72.779333 | 2.967304 | -3.705789 | NaN | 39.140011 | -0.669871 | -1.338342 | -1.697576 |
| 131 | s_000000 | lightgbm | forecast | 2025-01-11 | NaN | 288.924208 | 274.510762 | 313.650773 | 254.743168 | 72.779333 | 32.545727 | -3.705789 | NaN | 39.140011 | -0.669871 | -1.338342 | -1.697576 |
| 132 | s_000000 | lightgbm | forecast | 2025-01-12 | NaN | 280.102168 | 265.688722 | 304.828732 | 260.091224 | 72.779333 | 39.063253 | -3.705789 | NaN | 39.140011 | -0.669871 | -1.338342 | -1.697576 |
| 133 | s_000000 | lightgbm | forecast | 2025-01-13 | NaN | 258.126200 | 243.712753 | 282.852764 | 270.621244 | 72.779333 | 20.402427 | -3.705789 | NaN | 39.140011 | -0.669871 | -1.338342 | -1.697576 |
Where next¶
- Next in Track 3 (Accuracy & Post-Processing):
06_hierarchical_reconciliation.ipynb— reconcile forecasts acrosstotal -> region -> category -> itemhierarchies with all 7 Hyndman FPP3 estimators. - Documentation: Configuration reference (
features) · Models & explainability reference · Cost estimates & controls