A Northstar rendeléskori modellje azt a valószínűséget becsüli, hogy
egy rendelés kézbesítése 7 napnál későbbi lesz. A predikció a
predicted_at = order_ts pillanatban készül, tehát semmilyen
későbbi kézbesítési adat nem lehet predictor. A target_late
címke viszont késve, tényleges kézbesítéskor válik ismertté:
label_available_at = actual_delivery_at. Ebből azonnal
következik a monitoring-probléma: a friss predikciók egy része még nem
értékelhető, mert a címke nem érkezett meg.
A feladat egy gördülő teljesítményriport tervezése, amely az értékelés időpontjában ténylegesen rendelkezésre álló címkéket használja. A bemenet két tábla:
| Tábla | Megfigyelési egység | Fontos mezők |
|---|---|---|
predictions |
egy predikció, (model_id, order_id) |
predicted_at, predicted_late_prob,
features_schema_version |
labels |
ugyanaz a rendelés, order_id |
label_available_at, target_late |
A riport as-of időpontja legyen
V = 2025-07-15T00:00:00+00:00, a predikciós ablak pedig a
július 1-jei öt rendelés. Az előzetes becslés: körülbelül 80%-os
fedettség várható, mert egy szélsőségesen késő kézbesítés címkéje még
hiányzik. A kérdés: mi mondható erről az ablakról tényként, és mikor
szabad újratanítást indítani?
Késleltetett címke (delayed label) olyan célváltozó, amely a predikció után csak későbbi eseményből ismerhető meg. A Northstar szerződésében a késés nem egységesen 7 nap: a nem késő rendelés hamarabb, a késő rendelés később kap címkét. A címke hiánya tehát nem véletlen hiány, hanem magával a célváltozóval összefüggő szelekció: a hiányzó sorok nagyobb eséllyel pozitívak. Ezt a Sokaság, minta és adatfelvételi folyamat alfejezetben megismert mintavételi kérdéshez hasonlóan kell kezelni.
Értékelési ablak (evaluation window) két időkorlátból áll:
predicted_at értéke
a vizsgált intervallumba esik;label_available_at <= V.Egy rendelés csak akkor szerepel az értékelés nevezőjében, ha a
predikció az ablakon belül készült. A számlálóba viszont csak azok
kerülnek, amelyek címkéje V-ig megérkezett:
ahol a modellazonosító, az ablak, az összes ablakbeli predikció darabszáma, a rendelkezésre álló címkéjű soroké. A mennyiség dimenzió nélküli arány, értéke -ben van. A coverage nem modellminőség, hanem adatelérhetőségi mérőszám: azt mondja meg, mekkora részmintáról beszélhetünk egyáltalán.
A részmintán számolható diszkriminációs mutató, például az AUC, rangpár-képlettel:
ahol azon pozitív–negatív párok száma, ahol a pozitív rendeléshez magasabb pontszám tartozik, a döntetlen pároké, a pozitív, a negatív minták száma. A nevező az összes lehetséges pozitív–negatív pár. Véletlen rangolásnál (\text{AUC}=0{,}5), tökéletes elválasztásnál (1).
Kézi kontrollpélda a laborhoz:
| order_id | target_late | predicted_late_prob | label elérhető V-kor? |
|---|---|---|---|
| O1 | 0 | 0.10 | igen |
| O2 | 0 | 0.30 | igen |
| O3 | 1 | 0.20 | igen |
| O4 | 1 | 0.90 | igen |
| O5 | ? | 0.85 | nem |
A rangpárok: O3 pozitív pontszáma (0{,}20) egy negatívnál nagyobb, egynél kisebb; O4 pontszáma mindkét negatívnál nagyobb. Így (C=3), (T=0), (P=2), (N=2), tehát (\text{AUC}=3/4=0{,}75). A coverage (4/5=0{,}80). Fontos: az O5 hiányzó sor valószínűleg pozitív és magas pontszámú, ezért a (0{,}75) csak a négy elérhető címkéjű sorra igaz, nem a teljes ötelemű ablakra.
Visszacsatolási hurok (feedback loop) akkor
keletkezik, ha a modell kimenete befolyásolja a folyamatot, amelyből a
címke születik. Ha például a magas kockázatú rendeléseket gyorsított
kézbesítési sávba terelik, akkor a target_late már nem a
természetes kézbesítési folyamat kimenete, hanem a modell által
befolyásolt folyamaté. Ilyenkor a javuló AUC nem feltétlenül jobb
modellt, hanem a beavatkozás hatását is méri. A riportnak ezért
rögzítenie kell, hogy a champion modell mely rendeléseken kapott
beavatkozást, és a kauzális állítás külön véletlenített értékelést
igényel.
Újratanítási feltétel (retrain trigger) nem maga a modell, hanem explicit szabályzat. Akkor érdemes challenger modellt indítani, ha:
coverage >= 0.90 és
(n_{\text{labeled}}\ge 300);A champion az éles modell, a challenger pedig a jelölt.
Módszertani korlát. A 90%-os címkefedettség sem teszi automatikusan torzítatlanná a részleges AUC-t: a címke késése összefügghet a tényleges késéssel és a predikcióval. A teljesítményjelentésben jelölni kell a cenzorálást, és szükség szerint csak teljesen érett kohorszt, illetve külön indokolt késéskorrekciót szabad használni. A challenger predikciói naplózódnak, de üzleti beavatkozást nem vezérelnek, amíg független, érett ablakon jobbnak nem bizonyul, és a javulás becslési intervalluma nem zárja ki a nullát.
Az alábbi szándékosan kis, szintetikus példa standard library
sqlite3-mal fut. a kód futása még nincs automatizáltan
ellenőrizve, ezért a kimenet szemléltető, elméletileg
várt.
import sqlite3
AS_OF = "2025-07-15T00:00:00+00:00"
MIN_COVERAGE = 0.90
MIN_LABELED = 300
BASELINE_AUC = 0.80
PREDICTIONS = [
("O1", "champ-v3", "2025-07-01T09:30:00+00:00", 0.10, "fs-2"),
("O2", "champ-v3", "2025-07-01T09:31:00+00:00", 0.30, "fs-2"),
("O3", "champ-v3", "2025-07-01T09:32:00+00:00", 0.20, "fs-2"),
("O4", "champ-v3", "2025-07-01T09:33:00+00:00", 0.90, "fs-2"),
("O5", "champ-v3", "2025-07-01T09:34:00+00:00", 0.85, "fs-2"),
]
LABELS = [
("O1", "2025-07-03T11:00:00+00:00", 0),
("O2", "2025-07-04T08:10:00+00:00", 0),
("O3", "2025-07-10T12:00:00+00:00", 1),
("O4", "2025-07-13T09:00:00+00:00", 1),
]
def auc_pairs(y_true: list[int], scores: list[float]) -> float | None:
pos = [s for y, s in zip(y_true, scores) if y == 1]
neg = [s for y, s in zip(y_true, scores) if y == 0]
if not pos or not neg:
return None
concordant = ties = 0
for sp in pos:
for sn in neg:
if sp > sn:
concordant += 1
elif sp == sn:
ties += 1
return (concordant + 0.5 * ties) / (len(pos) * len(neg))
def decide_readiness(coverage, auc, n_labeled):
if coverage < MIN_COVERAGE:
return "hold_report", "fedetlen ablak: a részleges AUC nem kezelhető tényként"
if n_labeled < MIN_LABELED:
return "wait", "kevés címke a megbízható döntéshez"
threshold = max(0.75, BASELINE_AUC - 0.03)
if auc is None:
return "human_review", "üres AUC, adatfolyam-hiba gyanú"
if auc < threshold:
return "retrain_candidate", f"AUC {auc:.3f} < {threshold:.3f}"
return "none", "champion teljesítménye megfelelő"
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE predictions (
order_id TEXT,
model_id TEXT,
predicted_at TEXT,
predicted_late_prob REAL,
features_schema_version TEXT,
PRIMARY KEY (model_id, order_id)
);
CREATE TABLE labels (
order_id TEXT PRIMARY KEY,
label_available_at TEXT,
target_late INTEGER
);
""")
con.executemany(
"INSERT INTO predictions VALUES (?,?,?,?,?)", PREDICTIONS
)
con.executemany(
"INSERT INTO labels VALUES (?,?,?)", LABELS
)
summary = con.execute(
"""
SELECT p.model_id,
COUNT(*) AS n_pred,
COUNT(l.order_id) AS n_labeled,
ROUND(1.0 * COUNT(l.order_id) / COUNT(*), 4) AS coverage
FROM predictions p
LEFT JOIN labels l
ON l.order_id = p.order_id
AND l.label_available_at <= ?
WHERE p.predicted_at < ?
GROUP BY p.model_id
""",
(AS_OF, AS_OF),
).fetchall()
labelled = con.execute(
"""
SELECT p.order_id, p.predicted_late_prob, l.target_late
FROM predictions p
JOIN labels l ON l.order_id = p.order_id
WHERE l.label_available_at <= ?
AND p.predicted_at < ?
ORDER BY p.order_id
""",
(AS_OF, AS_OF),
).fetchall()
model_id, n_pred, n_labeled, coverage = summary[0]
scores = [row[1] for row in labelled]
y_true = [row[2] for row in labelled]
auc = auc_pairs(y_true, scores)
action, reason = decide_readiness(coverage, auc, n_labeled)
print(
{"model_id": model_id, "n_pred": n_pred, "n_labeled": n_labeled,
"coverage": coverage, "auc_on_labeled": round(auc, 3) if auc else None,
"action": action, "reason": reason}
)A szemléltető várható kimenet:
{'model_id': 'champ-v3', 'n_pred': 5, 'n_labeled': 4,
'coverage': 0.8, 'auc_on_labeled': 0.75,
'action': 'hold_report',
'reason': 'fedetlen ablak: a részleges AUC nem kezelhető tényként'}
A kód a label_available_at <= AS_OF szűrést a joinba
teszi, ezért a jövőbeli címke nem számít elérhetőnek. A
predicted_at < AS_OF a határon éles predikciókat is a
nevezőben hagyja, de a fedettség mutatja meg, hogy közülük mennyi
érett.
A kézi kontroll és a kód ugyanazt adja: öt predikció, négy címke, coverage (0{,}8), a négy elérhető címkén (\text{AUC}=0{,}75). A riport mégsem mondhatja, hogy „a modell AUC-ja 0,75 ebben az ablakban”. A helyes állítás:
Az ablak predikcióinak 80%-ára van címke; ezen a nem véletlenül elérhető részmintán az AUC 0,75. A teljes ablakra vonatkozó állítás addig nem tehető, amíg a coverage nem éri el a 0,90-es szabályzatot.
Az (O5) hiányzó címkéjű rendelés magas, (0{,}85) pontszámú. Ha később kiderül, hogy valóban késő kézbesítés volt, az öt teljes soron számolt AUC (5/6\approx0{,}833)-ra változik. Ez az ellenpélda mutatja, hogy a késleltetett pozitív címkék mechanikusan torzíthatják az interim AUC-t.
Az újratanítási policy külön választja az adatelérhetőségi feltételt
a teljesítményfeltételtől. A hold_report nem indít
újratanítást; előbb meg kell várni az érett ablakot, vagy visszafelé
kell tolni a predikciós ablak végét. Ha egy fedett ablakban továbbra is
romlik az AUC, akkor jön a retrain_candidate, de a champion
addig élesben marad.
A riportban mindig szerepelnie kell a model_id mezőnek.
Ha a champion és a shadow challenger sorait összevonva mérjük az AUC-t,
akkor sem a régi, sem az új modellhez nem tartozó állítást nem kapunk. A
challenger csak akkor tekinthető előléptethetőnek, ha az aktuális érett
ablakon mért AUC-javulás intervalluma nem tartalmaz nullát, és a
beavatkozási környezet azonos.
Értelmezés. A legfrissebb hétnapos ablakban (n_{\text{predicted}}=1000), (n_{\text{labeled}}=600), és a riport szerint (\text{AUC}=0{,}71). Szabad-e közleményt írni arról, hogy a modell teljesítménye ezen a héten (0{,}71)-re romlott? Indokold a coverage és a késleltetett pozitív címkék szerepével.
Implementálás. Egészítsd ki az előző kódot egy
label_delay_hours oszloppal, amely a
label_available_at - predicted_at különbség. A korábbi, már
lezárt történeti ablakban a sorted(label_delay_hours) lista
utolsó 95%-os kvantilisét használva számíts
mature_cutoff = V - d95 értéket, és csak a
predicted_at <= mature_cutoff sorokat nevezd érettnek.
Mutasd meg pszeudokóddal, hogyan kerül az így számított d95
a riportba.
Hibadiagnózis. Egy társad riportjában a
predictions és labels összekapcsolásnál ez a
feltétel szerepel:
LEFT JOIN labels l
ON l.order_id = p.order_id
AND l.label_available_at IS NOT NULLV = 2025-07-15 mellett egy rendelés címkéje
2025-07-12-én érkezett meg. Miért hamis a riport „coverage
= 1.0” állítása? Add meg a javított feltételt, és magyarázd el, miért
nem szabad az IS NOT NULL önmagában.
Megváltozott feltétel. A csapat bevezeti azt a
beavatkozást, hogy minden predicted_late_prob >= 0.7
rendelés automatikusan gyorsított logisztikai kezelést kap. Harminc nap
múlva a champion AUC-ja meredeken javul. Milyen visszacsatolási hurok
állhat a javulás mögött? Milyen kísérleti vagy shadow-kontrolltervezés
segítene különválasztani a modell javulását a beavatkozás
hatásától?
1. megoldás. Nem szabad így közölni. A coverage (600/1000=0{,}60), tehát az ablak 40%-a címke nélküli. A késő kézbesítések címkéje definíció szerint később érkezik, tehát a hiányzó sorokban felülreprezentáltak lehetnek a pozitív esetek. A (0{,}71) a rendelkezésre álló, önszelektált részmintára vonatkozó interim szám, nem a teljes hét ténye. Helyes közlés: „A vizsgált ablak még nem érett; a rendelkezésre álló 60%-os részen az AUC 0,71. A teljes ablakra vonatkozó következtetés elhalasztva.”
2. megoldás. A Python-pszeudokód így nézhet ki:
import math
from datetime import datetime, timedelta
def parse_ts(value):
return datetime.fromisoformat(value.replace("Z", "+00:00"))
delays = [
(parse_ts(l) - parse_ts(p)).total_seconds() / 3600
for p, l in labelled_rows
]
d95_hours = sorted(delays)[math.ceil(0.95 * len(delays)) - 1]
mature_cutoff = parse_ts(AS_OF) - timedelta(hours=d95_hours)Ellenőrző invariáns: a lezárt történeti ablakban a delay-ek legalább
95%-a kisebb vagy egyenlő (d_{95})-nél. A riport d95_days
mezőjét is menteni kell, mert ha a ciklusidő később megváltozik, a
mérési ablak visszafelé tolása igazolhatóvá válik. A kritikus korlát: a
(d_{95}) sem garancia, csak stacioner folyamatra érvényes üzemi
szabály.
3. megoldás. Az IS NOT NULL csak azt
vizsgálja, hogy a rendeléshez tartozik-e valamilyen címke a
labels táblában; azt nem, hogy az adott címke az értékelési
időpontban már ismert volt-e. A 2025-07-12-i címke
2025-07-15-i as-of nézetből visszatekintve ismert, de a
riportnak ezt a jövőt nem lett volna szabad felhasználnia, ha korábbi
V-t használt volna. A javítás:
LEFT JOIN labels l
ON l.order_id = p.order_id
AND l.label_available_at <= ? -- AS_OF/V értékAz IS NOT NULL tehát az adatbázis teljes történetére
vonatkozik, a <= V viszont az as-of pillanatra. A kettő
csak akkor egyenértékű, ha a label-tábla tranzakciósan nem tartalmaz
jövőbeli címkéket, de erre nem szabad építeni.
4. megoldás. A javulás mögött valószínűleg
beavatkozási hurok áll: a modell által kiemelt rendeléseket a gyorsított
logisztika részben „megmenti”, így a target_late csökken. A
champion AUC-ja ezt a módosított kimenetet méri, nem az eredeti,
beavatkozás nélküli kockázatot. A különválasztáshoz véletlenített
kontrollcsoportra van szükség, amely azonos pontszámküszöbű, de nem
beavatkozott rendelésekből áll. Ha ilyen véletlenítés nem áll
rendelkezésre, a modellre mint prediktív eszközre tett állítást le kell
cserélni a „modell + beavatkozási szabályzat” együttes hatásának
leírására. A challenger előléptetése ilyenkor a beavatkozási populációra
értelmezett üzleti cél szerint dőljön el.
A leggyakoribb hiba: a friss címke nélküli időszak teljesítményét tényként megadni. A fenti példa éppen ezt cáfolja: a négy elérhető címkén számolt (0{,}75) AUC-ból kiindulva valaki azt írta volna, hogy „a modell AUC-ja 0,75”, pedig O5 címkéjének megérkezése után a helyes érték (5/6\approx0{,}833). A helyes riport a mértéket, a részminta méretét és a fedettséget együtt írja le.
Önellenőrzés a következő riport készítése előtt:
label_available_at <= V, nem pedig
IS NOT NULL?model_id szerint
van-e csoportosítva?A korábbi Adat- és predikciómonitoring címke
nélkül driftriasztásai itt nyerik el a végső értelmüket: a
predikcióeloszlás-változás csak gyanú, a teljesítményromlás pedig csak
akkor bizonyítható, ha a késleltetett címkék miatti részmintát pontosan
kezeljük. A FastAPI végpont és ellenőrzött
bemenet alfejezetben bevezetett kötött predikciós szerződés nélkül a
predicted_at és a label_available_at
összekapcsolása is sérülne.
A késleltetett címkék miatt a friss predikciós ablak csak részben értékelhető. A coverage a rendelkezésre álló címkék arányát méri, nem a modell minőségét; ha (0{,}90) alatt van, a részleges AUC nem teljesítménytény. A riport as-of szűréssel zárja ki a jövőbeli címkéket, és külön választja az újratanítási feltételt az emberi vizsgálattól. Ha a modell beavatkozást vezérel, a címke visszacsatolási hurkon keresztül a modell hatását is tartalmazza; ilyenkor csak shadow- vagy véletlenített értékelés különíti el a predikció javulását a beavatkozásétól. A champion mellett a challenger naplózva mérhető. A következő Hozzáférés, titkok és adatminimalizálás alfejezetben a hozzáférés, titokkezelés és adatminimalizálás keretezi ugyanezt az éles monitorozási folyamatot.