Spaces:
Sleeping
Sleeping
| """Train the preparation-time regression model. | |
| Three models are compared on identical features and split: | |
| 1. LinearRegression (baseline) | |
| 2. RandomForestRegressor | |
| 3. XGBRegressor | |
| The best model (by MAE) is persisted as a sklearn Pipeline so that the | |
| preprocessing transformers travel together with the estimator. | |
| Usage: | |
| python -m src.ml.train | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import joblib | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.compose import ColumnTransformer | |
| from sklearn.ensemble import RandomForestRegressor | |
| from sklearn.impute import SimpleImputer | |
| from sklearn.linear_model import LinearRegression | |
| from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.preprocessing import OneHotEncoder, StandardScaler | |
| from xgboost import XGBRegressor | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[2])) | |
| from src.config import ML_METRICS_PATH, ML_PIPELINE_PATH, PROCESSED_DIR # noqa: E402 | |
| from src.ml.feature_engineering import ( # noqa: E402 | |
| CATEGORICAL_FEATURES, | |
| FEATURES_CSV, | |
| NUMERIC_FEATURES, | |
| TARGET, | |
| ) | |
| RANDOM_STATE = 42 | |
| def build_preprocessor(num_cols: list[str], cat_cols: list[str]) -> ColumnTransformer: | |
| numeric_pipe = Pipeline( | |
| [("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler())] | |
| ) | |
| categorical_pipe = Pipeline( | |
| [ | |
| ("impute", SimpleImputer(strategy="most_frequent")), | |
| ("oh", OneHotEncoder(handle_unknown="ignore", sparse_output=False)), | |
| ] | |
| ) | |
| return ColumnTransformer( | |
| [("num", numeric_pipe, num_cols), ("cat", categorical_pipe, cat_cols)], | |
| remainder="drop", | |
| ) | |
| def evaluate(name: str, y_true: pd.Series, y_pred: np.ndarray) -> dict: | |
| mae = mean_absolute_error(y_true, y_pred) | |
| rmse = float(np.sqrt(mean_squared_error(y_true, y_pred))) | |
| r2 = r2_score(y_true, y_pred) | |
| return {"model": name, "MAE": float(mae), "RMSE": rmse, "R2": float(r2)} | |
| def main() -> None: | |
| if not FEATURES_CSV.exists(): | |
| raise FileNotFoundError( | |
| f"{FEATURES_CSV} missing. Run 'python -m src.ml.feature_engineering' first." | |
| ) | |
| df = pd.read_csv(FEATURES_CSV) | |
| print(f"[train] dataset shape: {df.shape}") | |
| num_cols = [c for c in NUMERIC_FEATURES if c in df.columns] | |
| cat_cols = [c for c in CATEGORICAL_FEATURES if c in df.columns] | |
| feature_cols = num_cols + cat_cols | |
| X = df[feature_cols] | |
| y = df[TARGET] | |
| X_train, X_test, y_train, y_test = train_test_split( | |
| X, y, test_size=0.2, random_state=RANDOM_STATE | |
| ) | |
| candidates = { | |
| "LinearRegression": LinearRegression(), | |
| "RandomForest": RandomForestRegressor( | |
| n_estimators=300, | |
| max_depth=18, | |
| min_samples_leaf=3, | |
| n_jobs=-1, | |
| random_state=RANDOM_STATE, | |
| ), | |
| "XGBoost": XGBRegressor( | |
| n_estimators=600, | |
| max_depth=7, | |
| learning_rate=0.05, | |
| subsample=0.9, | |
| colsample_bytree=0.9, | |
| random_state=RANDOM_STATE, | |
| n_jobs=-1, | |
| tree_method="hist", | |
| ), | |
| } | |
| preprocessor = build_preprocessor(num_cols, cat_cols) | |
| results: list[dict] = [] | |
| fitted: dict[str, Pipeline] = {} | |
| for name, estimator in candidates.items(): | |
| pipe = Pipeline([("prep", preprocessor), ("model", estimator)]) | |
| pipe.fit(X_train, y_train) | |
| preds = pipe.predict(X_test) | |
| metrics = evaluate(name, y_test, preds) | |
| results.append(metrics) | |
| fitted[name] = pipe | |
| print( | |
| f"[train] {name:>17} MAE={metrics['MAE']:.2f} " | |
| f"RMSE={metrics['RMSE']:.2f} R2={metrics['R2']:.3f}" | |
| ) | |
| best = min(results, key=lambda r: r["MAE"]) | |
| best_name = best["model"] | |
| print(f"[train] best model: {best_name} (MAE {best['MAE']:.2f})") | |
| ML_PIPELINE_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| joblib.dump( | |
| { | |
| "pipeline": fitted[best_name], | |
| "feature_cols": feature_cols, | |
| "num_cols": num_cols, | |
| "cat_cols": cat_cols, | |
| "target": TARGET, | |
| "best_model": best_name, | |
| }, | |
| ML_PIPELINE_PATH, | |
| ) | |
| print(f"[train] saved pipeline to {ML_PIPELINE_PATH}") | |
| ML_METRICS_PATH.write_text( | |
| json.dumps({"results": results, "best": best_name}, indent=2) | |
| ) | |
| print(f"[train] wrote metrics to {ML_METRICS_PATH}") | |
| if __name__ == "__main__": | |
| main() | |