"""LightGBM 10-class classifier with isotonic calibration (D-CAL-01..03). Per OQ-2 resolution / D-CAL-02: pass LGBMClassifier directly to CalibratedClassifierCV — sklearn 1.8 applies OvR internally for method='isotonic' on multi-class. An explicit one-vs-rest wrapper around the base estimator would cause double-OvR (Pitfall 4). """ from __future__ import annotations import numpy as np from lightgbm import LGBMClassifier from sklearn.calibration import CalibratedClassifierCV from sklearn.model_selection import StratifiedKFold # D-PUB-07 fixed defaults at v1, no HPO. D-REPRO-01 determinism trio. LGBM_PARAMS: dict = { "n_estimators": 500, "learning_rate": 0.05, "num_leaves": 63, "max_depth": -1, "class_weight": "balanced", # carries forward Phase 1 D-07 "deterministic": True, # Pitfall 1 — required for 1e-4 reproducibility "force_col_wise": True, # required when deterministic=True (LightGBM docs) "n_jobs": -1, "verbose": -1, # silence LightGBM stdout for clean CI logs } def train_calibrated_classifier( X: np.ndarray, y: np.ndarray, *, classifier_seed: int, cv_seed: int, ) -> CalibratedClassifierCV: """Fit LightGBM + isotonic calibration with stratified 5-fold CV. Single-wrap CalibratedClassifierCV(LGBMClassifier, method='isotonic', cv=5). Returns a fitted CalibratedClassifierCV with len(.calibrated_classifiers_) == 5 (one per fold; ensemble='auto' resolves to True since base is not FrozenEstimator). """ base = LGBMClassifier(random_state=classifier_seed, **LGBM_PARAMS) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=cv_seed) calibrated = CalibratedClassifierCV( estimator=base, method="isotonic", # D-CAL-01 cv=cv, # D-CAL-03 — explicit StratifiedKFold for reproducibility ensemble="auto", # resolves to True; keeps 5 (base + isotonic) pairs n_jobs=-1, ) calibrated.fit(X, y) return calibrated def train_raw_classifier( X: np.ndarray, y: np.ndarray, *, classifier_seed: int ) -> LGBMClassifier: """Fit a non-calibrated LGBMClassifier on the same train data + seed. Needed by the orchestrator (Pattern 6 / 02-02) to produce raw_proba for the dual reliability grid (D-CAL-06). CalibratedClassifierCV does not expose the underlying full-train softmax — fold-internal estimators are on 4/5 of data; we want the matched-seed 5/5 baseline for the raw plot. """ clf = LGBMClassifier(random_state=classifier_seed, **LGBM_PARAMS) clf.fit(X, y) return clf