Spaces:
Sleeping
Sleeping
File size: 7,393 Bytes
61b4af1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | # scripts/evaluate_models.py
#
# Evaluate Moirai (zero-shot) vs a naive baseline
# using historical backtesting on multiple series.
#
# Idea:
# - Use the scaled Moirai dataset: data/processed/mandi_moirai_scaled.parquet
# - For each series_id with enough history:
# * Take last PRED_LEN points as test
# * Use the rest as context
# * Naive forecast = last context ModalPrice repeated PRED_LEN times
# * Moirai forecast = run predictor on truncated df (no leakage), get PRED_LEN preds
# - Compare to actual test ModalPrice using MAE, RMSE, MAPE
#
# NOTE:
# - LightGBM evaluation is left as a TODO stub at the bottom.
from pathlib import Path
import numpy as np
import pandas as pd
from typing import List, Dict, Tuple
from moirai_forecast_single import (
load_scaled_data,
load_moirai_predictor,
forecast_series_with_predictor,
PRED_LEN,
)
DATA_SCALED_PATH = Path("data/processed/mandi_moirai_scaled.parquet")
SERIES_MAPPING_PATH = Path("data/metadata/series_mapping.csv")
# Evaluation config
EVAL_PRED_LEN = PRED_LEN # use same horizon as production (14)
MIN_SERIES_LEN = 60 # require at least this many points per series
MAX_SERIES_EVAL = 100 # evaluate on at most this many series for speed
def mae(y_true: np.ndarray, y_pred: np.ndarray) -> float:
return float(np.mean(np.abs(y_true - y_pred)))
def rmse(y_true: np.ndarray, y_pred: np.ndarray) -> float:
return float(np.sqrt(np.mean((y_true - y_pred) ** 2)))
def mape(y_true: np.ndarray, y_pred: np.ndarray) -> float:
# avoid division by 0
eps = 1e-6
return float(np.mean(np.abs((y_true - y_pred) / (np.abs(y_true) + eps))) * 100.0)
def select_eval_series(df_scaled: pd.DataFrame) -> List[int]:
"""
Choose series_id that have enough length
and return up to MAX_SERIES_EVAL of them.
"""
counts = df_scaled.groupby("series_id").size()
valid_series = counts[counts >= (MIN_SERIES_LEN + EVAL_PRED_LEN)].index.tolist()
valid_series = sorted(valid_series)
if len(valid_series) > MAX_SERIES_EVAL:
valid_series = valid_series[:MAX_SERIES_EVAL]
return valid_series
def evaluate_moirai_vs_naive() -> None:
print("Loading scaled data...")
df_scaled = load_scaled_data() # same as used by Moirai forecasting
df_scaled["date"] = pd.to_datetime(df_scaled["date"])
print(f"Total rows in scaled data: {len(df_scaled)}")
eval_series_ids = select_eval_series(df_scaled)
print(f"Evaluating on {len(eval_series_ids)} series (series_id).")
print("Loading Moirai predictor...")
predictor = load_moirai_predictor()
metrics_naive: List[Dict] = []
metrics_moirai: List[Dict] = []
for idx, sid in enumerate(eval_series_ids):
df_s_full = df_scaled[df_scaled["series_id"] == sid].sort_values("date").copy()
if len(df_s_full) < MIN_SERIES_LEN + EVAL_PRED_LEN:
# extra safety
continue
# Split into context and test
context = df_s_full.iloc[:-EVAL_PRED_LEN]
test = df_s_full.iloc[-EVAL_PRED_LEN:]
# Ground truth = actual ModalPrice over last EVAL_PRED_LEN days
# This column should be present from the original engineered data
y_true = test["ModalPrice"].to_numpy(dtype=float)
# --------------------
# 1) Naive forecast
# --------------------
last_price = context["ModalPrice"].iloc[-1]
y_naive = np.full(shape=EVAL_PRED_LEN, fill_value=float(last_price))
# --------------------
# 2) Moirai forecast (backtest style)
# Trick: drop test rows for this series from df_scaled so Moirai
# only sees context, then forecast from there.
# --------------------
ctx_indices = context.index
df_ctx = df_scaled.drop(index=test.index)
try:
forecast_df = forecast_series_with_predictor(
predictor=predictor,
df_scaled=df_ctx,
series_id=sid,
pred_len=EVAL_PRED_LEN,
)
if forecast_df is None or forecast_df.empty:
print(f"[SKIP] Moirai returned empty for series_id={sid}")
continue
y_moirai = forecast_df["pred_modal_price"].to_numpy(dtype=float)
if len(y_moirai) != EVAL_PRED_LEN:
print(f"[SKIP] Moirai forecast length mismatch for series_id={sid}")
continue
except Exception as e:
print(f"[ERROR] Moirai forecast failed for series_id={sid}: {e}")
continue
# --------------------
# Compute metrics
# --------------------
row_meta = {
"series_id": sid,
"n_test": EVAL_PRED_LEN,
}
metrics_naive.append(
{
**row_meta,
"mae": mae(y_true, y_naive),
"rmse": rmse(y_true, y_naive),
"mape": mape(y_true, y_naive),
}
)
metrics_moirai.append(
{
**row_meta,
"mae": mae(y_true, y_moirai),
"rmse": rmse(y_true, y_moirai),
"mape": mape(y_true, y_moirai),
}
)
if (idx + 1) % 10 == 0:
print(f"Evaluated {idx+1}/{len(eval_series_ids)} series...")
# --------------------
# Aggregate results
# --------------------
if not metrics_naive or not metrics_moirai:
print("No metrics computed; something went wrong or no valid series.")
return
df_naive = pd.DataFrame(metrics_naive)
df_moirai = pd.DataFrame(metrics_moirai)
print("\n=== Per-series metrics (head) ===")
print("Naive baseline:")
print(df_naive.head())
print("\nMoirai:")
print(df_moirai.head())
print("\n=== Aggregate metrics over all evaluated series ===")
agg_naive = df_naive[["mae", "rmse", "mape"]].mean()
agg_moirai = df_moirai[["mae", "rmse", "mape"]].mean()
print("\nNaive baseline (avg over series):")
print(agg_naive)
print("\nMoirai (avg over series):")
print(agg_moirai)
# save to CSV for reporting
out_dir = Path("outputs/eval")
out_dir.mkdir(parents=True, exist_ok=True)
df_naive.to_csv(out_dir / "eval_naive_per_series.csv", index=False)
df_moirai.to_csv(out_dir / "eval_moirai_per_series.csv", index=False)
summary_df = pd.DataFrame(
{
"model": ["naive", "moirai"],
"mae": [agg_naive["mae"], agg_moirai["mae"]],
"rmse": [agg_naive["rmse"], agg_moirai["rmse"]],
"mape": [agg_naive["mape"], agg_moirai["mape"]],
}
)
summary_df.to_csv(out_dir / "eval_summary.csv", index=False)
print(f"\nSaved detailed metrics and summary to {out_dir}")
# ------------------------
# TODO: LightGBM evaluation
# ------------------------
#
# Outline (not implemented yet):
#
# def evaluate_lightgbm():
# - Load data/processed/training_data.parquet
# - Load models/price_lgbm_model.pkl
# - Do a time-based split on 'date' (e.g., last 60 days as test)
# - Build X_test from the same features used in train_model.py
# - Predict with model.predict(X_test)
# - Compute MAE, RMSE, MAPE vs target column
#
# Then compare its aggregate metrics with naive and Moirai.
if __name__ == "__main__":
evaluate_moirai_vs_naive()
|