feat(timesfm): complete all three examples with quality docs

- anomaly-detection: full two-phase rewrite (context Z-score + forecast PI),
  2-panel viz, Sep 2023 correctly flagged CRITICAL (z=+3.03)
- covariates-forecasting: v3 rewrite with variable-shadowing bug fixed,
  2x2 shared-axis viz showing actionable covariate decomposition,
  108-row CSV with distinct per-store price arrays
- global-temperature: output/ subfolder reorganization (all 6 output files
  moved, 5 scripts + shell script paths updated)
- SKILL.md: added Examples table, Quality Checklist, Common Mistakes (8 items),
  Validation & Verification with regression assertions
- .gitattributes already at repo root covering all binary types
This commit is contained in:
Clayton Young
2026-02-21 19:03:56 -05:00
parent 509190118f
commit df58339850
20 changed files with 1542 additions and 812 deletions

View File

@@ -2,26 +2,27 @@
"""
TimesFM Covariates (XReg) Example
Demonstrates the TimesFM covariate API structure using synthetic retail
sales data. TimesFM 1.0 does NOT support forecast_with_covariates().
That feature requires TimesFM 2.5 + `timesfm[xreg]`.
Demonstrates the TimesFM covariate API using synthetic retail sales data.
TimesFM 1.0 does NOT support forecast_with_covariates(); that requires
TimesFM 2.5 + `pip install timesfm[xreg]`.
This script:
1. Generates synthetic 3-store retail data (24-week context, 12-week horizon)
2. Visualises each covariate type (dynamic numerical, dynamic categorical, static)
3. Prints the forecast_with_covariates() call signature for reference
4. Exports a compact CSV (90 rows) and metadata JSON
1. Generates synthetic 3-store weekly retail data (24-week context, 12-week horizon)
2. Produces a 2x2 visualization showing WHAT each covariate contributes
and WHY knowing them improves forecasts -- all panels share the same
week x-axis (0 = first context week, 35 = last horizon week)
3. Exports a compact CSV (108 rows) and metadata JSON
NOTE ON REAL DATA:
If you want to use a real retail dataset (e.g., Kaggle Rossmann Store Sales),
download it to a TEMP location do NOT commit large CSVs to this repo.
Example:
download it to a TEMP location -- do NOT commit large CSVs to this repo.
import tempfile, urllib.request
tmp = tempfile.mkdtemp(prefix="timesfm_retail_")
# urllib.request.urlretrieve("https://...store_sales.csv", f"{tmp}/store_sales.csv")
# df = pd.read_csv(f"{tmp}/store_sales.csv")
Users should persist the data wherever makes sense for their workflow;
this skills directory intentionally keeps only tiny reference datasets.
This skills directory intentionally keeps only tiny reference datasets.
"""
from __future__ import annotations
@@ -29,320 +30,421 @@ from __future__ import annotations
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# Note: TimesFM 1.0 does not support forecast_with_covariates
# This example demonstrates the API with TimesFM 2.5
# Installation: pip install timesfm[xreg]
EXAMPLE_DIR = Path(__file__).parent
OUTPUT_DIR = EXAMPLE_DIR / "output"
# Synthetic data configuration — kept SMALL (24 weeks context, 90 CSV rows)
N_STORES = 3
CONTEXT_LEN = 24 # weeks of history (was 48 — halved for token efficiency)
HORIZON_LEN = 12 # weeks to forecast
TOTAL_LEN = CONTEXT_LEN + HORIZON_LEN # 36 weeks total per store
CONTEXT_LEN = 24
HORIZON_LEN = 12
TOTAL_LEN = CONTEXT_LEN + HORIZON_LEN # 36
def generate_sales_data() -> dict:
"""Generate synthetic retail sales data with covariates.
"""Generate synthetic retail sales data with covariate components stored separately.
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.
Returns a dict with:
stores: {store_id: {sales, config}}
covariates: {price, promotion, holiday, day_of_week, store_type, region}
components: {store_id: {base, price_effect, promo_effect, holiday_effect}}
Components let us show 'what would sales look like without covariates?' --
the gap between 'base' and 'sales' IS the covariate signal.
BUG FIX v3: Previous versions had variable-shadowing where inner dict
comprehension `{store_id: ... for store_id in stores}` overwrote the outer
loop variable causing all stores to get identical covariate arrays.
Fixed by accumulating per-store arrays separately before building covariate dict.
"""
rng = np.random.default_rng(42)
# Store configurations
stores = {
"store_A": {"type": "premium", "region": "urban", "base_sales": 1000},
"store_B": {"type": "standard", "region": "suburban", "base_sales": 750},
"store_C": {"type": "discount", "region": "rural", "base_sales": 500},
}
base_prices = {"store_A": 12.0, "store_B": 10.0, "store_C": 7.5}
data: dict = {"stores": {}, "covariates": {}}
data: dict = {"stores": {}, "covariates": {}, "components": {}}
# 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] = {}
dow_by_store: dict[str, np.ndarray] = {}
for store_id, config in stores.items():
bp = base_prices[store_id]
weeks = np.arange(TOTAL_LEN)
trend = config["base_sales"] * (1 + 0.005 * weeks)
seasonality = 100 * np.sin(2 * np.pi * weeks / 52)
noise = rng.normal(0, 50, TOTAL_LEN)
seasonality = 80 * np.sin(2 * np.pi * weeks / 52)
noise = rng.normal(0, 40, TOTAL_LEN)
base = (trend + seasonality + noise).astype(np.float32)
# Price — slightly different range per store to reflect market positioning
base_price = {"store_A": 12.0, "store_B": 10.0, "store_C": 7.5}[store_id]
price = base_price + rng.uniform(-0.5, 0.5, TOTAL_LEN)
price_effect = -20 * (price - base_price)
price = (bp + rng.uniform(-0.5, 0.5, TOTAL_LEN)).astype(np.float32)
price_effect = (-20 * (price - bp)).astype(np.float32)
# Holidays (major retail weeks)
holidays = np.zeros(TOTAL_LEN)
holidays = np.zeros(TOTAL_LEN, dtype=np.float32)
for hw in [0, 11, 23, 35]:
if hw < TOTAL_LEN:
holidays[hw] = 1.0
holiday_effect = 200 * holidays
holiday_effect = (200 * holidays).astype(np.float32)
# Promotion — random 20% of weeks
promotion = rng.choice([0.0, 1.0], TOTAL_LEN, p=[0.8, 0.2])
promo_effect = 150 * promotion
promotion = rng.choice([0.0, 1.0], TOTAL_LEN, p=[0.8, 0.2]).astype(np.float32)
promo_effect = (150 * promotion).astype(np.float32)
# Day-of-week proxy (weekly granularity → repeat 0-6 pattern)
day_of_week = np.tile(np.arange(7), TOTAL_LEN // 7 + 1)[:TOTAL_LEN]
sales = (
trend + seasonality + noise + price_effect + holiday_effect + promo_effect
day_of_week = np.tile(np.arange(7), TOTAL_LEN // 7 + 1)[:TOTAL_LEN].astype(
np.int32
)
sales = np.maximum(sales, 50.0).astype(np.float32)
sales = np.maximum(base + price_effect + holiday_effect + promo_effect, 50.0)
data["stores"][store_id] = {"sales": sales, "config": config}
data["components"][store_id] = {
"base": base,
"price_effect": price_effect,
"promo_effect": promo_effect,
"holiday_effect": holiday_effect,
}
prices_by_store[store_id] = price.astype(np.float32)
promos_by_store[store_id] = promotion.astype(np.float32)
holidays_by_store[store_id] = holidays.astype(np.float32)
day_of_week_by_store[store_id] = day_of_week.astype(np.int32)
prices_by_store[store_id] = price
promos_by_store[store_id] = promotion
holidays_by_store[store_id] = holidays
dow_by_store[store_id] = day_of_week
# Build covariates dict AFTER the loop (avoids shadowing bug)
data["covariates"] = {
"price": prices_by_store,
"promotion": promos_by_store,
"holiday": holidays_by_store,
"day_of_week": day_of_week_by_store,
"day_of_week": dow_by_store,
"store_type": {sid: stores[sid]["type"] for sid in stores},
"region": {sid: stores[sid]["region"] for sid in stores},
}
return data
def demonstrate_api() -> None:
"""Print the forecast_with_covariates API structure (TimesFM 2.5)."""
print("\n" + "=" * 70)
print(" TIMESFM COVARIATES API (TimesFM 2.5)")
print("=" * 70)
api_code = """
# Installation
pip install timesfm[xreg]
# Import
import timesfm
# Load TimesFM 2.5 (supports covariates)
hparams = timesfm.TimesFmHparams(
backend="cpu", # or "gpu"
per_core_batch_size=32,
horizon_len=12,
)
checkpoint = timesfm.TimesFmCheckpoint(
huggingface_repo_id="google/timesfm-2.5-200m-pytorch"
)
model = timesfm.TimesFm(hparams=hparams, checkpoint=checkpoint)
# Prepare inputs
inputs = [sales_store_a, sales_store_b, sales_store_c] # List of historical sales
# Dynamic numerical covariates (context + horizon values per series)
dynamic_numerical_covariates = {
"price": [
price_history_store_a, # Shape: (context_len + horizon_len,)
price_history_store_b,
price_history_store_c,
],
"promotion": [promo_a, promo_b, promo_c],
}
# Dynamic categorical covariates
dynamic_categorical_covariates = {
"holiday": [holiday_a, holiday_b, holiday_c], # 0 or 1 flags
"day_of_week": [dow_a, dow_b, dow_c], # 0-6 integer values
}
# Static categorical covariates (one value per series)
static_categorical_covariates = {
"store_type": ["premium", "standard", "discount"],
"region": ["urban", "suburban", "rural"],
}
# Forecast with covariates
point_forecast, quantile_forecast = model.forecast_with_covariates(
inputs=inputs,
dynamic_numerical_covariates=dynamic_numerical_covariates,
dynamic_categorical_covariates=dynamic_categorical_covariates,
static_categorical_covariates=static_categorical_covariates,
xreg_mode="xreg + timesfm", # or "timesfm + xreg"
ridge=0.0, # Ridge regularization
normalize_xreg_target_per_input=True,
)
# Output shapes
# point_forecast: (num_series, horizon_len)
# quantile_forecast: (num_series, horizon_len, 10)
"""
print(api_code)
def explain_xreg_modes() -> None:
"""Explain the two XReg modes."""
print("\n" + "=" * 70)
print(" XREG MODES EXPLAINED")
print("=" * 70)
print("""
┌─────────────────────────────────────────────────────────────────────┐
│ Mode 1: "xreg + timesfm" (DEFAULT) │
├─────────────────────────────────────────────────────────────────────┤
│ 1. TimesFM makes baseline forecast (ignoring covariates) │
│ 2. Calculate residuals: actual - baseline │
│ 3. Fit linear regression: residuals ~ covariates │
│ 4. Final forecast = TimesFM baseline + XReg adjustment │
│ │
│ Best for: Covariates capture residual patterns │
│ (e.g., promotions affecting baseline sales) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Mode 2: "timesfm + xreg"
├─────────────────────────────────────────────────────────────────────┤
│ 1. Fit linear regression: target ~ covariates │
│ 2. Calculate residuals: actual - regression_prediction │
│ 3. TimesFM forecasts residuals │
│ 4. Final forecast = XReg prediction + TimesFM residual forecast │
│ │
│ Best for: Covariates explain main signal │
│ (e.g., temperature driving ice cream sales) │
└─────────────────────────────────────────────────────────────────────┘
""")
def create_visualization(data: dict) -> None:
"""Create visualization of sales data with covariates."""
"""
2x2 figure -- ALL panels share x-axis = weeks 0-35.
(0,0) Sales by store -- context solid, horizon dashed
(0,1) Store A: actual vs baseline (no covariates), with event overlays showing uplift
(1,0) Price covariate for all stores -- full 36 weeks including horizon
(1,1) Covariate effect decomposition for Store A (stacked fill_between)
Each panel has a conclusion annotation box explaining what the data shows.
"""
OUTPUT_DIR.mkdir(exist_ok=True)
fig, axes = plt.subplots(3, 2, figsize=(16, 12))
store_colors = {"store_A": "#1a56db", "store_B": "#057a55", "store_C": "#c03221"}
weeks = np.arange(TOTAL_LEN)
context_weeks = weeks[:CONTEXT_LEN]
# Panel 1 — Sales by store (context only)
ax = axes[0, 0]
for store_id, store_data in data["stores"].items():
ax.plot(
context_weeks,
store_data["sales"][:CONTEXT_LEN],
label=f"{store_id} ({store_data['config']['type']})",
linewidth=2,
)
ax.axvline(
x=CONTEXT_LEN - 0.5, color="red", linestyle="--", label="Forecast Start →"
fig, axes = plt.subplots(
2,
2,
figsize=(16, 11),
sharex=True,
gridspec_kw={"hspace": 0.42, "wspace": 0.32},
)
ax.set_xlabel("Week")
ax.set_ylabel("Sales")
ax.set_title("Historical Sales by Store (24-week context)")
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 2 — Price covariate (all weeks including horizon)
ax = axes[0, 1]
for store_id in data["stores"]:
ax.plot(weeks, data["covariates"]["price"][store_id], label=store_id, alpha=0.8)
ax.axvline(x=CONTEXT_LEN - 0.5, color="red", linestyle="--")
ax.set_xlabel("Week")
ax.set_ylabel("Price ($)")
ax.set_title("Dynamic Numerical Covariate: Price\n(different baseline per store)")
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 3 — Holiday flag
ax = axes[1, 0]
# Show all 3 stores' holidays side by side (they're the same here but could differ)
ax.bar(weeks, data["covariates"]["holiday"]["store_A"], alpha=0.7, color="orange")
ax.axvline(x=CONTEXT_LEN - 0.5, color="red", linestyle="--")
ax.set_xlabel("Week")
ax.set_ylabel("Holiday Flag")
ax.set_title("Dynamic Categorical Covariate: Holiday")
ax.grid(True, alpha=0.3)
# Panel 4 — Promotion (store_A example — each store differs)
ax = axes[1, 1]
for store_id in data["stores"]:
ax.bar(
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_ylabel("Promotion Flag")
ax.set_title("Dynamic Categorical Covariate: Promotion\n(independent per store)")
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 5 — Store type (static)
ax = axes[2, 0]
store_types = [data["covariates"]["store_type"][s] for s in data["stores"]]
store_ids = list(data["stores"].keys())
colors = {"premium": "gold", "standard": "silver", "discount": "#cd7f32"}
ax.bar(store_ids, [1, 1, 1], color=[colors[t] for t in store_types])
ax.set_ylabel("Store Type")
ax.set_title("Static Categorical Covariate: Store Type")
ax.set_yticks([])
for i, (sid, t) in enumerate(zip(store_ids, store_types)):
ax.text(i, 0.5, t, ha="center", va="center", fontweight="bold", fontsize=11)
# Panel 6 — Data structure summary
ax = axes[2, 1]
ax.axis("off")
summary_text = (
" COVARIATE DATA STRUCTURE\n"
" ─────────────────────────\n\n"
" Dynamic Numerical Covariates:\n"
" • price: array[context_len + horizon_len] per series\n"
" • promotion: array[context_len + horizon_len] per series\n\n"
" Dynamic Categorical Covariates:\n"
" • holiday: array[context_len + horizon_len] per series\n"
" • day_of_week: array[context_len + horizon_len] per series\n\n"
" Static Categorical Covariates:\n"
" • store_type: ['premium', 'standard', 'discount']\n"
" • region: ['urban', 'suburban', 'rural']\n\n"
" ⚠ Future covariate values must be KNOWN at forecast time!\n"
" (Prices, promotion schedules, and holidays are planned.)"
)
ax.text(
0.05,
0.5,
summary_text,
transform=ax.transAxes,
fontfamily="monospace",
fontsize=9,
verticalalignment="center",
)
plt.suptitle(
"TimesFM Covariates (XReg) — Synthetic Retail Sales Demo",
fontsize=14,
fig.suptitle(
"TimesFM Covariates (XReg) -- Retail Sales with Exogenous Variables\n"
"Shared x-axis: Week 0-23 = context (observed) | Week 24-35 = forecast horizon",
fontsize=13,
fontweight="bold",
y=1.01,
)
plt.tight_layout()
def add_divider(ax, label_top=True):
ax.axvline(CONTEXT_LEN - 0.5, color="#9ca3af", lw=1.3, ls="--", alpha=0.8)
ax.axvspan(
CONTEXT_LEN - 0.5, TOTAL_LEN - 0.5, alpha=0.06, color="grey", zorder=0
)
if label_top:
ax.text(
CONTEXT_LEN + 0.3,
1.01,
"<- horizon ->",
transform=ax.get_xaxis_transform(),
fontsize=7.5,
color="#6b7280",
style="italic",
)
# -- (0,0): Sales by Store ---------------------------------------------------
ax = axes[0, 0]
base_price_labels = {"store_A": "$12", "store_B": "$10", "store_C": "$7.50"}
for sid, store_data in data["stores"].items():
sales = store_data["sales"]
c = store_colors[sid]
lbl = f"{sid} ({store_data['config']['type']}, {base_price_labels[sid]} base)"
ax.plot(
weeks[:CONTEXT_LEN],
sales[:CONTEXT_LEN],
color=c,
lw=2,
marker="o",
ms=3,
label=lbl,
)
ax.plot(
weeks[CONTEXT_LEN:],
sales[CONTEXT_LEN:],
color=c,
lw=1.5,
ls="--",
marker="o",
ms=3,
alpha=0.6,
)
add_divider(ax)
ax.set_ylabel("Weekly Sales (units)", fontsize=10)
ax.set_title("Sales by Store", fontsize=11, fontweight="bold")
ax.legend(fontsize=7.5, loc="upper left")
ax.grid(True, alpha=0.22)
ratio = (
data["stores"]["store_A"]["sales"][:CONTEXT_LEN].mean()
/ data["stores"]["store_C"]["sales"][:CONTEXT_LEN].mean()
)
ax.annotate(
f"Store A earns {ratio:.1f}x Store C\n(premium vs discount pricing)\n"
f"-> store_type is a useful static covariate",
xy=(0.97, 0.05),
xycoords="axes fraction",
ha="right",
fontsize=8,
bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
)
# -- (0,1): Store A actual vs baseline ---------------------------------------
ax = axes[0, 1]
comp_A = data["components"]["store_A"]
sales_A = data["stores"]["store_A"]["sales"]
base_A = comp_A["base"]
promo_A = data["covariates"]["promotion"]["store_A"]
holiday_A = data["covariates"]["holiday"]["store_A"]
ax.plot(
weeks[:CONTEXT_LEN],
base_A[:CONTEXT_LEN],
color="#9ca3af",
lw=1.8,
ls="--",
label="Baseline (no covariates)",
)
ax.fill_between(
weeks[:CONTEXT_LEN],
base_A[:CONTEXT_LEN],
sales_A[:CONTEXT_LEN],
where=(sales_A[:CONTEXT_LEN] > base_A[:CONTEXT_LEN]),
alpha=0.35,
color="#22c55e",
label="Covariate uplift",
)
ax.fill_between(
weeks[:CONTEXT_LEN],
sales_A[:CONTEXT_LEN],
base_A[:CONTEXT_LEN],
where=(sales_A[:CONTEXT_LEN] < base_A[:CONTEXT_LEN]),
alpha=0.30,
color="#ef4444",
label="Price suppression",
)
ax.plot(
weeks[:CONTEXT_LEN],
sales_A[:CONTEXT_LEN],
color=store_colors["store_A"],
lw=2,
label="Actual sales (Store A)",
)
for w in range(CONTEXT_LEN):
if holiday_A[w] > 0:
ax.axvspan(w - 0.45, w + 0.45, alpha=0.22, color="darkorange", zorder=0)
promo_weeks = [w for w in range(CONTEXT_LEN) if promo_A[w] > 0]
if promo_weeks:
ax.scatter(
promo_weeks,
sales_A[promo_weeks],
marker="^",
color="#16a34a",
s=70,
zorder=6,
label="Promotion week",
)
add_divider(ax)
ax.set_ylabel("Weekly Sales (units)", fontsize=10)
ax.set_title(
"Store A -- Actual vs Baseline (No Covariates)", fontsize=11, fontweight="bold"
)
ax.legend(fontsize=7.5, loc="upper left", ncol=2)
ax.grid(True, alpha=0.22)
hm = holiday_A[:CONTEXT_LEN] > 0
pm = promo_A[:CONTEXT_LEN] > 0
h_lift = (
(sales_A[:CONTEXT_LEN][hm] - base_A[:CONTEXT_LEN][hm]).mean() if hm.any() else 0
)
p_lift = (
(sales_A[:CONTEXT_LEN][pm] - base_A[:CONTEXT_LEN][pm]).mean() if pm.any() else 0
)
ax.annotate(
f"Holiday weeks: +{h_lift:.0f} units avg\n"
f"Promotion weeks: +{p_lift:.0f} units avg\n"
f"Future event schedules must be known for XReg",
xy=(0.97, 0.05),
xycoords="axes fraction",
ha="right",
fontsize=8,
bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
)
# -- (1,0): Price covariate -- full 36 weeks ---------------------------------
ax = axes[1, 0]
for sid in data["stores"]:
ax.plot(
weeks,
data["covariates"]["price"][sid],
color=store_colors[sid],
lw=2,
label=sid,
alpha=0.85,
)
add_divider(ax, label_top=False)
ax.set_xlabel("Week", fontsize=10)
ax.set_ylabel("Price ($)", fontsize=10)
ax.set_title(
"Price Covariate -- Context + Forecast Horizon", fontsize=11, fontweight="bold"
)
ax.legend(fontsize=8, loc="upper right")
ax.grid(True, alpha=0.22)
ax.annotate(
"Prices are planned -- known for forecast horizon\n"
"Price elasticity: -$1 increase -> -20 units sold\n"
"Store A ($12) consistently more expensive than C ($7.50)",
xy=(0.97, 0.05),
xycoords="axes fraction",
ha="right",
fontsize=8,
bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
)
# -- (1,1): Covariate effect decomposition -----------------------------------
ax = axes[1, 1]
pe = comp_A["price_effect"]
pre = comp_A["promo_effect"]
he = comp_A["holiday_effect"]
ax.fill_between(
weeks,
0,
pe,
alpha=0.65,
color="steelblue",
step="mid",
label=f"Price effect (max +/-{np.abs(pe).max():.0f} units)",
)
ax.fill_between(
weeks,
pe,
pe + pre,
alpha=0.70,
color="#22c55e",
step="mid",
label="Promotion effect (+150 units)",
)
ax.fill_between(
weeks,
pe + pre,
pe + pre + he,
alpha=0.70,
color="darkorange",
step="mid",
label="Holiday effect (+200 units)",
)
total = pe + pre + he
ax.plot(weeks, total, "k-", lw=1.5, alpha=0.75, label="Total covariate effect")
ax.axhline(0, color="black", lw=0.9, alpha=0.6)
add_divider(ax, label_top=False)
ax.set_xlabel("Week", fontsize=10)
ax.set_ylabel("Effect on sales (units)", fontsize=10)
ax.set_title(
"Store A -- Covariate Effect Decomposition", fontsize=11, fontweight="bold"
)
ax.legend(fontsize=7.5, loc="upper right")
ax.grid(True, alpha=0.22, axis="y")
ax.annotate(
f"Holidays (+200) and promotions (+150) dominate\n"
f"Price effect (+/-{np.abs(pe).max():.0f} units) is minor by comparison\n"
f"-> Time-varying covariates explain most sales spikes",
xy=(0.97, 0.55),
xycoords="axes fraction",
ha="right",
fontsize=8,
bbox=dict(boxstyle="round", fc="#fffbe6", ec="#d4a017", alpha=0.95),
)
tick_pos = list(range(0, TOTAL_LEN, 4))
for row in [0, 1]:
for col in [0, 1]:
axes[row, col].set_xticks(tick_pos)
plt.tight_layout()
output_path = OUTPUT_DIR / "covariates_data.png"
plt.savefig(output_path, dpi=150, bbox_inches="tight")
print(f"\n📊 Saved visualization: {output_path}")
plt.close()
print(f"\n Saved visualization: {output_path}")
def demonstrate_api() -> None:
print("\n" + "=" * 70)
print(" TIMESFM COVARIATES API (TimesFM 2.5)")
print("=" * 70)
print("""
# Installation
pip install timesfm[xreg]
import timesfm
hparams = timesfm.TimesFmHparams(backend="cpu", per_core_batch_size=32, horizon_len=12)
ckpt = timesfm.TimesFmCheckpoint(huggingface_repo_id="google/timesfm-2.5-200m-pytorch")
model = timesfm.TimesFm(hparams=hparams, checkpoint=ckpt)
point_fc, quant_fc = model.forecast_with_covariates(
inputs=[sales_a, sales_b, sales_c],
dynamic_numerical_covariates={"price": [price_a, price_b, price_c]},
dynamic_categorical_covariates={"holiday": [hol_a, hol_b, hol_c]},
static_categorical_covariates={"store_type": ["premium","standard","discount"]},
xreg_mode="xreg + timesfm",
normalize_xreg_target_per_input=True,
)
# point_fc: (num_series, horizon_len)
# quant_fc: (num_series, horizon_len, 10)
""")
def explain_xreg_modes() -> None:
print("\n" + "=" * 70)
print(" XREG MODES")
print("=" * 70)
print("""
"xreg + timesfm" (DEFAULT)
1. TimesFM makes baseline forecast
2. Fit regression on residuals (actual - baseline) ~ covariates
3. Final = TimesFM baseline + XReg adjustment
Best when: covariates explain residual variation (e.g. promotions)
"timesfm + xreg"
1. Fit regression: target ~ covariates
2. TimesFM forecasts the residuals
3. Final = XReg prediction + TimesFM residual forecast
Best when: covariates explain the main signal (e.g. temperature)
""")
def main() -> None:
@@ -350,27 +452,22 @@ def main() -> None:
print(" TIMESFM COVARIATES (XREG) EXAMPLE")
print("=" * 70)
# Generate synthetic data
print("\n📊 Generating synthetic retail sales data...")
print("\n Generating synthetic retail sales data...")
data = generate_sales_data()
print(f" Stores: {list(data['stores'].keys())}")
print(f" Context length: {CONTEXT_LEN} weeks")
print(f" Horizon length: {HORIZON_LEN} weeks")
print(f" Covariates: {list(data['covariates'].keys())}")
print(f" Stores: {list(data['stores'].keys())}")
print(f" Context length: {CONTEXT_LEN} weeks")
print(f" Horizon length: {HORIZON_LEN} weeks")
print(f" Covariates: {list(data['covariates'].keys())}")
# Show API
demonstrate_api()
# Explain modes
explain_xreg_modes()
# Create visualization
print("\n📊 Creating data visualization...")
print("\n Creating 2x2 visualization (shared x-axis)...")
create_visualization(data)
# Save data
print("\n💾 Saving synthetic data...")
print("\n Saving output data...")
OUTPUT_DIR.mkdir(exist_ok=True)
records = []
for store_id, store_data in data["stores"].items():
@@ -381,7 +478,13 @@ def main() -> None:
"week": i,
"split": "context" if i < CONTEXT_LEN else "horizon",
"sales": round(float(store_data["sales"][i]), 2),
"base_sales": round(
float(data["components"][store_id]["base"][i]), 2
),
"price": round(float(data["covariates"]["price"][store_id][i]), 4),
"price_effect": round(
float(data["components"][store_id]["price_effect"][i]), 2
),
"promotion": int(data["covariates"]["promotion"][store_id][i]),
"holiday": int(data["covariates"]["holiday"][store_id][i]),
"day_of_week": int(data["covariates"]["day_of_week"][store_id][i]),
@@ -393,17 +496,23 @@ def main() -> None:
df = pd.DataFrame(records)
csv_path = OUTPUT_DIR / "sales_with_covariates.csv"
df.to_csv(csv_path, index=False)
print(f" Saved: {csv_path} ({len(df)} rows × {len(df.columns)} cols)")
print(f" Saved: {csv_path} ({len(df)} rows x {len(df.columns)} cols)")
# Save metadata
metadata = {
"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."
"For real datasets (e.g., Kaggle Rossmann Store Sales), download to "
"tempfile.mkdtemp() -- do NOT commit to this repo."
),
"stores": {sid: sdata["config"] for sid, sdata in data["stores"].items()},
"stores": {
sid: {
**sdata["config"],
"mean_sales_context": round(
float(sdata["sales"][:CONTEXT_LEN].mean()), 1
),
}
for sid, sdata in data["stores"].items()
},
"dimensions": {
"context_length": CONTEXT_LEN,
"horizon_length": HORIZON_LEN,
@@ -412,20 +521,23 @@ def main() -> None:
"csv_rows": len(df),
},
"covariates": {
"dynamic_numerical": ["price", "promotion"],
"dynamic_categorical": ["holiday", "day_of_week"],
"dynamic_numerical": ["price"],
"dynamic_categorical": ["promotion", "holiday", "day_of_week"],
"static_categorical": ["store_type", "region"],
},
"xreg_modes": {
"xreg + timesfm": "Fit regression on residuals after TimesFM forecast",
"timesfm + xreg": "TimesFM forecasts residuals after regression fit",
"effect_magnitudes": {
"holiday": "+200 units per holiday week",
"promotion": "+150 units per promotion week",
"price": "-20 units per $1 above base price",
},
"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).",
"xreg_modes": {
"xreg + timesfm": "Regression on TimesFM residuals (default)",
"timesfm + xreg": "TimesFM on regression residuals",
},
"bug_fixes_history": [
"v1: Variable-shadowing -- all stores had identical covariates",
"v2: Fixed shadowing; CONTEXT_LEN 48->24",
"v3: Added component decomposition (base, price/promo/holiday effects); 2x2 sharex viz",
],
}
@@ -434,38 +546,21 @@ def main() -> None:
json.dump(metadata, f, indent=2)
print(f" Saved: {meta_path}")
# Summary
print("\n" + "=" * 70)
print(" COVARIATES EXAMPLE COMPLETE")
print(" COVARIATES EXAMPLE COMPLETE")
print("=" * 70)
print("""
💡 Key Points:
Key points:
1. Requires timesfm[xreg] + TimesFM 2.5+ for actual inference
2. Dynamic covariates need values for BOTH context AND horizon (future must be known!)
3. Static covariates: one value per series (store_type, region)
4. All 4 visualization panels share the same week x-axis (0-35)
5. Effect decomposition shows holidays/promotions dominate over price variation
1. INSTALLATION: Requires timesfm[xreg] extra
pip install timesfm[xreg]
2. COVARIATE TYPES:
• Dynamic Numerical: time-varying numeric (price, promotion)
• Dynamic Categorical: time-varying flags (holiday, day_of_week)
• Static Categorical: fixed per series (store_type, region)
3. DATA REQUIREMENTS:
• Dynamic covariates need values for context + horizon
• Future values must be known (prices, scheduled holidays, etc.)
4. XREG MODES:
"xreg + timesfm" (default): Regression on residuals
"timesfm + xreg": TimesFM on residuals after regression
5. LIMITATIONS:
• Requires TimesFM 2.5+ (v1.0 does not support XReg)
• String categoricals work but int encoding is faster
📁 Output Files:
• output/covariates_data.png — visualization (6 panels)
• output/sales_with_covariates.csv — 90-row compact dataset
• output/covariates_metadata.json — metadata + bug-fix log
Output files:
output/covariates_data.png -- 2x2 visualization with conclusions
output/sales_with_covariates.csv -- 108-row compact dataset
output/covariates_metadata.json -- metadata + effect magnitudes
""")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 448 KiB

View File

@@ -1,21 +1,24 @@
{
"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.",
"note_on_real_data": "For real datasets (e.g., Kaggle Rossmann Store Sales), download to tempfile.mkdtemp() -- do NOT commit to this repo.",
"stores": {
"store_A": {
"type": "premium",
"region": "urban",
"base_sales": 1000
"base_sales": 1000,
"mean_sales_context": 1148.7
},
"store_B": {
"type": "standard",
"region": "suburban",
"base_sales": 750
"base_sales": 750,
"mean_sales_context": 907.0
},
"store_C": {
"type": "discount",
"region": "rural",
"base_sales": 500
"base_sales": 500,
"mean_sales_context": 645.3
}
},
"dimensions": {
@@ -27,10 +30,10 @@
},
"covariates": {
"dynamic_numerical": [
"price",
"promotion"
"price"
],
"dynamic_categorical": [
"promotion",
"holiday",
"day_of_week"
],
@@ -39,12 +42,18 @@
"region"
]
},
"xreg_modes": {
"xreg + timesfm": "Fit regression on residuals after TimesFM forecast",
"timesfm + xreg": "TimesFM forecasts residuals after regression fit"
"effect_magnitudes": {
"holiday": "+200 units per holiday week",
"promotion": "+150 units per promotion week",
"price": "-20 units per $1 above base price"
},
"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)."
"xreg_modes": {
"xreg + timesfm": "Regression on TimesFM residuals (default)",
"timesfm + xreg": "TimesFM on regression residuals"
},
"bug_fixes_history": [
"v1: Variable-shadowing -- all stores had identical covariates",
"v2: Fixed shadowing; CONTEXT_LEN 48->24",
"v3: Added component decomposition (base, price/promo/holiday effects); 2x2 sharex viz"
]
}

View File

@@ -1,109 +1,109 @@
store_id,week,split,sales,price,promotion,holiday,day_of_week,store_type,region
store_A,0,context,1372.64,11.6299,1,1,0,premium,urban
store_A,1,context,965.54,11.9757,0,0,1,premium,urban
store_A,2,context,1076.92,11.7269,0,0,2,premium,urban
store_A,3,context,1094.09,12.1698,0,0,3,premium,urban
store_A,4,context,970.18,11.9372,0,0,4,premium,urban
store_A,5,context,1010.04,12.3327,0,0,5,premium,urban
store_A,6,context,1098.7,12.2003,0,0,6,premium,urban
store_A,7,context,1097.79,11.8124,0,0,0,premium,urban
store_A,8,context,1114.81,12.3323,0,0,1,premium,urban
store_A,9,context,1084.8,12.3048,0,0,2,premium,urban
store_A,10,context,1339.72,11.8875,1,0,3,premium,urban
store_A,11,context,1395.22,11.7883,0,1,4,premium,urban
store_A,12,context,1158.92,12.1825,0,0,5,premium,urban
store_A,13,context,1228.57,11.6398,0,0,6,premium,urban
store_A,14,context,1198.65,11.6999,0,0,0,premium,urban
store_A,15,context,1138.98,11.5074,0,0,1,premium,urban
store_A,16,context,1186.2,12.2869,0,0,2,premium,urban
store_A,17,context,1122.3,12.1649,0,0,3,premium,urban
store_A,18,context,1212.12,12.2052,0,0,4,premium,urban
store_A,19,context,1161.74,12.2807,0,0,5,premium,urban
store_A,20,context,1157.89,11.9589,0,0,6,premium,urban
store_A,21,context,1126.39,12.0687,0,0,0,premium,urban
store_A,22,context,1224.8,11.6398,0,0,1,premium,urban
store_A,23,context,1350.44,11.6145,0,1,2,premium,urban
store_A,24,horizon,1119.15,12.1684,0,0,3,premium,urban
store_A,25,horizon,1120.03,11.9711,0,0,4,premium,urban
store_A,26,horizon,1155.31,12.0652,0,0,5,premium,urban
store_A,27,horizon,1285.92,12.265,1,0,6,premium,urban
store_A,28,horizon,1284.01,12.1347,1,0,0,premium,urban
store_A,29,horizon,1130.01,12.0536,0,0,1,premium,urban
store_A,30,horizon,1209.43,12.0592,0,0,2,premium,urban
store_A,31,horizon,1231.79,11.804,1,0,3,premium,urban
store_A,32,horizon,1077.46,11.5308,0,0,4,premium,urban
store_A,33,horizon,1050.73,11.9367,0,0,5,premium,urban
store_A,34,horizon,1124.21,11.7146,0,0,6,premium,urban
store_A,35,horizon,1344.73,11.9085,0,1,0,premium,urban
store_B,0,context,1053.03,9.9735,1,1,0,standard,suburban
store_B,1,context,903.51,9.767,1,0,1,standard,suburban
store_B,2,context,826.82,9.8316,0,0,2,standard,suburban
store_B,3,context,709.93,10.0207,0,0,3,standard,suburban
store_B,4,context,834.42,9.9389,0,0,4,standard,suburban
store_B,5,context,847.01,9.5216,0,0,5,standard,suburban
store_B,6,context,802.58,10.3263,0,0,6,standard,suburban
store_B,7,context,770.87,10.3962,0,0,0,standard,suburban
store_B,8,context,873.1,9.6402,0,0,1,standard,suburban
store_B,9,context,844.74,10.054,0,0,2,standard,suburban
store_B,10,context,1050.46,9.6086,1,0,3,standard,suburban
store_B,11,context,1085.99,10.1722,0,1,4,standard,suburban
store_B,12,context,978.74,9.7812,0,0,5,standard,suburban
store_B,13,context,1033.59,10.1594,1,0,6,standard,suburban
store_B,14,context,846.06,10.227,0,0,0,standard,suburban
store_B,15,context,906.93,10.2686,0,0,1,standard,suburban
store_B,16,context,922.35,9.6077,0,0,2,standard,suburban
store_B,17,context,1111.93,10.416,1,0,3,standard,suburban
store_B,18,context,946.95,9.7302,0,0,4,standard,suburban
store_B,19,context,923.2,9.5374,0,0,5,standard,suburban
store_B,20,context,963.38,10.0549,0,0,6,standard,suburban
store_B,21,context,978.7,9.8709,1,0,0,standard,suburban
store_B,22,context,840.39,10.3298,0,0,1,standard,suburban
store_B,23,context,1019.22,10.3083,0,1,2,standard,suburban
store_B,24,horizon,848.1,9.8171,0,0,3,standard,suburban
store_B,25,horizon,777.91,10.4529,0,0,4,standard,suburban
store_B,26,horizon,883.44,9.7909,0,0,5,standard,suburban
store_B,27,horizon,827.78,10.0151,0,0,6,standard,suburban
store_B,28,horizon,762.41,9.756,0,0,0,standard,suburban
store_B,29,horizon,763.79,10.436,0,0,1,standard,suburban
store_B,30,horizon,838.41,9.6646,0,0,2,standard,suburban
store_B,31,horizon,860.45,9.5449,0,0,3,standard,suburban
store_B,32,horizon,904.82,9.9351,0,0,4,standard,suburban
store_B,33,horizon,1084.74,10.4924,1,0,5,standard,suburban
store_B,34,horizon,808.09,10.3917,0,0,6,standard,suburban
store_B,35,horizon,938.26,10.2486,0,1,0,standard,suburban
store_C,0,context,709.43,7.1053,0,1,0,discount,rural
store_C,1,context,649.01,7.0666,1,0,1,discount,rural
store_C,2,context,660.66,7.5944,1,0,2,discount,rural
store_C,3,context,750.17,7.1462,1,0,3,discount,rural
store_C,4,context,726.88,7.8247,1,0,4,discount,rural
store_C,5,context,639.97,7.3103,0,0,5,discount,rural
store_C,6,context,580.71,7.1439,0,0,6,discount,rural
store_C,7,context,549.13,7.921,0,0,0,discount,rural
store_C,8,context,597.79,7.1655,0,0,1,discount,rural
store_C,9,context,627.48,7.2847,0,0,2,discount,rural
store_C,10,context,634.26,7.1536,0,0,3,discount,rural
store_C,11,context,928.07,7.1155,1,1,4,discount,rural
store_C,12,context,643.37,7.0211,0,0,5,discount,rural
store_C,13,context,652.8,7.0554,0,0,6,discount,rural
store_C,14,context,766.65,7.1746,0,0,0,discount,rural
store_C,15,context,737.37,7.0534,0,0,1,discount,rural
store_C,16,context,589.02,7.5911,0,0,2,discount,rural
store_C,17,context,613.06,7.6807,0,0,3,discount,rural
store_C,18,context,556.25,7.3936,0,0,4,discount,rural
store_C,19,context,596.46,7.318,0,0,5,discount,rural
store_C,20,context,632.0,7.5045,0,0,6,discount,rural
store_C,21,context,662.1,7.875,0,0,0,discount,rural
store_C,22,context,558.0,7.8511,0,0,1,discount,rural
store_C,23,context,769.38,7.0435,0,1,2,discount,rural
store_C,24,horizon,482.94,7.1815,0,0,3,discount,rural
store_C,25,horizon,571.69,7.2367,0,0,4,discount,rural
store_C,26,horizon,666.89,7.2494,1,0,5,discount,rural
store_C,27,horizon,677.55,7.5712,1,0,6,discount,rural
store_C,28,horizon,503.9,7.4163,0,0,0,discount,rural
store_C,29,horizon,541.34,7.0493,0,0,1,discount,rural
store_C,30,horizon,443.17,7.3736,0,0,2,discount,rural
store_C,31,horizon,596.87,7.5238,1,0,3,discount,rural
store_C,32,horizon,628.12,7.1017,0,0,4,discount,rural
store_C,33,horizon,586.61,7.8335,1,0,5,discount,rural
store_C,34,horizon,456.82,7.052,0,0,6,discount,rural
store_C,35,horizon,782.3,7.9248,0,1,0,discount,rural
store_id,week,split,sales,base_sales,price,price_effect,promotion,holiday,day_of_week,store_type,region
store_A,0,context,1369.59,1012.19,11.6299,7.4,1,1,0,premium,urban
store_A,1,context,973.53,973.04,11.9757,0.49,0,0,1,premium,urban
store_A,2,context,1064.63,1059.16,11.7269,5.46,0,0,2,premium,urban
store_A,3,context,1077.59,1080.99,12.1698,-3.4,0,0,3,premium,urban
store_A,4,context,980.39,979.14,11.9372,1.26,0,0,4,premium,urban
store_A,5,context,1011.7,1018.36,12.3327,-6.65,0,0,5,premium,urban
store_A,6,context,1084.16,1088.16,12.2003,-4.01,0,0,6,premium,urban
store_A,7,context,1085.98,1082.23,11.8124,3.75,0,0,0,premium,urban
store_A,8,context,1098.52,1105.17,12.3323,-6.65,0,0,1,premium,urban
store_A,9,context,1075.62,1081.71,12.3048,-6.1,0,0,2,premium,urban
store_A,10,context,1312.23,1159.98,11.8875,2.25,1,0,3,premium,urban
store_A,11,context,1368.02,1163.79,11.7883,4.23,0,1,4,premium,urban
store_A,12,context,1138.41,1142.06,12.1825,-3.65,0,0,5,premium,urban
store_A,13,context,1197.29,1190.09,11.6398,7.2,0,0,6,premium,urban
store_A,14,context,1174.12,1168.12,11.6999,6.0,0,0,0,premium,urban
store_A,15,context,1128.16,1118.3,11.5074,9.85,0,0,1,premium,urban
store_A,16,context,1163.81,1169.55,12.2869,-5.74,0,0,2,premium,urban
store_A,17,context,1114.18,1117.48,12.1649,-3.3,0,0,3,premium,urban
store_A,18,context,1186.87,1190.98,12.2052,-4.1,0,0,4,premium,urban
store_A,19,context,1147.27,1152.88,12.2807,-5.61,0,0,5,premium,urban
store_A,20,context,1146.48,1145.66,11.9589,0.82,0,0,6,premium,urban
store_A,21,context,1121.83,1123.21,12.0687,-1.37,0,0,0,premium,urban
store_A,22,context,1203.28,1196.08,11.6398,7.2,0,0,1,premium,urban
store_A,23,context,1344.9,1137.19,11.6145,7.71,0,1,2,premium,urban
store_A,24,horizon,1118.64,1122.01,12.1684,-3.37,0,0,3,premium,urban
store_A,25,horizon,1121.14,1120.56,11.9711,0.58,0,0,4,premium,urban
store_A,26,horizon,1149.99,1151.29,12.0652,-1.3,0,0,5,premium,urban
store_A,27,horizon,1284.67,1139.97,12.265,-5.3,1,0,6,premium,urban
store_A,28,horizon,1284.67,1137.36,12.1347,-2.69,1,0,0,premium,urban
store_A,29,horizon,1132.79,1133.86,12.0536,-1.07,0,0,1,premium,urban
store_A,30,horizon,1197.3,1198.49,12.0592,-1.18,0,0,2,premium,urban
store_A,31,horizon,1247.22,1093.3,11.804,3.92,1,0,3,premium,urban
store_A,32,horizon,1095.84,1086.46,11.5308,9.38,0,0,4,premium,urban
store_A,33,horizon,1073.83,1072.57,11.9367,1.27,0,0,5,premium,urban
store_A,34,horizon,1134.51,1128.8,11.7146,5.71,0,0,6,premium,urban
store_A,35,horizon,1351.15,1149.32,11.9085,1.83,0,1,0,premium,urban
store_B,0,context,1062.53,712.0,9.9735,0.53,1,1,0,standard,suburban
store_B,1,context,904.49,749.83,9.767,4.66,1,0,1,standard,suburban
store_B,2,context,813.63,810.26,9.8316,3.37,0,0,2,standard,suburban
store_B,3,context,720.11,720.53,10.0207,-0.41,0,0,3,standard,suburban
store_B,4,context,820.78,819.55,9.9389,1.22,0,0,4,standard,suburban
store_B,5,context,833.27,823.7,9.5216,9.57,0,0,5,standard,suburban
store_B,6,context,795.26,801.78,10.3263,-6.53,0,0,6,standard,suburban
store_B,7,context,770.37,778.29,10.3962,-7.92,0,0,0,standard,suburban
store_B,8,context,855.92,848.72,9.6402,7.2,0,0,1,standard,suburban
store_B,9,context,832.33,833.41,10.054,-1.08,0,0,2,standard,suburban
store_B,10,context,1029.44,871.61,9.6086,7.83,1,0,3,standard,suburban
store_B,11,context,1066.35,869.8,10.1722,-3.44,0,1,4,standard,suburban
store_B,12,context,942.86,938.49,9.7812,4.38,0,0,5,standard,suburban
store_B,13,context,1015.99,869.18,10.1594,-3.19,1,0,6,standard,suburban
store_B,14,context,836.44,840.98,10.227,-4.54,0,0,0,standard,suburban
store_B,15,context,885.72,891.1,10.2686,-5.37,0,0,1,standard,suburban
store_B,16,context,901.45,893.6,9.6077,7.85,0,0,2,standard,suburban
store_B,17,context,1080.63,938.95,10.416,-8.32,1,0,3,standard,suburban
store_B,18,context,922.14,916.74,9.7302,5.4,0,0,4,standard,suburban
store_B,19,context,904.66,895.41,9.5374,9.25,0,0,5,standard,suburban
store_B,20,context,935.48,936.58,10.0549,-1.1,0,0,6,standard,suburban
store_B,21,context,979.23,826.64,9.8709,2.58,1,0,0,standard,suburban
store_B,22,context,837.49,844.09,10.3298,-6.6,0,0,1,standard,suburban
store_B,23,context,1021.39,827.56,10.3083,-6.17,0,1,2,standard,suburban
store_B,24,horizon,847.21,843.55,9.8171,3.66,0,0,3,standard,suburban
store_B,25,horizon,789.27,798.33,10.4529,-9.06,0,0,4,standard,suburban
store_B,26,horizon,877.09,872.91,9.7909,4.18,0,0,5,standard,suburban
store_B,27,horizon,832.42,832.72,10.0151,-0.3,0,0,6,standard,suburban
store_B,28,horizon,781.9,777.02,9.756,4.88,0,0,0,standard,suburban
store_B,29,horizon,781.04,789.76,10.436,-8.72,0,0,1,standard,suburban
store_B,30,horizon,844.57,837.86,9.6646,6.71,0,0,2,standard,suburban
store_B,31,horizon,863.43,854.33,9.5449,9.1,0,0,3,standard,suburban
store_B,32,horizon,898.12,896.82,9.9351,1.3,0,0,4,standard,suburban
store_B,33,horizon,1070.58,930.42,10.4924,-9.85,1,0,5,standard,suburban
store_B,34,horizon,820.4,828.24,10.3917,-7.83,0,0,6,standard,suburban
store_B,35,horizon,965.86,770.83,10.2486,-4.97,0,1,0,standard,suburban
store_C,0,context,709.12,501.23,7.1053,7.89,0,1,0,discount,rural
store_C,1,context,651.44,492.78,7.0666,8.67,1,0,1,discount,rural
store_C,2,context,659.15,511.04,7.5944,-1.89,1,0,2,discount,rural
store_C,3,context,733.06,575.98,7.1462,7.08,1,0,3,discount,rural
store_C,4,context,712.21,568.7,7.8247,-6.49,1,0,4,discount,rural
store_C,5,context,615.23,611.44,7.3103,3.79,0,0,5,discount,rural
store_C,6,context,568.99,561.87,7.1439,7.12,0,0,6,discount,rural
store_C,7,context,541.12,549.54,7.921,-8.42,0,0,0,discount,rural
store_C,8,context,583.57,576.88,7.1655,6.69,0,0,1,discount,rural
store_C,9,context,607.34,603.04,7.2847,4.31,0,0,2,discount,rural
store_C,10,context,613.79,606.86,7.1536,6.93,0,0,3,discount,rural
store_C,11,context,919.49,561.8,7.1155,7.69,1,1,4,discount,rural
store_C,12,context,622.61,613.04,7.0211,9.58,0,0,5,discount,rural
store_C,13,context,630.52,621.63,7.0554,8.89,0,0,6,discount,rural
store_C,14,context,721.62,715.12,7.1746,6.51,0,0,0,discount,rural
store_C,15,context,699.18,690.25,7.0534,8.93,0,0,1,discount,rural
store_C,16,context,578.85,580.67,7.5911,-1.82,0,0,2,discount,rural
store_C,17,context,598.23,601.84,7.6807,-3.61,0,0,3,discount,rural
store_C,18,context,554.43,552.3,7.3936,2.13,0,0,4,discount,rural
store_C,19,context,587.39,583.75,7.318,3.64,0,0,5,discount,rural
store_C,20,context,615.58,615.67,7.5045,-0.09,0,0,6,discount,rural
store_C,21,context,638.68,646.18,7.875,-7.5,0,0,0,discount,rural
store_C,22,context,555.99,563.01,7.8511,-7.02,0,0,1,discount,rural
store_C,23,context,768.83,559.7,7.0435,9.13,0,1,2,discount,rural
store_C,24,horizon,499.62,493.25,7.1815,6.37,0,0,3,discount,rural
store_C,25,horizon,570.9,565.64,7.2367,5.27,0,0,4,discount,rural
store_C,26,horizon,677.52,522.5,7.2494,5.01,1,0,5,discount,rural
store_C,27,horizon,685.25,536.68,7.5712,-1.42,1,0,6,discount,rural
store_C,28,horizon,517.46,515.78,7.4163,1.67,0,0,0,discount,rural
store_C,29,horizon,549.38,540.36,7.0493,9.01,0,0,1,discount,rural
store_C,30,horizon,470.04,467.51,7.3736,2.53,0,0,2,discount,rural
store_C,31,horizon,622.9,473.37,7.5238,-0.48,1,0,3,discount,rural
store_C,32,horizon,620.09,612.12,7.1017,7.97,0,0,4,discount,rural
store_C,33,horizon,614.45,471.12,7.8335,-6.67,1,0,5,discount,rural
store_C,34,horizon,484.25,475.29,7.052,8.96,0,0,6,discount,rural
store_C,35,horizon,781.64,590.14,7.9248,-8.5,0,1,0,discount,rural
1 store_id week split sales base_sales price price_effect promotion holiday day_of_week store_type region
2 store_A 0 context 1372.64 1369.59 1012.19 11.6299 7.4 1 1 0 premium urban
3 store_A 1 context 965.54 973.53 973.04 11.9757 0.49 0 0 1 premium urban
4 store_A 2 context 1076.92 1064.63 1059.16 11.7269 5.46 0 0 2 premium urban
5 store_A 3 context 1094.09 1077.59 1080.99 12.1698 -3.4 0 0 3 premium urban
6 store_A 4 context 970.18 980.39 979.14 11.9372 1.26 0 0 4 premium urban
7 store_A 5 context 1010.04 1011.7 1018.36 12.3327 -6.65 0 0 5 premium urban
8 store_A 6 context 1098.7 1084.16 1088.16 12.2003 -4.01 0 0 6 premium urban
9 store_A 7 context 1097.79 1085.98 1082.23 11.8124 3.75 0 0 0 premium urban
10 store_A 8 context 1114.81 1098.52 1105.17 12.3323 -6.65 0 0 1 premium urban
11 store_A 9 context 1084.8 1075.62 1081.71 12.3048 -6.1 0 0 2 premium urban
12 store_A 10 context 1339.72 1312.23 1159.98 11.8875 2.25 1 0 3 premium urban
13 store_A 11 context 1395.22 1368.02 1163.79 11.7883 4.23 0 1 4 premium urban
14 store_A 12 context 1158.92 1138.41 1142.06 12.1825 -3.65 0 0 5 premium urban
15 store_A 13 context 1228.57 1197.29 1190.09 11.6398 7.2 0 0 6 premium urban
16 store_A 14 context 1198.65 1174.12 1168.12 11.6999 6.0 0 0 0 premium urban
17 store_A 15 context 1138.98 1128.16 1118.3 11.5074 9.85 0 0 1 premium urban
18 store_A 16 context 1186.2 1163.81 1169.55 12.2869 -5.74 0 0 2 premium urban
19 store_A 17 context 1122.3 1114.18 1117.48 12.1649 -3.3 0 0 3 premium urban
20 store_A 18 context 1212.12 1186.87 1190.98 12.2052 -4.1 0 0 4 premium urban
21 store_A 19 context 1161.74 1147.27 1152.88 12.2807 -5.61 0 0 5 premium urban
22 store_A 20 context 1157.89 1146.48 1145.66 11.9589 0.82 0 0 6 premium urban
23 store_A 21 context 1126.39 1121.83 1123.21 12.0687 -1.37 0 0 0 premium urban
24 store_A 22 context 1224.8 1203.28 1196.08 11.6398 7.2 0 0 1 premium urban
25 store_A 23 context 1350.44 1344.9 1137.19 11.6145 7.71 0 1 2 premium urban
26 store_A 24 horizon 1119.15 1118.64 1122.01 12.1684 -3.37 0 0 3 premium urban
27 store_A 25 horizon 1120.03 1121.14 1120.56 11.9711 0.58 0 0 4 premium urban
28 store_A 26 horizon 1155.31 1149.99 1151.29 12.0652 -1.3 0 0 5 premium urban
29 store_A 27 horizon 1285.92 1284.67 1139.97 12.265 -5.3 1 0 6 premium urban
30 store_A 28 horizon 1284.01 1284.67 1137.36 12.1347 -2.69 1 0 0 premium urban
31 store_A 29 horizon 1130.01 1132.79 1133.86 12.0536 -1.07 0 0 1 premium urban
32 store_A 30 horizon 1209.43 1197.3 1198.49 12.0592 -1.18 0 0 2 premium urban
33 store_A 31 horizon 1231.79 1247.22 1093.3 11.804 3.92 1 0 3 premium urban
34 store_A 32 horizon 1077.46 1095.84 1086.46 11.5308 9.38 0 0 4 premium urban
35 store_A 33 horizon 1050.73 1073.83 1072.57 11.9367 1.27 0 0 5 premium urban
36 store_A 34 horizon 1124.21 1134.51 1128.8 11.7146 5.71 0 0 6 premium urban
37 store_A 35 horizon 1344.73 1351.15 1149.32 11.9085 1.83 0 1 0 premium urban
38 store_B 0 context 1053.03 1062.53 712.0 9.9735 0.53 1 1 0 standard suburban
39 store_B 1 context 903.51 904.49 749.83 9.767 4.66 1 0 1 standard suburban
40 store_B 2 context 826.82 813.63 810.26 9.8316 3.37 0 0 2 standard suburban
41 store_B 3 context 709.93 720.11 720.53 10.0207 -0.41 0 0 3 standard suburban
42 store_B 4 context 834.42 820.78 819.55 9.9389 1.22 0 0 4 standard suburban
43 store_B 5 context 847.01 833.27 823.7 9.5216 9.57 0 0 5 standard suburban
44 store_B 6 context 802.58 795.26 801.78 10.3263 -6.53 0 0 6 standard suburban
45 store_B 7 context 770.87 770.37 778.29 10.3962 -7.92 0 0 0 standard suburban
46 store_B 8 context 873.1 855.92 848.72 9.6402 7.2 0 0 1 standard suburban
47 store_B 9 context 844.74 832.33 833.41 10.054 -1.08 0 0 2 standard suburban
48 store_B 10 context 1050.46 1029.44 871.61 9.6086 7.83 1 0 3 standard suburban
49 store_B 11 context 1085.99 1066.35 869.8 10.1722 -3.44 0 1 4 standard suburban
50 store_B 12 context 978.74 942.86 938.49 9.7812 4.38 0 0 5 standard suburban
51 store_B 13 context 1033.59 1015.99 869.18 10.1594 -3.19 1 0 6 standard suburban
52 store_B 14 context 846.06 836.44 840.98 10.227 -4.54 0 0 0 standard suburban
53 store_B 15 context 906.93 885.72 891.1 10.2686 -5.37 0 0 1 standard suburban
54 store_B 16 context 922.35 901.45 893.6 9.6077 7.85 0 0 2 standard suburban
55 store_B 17 context 1111.93 1080.63 938.95 10.416 -8.32 1 0 3 standard suburban
56 store_B 18 context 946.95 922.14 916.74 9.7302 5.4 0 0 4 standard suburban
57 store_B 19 context 923.2 904.66 895.41 9.5374 9.25 0 0 5 standard suburban
58 store_B 20 context 963.38 935.48 936.58 10.0549 -1.1 0 0 6 standard suburban
59 store_B 21 context 978.7 979.23 826.64 9.8709 2.58 1 0 0 standard suburban
60 store_B 22 context 840.39 837.49 844.09 10.3298 -6.6 0 0 1 standard suburban
61 store_B 23 context 1019.22 1021.39 827.56 10.3083 -6.17 0 1 2 standard suburban
62 store_B 24 horizon 848.1 847.21 843.55 9.8171 3.66 0 0 3 standard suburban
63 store_B 25 horizon 777.91 789.27 798.33 10.4529 -9.06 0 0 4 standard suburban
64 store_B 26 horizon 883.44 877.09 872.91 9.7909 4.18 0 0 5 standard suburban
65 store_B 27 horizon 827.78 832.42 832.72 10.0151 -0.3 0 0 6 standard suburban
66 store_B 28 horizon 762.41 781.9 777.02 9.756 4.88 0 0 0 standard suburban
67 store_B 29 horizon 763.79 781.04 789.76 10.436 -8.72 0 0 1 standard suburban
68 store_B 30 horizon 838.41 844.57 837.86 9.6646 6.71 0 0 2 standard suburban
69 store_B 31 horizon 860.45 863.43 854.33 9.5449 9.1 0 0 3 standard suburban
70 store_B 32 horizon 904.82 898.12 896.82 9.9351 1.3 0 0 4 standard suburban
71 store_B 33 horizon 1084.74 1070.58 930.42 10.4924 -9.85 1 0 5 standard suburban
72 store_B 34 horizon 808.09 820.4 828.24 10.3917 -7.83 0 0 6 standard suburban
73 store_B 35 horizon 938.26 965.86 770.83 10.2486 -4.97 0 1 0 standard suburban
74 store_C 0 context 709.43 709.12 501.23 7.1053 7.89 0 1 0 discount rural
75 store_C 1 context 649.01 651.44 492.78 7.0666 8.67 1 0 1 discount rural
76 store_C 2 context 660.66 659.15 511.04 7.5944 -1.89 1 0 2 discount rural
77 store_C 3 context 750.17 733.06 575.98 7.1462 7.08 1 0 3 discount rural
78 store_C 4 context 726.88 712.21 568.7 7.8247 -6.49 1 0 4 discount rural
79 store_C 5 context 639.97 615.23 611.44 7.3103 3.79 0 0 5 discount rural
80 store_C 6 context 580.71 568.99 561.87 7.1439 7.12 0 0 6 discount rural
81 store_C 7 context 549.13 541.12 549.54 7.921 -8.42 0 0 0 discount rural
82 store_C 8 context 597.79 583.57 576.88 7.1655 6.69 0 0 1 discount rural
83 store_C 9 context 627.48 607.34 603.04 7.2847 4.31 0 0 2 discount rural
84 store_C 10 context 634.26 613.79 606.86 7.1536 6.93 0 0 3 discount rural
85 store_C 11 context 928.07 919.49 561.8 7.1155 7.69 1 1 4 discount rural
86 store_C 12 context 643.37 622.61 613.04 7.0211 9.58 0 0 5 discount rural
87 store_C 13 context 652.8 630.52 621.63 7.0554 8.89 0 0 6 discount rural
88 store_C 14 context 766.65 721.62 715.12 7.1746 6.51 0 0 0 discount rural
89 store_C 15 context 737.37 699.18 690.25 7.0534 8.93 0 0 1 discount rural
90 store_C 16 context 589.02 578.85 580.67 7.5911 -1.82 0 0 2 discount rural
91 store_C 17 context 613.06 598.23 601.84 7.6807 -3.61 0 0 3 discount rural
92 store_C 18 context 556.25 554.43 552.3 7.3936 2.13 0 0 4 discount rural
93 store_C 19 context 596.46 587.39 583.75 7.318 3.64 0 0 5 discount rural
94 store_C 20 context 632.0 615.58 615.67 7.5045 -0.09 0 0 6 discount rural
95 store_C 21 context 662.1 638.68 646.18 7.875 -7.5 0 0 0 discount rural
96 store_C 22 context 558.0 555.99 563.01 7.8511 -7.02 0 0 1 discount rural
97 store_C 23 context 769.38 768.83 559.7 7.0435 9.13 0 1 2 discount rural
98 store_C 24 horizon 482.94 499.62 493.25 7.1815 6.37 0 0 3 discount rural
99 store_C 25 horizon 571.69 570.9 565.64 7.2367 5.27 0 0 4 discount rural
100 store_C 26 horizon 666.89 677.52 522.5 7.2494 5.01 1 0 5 discount rural
101 store_C 27 horizon 677.55 685.25 536.68 7.5712 -1.42 1 0 6 discount rural
102 store_C 28 horizon 503.9 517.46 515.78 7.4163 1.67 0 0 0 discount rural
103 store_C 29 horizon 541.34 549.38 540.36 7.0493 9.01 0 0 1 discount rural
104 store_C 30 horizon 443.17 470.04 467.51 7.3736 2.53 0 0 2 discount rural
105 store_C 31 horizon 596.87 622.9 473.37 7.5238 -0.48 1 0 3 discount rural
106 store_C 32 horizon 628.12 620.09 612.12 7.1017 7.97 0 0 4 discount rural
107 store_C 33 horizon 586.61 614.45 471.12 7.8335 -6.67 1 0 5 discount rural
108 store_C 34 horizon 456.82 484.25 475.29 7.052 8.96 0 0 6 discount rural
109 store_C 35 horizon 782.3 781.64 590.14 7.9248 -8.5 0 1 0 discount rural