mirror of
https://github.com/K-Dense-AI/claude-scientific-skills.git
synced 2026-03-27 07:09:27 +08:00
feat(examples): add anomaly detection and covariates examples
Anomaly Detection Example: - Uses quantile forecasts as prediction intervals - Flags values outside 80%/90% CI as warnings/critical anomalies - Includes visualization with deviation plot Covariates (XReg) Example: - Demonstrates forecast_with_covariates() API - Shows dynamic numerical/categorical covariates - Shows static categorical covariates - Includes synthetic retail sales data with price, promotion, holiday SKILL.md Updates: - Added anomaly detection section with code example - Expanded covariates section with covariate types table - Added XReg modes explanation - Updated 'When not to use' section to note anomaly detection workaround
This commit is contained in:
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
@@ -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()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 386 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
Reference in New Issue
Block a user