Spaces:
Sleeping
Sleeping
| """Evaluate the persisted preparation-time pipeline. | |
| Produces error analysis: | |
| - overall metrics | |
| - residuals per time-of-day bucket | |
| - residuals per order type | |
| - distribution of absolute errors | |
| Usage: | |
| python -m src.ml.evaluate | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import joblib | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score | |
| from sklearn.model_selection import train_test_split | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[2])) | |
| from src.config import ML_METRICS_PATH, ML_PIPELINE_PATH, MODELS_DIR # noqa: E402 | |
| from src.ml.feature_engineering import FEATURES_CSV, TARGET # noqa: E402 | |
| ERROR_REPORT = MODELS_DIR / "ml_error_report.json" | |
| def main() -> None: | |
| if not ML_PIPELINE_PATH.exists(): | |
| raise FileNotFoundError("Pipeline not found. Train first with 'python -m src.ml.train'.") | |
| bundle = joblib.load(ML_PIPELINE_PATH) | |
| pipe = bundle["pipeline"] | |
| feature_cols = bundle["feature_cols"] | |
| df = pd.read_csv(FEATURES_CSV) | |
| X = df[feature_cols] | |
| y = df[TARGET] | |
| _, X_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=42) | |
| preds = pipe.predict(X_test) | |
| residuals = y_test.values - preds | |
| overall = { | |
| "MAE": float(mean_absolute_error(y_test, preds)), | |
| "RMSE": float(np.sqrt(mean_squared_error(y_test, preds))), | |
| "R2": float(r2_score(y_test, preds)), | |
| "n_test": int(len(y_test)), | |
| } | |
| print( | |
| f"[evaluate] overall MAE={overall['MAE']:.2f} RMSE={overall['RMSE']:.2f} " | |
| f"R2={overall['R2']:.3f} (n={overall['n_test']})" | |
| ) | |
| # Per time-of-day bucket | |
| eval_frame = X_test.copy() | |
| eval_frame["y_true"] = y_test.values | |
| eval_frame["y_pred"] = preds | |
| eval_frame["abs_error"] = np.abs(residuals) | |
| def _bucket(h: float) -> str: | |
| if pd.isna(h): | |
| return "unknown" | |
| h = int(h) | |
| if 5 <= h < 11: | |
| return "morning" | |
| if 11 <= h < 15: | |
| return "lunch" | |
| if 15 <= h < 18: | |
| return "afternoon" | |
| if 18 <= h < 23: | |
| return "dinner" | |
| return "late_night" | |
| error_by_daypart = {} | |
| if "hour_of_day" in eval_frame.columns: | |
| eval_frame["daypart"] = eval_frame["hour_of_day"].apply(_bucket) | |
| grp = eval_frame.groupby("daypart")["abs_error"].agg(["mean", "count"]) | |
| error_by_daypart = grp.to_dict(orient="index") | |
| print("[evaluate] MAE per daypart:") | |
| print(grp.round(2)) | |
| error_by_order = {} | |
| if "Type_of_order" in eval_frame.columns: | |
| grp = eval_frame.groupby("Type_of_order")["abs_error"].agg(["mean", "count"]) | |
| error_by_order = grp.to_dict(orient="index") | |
| print("[evaluate] MAE per order type:") | |
| print(grp.round(2)) | |
| report = { | |
| "overall": overall, | |
| "error_by_daypart": error_by_daypart, | |
| "error_by_order_type": error_by_order, | |
| "best_model": bundle.get("best_model"), | |
| } | |
| ERROR_REPORT.write_text(json.dumps(report, indent=2, default=str)) | |
| print(f"[evaluate] wrote {ERROR_REPORT}") | |
| # also refresh metrics file | |
| if ML_METRICS_PATH.exists(): | |
| prev = json.loads(ML_METRICS_PATH.read_text()) | |
| else: | |
| prev = {} | |
| prev["test_overall"] = overall | |
| ML_METRICS_PATH.write_text(json.dumps(prev, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |