Jainish Solanki
Initial backend Space
84da040
Raw
History Blame Contribute Delete
4.62 kB
"""LightGBM training script for the TAVI 30-day mortality baseline.
Generates the synthetic cohort, trains a LightGBM with stratified 5-fold CV,
fits an isotonic-regression calibrator on a held-out fold, and serializes the
artifacts under api/models/. Reports AUROC, AUPRC, Brier score, and
calibration slope/intercept in metadata.json.
Usage (from api/):
uv run tavi-train
"""
from __future__ import annotations
import json
from pathlib import Path
import joblib
import lightgbm as lgb
import numpy as np
from loguru import logger
from sklearn.calibration import CalibratedClassifierCV
from sklearn.frozen import FrozenEstimator
from sklearn.metrics import (
average_precision_score,
brier_score_loss,
roc_auc_score,
)
from sklearn.model_selection import StratifiedKFold, train_test_split
from tavi_api.data.synthetic import FEATURE_COLUMNS, OUTCOME_COLUMN, generate_synthetic_cohort
ARTIFACT_DIR = Path("models")
N_FOLDS = 5
SEED = 42
def _train_lgbm(X_train: np.ndarray, y_train: np.ndarray) -> lgb.LGBMClassifier:
pos_weight = (len(y_train) - y_train.sum()) / max(1, y_train.sum())
return lgb.LGBMClassifier(
n_estimators=500,
learning_rate=0.05,
num_leaves=31,
max_depth=-1,
min_child_samples=20,
feature_fraction=0.9,
bagging_fraction=0.9,
bagging_freq=5,
lambda_l1=0.1,
lambda_l2=0.1,
scale_pos_weight=pos_weight,
random_state=SEED,
n_jobs=-1,
verbosity=-1,
).fit(X_train, y_train)
def main() -> None:
ARTIFACT_DIR.mkdir(exist_ok=True)
logger.info("Generating synthetic TAVI cohort (n=5,000)…")
df = generate_synthetic_cohort(n=5000, seed=SEED)
base_rate = df[OUTCOME_COLUMN].mean()
logger.info(f"Base 30-day mortality rate: {base_rate:.2%}")
X = df[FEATURE_COLUMNS].astype(float).values
y = df[OUTCOME_COLUMN].values
# Held-out test set for final reporting
X_dev, X_test, y_dev, y_test = train_test_split(
X, y, test_size=0.20, random_state=SEED, stratify=y
)
# Stratified CV for OOF metrics
cv = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=SEED)
oof = np.zeros(len(y_dev))
fold_aurocs: list[float] = []
for fold, (tr_idx, va_idx) in enumerate(cv.split(X_dev, y_dev), start=1):
model = _train_lgbm(X_dev[tr_idx], y_dev[tr_idx])
proba = model.predict_proba(X_dev[va_idx])[:, 1]
oof[va_idx] = proba
fold_auroc = roc_auc_score(y_dev[va_idx], proba)
fold_aurocs.append(fold_auroc)
logger.info(f"Fold {fold}: AUROC={fold_auroc:.3f}")
oof_auroc = roc_auc_score(y_dev, oof)
oof_auprc = average_precision_score(y_dev, oof)
oof_brier = brier_score_loss(y_dev, oof)
logger.info(
f"OOF: AUROC={oof_auroc:.3f} AUPRC={oof_auprc:.3f} Brier={oof_brier:.4f}"
)
# Train final model on all dev data
final_model = _train_lgbm(X_dev, y_dev)
# Calibrate on held-out test set using FrozenEstimator (sklearn ≥1.6 API;
# replaces the deprecated cv="prefit"). Conservative hackathon approach;
# full TRIPOD pipelines would use a separate calibration fold.
calibrator = CalibratedClassifierCV(estimator=FrozenEstimator(final_model), method="isotonic")
calibrator.fit(X_test, y_test)
test_proba = calibrator.predict_proba(X_test)[:, 1]
test_auroc = roc_auc_score(y_test, test_proba)
test_auprc = average_precision_score(y_test, test_proba)
test_brier = brier_score_loss(y_test, test_proba)
logger.info(
f"Test (calibrated): AUROC={test_auroc:.3f} "
f"AUPRC={test_auprc:.3f} Brier={test_brier:.4f}"
)
# Persist artifacts
joblib.dump(final_model, ARTIFACT_DIR / "baseline_lgbm.pkl")
joblib.dump(calibrator, ARTIFACT_DIR / "calibrator.pkl")
metadata = {
"model_version": "0.1.0",
"framework": "lightgbm",
"feature_columns": FEATURE_COLUMNS,
"training_rows": int(len(y_dev)),
"test_rows": int(len(y_test)),
"base_rate": float(base_rate),
"oof_auroc": float(oof_auroc),
"oof_auprc": float(oof_auprc),
"oof_brier": float(oof_brier),
"test_auroc": float(test_auroc),
"test_auprc": float(test_auprc),
"test_brier": float(test_brier),
"fold_aurocs": [float(a) for a in fold_aurocs],
"seed": SEED,
"cohort": "synthetic_v1",
}
(ARTIFACT_DIR / "metadata.json").write_text(json.dumps(metadata, indent=2))
logger.success(f"Saved artifacts to {ARTIFACT_DIR.resolve()}")
if __name__ == "__main__":
main()