A feladat a szokásos prediktív kérdés: rendelés pillanatában
becsüljük, hogy a kézbesítés meghaladja-e a 7 napot. A bemenet a
lab/data/orders.csv és a
lab/data/customers.csv; a megfigyelési egység egy rendelés
(order_id), a csoportazonosító a customer_id,
az időablak pedig az order_ts UTC-időbélyeg. A célváltozó a
bináris target_late, a prediktorok a rendelés pillanatában
ismert distance_km, priority_flag,
basket_minor, quantity_sum,
account_age_days, segment,
region, acquisition_channel. Az előzetes
becslés: a Fák, random forest és
gradiensboosting alfejezetben látott HistGradientBoosting időbeli
spliten kapott AUC-ja 0.70 körüli volt; most nem egyetlen scoreszám,
hanem a felosztási stratégia hatása a kérdés. Ez vizsgálandó hipotézis,
nem garantált sorrend: a csoportos és időbeli felosztás akár magasabb
AUC-t is eredményezhet; a különbséget a validáció által szimulált
alkalmazási helyzet szerint kell értelmezni.
A keresztvalidáció (cross-validation) azt kérdezi: milyen jól általánosít a tanulási eljárás olyan adatra, amely nem volt a tanításban? A mintát részre osztjuk; a -adik iterációban a -adik fold a validáció, a többi a tanítás. A modellt a tanítórészen illesztjük, a validációs részen értékeljük, majd átlagolunk:
ahol a választott veszteség vagy negatív score, a kihagyott foldon kívüli adaton tanult modell, a validációs indexhalmaz, annak mérete. A cél az, hogy ez a becslés a valódi jövőbeli használat hibáját közelítse.
A KFold indexek szerinti felosztást készít
(alapesetben keverés nélkül, shuffle=True esetén
véletlenszerűsítve). Független, azonos eloszlású minták mellett hasznos
becslést adhat, de a CV-átlag nem általánosan torzítatlan becslő: a
kisebb foldonkénti tanítóméret és a kiválasztási lépések is
befolyásolhatják. A StratifiedKFold a véletlen
felosztást úgy készíti, hogy minden foldban megközelítőleg azonos
maradjon a célosztályok aránya; ritka eseménynél ez megakadályozza, hogy
egy fold véletlenül nagyon kevés pozitív esetet tartalmazzon. A
GroupKFold a csoportazonosítót figyeli: egy csoport
minden mintája ugyanabba a foldba kerül, így a tanító- és validációs
rész csoporthalmazai diszjunktak:
ahol a foldhoz tartozó csoportazonosítók halmaza. A TimeSeriesSplit az időrendet őrzi: a -adik splitben a tanítóhalmaz minden eleme korábbi, mint a validációs elemek:
A split manifest a felosztás rögzített, verziózott leirata. Ha a foldindexek egy JSON-fájlban vannak, akkor a kísérlet pontosan reprodukálható akkor is, ha a környezetet újraépítjük; a manifest az adatsorra, a stratégiára és a seedre hivatkozik.
A matematikai híd itt a függőség. Ha az egy ügyfélhez tartozó rendelések korrelálnak, akkor egy ügyfél rendeléseinek trainbe és validationbe szétosztása olyan, mintha a modell a tesztidőpontban ismerné az ügyfél válaszainak egy részét; ez optimista AUC-hoz vezet. Az egyes foldok metrikái nem független minták: közös adatokból, átfedő tanítóhalmazokból származnak. Ezért a foldok egyszerű szórása nem a becslés mintavételi szórása. A Parciális derivált és gradiens alfejezetben tanult optimalizálás itt is szerepet kap: minden foldban újraoptimalizáljuk a modell paramétereit, így a CV valójában a tanulási eljárás egészét értékeli, nem egyetlen paramétervektort.
Tegyük fel, hogy hat rendelésünk van, két-két rendelés három ügyféltől:
| Rendelés | Csoport | Idő | Cél |
|---|---|---|---|
| r1 | c1 | jan. | 0 |
| r2 | c1 | márc. | 1 |
| r3 | c2 | febr. | 0 |
| r4 | c2 | ápr. | 0 |
| r5 | c3 | máj. | 1 |
| r6 | c3 | jún. | 1 |
Egy két folddal dolgozó KFold például a {r1, r2, r3, r4}
tanítóhalmazt és {r5, r6} validációt adhat, majd fordítva.
Ekkor c1 és c2 ügyfelek mintái mindkét oldalon
előfordulhatnak. A GroupKFold ezzel szemben egy foldot a
c1, a másikat a c2, c3 csoportokra épít; az
ügyfél soha nem szerepel egyszerre tanítóban és validációban. A
TimeSeriesSplit egy bővülő ablakot ad: első splitnél train
{r1, r2}, validation {r3, r4}; másodiknál
train {r1, r2, r3, r4}, validation {r5, r6}.
Látszik a két tulajdonság: a TimeSeriesSplit a jövőt nem engedi a múlt
tanításába, de azonos ügyfél továbbra is átkerülhet a foldok között.
A következő kód a három stratégiát hasonlítja össze a
Northstar-adaton, a teljes pipeline-t validálva. A preprocessing a
ColumnTransformerben együtt van a modellel, így a
cross_validate minden foldban csak a tanítórészen tanítja a
skálázást és a one-hot kódolást. A kódot a projektgyökérből futtatva a
lab/data/ mappa elérhető.
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import (KFold, StratifiedKFold, GroupKFold,
TimeSeriesSplit, cross_validate)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# 1. Adat betöltése; a szerződés szerint a predikció pillanata order_ts.
orders = pd.read_csv("lab/data/orders.csv", parse_dates=["order_ts"])
customers = pd.read_csv("lab/data/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
features = ["distance_km", "priority_flag", "basket_minor", "quantity_sum",
"account_age_days", "segment", "region", "acquisition_channel"]
target = "target_late"
X = df[features]
y = df[target].astype(int)
groups = df["customer_id"].to_numpy()
preprocessor = ColumnTransformer([
("num", StandardScaler(), ["distance_km", "basket_minor",
"quantity_sum", "account_age_days"]),
("pass", "passthrough", ["priority_flag"]),
("cat", OneHotEncoder(handle_unknown="ignore"),
["segment", "region", "acquisition_channel"]),
])
model = Pipeline([
("prep", preprocessor),
("clf", HistGradientBoostingClassifier(
max_iter=150, learning_rate=0.08, random_state=42)),
])
# 2. Három stratégia, azonos scoring és azonos modell.
scoring = "roc_auc"
cv_choices = {
"kfold": KFold(n_splits=5, shuffle=True, random_state=20261020),
"stratified": StratifiedKFold(n_splits=5, shuffle=True, random_state=20261020),
"group": GroupKFold(n_splits=5),
}
results = {}
for name, cv in cv_choices.items():
kwargs = {"groups": groups} if name == "group" else {}
scores = cross_validate(model, X, y, cv=cv, scoring=scoring,
n_jobs=1, **kwargs)["test_score"]
results[name] = scores
# 3. Időbeli split: az adatot order_ts szerint rendezzük.
df_time = df.sort_values("order_ts")
X_time = df_time[features]
y_time = df_time[target].astype(int)
# Demonstráció: az időrendet megtartja, de önmagában NEM garantálja,
# hogy a tanítócímkék rendelkezésre álltak az adott validáció kezdete előtt.
# Éles értékeléshez minden foldban ellenőrizni kell:
# max(label_available_at[train_idx]) < min(order_ts[val_idx]),
# vagy az adott validáció kezdetéig ismert címkékre szűrt train-indexet használni.
tscv = TimeSeriesSplit(n_splits=5)
scores_time = cross_validate(model, X_time, y_time, cv=tscv,
scoring=scoring, n_jobs=1)["test_score"]
results["time"] = scores_time
for name, scores in results.items():
print(f"{name:10s} AUC folds: {scores.round(3)} "
f"mean={scores.mean():.3f} sd={scores.std(ddof=1):.3f}")A kimenetet szemléltető értékekkel jelölöm, mert ebben az írási módban nem futtattuk a kódot:
kfold AUC folds: [0.71 0.69 0.73 0.68 0.72] mean=0.706 sd=0.019
stratified AUC folds: [0.70 0.71 0.68 0.69 0.73] mean=0.702 sd=0.019
group AUC folds: [0.66 0.64 0.67 0.65 0.63] mean=0.650 sd=0.016
time AUC folds: [0.68 0.61 0.63 0.65 0.60] mean=0.634 sd=0.032
A tényleges számok a labor futtatásából származnak; a sorrend azonban a szerkezetből következik.
A KFold és a StratifiedKFold átlaga várhatóan magasabb, mert
ugyanazon ügyfél korábbi rendelései a tanítóhalmazból szivárogtatnak
információt az ügyfél későbbi rendeléseiről. A GroupKFold ezt kizárja,
ezért alacsonyabb, de az időrendet nem tiszteli: egy ügyfél jövőbeli
rendelése taníthatja a múltbeli rendelésre illesztett modellt. A
TimeSeriesSplit a jövőt nem engedi a múltba, viszont megengedi, hogy
ugyanazon ügyfél rendelései mindkét oldalon szerepeljenek; ez a
kezdőprojekt valós használatában részben elfogadható, ha a modell a
rendelés pillanatában valóban látja az ügyfél előzményét. A tervezett
használatra — jövőbeli rendelések becslése a rendelés pillanatában — a
címkeelérhetőséget is ellenőrző, időalapú felosztás a minimálisan
elfogadható felosztás; a GroupKFolddal való összevetés pedig megmutatja
az ügyfél-átszivárgás mértékét. Ha új ügyfélre is szeretnénk
általánosítani, a kettőt kombináló, időblokkokra osztott csoportsplitet
kell építenünk, amelyet a scikit-learn beépítetten nem ad, de a
foldindexeket generálva egy GroupKFold logikájával és
order_ts szerinti szűréssel előállítható.
Értelmezés. A fenti szemléltető eredményben a TimeSeriesSplit AUC-átlaga a legalacsonyabb. Magyarázza el, miért nem jelenti ez azt, hogy a modell rosszabb lett; mit mér a különbség a KFoldhoz képest?
Implementálás. Adott a df DataFrame
a fenti feature-ökkel, y = df["target_late"],
groups = df["customer_id"]. Írjon kódot, amely
LogisticRegression(max_iter=1000) modellt és
GroupKFold(n_splits=4) felosztást használva
cross_val_score-szal kiszámolja a roc_auc
pontszámokat, majd kiírja az egyes foldok AUC-ját és az
átlagot.
Hibadiagnózis. Az alábbi kódban valaki a foldok szórását konfidenciaintervallumként adja meg. Nevezze meg a két hibát, és javítsa az állítást úgy, hogy a kód kimenete ne állítson statisztikai bizonytalanságot a foldokból.
scores = cross_val_score(model, X, y, cv=5, scoring="roc_auc")
mean = scores.mean()
se = scores.std(ddof=1) / np.sqrt(5)
print(f"95% CI: {mean - 1.96*se:.3f} - {mean + 1.96*se:.3f}")Megváltozott feltétel. A laboradat 300 ügyfelet
tartalmaz, és a predikciót kifejezetten új ügyfelek első rendelésére
kell általánosítani. Hány ügyfél kerül egy
GroupKFold(n_splits=5) validációs foldjába? Milyen
gyakorlati korlátja van ennek a kis csoportszámnak, és milyen két
módosítást javasolna az értékelés megbízhatóságának növelésére?
1. megoldás. A TimeSeriesSplit nem rosszabb modellt mutat, hanem kevésbé optimista feltételek mellett értékel. A KFoldnál azonos ügyfél tanítórendelései segítenek a validációs rendeléseken; ez a valóságban a predikció pillanatában csak akkor lenne elérhető, ha a modell az ügyfél összes korábbi és jövőbeli rendelését ismerné. A két pontszám különbsége önmagában nem a szivárgás mértéke: eltérő tanítóadat-méret, időbeli eltolódás és osztályarány is alakítja. Az ok megállapításához kontrollált összehasonlítás szükséges.
2. megoldás. A kód a cross_val_score
params argumentumában adja át a csoportosítást:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GroupKFold, cross_val_score
model = LogisticRegression(max_iter=1000)
cv = GroupKFold(n_splits=4)
scores = cross_val_score(model, X, y, groups=groups, cv=cv,
scoring="roc_auc")
for i, score in enumerate(scores, start=1):
print(f"fold {i}: {score:.3f}")
print(f"mean AUC: {scores.mean():.3f}")Ellenőrző invariant: minden fold AUC-ja 0 és 1 közé esik, és az átlag a négy fold átlaga; ha a modell konvergens, az érték tipikusan 0.5 fölött van, de a pontos érték a futástól függ.
3. megoldás. Az első hiba: a foldmetrikák nem
függetlenek, mert a tanítóhalmazok átfednek, ezért a
scores.std(ddof=1) nem a mintavételi eloszlás szórása. A
második hiba: a np.sqrt(5)-tel való osztás úgy tenné,
mintha őt független minta lenne, ami hamis. Helyes megfogalmazás: a
foldok az adatfelosztás szerinti teljesítmény stabilitását mutatják, nem
konfidenciaintervallumot. A kód kimenetét ezért például így kell
átírni:
print(f"Fold scores (AUC): {scores.round(3)}")
print(f"Mean: {scores.mean():.3f}, sd across folds: {scores.std(ddof=1):.3f}")
print("A fold sd nem konfidenciaintervallum; a bizonytalansághoz "
"nested CV vagy bootstrap kell a megfelelő mintavételi egységgel.")4. megoldás. GroupKFold(n_splits=5)
esetén egy validációs foldban körülbelül 60 ügyfél van; a konkrét
felosztás ettől kissé eltérhet, mert a csoportokat kell egészben
tartani. A korlát: a validációs AUC így mindössze 60 ügyfél rendeléseire
épül, ami nagy varianciát okoz; a foldok közötti különbség részben a kis
csoportszámból fakad, nem valódi modellkülönbségből. Két módosítás:
végezzünk több, kifejezetten csoportszintű újramintavételezést vagy
független időszakon értékelést; a nagyobb foldszám önmagában nem növeli
a rendelkezésre álló független ügyfelek számát, és a teljesítményt a
csoportszintű bootstrap vagy nested CV segítségével értékeljük, ahol a
külső ciklus a csoportokat bontja.
A Standard error és konfidenciaintervallum
alfejezetben látott standard error a becslő mintavételi eloszlásából
származik, nem egyetlen adathalmaz belső szóródásából. A CV-foldok éppen
ilyen belső szóródást mutatnak: ugyanazokat az adatokat többféleképpen
osztjuk fel, a tanítóhalmazok átfednek, ezért a foldmetrikák
korreláltak. Az sd(ddof=1) kiszámítása még nem rossz, de az
1.96*se/√K képlet alkalmazása hamis precizitást ad. A
GroupKFold és TimeSeriesSplit foldjai ráadásul
eltérő méretűek is lehetnek, ezért a szórásuk még kevésbé értelmezhető
közvetlenül. A helyes bizonytalanságbecsléshez a teljes tanulási
eljárást ismétlő mintavétel kell, például a megfelelő mintavételi
egységre épített bootstrap vagy nested CV.
Miért okoz optimista AUC-t, ha egy ügyfél rendelései a trainben
és a validationben is szerepelnek?
Mert a modell megtanulhatja az ügyfélspecifikus zajt, ami a jövőbeli,
valódi predikciónál nem áll rendelkezésre.
Mikor választunk GroupKFoldot TimeSeriesSplit helyett?
Ha a predikció elsősorban új csoportokra, például új ügyfelekre
történik, és az időbeli sorrend kevésbé hangsúlyos, vagy kombináljuk
mindkettőt.
Mit jelent a split manifest?
A foldindexek rögzített, verziózott leirata, amely a reprodukálhatóságot
biztosítja.
A keresztvalidáció csak akkor becsüli jól a jövőbeli hibát, ha a foldok a valódi használat függőségeit modellezik. A KFold független mintákat feltételez, ezért ügyfél- és időfüggő rendeléseknél optimista; a StratifiedKFold az osztályarányt őrzi. A GroupKFold a csoportot tartja egy oldalon, a TimeSeriesSplit a múltból a jövőbe tanít. A Northstar laborban a TimeSeriesSplit a tervezett rendelés pillanatára vonatkozó predikció minimálisan elfogadható felosztása, de új ügyfélre általánosításhoz csoportos idősplitre van szükség. A foldmetrikák szórása stabilitást mutat, nem konfidenciaintervallumot. Következő lépésként Hiperparaméter és nested értékelés alfejezetben a hiperparaméter-keresés és a nested értékelés kérdését vizsgáljuk.