26.2 Kalibrált valószínűség és predikciós bizonytalanság

26.2.1 Kalibráció és predikciós bizonytalanság a Northstar-modellben

A Globális és lokális magyarázat alfejezetben megnéztük, mit miért mond a modell; most azt kérdezzük, mennyire hihetünk a kimenet konkrét számértékének. A Northstar prediktív projektben a célváltozó target_late: 1, ha delivery_days > promised_days (7 nap), egyébként 0. A modell egy HistGradientBoostingClassifier, és a predict_proba például 0,73-et ad egy új rendelésre. Kérdés: ha sok olyan rendelést nézünk, amelyre a modell 0,73 körüli értéket mond, valóban 73% körüli lesz a késői kézbesítések aránya? Ez a kalibráció (calibration) kérdése.

A kérdés mögött két különböző bizonytalanság van. Az eseménybizonytalanság (aleatoric uncertainty) magából a megfigyelési folyamatból fakad: egy adott rendelés kimenete akkor is véletlen, ha a valódi valószínűség pontosan ismert; a target_late Bernoulli-eloszlású Véletlen változó és gyakori eloszlásmodellek szerint. A modellhiba (epistemic uncertainty) viszont onnan ered, hogy a valódi valószínűséget nem ismerjük pontosan; csak a tanítóadaton becsült modellünk van. Több vagy jobb tanítóadat ezt csökkentheti, míg az eseménybizonytalanság nem szüntethető meg.

A következő feladat előzetes becslésként segít a gondolkodás elindításában: egy modell a teszthalmaz 100 olyan pontjára, ahol a predikált valószínűség 0,70 és 0,80 közé esik, átlagosan 0,74-et ad. Körülbelül hány pozitív esetet várunk, ha a modell kalibrált? Ha nem az elvárt számot kapjuk, milyen irányú torzításról beszélünk? (A megoldást a feladatok után adjuk meg.)

26.2.2 Kalibráció és megbízhatósági görbe

A kalibráció formális definíciója: egy modell jól kalibrált, ha minden predikált valószínűség-értékre a tényleges feltételes valószínűség közelítőleg megegyezik a predikcióval:

ℙ(Y=1∣p̂=p)≈p, \mathbb{P}(Y=1 \mid \hat{p}=p) \approx p,

ahol Y∈{0,1}Y \in \{0,1\} a megfigyelt kimenet, p̂∈[0,1]\hat{p} \in [0,1] a modell által adott valószínűség. Mivel folytonos p̂\hat{p} mellett egy-egy konkrét értékhez alig tartozna megfigyelés, a gyakorlatban bineket képezünk. A megbízhatósági görbe (reliability diagram) minden binre két átlagot ábrázol:

ahol nbn_b a bb-edik binbe eső tesztelemek száma, p̂i\hat{p}_i a predikció, yiy_i a megfigyelt 0/1 kimenet. Tökéletes kalibrációnál a pontok a 45°-os egyenesen fekszenek. Ha egy pont az egyenes alatt van, a modell abban a tartományban felülbecsli a valószínűséget; ha felette, alulbecsli.

A kalibráció egy skalár mutatója a Brier-pontszám (Brier score):

B=1n∑i=1n(p̂i−yi)2, B = \frac{1}{n}\sum_{i=1}^{n} (\hat{p}_i - y_i)^2,

ahol nn a tesztpontok száma, p̂i∈[0,1]\hat{p}_i \in [0,1] a predikált valószínűség, yi∈{0,1}y_i \in \{0,1\} a megfigyelt kimenet. A Brier-pontszám 0 és 1 közötti, minél kisebb, annál jobb. Két hasznos invariáns: a konstans 0,5-ös modell Brier-pontszáma mindig 0,25, mert (0,5−yi)2=0,25(0{,}5 - y_i)^2 = 0{,}25 minden yiy_i-re; ha a tesztmintában a pozitívok aránya qq, akkor a p̂i=q\hat{p}_i=q konstans predikció Brier-pontszáma q(1−q)q(1-q), ami legfeljebb 0,25. A Brier-pontszám egyszerre jutalmazza a jó kalibrációt és a jó diszkriminációt, ezért két különböző hibát is összemoshat; ezért mindig a görbét is meg kell nézni.

26.2.3 Teljes minimális munkapélda: kalibráció időablakokban

A következő kód a lab data/orders.csv és data/customers.csv fájljait használja, és a Fák, random forest és gradiensboosting alfejezetben bemutatott boosting modellt tanítja az időbeli split szerint. A scriptet a lab könyvtárból futtatjuk, miután a környezetet uv sync paranccsal telepítettük. A kód nem futtatott mérési jegyzék; a kimenet szemléltető, a pontos számok a helyi futtatástól függnek.

# s102_calibration.py — futtatandó: cd lab && uv run python s102_calibration.py
from pathlib import Path
import pandas as pd
import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.calibration import calibration_curve
from sklearn.metrics import brier_score_loss

orders = pd.read_csv(Path("data") / "orders.csv")
customers = pd.read_csv(Path("data") / "customers.csv")

orders["order_ts"] = pd.to_datetime(orders["order_ts"], utc=True)
orders["label_available_at"] = pd.to_datetime(orders["label_available_at"], utc=True)
customers["signup_date"] = pd.to_datetime(customers["signup_date"], utc=True)

df = orders.merge(
    customers[["customer_id", "signup_date", "segment", "region", "acquisition_channel"]],
    on="customer_id", how="left"
)
df["account_age_days"] = (
    df["order_ts"] - df["signup_date"]
).dt.days

cutoff = pd.Timestamp("2025-05-01T00:00:00+00:00")
train_mask = (df["order_ts"] < cutoff) & (df["label_available_at"] < cutoff)
test_mask = df["order_ts"] >= cutoff

features = [
    "distance_km", "priority_flag", "basket_minor", "quantity_sum",
    "account_age_days", "segment", "region", "acquisition_channel"
]
X_train = df.loc[train_mask, features]
y_train = df.loc[train_mask, "target_late"]
X_test = df.loc[test_mask, features]
y_test = df.loc[test_mask, "target_late"]

numeric_features = [
    "distance_km", "priority_flag", "basket_minor", "quantity_sum", "account_age_days"
]
categorical_features = ["segment", "region", "acquisition_channel"]
preprocess = ColumnTransformer([
    ("num", "passthrough", numeric_features),
    ("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), categorical_features),
])
model = Pipeline([
    ("preprocess", preprocess),
    ("classifier", HistGradientBoostingClassifier(
        max_iter=200, learning_rate=0.08, early_stopping=False, random_state=42
    )),
])
model.fit(X_train, y_train)
y_prob = model.predict_proba(X_test)[:, 1]

prob_true, prob_pred = calibration_curve(
    y_test, y_prob, n_bins=10, strategy="quantile"
)
brier = brier_score_loss(y_test, y_prob)

print("Brier a teljes teszten:", round(brier, 4))
print("Kalibrációs pontok (predikált, megfigyelt):")
for px, py in zip(prob_pred, prob_true):
    print(f"  {px:.3f}  {py:.3f}")

# Külön időablakok: 2025. május és 2025. június
for start, end, name in [
    ("2025-05-01", "2025-06-01", "majus"),
    ("2025-06-01", "2025-07-01", "junius"),
]:
    mask = (df.loc[test_mask, "order_ts"] >= pd.Timestamp(start, tz="UTC")) & (
        df.loc[test_mask, "order_ts"] < pd.Timestamp(end, tz="UTC")
    )
    y_t = y_test[mask]
    y_p = y_prob[mask]
    if len(y_t) == 0:
        print(f"{name}: nincs tesztmegfigyelés ebben az időablakban")
        continue
    b = brier_score_loss(y_t, y_p)
    print(f"{name}: tesztpont {len(y_t)}, Brier {b:.4f}")

A szemléltető kimenetben a teljes teszt Brier-pontszáma 0,18 körül lehet, ami jobb a 0,25-ös állandó modellnél; a kalibrációs pontok a 45°-os vonal közelében, de nem pontosan rajta helyezkednek el. A két időablak Brier-pontszáma eltérhet, mert a májusi és júniusi forgalom eloszlása nem azonos. A kalibrációs pontpárok egy lehetséges szemléltető alakja:

Szemléltető kalibrációs görbe - Szimulált illusztráció; a tényleges pontok a sklearn calibration_curve kimenetéből jönnek.

A calibration_curve két, azonos hosszúságú tömböt ad vissza: prob_pred a nem üres binek átlagos predikciója, prob_true a bennük mért pozitív arány. n_bins=10 esetén is legfeljebb tíz pont jelenik meg, mert az üres binek kimaradnak. Az értékeléshez érdemes a binenkénti elemszámokat külön is meghatározni. Az ellenőrzés során érdemes együtt vizsgálni a binméretet is: ha egy binbe csak néhány pont esik, a mért arány nagy véletlen ingadozást mutathat. A bin gyakoriságának fbf_b becsült standard hibája közelítőleg fb(1−fb)/nb\sqrt{f_b(1-f_b)/n_b}, tehát nb=30n_b=30 és fb=0,7f_b=0{,}7 mellett körülbelül 0,084.

26.2.4 Bootstrap modellcsoport és modellhiba

A kalibráció az eseménybizonytalanság átlagos egyezését méri; nem mutatja meg, mennyire stabil maga a modell. A bootstrap modellcsoport (bootstrap models) a modellhiba becslésére használ egyszerű újramintavételezést: a tanító indexekből BB darab, visszatevéssel vett mintát képezünk, mindegyiken újratanítjuk a modellt, majd egy rögzített tesztpontra megnézzük a predikciók eloszlását. A szórás a modellhiba közelítő mértéke; ha a tanítóhalmaz kicsi vagy a modell instabil, a szórás nagy.

from sklearn.utils import resample

def bootstrap_preds(model_cls, X_train, y_train, X_point, B=50, seed=42):
    preds = []
    n = len(X_train)
    rng = np.random.default_rng(seed)
    for b in range(B):
        idx = rng.integers(0, n, size=n)  # bootstrap soronként
        if y_train.iloc[idx].nunique() < 2:
            continue  # egyszínű bootstrap-minta nem tanítható kétosztályos modellként
        m = model_cls(random_state=b)
        m.fit(X_train.iloc[idx], y_train.iloc[idx])
        preds.append(m.predict_proba(X_point)[0, 1])
    return np.array(preds)

# Példa egy rögzített tesztpontra:
point = X_test.iloc[[0]]
bp = bootstrap_preds(lambda random_state: Pipeline([
    ("preprocess", ColumnTransformer([
        ("num", "passthrough", numeric_features),
        ("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), categorical_features),
    ])),
    ("classifier", HistGradientBoostingClassifier(random_state=random_state)),
]), X_train, y_train, point, B=50)
print(f"predikció átlag: {bp.mean():.3f}, szórás: {bp.std():.3f}")

A szemléltető szórás 0,01–0,05 között lehet; a konkrét érték a seedtől és a modellparaméterektől függ. Fontos korlát: a Northstar adatban egy ügyfélnek több rendelése is van, ezért a soronkénti bootstrap mintavétel nem kezeli az ügyfélszintű függést. A mintavételi egység itt a rendelés, nem az ügyfél; ha ügyfélszintű bizonytalanságot akarunk, az ügyfél-azonosítók újramintavételezése lenne a helyes út.

26.2.5 Split-conformal körkép: predikciós intervallum

A konform predikció (conformal prediction) közvetlenül az új megfigyelésre ad predikciós intervallumot (prediction interval), nem a modell paraméterére. Az alábbi split-conformal változat két diszjunkt halmazt használ: egy tanítóhalmazt a modell illesztésére és egy kalibrációs halmazt a hibák kalibrálására. A cél egy regressziós kimenet, például delivery_days; a Northstar fő céljában ez nem predictor, de itt egy külön, szintetikus illusztrációban természetes.

A módszer lépései:

  1. Tanítsunk f̂\hat{f} modellt a tanítóhalmazon.
  2. A kalibrációs halmaz minden elemére számítsuk a nem-megfelelőségi pontszámot (nonconformity score), például az abszolút hibát: Ri=|Yi−f̂(Xi)|R_i = |Y_i - \hat{f}(X_i)|.
  3. Válasszunk α\alpha hibaszintet (például 0,1), és vegyük a rendezett RiR_i értékek közül a k=⌈(ncal+1)(1−α)⌉k = \lceil (n_{\text{cal}}+1)(1-\alpha)\rceil-edik legkisebbet; ez a qq kvantilis.
  4. Új Xn+1X_{n+1} pontra az intervallum: C(Xn+1)=[f̂(Xn+1)−q,f̂(Xn+1)+q]C(X_{n+1}) = [\hat{f}(X_{n+1}) - q,\; \hat{f}(X_{n+1}) + q].

A felhasznált kvantilis itt nem a hibaeloszlás elméleti paramétere, hanem a kalibrációs halmaz empirikus rendezett statisztikája. A garantált lefedettség marginális:

ℙ(Yn+1∈C(Xn+1))≥1−α, \mathbb{P}(Y_{n+1} \in C(X_{n+1})) \ge 1-\alpha,

feltéve, hogy a kalibrációs és az új pontok kicserélhetők (exchangeable). A kicserélhetőség azt jelenti, hogy a pontok együttes eloszlása nem változik, ha sorrendjüket tetszőlegesen átrendezzük. Független, azonos eloszlású (i.i.d.) pontok kicserélhetők; időbeli adatnál azonban a múltbeli kalibrációs pontok és a jövőbeli tesztpontok általában nem, mert a folyamat eltolódhat. A Northstar időbeli splitje ezért nem teljesíti automatikusan a konform garanciát; ez a módszer inkább akkor használható, ha a kalibrációs és az új pontok ugyanabból a stacionárius folyamatból származnak.

A következő szintetikus kód i.i.d. adaton mutatja a split-conformal eljárást:

rng = np.random.default_rng(42)
X = rng.uniform(0, 1, size=(400, 1))
y = 3.0 * X[:, 0] + rng.normal(0, 0.5, size=400)

X_fit, X_cal, X_test = X[:150], X[150:300], X[300:]
y_fit, y_cal, y_test = y[:150], y[150:300], y[300:]

# A modellillesztés és a konform kalibráció adatai diszjunktak.
fit = LinearRegression().fit(X_fit, y_fit)
cal_scores = np.abs(y_cal - fit.predict(X_cal))
alpha = 0.1
n_cal = len(cal_scores)
q = np.sort(cal_scores)[int(np.ceil((n_cal + 1) * (1 - alpha))) - 1]

test_scores = np.abs(y_test - fit.predict(X_test))
coverage = np.mean(test_scores <= q)
print(f"q = {q:.3f}, empirikus lefedettség = {coverage:.3f}")

Itt ncal=150n_{\text{cal}}=150, α=0,1\alpha=0{,}1, tehát k=⌈151⋅0,9⌉=136k=\lceil 151 \cdot 0{,}9\rceil = 136; a kvantilis a 136. legkisebb kalibrációs abszolút hiba. A tesztmintán mért lefedettség véletlenszerűen ingadozhat; a legalább 90%-os marginális garancia az új, kicserélhető megfigyelésre vonatkozó ismételt mintavételi állítás, nem egy adott tesztmintára vagy alcsoportra előírt alsó korlát. Az intervallum jelentése: az új pontok 90%-át várhatóan lefedi, de ez nem jelenti, hogy egy adott csoportban is 90%.

26.2.6 Feladatok

  1. Értelmezés. Egy kalibrációs binben 80 tesztpont van, a predikciók átlaga 0,55, a megfigyelt pozitívok aránya 0,42. Kalibrált-e a modell ebben a tartományban? Milyen irányú torzítást mutat?

  2. Megvalósítás. Adott y_true = np.array([0, 1, 0, 1, 1]) és y_prob = np.array([0.2, 0.6, 0.1, 0.8, 0.7]). Számítsa ki a Brier-pontszámot kézzel (NumPy nélkül is lehet), és ellenőrizze a sklearn.metrics.brier_score_loss kimenetével. Mi a minimális és maximális lehetséges érték erre az öt pontra?

  3. Hibakeresés. Egy tanuló a következő sort használja: y_prob = model.predict(X_test), majd ezt adja a calibration_curve-nak. A függvény fut, de a görbe két szélső pontba koncentrálódik. Mi a hiba, és hogyan javítandó?

  4. Transzfer megváltozott feltételre. A modell a teljes júniusi tesztablakon jól kalibráltnak tűnik, de a segment == "business" szegmensben a megfigyelt pozitív arány minden binben alacsonyabb a predikált átlagnál. Milyen további vizsgálatot végezne, és milyen műveletet fontolna meg? Indokolja a csoportos kalibráció szükségességét.

  5. Konform ellenőrzés. A split-conformal példában ncal=199n_{\text{cal}}=199 és α=0,2\alpha=0{,}2. Hányadik legkisebb kalibrációs pontszámot kell kvantilisként használni? Ha a rendezett pontszámok 160. eleme 0,47 és 161. eleme 0,51, akkor a kvantilis 0,51; miért nem a kerekítés másik iránya?

26.2.7 Megoldások és indoklás

  1. A modell felülbecsli a valószínűséget ebben a tartományban: a predikciók átlaga 0,55, a tényleges gyakoriság 0,42. A pont a 45°-os vonal alatt helyezkedne el. A standard hiba 0,42⋅0,58/80≈0,055\sqrt{0{,}42 \cdot 0{,}58 / 80} \approx 0{,}055, tehát a 0,13-as eltérés valószínűleg nem pusztán binméret-ingadozás.

  2. A Brier-pontszám: B=15[(0,2−0)2+(0,6−1)2+(0,1−0)2+(0,8−1)2+(0,7−1)2]=0,04+0,16+0,01+0,04+0,095=0,068. B = \frac{1}{5}[(0{,}2{-}0)^2 + (0{,}6{-}1)^2 + (0{,}1{-}0)^2 + (0{,}8{-}1)^2 + (0{,}7{-}1)^2] = \frac{0{,}04+0{,}16+0{,}01+0{,}04+0{,}09}{5} = 0{,}068. A brier_score_loss(y_true, y_prob) ugyanezt adja. Az öt ponton a minimális 0, ha minden predikció pontosan egyezik a kimenettel; a maximális 1, ha minden predikció a rossz osztályt kapja 1 valószínűséggel.

  3. A predict osztálycímkét ad vissza (0 vagy 1), a kalibrációhoz a pozitív osztály valószínűsége kell. A helyes sor:

    y_prob = model.predict_proba(X_test)[:, 1]

    Ekkor a calibration_curve bemenete a [0,1][0,1] intervallumba eső valószínűség, és a binekben értelmes átlagok képződnek.

  4. A csoportszintű eltérés szisztematikus torzítás jele: a business szegmensben a modell folyamatosan túl magas valószínűséget ad. Ilyenkor külön kalibrációs görbét kell számítani a szegmensre; ha az eltérés stabil, egy kalibrációs réteg (például isotonic vagy Platt) illeszthető, de csak a tanítóidőszaki adaton, és a csoportazonosítót dokumentálni kell. Ugyanakkor vigyázni kell: a szegmensen belüli mintaméret kicsi lehet, és a kalibrációs réteg maga is túlilleszthet.

  5. A képlet szerint k=⌈(199+1)(1−0,2)⌉=⌈200⋅0,8⌉=160k = \lceil (199+1)(1-0{,}2)\rceil = \lceil 200 \cdot 0{,}8 \rceil = 160. A 160. legkisebb pontszám a kvantilis; mivel a 160. elem 0,47 és a 161. elem 0,51, a keresett érték 0,47. (A feladat szándékosan kérdezett rá a kerekítés irányára: a felső egészrész miatt a 160. elemet vesszük, nem a 161.-et; ha a 161.-et vennénk, az α\alpha nagyobb lenne a tervezettnél.)

26.2.8 Gyakori tévedés ellenpéldával

A leggyakoribb félreértés: az érvényes összesített lefedettség nem garantál minden csoportra azonos lefedettséget. Tegyük fel, hogy a split-conformal eljárás kalibrációs pontszámai két részsokaságból származnak. Az A csoportban a hibák jellemzően 0,8 körüliek, a B csoportban 0,3 körüliek. A globális kvantilis 0,75 lehet, mert a pontok fele A-ból, fele B-ből jön. Ekkor az A csoportban az új pontok többsége kívül esik az intervallumon, mondjuk a lefedettség 40%, míg a B csoportban 95%. Ha a két csoport azonos súlyú, e példában az összesített lefedettség mindössze (0,40+0,95)/2=0,675(0{,}40+0{,}95)/2=0{,}675. Ahhoz, hogy az összesített lefedettség 90% legyen, a 95%-os lefedettségű B csoport súlyának legalább 10/11≈90,9%10/11\approx90{,}9\%-nak kell lennie; ilyenkor lehetséges a rossz A csoportos lefedettség mellett is magas globális lefedettség, de az A csoportra a gyakorlati predikció használhatatlan. Ezért a kalibrációs és konform ellenőrzéseket mindig szeletelt (szegmens, időablak, régió szerinti) bontásban is el kell végezni; a Hibaszeletek és felelős értékelés alfejezetben ezt a hibaszelet-elemzést folytatjuk.

Visszautalva a korábbi fogalmakra: a Véletlen változó és gyakori eloszlásmodellek alfejezetben bevezetett Bernoulli-eloszlás adja az eseménybizonytalanság matematikai modelljét; a Fák, random forest és gradiensboosting alfejezetben tanított boosting modell a predikció forrása; a Kategória, hiány és ferde változó alfejezetben használt pipeline biztosítja, hogy a kalibrációs réteg tanulása ne szivárogtasson tesztinformációt.

Önellenőrzés. Fogalmazza meg saját szavaival: miért nem elég egyetlen Brier-pontszám a modell megbízhatóságának megítélésére? Melyik feltétel sérül, ha a split-conformal kalibrációs halmaz a múltból, a tesztpont a jövőből származik? Milyen lépést tenne, ha a kalibrációs görbe a magas valószínűségű tartományban következetesen a 45°-os vonal alatt fut?

26.2.9 Rövid összefoglaló

A kalibráció azt ellenőrzi, hogy a modell által adott valószínűség átlagosan egyezik-e a megfigyelt gyakorisággal; a megbízhatósági görbe ezt binekre bontva mutatja. Az eseménybizonytalanság a véletlen kimenetből, a modellhiba a becsült paraméterek korlátosságából ered; utóbbira a bootstrap modellcsoport szórása ad közelítő mértéket. A split-conformal predikció kicserélhető pontok esetén marginális fedési garanciájú intervallumot képez, de időbeli vagy csoportszerkezet mellett a garancia sérülhet. A laborban a kalibrációt két időablakban számítottuk; a következő alfejezet a hibaszeletek és a felelős modellkártya felé viszi tovább ezt a gondolatot.