A Northstar modell championja már batch predikcióban fut a rendelés
pillanatában. A működő kiadásnál Batch inference és
idempotens output rögzítette a bemeneti sémát és Modelljelölt, jóváhagyás és registry a
modellverziót, de a valódi célváltozó, a target_late, csak
a kézbesítés után keletkezik. Az első hetekben tehát a monitoringgyakran
csak magát az inputot, a feature-öket és a prediktált valószínűséget
látja. A megfigyelési egység: egy batch predikciós sor, azaz egy
rendeléshez tartozó feature-vektor a score oszloppal. A cél
az, hogy a címke nélküli ablakban is riasztást adjunk, ha a feldolgozási
szerződés sérül vagy a bemeneti eloszlás el távolodik a referenciától.
Az ellenőrzés feltétele: a jelentés sémaállapotot,
distance_psi-t és a score átlagának
eltolódását adja, majd OK, REVIEW,
WARN vagy CRITICAL állapotot képez. Előzetes
becslés a szintetikus ablakokra: a normál ablak PSI-je 0,1 alatt marad,
az eltolt ablaké a 0,15–0,25 sávba kerülhet, a sémahibás ablak pedig
biztosan kritikus, mert negatív távolságot és hiányzó oszlopot
tartalmaz.
A „drift” itt nem egyetlen jelenség, ezért először négy fogalmat különítünk el.
| Típus | Amit mér | Címke kell? | Tipikus ok |
|---|---|---|---|
| schema drift | a bemeneti szerződés sérülése | nem | upstream hiba, új forrás, hiányzó mező |
| data drift | változása a baseline-hoz képest | nem | szezon, kampány, mérési változás |
| prediction drift | a prediktált score eloszlásának változása |
nem | data drift, küszöbhiba, modellcsere |
| concept drift | változása | igen | ügyfélviselkedés, folyamatváltozás |
A schema drift nem statisztikai kérdés. Ugyanaz a
szerződés, amelyet a batch inference Batch inference
és idempotens output sémavalidációnál használt: mely oszlopoknak
kell létezniük, milyen dtype-pal és tartománnyal. Ha a
region oszlopban megjelenik egy west érték,
vagy a distance_km negatív lesz, az szerződésszegés, nem
eloszlásváltozás. Hiába hibátlan a statisztika, ha a sorokat eleve rossz
formában töltöttük be.
A data drift az input eloszlását veti össze egy rögzített baseline window-val. A baseline ablak az az időszak, amikor a modell kiadásához tartozó validáció szerint ismert volt a teljesítmény; példánkban 2025-04-01 és 2025-04-30. A baseline window nem csúszik naponta, mert a folyamatos frissítés lassan követné a trendet, és a riasztás elveszne. Új baseline csak explicit modellváltáskor, a registry jóváhagyásával Modelljelölt, jóváhagyás és registry képződik.
A prediction drift a modell kimenetét, a
score eloszlását figyeli. Ha a prediktált valószínűség
átlaga vagy a magas kockázatú részarány elmozdul, az lehet data drift
következménye, de lehet koncepcionális változás vagy rossz modellverzió
is. Önmagában vizsgálati jel, nem bizonyíték.
A concept drift a feltételes kimeneteloszlás változása; nem feltétlenül jelent romló teljesítményt, mert annak mértéke a modellre és az értékelési eloszlásra is függ: ugyanaz a feature-vektor már más valószínűséggel jár. Ehhez címke kell, ezért a jelen alfejezetben csak annyit mondhatunk: a címke nélküli eszközök nem azonosítják, legfeljebb gyanút keltenek.
A leggyakoribb két mérőszám a PSI és a Kolmogorov–Smirnov-statisztika.
A PSI kategorizált binningre épül:
ahol a baseline ablakbeli relatív gyakoriság az -edik binben, a jelen ablakbeli relatív gyakoriság ugyanott, a binek száma, természetes logaritmus. A mennyiség dimenziómentes, mert gyakoriságok hányadosából számoljuk. Kézi kontroll: ha és , akkor
A formula szimmetrikus: a referencia és a jelen ablak felcserélése azonos értéket ad.
A KS-statisztika folytonosabb leírást ad:
ahol
a megfelelő ablak empirikus eloszlásfüggvénye. A D_KS a két
eloszlásfüggvény legnagyobb függőleges távolsága, szintén
dimenziómentes. A scipy.stats.ks_2samp e mellé p-értéket is
ad, de itt a matematikai híd kulcsa: a p-érték erősen
mintanagyság-függő. Kis mintán még nagy
is könnyen lehet véletlen; nagy mintán egészen apró, működés
szempontjából jelentéktelen eltolódás is „szignifikáns” lehet. Ezért a
monitoring riasztás küszöbét hatásméretre, tipikusan PSI-re tesszük, a
p-értéket pedig csak vizsgálati támogatásként használjuk. Egy 10 binre
számolt PSI-nél a 0,1 alatti érték e projektben alacsony, a 0,1–0,2
közötti vizsgálandó, a 0,2 feletti pedig erős riasztási szintet
jelezhet. A PSI érzékeny a binhatárokra, a ritka kategóriákra és az
alkalmazott simításra; önmagában nem bizonyít eloszlásazonosságot. Ezek
működési küszöbök, nem statisztikai törvények; a projekt a saját döntési
költségéhez igazíthatja őket.
A labor egy lab/monitoring_lab mappában él, és négy
ablakot állít elő: egy referenciaablakot és három vizsgált ablakot. A
generálás Python standard libraryt, NumPy-t és SciPy-t használ.
# drift_report.py
"""Séma-, input- és predikciódrift jelentés szintetikus ablakokra."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
import pandas as pd
SCHEMA = {
"distance_km": {"kind": "numeric", "min": 0.0, "max": 500.0},
"basket_minor": {"kind": "numeric", "min": 0.0},
"quantity_sum": {"kind": "integer", "min": 1, "max": 50},
"segment": {"kind": "category", "allowed": {"consumer", "business"}},
"priority_flag": {"kind": "integer", "min": 0, "max": 1},
"score": {"kind": "numeric", "min": 0.0, "max": 1.0},
}
def check_schema(df: pd.DataFrame) -> list[str]:
issues: list[str] = []
for col, spec in SCHEMA.items():
if col not in df.columns:
issues.append(f"missing:{col}")
continue
s = df[col]
if spec["kind"] == "numeric" and not pd.api.types.is_numeric_dtype(s):
issues.append(f"dtype:{col}")
continue
if spec["kind"] == "integer" and not pd.api.types.is_integer_dtype(s):
issues.append(f"dtype:{col}")
continue
if spec["kind"] == "category" and not set(s.dropna().unique()) <= spec["allowed"]:
issues.append(f"allowed:{col}")
if "min" in spec and s.min(skipna=True) < spec["min"]:
issues.append(f"min:{col}")
if "max" in spec and s.max(skipna=True) > spec["max"]:
issues.append(f"max:{col}")
if s.isna().any():
issues.append(f"null:{col}")
return issues
def make_window(n: int, seed: int, mean_dist: float,
schema_bad: bool = False) -> pd.DataFrame:
rng = np.random.default_rng(seed)
df = pd.DataFrame({
"distance_km": rng.normal(mean_dist, 25.0, n).clip(0.0, 500.0),
"basket_minor": rng.integers(200, 50_000, n),
"quantity_sum": rng.integers(1, 12, n),
"segment": rng.choice(["consumer", "business"], n, p=[0.8, 0.2]),
"priority_flag": rng.integers(0, 2, n),
})
z = -1.8 + 0.012 * df["distance_km"] + 0.35 * df["priority_flag"]
df["score"] = 1.0 / (1.0 + np.exp(-z))
if schema_bad:
df = df.drop(columns=["priority_flag"])
df.loc[0, "distance_km"] = -10.0
return df
def psi_binned(ref: np.ndarray, cur: np.ndarray, bins: int = 10) -> float:
edges = np.quantile(ref, np.linspace(0.0, 1.0, bins + 1))
edges[0] = -np.inf
edges[-1] = np.inf
p = np.histogram(ref, bins=edges)[0] / len(ref)
q = np.histogram(cur, bins=edges)[0] / len(cur)
eps = 1e-4
p = np.where(p == 0, eps, p)
q = np.where(q == 0, eps, q)
return float(np.sum((q - p) * np.log(q / p)))
def report(baseline: pd.DataFrame, current: pd.DataFrame,
current_name: str, row_floor: int = 500) -> dict:
schema_issues = check_schema(current)
if "distance_km" in current.columns:
psi_dist = psi_binned(
baseline["distance_km"].to_numpy(),
current["distance_km"].to_numpy())
else:
psi_dist = float("nan")
score_delta = float(current["score"].mean() - baseline["score"].mean())
if schema_issues:
state = "CRITICAL"
elif psi_dist > 0.2 or abs(score_delta) > 0.05:
state = "WARN"
elif len(current) < row_floor:
state = "REVIEW"
else:
state = "OK"
return {
"window": current_name,
"rows": len(current),
"schema_issues": schema_issues,
"distance_psi": round(psi_dist, 4),
"score_mean_delta": round(score_delta, 4),
"state": state,
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--baseline", type=Path, required=True)
ap.add_argument("--current", type=Path, required=True)
ap.add_argument("--window-name", required=True)
args = ap.parse_args()
rep = report(pd.read_csv(args.baseline), pd.read_csv(args.current),
args.window_name)
print(json.dumps(rep, indent=2))
code = {"OK": 0, "REVIEW": 1, "WARN": 1, "CRITICAL": 2}[rep["state"]]
return code
if __name__ == "__main__":
raise SystemExit(main())A windowokat külön file gyártja:
# make_windows.py
from pathlib import Path
from drift_report import make_window
out = Path("data")
out.mkdir(exist_ok=True)
make_window(2000, 101, 50.0).to_csv(out / "reference.csv", index=False)
make_window(800, 102, 50.0).to_csv(out / "current_normal.csv", index=False)
make_window(800, 103, 65.0).to_csv(out / "current_shift.csv", index=False)
make_window(800, 104, 50.0, schema_bad=True).to_csv(
out / "current_schema_bad.csv", index=False)Futtatás Linux/macOS bash alatt, lab/monitoring_lab
munkakönyvtárból:
python make_windows.py
python drift_report.py --baseline data/reference.csv \
--current data/current_normal.csv --window-name current_normal
python drift_report.py --baseline data/reference.csv \
--current data/current_shift.csv --window-name current_shift
python drift_report.py --baseline data/reference.csv \
--current data/current_schema_bad.csv --window-name current_schema_badHa a script WARN vagy REVIEW állapotot ad,
a folyamat 1-es, CRITICAL esetén 2-es kilépési kódot
használ, így a pipeline-ban VIII. ellenőrző pont:
átadható modellprojekt is automatikus kapu lehet.
A könyv írásakor nem futtattuk a fenti kódot, ezért a konkrét JSON-t
szemléltetőnek, a generátorparaméterekből levezetettnek tekintjük. A
current_normal ablakban a distance_psi közel
0, a score_mean_delta közel 0, az állapot várhatóan
OK. A current_shift ablakban a
distance_km átlagát 50-ről 65 km-re növeltük; a reference
kvantilisek alapján a felső binekbe több sor kerül, ez a PSI-t a
0,15–0,25 sávba viheti, és a score átlaga is emelkedik,
mert a demó score egyenlete a távolságtól függ. Ez együtt
WARN. A current_schema_bad ablakból hiányzik a
priority_flag, és az első sor távolsága negatív; a
sémaellenőrzés missing:priority_flag és
min:distance_km hibákat ad, az állapot
CRITICAL. A kézi kontroll: a PSI-hoz a bin-arányokból a
korábbi képlet pontosan kiszámítható; a check_schema
semmilyen WARN-állapotot nem adhat, amíg egyetlen sémahiba
is van, mert akkor a CRITICAL ág az első.
A példában a score-ot a feature-ökből számítjuk, de ez
csak tanulási modell, nem a regisztrált champion. A valódi rendszerben a
score a batch manifestből jön Batch
inference és idempotens output, és a monitoring nem a feature-ökből
újraszámolja, hanem a naplózott kimenetet ellenőrzi. Azt is pontosan
mondjuk ki: a PSI a distance_km-re vonatkozik, nem az egész
adatsorra; a többi oszlopra külön PSI számítható, de a példa a
legérzékenyebb folytonos inputot emeli ki.
Értelmezés. A jelentés szerint
distance_psi = 0.31, score_mean_delta = 0.00.
Miért nem jelenti ez, hogy a modell használhatatlan? Mondj egy olyan
mechanizmust, amely ilyen párost adhat, és írd le, milyen további adat
kell a tényleges döntéshez.
Megvalósítás. Bővítsd a report
függvényt high_risk_share kiszámításával, ahol a magas
kockázatú sor definíciója score >= 0.7. A referencia és
a jelen ablak aránykülönbségére risk_delta néven
figyelmeztess, ha abs(risk_delta) > 0.05. Készíts hozzá
egy 4 soros DataFrame-példát, amelyben a referencia score
értékei [0.6, 0.6, 0.8, 0.8], a jelené
[0.6, 0.8, 0.8, 0.8], és várd el a WARN
állapotot.
Hibadiagnózis. A monitoring WARN
állapotot ad: distance_psi = 0.24,
score_mean_delta = 0.01. Milyen sorrendben vizsgálnád az
upstream adatot, a modellverziót és a score-kvantiliseket? Miért nem
írhatod a jegyzőkönyvbe, hogy „igazolt teljesítményromlás”
történt?
Transzfer kis ablakra. Tegyük fel, hogy a
következő batch csak 200 sort tartalmaz, és a mért
distance_psi = 0.19 a küszöb alatt van. Hogyan változtatja
ez a döntést? Módosítsd a report függvényt úgy, hogy 500
sor alatt REVIEW állapotot állítson be, és a jelentésbe
note mezőt is írjon: „kis minta, vizsgálat javasolt”. A
row_floor paraméter már létezik, tehát a feladat a
hívásoldal és a note mező kiegészítése.
A PSI csak
eltolódást mér, nem
-et.
Ha a modell a distance_km-re gyenge érzékenységű, akkor az
input eltolódás nem változtatja a predikciót; az is lehet, hogy a modell
épp ebben a tartományban kalibrált marad. A további adat: a későbbi
címkék, azaz a kézbesítés után számolt target_late, amit a
következő alfejezet Késő címke, teljesítmény és
újratanítás használ, vagy legalább egy stresszteszt a Robusztusság és korlátokkal írt modellkártya
módszeréből, hogy a score érzékenységét ismerjük.
A report bővítése:
ref_high = (baseline["score"] >= 0.7).mean()
cur_high = (current["score"] >= 0.7).mean()
risk_delta = float(cur_high - ref_high)majd a WARN feltétel:
elif psi_dist > 0.2 or abs(score_delta) > 0.05 or abs(risk_delta) > 0.05:A példában a referencia magas aránya
,
a jelené
,
risk_delta = 0.25, tehát WARN. Ellenőrző
invariant: az adatsor hossza kicsi, de a mean() itt
egyszerű arány; a kapott érték kerekítés nélkül is pontosan
.
Először a sémaellenőrzést kell megerősíteni: van-e új adatforrás, mértékegységváltás vagy csonkolt batch? Másodszor a modellverziót: a registry Modelljelölt, jóváhagyás és registry szerint a champion hash-e megegyezik-e a futtatott modellel? Harmadszor a score eloszlás kvantiliseit: az átlag alig változhat úgy, hogy a farok eltolódik, ezért a -es kvantilisek is kellenek. A teljesítményromláshoz címke kell, tehát a „igazolt” szó hibás lenne; a jegyzőkönyv legfeljebb „vizsgálati jel: input drift score-stabilitással” állítást tehet.
Kis mintán a PSI becslés varianciája nagy, ezért a küszöb alatti
érték nem megnyugtató, csak nem riaszt. A REVIEW állapot
nem blokkoló hiba, hanem emberi vizsgálati igény. A módosítás:
state = "REVIEW" if len(current) < row_floor else "OK"
rep["note"] = "kis minta, vizsgálat javasolt" if len(current) < row_floor else ""A küszöb feletti PSI továbbra is WARN legyen, tehát a
feltételsorrend ne változzon: séma, nagy eltérés, majd kis minta.
A leggyakoribb tévedés az, hogy a drift kimutatása a
modell használhatatlanságát jelenti. Ellenpélda: a
current_shift ablakban a distance_km erősen
eltolódik, a PSI magas, de a demó score függvénye csak
mérsékelten változik, és ha a tényleges target_late is alig
változik, a modell továbbra is jól dönt. A PSI tehát csak annyit mond:
„a jelenlegi input már nem hasonlít a baseline-ra”; a következményt
külön kell igazolni.
A másik irány sem bizonyíték. Ha a score eloszlása
napokon át változatlan, az nem jelenti, hogy a modell jó; lehet, hogy
minden szegmensben nő a késés, mert a szállító folyamat megváltozott, és
a modell ugyanazt a valószínűséget adja, miközben
eltolódott. Ezt a concept driftet a címke nélküli monitor nem látja; a
késői címke Késő címke, teljesítmény és
újratanítás fogja feltárni. A monitoring tehát riasztási és
vizsgálati eszköz, nem a modellminőség független tanúsítványa.
Önellenőrzés: Mi a különbség a séma- és a data drift között a beavatkozás sorrendjében? Miért veszélyes a KS-teszt p-értékét önmagában riasztási küszöbként használni? Mikor szabad megváltoztatni a baseline window-t?
A címke nélküli monitoring négy különböző jelet kezel: a séma drift
szerződésszegést jelez, azonnal kritikus; a data drift PSI-vel a
baseline-hoz méri a bemeneti eloszlást; a prediction drift a score
eloszlását; a concept drift csak későbbi címkével igazolható. A baseline
window a champion validációs időszaka, és modellváltásig rögzítve marad.
A PSI hatásméret-küszöb, a KS-féle p-érték mintanagyság-függő támogatás;
kis mintán a bizonytalanságot REVIEW állapottal kell
jelölni. A következő alfejezet Késő címke,
teljesítmény és újratanítás a megérkező címkét köti a tényleges
teljesítményhez és az újratanításhoz.