import numpy as np from xgboost import XGBClassifier from sklearn.metrics import accuracy_score, f1_score, roc_auc_score import sys sys.path.insert(0, '.') from data.data_loader import load_data, preprocess, split_and_scale from src.smote import smote from src.hyperparameter_tuning import grid_search # run at import time so BEST_ALPHA/BEST_LAMBDA are available to both experiments BEST_ALPHA, BEST_LAMBDA = grid_search() XGB_PARAMS = dict( n_estimators=100, max_depth=4, learning_rate=0.05, subsample=0.7, colsample_bytree=0.7, min_child_weight=5, reg_alpha=BEST_ALPHA, reg_lambda=BEST_LAMBDA, eval_metric='logloss', early_stopping_rounds=15, random_state=42, ) # searches val set for threshold maximising F1 — test set never touched def find_best_threshold(y_true, proba): best_thresh, best_f1 = 0.5, 0.0 for t in np.linspace(0.1, 0.9, 81): preds = (proba >= t).astype(int) f1 = f1_score(y_true, preds, zero_division=0) if f1 > best_f1: best_f1, best_thresh = f1, t return best_thresh, best_f1 def evaluate(model, X, y, name, threshold=0.5): proba = model.predict_proba(X)[:, 1] preds = (proba >= threshold).astype(int) acc = accuracy_score(y, preds) f1 = f1_score(y, preds, zero_division=0) auc = roc_auc_score(y, proba) suffix = f" (thresh={threshold:.2f})" if threshold != 0.5 else "" print(f" {name:48s} | F1: {f1:.3f} | AUC: {auc:.3f} | Acc: {acc:.3f}{suffix}") return acc, f1, auc def run_condition(label, X_train, y_train, X_val, y_val, X_test, y_test, use_smote=False, use_threshold_tuning=False): if use_smote: X_train, y_train = smote(X_train, np.array(y_train), k=5, random_state=42) model = XGBClassifier(**XGB_PARAMS) model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False) threshold = 0.5 if use_threshold_tuning: val_proba = model.predict_proba(X_val)[:, 1] threshold, _ = find_best_threshold(y_val, val_proba) _, f1, auc = evaluate(model, X_test, y_test, label, threshold=threshold) return f1, auc def run_preprocessing_experiment(df): results = {} X, y, _ = preprocess(df.copy(), use_domain_cleaning=False) X_train, X_val, X_test, y_train, y_val, y_test, _ = split_and_scale(X, y) results['Baseline'] = run_condition( "Baseline", X_train, y_train, X_val, y_val, X_test, y_test) X, y, _ = preprocess(df.copy(), use_domain_cleaning=True) X_train, X_val, X_test, y_train, y_val, y_test, _ = split_and_scale(X, y) results['Domain cleaning'] = run_condition( "Domain cleaning", X_train, y_train, X_val, y_val, X_test, y_test) results['+ SMOTE'] = run_condition( "Domain cleaning + SMOTE", X_train, y_train, X_val, y_val, X_test, y_test, use_smote=True) results['+ Threshold'] = run_condition( "Domain cleaning + threshold tuning", X_train, y_train, X_val, y_val, X_test, y_test, use_threshold_tuning=True) results['Full pipeline'] = run_condition( "Full pipeline (cleaning + SMOTE + threshold)", X_train, y_train, X_val, y_val, X_test, y_test, use_smote=True, use_threshold_tuning=True) print(f"\n {'Condition':<40} | {'F1':>6} | {'AUC':>6}") for name, (f1, auc) in results.items(): marker = " ◀" if f1 == max(v[0] for v in results.values()) else "" print(f" {name:<40} | {f1:>6.3f} | {auc:>6.3f}{marker}") def regularization_experiment(df): from sklearn.metrics import roc_auc_score X, y, _ = preprocess(df.copy(), use_domain_cleaning=True) X_train, X_val, X_test, y_train, y_val, y_test, _ = split_and_scale(X, y) X_train_s, y_train_s = smote(X_train, np.array(y_train), random_state=42) def fit_eval(name, max_depth=8, min_child_weight=1, reg_alpha=0, reg_lambda=0, use_early_stopping=False, n_estimators=500): model = XGBClassifier( n_estimators=n_estimators, max_depth=max_depth, learning_rate=0.05, subsample=0.7, colsample_bytree=0.7, min_child_weight=min_child_weight, reg_alpha=reg_alpha, reg_lambda=reg_lambda, eval_metric='logloss', random_state=42, **(dict(early_stopping_rounds=20) if use_early_stopping else {}), ) fit_kw = dict(eval_set=[(X_val, y_val)], verbose=False) \ if use_early_stopping else dict(verbose=False) model.fit(X_train_s, y_train_s, **fit_kw) stopped_at = (model.best_iteration + 1) if use_early_stopping else n_estimators train_auc = roc_auc_score(y_train_s, model.predict_proba(X_train_s)[:, 1]) test_auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) gap = train_auc - test_auc print(f" {name:<42s} | train: {train_auc:.3f} | test: {test_auc:.3f} | gap: {gap:.3f} | trees: {stopped_at}") return gap, test_auc print(f"\n {'Condition':<42s} | train AUC | test AUC | gap | trees") r = {} r['No reg'] = fit_eval("No regularization (depth=8, min_child=1)") r['L2'] = fit_eval("L2 only (reg_lambda=5.0)", reg_lambda=5.0) r['L1'] = fit_eval("L1 only (reg_alpha=2.0)", reg_alpha=2.0) r['L1+L2'] = fit_eval("L1 + L2 (alpha=2.0, lambda=5.0)", reg_alpha=2.0, reg_lambda=5.0) r['ES'] = fit_eval("Early stopping only (depth=8)", use_early_stopping=True) r['Production'] = fit_eval("L1 + L2 + early stopping ◀ production", max_depth=4, min_child_weight=5, reg_alpha=BEST_ALPHA, reg_lambda=BEST_LAMBDA, use_early_stopping=True) bg, ba = r['No reg'] fg, fa = r['Production'] print(f"\n gap {bg:.3f} → {fg:.3f} | test AUC {ba:.3f} → {fa:.3f}") if __name__ == '__main__': df = load_data() run_preprocessing_experiment(df) regularization_experiment(df)