| """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 |
|
|
| |
| LGBM_PARAMS: dict = { |
| "n_estimators": 500, |
| "learning_rate": 0.05, |
| "num_leaves": 63, |
| "max_depth": -1, |
| "class_weight": "balanced", |
| "deterministic": True, |
| "force_col_wise": True, |
| "n_jobs": -1, |
| "verbose": -1, |
| } |
|
|
|
|
| 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", |
| cv=cv, |
| ensemble="auto", |
| 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 |
|
|