Trains XGBoost and DNN separately, then feeds their validation probabilities to a logistic meta-learner that delivers the hybrid AUC lift.
def train_base_models_and_meta(X_train, y_train, X_val, y_val):
pos_ratio = y_train.mean()
scale_pos_weight = (1 - pos_ratio) / max(pos_ratio, 1e-6)
xgb_model = build_xgb_model(scale_pos_weight)
xgb_model.fit(X_train, y_train)
xgb_val = xgb_model.predict_proba(X_val)[:, 1]
dnn_model = build_dnn_model(X_train.shape[1])
dnn_model.fit(X_train, y_train, validation_data=(X_val, y_val),
epochs=DNN_EPOCHS, batch_size=DNN_BATCH_SIZE,
callbacks=[early_stop])
dnn_val = dnn_model.predict(X_val, batch_size=DNN_BATCH_SIZE).ravel()
meta_X = np.vstack([xgb_val, dnn_val]).T
meta_model = LogisticRegression(...).fit(meta_X, y_val)
hybrid_val_auc = roc_auc_score(y_val, meta_model.predict_proba(meta_X)[:, 1])
Sweeps decision thresholds to guarantee ≥90% recall on validation, then locks in the option with the strongest precision, falling back to F1 if needed.
def tune_threshold_for_recall_precision(y_true, y_proba, min_recall=0.90):
best_thresh = 0.5
best_prec = best_rec = best_f1 = -1
chosen_by = "f1_fallback"
fallback_t = 0.5
fallback_prec = fallback_rec = 0.0
fallback_f1 = -1
for t in THRESH_GRID:
y_pred = (y_proba >= t).astype(int)
prec = precision_score(y_true, y_pred, zero_division=0)
rec = recall_score(y_true, y_pred, zero_division=0)
f1 = f1_score(y_true, y_pred, zero_division=0)
if rec >= min_recall and prec > best_prec:
best_thresh = float(t)
best_prec, best_rec, best_f1 = prec, rec, f1
chosen_by = "recall_constrained_precision"
if f1 > fallback_f1:
fallback_t, fallback_prec = float(t), prec
fallback_rec, fallback_f1 = rec, f1
if chosen_by == "f1_fallback":
best_thresh = fallback_t
best_prec, best_rec, best_f1 = fallback_prec, fallback_rec, fallback_f1
return best_thresh, {
"val_precision": float(best_prec),
"val_recall": float(best_rec),
"threshold_source": chosen_by,
}