26.4 Robusztusság és korlátokkal írt modellkártya

26.4.1 Cél és konkrét helyzet

A Hibaszeletek és felelős értékelés alfejezetben a modell hibáit szeletekre bontva vizsgáltuk. Most más irányból kérdezünk: meddig bízhatunk a modellben, ha a jövőbeli adat vagy a mérési környezet eltolódik? A Northstar rendeléskori késésmodellt használjuk; a megfigyelési egység az order_id, a bemenet egy rendelés pillanatában ismert feature-sor, a cél a target_late indikátor (1, ha delivery_days > 7). A már rögzített teszthalmazon két perturbációt szimulálunk: a south régió eltűnését és a distance_km +15%-os mérési eltolódását. Az eredmény egy stresszteszt-jegyzőkönyv és egy kitöltött modellkártya. Előzetes becslés: a kategóriaeltűnés kisebb, a distance-eltolódás nagyobb log loss változást okoz, mert a távolság erősebb prediktor; a tényleges értéket a tanuló gépe adja.

26.4.2 Fogalmi magyarázat

Robusztusság (robustness). A modell robusztussága azt méri, mennyire marad stabil a teljesítménye, ha a bemenet eloszlása, mérési skálája vagy hiányzási mintázata megváltozik. Rögzített teszthalmazon a referencia log loss:

Lref=−1n∑i=1n[yilnp̂i+(1−yi)ln(1−p̂i)],L_{\text{ref}} = -\frac{1}{n}\sum_{i=1}^n \left[y_i \ln \hat p_i + (1-y_i)\ln(1-\hat p_i)\right],

ahol nn a teszt sorainak száma, yi∈{0,1}y_i \in \{0,1\} a tényleges címke, p̂i∈(0,1)\hat p_i \in (0,1) a prediktált késési valószínűség. A perturbált adatra számított LpertL_{\text{pert}} különbsége ΔL=Lpert−Lref\Delta L = L_{\text{pert}} - L_{\text{ref}}. Az érzékenységi mutatót a perturbáció nagyságához viszonyítjuk: S=ΔL/|δ|S = \Delta L / |\delta|, ahol δ\delta a relatív eltolódás, például 0,150{,}15. A log loss mértékegysége nat; SS mértékegysége nat/szorzó. A ΔL\Delta L csak a specifikált perturbációra adott mért válasz, nem általános garancia.

Hiányzási mintázat eltolódása (missingness shift). A hiányzó értékek arányának vagy mechanizmusának megváltozása a scoring időben. Ha a tanítóban a distance_km hiánytalan, a pipeline mediánnal pótol; ha a scoringban a hiány pont a nagy távolságú rendeléseknél jelenik meg, a medián alábecsüli a valódi értéket. Kézi kontroll: három tanító távolság 10, 20, 30 km, a medián 20. A scoringban 10, 20, hiány, a hiányzó valódi értéke 50; a pótolt 20, tehát a modell 30 km-rel rövidebb utat lát. A hiányzás okának azonosítása a Hiány és anomália mint vizsgálati kérdés adatminőségi vizsgálata, nem a modell feladata; a modell csak a pótlási szabályt örökli.

Sémaváltozás (schema change). A bemeneti oszlopok nevének, típusának, kategóriáinak vagy mértékegységének megváltozása. Példák: új region érték, a basket_minor string dtype, a distance_km mérföldben. A scikit-learn OneHotEncoder paramétere dönt: handle_unknown="error" hibát dob, handle_unknown="ignore" csendben nullázza az ismeretlen dummy oszlopokat, és a sort a referencia szintre teszi. A Feature store és adatkontraktus szükségessége adatszerződése az első védvonal; a sémavalidációt a predict előtt kötelező lefuttatni.

Stresszteszt. Meghatározott perturbációk sorozata, amelynek célja a töréspont megtalálása, nem a jövőbeli garancia. Példa: a distance_km szorzó c∈{0,9;1,0;1,15;1,3}c \in \{0{,}9;\ 1{,}0;\ 1{,}15;\ 1{,}3\} mellett kiszámítjuk a log loss-t; ha c=1,3c=1{,}3-nál ΔL\Delta L meghaladja az előre rögzített bevezetési küszöböt, a kártya rögzíti: „30%-os distance-eltolódás nem támogatott”.

Modellkártya (model card). Strukturált dokumentáció: azonosító, rendeltetés, bemeneti és kimeneti szerződés, tanítási adat és cutoff, referenciametrika, stressztesztek, alkalmazási tartomány, nem támogatott használat, korlátok. A kártya a felelős átadás eszköze, nem a kód helyettesítője.

Jelenség Mi változik Northstar példa Első védvonal
missingness shift hiányzási mechanizmus distance_km 10% hiány a nagy távolságoknál adatminőségi monitor, imputálási jegyzőkönyv
schema change oszlop, dtype, kategória új region érték, forintban érkező basket_minor bemeneti sémavalidáció
measurement shift mérési skála distance_km +15% kalibráció, eloszlás-ellenőrzés

A mérési eltolódás kézi kontrollja: ha a tanító distance_km átlaga μ̂train\hat\mu_{\text{train}} és szórása σ̂train\hat\sigma_{\text{train}}, akkor egy xx távolság standardizált értéke z=(x−μ̂train)/σ̂trainz=(x-\hat\mu_{\text{train}})/\hat\sigma_{\text{train}}. A cc szorzójú perturbáció után z′=(cx−μ̂train)/σ̂trainz' = (cx-\hat\mu_{\text{train}})/\hat\sigma_{\text{train}}, a logit változása βd(c−1)x/σ̂train\beta_d (c-1)x/\hat\sigma_{\text{train}}, ahol βd\beta_d a distance együtthatója a logisztikus modellben. Ha βd>0\beta_d > 0, a nagyobb távolság nagyobb késési valószínűséget ad.

26.4.3 Teljes minimális munkapélda

A lab/ projektgyökérből futtatandó script a szerződés szerinti fájlokat használja. A kód a splitet maga számítja a cutoff és a label-availability szabály alapján, így nem igényel ismeretlen artifactot. A random_state=42 a logisztikus optimalizáló indulását rögzíti; az adatgeneráló seedje 20261020, a kettő külön szerep.

# s104_robustness.py -- futtatás a lab/ projektgyökérből:
# python s104_robustness.py
import json
from pathlib import Path

import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss, roc_auc_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

# 1. Betöltés és feature-összerakás
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"]], on="customer_id")
df["account_age_days"] = (df["order_ts"] - df["signup_date"]).dt.days
# target_late utólag ismert, prediktorból mindig kizárva; itt kizárólag tanító címke.

feature_cols = ["distance_km", "priority_flag", "basket_minor",
                "quantity_sum", "account_age_days"]
category_cols = ["segment", "region", "acquisition_channel"]

# 2. Időbeli split a szerződés alapján
cutoff = pd.Timestamp("2025-05-01T00:00:00+00:00")
train = df[(df["order_ts"] < cutoff) & (df["label_available_at"] < cutoff)].copy()
test = df[df["order_ts"] >= cutoff].copy()
# purged: order_ts < cutoff, de label_available_at >= cutoff; kimarad a példából.

X_train, y_train = train[feature_cols + category_cols], train["target_late"]
X_test, y_test = test[feature_cols + category_cols], test["target_late"]

# 3. Pipeline és tanítás
preprocessor = ColumnTransformer([
    ("num", StandardScaler(), feature_cols),
    ("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), category_cols),
])
model = Pipeline([
    ("prep", preprocessor),
    ("clf", LogisticRegression(max_iter=1000, random_state=42)),
])
model.fit(X_train, y_train)

# 4. Referencia metrika
p_ref = model.predict_proba(X_test)[:, 1]
ref = {"auc": roc_auc_score(y_test, p_ref),
       "log_loss": log_loss(y_test, p_ref)}

# 5. Stresszteszt 1: south régió eltűnik a scoringból
X_region = X_test.copy()
X_region["region"] = X_region["region"].replace("south", "central")
p_reg = model.predict_proba(X_region)[:, 1]
region_result = {"auc": roc_auc_score(y_test, p_reg),
                 "log_loss": log_loss(y_test, p_reg)}

# 6. Stresszteszt 2: distance_km mérési eltolódás +15%
X_dist = X_test.copy()
X_dist["distance_km"] = X_dist["distance_km"] * 1.15
p_dist = model.predict_proba(X_dist)[:, 1]
dist_result = {"auc": roc_auc_score(y_test, p_dist),
               "log_loss": log_loss(y_test, p_dist)}

print(f"test rows: {len(X_test)}")
print(f"reference: AUC={ref['auc']:.3f} log_loss={ref['log_loss']:.3f}")
print(f"region removed: AUC={region_result['auc']:.3f} "
      f"log_loss={region_result['log_loss']:.3f}")
print(f"distance +15%: AUC={dist_result['auc']:.3f} "
      f"log_loss={dist_result['log_loss']:.3f}")

# 7. Kézi invariant-ellenőrzés
assert X_region.shape == X_test.shape
assert X_dist.shape == X_test.shape
assert X_region["region"].nunique() == 2  # south valóban eltűnt
np.testing.assert_allclose(X_dist["distance_km"].mean(),
                           X_test["distance_km"].mean() * 1.15, rtol=1e-12)
print("shape és átlag-skála invariant rendben")

A kimenet szemléltető, nem e könyv futási jegyzéke; a tanuló gépén az értékek eltérhetnek:

test rows: 786
reference: AUC=0.72 log_loss=0.41
region removed: AUC=0.71 log_loss=0.42
distance +15%: AUC=0.65 log_loss=0.49
shape és átlag-skála invariant rendben

Eredmény értelmezése. A south régió eltűnése kicsi ΔL\Delta L értéket ad, mert a régió gyenge prediktor; a +15%-os distance-eltolódás nagyobbat, mert a távolság a késés fő mozgatója. A shape-invariant garantálja, hogy nem véletlenül dobtunk ki sorokat; a distance-átlag 1,15-szeresre növekedése a mérési perturbáció pontos betartását ellenőrzi. A log loss növekedése itt a kalibrálatlanság és a rosszabb rangsorolás keveréke; a két komponenst a Hibaszeletek és felelős értékelés szeletelt vagy a kalibrációs görbe elemzés külön vizsgálhatja.

A fenti két mérésből kitöltött modellkártya vázlata:

A kártya „Nem támogatott használat” mezője konkrét, mérhető feltételeket tartalmaz; az általános jogi szöveg nem helyettesíti a technikai határt.

26.4.4 Feladatok

  1. Értelmezés. A +15%-os distance-stresszteszt eredménye elfogadható. Miért nem állíthatja a kártya, hogy a modell minden jövőbeli eltolódásra robusztus? Fogalmazzon egy konkrét ellenpélda-perturbációt, amelyre a fenti teszt nem terjed ki, és indokolja, miért törné meg a modellt.
  2. Implementálás. Egészítse ki a scriptet hiányzó-distance stresszteszttel: a teszt 10%-ában (np.random.default_rng(7) használatával) állítsa a distance_km értéket NaN-re, a numerikus ágba tegyen SimpleImputer(strategy="median") lépést a StandardScaler elé, majd számítsa ki az AUC-t és log loss-t a referencia értékekkel együtt. A medián a teljes tanító distance_km oszlopából származzon.
  3. Hibadiagnózis. A scoring adatban megjelenik egy island régió. A pipeline OneHotEncoder(handle_unknown="error") beállítással fut. Mi történik a predict hívásnál? Adjon diagnózist és két lehetséges, dokumentált megoldást a modellkártya szempontjából.
  4. Megváltozott feltétel. A scoring rendszer elkezdi a distance_km értéket mérföldben küldeni (1 mérföld = 1,609 km). A kártya melyik mezője tiltja ezt, és milyen bemeneti ellenőrzést kell bevezetni, hogy a hiba ne jusson el a modellhez? Adjon egy konkrét drift-ellenőrző szabályt a tanító medián és a scoring medián alapján.

26.4.5 Megoldások és indoklás

1. megoldás. A stresszteszt egy rögzített perturbációra adott mérés; a jövőbeli eltolódások tere nem fedhető le véges teszttel. Ellenpélda: a basket_minor értékek forintban kezdenek érkezni a scoringba. A tanító StandardScaler euró-cent eloszlásra illeszkedett, ezért a forintban érkező, nagyságrendileg 400-szoros értékek a numerikus feature-öket szélsőséges standardizált értékekké teszik, a logisztikus modell predikciói 0 vagy 1 közelébe torzulnak, és a log loss drasztikusan nő. A stresszteszt nem tartalmazta ezt a skálaváltást, ezért nem bizonyíték rá.

2. megoldás. A numerikus ág módosítása:

from sklearn.impute import SimpleImputer

numeric_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])
preprocessor = ColumnTransformer([
    ("num", numeric_pipe, feature_cols),
    ("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), category_cols),
])

A hiányzó teszt előállítása:

rng = np.random.default_rng(7)
X_missing = X_test.copy()
mask = rng.random(len(X_missing)) < 0.10
X_missing.loc[mask, "distance_km"] = np.nan

p_miss = model.predict_proba(X_missing)[:, 1]
missing_result = {"auc": roc_auc_score(y_test, p_miss),
                  "log_loss": log_loss(y_test, p_miss)}

Az ellenőrző invariant: mask.sum() körülbelül a sorok 10%-a, és a nem maszkolt sorok distance értékei változatlanok. A log loss várt iránya: a mediánnal pótolt hiányzó értékek szisztematikusan alábecsülik a nagy távolságú rendeléseket, ezért a modell kevésbé valószínűsíti a késést azoknál a soroknál, a log loss általában nő; a pontos mértéket a tanuló saját futása adja. A túl alacsony imputálási aránynál a változás nem szignifikáns, ezért a kártya a scoring 5% feletti hiányzást eleve tiltja.

3. megoldás. Az OneHotEncoder.transform ValueError kivételt dob, mert a scoringban olyan kategória jelenik meg, amelyet a tanító nem látott. A diagnózis: sémaváltozás, nem pedig modellhiba. Két dokumentált megoldás: (a) a bemeneti validátor elutasítja az ismeretlen kategóriájú sort, és a rendelést emberi felülvizsgálatra küldi; ekkor a modell csak a támogatott tartományon prediktál. (b) Ha az üzleti döntés az ismeretlen kategória lenémítása, akkor handle_unknown="ignore" beállítást használunk, és a modellkártya rögzíti: „Az ismeretlen régió a referencia szintre kerül; a predikció félrevezető lehet, ezért az ilyen sorokat külön riportálni kell.” A csendes váltás dokumentáció nélkül azt a hamis látszatot kelti, hogy a modell támogatja az új régiót.

4. megoldás. A kártya Bemeneti szerződés mezője tiltja: distance_km numerikus, kilométerben, pozitív; a tanító mediánt és IQR-t a futtatás nyomtatja. A bemeneti ellenőrzés a scoring első 500 sorában kiszámítja a mediánt; ha

|ln(medianscoremediantrain)|>0,1,\left|\ln\left(\frac{\text{median}_{\text{score}}}{\text{median}_{\text{train}}}\right)\right| > 0{,}1,

akkor a scoring leáll és riaszt, mert a feltételezett azonos skála sérül. A mérföldes adat 1,6091{,}609-szeres mediánt adna, a log-hányados körülbelül 0,48, ami azonnal kiszűri. Az Adat- és predikciómonitoring címke nélkül monitoring ezt a szabályt automatizálja a batch folyamatban.

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

A leggyakoribb hiba a stresszteszt zöld eredményének általános garancialként kezelése. A Modellcsalád választása tudományos jegyzőkönyvvel modellcsalád-választásnál azt tanultuk, hogy a tesztet egyszer használjuk a kiválasztott modell jelentéséhez; a stresszteszt ugyanígy csak a specifikált perturbációról ad bizonyítékot. Ha a +15% distance-teszt elfogadható, az nem jelenti, hogy a modell a +25%-os eltolódásra, a forintban küldött kosárértékre vagy az új régióra is megbízható. A kártya „Ki nem próbált” szekciója ezért kötelező; a modell akkor felelős átadású, ha pontosan megmondja, mit nem tud.

Önellenőrzés. Ellenőrizze, hogy a perturbált teszt shape-je egyezik a referencia teszttel, a célvektor érintetlen maradt, a perturbáció csak a megcélzott oszlopot változtatta, és a modellkártya tiltásai konkrét, mérhető feltételeket tartalmaznak-e. Ha a kártya csak „általános környezeti változásra” hivatkozik mérés nélkül, az nem teljesíti a feladatot.

A robusztussági tesztben a perturbáció nagysága és a modellhibát mérő küszöb előre rögzítendő. A szintetikus régiótörlés nem bizonyít teljesítményt valódi eloszláseltolódás mellett.

26.4.7 Rövid összefoglaló

A robusztussági jegyzőkönyv azt méri, hogyan változik a referencia log loss specifikált perturbációk hatására: a régió eltűnése kicsi, a +15%-os distance-eltolódás nagyobb ΔL\Delta L értéket ad. A missingness shift, a séma- és mértékegység-változás külön meghibásodási mód; mindegyikhez konkrét bemeneti validáció tartozik. A modellkártya ezeket a határokat a „Nem támogatott használat” mezőben rögzíti, így a jövőbeli felhasználó nem feltételezi a nem igazolt tartományt. A stresszteszt nem bizonyíték minden eloszlásváltozásra, csak a felsorolt perturbációra. A következő lépésben a Szöveges baseline és ritka mátrix szöveges baseline-oknál ugyanezt a gondolatot alkalmazzuk a szöveg-előfeldolgozás sérüléseire.

További olvasmány és elsődleges dokumentáció: [SklearnCalibration] scikit-learn Calibration, https://scikit-learn.org/stable/modules/calibration.html (hozzáférés: 2026-10-21).