A Northstar prediktív projektben a Hiperparaméter és nested értékelés egység nested keresztvalidációval kiválasztotta a logisztikus regressziót, és befagyasztotta a split manifestet. A modell minden rendeléshez egy késési valószínűséget ad, de ez még nem döntés: a működéshez küszöb kell. A kérdés nem az, hogy „mekkora az AUC”, hanem hogy milyen küszöbre mondjuk azt, hogy a rendelés várhatóan késik, és mennyibe kerül ez a döntés.
A feladat bemenete:
data/orders.csv és
data/customers.csv,artifacts/split.json train/test/purged
sorazonosítói,distance_km,
priority_flag, basket_minor,
quantity_sum, account_age_days,
segment, region,
acquisition_channel,target_late (0/1),Megfigyelési egység egy rendelés; a döntés a rendelés pillanatában születik, a címke csak később válik ismertté. A labor kimenete egy befagyasztott küszöb, a validhalmaz kalibrációs és költséggörbéje, végül az érintetlen teszt egyszeri költségértékelése. Induló becslésként ellenőrizze a tanítóhalmaz késési arányát: ha például 9%, akkor a mindig negatív baseline költség egység/rendelés; a modellnek ennél kell jobbnak lennie.
Küszöb (threshold). A modell folytonos kimenetét a szabállyal alakítjuk bináris döntéssé. A megválasztása trade-off: alacsonyabb küszöb több riasztást és több hamis riasztást, magasabb küszöb több kihagyott eseményt ad. A confusion matrix minden -re más:
| Döntés | Tényleges 0 | Tényleges 1 |
|---|---|---|
| TN | FN | |
| FP | TP |
Várható költség (expected cost). A költségtábla csak a hibás döntéseket árazza: a hamis riasztás , a kihagyott esemény , a helyes döntés 0. A validhalmazon megfigyeléssel a várható költség:
Itt az indikátorfüggvény, értéke 1, ha a feltétel igaz, különben 0; a prediktált valószínűség, a tényleges címke. Az mértékegysége költségegység/rendelés. A döntési probléma célja minimalizálása a validhalmazon.
Ha a modell valószínűségei jól kalibráltak és a költségek nem függnek -től, az elméleti optimum levezethető feltételes várható értékkel: egy pontszámú rendelés riasztásának várható költsége -szerű, és a határ ott van, ahol a riasztás várható többletköltsége egyenlő a nem riasztáséval. A gyakorlati képlet egyszerű:
A jelen költségtáblával . Ez nem azt jelenti, hogy pontosan ezt a küszöböt kell használni; a valid görbe empirikus minimuma a véges minta és a kalibrálatlanság miatt eltérhet.
Kalibrációs görbe (calibration curve). A kalibráció azt kérdezi: amikor a modell valószínűséget mond, a tényleges pozitív arány valóban körül van-e? A validhalmaz predikcióit binekre osztjuk, minden binben kiszámoljuk az átlagos predikciót és a tényleges pozitív arányt . A kalibrációs görbe ezeket ábrázolja; az ideális görbe az átló. A logisztikus regresszió gyakran jól kalibrált, a random forest és a boosting viszont túl magabiztos lehet – erre a későbbi Kalibrált valószínűség és predikciós bizonytalanság épít.
Brier-pontszám (Brier score). Az egyetlen számba sűrített kalibrációs és élességi mérték a predikciók és a címkék négyzetes eltérése:
Értéke mindig 0 és 1 között van; a tökéletes, mindig magabiztos előrejelzés. A mindig átlag-arányt mondó nullmodell Brier-értéke , tehát ritka eseménynél már egy egyszerű konstans is alacsony Brier-t ad; a modellt ehhez, nem 0-hoz kell viszonyítani.
Imbalance és a resampling helye. A késés ritka
esemény: ha a pozitív arány
,
a mindig negatív modell accuracy-je már
.
Az accuracy ezért nem választ küszöböt. Ha a ritkaságot SMOTE-tal vagy
más resamplinggel kezeljük, a resampling csak a keresztvalidáció
tanító foldján belül futhat, különben a szintetikus pontok a
validációs fold információját szivárogtatják. A
imblearn.pipeline.Pipeline ezt a korlátot beépítve
kezeli.
A következő szkript a lab gyökeréből futtatható:
python notebooks/s092_threshold_calibration.pyA kód előfeltétele, hogy a starter_experiment.py már
létrehozta az artifacts/split.json-t. A split kulcsait a
projekt manifestjében ellenőrizze; a példa a train,
test, purged neveket használja.
# notebooks/s092_threshold_calibration.py
from pathlib import Path
import json
import joblib
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
brier_score_loss, confusion_matrix, roc_auc_score
)
from sklearn.calibration import calibration_curve
DATA = Path("data")
ART = Path("artifacts")
SPLIT_PATH = ART / "split.json"
C_FP = 1.0 # hamis riasztás: fölösleges figyelmeztetés
C_FN = 5.0 # kihagyott késés: ügyfélkompenzáció
SEED = 20261020
orders = pd.read_csv(DATA / "orders.csv", parse_dates=["order_ts", "actual_delivery_at", "label_available_at"])
customers = pd.read_csv(DATA / "customers.csv", parse_dates=["signup_date"])
df = orders.merge(
customers[["customer_id", "signup_date", "segment", "region", "acquisition_channel"]],
on="customer_id", how="inner"
)
df["account_age_days"] = (df["order_ts"] - df["signup_date"]).dt.days
with open(SPLIT_PATH) as f:
split = json.load(f)
train_ids = split["train"]
test_ids = split["test"]
purged_ids = split["purged"] # a kihagyott határsáv, itt nem használjuk
feature_cols = ["distance_km", "priority_flag", "basket_minor", "quantity_sum",
"account_age_days", "segment", "region", "acquisition_channel"]
target_col = "target_late"
X_train_full = df[df["order_id"].isin(train_ids)][feature_cols].copy()
y_train_full = df[df["order_id"].isin(train_ids)][target_col].copy()
X_test = df[df["order_id"].isin(test_ids)][feature_cols].copy()
y_test = df[df["order_id"].isin(test_ids)][target_col].copy()
# Belső split: inner_train a modellhez, threshold_valid a küszöbválasztáshoz.
X_inner, X_valid, y_inner, y_valid = train_test_split(
X_train_full, y_train_full, test_size=0.30,
random_state=SEED, stratify=y_train_full
)
numeric_cols = ["distance_km", "priority_flag", "basket_minor", "quantity_sum", "account_age_days"]
categorical_cols = ["segment", "region", "acquisition_channel"]
preprocessor = ColumnTransformer([
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols)
])
model = Pipeline([
("prep", preprocessor),
("clf", LogisticRegression(max_iter=1000, random_state=SEED))
])
model.fit(X_inner, y_inner)
p_valid = model.predict_proba(X_valid)[:, 1]
brier_valid = brier_score_loss(y_valid, p_valid)
auc_valid = roc_auc_score(y_valid, p_valid)
thresholds = np.linspace(0.01, 0.99, 199)
costs = []
for t in thresholds:
y_pred = (p_valid >= t).astype(int)
tn, fp, fn, tp = confusion_matrix(y_valid, y_pred).ravel()
costs.append((fp * C_FP + fn * C_FN) / len(y_valid))
costs = np.array(costs)
best_t = thresholds[int(np.argmin(costs))]
best_valid_cost = costs.min()
frac_pos, mean_pred = calibration_curve(y_valid, p_valid, n_bins=10)
# Befagyasztott küszöb: az érintetlen teszt egyszeri értékelése.
model.fit(X_train_full, y_train_full) # újratanítás a teljes trainen
p_test = model.predict_proba(X_test)[:, 1]
y_pred_test = (p_test >= best_t).astype(int)
tn_t, fp_t, fn_t, tp_t = confusion_matrix(y_test, y_pred_test).ravel()
test_cost = (fp_t * C_FP + fn_t * C_FN) / len(y_test)
brier_test = brier_score_loss(y_test, p_test)
auc_test = roc_auc_score(y_test, p_test)
result = {
"inner_n": int(len(X_inner)),
"valid_n": int(len(X_valid)),
"test_n": int(len(X_test)),
"train_late_rate": float(y_train_full.mean()),
"brier_valid": float(brier_valid),
"auc_valid": float(auc_valid),
"theoretical_threshold": C_FP / (C_FP + C_FN),
"chosen_threshold": float(best_t),
"valid_cost_at_chosen": float(best_valid_cost),
"test_cost_at_chosen": float(test_cost),
"test_confusion": {"tn": int(tn_t), "fp": int(fp_t),
"fn": int(fn_t), "tp": int(tp_t)},
"brier_test": float(brier_test),
"auc_test": float(auc_test),
}
ART.mkdir(exist_ok=True)
joblib.dump((model, best_t), ART / "s092_threshold_model.pkl")
with open(ART / "s092_threshold_result.json", "w") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
print(json.dumps(result, indent=2, ensure_ascii=False))A tényleges futás a helyi lab projektben ellenőrzi a számokat. Az alábbiak a várt tartományt szemléltetik, nem a mért eredményt: a train késési arány körülbelül 8–10%, a valid optimum közelében, körülbelül 0,15–0,20 között, a tesztköltség pedig a baseline – alatt várható. A pontos értékeket a manifest és a saját futtatás igazolja.
A kód három dolgot választ el. A threshold_valid halmaz
a küszöbgörbe és a kalibráció tere; a választott
a valid költséggörbe minimuma. Ezután a modellt újratanítjuk a teljes
trainen, és a befagyasztott küszöbbel a tesztet pontosan egyszer
értékeljük. A teszt confusion matrix és költség már nem vezérel további
modell- vagy küszöbválasztást.
A valid költséggörbe aszimmetrikus: a miatt a túl magas küszöb drága, mert kihagy késéseket. A elméleti optimum csak akkor pontos, ha a kalibrációs görbe az átlón van. Ha a valid halmazon a modell alacsony tartományban alulbecsül, az optimum jobbra tolódik. A Brier-pontszám együtt olvasandó a kalibrációs görbével: az alacsony Brier nem garancia a jó kalibrációra, ha az esemény ritka.
A tesztköltség becslése maga is mintavételi becslés: a kis tesztmintán a költség standard hibája a költségarányok binomiális szórásából közelíthető, de egyszeri futtatásban csak a pontbecslés jelenik meg. A nested CV Hiperparaméter és nested értékelés becsléséhez hasonlóan ez a teszt is egyetlen véletlen szelet; a szórását nem a teszt többszöri újraoptimalizálásával, hanem resamplinggel vagy a jövőbeli monitoringgal lehet becsülni.
A kalibrálót kizárólag a modell kiválasztásától és a végső teszttől elkülönített adaton illesszük, vagy használjunk szivárgásmentes CV-t. Ritka eseménynél a PR-görbe baseline-ja az adott halmaz pozitív aránya.
Értelmezés. Az alábbi valid confusion matrix adott: TP=40, FP=20, FN=10, TN=930. Számítsa ki a várható költséget , mellett, a pontosságot (accuracy), valamint a mindig negatív baseline költségét. Miért vezet félre az accuracy a küszöbválasztásban?
Megvalósítás. Változtassa a költségtáblát , értékekre, és futtassa újra a küszöbgörbét. Mi az elméleti , és miért nem változik? Mi változik viszont a költség mértékegységében és a tesztköltség értékében?
Hibadiagnózis. A valid költség 0,36, a teszt költség viszont 0,71. Soroljon fel legalább három lehetséges okot, és nevezze meg, melyik okot hogyan zárná ki a split manifest, a label-availability szűrés és a feaure-ök időbeli eloszlásának összehasonlításával.
Megváltozott feltétel. Tegyük fel, hogy a train
halmaz késési aránya 6%, és egy kolléga a teljes trainen SMOTE-ot
futtat, mielőtt a keresztvalidációt lefuttatja. Magyarázza el, miért
szivárogtat ez információt a valid foldokba, és írja le a helyes
imblearn.pipeline.Pipeline szerkezetet, amelyben a
resampling a tanító foldon belül marad.
1. feladat. A confusion matrix cellái: , , . A várható költség:
Az accuracy . A mindig negatív modell esetén , , tehát , accuracy . Az accuracy alig különbözik, miközben a költség több mint háromszoros; ritka eseménynél az accuracy a konstans negatív szabályt is magasra jutalmazza, ezért nem alkalmas működési küszöb választására.
2. feladat. Az elméleti küszöb , ugyanaz, mert a két költség aránya nem változott: mindkét tábla az 1:5 arányt használja. A valid görbe minimuma a véletlen miatt kissé elmozdulhat, de a döntési határ elvileg változatlan. A teljes költség viszont kétszeresére skálázódik, mert minden hibás döntés ára duplázódik; a tesztköltség várhatóan körülbelül , ahol az eredeti költségtáblával mért tesztköltség.
3. feladat. Lehetséges okok:
threshold_valid halmaz túl kicsi, ezért a valid
költség alacsony varianciájú becslés, de a teszt időszak más eloszlást
mutat; a valid és teszt késési arányát a split manifest alapján
összevethető.distance_km,
basket_minor eloszlásának valid/teszt összehasonlítása ezt
jelzi.split.json purged listája és a
label_available_at szerinti szűrés ellenőrzése ezt zárja
ki.4. feladat. A SMOTE a kisebbségi osztály pontjainak szomszédjai alapján szintetikus mintát generál. Ha ezt a teljes trainen, a CV előtt futtatjuk, a valid fold valódi pontjainak szomszédságát is felhasználja, ezért a valid fold nem marad érintetlen; az értékelés optimista lesz. A helyes megoldás:
from imblearn.pipeline import Pipeline as ImbPipeline
from imblearn.over_sampling import SMOTE
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
pipe = ImbPipeline([
("smote", SMOTE(random_state=20261020)),
("lr", LogisticRegression(max_iter=1000, random_state=20261020))
])
scores = cross_val_score(pipe, X_train_full, y_train_full,
scoring="neg_log_loss", cv=5)Itt a cross_val_score minden foldon belül előbb a
tanítórészen futtatja a SMOTE-ot, a validációs fold pedig a nyers,
eredetileg kiegyensúlyozatlan címkékkel értékel. A Pipeline
tehát nem a fold előtt, hanem a fold tanító ágában hajtja végre a
resamplinget.
A leggyakoribb hiba a teszten optimalizált küszöb. Tegyük fel, hogy a valid optimum , a teszt költséggörbéjének minimuma viszont -nél van, és ott a tesztköltség 0,39 a valid-küszöbös 0,43-mal szemben. Ha a jelentésbe a 0,39-et és a 0,24-et írjuk, az első ránézésre jobb, de a döntési szabály most már a teszt címkéit ismerve alakult ki. A teszt zajához idomuló küszöb a jövőbeli mintán általában rosszabb, mint a validon befagyasztott; a 0,24-es választás tehát optimista önértékelés. A helyes sorrend: validon választunk küszöböt, a tesztet utána egyszer, változatlan szabállyal értékeljük. Ugyanez a logika ismétlődött a Hiperparaméter és nested értékelés belső/ külső CV szétválasztásánál: minden adaptív döntést a teszt előtti rétegben kell lezárni.
Önellenőrzés: a teszt confusion matrix oszlopösszegei egyeznek-e a
teszt tényleges osztálygyakoriságával; a költség nem negatív; a
Brier-érték 0 és 1 között van; a split.json
train/test/purged halmazai diszjunktak. Ha bármelyik sérül, a küszöb
vagy az értékelés nem megbízható.
A modell valószínűségi kimenetéből a döntési küszöböt a döntési költségtábla alakítja működési szabállyá. A validhalmazon kiszámított várható költség görbéje választja a küszöböt; az elméleti érték csak jól kalibrált modellnél pontos. A kalibrációs görbe és a Brier-pontszám azt ellenőrzi, hogy a prediktált valószínűségek tényleges gyakoriságként értelmezhetők-e. A küszöböt a teszt előtt befagyasztjuk; a teszten optimalizált küszöb már nem független értékelés. Ritka eseménynél az accuracy helyett a költség és a kalibráció vezet, a resampling pedig csak a tanító foldon belül élhet. A következő egység a PCA: skála, vetítés és megőrzött variancia PCA-val és nem felügyelt módszerekkel bővíti a predikciós eszköztárat.