gridpulse / scripts /diagnose_models.py
adwitiyashukla's picture
Sync from GitHub e84e64a
18a63f1 verified
Raw
History Blame Contribute Delete
4.67 kB
"""Diagnostic: find where the model predictions go wrong.
Prints the target scaler's fitted statistics, per-balancing-authority error, and
the worst individual predictions. Run after `gridpulse train`::
python scripts/diagnose_models.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from gridpulse.features.build import build_features # noqa: E402
from gridpulse.models.gbm import TrainedGBM # noqa: E402
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
TEST_DAYS = 90
VALID_DAYS = 60
def rule(title: str) -> None:
print(f"\n{'=' * 78}\n {title}\n{'=' * 78}")
def main() -> int:
rule("1. Loading model artifacts")
gbm = TrainedGBM.load()
print(f" best_iteration : {gbm.best_iteration}")
print(f" features : {len(gbm.feature_names)}")
print(f" ba_categories : {gbm.ba_categories}")
rule("2. Target scaler statistics (fitted on training rows)")
stats = pd.DataFrame(
[{"ba_code": k, "mean_mw": v[0], "std_mw": v[1]} for k, v in gbm.target_scaler.stats.items()]
).sort_values("mean_mw", ascending=False)
print(stats.to_string(index=False))
print(f"\n global_mean : {gbm.target_scaler.global_mean:,.1f}")
print(f" global_std : {gbm.target_scaler.global_std:,.1f}")
print(f" scale ratio (largest/smallest BA mean): "
f"{stats['mean_mw'].max() / stats['mean_mw'].min():.1f}x")
rule("3. Rebuilding the test split")
frame = build_features()
test_start = frame["period_utc"].max() - pd.Timedelta(days=TEST_DAYS)
test = frame[frame["period_utc"] >= test_start].copy()
print(f" test rows : {len(test):,}")
print(f" BAs : {sorted(test['ba_code'].unique())}")
rule("4. Does every test BA resolve in the scaler?")
missing = sorted(set(test["ba_code"].unique()) - set(gbm.target_scaler.stats))
if missing:
print(f" !! MISSING, will silently fall back to global stats: {missing}")
else:
print(" OK - every test BA is present in the scaler.")
rule("5. Raw model output, BEFORE the inverse transform")
matrix = test[gbm.feature_names].copy()
matrix["ba_code"] = pd.Categorical(test["ba_code"], categories=gbm.ba_categories)
raw = gbm.point_model.predict(matrix)
print(f" z-score predictions min={raw.min():9.3f} max={raw.max():9.3f} "
f"mean={raw.mean():8.3f} std={raw.std():7.3f}")
print(" (a healthy z-score prediction sits roughly within -4 .. +4)")
actual_z = gbm.target_scaler.transform(test)
print(f" z-score actuals min={actual_z.min():9.3f} max={actual_z.max():9.3f} "
f"mean={actual_z.mean():8.3f} std={actual_z.std():7.3f}")
rule("6. Predictions AFTER the inverse transform")
predicted = gbm.predict(test)
test = test.reset_index(drop=True)
predicted = predicted.reset_index(drop=True)
test["pred"] = predicted["pred_gbm"].to_numpy()
test["ape"] = (test["pred"] - test["demand_mwh"]).abs() / test["demand_mwh"] * 100
print(f" predicted MW min={test['pred'].min():12,.0f} max={test['pred'].max():12,.0f}")
print(f" actual MW min={test['demand_mwh'].min():12,.0f} max={test['demand_mwh'].max():12,.0f}")
rule("7. Error by balancing authority")
by_ba = (
test.groupby("ba_code")
.agg(
hours=("demand_mwh", "size"),
actual_mean_mw=("demand_mwh", "mean"),
pred_mean_mw=("pred", "mean"),
mape_pct=("ape", "mean"),
worst_ape_pct=("ape", "max"),
)
.sort_values("mape_pct", ascending=False)
.round(2)
)
print(by_ba.to_string())
rule("8. The 15 worst individual predictions")
worst = test.nlargest(15, "ape")[
["period_utc", "ba_code", "demand_mwh", "pred", "ape", "demand_lag_24h", "demand_lag_168h"]
]
print(worst.to_string(index=False))
rule("9. How concentrated is the damage?")
for threshold in (10, 25, 50, 100, 500):
share = (test["ape"] > threshold).mean() * 100
print(f" rows with APE > {threshold:4d}% : {share:6.2f}%")
contribution = test.nlargest(int(len(test) * 0.01), "ape")["ape"].sum() / test["ape"].sum() * 100
print(f"\n the worst 1% of rows account for {contribution:.1f}% of total absolute error")
print(f" overall MAPE {test['ape'].mean():.3f}% median APE {test['ape'].median():.3f}%")
print(" (a median far below the mean confirms a small number of extreme outliers)")
return 0
if __name__ == "__main__":
raise SystemExit(main())