PrjPerso_credexp / scripts /train_final.py
Benoît Girard
Deploy from GitHub Actions
a58ec05 verified
Raw
History Blame Contribute Delete
7.79 kB
from __future__ import annotations
import argparse
import json
from pathlib import Path
import joblib
import mlflow
import pandas as pd
from sklearn.metrics import average_precision_score, confusion_matrix, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from credexp.config import ARTIFACTS_DIR, DATA_DIR, settings
from credexp.modeling.pipelines import make_numeric_steps
from credexp.modeling.threshold import business_cost, find_best_threshold
from credexp.utils.logging import get_logger
log = get_logger(__name__)
try:
import lightgbm as lgb
except Exception as e:
raise RuntimeError("LightGBM must be installed to run train_final.py") from e
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--features", type=str, default=str(DATA_DIR / "processed" / "features.parquet")
)
parser.add_argument("--holdout-size", type=float, default=0.10)
parser.add_argument("--val-size", type=float, default=0.20)
parser.add_argument("--cost-fn", type=float, default=10.0)
parser.add_argument("--cost-fp", type=float, default=1.0)
parser.add_argument(
"--undersample",
action="store_true",
help="(optional) not used in this final script for simplicity",
)
parser.add_argument("--run-name", type=str, default="lgbm_final")
args = parser.parse_args()
features_path = Path(args.features)
df = pd.read_parquet(features_path)
df_train = df[df["TARGET"].notna()].copy()
y = df_train["TARGET"].astype(int)
X = df_train.drop(columns=["TARGET"])
# 1) Holdout split (never seen by the model)
X_dev, X_holdout, y_dev, y_holdout = train_test_split(
X,
y,
test_size=args.holdout_size,
random_state=settings.random_state,
stratify=y,
)
holdout_path = DATA_DIR / "processed" / "api_holdout.parquet"
holdout = X_holdout.copy()
holdout["TARGET"] = y_holdout.values
holdout.to_parquet(holdout_path, index=False)
log.info(f"Saved API holdout to {holdout_path} shape={holdout.shape}")
# 2) Dev split to pick threshold (train/val)
X_tr, X_va, y_tr, y_va = train_test_split(
X_dev,
y_dev,
test_size=args.val_size,
random_state=settings.random_state,
stratify=y_dev,
)
# Baseline LGBM params (you can tune later via Optuna)
# class_weight = "balanced"
# model = lgb.LGBMClassifier(
# n_estimators=2000,
# learning_rate=0.03,
# num_leaves=64,
# subsample=0.8,
# colsample_bytree=0.8,
# reg_lambda=1.0,
# objective="binary",
# class_weight=class_weight,
# random_state=settings.random_state,
# n_jobs=-1,
# )
# Tuned LGBM params (from Optuna best trial)
class_weight = "balanced"
model = lgb.LGBMClassifier(
n_estimators=781,
learning_rate=0.022855,
num_leaves=59,
max_depth=6,
min_child_samples=117,
subsample=0.609379,
colsample_bytree=0.900507,
reg_alpha=1.233276,
reg_lambda=4.120487,
objective="binary",
class_weight=class_weight,
random_state=settings.random_state,
n_jobs=-1,
verbosity=-1,
)
pipe = Pipeline(
[
*make_numeric_steps(scale=False),
("model", model),
]
)
# MLflow setup
mlflow.set_tracking_uri(settings.mlflow_tracking_uri)
mlflow.set_registry_uri(settings.mlflow_registry_uri)
mlflow.set_experiment(settings.mlflow_experiment_name)
with mlflow.start_run(run_name=args.run_name):
mlflow.log_param("model_family", "lightgbm")
mlflow.log_param("holdout_size", args.holdout_size)
mlflow.log_param("val_size", args.val_size)
mlflow.log_param("cost_fn", args.cost_fn)
mlflow.log_param("cost_fp", args.cost_fp)
mlflow.log_param("class_weight", class_weight)
mlflow.log_param("features_file", str(features_path))
# Log tuned hyperparams
mlflow.log_params(
{
"lgbm_n_estimators": 781,
"lgbm_learning_rate": 0.022855,
"lgbm_num_leaves": 59,
"lgbm_max_depth": 6,
"lgbm_min_child_samples": 117,
"lgbm_subsample": 0.609379,
"lgbm_colsample_bytree": 0.900507,
"lgbm_reg_alpha": 1.233276,
"lgbm_reg_lambda": 4.120487,
}
)
# 3) Fit on train split, choose threshold on val split
pipe.fit(X_tr, y_tr)
proba_va = pipe.predict_proba(X_va)[:, 1]
best_thr, best_cost = find_best_threshold(
y_true=y_va.to_numpy(),
y_proba=proba_va,
cost_fn=args.cost_fn,
cost_fp=args.cost_fp,
)
# Val metrics
val_auc = roc_auc_score(y_va, proba_va)
val_prauc = average_precision_score(y_va, proba_va)
mlflow.log_metric("val_roc_auc", float(val_auc))
mlflow.log_metric("val_pr_auc", float(val_prauc))
mlflow.log_metric("val_best_threshold", float(best_thr))
mlflow.log_metric("val_business_cost", float(best_cost))
log.info(
f"Val: AUC={val_auc:.4f} PR-AUC={val_prauc:.4f} best_thr={best_thr:.3f} cost={best_cost:.1f}"
)
# 4) Refit final model on ALL dev data (train+val)
pipe.fit(X_dev, y_dev)
# 5) Evaluate on holdout (never seen)
proba_hold = pipe.predict_proba(X_holdout)[:, 1]
hold_auc = roc_auc_score(y_holdout, proba_hold)
hold_prauc = average_precision_score(y_holdout, proba_hold)
hold_cost = business_cost(
y_holdout.to_numpy(), proba_hold, best_thr, args.cost_fn, args.cost_fp
)
y_pred_hold = (proba_hold >= best_thr).astype(int)
cm = confusion_matrix(y_holdout, y_pred_hold).tolist()
mlflow.log_metric("holdout_roc_auc", float(hold_auc))
mlflow.log_metric("holdout_pr_auc", float(hold_prauc))
mlflow.log_metric("holdout_business_cost", float(hold_cost))
# Log confusion matrix elements (handy for dashboards)
tn, fp = cm[0]
fn, tp = cm[1]
mlflow.log_metric("holdout_tn", tn)
mlflow.log_metric("holdout_fp", fp)
mlflow.log_metric("holdout_fn", fn)
mlflow.log_metric("holdout_tp", tp)
log.info(
f"Holdout: AUC={hold_auc:.4f} PR-AUC={hold_prauc:.4f} cost={hold_cost:.1f} cm={cm}"
)
# 6) Save local artifacts (joblib + threshold json)
out_dir = ARTIFACTS_DIR / "models"
out_dir.mkdir(parents=True, exist_ok=True)
model_path = out_dir / "pipeline.joblib"
joblib.dump(pipe, model_path)
threshold_path = out_dir / "threshold.json"
threshold_payload = {
"threshold": float(best_thr),
"cost_fn": float(args.cost_fn),
"cost_fp": float(args.cost_fp),
"selection": "val_optimized_then_refit_on_dev",
}
threshold_path.write_text(json.dumps(threshold_payload, indent=2), encoding="utf-8")
# 7) Log artifacts to MLflow
mlflow.log_artifact(str(model_path), artifact_path="export")
mlflow.log_artifact(str(threshold_path), artifact_path="export")
mlflow.log_artifact(str(holdout_path), artifact_path="export")
# Also log as MLflow model (appears under run -> Artifacts/model)
mlflow.sklearn.log_model(
pipe,
artifact_path="model",
registered_model_name="credit_scoring_model",
)
log.info("Final model exported and logged to MLflow.")
if __name__ == "__main__":
main()