Spaces:
Build error
Build error
| """ | |
| train.py | |
| LightGBM model with: | |
| - Stratified K-Fold cross validation (5 fold) | |
| - Class imbalance handling (scale_pos_weight) | |
| - Early stopping | |
| - Feature importance | |
| - Model + metadata saved to models/ | |
| """ | |
| import os | |
| import json | |
| import warnings | |
| import numpy as np | |
| import pandas as pd | |
| import lightgbm as lgb | |
| import joblib | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from pathlib import Path | |
| from sklearn.model_selection import StratifiedKFold | |
| from sklearn.metrics import ( | |
| roc_auc_score, average_precision_score, | |
| classification_report, confusion_matrix, | |
| roc_curve, precision_recall_curve | |
| ) | |
| warnings.filterwarnings("ignore") | |
| # ββ Paths ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PROCESSED_DIR = Path("data/processed") | |
| MODELS_DIR = Path("models") | |
| MODELS_DIR.mkdir(parents=True, exist_ok=True) | |
| # ββ LightGBM Parameters ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # These are well-tuned defaults for this dataset based on top Kaggle solutions | |
| LGB_PARAMS = { | |
| "objective": "binary", | |
| "metric": "auc", | |
| "boosting_type": "gbdt", | |
| "n_estimators": 5000, | |
| "learning_rate": 0.02, | |
| "num_leaves": 34, | |
| "max_depth": -1, | |
| "min_child_samples": 80, | |
| "min_child_weight": 40, | |
| "subsample": 0.85, | |
| "subsample_freq": 1, | |
| "colsample_bytree": 0.85, | |
| "reg_alpha": 0.1, | |
| "reg_lambda": 0.1, | |
| "min_split_gain": 0.02, | |
| "cat_smooth": 10, | |
| "random_state": 42, | |
| "n_jobs": -1, | |
| "verbose": -1, | |
| } | |
| N_FOLDS = 5 | |
| THRESHOLD = 0.5 # default β we tune this below | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 1. LOAD DATA | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_data(): | |
| print("[1/5] Loading processed data ...") | |
| train = pd.read_csv(PROCESSED_DIR / "train_processed.csv") | |
| test = pd.read_csv(PROCESSED_DIR / "test_processed.csv") | |
| feature_cols = [c for c in train.columns if c not in ("TARGET", "SK_ID_CURR")] | |
| # Drop any remaining object/string columns that slipped through preprocessing | |
| obj_cols_train = train[feature_cols].select_dtypes(include=["object", "category"]).columns.tolist() | |
| obj_cols_test = test[feature_cols].select_dtypes(include=["object", "category"]).columns.tolist() | |
| drop_str_cols = list(set(obj_cols_train + obj_cols_test)) | |
| if drop_str_cols: | |
| print(f" Dropping {len(drop_str_cols)} leftover string columns: {drop_str_cols}") | |
| feature_cols = [c for c in feature_cols if c not in drop_str_cols] | |
| # Verify no strings remain | |
| remaining_obj = train[feature_cols].select_dtypes(include=["object","category"]).columns.tolist() | |
| if remaining_obj: | |
| raise ValueError(f"Still has string columns: {remaining_obj}") | |
| X = train[feature_cols].values.astype(np.float32) | |
| y = train["TARGET"].values.astype(np.int8) | |
| X_test = test[feature_cols].values.astype(np.float32) | |
| ids = test["SK_ID_CURR"].values | |
| print(f" Final feature count: {len(feature_cols)}") | |
| print(f" X_train : {X.shape}") | |
| print(f" X_test : {X_test.shape}") | |
| print(f" Positive rate: {y.mean():.4f} " | |
| f"(imbalance ratio: {(1-y.mean())/y.mean():.1f}:1)") | |
| return X, y, X_test, ids, feature_cols | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 2. CROSS-VALIDATED TRAINING | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def train_cv(X, y, X_test, feature_cols): | |
| print(f"\n[2/5] Training LightGBM β {N_FOLDS}-fold Stratified CV ...") | |
| skf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=42) | |
| oof_preds = np.zeros(len(X)) # out-of-fold predictions | |
| test_preds = np.zeros(len(X_test)) # averaged test predictions | |
| fold_aucs = [] | |
| fold_models = [] | |
| # Class imbalance: weight negative class less | |
| neg_pos_ratio = (y == 0).sum() / (y == 1).sum() | |
| for fold, (tr_idx, val_idx) in enumerate(skf.split(X, y), 1): | |
| print(f"\n ββ Fold {fold}/{N_FOLDS} ββ") | |
| X_tr, X_val = X[tr_idx], X[val_idx] | |
| y_tr, y_val = y[tr_idx], y[val_idx] | |
| params = LGB_PARAMS.copy() | |
| params["scale_pos_weight"] = neg_pos_ratio | |
| model = lgb.LGBMClassifier(**params) | |
| model.fit( | |
| X_tr, y_tr, | |
| eval_set = [(X_val, y_val)], | |
| eval_metric = "auc", | |
| callbacks = [ | |
| lgb.early_stopping(stopping_rounds=100, verbose=False), | |
| lgb.log_evaluation(period=200), | |
| ], | |
| ) | |
| # OOF predictions | |
| val_pred = model.predict_proba(X_val)[:, 1] | |
| oof_preds[val_idx] = val_pred | |
| fold_auc = roc_auc_score(y_val, val_pred) | |
| fold_aucs.append(fold_auc) | |
| print(f" Fold {fold} AUC = {fold_auc:.5f} " | |
| f"Best iter = {model.best_iteration_}") | |
| # Test predictions (average across folds) | |
| test_preds += model.predict_proba(X_test)[:, 1] / N_FOLDS | |
| fold_models.append(model) | |
| print(f"\n ββ CV Summary ββ") | |
| print(f" Fold AUCs : {[round(a,5) for a in fold_aucs]}") | |
| print(f" Mean AUC : {np.mean(fold_aucs):.5f}") | |
| print(f" Std AUC : {np.std(fold_aucs):.5f}") | |
| print(f" OOF AUC : {roc_auc_score(y, oof_preds):.5f}") | |
| print(f" OOF Avg Prec : {average_precision_score(y, oof_preds):.5f}") | |
| return fold_models, oof_preds, test_preds, fold_aucs | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 3. THRESHOLD TUNING | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def tune_threshold(y_true, oof_preds): | |
| """ | |
| Find threshold that maximises F1 on OOF predictions. | |
| Important for imbalanced datasets β default 0.5 is rarely optimal. | |
| """ | |
| print("\n[3/5] Tuning classification threshold ...") | |
| thresholds = np.arange(0.05, 0.95, 0.01) | |
| f1_scores = [] | |
| for t in thresholds: | |
| preds = (oof_preds >= t).astype(int) | |
| tp = ((preds == 1) & (y_true == 1)).sum() | |
| fp = ((preds == 1) & (y_true == 0)).sum() | |
| fn = ((preds == 0) & (y_true == 1)).sum() | |
| prec = tp / (tp + fp + 1e-9) | |
| rec = tp / (tp + fn + 1e-9) | |
| f1 = 2 * prec * rec / (prec + rec + 1e-9) | |
| f1_scores.append(f1) | |
| best_idx = np.argmax(f1_scores) | |
| best_threshold = thresholds[best_idx] | |
| best_f1 = f1_scores[best_idx] | |
| print(f" Best threshold : {best_threshold:.2f}") | |
| print(f" Best F1 : {best_f1:.4f}") | |
| return float(best_threshold) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4. EVALUATION REPORT | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def evaluate(y_true, oof_preds, threshold, fold_aucs): | |
| print(f"\n[4/5] Full evaluation (threshold = {threshold:.2f}) ...") | |
| preds_binary = (oof_preds >= threshold).astype(int) | |
| print("\n Classification Report:") | |
| print(classification_report(y_true, preds_binary, | |
| target_names=["No Default", "Default"], | |
| digits=4)) | |
| cm = confusion_matrix(y_true, preds_binary) | |
| tn, fp, fn, tp = cm.ravel() | |
| print(f" Confusion Matrix : TN={tn} FP={fp} FN={fn} TP={tp}") | |
| print(f" Sensitivity/Recall : {tp/(tp+fn):.4f} (defaults caught)") | |
| print(f" Specificity : {tn/(tn+fp):.4f} (non-defaults correct)") | |
| print(f" ROC-AUC (OOF) : {roc_auc_score(y_true, oof_preds):.5f}") | |
| print(f" Avg Precision : {average_precision_score(y_true, oof_preds):.5f}") | |
| # Save metrics | |
| metrics = { | |
| "oof_roc_auc": round(roc_auc_score(y_true, oof_preds), 5), | |
| "avg_precision": round(average_precision_score(y_true, oof_preds), 5), | |
| "best_threshold": round(threshold, 4), | |
| "fold_aucs": [round(a, 5) for a in fold_aucs], | |
| "cv_mean_auc": round(np.mean(fold_aucs), 5), | |
| "cv_std_auc": round(np.std(fold_aucs), 5), | |
| "confusion_matrix": {"TN": int(tn), "FP": int(fp), | |
| "FN": int(fn), "TP": int(tp)}, | |
| } | |
| with open(MODELS_DIR / "metrics.json", "w") as f: | |
| json.dump(metrics, f, indent=2) | |
| print(f"\n Metrics saved β models/metrics.json") | |
| return metrics | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 5. FEATURE IMPORTANCE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_feature_importance(fold_models, feature_cols): | |
| print("\n Computing feature importance ...") | |
| # Average importance across folds | |
| importance_df = pd.DataFrame({ | |
| "feature": feature_cols, | |
| "importance": np.mean([m.feature_importances_ for m in fold_models], axis=0) | |
| }).sort_values("importance", ascending=False).reset_index(drop=True) | |
| importance_df.to_csv(MODELS_DIR / "feature_importance.csv", index=False) | |
| # Plot top 40 | |
| top40 = importance_df.head(40) | |
| fig, ax = plt.subplots(figsize=(10, 12)) | |
| ax.barh(top40["feature"][::-1], top40["importance"][::-1], color="#1a73e8") | |
| ax.set_xlabel("Average Importance (across 5 folds)") | |
| ax.set_title("Top 40 Feature Importances β Home Credit Default Model") | |
| ax.tick_params(axis="y", labelsize=8) | |
| plt.tight_layout() | |
| plt.savefig(MODELS_DIR / "feature_importance.png", dpi=150) | |
| plt.close() | |
| print(f" Top 5 features: {importance_df['feature'].head(5).tolist()}") | |
| print(f" Importance chart β models/feature_importance.png") | |
| return importance_df | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 6. SAVE ARTEFACTS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_artifacts(fold_models, test_preds, ids, threshold, | |
| feature_cols, metrics, importance_df): | |
| print("\n[5/5] Saving artefacts ...") | |
| # Save all fold models | |
| for i, m in enumerate(fold_models): | |
| joblib.dump(m, MODELS_DIR / f"lgbm_fold_{i+1}.pkl") | |
| # Save best single model (fold with highest AUC) | |
| best_idx = int(np.argmax(metrics["fold_aucs"])) | |
| joblib.dump(fold_models[best_idx], MODELS_DIR / "lgbm_best.pkl") | |
| print(f" Best model = fold {best_idx+1} " | |
| f"(AUC={metrics['fold_aucs'][best_idx]})") | |
| # Save feature columns list | |
| with open(MODELS_DIR / "feature_cols.json", "w") as f: | |
| json.dump(feature_cols, f) | |
| # Save threshold | |
| with open(MODELS_DIR / "threshold.json", "w") as f: | |
| json.dump({"threshold": threshold}, f) | |
| # Save test submission | |
| submission = pd.DataFrame({ | |
| "SK_ID_CURR": ids, | |
| "TARGET": test_preds | |
| }) | |
| submission.to_csv(MODELS_DIR / "submission.csv", index=False) | |
| print(f" Submission saved β models/submission.csv") | |
| # Save OOF predictions | |
| oof_df = pd.read_csv(PROCESSED_DIR / "train_processed.csv", | |
| usecols=["SK_ID_CURR", "TARGET"]) | |
| oof_df["OOF_PRED"] = np.round( | |
| np.load(MODELS_DIR / "oof_preds.npy") if | |
| (MODELS_DIR / "oof_preds.npy").exists() else | |
| np.zeros(len(oof_df)), 6) | |
| print(f" All artefacts saved to: {MODELS_DIR}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ENTRY POINT | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_training(): | |
| print("\n" + "="*60) | |
| print(" HOME CREDIT β LIGHTGBM TRAINING PIPELINE") | |
| print("="*60) | |
| X, y, X_test, ids, feature_cols = load_data() | |
| fold_models, oof_preds, test_preds, fold_aucs = train_cv( | |
| X, y, X_test, feature_cols) | |
| # Save OOF preds array for later use | |
| np.save(MODELS_DIR / "oof_preds.npy", oof_preds) | |
| np.save(MODELS_DIR / "test_preds.npy", test_preds) | |
| threshold = tune_threshold(y, oof_preds) | |
| metrics = evaluate(y, oof_preds, threshold, fold_aucs) | |
| importance_df = save_feature_importance(fold_models, feature_cols) | |
| save_artifacts(fold_models, test_preds, ids, threshold, | |
| feature_cols, metrics, importance_df) | |
| print("\n" + "="*60) | |
| print(" TRAINING COMPLETE") | |
| print(f" OOF ROC-AUC : {metrics['oof_roc_auc']}") | |
| print(f" CV Mean AUC : {metrics['cv_mean_auc']} Β± {metrics['cv_std_auc']}") | |
| print(f" Threshold : {threshold:.2f}") | |
| print("="*60 + "\n") | |
| return fold_models, oof_preds, test_preds, metrics | |
| if __name__ == "__main__": | |
| run_training() |