Spaces:
Running
Running
| """ | |
| Layer 2: Model Training | |
| - Train multi-target stacked LightGBM predictor. | |
| - Base models (LOO CV) + Meta-models + Quantile regression. | |
| - SHAP explainer for interpretability. | |
| """ | |
| import pandas as pd | |
| import numpy as np | |
| import pickle | |
| import os | |
| import sys | |
| from lightgbm import LGBMRegressor | |
| from sklearn.model_selection import LeaveOneOut, cross_val_predict | |
| import shap | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| # Add parent directory to path for config import | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from config import CFG | |
| BOUNDS = { | |
| "Hardness": (0, 200), | |
| "Friability": (0, 5), | |
| "Dissolution_Rate": (50, 100), | |
| "Content_Uniformity": (80, 120), | |
| "Disintegration_Time": (0, 60) | |
| } | |
| def predict_batch(X_new: pd.DataFrame, | |
| base_median: dict, base_q10: dict, base_q90: dict, | |
| meta: dict) -> dict: | |
| """ | |
| Exposed function for real-time quality prediction of one batch. | |
| """ | |
| ranges = { | |
| "Hardness": [0, 200], | |
| "Friability": [0, 5], | |
| "Dissolution_Rate": [50, 100], | |
| "Content_Uniformity": [80, 120], | |
| "Disintegration_Time": [0, 60] | |
| } | |
| results = {} | |
| base_preds_df = pd.DataFrame(index=X_new.index) | |
| # Step 2: Meta Median Prediction | |
| for target in CFG.TARGET_COLS: | |
| # Base Prediction for this target | |
| b_pred = base_median[target].predict(X_new) | |
| # Meta features: X + base_pred | |
| X_meta = pd.concat([X_new.reset_index(drop=True), | |
| pd.Series(b_pred, name=f"base_{target}")], axis=1) | |
| X_meta.columns = [str(c) for c in X_meta.columns] | |
| median = meta[target].predict(X_meta.astype(float))[0] | |
| q10 = base_q10[target].predict(X_new.astype(float))[0] | |
| q90 = base_q90[target].predict(X_new.astype(float))[0] | |
| # Clip | |
| c_min, c_max = ranges.get(target, [0, 100]) | |
| median = np.clip(median, c_min, c_max) | |
| q10 = np.clip(q10, c_min, c_max) | |
| q90 = np.clip(q90, c_min, c_max) | |
| results[target] = { | |
| "median": float(median), | |
| "lower": float(q10), | |
| "upper": float(q90) | |
| } | |
| return results | |
| def main(): | |
| print(">>> Starting Layer 2: Stacked Model Training") | |
| with open(os.path.join(CFG.PROC_DIR, "X_final.pkl"), "rb") as f: | |
| X = pickle.load(f) | |
| with open(os.path.join(CFG.PROC_DIR, "production_clean.pkl"), "rb") as f: | |
| dfy = pickle.load(f) | |
| y = dfy.set_index("Batch_ID").loc[X.index, CFG.TARGET_COLS] | |
| base_models = {} | |
| q10_models = {} | |
| q90_models = {} | |
| meta_models = {} | |
| loo_preds = pd.DataFrame(index=X.index, columns=CFG.TARGET_COLS) | |
| loo = LeaveOneOut() | |
| for target in CFG.TARGET_COLS: | |
| print(f"Training models for {target}...") | |
| # Step 1: Base Model (LOO CV for meta-features) | |
| base_model = LGBMRegressor(**CFG.LGB_PARAMS) | |
| oof_preds = cross_val_predict(base_model, X, y[target], cv=loo) | |
| base_model.fit(X, y[target]) | |
| base_models[target] = base_model | |
| # Step 2: Quantile Models (Uncertainty) | |
| q10_params = CFG.LGB_PARAMS.copy() | |
| q10_params.update({"objective": "quantile", "alpha": 0.10}) | |
| q10_model = LGBMRegressor(**q10_params) | |
| q10_model.fit(X, y[target]) | |
| q10_models[target] = q10_model | |
| q90_params = CFG.LGB_PARAMS.copy() | |
| q90_params.update({"objective": "quantile", "alpha": 0.90}) | |
| q90_model = LGBMRegressor(**q90_params) | |
| q90_model.fit(X, y[target]) | |
| q90_models[target] = q90_model | |
| # Step 3: Meta-model | |
| # Use OOF preds as features | |
| X_meta = pd.concat([X, pd.Series(oof_preds, index=X.index, name=f"base_{target}")], axis=1) | |
| X_meta.columns = [str(c) for c in X_meta.columns] | |
| meta_model = LGBMRegressor(**CFG.LGB_PARAMS) | |
| # Final OOF for evaluation | |
| loo_preds[target] = cross_val_predict(meta_model, X_meta, y[target], cv=loo) | |
| meta_model.fit(X_meta, y[target]) | |
| meta_models[target] = meta_model | |
| # SHAP Explainer (on Dissolution meta model) | |
| try: | |
| example_meta = pd.concat([X, loo_preds["Dissolution_Rate"]], axis=1) | |
| example_meta.columns = [str(c) for c in example_meta.columns] | |
| explainer = shap.TreeExplainer(meta_models["Dissolution_Rate"]) | |
| with open(os.path.join(CFG.MODEL_DIR, "shap_explainer.pkl"), "wb") as f: | |
| pickle.dump(explainer, f) | |
| except Exception as e: | |
| print(f"SHAP Warning: {e}") | |
| # Clip LOO predictions for evaluation | |
| ranges = { | |
| "Hardness": [0, 200], | |
| "Friability": [0, 5], | |
| "Dissolution_Rate": [50, 100], | |
| "Content_Uniformity": [80, 120], | |
| "Disintegration_Time": [0, 60] | |
| } | |
| for target, r in ranges.items(): | |
| loo_preds[target] = np.clip(loo_preds[target], r[0], r[1]) | |
| # Save | |
| with open(os.path.join(CFG.MODEL_DIR, "base_models_median.pkl"), "wb") as f: | |
| pickle.dump(base_models, f) | |
| with open(os.path.join(CFG.MODEL_DIR, "base_models_q10.pkl"), "wb") as f: | |
| pickle.dump(q10_models, f) | |
| with open(os.path.join(CFG.MODEL_DIR, "base_models_q90.pkl"), "wb") as f: | |
| pickle.dump(q90_models, f) | |
| with open(os.path.join(CFG.MODEL_DIR, "meta_models.pkl"), "wb") as f: | |
| pickle.dump(meta_models, f) | |
| with open(os.path.join(CFG.MODEL_DIR, "loo_predictions.pkl"), "wb") as f: | |
| pickle.dump(loo_preds, f) | |
| print("="*60) | |
| print(f"✅ LAYER 2 COMPLETE") | |
| print(f" Models trained: 20 (5 targets x 4 types)") | |
| print(f" Output file: {os.path.join(CFG.MODEL_DIR, 'meta_models.pkl')}") | |
| print("="*60) | |
| if __name__ == "__main__": | |
| main() | |