A csapat a Predikciós szerződés és hibateszt
predikciós szerződését követve a Northstar rendelésadataiban azt
szeretné becsülni, hogy egy rendelés kézbesítése meghaladja-e a 7 napos
ígéretet. A megfigyelési egység egy order_id; a célváltozó
a rendeléskor még ismeretlen target_late, a predikció
pillanata az order_ts. A rendeléskor ismert feature-ök:
distance_km, priority_flag,
basket_minor, quantity_sum,
account_age_days, segment,
region, acquisition_channel. A cél egy
dokumentált, előre rögzített összehasonlító kísérlet: három modellcsalád
közül választunk, nem utólag a legszebb számot keresve.
Induló becslés a megoldás előtt: a nullmodell, amely minden
rendelésre a tanítóbeli pozitív arányt adja, a lehető legegyszerűbb
baseline. Ha a tanító target_late aránya például 22%, akkor
az entrópiás nullmodell log loss-a körülbelül 0,53. Azt várjuk, hogy a
távolság és a kosárérték legalább enyhe jelet ad, ezért a validációs log
loss a baseline alá kerül; ugyanakkor a 2000-nél kisebb tanítóhalmaz
miatt a faensemble-ek varianciája már számít. Ennél pontosabb, nem mért
jóslatot nem állíthatunk: a számokat a helyi futtatás hozza létre.
Az induktív torzítás (inductive bias) az a feltevéskészlet, amelyet egy modellcsalád akkor is alkalmaz, amikor a minta nem határozza meg egyértelműen a szabályt. Nem hiba, hanem a tanulás előfeltétele: az összes lehetséges osztályozó közül valahogyan szűkíteni kell.
Kis ellenpélda: legyen két input, , és (XOR). Négy pont:
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
A lineáris logisztikus modell legjobb becslése minden pontra , mert nincs olyan egyenes, amely a XOR-pontokat szétválasztja. Ennek log loss-a . Egy döntési fa viszont két tengelypárhuzamos vágással tökéletesen leírja a szabályt, log loss-a 0 lesz. Ez kizárólag az induktív torzítás különbségét mutatja: a lineáris modell sima, korlátos határt feltételez; a fa darabonként konstans, tengelypárhuzamos szabályokat.
A három jelölt család torzítása:
HistGradientBoostingClassifier): additívan, kis
lépésenként új fákat illeszt a maradékhibára; erős nemlineáris közelítő,
de nagyobb kapacitása miatt kisebb mintán hajlamosabb a
túlillesztésre.A Távolság, kernel és méretkorlát alfejezetben látott kNN ezzel szemben lokális egyenletességet feltételez: a közeli pontok címkéi hasonlók. Ez a torzítás nagy dimenzióban és rossz skálázás mellett drámaian gyengülhet; az ott tanult skálázási követelményt itt a lineáris modellnél is megőrizzük.
A választás mögött ugyanaz a kapacitás–variancia feszültség áll, amelyet a Sajátérték, SVD és dimenziócsökkentés előképe irányfelbontásnál és a Távolság, kernel és méretkorlát méretkorlátjánál már érintettünk. Négyzetes veszteségre egy rögzített feature-vektor mellett:
Jelölések: a zajos cél, várható értéke 0, varianciája ; a tanítómintára illesztett modell becslése az pontban; a várható érték az mintavételi eloszlására vonatkozik. Minden tag skalár egy rögzített -re. A három tag rendre: torzításnégyzet, variancia, irreducibilis zaj. Bináris osztályozásban a log loss-ra nincs ugyanez a tiszta felbontás, de a logika irányadó: nagyobb kapacitás csökkentheti a torzítást, miközben növeli a varianciát; kis minta a varianciát bünteti.
A nullmodell torzítása nagy, varianciája kicsi. A 100–200 fás ensemble kis mintán már érezhető varianciát vihet a validációs log loss-ba, különösen akkor, ha a validhalmaz kicsi.
A jegyzőkönyv a következő hat dimenziót fagyasztja be, még mielőtt bárki megnézné a számokat.
HistGradientBoostingClassifier a főúton van; a külső
XGBoost, LightGBM vagy CatBoost külön függőséget, buildelési és
verziókövetési költséget jelent. Opcionális alternatíva lehet, de nem
kötelező a döntéshez.predict_proba-ja általában jobban kalibrált; a
faensemble-ek gyakran szélsőséges 0/1 közeli értékeket adnak. Ha a késés
valószínűségét közvetlenül üzleti küszöbhöz kötjük, kalibrációs görbe
vagy CalibratedClassifierCV szükséges.decision_protocol:
target: target_late
observation_unit: order_id
prediction_time_ref: order_ts
split:
test_cutoff: "2025-05-01T00:00:00+00:00"
valid_fraction_of_train: 0.2
purge_rule: train_cimket_cutoff_elott_ismerni_kell
baseline: prior_model
primary_metric: valid_log_loss
guardrail_metric: valid_roc_auc
latency_gate_p95_ms: 10
acceptance_rules:
- valid_log_loss <= 0.95 * baseline_log_loss
- valid_roc_auc >= 0.5
- p95_predict_ms <= 10
tie_break:
- prefer lower valid_log_loss
- if diff < 0.005: prefer lower p95_predict_ms
- if still close: prefer stronger interpretability
candidates: [logistic, rf, hgb]
forbidden_test_use: trueKét munkahelyi mozzanat fontos. Először: a validhalmaz a cutoff előtt
címkével rendelkező rekordok késői időszakából jön; a tanítás címkéit
ezen belül is a validáció kezdetén ismert címkékre korlátozzuk. Ez
retrospektív modellszelekció, nem teljes előrecsúszó
backtest. A validációs méréskor a validcímkéket utólag használjuk, a
tesztet nem nézzük meg a választás előtt. Másodszor: a három jelöltet
azonos validhalmazon, azonos előfeldolgozási szerződéssel, azonos
seeddel futtatjuk. Azt mérjük, hogy a modellcsalád hogyan teljesít az
adott feladaton, nem katalógusígéreteket. A p95-kapuhoz azonos gépen,
bemelegítés után külön-külön mérjük az egyrekordos
predict_proba hívások teljes idejét, beleértve a pipeline
előfeldolgozását. A batch-átbocsátást külön metrikaként érdemes
rögzíteni.
A következő példafájl helye lab/model_protocol.py; a
repó gyökeréből python lab/model_protocol.py paranccsal
indítható (telepített függőségek mellett). Az adatútvonalak a szkript
helyéhez képest feloldott abszolút útvonalak, ezért nem függnek az
indításkori munkakönyvtártól. A kimenet környezetfüggő; az automatikus,
teljes adatkészletes ellenőrzés a kapcsolódó példaprojekt feladata.
# lab/model_protocol.py — modellcsalád-választási jegyzőkönyv
import time
from pathlib import Path
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
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
ROOT = Path(__file__).resolve().parent
CUTOFF = pd.Timestamp("2025-05-01T00:00:00+00:00")
orders = pd.read_csv(
ROOT / "data" / "orders.csv",
parse_dates=["order_ts", "actual_delivery_at", "label_available_at"],
)
customers = pd.read_csv(
ROOT / "data" / "customers.csv",
parse_dates=["signup_date"],
)
df = orders.merge(
customers[
["customer_id", "signup_date", "segment", "region", "acquisition_channel"]
],
on="customer_id",
)
df["account_age_days"] = (df["order_ts"] - df["signup_date"]).dt.days
FEATURES = [
"distance_km", "priority_flag", "basket_minor", "quantity_sum",
"account_age_days", "segment", "region", "acquisition_channel",
]
TARGET = "target_late"
FORBIDDEN = {
"delivery_days", "actual_delivery_at", "label_available_at",
"order_id", "customer_id",
}
assert set(FEATURES).isdisjoint(FORBIDDEN), "tiltott oszlop került a feature-ök közé"
# Retrospektív validáció a CUTOFF előtt címkével már rendelkező rekordokon.
# A validáció kezdő időpontjánál a train-címkéknek is rendelkezésre kell állniuk.
train_all = df[(df["order_ts"] < CUTOFF) & (df["label_available_at"] < CUTOFF)].copy()
test = df[df["order_ts"] >= CUTOFF].copy()
purged = df[(df["order_ts"] < CUTOFF) & (df["label_available_at"] >= CUTOFF)]
print(f"train_all={len(train_all)} purged={len(purged)} test={len(test)}")
if train_all.empty:
raise ValueError("Nincs a teszt-cutoff előtt ismert címkéjű rekord")
valid_cutoff = train_all["order_ts"].quantile(0.8)
train = train_all[
(train_all["order_ts"] < valid_cutoff)
& (train_all["label_available_at"] < valid_cutoff)
].copy()
valid = train_all[train_all["order_ts"] >= valid_cutoff].copy()
print(f"train={len(train)} valid={len(valid)}")
if train.empty or valid.empty:
raise ValueError("Üres train/valid halmaz; ellenőrizd az idő- és címkecutoffot")
if train[TARGET].nunique() < 2 or valid[TARGET].nunique() < 2:
raise ValueError("AUC-hoz és modellillesztéshez mindkét osztálynak jelen kell lennie")
base_p = float(np.clip(train[TARGET].mean(), 1e-12, 1 - 1e-12))
baseline_p = np.full(len(valid), base_p, dtype=float)
baseline_log_loss = log_loss(valid[TARGET], baseline_p, labels=[0, 1])
gate = 0.95 * baseline_log_loss
print(f"baseline_log_loss={baseline_log_loss:.4f} gate={gate:.4f}")
CAT_COLS = ["segment", "region", "acquisition_channel"]
NUM_COLS = ["distance_km", "priority_flag", "basket_minor", "quantity_sum", "account_age_days"]
def build(name: str) -> Pipeline:
if name == "logistic":
prep = ColumnTransformer([
("num", StandardScaler(), NUM_COLS),
("cat", OneHotEncoder(handle_unknown="ignore"), CAT_COLS),
])
model = LogisticRegression(max_iter=1000, C=1.0)
elif name == "rf":
prep = ColumnTransformer([
("num", "passthrough", NUM_COLS),
("cat", OneHotEncoder(handle_unknown="ignore"), CAT_COLS),
])
model = RandomForestClassifier(
n_estimators=200, max_depth=8, min_samples_leaf=5,
random_state=20261020, n_jobs=-1,
)
elif name == "hgb":
prep = ColumnTransformer([
("num", "passthrough", NUM_COLS),
("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), CAT_COLS),
])
model = HistGradientBoostingClassifier(
max_iter=100, learning_rate=0.05, max_depth=3,
l2_regularization=0.1, random_state=20261020,
)
else:
raise ValueError(name)
return Pipeline([("prep", prep), ("model", model)])
rows = []
for name in ["logistic", "rf", "hgb"]:
pipe = build(name)
t0 = time.perf_counter()
pipe.fit(train[FEATURES], train[TARGET])
fit_s = time.perf_counter() - t0
# Egyrekordos end-to-end latency (nem batch-idő / elemszám).
# Bemelegítés után külön mérünk legalább 100 egyrekordos hívást.
if valid.empty:
raise ValueError("Üres validációs halmaz: latency és metrika nem számolható")
probe = valid[FEATURES].iloc[[0]]
for _ in range(10):
pipe.predict_proba(probe)
lat = []
for i in range(100):
one_row = valid[FEATURES].iloc[[i % len(valid)]]
t0 = time.perf_counter()
pipe.predict_proba(one_row)
lat.append((time.perf_counter() - t0) * 1000)
p95_ms = float(np.percentile(lat, 95))
# A batch throughput külön mérendő; nem helyettesíti a p95 latency-t.
p = pipe.predict_proba(valid[FEATURES])[:, 1]
ll = log_loss(valid[TARGET], p)
auc = roc_auc_score(valid[TARGET], p)
rows.append((name, fit_s, p95_ms, ll, auc))
print(
f"{name:9s} fit_s={fit_s:6.2f} p95_ms={p95_ms:7.3f} "
f"valid_log_loss={ll:.4f} valid_auc={auc:.4f}"
)
accepted = [
row for row in rows
if row[3] <= gate and row[4] >= 0.5 and row[2] <= 10.0
]
if not accepted:
decision = "baseline"
else:
# először valid_log_loss, majd p95 latency szerint rendezünk
accepted.sort(key=lambda row: (row[3], row[2]))
decision = accepted[0][0]
print(f"decision={decision}")Kézi kontroll: ha a validációs halmaz pozitív aránya éppen megegyezik a tanítóhalmaz arányával, a konstans baseline log loss-a . Eltérő validációs prevalencia mellett a tényleges log loss más: ezért a kaput a rögzített tanítóarányból előállított valószínűségekkel, a validációs címkéken számítjuk. A XOR-példa másik független kontrollja: a mindig modellt adó osztályozó log loss-a ; ezt a log loss definíciójából kézzel megkapjuk.
A log loss definíciója a validhalmazra:
ahol az -edik rendelés megfigyelt késésjelzője, a modell becsült pozitív valószínűsége; a bemeneti tömbök alakja , az eredmény skalár. A numerikusan instabil lenne; a gyakorlati implementációk ezért kis epszilonnal vágják a valószínűséget. A validációs log loss nem vihető át automatikusan más időszakra: azt az adott validablakra, a modellépítés pillanatában mértük.
Három azonnali diagnosztikai szabály:
A kód által kiírt decision tehát az előre rögzített
kapuból következik; a tesztet legfeljebb a kiválasztott modell egyszeri
jelentéséhez nyitjuk meg, és akkor is beírjuk a jegyzőkönyvbe a döntés
időpontjában ismeretlen státuszt.
A három modellcsalád összehasonlításához előzetesen különítsük el a hiperparaméter-választásra használt validációt a lezárt teszttől; egyetlen eredménykülönbségből ne következtessünk általános modellfölényre.
lab/model_protocol.py fájlt a repó gyökeréből
(python lab/model_protocol.py), a
lab/data/orders.csv és lab/data/customers.csv
bemenetekkel. Rögzítse a kiírt train/valid/test méreteket, a három
modell valid log loss-át, egyrekordos p95 predikciós idejét és a végső
decision értékét. Írja le a futtató környezet verzióját és
a mérési parancsot.uncertainty és primary_metric sorát?valid_auc >= 0.5
(az elutasított jelöltekre ez nem garantált); a
baseline_log_loss pozitív és az adott trainarányból kézzel
visszaszámolható; a train + valid legfeljebb
train_all mérete (a valid kezdete előtt még meg nem
érkezett címkék kieshetnek). A konkrét számjegyek a géptől és a
könyvtárverziótól függenek; elfogadható numerikus tolerancia a log
loss-ban 0,005, a p95 ms környezet- és terhelésfüggő, ezért ismételt
mérési sorozatok szükségesek a stabil döntéshez.delivery_days vagy
actual_delivery_at bekerült a feature-ök közé, esetleg a
validhalmazt a tesztidőszakból vágták, és a modell a jövőben ismertté
váló címkét látta. Két ellenőrzés: (a)
assert set(FEATURES).isdisjoint(FORBIDDEN), ahogy a kód
elején; (b)
pd.merge(train, test, on="order_id", how="inner") üres
halmaz kell legyen.pickle.dumps(pipe).__len__() minden jelöltre; (b) a fenti
bemelegített, 100 egyrekordos hívásból számított p95 mérés; (c) a valid
log loss változását a 15 ms-os korlátot sértő modellek kizárása után. A
döntés innen a jegyzőkönyv tie_break-jét követi.uncertainty
szakaszba kalibrációs görbe vagy CalibratedClassifierCV
kerül. A kódban ehhez brier_score_loss(valid[TARGET], p) és
a kalibrációs görbe meredeksége illeszkedne.„A random forest mindig jobb, mert a legnépszerűbb eszköz.” Ez a tévedés az eszközhírnevet összekeveri az adott adatra mért teljesítménnyel. Ellenpélda: képzeljünk el 120 rendelést, ahol a késés főként a távolság monoton függvénye, de erős zajjal. A default 200 fás, mélységi regularizálás nélküli erdő a kis mintán túlilleszt; a valid log loss 0,68, míg a lineáris modellé 0,58. A számok szemléltetőek, nem e könyv által futtatott mérés – de pontosan azt mutatják, hogy a népszerűség nem ad felmentést a Predikciós szerződés és hibateszt szerződésben rögzített teszt alól. A Kísérlet eredményének tudományos közlése jelentésszemlélete ugyanezt kéri: a módszer neve nem bizonyíték, a mért hatás és a design korlátja az.
A modellcsalád választása előre rögzített tudományos jegyzőkönyv, nem utólagos esztétikai döntés. A jegyzőkönyv rögzíti a célt, a validhalmazt, a baseline-t, a log loss elsődleges metrikát, az AUC védőkorlátot, a p95 latency kaput és a döntési szabályt. A három jelölt – logisztikus regresszió, random forest és hisztogramos gradiens boosting – eltérő induktív torzítást, kapacitást és értelmezhetőséget képvisel; a választást a mért valid log loss és a latency hozza, a tesztet kizárólag a kiválasztott modell egyszeri jelentéséhez használjuk. A következő egység a Hibamértékből döntési cél hibamértékből döntési céllá alakításával pontosítja ezt a jegyzőkönyvet.
További olvasmány és elsődleges dokumentáció: [SklearnHGB] scikit-learn: Gradient Boosting and Categorical Features, https://scikit-learn.org/stable/modules/ensemble.html (hozzáférés: 2026-10-21).