fix(examples): correct quantile indices, variable shadowing, and test design in anomaly + covariates examples

Anomaly detection fixes:
- Fix critical quantile index bug: index 0 is mean not q10; correct indices are q10=1, q20=2, q80=8, q90=9
- Redesign test: use all 36 months as context, inject 3 synthetic anomalies into future
- Result: 3 CRITICAL detected (was 11/12 — caused by test-set leakage + wrong indices)
- Update severity labels: CRITICAL = outside 80% PI, WARNING = outside 60% PI

Covariates fixes:
- Fix variable-shadowing bug: inner dict comprehension overwrote outer loop store_id
  causing all stores to get identical covariate arrays (store_A's price for everyone)
- Give each store a distinct price baseline (premium $12, standard $10, discount $7.50)
- Trim CONTEXT_LEN from 48 → 24 weeks; CSV now 108 rows (was 180)
- Add NOTE ON REAL DATA comment: temp file pattern for large external datasets

Both scripts regenerated with clean outputs.
This commit is contained in:
Clayton Young
2026-02-21 18:27:45 -05:00
parent 0d98fa353c
commit 509190118f
7 changed files with 612 additions and 697 deletions

View File

@@ -2,14 +2,19 @@
""" """
TimesFM Anomaly Detection Example TimesFM Anomaly Detection Example
This example demonstrates how to use TimesFM's quantile forecasts for Demonstrates using TimesFM quantile forecasts as prediction intervals
anomaly detection. The approach: for anomaly detection. Approach:
1. Forecast with quantile intervals (10th-90th percentiles) 1. Use 36 months of real data as context
2. Compare actual values against prediction intervals 2. Create synthetic 12-month future (natural continuation of trend)
3. Flag values outside intervals as anomalies 3. Inject 3 clear anomalies into that future
4. Forecast with quantile intervals → flag anomalies by severity
TimesFM does NOT have built-in anomaly detection, but the quantile TimesFM has NO built-in anomaly detection. Quantile forecasts provide
forecasts provide natural anomaly detection via prediction intervals. natural prediction intervals — values outside them are statistically unusual.
Quantile index reference (index 0 = mean, 1-9 = q10-q90):
80% PI = q10 (idx 1) to q90 (idx 9)
60% PI = q20 (idx 2) to q80 (idx 8)
""" """
from __future__ import annotations from __future__ import annotations
@@ -18,36 +23,51 @@ import json
from pathlib import Path from pathlib import Path
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import timesfm import timesfm
# Configuration # Configuration
HORIZON = 12 # Forecast horizon HORIZON = 12 # Forecast horizon (months)
ANOMALY_THRESHOLD_WARNING = 0.80 # Outside 80% CI = warning
ANOMALY_THRESHOLD_CRITICAL = 0.90 # Outside 90% CI = critical
EXAMPLE_DIR = Path(__file__).parent
DATA_FILE = ( DATA_FILE = (
Path(__file__).parent.parent / "global-temperature" / "temperature_anomaly.csv" Path(__file__).parent.parent / "global-temperature" / "temperature_anomaly.csv"
) )
OUTPUT_DIR = EXAMPLE_DIR / "output" OUTPUT_DIR = Path(__file__).parent / "output"
# Anomaly thresholds using available quantile outputs
# 80% PI = q10-q90 → "critical" if outside
# 60% PI = q20-q80 → "warning" if outside
IDX_Q10, IDX_Q20, IDX_Q80, IDX_Q90 = 1, 2, 8, 9
def inject_anomalies( def build_synthetic_future(
values: np.ndarray, n_anomalies: int = 3, seed: int = 42 context: np.ndarray, n: int, seed: int = 42
) -> tuple[np.ndarray, list[int]]: ) -> tuple[np.ndarray, list[int]]:
"""Inject synthetic anomalies into the data for demonstration.""" """Build synthetic future that looks like a natural continuation.
Takes the mean/std of the last 6 context months as the baseline,
then injects 3 clear anomalies (2 high, 1 low) at fixed positions.
"""
rng = np.random.default_rng(seed) rng = np.random.default_rng(seed)
anomaly_indices = rng.choice(len(values), size=n_anomalies, replace=False).tolist() recent_mean = float(context[-6:].mean())
recent_std = float(context[-6:].std())
anomalous_values = values.copy() # Natural-looking continuation: small gaussian noise around recent mean
for idx in anomaly_indices: future = recent_mean + rng.normal(0, recent_std * 0.4, n).astype(np.float32)
# Inject spike or dip (±40-60% of value)
multiplier = rng.choice([0.4, 0.6]) * rng.choice([1, -1])
anomalous_values[idx] = values[idx] * (1 + multiplier)
return anomalous_values, sorted(anomaly_indices) # Inject 3 unmistakable anomalies
anomaly_cfg = [
(2, +0.55), # month 3 — large spike up
(7, -0.50), # month 8 — large dip down
(10, +0.48), # month 11 — spike up
]
anomaly_indices = []
for idx, delta in anomaly_cfg:
future[idx] = recent_mean + delta
anomaly_indices.append(idx)
return future, sorted(anomaly_indices)
def main() -> None: def main() -> None:
@@ -57,27 +77,30 @@ def main() -> None:
OUTPUT_DIR.mkdir(exist_ok=True) OUTPUT_DIR.mkdir(exist_ok=True)
# Load temperature data # ── Load all 36 months as context ─────────────────────────────
print("\n📊 Loading temperature anomaly data...") print("\n📊 Loading temperature data (all 36 months as context)...")
df = pd.read_csv(DATA_FILE, parse_dates=["date"]) df = pd.read_csv(DATA_FILE, parse_dates=["date"])
df = df.sort_values("date").reset_index(drop=True) df = df.sort_values("date").reset_index(drop=True)
context_values = df["anomaly_c"].values.astype(np.float32) # all 36 months
context_dates = df["date"].tolist()
# Split into context (first 24 months) and test (last 12 months) print(
context_values = df["anomaly_c"].values[:24].astype(np.float32) f" Context: {len(context_values)} months ({context_dates[0].strftime('%Y-%m')}{context_dates[-1].strftime('%Y-%m')})"
actual_future = df["anomaly_c"].values[24:36].astype(np.float32)
dates_future = df["date"].values[24:36]
print(f" Context: 24 months (2022-01 to 2023-12)")
print(f" Test: 12 months (2024-01 to 2024-12)")
# Inject anomalies into test data for demonstration
print("\n🔬 Injecting synthetic anomalies for demonstration...")
test_values_with_anomalies, anomaly_indices = inject_anomalies(
actual_future, n_anomalies=3
) )
print(f" Injected anomalies at months: {anomaly_indices}")
# Load TimesFM # ── Build synthetic future with known anomalies ────────────────
print("\n🔬 Building synthetic 12-month future with injected anomalies...")
future_values, injected_at = build_synthetic_future(context_values, HORIZON)
future_dates = pd.date_range(
start=context_dates[-1] + pd.DateOffset(months=1),
periods=HORIZON,
freq="MS",
)
print(
f" Anomalies injected at months: {[future_dates[i].strftime('%Y-%m') for i in injected_at]}"
)
# ── Load TimesFM and forecast ──────────────────────────────────
print("\n🤖 Loading TimesFM 1.0 (200M) PyTorch...") print("\n🤖 Loading TimesFM 1.0 (200M) PyTorch...")
hparams = timesfm.TimesFmHparams(horizon_len=HORIZON) hparams = timesfm.TimesFmHparams(horizon_len=HORIZON)
checkpoint = timesfm.TimesFmCheckpoint( checkpoint = timesfm.TimesFmCheckpoint(
@@ -85,254 +108,186 @@ def main() -> None:
) )
model = timesfm.TimesFm(hparams=hparams, checkpoint=checkpoint) model = timesfm.TimesFm(hparams=hparams, checkpoint=checkpoint)
# Forecast with quantiles print("\n📈 Forecasting...")
print("\n📈 Forecasting with quantile intervals...") point_fc, quant_fc = model.forecast([context_values], freq=[0])
point_forecast, quantile_forecast = model.forecast(
[context_values],
freq=[0],
)
# Extract quantiles # quantile_forecast shape: (1, horizon, 10)
# quantile_forecast shape: (1, 12, 10) - [mean, q10, q20, ..., q90] # index 0 = mean, index 1 = q10, ..., index 9 = q90
point = point_forecast[0] point = point_fc[0] # shape (12,)
q10 = quantile_forecast[0, :, 0] # 10th percentile q10 = quant_fc[0, :, IDX_Q10] # 10th pct
q20 = quantile_forecast[0, :, 1] # 20th percentile q20 = quant_fc[0, :, IDX_Q20] # 20th pct
q50 = quantile_forecast[0, :, 4] # 50th percentile (median) q80 = quant_fc[0, :, IDX_Q80] # 80th pct
q80 = quantile_forecast[0, :, 7] # 80th percentile q90 = quant_fc[0, :, IDX_Q90] # 90th pct
q90 = quantile_forecast[0, :, 8] # 90th percentile
print(f" Forecast mean: {point.mean():.3f}°C") print(f" Forecast mean: {point.mean():.3f}°C")
print(f" 90% CI width: {(q90 - q10).mean():.3f}°C (avg)") print(f" 80% PI width: {(q90 - q10).mean():.3f}°C (avg)")
# Detect anomalies # ── Detect anomalies ───────────────────────────────────────────
print("\n🔍 Detecting anomalies...") print("\n🔍 Detecting anomalies...")
anomalies = [] records = []
for i, (actual, lower_80, upper_80, lower_90, upper_90) in enumerate( for i, (actual, fcast, lo60, hi60, lo80, hi80) in enumerate(
zip(test_values_with_anomalies, q20, q80, q10, q90) zip(future_values, point, q20, q80, q10, q90)
): ):
month = dates_future[i] month = future_dates[i].strftime("%Y-%m")
month_str = pd.to_datetime(month).strftime("%Y-%m")
if actual < lower_90 or actual > upper_90: if actual < lo80 or actual > hi80:
severity = "CRITICAL" severity = "CRITICAL" # outside 80% PI
threshold = "90% CI" elif actual < lo60 or actual > hi60:
color = "red" severity = "WARNING" # outside 60% PI
elif actual < lower_80 or actual > upper_80:
severity = "WARNING"
threshold = "80% CI"
color = "orange"
else: else:
severity = "NORMAL" severity = "NORMAL"
threshold = "within bounds"
color = "green"
anomalies.append( records.append(
{ {
"month": month_str, "month": month,
"actual": float(actual), "actual": round(float(actual), 4),
"forecast": float(point[i]), "forecast": round(float(fcast), 4),
"lower_80": float(lower_80), "lower_60pi": round(float(lo60), 4),
"upper_80": float(upper_80), "upper_60pi": round(float(hi60), 4),
"lower_90": float(lower_90), "lower_80pi": round(float(lo80), 4),
"upper_90": float(upper_90), "upper_80pi": round(float(hi80), 4),
"severity": severity, "severity": severity,
"threshold": threshold, "injected": (i in injected_at),
"color": color,
} }
) )
if severity != "NORMAL": if severity != "NORMAL":
deviation = abs(actual - point[i]) dev = actual - fcast
print( print(
f" [{severity}] {month_str}: {actual:.2f}°C (forecast: {point[i]:.2f}°C, deviation: {deviation:.2f}°C)" f" [{severity}] {month}: actual={actual:.2f} forecast={fcast:.2f} Δ={dev:+.2f}°C"
) )
# Create visualization # ── Visualise ─────────────────────────────────────────────────
print("\n📊 Creating anomaly visualization...") print("\n📊 Creating visualization...")
fig, axes = plt.subplots(2, 1, figsize=(14, 10)) fig, axes = plt.subplots(2, 1, figsize=(13, 9))
# Plot 1: Full time series with forecast and anomalies clr = {"CRITICAL": "red", "WARNING": "orange", "NORMAL": "steelblue"}
ax1 = axes[0]
# Historical data # — Panel 1: full series ———————————————————————————————————————
historical_dates = df["date"].values[:24] ax = axes[0]
ax1.plot( ax.plot(
historical_dates, context_dates,
context_values, context_values,
"b-", "b-",
linewidth=2, lw=2,
label="Historical Data",
marker="o", marker="o",
markersize=4, ms=4,
label="Context (36 months)",
) )
ax.fill_between(
# Actual future (with anomalies) future_dates, q10, q90, alpha=0.18, color="tomato", label="80% PI (q10q90)"
ax1.plot( )
dates_future, ax.fill_between(
actual_future, future_dates, q20, q80, alpha=0.28, color="tomato", label="60% PI (q20q80)"
"g--", )
linewidth=1.5, ax.plot(future_dates, point, "r-", lw=2, marker="s", ms=5, label="Forecast")
label="Actual (clean)", ax.plot(
future_dates,
future_values,
"k--",
lw=1.3,
alpha=0.5, alpha=0.5,
) label="Synthetic future (clean)",
ax1.plot(
dates_future,
test_values_with_anomalies,
"ko",
markersize=8,
label="Actual (with anomalies)",
alpha=0.7,
) )
# Forecast # mark anomalies
ax1.plot( for rec in records:
dates_future, if rec["severity"] != "NORMAL":
point, dt = pd.to_datetime(rec["month"])
"r-", c = "red" if rec["severity"] == "CRITICAL" else "orange"
linewidth=2, mk = "X" if rec["severity"] == "CRITICAL" else "^"
label="Forecast (median)", ax.scatter(
marker="s", [dt], [rec["actual"]], c=c, s=220, marker=mk, zorder=6, linewidths=2
markersize=6,
)
# 90% CI
ax1.fill_between(dates_future, q10, q90, alpha=0.2, color="red", label="90% CI")
# 80% CI
ax1.fill_between(dates_future, q20, q80, alpha=0.3, color="red", label="80% CI")
# Highlight anomalies
for anomaly in anomalies:
if anomaly["severity"] != "NORMAL":
idx = [pd.to_datetime(d).strftime("%Y-%m") for d in dates_future].index(
anomaly["month"]
)
ax1.scatter(
[dates_future[idx]],
[test_values_with_anomalies[idx]],
c=anomaly["color"],
s=200,
marker="x" if anomaly["severity"] == "CRITICAL" else "^",
linewidths=3,
zorder=5,
) )
ax1.set_xlabel("Date", fontsize=12) ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
ax1.set_ylabel("Temperature Anomaly (°C)", fontsize=12) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
ax1.set_title( plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha="right")
"TimesFM Anomaly Detection: Forecast Intervals Method", ax.set_ylabel("Temperature Anomaly (°C)", fontsize=11)
fontsize=14, ax.set_title(
"TimesFM Anomaly Detection — Prediction Interval Method",
fontsize=13,
fontweight="bold", fontweight="bold",
) )
ax1.legend(loc="upper left", fontsize=10) ax.legend(loc="upper left", fontsize=9, ncol=2)
ax1.grid(True, alpha=0.3) ax.grid(True, alpha=0.25)
ax.annotate(
# Add annotation for anomalies "X = Critical (outside 80% PI)\n▲ = Warning (outside 60% PI)",
ax1.annotate( xy=(0.98, 0.04),
"× = Critical (outside 90% CI)\n▲ = Warning (outside 80% CI)",
xy=(0.98, 0.02),
xycoords="axes fraction", xycoords="axes fraction",
ha="right", ha="right",
va="bottom", fontsize=9,
fontsize=10,
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8), bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
) )
# Plot 2: Deviation from forecast with thresholds # — Panel 2: deviation bars ———————————————————————————————————
ax2 = axes[1] ax2 = axes[1]
deviations = future_values - point
lo80_dev = q10 - point
hi80_dev = q90 - point
lo60_dev = q20 - point
hi60_dev = q80 - point
x = np.arange(HORIZON)
deviation = test_values_with_anomalies - point ax2.fill_between(x, lo80_dev, hi80_dev, alpha=0.15, color="tomato", label="80% PI")
lower_90_dev = q10 - point ax2.fill_between(x, lo60_dev, hi60_dev, alpha=0.25, color="tomato", label="60% PI")
upper_90_dev = q90 - point bar_colors = [clr[r["severity"]] for r in records]
lower_80_dev = q20 - point ax2.bar(x, deviations, color=bar_colors, alpha=0.75, edgecolor="black", lw=0.5)
upper_80_dev = q80 - point ax2.axhline(0, color="black", lw=1)
months = [pd.to_datetime(d).strftime("%Y-%m") for d in dates_future] ax2.set_xticks(x)
x = np.arange(len(months)) ax2.set_xticklabels(
[r["month"] for r in records], rotation=45, ha="right", fontsize=9
# Threshold bands
ax2.fill_between(
x, lower_90_dev, upper_90_dev, alpha=0.2, color="red", label="90% CI bounds"
) )
ax2.fill_between( ax2.set_ylabel("Δ from Forecast (°C)", fontsize=11)
x, lower_80_dev, upper_80_dev, alpha=0.3, color="red", label="80% CI bounds"
)
# Deviation bars
colors = [
"red"
if d < lower_90_dev[i] or d > upper_90_dev[i]
else "orange"
if d < lower_80_dev[i] or d > upper_80_dev[i]
else "green"
for i, d in enumerate(deviation)
]
ax2.bar(x, deviation, color=colors, alpha=0.7, edgecolor="black", linewidth=0.5)
# Zero line
ax2.axhline(y=0, color="black", linestyle="-", linewidth=1)
ax2.set_xlabel("Month", fontsize=12)
ax2.set_ylabel("Deviation from Forecast (°C)", fontsize=12)
ax2.set_title( ax2.set_title(
"Deviation from Forecast with Anomaly Thresholds", "Deviation from Forecast with Anomaly Thresholds",
fontsize=14, fontsize=13,
fontweight="bold", fontweight="bold",
) )
ax2.set_xticks(x) ax2.legend(loc="upper right", fontsize=9)
ax2.set_xticklabels(months, rotation=45, ha="right") ax2.grid(True, alpha=0.25, axis="y")
ax2.legend(loc="upper right", fontsize=10)
ax2.grid(True, alpha=0.3, axis="y")
plt.tight_layout() plt.tight_layout()
png_path = OUTPUT_DIR / "anomaly_detection.png"
output_path = OUTPUT_DIR / "anomaly_detection.png" plt.savefig(png_path, dpi=150, bbox_inches="tight")
plt.savefig(output_path, dpi=150, bbox_inches="tight")
print(f" Saved: {output_path}")
plt.close() plt.close()
print(f" Saved: {png_path}")
# Save results # ── Save JSON results ──────────────────────────────────────────
results = { summary = {
"method": "quantile_intervals", "total": len(records),
"description": "Anomaly detection using TimesFM quantile forecasts as prediction intervals", "critical": sum(1 for r in records if r["severity"] == "CRITICAL"),
"thresholds": { "warning": sum(1 for r in records if r["severity"] == "WARNING"),
"warning": f"Outside {ANOMALY_THRESHOLD_WARNING * 100:.0f}% CI (q20-q80)", "normal": sum(1 for r in records if r["severity"] == "NORMAL"),
"critical": f"Outside {ANOMALY_THRESHOLD_CRITICAL * 100:.0f}% CI (q10-q90)",
},
"anomalies": anomalies,
"summary": {
"total_points": len(anomalies),
"critical": sum(1 for a in anomalies if a["severity"] == "CRITICAL"),
"warning": sum(1 for a in anomalies if a["severity"] == "WARNING"),
"normal": sum(1 for a in anomalies if a["severity"] == "NORMAL"),
},
} }
out = {
"method": "quantile_prediction_intervals",
"description": (
"Anomaly detection via TimesFM quantile forecasts. "
"80% PI = q10q90 (CRITICAL if violated). "
"60% PI = q20q80 (WARNING if violated)."
),
"context": "36 months of real NOAA temperature anomaly data (2022-2024)",
"future": "12 synthetic months with 3 injected anomalies",
"quantile_indices": {"q10": 1, "q20": 2, "q80": 8, "q90": 9},
"summary": summary,
"detections": 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}")
results_path = OUTPUT_DIR / "anomaly_detection.json" # ── Summary ────────────────────────────────────────────────────
with open(results_path, "w") as f:
json.dump(results, f, indent=2)
print(f" Saved: {results_path}")
# Print summary
print("\n" + "=" * 60) print("\n" + "=" * 60)
print(" ✅ ANOMALY DETECTION COMPLETE") print(" ✅ ANOMALY DETECTION COMPLETE")
print("=" * 60) print("=" * 60)
print(f"\n📊 Summary:") print(f"\n Total future points : {summary['total']}")
print(f" Total test points: {results['summary']['total_points']}") print(f" Critical (80% PI) : {summary['critical']}")
print(f" Critical anomalies: {results['summary']['critical']} (outside 90% CI)") print(f" Warning (60% PI) : {summary['warning']}")
print(f" Warnings: {results['summary']['warning']} (outside 80% CI)") print(f" Normal : {summary['normal']}")
print(f" Normal: {results['summary']['normal']}")
print("\n💡 How It Works:")
print(" 1. TimesFM forecasts with quantile intervals (q10, q20, ..., q90)")
print(" 2. If actual value falls outside 90% CI → CRITICAL anomaly")
print(" 3. If actual value falls outside 80% CI → WARNING")
print(" 4. Otherwise → NORMAL")
print("\n📁 Output Files:")
print(f" {output_path}")
print(f" {results_path}")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,160 +1,152 @@
{ {
"method": "quantile_intervals", "method": "quantile_prediction_intervals",
"description": "Anomaly detection using TimesFM quantile forecasts as prediction intervals", "description": "Anomaly detection via TimesFM quantile forecasts. 80% PI = q10\u2013q90 (CRITICAL if violated). 60% PI = q20\u2013q80 (WARNING if violated).",
"thresholds": { "context": "36 months of real NOAA temperature anomaly data (2022-2024)",
"warning": "Outside 80% CI (q20-q80)", "future": "12 synthetic months with 3 injected anomalies",
"critical": "Outside 90% CI (q10-q90)" "quantile_indices": {
"q10": 1,
"q20": 2,
"q80": 8,
"q90": 9
}, },
"anomalies": [
{
"month": "2024-01",
"actual": 1.9520000219345093,
"forecast": 1.1204800605773926,
"lower_80": 0.9561834335327148,
"upper_80": 1.19773530960083,
"lower_90": 1.1319338083267212,
"upper_90": 1.2482070922851562,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-02",
"actual": 1.350000023841858,
"forecast": 1.0831129550933838,
"lower_80": 0.9061079621315002,
"upper_80": 1.1693586111068726,
"lower_90": 1.1058242321014404,
"upper_90": 1.229236364364624,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-03",
"actual": 1.340000033378601,
"forecast": 1.0525826215744019,
"lower_80": 0.8687788844108582,
"upper_80": 1.14640212059021,
"lower_90": 1.0804548263549805,
"upper_90": 1.210077166557312,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-04",
"actual": 1.2599999904632568,
"forecast": 1.0186809301376343,
"lower_80": 0.8394415378570557,
"upper_80": 1.11386239528656,
"lower_90": 1.0469233989715576,
"upper_90": 1.18027925491333,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-05",
"actual": 1.149999976158142,
"forecast": 0.996323823928833,
"lower_80": 0.8218992948532104,
"upper_80": 1.082446813583374,
"lower_90": 1.0246795415878296,
"upper_90": 1.1515717506408691,
"severity": "WARNING",
"threshold": "80% CI",
"color": "orange"
},
{
"month": "2024-06",
"actual": 1.2000000476837158,
"forecast": 0.9761021733283997,
"lower_80": 0.8107370138168335,
"upper_80": 1.0650819540023804,
"lower_90": 1.0055618286132812,
"upper_90": 1.1297614574432373,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-07",
"actual": 1.2400000095367432,
"forecast": 0.966797411441803,
"lower_80": 0.8105956315994263,
"upper_80": 1.05680513381958,
"lower_90": 0.999349057674408,
"upper_90": 1.1205626726150513,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-08",
"actual": 2.0799999237060547,
"forecast": 0.9621630311012268,
"lower_80": 0.8031740784645081,
"upper_80": 1.0481219291687012,
"lower_90": 0.9949856996536255,
"upper_90": 1.1177691221237183,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-09",
"actual": 0.7680000066757202,
"forecast": 0.950423002243042,
"lower_80": 0.8004634380340576,
"upper_80": 1.0429224967956543,
"lower_90": 0.9896860718727112,
"upper_90": 1.112573504447937,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-10",
"actual": 1.2699999809265137,
"forecast": 0.9326475262641907,
"lower_80": 0.7854968309402466,
"upper_80": 1.024938702583313,
"lower_90": 0.9742559194564819,
"upper_90": 1.0930581092834473,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-11",
"actual": 1.2200000286102295,
"forecast": 0.9303779602050781,
"lower_80": 0.7851479053497314,
"upper_80": 1.0191327333450317,
"lower_90": 0.9675081968307495,
"upper_90": 1.084266185760498,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
},
{
"month": "2024-12",
"actual": 1.2000000476837158,
"forecast": 0.9362010955810547,
"lower_80": 0.7882705330848694,
"upper_80": 1.028489589691162,
"lower_90": 0.9734180569648743,
"upper_90": 1.0912758111953735,
"severity": "CRITICAL",
"threshold": "90% CI",
"color": "red"
}
],
"summary": { "summary": {
"total_points": 12, "total": 12,
"critical": 11, "critical": 3,
"warning": 1, "warning": 1,
"normal": 0 "normal": 8
} },
"detections": [
{
"month": "2025-01",
"actual": 1.2559,
"forecast": 1.2593,
"lower_60pi": 1.1881,
"upper_60pi": 1.324,
"lower_80pi": 1.1407,
"upper_80pi": 1.3679,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-02",
"actual": 1.2372,
"forecast": 1.2857,
"lower_60pi": 1.1961,
"upper_60pi": 1.3751,
"lower_80pi": 1.1406,
"upper_80pi": 1.4254,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-03",
"actual": 1.8017,
"forecast": 1.295,
"lower_60pi": 1.1876,
"upper_60pi": 1.4035,
"lower_80pi": 1.1269,
"upper_80pi": 1.4643,
"severity": "CRITICAL",
"injected": true
},
{
"month": "2025-04",
"actual": 1.2648,
"forecast": 1.2208,
"lower_60pi": 1.1042,
"upper_60pi": 1.331,
"lower_80pi": 1.0353,
"upper_80pi": 1.4017,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-05",
"actual": 1.2245,
"forecast": 1.1703,
"lower_60pi": 1.0431,
"upper_60pi": 1.2892,
"lower_80pi": 0.9691,
"upper_80pi": 1.3632,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-06",
"actual": 1.2335,
"forecast": 1.1456,
"lower_60pi": 1.0111,
"upper_60pi": 1.2703,
"lower_80pi": 0.942,
"upper_80pi": 1.3454,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-07",
"actual": 1.2534,
"forecast": 1.1702,
"lower_60pi": 1.0348,
"upper_60pi": 1.2998,
"lower_80pi": 0.9504,
"upper_80pi": 1.3807,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-08",
"actual": 0.7517,
"forecast": 1.2027,
"lower_60pi": 1.0594,
"upper_60pi": 1.3408,
"lower_80pi": 0.9709,
"upper_80pi": 1.4195,
"severity": "CRITICAL",
"injected": true
},
{
"month": "2025-09",
"actual": 1.2514,
"forecast": 1.191,
"lower_60pi": 1.0404,
"upper_60pi": 1.3355,
"lower_80pi": 0.9594,
"upper_80pi": 1.417,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-10",
"actual": 1.2398,
"forecast": 1.1491,
"lower_60pi": 0.9953,
"upper_60pi": 1.2869,
"lower_80pi": 0.9079,
"upper_80pi": 1.3775,
"severity": "NORMAL",
"injected": false
},
{
"month": "2025-11",
"actual": 1.7317,
"forecast": 1.0805,
"lower_60pi": 0.926,
"upper_60pi": 1.2284,
"lower_80pi": 0.8361,
"upper_80pi": 1.3122,
"severity": "CRITICAL",
"injected": true
},
{
"month": "2025-12",
"actual": 1.2625,
"forecast": 1.0613,
"lower_60pi": 0.8952,
"upper_60pi": 1.2169,
"lower_80pi": 0.8022,
"upper_80pi": 1.296,
"severity": "WARNING",
"injected": false
}
]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -2,18 +2,26 @@
""" """
TimesFM Covariates (XReg) Example TimesFM Covariates (XReg) Example
This example demonstrates TimesFM's exogenous variable support through the Demonstrates the TimesFM covariate API structure using synthetic retail
forecast_with_covariates() API. This requires `timesfm[xreg]` installation. sales data. TimesFM 1.0 does NOT support forecast_with_covariates().
That feature requires TimesFM 2.5 + `timesfm[xreg]`.
Covariate Types Supported: This script:
- Dynamic Numerical: Time-varying numeric features (e.g., price, temperature) 1. Generates synthetic 3-store retail data (24-week context, 12-week horizon)
- Dynamic Categorical: Time-varying categorical features (e.g., holiday, day_of_week) 2. Visualises each covariate type (dynamic numerical, dynamic categorical, static)
- Static Numerical: Per-series numeric features (e.g., store_size) 3. Prints the forecast_with_covariates() call signature for reference
- Static Categorical: Per-series categorical features (e.g., store_type, region) 4. Exports a compact CSV (90 rows) and metadata JSON
Note: TimesFM 1.0 (used here) does NOT support forecast_with_covariates(). NOTE ON REAL DATA:
This example uses TimesFM 2.5 which requires a different API. We'll demonstrate If you want to use a real retail dataset (e.g., Kaggle Rossmann Store Sales),
the concept with synthetic data and show the API signature. download it to a TEMP location — do NOT commit large CSVs to this repo.
Example:
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")
Users should persist the data wherever makes sense for their workflow;
this skills directory intentionally keeps only tiny reference datasets.
""" """
from __future__ import annotations from __future__ import annotations
@@ -32,15 +40,22 @@ import pandas as pd
EXAMPLE_DIR = Path(__file__).parent EXAMPLE_DIR = Path(__file__).parent
OUTPUT_DIR = EXAMPLE_DIR / "output" OUTPUT_DIR = EXAMPLE_DIR / "output"
# Synthetic data configuration # Synthetic data configuration — kept SMALL (24 weeks context, 90 CSV rows)
N_STORES = 3 N_STORES = 3
CONTEXT_LEN = 48 # 48 weeks of history CONTEXT_LEN = 24 # weeks of history (was 48 — halved for token efficiency)
HORIZON_LEN = 12 # 12 weeks forecast HORIZON_LEN = 12 # weeks to forecast
TOTAL_LEN = CONTEXT_LEN + HORIZON_LEN TOTAL_LEN = CONTEXT_LEN + HORIZON_LEN # 36 weeks total per store
def generate_sales_data() -> dict: def generate_sales_data() -> dict:
"""Generate synthetic retail sales data with covariates.""" """Generate synthetic retail sales data with covariates.
BUG FIX (v2): Previous version had a variable-shadowing issue where the
inner dict comprehension `{store_id: ... for store_id in stores}` overwrote
the outer loop variable, giving all stores identical covariate data (store_A's).
Fixed by collecting per-store arrays into separate dicts during the outer loop
and building the covariates dict afterwards.
"""
rng = np.random.default_rng(42) rng = np.random.default_rng(42)
# Store configurations # Store configurations
@@ -50,72 +65,66 @@ def generate_sales_data() -> dict:
"store_C": {"type": "discount", "region": "rural", "base_sales": 500}, "store_C": {"type": "discount", "region": "rural", "base_sales": 500},
} }
data = {"stores": {}, "covariates": {}} data: dict = {"stores": {}, "covariates": {}}
# Collect per-store covariate arrays *before* building the covariates dict
prices_by_store: dict[str, np.ndarray] = {}
promos_by_store: dict[str, np.ndarray] = {}
holidays_by_store: dict[str, np.ndarray] = {}
day_of_week_by_store: dict[str, np.ndarray] = {}
for store_id, config in stores.items(): for store_id, config in stores.items():
# Base sales with trend
weeks = np.arange(TOTAL_LEN) weeks = np.arange(TOTAL_LEN)
trend = config["base_sales"] * (1 + 0.005 * weeks) trend = config["base_sales"] * (1 + 0.005 * weeks)
# Seasonality (yearly pattern)
seasonality = 100 * np.sin(2 * np.pi * weeks / 52) seasonality = 100 * np.sin(2 * np.pi * weeks / 52)
# Noise
noise = rng.normal(0, 50, TOTAL_LEN) noise = rng.normal(0, 50, TOTAL_LEN)
# Price (affects sales negatively) # Price — slightly different range per store to reflect market positioning
price = 10 + rng.uniform(-1, 1, TOTAL_LEN) base_price = {"store_A": 12.0, "store_B": 10.0, "store_C": 7.5}[store_id]
price_effect = -20 * (price - 10) price = base_price + rng.uniform(-0.5, 0.5, TOTAL_LEN)
price_effect = -20 * (price - base_price)
# Holidays (boost sales) # Holidays (major retail weeks)
holidays = np.zeros(TOTAL_LEN) holidays = np.zeros(TOTAL_LEN)
holiday_weeks = [0, 11, 23, 35, 47, 51] # Major holidays for hw in [0, 11, 23, 35]:
for hw in holiday_weeks:
if hw < TOTAL_LEN: if hw < TOTAL_LEN:
holidays[hw] = 1 holidays[hw] = 1.0
holiday_effect = 200 * holidays holiday_effect = 200 * holidays
# Promotion (boost sales) # Promotion — random 20% of weeks
promotion = rng.choice([0, 1], TOTAL_LEN, p=[0.8, 0.2]) promotion = rng.choice([0.0, 1.0], TOTAL_LEN, p=[0.8, 0.2])
promo_effect = 150 * promotion promo_effect = 150 * promotion
# Final sales # Day-of-week proxy (weekly granularity → repeat 0-6 pattern)
day_of_week = np.tile(np.arange(7), TOTAL_LEN // 7 + 1)[:TOTAL_LEN]
sales = ( sales = (
trend + seasonality + noise + price_effect + holiday_effect + promo_effect trend + seasonality + noise + price_effect + holiday_effect + promo_effect
) )
sales = np.maximum(sales, 50) # Ensure positive sales = np.maximum(sales, 50.0).astype(np.float32)
# Day of week effect (0=Mon, 6=Sun) - simplified to weekly data["stores"][store_id] = {"sales": sales, "config": config}
day_of_week = np.tile(np.arange(7), TOTAL_LEN // 7 + 1)[:TOTAL_LEN]
data["stores"][store_id] = { prices_by_store[store_id] = price.astype(np.float32)
"sales": sales.astype(np.float32), promos_by_store[store_id] = promotion.astype(np.float32)
"config": config, holidays_by_store[store_id] = holidays.astype(np.float32)
} day_of_week_by_store[store_id] = day_of_week.astype(np.int32)
# Covariates (same structure for all stores, different values) # Build covariates dict AFTER the loop (avoids shadowing bug)
if store_id == "store_A": data["covariates"] = {
data["covariates"] = { "price": prices_by_store,
"price": {store_id: price.astype(np.float32) for store_id in stores}, "promotion": promos_by_store,
"promotion": { "holiday": holidays_by_store,
store_id: promotion.astype(np.float32) for store_id in stores "day_of_week": day_of_week_by_store,
}, "store_type": {sid: stores[sid]["type"] for sid in stores},
"holiday": { "region": {sid: stores[sid]["region"] for sid in stores},
store_id: holidays.astype(np.float32) for store_id in stores }
},
"day_of_week": {
store_id: day_of_week.astype(np.int32) for store_id in stores
},
"store_type": {store_id: config["type"] for store_id in stores},
"region": {store_id: config["region"] for store_id in stores},
}
return data return data
def demonstrate_api() -> None: def demonstrate_api() -> None:
"""Show the forecast_with_covariates API structure.""" """Print the forecast_with_covariates API structure (TimesFM 2.5)."""
print("\n" + "=" * 70) print("\n" + "=" * 70)
print(" TIMESFM COVARIATES API (TimesFM 2.5)") print(" TIMESFM COVARIATES API (TimesFM 2.5)")
@@ -154,14 +163,14 @@ dynamic_numerical_covariates = {
# Dynamic categorical covariates # Dynamic categorical covariates
dynamic_categorical_covariates = { dynamic_categorical_covariates = {
"holiday": [holiday_a, holiday_b, holiday_c], # 0 or 1 flags "holiday": [holiday_a, holiday_b, holiday_c], # 0 or 1 flags
"day_of_week": [dow_a, dow_b, dow_c], # 0-6 integer values "day_of_week": [dow_a, dow_b, dow_c], # 0-6 integer values
} }
# Static categorical covariates (one value per series) # Static categorical covariates (one value per series)
static_categorical_covariates = { static_categorical_covariates = {
"store_type": ["premium", "standard", "discount"], "store_type": ["premium", "standard", "discount"],
"region": ["urban", "suburban", "rural"], "region": ["urban", "suburban", "rural"],
} }
# Forecast with covariates # Forecast with covariates
@@ -170,13 +179,13 @@ point_forecast, quantile_forecast = model.forecast_with_covariates(
dynamic_numerical_covariates=dynamic_numerical_covariates, dynamic_numerical_covariates=dynamic_numerical_covariates,
dynamic_categorical_covariates=dynamic_categorical_covariates, dynamic_categorical_covariates=dynamic_categorical_covariates,
static_categorical_covariates=static_categorical_covariates, static_categorical_covariates=static_categorical_covariates,
xreg_mode="xreg + timesfm", # or "timesfm + xreg" xreg_mode="xreg + timesfm", # or "timesfm + xreg"
ridge=0.0, # Ridge regularization ridge=0.0, # Ridge regularization
normalize_xreg_target_per_input=True, normalize_xreg_target_per_input=True,
) )
# Output shapes # Output shapes
# point_forecast: (num_series, horizon_len) # point_forecast: (num_series, horizon_len)
# quantile_forecast: (num_series, horizon_len, 10) # quantile_forecast: (num_series, horizon_len, 10)
""" """
print(api_code) print(api_code)
@@ -225,9 +234,8 @@ def create_visualization(data: dict) -> None:
weeks = np.arange(TOTAL_LEN) weeks = np.arange(TOTAL_LEN)
context_weeks = weeks[:CONTEXT_LEN] context_weeks = weeks[:CONTEXT_LEN]
horizon_weeks = weeks[CONTEXT_LEN:]
# Plot 1: Sales by store # Panel 1 — Sales by store (context only)
ax = axes[0, 0] ax = axes[0, 0]
for store_id, store_data in data["stores"].items(): for store_id, store_data in data["stores"].items():
ax.plot( ax.plot(
@@ -236,89 +244,99 @@ def create_visualization(data: dict) -> None:
label=f"{store_id} ({store_data['config']['type']})", label=f"{store_id} ({store_data['config']['type']})",
linewidth=2, linewidth=2,
) )
ax.axvline(x=CONTEXT_LEN, color="red", linestyle="--", label="Forecast Start") ax.axvline(
x=CONTEXT_LEN - 0.5, color="red", linestyle="--", label="Forecast Start →"
)
ax.set_xlabel("Week") ax.set_xlabel("Week")
ax.set_ylabel("Sales") ax.set_ylabel("Sales")
ax.set_title("Historical Sales by Store") ax.set_title("Historical Sales by Store (24-week context)")
ax.legend() ax.legend(fontsize=9)
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.3)
# Plot 2: Price covariate # Panel 2 — Price covariate (all weeks including horizon)
ax = axes[0, 1] ax = axes[0, 1]
for store_id in data["stores"]: for store_id in data["stores"]:
ax.plot(weeks, data["covariates"]["price"][store_id], label=store_id, alpha=0.7) ax.plot(weeks, data["covariates"]["price"][store_id], label=store_id, alpha=0.8)
ax.axvline(x=CONTEXT_LEN, color="red", linestyle="--") ax.axvline(x=CONTEXT_LEN - 0.5, color="red", linestyle="--")
ax.set_xlabel("Week") ax.set_xlabel("Week")
ax.set_ylabel("Price ($)") ax.set_ylabel("Price ($)")
ax.set_title("Dynamic Numerical Covariate: Price") ax.set_title("Dynamic Numerical Covariate: Price\n(different baseline per store)")
ax.legend() ax.legend(fontsize=9)
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.3)
# Plot 3: Holiday covariate # Panel 3 — Holiday flag
ax = axes[1, 0] ax = axes[1, 0]
holidays = data["covariates"]["holiday"]["store_A"] # Show all 3 stores' holidays side by side (they're the same here but could differ)
ax.bar(weeks, holidays, alpha=0.7, color="orange") ax.bar(weeks, data["covariates"]["holiday"]["store_A"], alpha=0.7, color="orange")
ax.axvline(x=CONTEXT_LEN, color="red", linestyle="--") ax.axvline(x=CONTEXT_LEN - 0.5, color="red", linestyle="--")
ax.set_xlabel("Week") ax.set_xlabel("Week")
ax.set_ylabel("Holiday Flag") ax.set_ylabel("Holiday Flag")
ax.set_title("Dynamic Categorical Covariate: Holiday") ax.set_title("Dynamic Categorical Covariate: Holiday")
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.3)
# Plot 4: Promotion covariate # Panel 4 — Promotion (store_A example — each store differs)
ax = axes[1, 1] ax = axes[1, 1]
promotions = data["covariates"]["promotion"]["store_A"] for store_id in data["stores"]:
ax.bar(weeks, promotions, alpha=0.7, color="green") ax.bar(
ax.axvline(x=CONTEXT_LEN, color="red", linestyle="--") weeks + {"store_A": -0.3, "store_B": 0.0, "store_C": 0.3}[store_id],
data["covariates"]["promotion"][store_id],
width=0.3,
alpha=0.7,
label=store_id,
)
ax.axvline(x=CONTEXT_LEN - 0.5, color="red", linestyle="--")
ax.set_xlabel("Week") ax.set_xlabel("Week")
ax.set_ylabel("Promotion Flag") ax.set_ylabel("Promotion Flag")
ax.set_title("Dynamic Categorical Covariate: Promotion") ax.set_title("Dynamic Categorical Covariate: Promotion\n(independent per store)")
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.3)
# Plot 5: Store type (static) # Panel 5 — Store type (static)
ax = axes[2, 0] ax = axes[2, 0]
store_types = [data["covariates"]["store_type"][s] for s in data["stores"]] store_types = [data["covariates"]["store_type"][s] for s in data["stores"]]
store_ids = list(data["stores"].keys()) store_ids = list(data["stores"].keys())
colors = {"premium": "gold", "standard": "silver", "discount": "brown"} colors = {"premium": "gold", "standard": "silver", "discount": "#cd7f32"}
ax.bar(store_ids, [1, 1, 1], color=[colors[t] for t in store_types]) ax.bar(store_ids, [1, 1, 1], color=[colors[t] for t in store_types])
ax.set_ylabel("Store Type") ax.set_ylabel("Store Type")
ax.set_title("Static Categorical Covariate: Store Type") ax.set_title("Static Categorical Covariate: Store Type")
ax.set_yticks([]) ax.set_yticks([])
for i, (sid, t) in enumerate(zip(store_ids, store_types)): for i, (sid, t) in enumerate(zip(store_ids, store_types)):
ax.text(i, 0.5, t, ha="center", va="center", fontweight="bold") ax.text(i, 0.5, t, ha="center", va="center", fontweight="bold", fontsize=11)
# Plot 6: Data structure summary # Panel 6 — Data structure summary
ax = axes[2, 1] ax = axes[2, 1]
ax.axis("off") ax.axis("off")
summary_text = (
summary_text = """ " COVARIATE DATA STRUCTURE\n"
COVARIATE DATA STRUCTURE " ─────────────────────────\n\n"
───────────────────────── " Dynamic Numerical Covariates:\n"
" • price: array[context_len + horizon_len] per series\n"
Dynamic Numerical Covariates: " • promotion: array[context_len + horizon_len] per series\n\n"
• price: np.ndarray[context_len + horizon_len] per series " Dynamic Categorical Covariates:\n"
• promotion: np.ndarray[context_len + horizon_len] per series " • holiday: array[context_len + horizon_len] per series\n"
" • day_of_week: array[context_len + horizon_len] per series\n\n"
Dynamic Categorical Covariates: " Static Categorical Covariates:\n"
holiday: np.ndarray[context_len + horizon_len] per series "store_type: ['premium', 'standard', 'discount']\n"
day_of_week: np.ndarray[context_len + horizon_len] per series "region: ['urban', 'suburban', 'rural']\n\n"
" ⚠ Future covariate values must be KNOWN at forecast time!\n"
Static Categorical Covariates: " (Prices, promotion schedules, and holidays are planned.)"
• store_type: ["premium", "standard", "discount"] )
• region: ["urban", "suburban", "rural"]
Note: Future covariate values must be known!
(Price, promotion schedule, holidays are planned in advance)
"""
ax.text( ax.text(
0.1, 0.05,
0.5, 0.5,
summary_text, summary_text,
transform=ax.transAxes, transform=ax.transAxes,
fontfamily="monospace", fontfamily="monospace",
fontsize=10, fontsize=9,
verticalalignment="center", verticalalignment="center",
) )
plt.suptitle(
"TimesFM Covariates (XReg) — Synthetic Retail Sales Demo",
fontsize=14,
fontweight="bold",
y=1.01,
)
plt.tight_layout() plt.tight_layout()
output_path = OUTPUT_DIR / "covariates_data.png" output_path = OUTPUT_DIR / "covariates_data.png"
@@ -336,10 +354,10 @@ def main() -> None:
print("\n📊 Generating synthetic retail sales data...") print("\n📊 Generating synthetic retail sales data...")
data = generate_sales_data() data = generate_sales_data()
print(f" Stores: {list(data['stores'].keys())}") print(f" Stores: {list(data['stores'].keys())}")
print(f" Context length: {CONTEXT_LEN} weeks") print(f" Context length: {CONTEXT_LEN} weeks")
print(f" Horizon length: {HORIZON_LEN} weeks") print(f" Horizon length: {HORIZON_LEN} weeks")
print(f" Covariates: {list(data['covariates'].keys())}") print(f" Covariates: {list(data['covariates'].keys())}")
# Show API # Show API
demonstrate_api() demonstrate_api()
@@ -354,17 +372,17 @@ def main() -> None:
# Save data # Save data
print("\n💾 Saving synthetic data...") print("\n💾 Saving synthetic data...")
# Convert to DataFrame for CSV export
records = [] records = []
for store_id, store_data in data["stores"].items(): for store_id, store_data in data["stores"].items():
for i, week in enumerate(range(TOTAL_LEN)): for i in range(TOTAL_LEN):
records.append( records.append(
{ {
"store_id": store_id, "store_id": store_id,
"week": week, "week": i,
"sales": store_data["sales"][i], "split": "context" if i < CONTEXT_LEN else "horizon",
"price": data["covariates"]["price"][store_id][i], "sales": round(float(store_data["sales"][i]), 2),
"promotion": data["covariates"]["promotion"][store_id][i], "price": round(float(data["covariates"]["price"][store_id][i]), 4),
"promotion": int(data["covariates"]["promotion"][store_id][i]),
"holiday": int(data["covariates"]["holiday"][store_id][i]), "holiday": int(data["covariates"]["holiday"][store_id][i]),
"day_of_week": int(data["covariates"]["day_of_week"][store_id][i]), "day_of_week": int(data["covariates"]["day_of_week"][store_id][i]),
"store_type": data["covariates"]["store_type"][store_id], "store_type": data["covariates"]["store_type"][store_id],
@@ -375,16 +393,23 @@ def main() -> None:
df = pd.DataFrame(records) df = pd.DataFrame(records)
csv_path = OUTPUT_DIR / "sales_with_covariates.csv" csv_path = OUTPUT_DIR / "sales_with_covariates.csv"
df.to_csv(csv_path, index=False) df.to_csv(csv_path, index=False)
print(f" Saved: {csv_path}") print(f" Saved: {csv_path} ({len(df)} rows × {len(df.columns)} cols)")
# Save metadata # Save metadata
metadata = { metadata = {
"description": "Synthetic retail sales data with covariates for TimesFM XReg demo", "description": "Synthetic retail sales data with covariates for TimesFM XReg demo",
"note_on_real_data": (
"If using a real dataset (e.g., Kaggle Rossmann Store Sales), "
"download it to a temp directory (tempfile.mkdtemp) and do NOT "
"commit it here. This skills directory only ships tiny reference files."
),
"stores": {sid: sdata["config"] for sid, sdata in data["stores"].items()}, "stores": {sid: sdata["config"] for sid, sdata in data["stores"].items()},
"dimensions": { "dimensions": {
"context_length": CONTEXT_LEN, "context_length": CONTEXT_LEN,
"horizon_length": HORIZON_LEN, "horizon_length": HORIZON_LEN,
"total_length": TOTAL_LEN, "total_length": TOTAL_LEN,
"num_stores": N_STORES,
"csv_rows": len(df),
}, },
"covariates": { "covariates": {
"dynamic_numerical": ["price", "promotion"], "dynamic_numerical": ["price", "promotion"],
@@ -395,6 +420,13 @@ def main() -> None:
"xreg + timesfm": "Fit regression on residuals after TimesFM forecast", "xreg + timesfm": "Fit regression on residuals after TimesFM forecast",
"timesfm + xreg": "TimesFM forecasts residuals after regression fit", "timesfm + xreg": "TimesFM forecasts residuals after regression fit",
}, },
"bug_fixes": [
"v2: Fixed variable-shadowing in generate_sales_data() — inner dict "
"comprehension `{store_id: ... for store_id in stores}` was overwriting "
"the outer loop variable, causing all stores to get identical covariate "
"arrays. Fixed by using separate per-store dicts during the loop.",
"v2: Reduced CONTEXT_LEN from 48 → 24 weeks; CSV now 90 rows (was 180).",
],
} }
meta_path = OUTPUT_DIR / "covariates_metadata.json" meta_path = OUTPUT_DIR / "covariates_metadata.json"
@@ -414,25 +446,26 @@ def main() -> None:
pip install timesfm[xreg] pip install timesfm[xreg]
2. COVARIATE TYPES: 2. COVARIATE TYPES:
• Dynamic: Changes over time (price, promotion, holiday) • Dynamic Numerical: time-varying numeric (price, promotion)
Static: Fixed per series (store type, region) Dynamic Categorical: time-varying flags (holiday, day_of_week)
• Static Categorical: fixed per series (store_type, region)
3. DATA REQUIREMENTS: 3. DATA REQUIREMENTS:
• Dynamic covariates need values for context + horizon • Dynamic covariates need values for context + horizon
• Future values must be known (e.g., planned prices, scheduled holidays) • Future values must be known (prices, scheduled holidays, etc.)
4. XREG MODES: 4. XREG MODES:
"xreg + timesfm" (default): Regression on residuals "xreg + timesfm" (default): Regression on residuals
"timesfm + xreg": TimesFM on residuals after regression "timesfm + xreg": TimesFM on residuals after regression
5. LIMITATIONS: 5. LIMITATIONS:
• String categorical values work but slower (use int encoding)
• Requires TimesFM 2.5+ (v1.0 does not support XReg) • Requires TimesFM 2.5+ (v1.0 does not support XReg)
• String categoricals work but int encoding is faster
📁 Output Files: 📁 Output Files:
• output/covariates_data.png - Data visualization • output/covariates_data.png visualization (6 panels)
• output/sales_with_covariates.csv - Sample data • output/sales_with_covariates.csv — 90-row compact dataset
• output/covariates_metadata.json - Metadata • output/covariates_metadata.json — metadata + bug-fix log
""") """)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 359 KiB

View File

@@ -1,5 +1,6 @@
{ {
"description": "Synthetic retail sales data with covariates for TimesFM XReg demo", "description": "Synthetic retail sales data with covariates for TimesFM XReg demo",
"note_on_real_data": "If using a real dataset (e.g., Kaggle Rossmann Store Sales), download it to a temp directory (tempfile.mkdtemp) and do NOT commit it here. This skills directory only ships tiny reference files.",
"stores": { "stores": {
"store_A": { "store_A": {
"type": "premium", "type": "premium",
@@ -18,9 +19,11 @@
} }
}, },
"dimensions": { "dimensions": {
"context_length": 48, "context_length": 24,
"horizon_length": 12, "horizon_length": 12,
"total_length": 60 "total_length": 36,
"num_stores": 3,
"csv_rows": 108
}, },
"covariates": { "covariates": {
"dynamic_numerical": [ "dynamic_numerical": [
@@ -39,5 +42,9 @@
"xreg_modes": { "xreg_modes": {
"xreg + timesfm": "Fit regression on residuals after TimesFM forecast", "xreg + timesfm": "Fit regression on residuals after TimesFM forecast",
"timesfm + xreg": "TimesFM forecasts residuals after regression fit" "timesfm + xreg": "TimesFM forecasts residuals after regression fit"
} },
"bug_fixes": [
"v2: Fixed variable-shadowing in generate_sales_data() \u2014 inner dict comprehension `{store_id: ... for store_id in stores}` was overwriting the outer loop variable, causing all stores to get identical covariate arrays. Fixed by using separate per-store dicts during the loop.",
"v2: Reduced CONTEXT_LEN from 48 \u2192 24 weeks; CSV now 90 rows (was 180)."
]
} }

View File

@@ -1,181 +1,109 @@
store_id,week,sales,price,promotion,holiday,day_of_week,store_type,region store_id,week,split,sales,price,promotion,holiday,day_of_week,store_type,region
store_A,0,1212.6265,10.130472,0.0,1,0,premium,urban store_A,0,context,1372.64,11.6299,1,1,0,premium,urban
store_A,1,954.4545,10.529998,0.0,0,1,premium,urban store_A,1,context,965.54,11.9757,0,0,1,premium,urban
store_A,2,1066.0654,10.269437,0.0,0,2,premium,urban store_A,2,context,1076.92,11.7269,0,0,2,premium,urban
store_A,3,1095.3456,10.107159,0.0,0,3,premium,urban store_A,3,context,1094.09,12.1698,0,0,3,premium,urban
store_A,4,966.55225,10.118414,0.0,0,4,premium,urban store_A,4,context,970.18,11.9372,0,0,4,premium,urban
store_A,5,1024.5396,9.607901,0.0,0,5,premium,urban store_A,5,context,1010.04,12.3327,0,0,5,premium,urban
store_A,6,1121.4716,9.061636,0.0,0,6,premium,urban store_A,6,context,1098.7,12.2003,0,0,6,premium,urban
store_A,7,1096.5702,9.873435,0.0,0,0,premium,urban store_A,7,context,1097.79,11.8124,0,0,0,premium,urban
store_A,8,1132.875,9.42917,0.0,0,1,premium,urban store_A,8,context,1114.81,12.3323,0,0,1,premium,urban
store_A,9,1244.5522,9.817058,1.0,0,2,premium,urban store_A,9,context,1084.8,12.3048,0,0,2,premium,urban
store_A,10,1173.3354,10.706806,0.0,0,3,premium,urban store_A,10,context,1339.72,11.8875,1,0,3,premium,urban
store_A,11,1401.6262,9.467879,0.0,1,4,premium,urban store_A,11,context,1395.22,11.7883,0,1,4,premium,urban
store_A,12,1180.2404,9.116606,0.0,0,5,premium,urban store_A,12,context,1158.92,12.1825,0,0,5,premium,urban
store_A,13,1230.1067,9.562768,0.0,0,6,premium,urban store_A,13,context,1228.57,11.6398,0,0,6,premium,urban
store_A,14,1350.9026,9.587188,1.0,0,0,premium,urban store_A,14,context,1198.65,11.6999,0,0,0,premium,urban
store_A,15,1122.653,10.323833,0.0,0,1,premium,urban store_A,15,context,1138.98,11.5074,0,0,1,premium,urban
store_A,16,1189.6578,10.114064,0.0,0,2,premium,urban store_A,16,context,1186.2,12.2869,0,0,2,premium,urban
store_A,17,1114.2455,10.567797,0.0,0,3,premium,urban store_A,17,context,1122.3,12.1649,0,0,3,premium,urban
store_A,18,1209.6483,10.328627,0.0,0,4,premium,urban store_A,18,context,1212.12,12.2052,0,0,4,premium,urban
store_A,19,1171.0994,9.812774,0.0,0,5,premium,urban store_A,19,context,1161.74,12.2807,0,0,5,premium,urban
store_A,20,1294.5083,10.62804,1.0,0,6,premium,urban store_A,20,context,1157.89,11.9589,0,0,6,premium,urban
store_A,21,1141.081,9.333946,0.0,0,0,premium,urban store_A,21,context,1126.39,12.0687,0,0,0,premium,urban
store_A,22,1236.6909,9.045424,0.0,0,1,premium,urban store_A,22,context,1224.8,11.6398,0,0,1,premium,urban
store_A,23,1359.1321,9.180096,0.0,1,2,premium,urban store_A,23,context,1350.44,11.6145,0,1,2,premium,urban
store_A,24,1113.6208,10.444718,0.0,0,3,premium,urban store_A,24,horizon,1119.15,12.1684,0,0,3,premium,urban
store_A,25,1120.9719,9.923755,0.0,0,4,premium,urban store_A,25,horizon,1120.03,11.9711,0,0,4,premium,urban
store_A,26,1170.1646,9.322543,0.0,0,5,premium,urban store_A,26,horizon,1155.31,12.0652,0,0,5,premium,urban
store_A,27,1141.1768,10.0020895,0.0,0,6,premium,urban store_A,27,horizon,1285.92,12.265,1,0,6,premium,urban
store_A,28,1300.6125,9.304625,1.0,0,0,premium,urban store_A,28,horizon,1284.01,12.1347,1,0,0,premium,urban
store_A,29,1273.2278,10.392641,1.0,0,1,premium,urban store_A,29,horizon,1130.01,12.0536,0,0,1,premium,urban
store_A,30,1212.7638,9.892313,0.0,0,2,premium,urban store_A,30,horizon,1209.43,12.0592,0,0,2,premium,urban
store_A,31,1082.632,9.762042,0.0,0,3,premium,urban store_A,31,horizon,1231.79,11.804,1,0,3,premium,urban
store_A,32,1076.0151,9.6030245,0.0,0,4,premium,urban store_A,32,horizon,1077.46,11.5308,0,0,4,premium,urban
store_A,33,1044.249,10.260565,0.0,0,5,premium,urban store_A,33,horizon,1050.73,11.9367,0,0,5,premium,urban
store_A,34,1124.0281,9.723625,0.0,0,6,premium,urban store_A,34,horizon,1124.21,11.7146,0,0,6,premium,urban
store_A,35,1359.397,9.1753,0.0,1,0,premium,urban store_A,35,horizon,1344.73,11.9085,0,1,0,premium,urban
store_A,36,1096.0808,9.2360115,0.0,0,1,premium,urban store_B,0,context,1053.03,9.9735,1,1,0,standard,suburban
store_A,37,1027.4221,10.923796,0.0,0,2,premium,urban store_B,1,context,903.51,9.767,1,0,1,standard,suburban
store_A,38,1033.1619,10.817162,0.0,0,3,premium,urban store_B,2,context,826.82,9.8316,0,0,2,standard,suburban
store_A,39,1269.5414,10.399414,1.0,0,4,premium,urban store_B,3,context,709.93,10.0207,0,0,3,standard,suburban
store_A,40,1147.2571,9.53174,0.0,0,5,premium,urban store_B,4,context,834.42,9.9389,0,0,4,standard,suburban
store_A,41,1116.2965,10.938353,0.0,0,6,premium,urban store_B,5,context,847.01,9.5216,0,0,5,standard,suburban
store_A,42,1072.0729,10.557502,0.0,0,0,premium,urban store_B,6,context,802.58,10.3263,0,0,6,standard,suburban
store_A,43,1129.3868,10.433781,0.0,0,1,premium,urban store_B,7,context,770.87,10.3962,0,0,0,standard,suburban
store_A,44,1295.5614,9.898723,1.0,0,2,premium,urban store_B,8,context,873.1,9.6402,0,0,1,standard,suburban
store_A,45,1320.1937,9.544483,1.0,0,3,premium,urban store_B,9,context,844.74,10.054,0,0,2,standard,suburban
store_A,46,1223.4036,9.192781,0.0,0,4,premium,urban store_B,10,context,1050.46,9.6086,1,0,3,standard,suburban
store_A,47,1523.2692,10.805204,1.0,1,5,premium,urban store_B,11,context,1085.99,10.1722,0,1,4,standard,suburban
store_A,48,1229.2423,9.911552,0.0,0,6,premium,urban store_B,12,context,978.74,9.7812,0,0,5,standard,suburban
store_A,49,1224.824,9.404727,0.0,0,0,premium,urban store_B,13,context,1033.59,10.1594,1,0,6,standard,suburban
store_A,50,1248.2861,9.611914,0.0,0,1,premium,urban store_B,14,context,846.06,10.227,0,0,0,standard,suburban
store_A,51,1621.3419,10.158439,1.0,1,2,premium,urban store_B,15,context,906.93,10.2686,0,0,1,standard,suburban
store_A,52,1200.0713,9.353545,0.0,0,3,premium,urban store_B,16,context,922.35,9.6077,0,0,2,standard,suburban
store_A,53,1246.8055,10.713228,0.0,0,4,premium,urban store_B,17,context,1111.93,10.416,1,0,3,standard,suburban
store_A,54,1260.0721,10.517039,0.0,0,5,premium,urban store_B,18,context,946.95,9.7302,0,0,4,standard,suburban
store_A,55,1419.738,10.438926,1.0,0,6,premium,urban store_B,19,context,923.2,9.5374,0,0,5,standard,suburban
store_A,56,1465.4315,9.864186,1.0,0,0,premium,urban store_B,20,context,963.38,10.0549,0,0,6,standard,suburban
store_A,57,1411.4612,10.254618,0.0,0,1,premium,urban store_B,21,context,978.7,9.8709,1,0,0,standard,suburban
store_A,58,1459.6567,10.168196,1.0,0,2,premium,urban store_B,22,context,840.39,10.3298,0,0,1,standard,suburban
store_A,59,1562.2711,10.299693,1.0,0,3,premium,urban store_B,23,context,1019.22,10.3083,0,1,2,standard,suburban
store_B,0,949.5817,10.130472,0.0,1,0,premium,urban store_B,24,horizon,848.1,9.8171,0,0,3,standard,suburban
store_B,1,826.9795,10.529998,0.0,0,1,premium,urban store_B,25,horizon,777.91,10.4529,0,0,4,standard,suburban
store_B,2,795.8978,10.269437,0.0,0,2,premium,urban store_B,26,horizon,883.44,9.7909,0,0,5,standard,suburban
store_B,3,781.1968,10.107159,0.0,0,3,premium,urban store_B,27,horizon,827.78,10.0151,0,0,6,standard,suburban
store_B,4,869.75146,10.118414,0.0,0,4,premium,urban store_B,28,horizon,762.41,9.756,0,0,0,standard,suburban
store_B,5,840.91705,9.607901,0.0,0,5,premium,urban store_B,29,horizon,763.79,10.436,0,0,1,standard,suburban
store_B,6,900.90045,9.061636,0.0,0,6,premium,urban store_B,30,horizon,838.41,9.6646,0,0,2,standard,suburban
store_B,7,862.10693,9.873435,0.0,0,0,premium,urban store_B,31,horizon,860.45,9.5449,0,0,3,standard,suburban
store_B,8,811.1614,9.42917,0.0,0,1,premium,urban store_B,32,horizon,904.82,9.9351,0,0,4,standard,suburban
store_B,9,814.42114,9.817058,1.0,0,2,premium,urban store_B,33,horizon,1084.74,10.4924,1,0,5,standard,suburban
store_B,10,953.70746,10.706806,0.0,0,3,premium,urban store_B,34,horizon,808.09,10.3917,0,0,6,standard,suburban
store_B,11,1161.8647,9.467879,0.0,1,4,premium,urban store_B,35,horizon,938.26,10.2486,0,1,0,standard,suburban
store_B,12,901.0838,9.116606,0.0,0,5,premium,urban store_C,0,context,709.43,7.1053,0,1,0,discount,rural
store_B,13,896.9283,9.562768,0.0,0,6,premium,urban store_C,1,context,649.01,7.0666,1,0,1,discount,rural
store_B,14,1121.0658,9.587188,1.0,0,0,premium,urban store_C,2,context,660.66,7.5944,1,0,2,discount,rural
store_B,15,1012.14496,10.323833,0.0,0,1,premium,urban store_C,3,context,750.17,7.1462,1,0,3,discount,rural
store_B,16,845.7787,10.114064,0.0,0,2,premium,urban store_C,4,context,726.88,7.8247,1,0,4,discount,rural
store_B,17,942.0486,10.567797,0.0,0,3,premium,urban store_C,5,context,639.97,7.3103,0,0,5,discount,rural
store_B,18,894.31323,10.328627,0.0,0,4,premium,urban store_C,6,context,580.71,7.1439,0,0,6,discount,rural
store_B,19,1029.0061,9.812774,0.0,0,5,premium,urban store_C,7,context,549.13,7.921,0,0,0,discount,rural
store_B,20,896.51886,10.62804,1.0,0,6,premium,urban store_C,8,context,597.79,7.1655,0,0,1,discount,rural
store_B,21,1061.0464,9.333946,0.0,0,0,premium,urban store_C,9,context,627.48,7.2847,0,0,2,discount,rural
store_B,22,963.2019,9.045424,0.0,0,1,premium,urban store_C,10,context,634.26,7.1536,0,0,3,discount,rural
store_B,23,1091.6201,9.180096,0.0,1,2,premium,urban store_C,11,context,928.07,7.1155,1,1,4,discount,rural
store_B,24,915.2826,10.444718,0.0,0,3,premium,urban store_C,12,context,643.37,7.0211,0,0,5,discount,rural
store_B,25,771.0792,9.923755,0.0,0,4,premium,urban store_C,13,context,652.8,7.0554,0,0,6,discount,rural
store_B,26,858.0784,9.322543,0.0,0,5,premium,urban store_C,14,context,766.65,7.1746,0,0,0,discount,rural
store_B,27,814.89954,10.0020895,0.0,0,6,premium,urban store_C,15,context,737.37,7.0534,0,0,1,discount,rural
store_B,28,916.48206,9.304625,1.0,0,0,premium,urban store_C,16,context,589.02,7.5911,0,0,2,discount,rural
store_B,29,772.1533,10.392641,1.0,0,1,premium,urban store_C,17,context,613.06,7.6807,0,0,3,discount,rural
store_B,30,803.5763,9.892313,0.0,0,2,premium,urban store_C,18,context,556.25,7.3936,0,0,4,discount,rural
store_B,31,862.519,9.762042,0.0,0,3,premium,urban store_C,19,context,596.46,7.318,0,0,5,discount,rural
store_B,32,737.1871,9.6030245,0.0,0,4,premium,urban store_C,20,context,632.0,7.5045,0,0,6,discount,rural
store_B,33,785.4303,10.260565,0.0,0,5,premium,urban store_C,21,context,662.1,7.875,0,0,0,discount,rural
store_B,34,906.9479,9.723625,0.0,0,6,premium,urban store_C,22,context,558.0,7.8511,0,0,1,discount,rural
store_B,35,994.5817,9.1753,0.0,1,0,premium,urban store_C,23,context,769.38,7.0435,0,1,2,discount,rural
store_B,36,1004.37634,9.2360115,0.0,0,1,premium,urban store_C,24,horizon,482.94,7.1815,0,0,3,discount,rural
store_B,37,979.0918,10.923796,0.0,0,2,premium,urban store_C,25,horizon,571.69,7.2367,0,0,4,discount,rural
store_B,38,870.12354,10.817162,0.0,0,3,premium,urban store_C,26,horizon,666.89,7.2494,1,0,5,discount,rural
store_B,39,785.6754,10.399414,1.0,0,4,premium,urban store_C,27,horizon,677.55,7.5712,1,0,6,discount,rural
store_B,40,769.2815,9.53174,0.0,0,5,premium,urban store_C,28,horizon,503.9,7.4163,0,0,0,discount,rural
store_B,41,963.49274,10.938353,0.0,0,6,premium,urban store_C,29,horizon,541.34,7.0493,0,0,1,discount,rural
store_B,42,831.17865,10.557502,0.0,0,0,premium,urban store_C,30,horizon,443.17,7.3736,0,0,2,discount,rural
store_B,43,830.58295,10.433781,0.0,0,1,premium,urban store_C,31,horizon,596.87,7.5238,1,0,3,discount,rural
store_B,44,794.41534,9.898723,1.0,0,2,premium,urban store_C,32,horizon,628.12,7.1017,0,0,4,discount,rural
store_B,45,835.0851,9.544483,1.0,0,3,premium,urban store_C,33,horizon,586.61,7.8335,1,0,5,discount,rural
store_B,46,885.5207,9.192781,0.0,0,4,premium,urban store_C,34,horizon,456.82,7.052,0,0,6,discount,rural
store_B,47,1178.3236,10.805204,1.0,1,5,premium,urban store_C,35,horizon,782.3,7.9248,0,1,0,discount,rural
store_B,48,993.4054,9.911552,0.0,0,6,premium,urban
store_B,49,841.88434,9.404727,0.0,0,0,premium,urban
store_B,50,883.09314,9.611914,0.0,0,1,premium,urban
store_B,51,1036.8414,10.158439,1.0,1,2,premium,urban
store_B,52,903.3836,9.353545,0.0,0,3,premium,urban
store_B,53,965.40485,10.713228,0.0,0,4,premium,urban
store_B,54,1031.0249,10.517039,0.0,0,5,premium,urban
store_B,55,1094.0964,10.438926,1.0,0,6,premium,urban
store_B,56,988.38293,9.864186,1.0,0,0,premium,urban
store_B,57,911.7493,10.254618,0.0,0,1,premium,urban
store_B,58,1025.1101,10.168196,1.0,0,2,premium,urban
store_B,59,978.6775,10.299693,1.0,0,3,premium,urban
store_C,0,728.35284,10.130472,0.0,1,0,premium,urban
store_C,1,503.7172,10.529998,0.0,0,1,premium,urban
store_C,2,557.5812,10.269437,0.0,0,2,premium,urban
store_C,3,579.2723,10.107159,0.0,0,3,premium,urban
store_C,4,557.2319,10.118414,0.0,0,4,premium,urban
store_C,5,573.1017,9.607901,0.0,0,5,premium,urban
store_C,6,581.31024,9.061636,0.0,0,6,premium,urban
store_C,7,567.57776,9.873435,0.0,0,0,premium,urban
store_C,8,606.85065,9.42917,0.0,0,1,premium,urban
store_C,9,618.42255,9.817058,1.0,0,2,premium,urban
store_C,10,637.49005,10.706806,0.0,0,3,premium,urban
store_C,11,864.7779,9.467879,0.0,1,4,premium,urban
store_C,12,571.1436,9.116606,0.0,0,5,premium,urban
store_C,13,612.2043,9.562768,0.0,0,6,premium,urban
store_C,14,872.13513,9.587188,1.0,0,0,premium,urban
store_C,15,738.0299,10.323833,0.0,0,1,premium,urban
store_C,16,604.6675,10.114064,0.0,0,2,premium,urban
store_C,17,650.33057,10.567797,0.0,0,3,premium,urban
store_C,18,661.12146,10.328627,0.0,0,4,premium,urban
store_C,19,603.7142,9.812774,0.0,0,5,premium,urban
store_C,20,828.2985,10.62804,1.0,0,6,premium,urban
store_C,21,669.9662,9.333946,0.0,0,0,premium,urban
store_C,22,638.91095,9.045424,0.0,0,1,premium,urban
store_C,23,838.9723,9.180096,0.0,1,2,premium,urban
store_C,24,834.94836,10.444718,0.0,0,3,premium,urban
store_C,25,555.9125,9.923755,0.0,0,4,premium,urban
store_C,26,477.89877,9.322543,0.0,0,5,premium,urban
store_C,27,651.99023,10.0020895,0.0,0,6,premium,urban
store_C,28,535.84216,9.304625,1.0,0,0,premium,urban
store_C,29,523.2324,10.392641,1.0,0,1,premium,urban
store_C,30,595.6628,9.892313,0.0,0,2,premium,urban
store_C,31,429.21732,9.762042,0.0,0,3,premium,urban
store_C,32,595.64905,9.6030245,0.0,0,4,premium,urban
store_C,33,574.6885,10.260565,0.0,0,5,premium,urban
store_C,34,477.18958,9.723625,0.0,0,6,premium,urban
store_C,35,703.0953,9.1753,0.0,1,0,premium,urban
store_C,36,530.65405,9.2360115,0.0,0,1,premium,urban
store_C,37,506.09885,10.923796,0.0,0,2,premium,urban
store_C,38,417.0998,10.817162,0.0,0,3,premium,urban
store_C,39,526.0255,10.399414,1.0,0,4,premium,urban
store_C,40,635.823,9.53174,0.0,0,5,premium,urban
store_C,41,495.87946,10.938353,0.0,0,6,premium,urban
store_C,42,534.13354,10.557502,0.0,0,0,premium,urban
store_C,43,557.8907,10.433781,0.0,0,1,premium,urban
store_C,44,535.6469,9.898723,1.0,0,2,premium,urban
store_C,45,590.8869,9.544483,1.0,0,3,premium,urban
store_C,46,574.78455,9.192781,0.0,0,4,premium,urban
store_C,47,796.0737,10.805204,1.0,1,5,premium,urban
store_C,48,546.10583,9.911552,0.0,0,6,premium,urban
store_C,49,580.9428,9.404727,0.0,0,0,premium,urban
store_C,50,606.4677,9.611914,0.0,0,1,premium,urban
store_C,51,851.0876,10.158439,1.0,1,2,premium,urban
store_C,52,763.8405,9.353545,0.0,0,3,premium,urban
store_C,53,824.2607,10.713228,0.0,0,4,premium,urban
store_C,54,656.9345,10.517039,0.0,0,5,premium,urban
store_C,55,813.55115,10.438926,1.0,0,6,premium,urban
store_C,56,885.26666,9.864186,1.0,0,0,premium,urban
store_C,57,618.21106,10.254618,0.0,0,1,premium,urban
store_C,58,649.7526,10.168196,1.0,0,2,premium,urban
store_C,59,649.2765,10.299693,1.0,0,3,premium,urban
1 store_id week split sales price promotion holiday day_of_week store_type region
2 store_A 0 context 1212.6265 1372.64 10.130472 11.6299 0.0 1 1 0 premium urban
3 store_A 1 context 954.4545 965.54 10.529998 11.9757 0.0 0 0 1 premium urban
4 store_A 2 context 1066.0654 1076.92 10.269437 11.7269 0.0 0 0 2 premium urban
5 store_A 3 context 1095.3456 1094.09 10.107159 12.1698 0.0 0 0 3 premium urban
6 store_A 4 context 966.55225 970.18 10.118414 11.9372 0.0 0 0 4 premium urban
7 store_A 5 context 1024.5396 1010.04 9.607901 12.3327 0.0 0 0 5 premium urban
8 store_A 6 context 1121.4716 1098.7 9.061636 12.2003 0.0 0 0 6 premium urban
9 store_A 7 context 1096.5702 1097.79 9.873435 11.8124 0.0 0 0 0 premium urban
10 store_A 8 context 1132.875 1114.81 9.42917 12.3323 0.0 0 0 1 premium urban
11 store_A 9 context 1244.5522 1084.8 9.817058 12.3048 1.0 0 0 2 premium urban
12 store_A 10 context 1173.3354 1339.72 10.706806 11.8875 0.0 1 0 3 premium urban
13 store_A 11 context 1401.6262 1395.22 9.467879 11.7883 0.0 0 1 4 premium urban
14 store_A 12 context 1180.2404 1158.92 9.116606 12.1825 0.0 0 0 5 premium urban
15 store_A 13 context 1230.1067 1228.57 9.562768 11.6398 0.0 0 0 6 premium urban
16 store_A 14 context 1350.9026 1198.65 9.587188 11.6999 1.0 0 0 0 premium urban
17 store_A 15 context 1122.653 1138.98 10.323833 11.5074 0.0 0 0 1 premium urban
18 store_A 16 context 1189.6578 1186.2 10.114064 12.2869 0.0 0 0 2 premium urban
19 store_A 17 context 1114.2455 1122.3 10.567797 12.1649 0.0 0 0 3 premium urban
20 store_A 18 context 1209.6483 1212.12 10.328627 12.2052 0.0 0 0 4 premium urban
21 store_A 19 context 1171.0994 1161.74 9.812774 12.2807 0.0 0 0 5 premium urban
22 store_A 20 context 1294.5083 1157.89 10.62804 11.9589 1.0 0 0 6 premium urban
23 store_A 21 context 1141.081 1126.39 9.333946 12.0687 0.0 0 0 0 premium urban
24 store_A 22 context 1236.6909 1224.8 9.045424 11.6398 0.0 0 0 1 premium urban
25 store_A 23 context 1359.1321 1350.44 9.180096 11.6145 0.0 0 1 2 premium urban
26 store_A 24 horizon 1113.6208 1119.15 10.444718 12.1684 0.0 0 0 3 premium urban
27 store_A 25 horizon 1120.9719 1120.03 9.923755 11.9711 0.0 0 0 4 premium urban
28 store_A 26 horizon 1170.1646 1155.31 9.322543 12.0652 0.0 0 0 5 premium urban
29 store_A 27 horizon 1141.1768 1285.92 10.0020895 12.265 0.0 1 0 6 premium urban
30 store_A 28 horizon 1300.6125 1284.01 9.304625 12.1347 1.0 1 0 0 premium urban
31 store_A 29 horizon 1273.2278 1130.01 10.392641 12.0536 1.0 0 0 1 premium urban
32 store_A 30 horizon 1212.7638 1209.43 9.892313 12.0592 0.0 0 0 2 premium urban
33 store_A 31 horizon 1082.632 1231.79 9.762042 11.804 0.0 1 0 3 premium urban
34 store_A 32 horizon 1076.0151 1077.46 9.6030245 11.5308 0.0 0 0 4 premium urban
35 store_A 33 horizon 1044.249 1050.73 10.260565 11.9367 0.0 0 0 5 premium urban
36 store_A 34 horizon 1124.0281 1124.21 9.723625 11.7146 0.0 0 0 6 premium urban
37 store_A 35 horizon 1359.397 1344.73 9.1753 11.9085 0.0 0 1 0 premium urban
38 store_A store_B 36 0 context 1096.0808 1053.03 9.2360115 9.9735 0.0 1 0 1 1 0 premium standard urban suburban
39 store_A store_B 37 1 context 1027.4221 903.51 10.923796 9.767 0.0 1 0 2 1 premium standard urban suburban
40 store_A store_B 38 2 context 1033.1619 826.82 10.817162 9.8316 0.0 0 0 3 2 premium standard urban suburban
41 store_A store_B 39 3 context 1269.5414 709.93 10.399414 10.0207 1.0 0 0 4 3 premium standard urban suburban
42 store_A store_B 40 4 context 1147.2571 834.42 9.53174 9.9389 0.0 0 0 5 4 premium standard urban suburban
43 store_A store_B 41 5 context 1116.2965 847.01 10.938353 9.5216 0.0 0 0 6 5 premium standard urban suburban
44 store_A store_B 42 6 context 1072.0729 802.58 10.557502 10.3263 0.0 0 0 0 6 premium standard urban suburban
45 store_A store_B 43 7 context 1129.3868 770.87 10.433781 10.3962 0.0 0 0 1 0 premium standard urban suburban
46 store_A store_B 44 8 context 1295.5614 873.1 9.898723 9.6402 1.0 0 0 2 1 premium standard urban suburban
47 store_A store_B 45 9 context 1320.1937 844.74 9.544483 10.054 1.0 0 0 3 2 premium standard urban suburban
48 store_A store_B 46 10 context 1223.4036 1050.46 9.192781 9.6086 0.0 1 0 4 3 premium standard urban suburban
49 store_A store_B 47 11 context 1523.2692 1085.99 10.805204 10.1722 1.0 0 1 5 4 premium standard urban suburban
50 store_A store_B 48 12 context 1229.2423 978.74 9.911552 9.7812 0.0 0 0 6 5 premium standard urban suburban
51 store_A store_B 49 13 context 1224.824 1033.59 9.404727 10.1594 0.0 1 0 0 6 premium standard urban suburban
52 store_A store_B 50 14 context 1248.2861 846.06 9.611914 10.227 0.0 0 0 1 0 premium standard urban suburban
53 store_A store_B 51 15 context 1621.3419 906.93 10.158439 10.2686 1.0 0 1 0 2 1 premium standard urban suburban
54 store_A store_B 52 16 context 1200.0713 922.35 9.353545 9.6077 0.0 0 0 3 2 premium standard urban suburban
55 store_A store_B 53 17 context 1246.8055 1111.93 10.713228 10.416 0.0 1 0 4 3 premium standard urban suburban
56 store_A store_B 54 18 context 1260.0721 946.95 10.517039 9.7302 0.0 0 0 5 4 premium standard urban suburban
57 store_A store_B 55 19 context 1419.738 923.2 10.438926 9.5374 1.0 0 0 6 5 premium standard urban suburban
58 store_A store_B 56 20 context 1465.4315 963.38 9.864186 10.0549 1.0 0 0 0 6 premium standard urban suburban
59 store_A store_B 57 21 context 1411.4612 978.7 10.254618 9.8709 0.0 1 0 1 0 premium standard urban suburban
60 store_A store_B 58 22 context 1459.6567 840.39 10.168196 10.3298 1.0 0 0 2 1 premium standard urban suburban
61 store_A store_B 59 23 context 1562.2711 1019.22 10.299693 10.3083 1.0 0 0 1 3 2 premium standard urban suburban
62 store_B 0 24 horizon 949.5817 848.1 10.130472 9.8171 0.0 0 1 0 0 3 premium standard urban suburban
63 store_B 1 25 horizon 826.9795 777.91 10.529998 10.4529 0.0 0 0 1 4 premium standard urban suburban
64 store_B 2 26 horizon 795.8978 883.44 10.269437 9.7909 0.0 0 0 2 5 premium standard urban suburban
65 store_B 3 27 horizon 781.1968 827.78 10.107159 10.0151 0.0 0 0 3 6 premium standard urban suburban
66 store_B 4 28 horizon 869.75146 762.41 10.118414 9.756 0.0 0 0 4 0 premium standard urban suburban
67 store_B 5 29 horizon 840.91705 763.79 9.607901 10.436 0.0 0 0 5 1 premium standard urban suburban
68 store_B 6 30 horizon 900.90045 838.41 9.061636 9.6646 0.0 0 0 6 2 premium standard urban suburban
69 store_B 7 31 horizon 862.10693 860.45 9.873435 9.5449 0.0 0 0 0 3 premium standard urban suburban
70 store_B 8 32 horizon 811.1614 904.82 9.42917 9.9351 0.0 0 0 1 4 premium standard urban suburban
71 store_B 9 33 horizon 814.42114 1084.74 9.817058 10.4924 1.0 1 0 2 5 premium standard urban suburban
72 store_B 10 34 horizon 953.70746 808.09 10.706806 10.3917 0.0 0 0 3 6 premium standard urban suburban
73 store_B 11 35 horizon 1161.8647 938.26 9.467879 10.2486 0.0 0 1 4 0 premium standard urban suburban
74 store_B store_C 12 0 context 901.0838 709.43 9.116606 7.1053 0.0 0 0 1 5 0 premium discount urban rural
75 store_B store_C 13 1 context 896.9283 649.01 9.562768 7.0666 0.0 1 0 6 1 premium discount urban rural
76 store_B store_C 14 2 context 1121.0658 660.66 9.587188 7.5944 1.0 1 0 0 2 premium discount urban rural
77 store_B store_C 15 3 context 1012.14496 750.17 10.323833 7.1462 0.0 1 0 1 3 premium discount urban rural
78 store_B store_C 16 4 context 845.7787 726.88 10.114064 7.8247 0.0 1 0 2 4 premium discount urban rural
79 store_B store_C 17 5 context 942.0486 639.97 10.567797 7.3103 0.0 0 0 3 5 premium discount urban rural
80 store_B store_C 18 6 context 894.31323 580.71 10.328627 7.1439 0.0 0 0 4 6 premium discount urban rural
81 store_B store_C 19 7 context 1029.0061 549.13 9.812774 7.921 0.0 0 0 5 0 premium discount urban rural
82 store_B store_C 20 8 context 896.51886 597.79 10.62804 7.1655 1.0 0 0 6 1 premium discount urban rural
83 store_B store_C 21 9 context 1061.0464 627.48 9.333946 7.2847 0.0 0 0 0 2 premium discount urban rural
84 store_B store_C 22 10 context 963.2019 634.26 9.045424 7.1536 0.0 0 0 1 3 premium discount urban rural
85 store_B store_C 23 11 context 1091.6201 928.07 9.180096 7.1155 0.0 1 1 2 4 premium discount urban rural
86 store_B store_C 24 12 context 915.2826 643.37 10.444718 7.0211 0.0 0 0 3 5 premium discount urban rural
87 store_B store_C 25 13 context 771.0792 652.8 9.923755 7.0554 0.0 0 0 4 6 premium discount urban rural
88 store_B store_C 26 14 context 858.0784 766.65 9.322543 7.1746 0.0 0 0 5 0 premium discount urban rural
89 store_B store_C 27 15 context 814.89954 737.37 10.0020895 7.0534 0.0 0 0 6 1 premium discount urban rural
90 store_B store_C 28 16 context 916.48206 589.02 9.304625 7.5911 1.0 0 0 0 2 premium discount urban rural
91 store_B store_C 29 17 context 772.1533 613.06 10.392641 7.6807 1.0 0 0 1 3 premium discount urban rural
92 store_B store_C 30 18 context 803.5763 556.25 9.892313 7.3936 0.0 0 0 2 4 premium discount urban rural
93 store_B store_C 31 19 context 862.519 596.46 9.762042 7.318 0.0 0 0 3 5 premium discount urban rural
94 store_B store_C 32 20 context 737.1871 632.0 9.6030245 7.5045 0.0 0 0 4 6 premium discount urban rural
95 store_B store_C 33 21 context 785.4303 662.1 10.260565 7.875 0.0 0 0 5 0 premium discount urban rural
96 store_B store_C 34 22 context 906.9479 558.0 9.723625 7.8511 0.0 0 0 6 1 premium discount urban rural
97 store_B store_C 35 23 context 994.5817 769.38 9.1753 7.0435 0.0 0 1 0 2 premium discount urban rural
98 store_B store_C 36 24 horizon 1004.37634 482.94 9.2360115 7.1815 0.0 0 0 1 3 premium discount urban rural
99 store_B store_C 37 25 horizon 979.0918 571.69 10.923796 7.2367 0.0 0 0 2 4 premium discount urban rural
100 store_B store_C 38 26 horizon 870.12354 666.89 10.817162 7.2494 0.0 1 0 3 5 premium discount urban rural
101 store_B store_C 39 27 horizon 785.6754 677.55 10.399414 7.5712 1.0 1 0 4 6 premium discount urban rural
102 store_B store_C 40 28 horizon 769.2815 503.9 9.53174 7.4163 0.0 0 0 5 0 premium discount urban rural
103 store_B store_C 41 29 horizon 963.49274 541.34 10.938353 7.0493 0.0 0 0 6 1 premium discount urban rural
104 store_B store_C 42 30 horizon 831.17865 443.17 10.557502 7.3736 0.0 0 0 0 2 premium discount urban rural
105 store_B store_C 43 31 horizon 830.58295 596.87 10.433781 7.5238 0.0 1 0 1 3 premium discount urban rural
106 store_B store_C 44 32 horizon 794.41534 628.12 9.898723 7.1017 1.0 0 0 2 4 premium discount urban rural
107 store_B store_C 45 33 horizon 835.0851 586.61 9.544483 7.8335 1.0 1 0 3 5 premium discount urban rural
108 store_B store_C 46 34 horizon 885.5207 456.82 9.192781 7.052 0.0 0 0 4 6 premium discount urban rural
109 store_B store_C 47 35 horizon 1178.3236 782.3 10.805204 7.9248 1.0 0 1 5 0 premium discount urban rural
store_B 48 993.4054 9.911552 0.0 0 6 premium urban
store_B 49 841.88434 9.404727 0.0 0 0 premium urban
store_B 50 883.09314 9.611914 0.0 0 1 premium urban
store_B 51 1036.8414 10.158439 1.0 1 2 premium urban
store_B 52 903.3836 9.353545 0.0 0 3 premium urban
store_B 53 965.40485 10.713228 0.0 0 4 premium urban
store_B 54 1031.0249 10.517039 0.0 0 5 premium urban
store_B 55 1094.0964 10.438926 1.0 0 6 premium urban
store_B 56 988.38293 9.864186 1.0 0 0 premium urban
store_B 57 911.7493 10.254618 0.0 0 1 premium urban
store_B 58 1025.1101 10.168196 1.0 0 2 premium urban
store_B 59 978.6775 10.299693 1.0 0 3 premium urban
store_C 0 728.35284 10.130472 0.0 1 0 premium urban
store_C 1 503.7172 10.529998 0.0 0 1 premium urban
store_C 2 557.5812 10.269437 0.0 0 2 premium urban
store_C 3 579.2723 10.107159 0.0 0 3 premium urban
store_C 4 557.2319 10.118414 0.0 0 4 premium urban
store_C 5 573.1017 9.607901 0.0 0 5 premium urban
store_C 6 581.31024 9.061636 0.0 0 6 premium urban
store_C 7 567.57776 9.873435 0.0 0 0 premium urban
store_C 8 606.85065 9.42917 0.0 0 1 premium urban
store_C 9 618.42255 9.817058 1.0 0 2 premium urban
store_C 10 637.49005 10.706806 0.0 0 3 premium urban
store_C 11 864.7779 9.467879 0.0 1 4 premium urban
store_C 12 571.1436 9.116606 0.0 0 5 premium urban
store_C 13 612.2043 9.562768 0.0 0 6 premium urban
store_C 14 872.13513 9.587188 1.0 0 0 premium urban
store_C 15 738.0299 10.323833 0.0 0 1 premium urban
store_C 16 604.6675 10.114064 0.0 0 2 premium urban
store_C 17 650.33057 10.567797 0.0 0 3 premium urban
store_C 18 661.12146 10.328627 0.0 0 4 premium urban
store_C 19 603.7142 9.812774 0.0 0 5 premium urban
store_C 20 828.2985 10.62804 1.0 0 6 premium urban
store_C 21 669.9662 9.333946 0.0 0 0 premium urban
store_C 22 638.91095 9.045424 0.0 0 1 premium urban
store_C 23 838.9723 9.180096 0.0 1 2 premium urban
store_C 24 834.94836 10.444718 0.0 0 3 premium urban
store_C 25 555.9125 9.923755 0.0 0 4 premium urban
store_C 26 477.89877 9.322543 0.0 0 5 premium urban
store_C 27 651.99023 10.0020895 0.0 0 6 premium urban
store_C 28 535.84216 9.304625 1.0 0 0 premium urban
store_C 29 523.2324 10.392641 1.0 0 1 premium urban
store_C 30 595.6628 9.892313 0.0 0 2 premium urban
store_C 31 429.21732 9.762042 0.0 0 3 premium urban
store_C 32 595.64905 9.6030245 0.0 0 4 premium urban
store_C 33 574.6885 10.260565 0.0 0 5 premium urban
store_C 34 477.18958 9.723625 0.0 0 6 premium urban
store_C 35 703.0953 9.1753 0.0 1 0 premium urban
store_C 36 530.65405 9.2360115 0.0 0 1 premium urban
store_C 37 506.09885 10.923796 0.0 0 2 premium urban
store_C 38 417.0998 10.817162 0.0 0 3 premium urban
store_C 39 526.0255 10.399414 1.0 0 4 premium urban
store_C 40 635.823 9.53174 0.0 0 5 premium urban
store_C 41 495.87946 10.938353 0.0 0 6 premium urban
store_C 42 534.13354 10.557502 0.0 0 0 premium urban
store_C 43 557.8907 10.433781 0.0 0 1 premium urban
store_C 44 535.6469 9.898723 1.0 0 2 premium urban
store_C 45 590.8869 9.544483 1.0 0 3 premium urban
store_C 46 574.78455 9.192781 0.0 0 4 premium urban
store_C 47 796.0737 10.805204 1.0 1 5 premium urban
store_C 48 546.10583 9.911552 0.0 0 6 premium urban
store_C 49 580.9428 9.404727 0.0 0 0 premium urban
store_C 50 606.4677 9.611914 0.0 0 1 premium urban
store_C 51 851.0876 10.158439 1.0 1 2 premium urban
store_C 52 763.8405 9.353545 0.0 0 3 premium urban
store_C 53 824.2607 10.713228 0.0 0 4 premium urban
store_C 54 656.9345 10.517039 0.0 0 5 premium urban
store_C 55 813.55115 10.438926 1.0 0 6 premium urban
store_C 56 885.26666 9.864186 1.0 0 0 premium urban
store_C 57 618.21106 10.254618 0.0 0 1 premium urban
store_C 58 649.7526 10.168196 1.0 0 2 premium urban
store_C 59 649.2765 10.299693 1.0 0 3 premium urban