23f3002638
Initial commit with LFS tracking
038ee19
"""
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()