burnout-tracker / src /preprocessing_experiment.py
Katherine
cleaned up code/print statements/comments
e703447
Raw
History Blame Contribute Delete
6.03 kB
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)