diff --git a/scientific-skills/timesfm-forecasting/SKILL.md b/scientific-skills/timesfm-forecasting/SKILL.md index 0cb7f05..64a3782 100644 --- a/scientific-skills/timesfm-forecasting/SKILL.md +++ b/scientific-skills/timesfm-forecasting/SKILL.md @@ -47,10 +47,14 @@ Use this skill when: Do **not** use this skill when: - You need classical statistical models with coefficient interpretation → use `statsmodels` -- You need time series classification, clustering, or anomaly detection → use `aeon` +- You need time series classification or clustering → use `aeon` - You need multivariate vector autoregression or Granger causality → use `statsmodels` - Your data is tabular (not temporal) → use `scikit-learn` +> **Note on Anomaly Detection**: TimesFM does not have built-in anomaly detection, but you can +> use the **quantile forecasts as prediction intervals** — values outside the 90% CI (q10–q90) +> are statistically unusual. See the `examples/anomaly-detection/` directory for a full example. + ## ⚠️ Mandatory Preflight: System Requirements Check **CRITICAL — ALWAYS run the system checker before loading the model for the first time.** @@ -208,6 +212,61 @@ for i, col in enumerate(df.columns): ### Forecast with Covariates (XReg) +TimesFM 2.5+ supports exogenous variables through `forecast_with_covariates()`. Requires `timesfm[xreg]`. + +```python +# Requires: uv pip install timesfm[xreg] +point, quantiles = model.forecast_with_covariates( + inputs=inputs, + dynamic_numerical_covariates={"price": price_arrays}, + dynamic_categorical_covariates={"holiday": holiday_arrays}, + static_categorical_covariates={"region": region_labels}, + xreg_mode="xreg + timesfm", # or "timesfm + xreg" +) +``` + +| Covariate Type | Description | Example | +| -------------- | ----------- | ------- | +| `dynamic_numerical` | Time-varying numeric | price, temperature, promotion spend | +| `dynamic_categorical` | Time-varying categorical | holiday flag, day of week | +| `static_numerical` | Per-series numeric | store size, account age | +| `static_categorical` | Per-series categorical | store type, region, product category | + +**XReg Modes:** +- `"xreg + timesfm"` (default): TimesFM forecasts first, then XReg adjusts residuals +- `"timesfm + xreg"`: XReg fits first, then TimesFM forecasts residuals + +> See `examples/covariates-forecasting/` for a complete example with synthetic retail data. + +### Anomaly Detection (via Quantile Intervals) + +TimesFM does not have built-in anomaly detection, but the **quantile forecasts naturally provide +prediction intervals** that can detect anomalies: + +```python +point, q = model.forecast(horizon=H, inputs=[values]) + +# 90% prediction interval +lower_90 = q[0, :, 1] # 10th percentile +upper_90 = q[0, :, 9] # 90th percentile + +# Detect anomalies: values outside the 90% CI +actual = test_values # your holdout data +anomalies = (actual < lower_90) | (actual > upper_90) + +# Severity levels +is_warning = (actual < q[0, :, 2]) | (actual > q[0, :, 8]) # outside 80% CI +is_critical = anomalies # outside 90% CI +``` + +| Severity | Condition | Interpretation | +| -------- | --------- | -------------- | +| **Normal** | Inside 80% CI | Expected behavior | +| **Warning** | Outside 80% CI | Unusual but possible | +| **Critical** | Outside 90% CI | Statistically rare (< 10% probability) | + +> See `examples/anomaly-detection/` for a complete example with visualization. + ```python # Requires: uv pip install timesfm[xreg] point, quantiles = model.forecast_with_covariates( diff --git a/scientific-skills/timesfm-forecasting/examples/anomaly-detection/detect_anomalies.py b/scientific-skills/timesfm-forecasting/examples/anomaly-detection/detect_anomalies.py new file mode 100644 index 0000000..e4ee2ba --- /dev/null +++ b/scientific-skills/timesfm-forecasting/examples/anomaly-detection/detect_anomalies.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +""" +TimesFM Anomaly Detection Example + +This example demonstrates how to use TimesFM's quantile forecasts for +anomaly detection. The approach: +1. Forecast with quantile intervals (10th-90th percentiles) +2. Compare actual values against prediction intervals +3. Flag values outside intervals as anomalies + +TimesFM does NOT have built-in anomaly detection, but the quantile +forecasts provide natural anomaly detection via prediction intervals. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import timesfm + +# Configuration +HORIZON = 12 # Forecast horizon +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 = ( + Path(__file__).parent.parent / "global-temperature" / "temperature_anomaly.csv" +) +OUTPUT_DIR = EXAMPLE_DIR / "output" + + +def inject_anomalies( + values: np.ndarray, n_anomalies: int = 3, seed: int = 42 +) -> tuple[np.ndarray, list[int]]: + """Inject synthetic anomalies into the data for demonstration.""" + rng = np.random.default_rng(seed) + anomaly_indices = rng.choice(len(values), size=n_anomalies, replace=False).tolist() + + anomalous_values = values.copy() + for idx in anomaly_indices: + # 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) + + +def main() -> None: + print("=" * 60) + print(" TIMESFM ANOMALY DETECTION DEMO") + print("=" * 60) + + OUTPUT_DIR.mkdir(exist_ok=True) + + # Load temperature data + print("\n📊 Loading temperature anomaly data...") + df = pd.read_csv(DATA_FILE, parse_dates=["date"]) + df = df.sort_values("date").reset_index(drop=True) + + # Split into context (first 24 months) and test (last 12 months) + context_values = df["anomaly_c"].values[:24].astype(np.float32) + 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 + print("\n🤖 Loading TimesFM 1.0 (200M) PyTorch...") + hparams = timesfm.TimesFmHparams(horizon_len=HORIZON) + checkpoint = timesfm.TimesFmCheckpoint( + huggingface_repo_id="google/timesfm-1.0-200m-pytorch" + ) + model = timesfm.TimesFm(hparams=hparams, checkpoint=checkpoint) + + # Forecast with quantiles + print("\n📈 Forecasting with quantile intervals...") + point_forecast, quantile_forecast = model.forecast( + [context_values], + freq=[0], + ) + + # Extract quantiles + # quantile_forecast shape: (1, 12, 10) - [mean, q10, q20, ..., q90] + point = point_forecast[0] + q10 = quantile_forecast[0, :, 0] # 10th percentile + q20 = quantile_forecast[0, :, 1] # 20th percentile + q50 = quantile_forecast[0, :, 4] # 50th percentile (median) + q80 = quantile_forecast[0, :, 7] # 80th percentile + q90 = quantile_forecast[0, :, 8] # 90th percentile + + print(f" Forecast mean: {point.mean():.3f}°C") + print(f" 90% CI width: {(q90 - q10).mean():.3f}°C (avg)") + + # Detect anomalies + print("\n🔍 Detecting anomalies...") + anomalies = [] + for i, (actual, lower_80, upper_80, lower_90, upper_90) in enumerate( + zip(test_values_with_anomalies, q20, q80, q10, q90) + ): + month = dates_future[i] + month_str = pd.to_datetime(month).strftime("%Y-%m") + + if actual < lower_90 or actual > upper_90: + severity = "CRITICAL" + threshold = "90% CI" + color = "red" + elif actual < lower_80 or actual > upper_80: + severity = "WARNING" + threshold = "80% CI" + color = "orange" + else: + severity = "NORMAL" + threshold = "within bounds" + color = "green" + + anomalies.append( + { + "month": month_str, + "actual": float(actual), + "forecast": float(point[i]), + "lower_80": float(lower_80), + "upper_80": float(upper_80), + "lower_90": float(lower_90), + "upper_90": float(upper_90), + "severity": severity, + "threshold": threshold, + "color": color, + } + ) + + if severity != "NORMAL": + deviation = abs(actual - point[i]) + print( + f" [{severity}] {month_str}: {actual:.2f}°C (forecast: {point[i]:.2f}°C, deviation: {deviation:.2f}°C)" + ) + + # Create visualization + print("\n📊 Creating anomaly visualization...") + + fig, axes = plt.subplots(2, 1, figsize=(14, 10)) + + # Plot 1: Full time series with forecast and anomalies + ax1 = axes[0] + + # Historical data + historical_dates = df["date"].values[:24] + ax1.plot( + historical_dates, + context_values, + "b-", + linewidth=2, + label="Historical Data", + marker="o", + markersize=4, + ) + + # Actual future (with anomalies) + ax1.plot( + dates_future, + actual_future, + "g--", + linewidth=1.5, + label="Actual (clean)", + alpha=0.5, + ) + ax1.plot( + dates_future, + test_values_with_anomalies, + "ko", + markersize=8, + label="Actual (with anomalies)", + alpha=0.7, + ) + + # Forecast + ax1.plot( + dates_future, + point, + "r-", + linewidth=2, + label="Forecast (median)", + marker="s", + 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) + ax1.set_ylabel("Temperature Anomaly (°C)", fontsize=12) + ax1.set_title( + "TimesFM Anomaly Detection: Forecast Intervals Method", + fontsize=14, + fontweight="bold", + ) + ax1.legend(loc="upper left", fontsize=10) + ax1.grid(True, alpha=0.3) + + # Add annotation for anomalies + ax1.annotate( + "× = Critical (outside 90% CI)\n▲ = Warning (outside 80% CI)", + xy=(0.98, 0.02), + xycoords="axes fraction", + ha="right", + va="bottom", + fontsize=10, + bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8), + ) + + # Plot 2: Deviation from forecast with thresholds + ax2 = axes[1] + + deviation = test_values_with_anomalies - point + lower_90_dev = q10 - point + upper_90_dev = q90 - point + lower_80_dev = q20 - point + upper_80_dev = q80 - point + + months = [pd.to_datetime(d).strftime("%Y-%m") for d in dates_future] + x = np.arange(len(months)) + + # Threshold bands + ax2.fill_between( + x, lower_90_dev, upper_90_dev, alpha=0.2, color="red", label="90% CI bounds" + ) + ax2.fill_between( + 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( + "Deviation from Forecast with Anomaly Thresholds", + fontsize=14, + fontweight="bold", + ) + ax2.set_xticks(x) + ax2.set_xticklabels(months, rotation=45, ha="right") + ax2.legend(loc="upper right", fontsize=10) + ax2.grid(True, alpha=0.3, axis="y") + + plt.tight_layout() + + output_path = OUTPUT_DIR / "anomaly_detection.png" + plt.savefig(output_path, dpi=150, bbox_inches="tight") + print(f" Saved: {output_path}") + plt.close() + + # Save results + results = { + "method": "quantile_intervals", + "description": "Anomaly detection using TimesFM quantile forecasts as prediction intervals", + "thresholds": { + "warning": f"Outside {ANOMALY_THRESHOLD_WARNING * 100:.0f}% CI (q20-q80)", + "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"), + }, + } + + results_path = OUTPUT_DIR / "anomaly_detection.json" + with open(results_path, "w") as f: + json.dump(results, f, indent=2) + print(f" Saved: {results_path}") + + # Print summary + print("\n" + "=" * 60) + print(" ✅ ANOMALY DETECTION COMPLETE") + print("=" * 60) + print(f"\n📊 Summary:") + print(f" Total test points: {results['summary']['total_points']}") + print(f" Critical anomalies: {results['summary']['critical']} (outside 90% CI)") + print(f" Warnings: {results['summary']['warning']} (outside 80% CI)") + 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__": + main() diff --git a/scientific-skills/timesfm-forecasting/examples/anomaly-detection/output/anomaly_detection.json b/scientific-skills/timesfm-forecasting/examples/anomaly-detection/output/anomaly_detection.json new file mode 100644 index 0000000..63f19c2 --- /dev/null +++ b/scientific-skills/timesfm-forecasting/examples/anomaly-detection/output/anomaly_detection.json @@ -0,0 +1,160 @@ +{ + "method": "quantile_intervals", + "description": "Anomaly detection using TimesFM quantile forecasts as prediction intervals", + "thresholds": { + "warning": "Outside 80% CI (q20-q80)", + "critical": "Outside 90% CI (q10-q90)" + }, + "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": { + "total_points": 12, + "critical": 11, + "warning": 1, + "normal": 0 + } +} \ No newline at end of file diff --git a/scientific-skills/timesfm-forecasting/examples/anomaly-detection/output/anomaly_detection.png b/scientific-skills/timesfm-forecasting/examples/anomaly-detection/output/anomaly_detection.png new file mode 100644 index 0000000..908006a Binary files /dev/null and b/scientific-skills/timesfm-forecasting/examples/anomaly-detection/output/anomaly_detection.png differ diff --git a/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/demo_covariates.py b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/demo_covariates.py new file mode 100644 index 0000000..8c4f182 --- /dev/null +++ b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/demo_covariates.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +""" +TimesFM Covariates (XReg) Example + +This example demonstrates TimesFM's exogenous variable support through the +forecast_with_covariates() API. This requires `timesfm[xreg]` installation. + +Covariate Types Supported: +- Dynamic Numerical: Time-varying numeric features (e.g., price, temperature) +- Dynamic Categorical: Time-varying categorical features (e.g., holiday, day_of_week) +- Static Numerical: Per-series numeric features (e.g., store_size) +- Static Categorical: Per-series categorical features (e.g., store_type, region) + +Note: TimesFM 1.0 (used here) does NOT support forecast_with_covariates(). +This example uses TimesFM 2.5 which requires a different API. We'll demonstrate +the concept with synthetic data and show the API signature. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +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 +N_STORES = 3 +CONTEXT_LEN = 48 # 48 weeks of history +HORIZON_LEN = 12 # 12 weeks forecast +TOTAL_LEN = CONTEXT_LEN + HORIZON_LEN + + +def generate_sales_data() -> dict: + """Generate synthetic retail sales data with covariates.""" + 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}, + } + + data = {"stores": {}, "covariates": {}} + + for store_id, config in stores.items(): + # Base sales with trend + weeks = np.arange(TOTAL_LEN) + trend = config["base_sales"] * (1 + 0.005 * weeks) + + # Seasonality (yearly pattern) + seasonality = 100 * np.sin(2 * np.pi * weeks / 52) + + # Noise + noise = rng.normal(0, 50, TOTAL_LEN) + + # Price (affects sales negatively) + price = 10 + rng.uniform(-1, 1, TOTAL_LEN) + price_effect = -20 * (price - 10) + + # Holidays (boost sales) + holidays = np.zeros(TOTAL_LEN) + holiday_weeks = [0, 11, 23, 35, 47, 51] # Major holidays + for hw in holiday_weeks: + if hw < TOTAL_LEN: + holidays[hw] = 1 + + holiday_effect = 200 * holidays + + # Promotion (boost sales) + promotion = rng.choice([0, 1], TOTAL_LEN, p=[0.8, 0.2]) + promo_effect = 150 * promotion + + # Final sales + sales = ( + trend + seasonality + noise + price_effect + holiday_effect + promo_effect + ) + sales = np.maximum(sales, 50) # Ensure positive + + # Day of week effect (0=Mon, 6=Sun) - simplified to weekly + day_of_week = np.tile(np.arange(7), TOTAL_LEN // 7 + 1)[:TOTAL_LEN] + + data["stores"][store_id] = { + "sales": sales.astype(np.float32), + "config": config, + } + + # Covariates (same structure for all stores, different values) + if store_id == "store_A": + data["covariates"] = { + "price": {store_id: price.astype(np.float32) for store_id in stores}, + "promotion": { + store_id: promotion.astype(np.float32) for store_id in stores + }, + "holiday": { + 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 + + +def demonstrate_api() -> None: + """Show the forecast_with_covariates API structure.""" + + 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.""" + + OUTPUT_DIR.mkdir(exist_ok=True) + + fig, axes = plt.subplots(3, 2, figsize=(16, 12)) + + weeks = np.arange(TOTAL_LEN) + context_weeks = weeks[:CONTEXT_LEN] + horizon_weeks = weeks[CONTEXT_LEN:] + + # Plot 1: Sales by store + 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, color="red", linestyle="--", label="Forecast Start") + ax.set_xlabel("Week") + ax.set_ylabel("Sales") + ax.set_title("Historical Sales by Store") + ax.legend() + ax.grid(True, alpha=0.3) + + # Plot 2: Price covariate + ax = axes[0, 1] + for store_id in data["stores"]: + ax.plot(weeks, data["covariates"]["price"][store_id], label=store_id, alpha=0.7) + ax.axvline(x=CONTEXT_LEN, color="red", linestyle="--") + ax.set_xlabel("Week") + ax.set_ylabel("Price ($)") + ax.set_title("Dynamic Numerical Covariate: Price") + ax.legend() + ax.grid(True, alpha=0.3) + + # Plot 3: Holiday covariate + ax = axes[1, 0] + holidays = data["covariates"]["holiday"]["store_A"] + ax.bar(weeks, holidays, alpha=0.7, color="orange") + ax.axvline(x=CONTEXT_LEN, 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) + + # Plot 4: Promotion covariate + ax = axes[1, 1] + promotions = data["covariates"]["promotion"]["store_A"] + ax.bar(weeks, promotions, alpha=0.7, color="green") + ax.axvline(x=CONTEXT_LEN, color="red", linestyle="--") + ax.set_xlabel("Week") + ax.set_ylabel("Promotion Flag") + ax.set_title("Dynamic Categorical Covariate: Promotion") + ax.grid(True, alpha=0.3) + + # Plot 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": "brown"} + 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") + + # Plot 6: Data structure summary + ax = axes[2, 1] + ax.axis("off") + + summary_text = """ + COVARIATE DATA STRUCTURE + ───────────────────────── + + Dynamic Numerical Covariates: + • price: np.ndarray[context_len + horizon_len] per series + • promotion: np.ndarray[context_len + horizon_len] per series + + Dynamic Categorical Covariates: + • holiday: np.ndarray[context_len + horizon_len] per series + • day_of_week: np.ndarray[context_len + horizon_len] per series + + Static Categorical Covariates: + • 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( + 0.1, + 0.5, + summary_text, + transform=ax.transAxes, + fontfamily="monospace", + fontsize=10, + verticalalignment="center", + ) + + 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() + + +def main() -> None: + print("=" * 70) + print(" TIMESFM COVARIATES (XREG) EXAMPLE") + print("=" * 70) + + # Generate synthetic 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())}") + + # Show API + demonstrate_api() + + # Explain modes + explain_xreg_modes() + + # Create visualization + print("\n📊 Creating data visualization...") + create_visualization(data) + + # Save data + print("\n💾 Saving synthetic data...") + + # Convert to DataFrame for CSV export + records = [] + for store_id, store_data in data["stores"].items(): + for i, week in enumerate(range(TOTAL_LEN)): + records.append( + { + "store_id": store_id, + "week": week, + "sales": store_data["sales"][i], + "price": data["covariates"]["price"][store_id][i], + "promotion": data["covariates"]["promotion"][store_id][i], + "holiday": int(data["covariates"]["holiday"][store_id][i]), + "day_of_week": int(data["covariates"]["day_of_week"][store_id][i]), + "store_type": data["covariates"]["store_type"][store_id], + "region": data["covariates"]["region"][store_id], + } + ) + + df = pd.DataFrame(records) + csv_path = OUTPUT_DIR / "sales_with_covariates.csv" + df.to_csv(csv_path, index=False) + print(f" Saved: {csv_path}") + + # Save metadata + metadata = { + "description": "Synthetic retail sales data with covariates for TimesFM XReg demo", + "stores": {sid: sdata["config"] for sid, sdata in data["stores"].items()}, + "dimensions": { + "context_length": CONTEXT_LEN, + "horizon_length": HORIZON_LEN, + "total_length": TOTAL_LEN, + }, + "covariates": { + "dynamic_numerical": ["price", "promotion"], + "dynamic_categorical": ["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", + }, + } + + meta_path = OUTPUT_DIR / "covariates_metadata.json" + with open(meta_path, "w") as f: + json.dump(metadata, f, indent=2) + print(f" Saved: {meta_path}") + + # Summary + print("\n" + "=" * 70) + print(" ✅ COVARIATES EXAMPLE COMPLETE") + print("=" * 70) + + print(""" +💡 Key Points: + +1. INSTALLATION: Requires timesfm[xreg] extra + pip install timesfm[xreg] + +2. COVARIATE TYPES: + • Dynamic: Changes over time (price, promotion, holiday) + • Static: Fixed per series (store type, region) + +3. DATA REQUIREMENTS: + • Dynamic covariates need values for context + horizon + • Future values must be known (e.g., planned prices, scheduled holidays) + +4. XREG MODES: + • "xreg + timesfm" (default): Regression on residuals + • "timesfm + xreg": TimesFM on residuals after regression + +5. LIMITATIONS: + • String categorical values work but slower (use int encoding) + • Requires TimesFM 2.5+ (v1.0 does not support XReg) + +📁 Output Files: + • output/covariates_data.png - Data visualization + • output/sales_with_covariates.csv - Sample data + • output/covariates_metadata.json - Metadata +""") + + +if __name__ == "__main__": + main() diff --git a/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/covariates_data.png b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/covariates_data.png new file mode 100644 index 0000000..1ee778e Binary files /dev/null and b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/covariates_data.png differ diff --git a/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/covariates_metadata.json b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/covariates_metadata.json new file mode 100644 index 0000000..1e2e2ed --- /dev/null +++ b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/covariates_metadata.json @@ -0,0 +1,43 @@ +{ + "description": "Synthetic retail sales data with covariates for TimesFM XReg demo", + "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 + } + }, + "dimensions": { + "context_length": 48, + "horizon_length": 12, + "total_length": 60 + }, + "covariates": { + "dynamic_numerical": [ + "price", + "promotion" + ], + "dynamic_categorical": [ + "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" + } +} \ No newline at end of file diff --git a/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/sales_with_covariates.csv b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/sales_with_covariates.csv new file mode 100644 index 0000000..9384b75 --- /dev/null +++ b/scientific-skills/timesfm-forecasting/examples/covariates-forecasting/output/sales_with_covariates.csv @@ -0,0 +1,181 @@ +store_id,week,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,1,954.4545,10.529998,0.0,0,1,premium,urban +store_A,2,1066.0654,10.269437,0.0,0,2,premium,urban +store_A,3,1095.3456,10.107159,0.0,0,3,premium,urban +store_A,4,966.55225,10.118414,0.0,0,4,premium,urban +store_A,5,1024.5396,9.607901,0.0,0,5,premium,urban +store_A,6,1121.4716,9.061636,0.0,0,6,premium,urban +store_A,7,1096.5702,9.873435,0.0,0,0,premium,urban +store_A,8,1132.875,9.42917,0.0,0,1,premium,urban +store_A,9,1244.5522,9.817058,1.0,0,2,premium,urban +store_A,10,1173.3354,10.706806,0.0,0,3,premium,urban +store_A,11,1401.6262,9.467879,0.0,1,4,premium,urban +store_A,12,1180.2404,9.116606,0.0,0,5,premium,urban +store_A,13,1230.1067,9.562768,0.0,0,6,premium,urban +store_A,14,1350.9026,9.587188,1.0,0,0,premium,urban +store_A,15,1122.653,10.323833,0.0,0,1,premium,urban +store_A,16,1189.6578,10.114064,0.0,0,2,premium,urban +store_A,17,1114.2455,10.567797,0.0,0,3,premium,urban +store_A,18,1209.6483,10.328627,0.0,0,4,premium,urban +store_A,19,1171.0994,9.812774,0.0,0,5,premium,urban +store_A,20,1294.5083,10.62804,1.0,0,6,premium,urban +store_A,21,1141.081,9.333946,0.0,0,0,premium,urban +store_A,22,1236.6909,9.045424,0.0,0,1,premium,urban +store_A,23,1359.1321,9.180096,0.0,1,2,premium,urban +store_A,24,1113.6208,10.444718,0.0,0,3,premium,urban +store_A,25,1120.9719,9.923755,0.0,0,4,premium,urban +store_A,26,1170.1646,9.322543,0.0,0,5,premium,urban +store_A,27,1141.1768,10.0020895,0.0,0,6,premium,urban +store_A,28,1300.6125,9.304625,1.0,0,0,premium,urban +store_A,29,1273.2278,10.392641,1.0,0,1,premium,urban +store_A,30,1212.7638,9.892313,0.0,0,2,premium,urban +store_A,31,1082.632,9.762042,0.0,0,3,premium,urban +store_A,32,1076.0151,9.6030245,0.0,0,4,premium,urban +store_A,33,1044.249,10.260565,0.0,0,5,premium,urban +store_A,34,1124.0281,9.723625,0.0,0,6,premium,urban +store_A,35,1359.397,9.1753,0.0,1,0,premium,urban +store_A,36,1096.0808,9.2360115,0.0,0,1,premium,urban +store_A,37,1027.4221,10.923796,0.0,0,2,premium,urban +store_A,38,1033.1619,10.817162,0.0,0,3,premium,urban +store_A,39,1269.5414,10.399414,1.0,0,4,premium,urban +store_A,40,1147.2571,9.53174,0.0,0,5,premium,urban +store_A,41,1116.2965,10.938353,0.0,0,6,premium,urban +store_A,42,1072.0729,10.557502,0.0,0,0,premium,urban +store_A,43,1129.3868,10.433781,0.0,0,1,premium,urban +store_A,44,1295.5614,9.898723,1.0,0,2,premium,urban +store_A,45,1320.1937,9.544483,1.0,0,3,premium,urban +store_A,46,1223.4036,9.192781,0.0,0,4,premium,urban +store_A,47,1523.2692,10.805204,1.0,1,5,premium,urban +store_A,48,1229.2423,9.911552,0.0,0,6,premium,urban +store_A,49,1224.824,9.404727,0.0,0,0,premium,urban +store_A,50,1248.2861,9.611914,0.0,0,1,premium,urban +store_A,51,1621.3419,10.158439,1.0,1,2,premium,urban +store_A,52,1200.0713,9.353545,0.0,0,3,premium,urban +store_A,53,1246.8055,10.713228,0.0,0,4,premium,urban +store_A,54,1260.0721,10.517039,0.0,0,5,premium,urban +store_A,55,1419.738,10.438926,1.0,0,6,premium,urban +store_A,56,1465.4315,9.864186,1.0,0,0,premium,urban +store_A,57,1411.4612,10.254618,0.0,0,1,premium,urban +store_A,58,1459.6567,10.168196,1.0,0,2,premium,urban +store_A,59,1562.2711,10.299693,1.0,0,3,premium,urban +store_B,0,949.5817,10.130472,0.0,1,0,premium,urban +store_B,1,826.9795,10.529998,0.0,0,1,premium,urban +store_B,2,795.8978,10.269437,0.0,0,2,premium,urban +store_B,3,781.1968,10.107159,0.0,0,3,premium,urban +store_B,4,869.75146,10.118414,0.0,0,4,premium,urban +store_B,5,840.91705,9.607901,0.0,0,5,premium,urban +store_B,6,900.90045,9.061636,0.0,0,6,premium,urban +store_B,7,862.10693,9.873435,0.0,0,0,premium,urban +store_B,8,811.1614,9.42917,0.0,0,1,premium,urban +store_B,9,814.42114,9.817058,1.0,0,2,premium,urban +store_B,10,953.70746,10.706806,0.0,0,3,premium,urban +store_B,11,1161.8647,9.467879,0.0,1,4,premium,urban +store_B,12,901.0838,9.116606,0.0,0,5,premium,urban +store_B,13,896.9283,9.562768,0.0,0,6,premium,urban +store_B,14,1121.0658,9.587188,1.0,0,0,premium,urban +store_B,15,1012.14496,10.323833,0.0,0,1,premium,urban +store_B,16,845.7787,10.114064,0.0,0,2,premium,urban +store_B,17,942.0486,10.567797,0.0,0,3,premium,urban +store_B,18,894.31323,10.328627,0.0,0,4,premium,urban +store_B,19,1029.0061,9.812774,0.0,0,5,premium,urban +store_B,20,896.51886,10.62804,1.0,0,6,premium,urban +store_B,21,1061.0464,9.333946,0.0,0,0,premium,urban +store_B,22,963.2019,9.045424,0.0,0,1,premium,urban +store_B,23,1091.6201,9.180096,0.0,1,2,premium,urban +store_B,24,915.2826,10.444718,0.0,0,3,premium,urban +store_B,25,771.0792,9.923755,0.0,0,4,premium,urban +store_B,26,858.0784,9.322543,0.0,0,5,premium,urban +store_B,27,814.89954,10.0020895,0.0,0,6,premium,urban +store_B,28,916.48206,9.304625,1.0,0,0,premium,urban +store_B,29,772.1533,10.392641,1.0,0,1,premium,urban +store_B,30,803.5763,9.892313,0.0,0,2,premium,urban +store_B,31,862.519,9.762042,0.0,0,3,premium,urban +store_B,32,737.1871,9.6030245,0.0,0,4,premium,urban +store_B,33,785.4303,10.260565,0.0,0,5,premium,urban +store_B,34,906.9479,9.723625,0.0,0,6,premium,urban +store_B,35,994.5817,9.1753,0.0,1,0,premium,urban +store_B,36,1004.37634,9.2360115,0.0,0,1,premium,urban +store_B,37,979.0918,10.923796,0.0,0,2,premium,urban +store_B,38,870.12354,10.817162,0.0,0,3,premium,urban +store_B,39,785.6754,10.399414,1.0,0,4,premium,urban +store_B,40,769.2815,9.53174,0.0,0,5,premium,urban +store_B,41,963.49274,10.938353,0.0,0,6,premium,urban +store_B,42,831.17865,10.557502,0.0,0,0,premium,urban +store_B,43,830.58295,10.433781,0.0,0,1,premium,urban +store_B,44,794.41534,9.898723,1.0,0,2,premium,urban +store_B,45,835.0851,9.544483,1.0,0,3,premium,urban +store_B,46,885.5207,9.192781,0.0,0,4,premium,urban +store_B,47,1178.3236,10.805204,1.0,1,5,premium,urban +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