21.3 Baseline és egyetlen teljes kísérlet

21.3.1 Cél és konkrét helyzet

A Northstar prediktív projekt kérdése: egy rendelés pillanatában meg tudjuk-e becsülni, hogy a kézbesítés a 7 napos ígéretnél később érkezik? A megfigyelési egység egy rendelés; a célváltozó target_late, amely 1, ha delivery_days > promised_days, egyébként 0. Bemenet a rendeléskor ténylegesen ismert feature-halmaz: distance_km, priority_flag, basket_minor, quantity_sum, account_age_days, segment, region, acquisition_channel. A prediktorok közé tilos bevenni a delivery_days, actual_delivery_at, label_available_at oszlopokat, mert azok a predikció pillanatában még nem ismertek.

Ebben az alfejezetben egyetlen, jól naplózott kísérletet építünk: egy üres referenciamodellt (DummyClassifier) és egy logisztikus lineáris baseline-t futtatunk ugyanazon a train/test spliten. Az eredmény egy artifact: a modell, a tesztpredikciók és egy manifest, amely a feltételeket és a mérőszámokat rögzíti.

Mielőtt a kódot megnézné, tegyen előzetes becslést: mit vár a dummy modell ROC AUC-jától, és mit vár a logisztikus baseline-tól ugyanezen a metrikán? A válasz a fogalmi rész után ellenőrizhető.

21.3.2 Fogalmi magyarázat: baseline, pontozás és a logisztikus veszteség

21.3.2.1 DummyClassifier és DummyRegressor

A dummy osztályozó (dummy classifier) olyan modell, amely nem használja a magyarázó változókat, csak a célváltozó tanítóbeli eloszlásából készít predikciót. A scikit-learn DummyClassifier osztályának több stratégiája van:

A regressziós párja a DummyRegressor, amely tipikusan a tanító átlagát, mediánját vagy egy kvantilist jósolja. A mi célunk bináris osztályozás, ezért a prior stratégia a helyes probabilisztikus baseline: minden tesztbeli sorra ugyanazt a πtrain\pi_{\text{train}} valószínűséget adja, ahol

πtrain=1ntrain∑i∈trainyi, \pi_{\text{train}} = \frac{1}{n_{\text{train}}}\sum_{i \in \text{train}} y_i,

yi∈{0,1}y_i \in \{0,1\} a tanítóbeli cél, ntrainn_{\text{train}} a tanítósorok száma. A πtrain\pi_{\text{train}} értéke a [0,1][0,1] intervallumba esik, mértékegysége nincs.

A dummy modell értelme: ez az a teljesítmény, amit feature-ek nélkül, pusztán a késési arány ismeretében el lehet érni. Ha egy bonyolult modell nem tudja felülmúlni, akkor nincs bizonyíték arra, hogy a feature-ök hasznos jelet hordoznak.

21.3.2.2 A logisztikus baseline matematikája

A logisztikus regresszió a statisztikai modellekből ismert logit kapcsolatot használja. Legyen pip_i annak a valószínűsége, hogy a ii-edik rendelés késik. A modell lineáris prediktora

zi=β0+β1xi1+β2xi2+…+βdxid, z_i = \beta_0 + \beta_1 x_{i1} + \beta_2 x_{i2} + \dots + \beta_d x_{id},

ahol xijx_{ij} a jj-edik feature, βj\beta_j a hozzá tartozó paraméter. A ziz_i lineáris prediktor valós szám, tehát önmagában nem valószínűség. A logisztikus függvény (logistic function, szigmoid) viszi át a [0,1][0,1] intervallumba:

pi=σ(zi)=11+e−zi. p_i = \sigma(z_i) = \frac{1}{1 + e^{-z_i}}.

A függvény alakja:

Logisztikus függvény - Számított függvénypélda, nem mérés.

A függvény inverze a logit: logit(p)=lnp1−p\operatorname{logit}(p)=\ln\frac{p}{1-p}. A modell tehát azt mondja, hogy a késés log-odds-a lineáris a feature-ökben. Ez ugyanaz a gondolat, mint a Diagnosztika és robusztus standard error alfejezetben tárgyalt OLS lineáris prediktor, de itt a cél nem folytonos, hanem bináris, és a linkfüggvény biztosítja a megengedett tartományt.

A paramétereket a bináris keresztentrópia (binary cross-entropy), más néven logisztikus veszteség minimalizálásával becsüljük:

L=−1n∑i=1n[yilnpi+(1−yi)ln(1−pi)], L = -\frac{1}{n}\sum_{i=1}^n \left[ y_i \ln p_i + (1-y_i)\ln(1-p_i) \right],

ahol yi∈{0,1}y_i \in \{0,1\} a tényleges osztály, pi∈(0,1)p_i \in (0,1) a predikált pozitív valószínűség, nn a sorok száma. A yy és pp vektor alakja is (n,)(n,); a veszteség mértékegysége a természetes logaritmus miatt „nat”, de a gyakorlatban egység nélküli mutatóként kezeljük. A formula a negatív log-likelihood átlaga: ha yi=1y_i=1, akkor a lnpi\ln p_i tag marad, ha yi=0y_i=0, akkor a ln(1−pi)\ln(1-p_i) tag.

Kézi kontroll. Legyen három megfigyelés: y=(1,0,1)y=(1,0,1) és p=(0.7,0.3,0.9)p=(0.7,0.3,0.9). A veszteség:

L=−13[ln0.7+ln(1−0.3)+ln0.9]=−13[ln0.7+ln0.7+ln0.9]≈0.2729. L = -\frac{1}{3}\left[\ln 0.7 + \ln(1-0.3) + \ln 0.9\right] = -\frac{1}{3}\left[\ln 0.7 + \ln 0.7 + \ln 0.9\right] \approx 0.2729.

Figyelje meg: a magabiztos tévedést a logaritmus erősen bünteti. Ha például yi=1y_i=1, de pi=0.01p_i=0.01, akkor −ln(0.01)≈4.605-\ln(0.01)\approx 4.605, míg ha pi=0.5p_i=0.5, csak −ln(0.5)≈0.693-\ln(0.5)\approx 0.693.

21.3.2.3 random_state: mit rögzít és mit nem

A random_state egy pszeudovéletlen generátor seedje. Nem teszi a modellt jobbá, és nem bizonyítja az általánosítást; csak reprodukálhatóvá teszi az olyan lépéseket, amelyek véletlent használnak, például a DummyClassifier(strategy="stratified")-et vagy egyes szolverek belső inicializálását. A numerikus reprodukálhatóság nem azonos a környezeti reprodukálhatósággal: ugyanaz a seed nem garantál bitazonos eredményt más platformon, BLAS- vagy pandas-verzión. A scriptben ezért a seedet a manifestben rögzítjük, de az eredményt a tényleges környezetben mért szám jelenti.

A logisztikus regresszió szolverje iteratív optimalizálást végez, hasonlóan a Határérték mint közelítési folyamat alfejezetben tárgyalt közelítési folyamatokhoz. A max_iter azt szabályozza, hány iterációt engedünk; ha a modell nem konvergál, figyelmeztetést kapunk. Az alábbi kódban max_iter=1000, és a futás után ellenőrizhető a n_iter_ értéke.

21.3.2.4 scoring: milyen metrikát használjunk?

A scoring a modellválasztás célfüggvénye: az a mérőszám, amely alapján két modellt összehasonlítunk. A késési cél ritka lehet, ezért a puszta accuracy félrevezető: ha a késési arány 10%, egy „mindig nem késik” modell 90%-os accuracy-t ér el, de semmilyen döntési értéke nincs. Ezért két metrikát használunk:

A scoring megválasztása tudományos döntés: az AUC küszöbfüggetlen, de nem mondja meg, hogy a predikált valószínűség kalibrált-e. A log loss ezt is bünteti, viszont egyetlen rossz, nagyon magabiztos predikció nagy hibát okoz. A kísérletben mindkettőt jelentjük, de a modellválasztás elsődleges mutatója az AUC.

21.3.2.5 artifact: a kísérlet lenyomata

Az artifact a futtatás eredményterméke, nem csak a modellfájl. Az alábbi kísérlet három fájlt hoz létre:

A manifest azért fontos, mert a modellfájl önmagában nem mondja el, milyen spliten és milyen környezetben született a szám. A saját, megbízható forrásból származó joblib fájlt betölthetjük; ismeretlen fájlt nem.

21.3.3 Teljes minimális munkapélda

A következő script a lab/ munkakönyvtárból futtatható, ahol az orders.csv és customers.csv megtalálható. Először a splitet reprodukáljuk a Preprocessing tanulás kizárólag a training részen alfejezetben tanult időbeli szerződés szerint: tanítóba azok a rendelések kerülnek, amelyek order_ts < cutoff és label_available_at < cutoff; tesztbe azok, ahol order_ts >= cutoff. A köztes, kihagyott határsávot kihagyjuk.

# baseline_experiment.py — futtatandó a lab/ munkakönyvtárból
import json
import hashlib
from datetime import datetime, timezone
from pathlib import Path

import joblib
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
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

EXPERIMENT_SEED = 20261021
CUTOFF = pd.Timestamp("2025-05-01T00:00:00+00:00")
TARGET = "target_late"

orders = pd.read_csv(
    "orders.csv",
    parse_dates=["order_ts", "label_available_at"],
)
customers = pd.read_csv("customers.csv", parse_dates=["signup_date"])
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

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

train_mask = (df["order_ts"] < CUTOFF) & (df["label_available_at"] < CUTOFF)
test_mask = df["order_ts"] >= CUTOFF

X_train = df.loc[train_mask, feature_cols]
y_train = df.loc[train_mask, TARGET]
X_test = df.loc[test_mask, feature_cols]
y_test = df.loc[test_mask, TARGET]
test_ids = df.loc[test_mask, "order_id"].to_frame()

if y_test.nunique() < 2:
    raise ValueError("A teszt céloszlop csak egy osztályt tartalmaz; ROC AUC nem értelmezhető.")

preprocessor = ColumnTransformer(
    [
        ("numeric", Pipeline([
            ("impute", SimpleImputer(strategy="median")),
            ("scale", StandardScaler()),
        ]), numeric_cols),
        ("categorical", Pipeline([
            ("impute", SimpleImputer(strategy="most_frequent")),
            ("encode", OneHotEncoder(handle_unknown="ignore")),
        ]), categorical_cols),
    ]
)

dummy = DummyClassifier(strategy="prior")
dummy.fit(X_train, y_train)
dummy_prob = dummy.predict_proba(X_test)[:, 1]

clf = Pipeline([
    ("prep", preprocessor),
    ("model", LogisticRegression(max_iter=1000, random_state=EXPERIMENT_SEED)),
])
clf.fit(X_train, y_train)
clf_prob = clf.predict_proba(X_test)[:, 1]

def evaluate(name, prob):
    auc = roc_auc_score(y_test, prob)
    ll = log_loss(y_test, prob)
    print(f"{name:>10}: ROC AUC={auc:.4f}, log loss={ll:.4f}")
    return {"roc_auc": round(auc, 4), "log_loss": round(ll, 4)}

print(f"train positive rate: {y_train.mean():.4f}")
print("iterations:", clf.named_steps["model"].n_iter_)
results = {}
results["dummy_prior"] = evaluate("dummy", dummy_prob)
results["logistic"] = evaluate("logistic", clf_prob)

artifact_dir = Path("artifacts") / "s083_baseline"
artifact_dir.mkdir(parents=True, exist_ok=True)
model_path = artifact_dir / "model.joblib"
joblib.dump(clf, model_path)

test_predictions = test_ids.copy()
test_predictions[TARGET] = y_test
test_predictions["prob_late"] = clf_prob
test_predictions.to_csv(artifact_dir / "test_predictions.csv", index=False)

manifest = {
    "created_at_utc": datetime.now(timezone.utc).isoformat(),
    "experiment_seed": EXPERIMENT_SEED,
    "cutoff": CUTOFF.isoformat(),
    "sklearn_version": sklearn.__version__,
    "pandas_version": pd.__version__,
    "n_train": int(train_mask.sum()),
    "n_test": int(test_mask.sum()),
    "n_purged": int((~train_mask & ~test_mask).sum()),
    "features": feature_cols,
    "model_file": model_path.name,
    "model_sha256": hashlib.sha256(model_path.read_bytes()).hexdigest(),
    "results": results,
}
(artifact_dir / "manifest.json").write_text(
    json.dumps(manifest, indent=2), encoding="utf-8"
)
print(json.dumps(manifest, indent=2))

A kimenet szemléltető formája — ezt a szöveget író modell nem futtatta, tehát a tényleges számok az Ön helyi futtatásából származnak:

train positive rate: 0.1847
iterations: [17]
dummy: ROC AUC=0.5000, log loss=0.4782
logistic: ROC AUC=0.6513, log loss=0.4590
{
  "experiment_seed": 20261021,
  "cutoff": "2025-05-01T00:00:00+00:00",
  ...
  "results": {
    "dummy_prior": {"roc_auc": 0.5, "log_loss": 0.4782},
    "logistic": {"roc_auc": 0.6513, "log_loss": 0.459}
  }
}

21.3.4 Eredmény értelmezése

A futás után az alábbi invariánsokat ellenőrizheti:

Ldummy≈−πtestlnπtrain−(1−πtest)ln(1−πtrain). L_{\text{dummy}} \approx -\pi_{\text{test}}\ln \pi_{\text{train}} - (1-\pi_{\text{test}})\ln(1-\pi_{\text{train}}).

Ha a tesztarány eltér a train-priortól, a dummy nem az optimális konstans predikció a teszten; ez is mutatja, miért nem szabad a tesztarányt használni a baseline-hoz.

Ha a logisztikus AUC 0.5 alatt vagy nagyon közel 0.5-höz van, az még nem bizonyítja, hogy a feladat megoldhatatlan; előbb a 3. feladat hibalehetőségeit kell kizárni.

A DummyClassifier csak referencia; a végső tesztpontszámot nem szabad ismételt modellválasztásra használni. A pozitív osztály és a predict_proba oszlopsorrendje classes_ alapján ellenőrzendő.

21.3.5 Feladatok

1. Kézi log loss és dummy várható veszteség. Adott y=(1,0,1)y=(1,0,1) és p=(0.7,0.3,0.9)p=(0.7,0.3,0.9). Számolja ki a bináris log loss-t három tizedesjegyre. Ezután legyen a train-prior πtrain=0.2\pi_{\text{train}}=0.2 és a tesztbeli pozitív arány πtest=0.3\pi_{\text{test}}=0.3. Számolja ki a dummy prior várható log loss-át, és hasonlítsa össze a tesztarányhoz tartozó H(πtest)=−πtestlnπtest−(1−πtest)ln(1−πtest)H(\pi_{\text{test}})=-\pi_{\text{test}}\ln\pi_{\text{test}}-(1-\pi_{\text{test}})\ln(1-\pi_{\text{test}}) entrópiával. Miért nagyobb a dummy várható vesztesége az entrópiánál?

2. Implementálás: futtatás és manifest-ellenőrzés. Másolja a fenti scriptet baseline_experiment.py néven a lab/ munkakönyvtárba, és futtassa python baseline_experiment.py paranccsal. Ellenőrizze: a dummy AUC értéke 0.5000 tolerancián belül, a test_predictions.csv sorainak száma egyenlő a manifest n_test értékével, és a model_sha256 megegyezik a futtatás után a fájlra újraszámolt hash-sel. Jegyezze fel a tényleges számokat a munkanaplójába.

3. Hibakeresés: felcserélt valószínűségi oszlop. Egy kolléga a fenti scriptben ezt a sort használta:

clf_prob = clf.predict_proba(X_test)[:, 0]

A kapott logisztikus AUC 0.41 lett, a dummy AUC 0.50 maradt. Mi a hiba? Adjon olyan ellenőrzést, amely a clf.classes_ alapján kideríti, hogy melyik oszlop a pozitív osztály valószínűsége, és írja le a javítást.

4. Megváltozott feltétel: most_frequent stratégia. Cserélje ki a dummy stratégiát DummyClassifier(strategy="most_frequent")-re, futtassa újra, és értelmezze a log loss értékét. Miért nem alkalmas a most_frequent stratégia a valószínűségi pontszámot igénylő AUC/log loss összehasonlításra? Mi lenne a helyes baseline-választás, ha a döntési cél accuracy lenne?

21.3.6 Megoldások és indoklás

1. megoldás. Az első három tag:

−ln0.7≈0.3567,−ln(1−0.3)=−ln0.7≈0.3567,−ln0.9≈0.1054. -\ln 0.7 \approx 0.3567,\qquad -\ln(1-0.3) = -\ln 0.7 \approx 0.3567,\qquad -\ln 0.9 \approx 0.1054.

Az átlaguk (0.3567+0.3567+0.1054)/3≈0.2729(0.3567+0.3567+0.1054)/3 \approx 0.2729. A dummy prior várható vesztesége:

Ldummy=−0.3ln0.2−0.7ln0.8≈0.3⋅1.6094+0.7⋅0.2231≈0.6390. L_{\text{dummy}} = -0.3\ln 0.2 - 0.7\ln 0.8 \approx 0.3\cdot 1.6094 + 0.7\cdot 0.2231 \approx 0.6390.

A tesztarányhoz tartozó entrópia:

H(0.3)=−0.3ln0.3−0.7ln0.7≈0.3⋅1.2040+0.7⋅0.3567≈0.6109. H(0.3) = -0.3\ln 0.3 - 0.7\ln 0.7 \approx 0.3\cdot 1.2040 + 0.7\cdot 0.3567 \approx 0.6109.

A dummy veszteség nagyobb, mert a train-prior 0.2, miközben a tesztarány 0.3; a dummy nem használhatja a tesztarányt, ezért a tesztarány eltolódása többletveszteséget okoz. Tolerancia: ±0.001\pm 0.001 a kerekítések miatt.

2. megoldás. A konkrét AUC és log loss értékek a futtató géptől és a csomagverzióktól függnek, ezért a megoldás az ellenőrző invariantokban van. A dummy AUC-nak |0.5000−auc|<10−6|0.5000 - \text{auc}| < 10^{-6}-nak kell lennie; ha nem, a split vagy a predict_proba oszlopválasztás hibás. A test_predictions.csv sorainak száma pontosan a manifest n_test értéke. A hash ellenőrzése Pythonból:

import hashlib
print(hashlib.sha256(Path("artifacts/s083_baseline/model.joblib").read_bytes()).hexdigest())

Ha a logisztikus AUC nem haladja meg a dummy 0.5-öt, előbb nézze meg a 3. feladat diagnózisát, és ellenőrizze, hogy a preprocessing tényleg csak a tanítórészen lett illesztve.

3. megoldás. A predict_proba két oszlopot ad: az első a classes_[0] osztály valószínűsége, a második a classes_[1] osztályé. Mivel a cél osztályai [0,1], a [:,0] a „nem késik” valószínűségét jelenti. Ha ezt adjuk pozitív pontszámként a ROC-hoz, a jó modell sorrendjét megfordítjuk; a kapott AUC körülbelül 1−eredeti AUC1 - \text{eredeti AUC}, ezért lett 0.41 egy körülbelül 0.59-es modellnél. A javítás:

pos_class = clf.classes_[1]
clf_prob = clf.predict_proba(X_test)[:, 1]
assert pos_class == 1

Az assert a szándékot is rögzíti: ha a célkódolás megváltozna, a hiba azonnal kiderül.

4. megoldás. A most_frequent stratégia a tanító leggyakoribb osztályát adja 1 valószínűséggel, a másikat 0 valószínűséggel. Ha a pozitív osztály a ritkább, akkor minden pozitív valószínűség 0; a log_loss a 0 valószínűséget csak numerikus klipeléssel tudja kezelni, ezért óriási értéket kapunk, az AUC pedig nem hordoz valódi pontszám-információt. Ez a stratégia accuracy-alapú döntéshez való: ha a cél a találati arány maximalizálása, a most_frequent a helyes baseline. A mi scoringunk AUC és log loss, ezért a prior stratégia a megfelelő.

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

A jó tanítómetrika nem bizonyítja az általánosítást. Tegyük fel, hogy a késési arány 10% körüli. A DummyClassifier(strategy="most_frequent") mindig „nem késik”-et mond, ezért a tanító accuracy 0.90 lehet — miközben az AUC 0.5, tehát semmilyen szétválasztó ereje nincs. Ugyanez a csapda a bonyolult modellekkel: a logisztikus vagy faalapú modell tanító AUC-ja akár 0.95 is lehet, de ha a teszt AUC csak 0.53, a modell a tanító zaját tanulta meg, nem a jövőbeli rendeléseken érvényes jelet. Az egyetlen train/test spliten mért tesztmetrika sem feltétlenül végleges becslés: ha a küszöböt vagy a feature-választást a teszten finomítjuk, a teszt is „elszennyeződik”. Az érintetlen teszt és a külön validációs réteg elvét a Tanítás, validáció és érintetlen teszt vezette be.

Egy másik gyakori félreértés: a random_state növelése nem hitelesíti a modellt. Ha öt különböző seeddel futunk, és a legjobb teszt-AUC-t jelentjük, az maga is szelekciós torzítás: a tesztet használtuk modellválasztásra. A seed arra való, hogy egyetlen futás megismételhető legyen, nem arra, hogy a szerencsés futást keressük.

21.3.8 Rövid összefoglaló

A DummyClassifier(strategy="prior") a tanítóbeli késési arányt adja minden sorhoz, és 0.5-ös ROC AUC-t kell adnia; ez az a szint, amelyet minden modellnek meg kell haladnia. A logisztikus baseline lineáris prediktort visz át a logisztikus függvénnyel valószínűséggé, és a bináris keresztentrópiát minimalizálja. A kísérlet akkor korrekt, ha a split a predikció pillanatához igazodik, a preprocessing csak a tanítórészen illeszkedik, és a scoring az AUC-t meg a log loss-t is naplózza. A manifest a futás feltételeit, verzióit és hash-ét rögzíti. A következő alkalmazás, a Predikciós szerződés és hibateszt, ezt a baseline-t predikciós szerződéssé alakítja: bemeneti séma, kimeneti alak és hibateszt.