esettanulmány · AIbet / ML-validáció
Hogyan öltem meg a saját backtestemet
A foci-predikciós modellem backtestje +6,42%-os hozamot mutatott. Egy célzott mérés kimutatta, hogy ez az „él” teljes egészében adatszivárgásból jött: a modell a jövőből is látott adatot. Szivárgás nélkül +0,68% maradt, ami zajszint. Ez a történet arról szól, hogyan derült ki, és mit építettem, hogy ne fordulhasson elő újra.
- backteszt-ROI, szivárgással → nélküle
- +6,42% → +0,68%
- értékelt meccs (walk-forward)
- 17 356
- érintett feature
- 12
- random seed, ugyanaz az eredmény
- 3×
$ cat lelet.md
A modell 60 feature-ből dolgozik. Ebből 12 (labdabirtoklás, lövések/90, védési arány, szabálytalanságok, hazai/vendég/különbség bontásban) egy fbref-táblából jön, ami csapatonként és szezononként egyetlen sort tartalmaz. Ez a sor viszont a teljes szezon összesítése — a szezon végén keletkezik.
row = fbref_df[(fbref_df["Squad"] == team) & (fbref_df["season"] == season)]
if len(row):
return row.iloc[0]Vagyis egy októberi meccs előrejelzésekor a modell már „tudta”, hogyan teljesít a csapat májusig. A gördülő (rolling) statisztikák és a keret-változás feature-ök tiszták voltak — csak ez az egy illesztés szivárgott, és ez elég volt ahhoz, hogy az egész eredmény hamis legyen.
$ ./meres --szivargas=be|ki
A mérés négy változatot hasonlított össze ugyanazon a walk-forward kereten (2018/19–2022/23, szezononként újratanított modell, 17 356 meccs), ugyanazzal a döntési szabállyal, három különböző random seeddel. Kapcsold ki a szivárgást, és nézd, mi marad az élből.
a pontok fölé húzva: seedenkénti érték
+6,42%
V1 — azonos szezonos fbref (szivárgó)
átlag 3 seeden · ~2151 tét · logloss 0,96240
Ezt mutatta a backteszt: stabil, szép, mindhárom seeden megismételhető hozam. Pont ez volt a gyanús benne.
táblázatként
| változat | seed 42 | seed 7 | seed 123 | átlag | tét | logloss |
|---|---|---|---|---|---|---|
| V1 — azonos szezonos fbref (szivárgó) | +6,09% | +6,65% | +6,52% | +6,42% | ~2151 | 0,96240 |
| V2 — előző szezonos fbref (amit az éles rendszer kapott) | -0,75% | -1,28% | -0,54% | -0,86% | ~1325 | 0,96668 |
| V3 — a 12 fbref-feature nélkül | +0,44% | +0,23% | +1,36% | +0,68% | ~1446 | 0,96604 |
| V4 — szezon-eddigi átlagok, csak múltbeli meccsekből | -0,23% | +1,36% | +0,78% | +0,64% | ~1392 | 0,96619 |
A lényeg nem a pontos számokban van — azok ±1%-on belül szórnak —, hanem az előjelváltásban: a szivárgó változat mindhárom seeden +6% fölött van, minden tiszta változat a nulla körül. Ez nem zaj, hanem mechanizmus.
$ cat train_serve_skew.md
A mérés egy másik rejtélyt is megoldott. Élesben a modell rosszabbul teljesített, mint a backtestben, és ezt eddig „modell-zsugorodásnak” gondoltam. Valójában az éles rendszer a folyó szezonra még nem kapott fbref-sort, ezért az előző szezon adatait használta — ez a V2 állapot, −0,86%-kal.
A modellt tehát a V1-ben tanítottam, és a V2-ben futtattam. Ez a klasszikus train/serve skew: nem a modell romlott el élesben, hanem a tanítás hazudott.
$ ls ./vedelem
A javítás nem egy sor törlése volt, hanem olyan szabályok, amik a következő hasonló hibát is elkapják:
xfail(strict=True) teszt
Egy teszt kifejezetten a szivárgást mutatja ki. Amíg a hiba fennáll, „várt hibaként” fut; ha valaki megjavítja, a teszt piros lesz, amíg a jelölést tudatosan le nem veszik. A hiba nem tűnhet el csendben.
kötelező időablak
A főkönyv csak egy kötelező ablak-paraméterrel olvasható, alapértelmezés nincs. Nem lehet „véletlenül” a teljes adatot betölteni.
fail-closed záró ár
Záró árnak csak a kezdés előtti pillanatkép számít. Ha nincs ilyen, a sor kimarad, és nem egy később mentett (élő) árral pótlódik.
zárolt holdout + variáns-számláló
Egy félretett, zárolt adathalmazhoz csak jóváhagyott körben lehet nyúlni, és minden kipróbált változat számít. 10 variáns fölött szigorúbb küszöb jön, 20 fölött a keresés leáll.
$ git log --agent
A mérést egy AI kódoló-agent (Claude Code) futtatta, előre rögzített szabályok között. Így dolgoztunk:
- 01Külön ágon mért, a keresési ablakon belül (2023-07-01 előtt); a zárolt holdouthoz nem nyúlt.
- 02A feladat mérés volt, nem javítás — a riportban külön „amit ez NEM állít” szakasz szerepel.
- 03Az állítást három seeddel ellenőrizte, mielőtt kimondta, hogy nem zaj.
- 04Az ágat én néztem át és mergeltem; a javítás külön döntés lett, dátummal rögzítve.
- 05Szabály maradt: agent kódot ír és tesztel, élesbe, pénzhez és a main ágra csak ember nyúl.
$ cat NEM_ALLITJA.md
- Minden ROI Pinnacle nyers áron értendő, döntési szabállyal szűrt tétekre — nem általános modell-pontosság.
- Egy hiperparaméter-készlet, három seed, öt szezon. Az előjelváltás robusztus, a ±1%-os különbségek nem.
- Nem állítom, hogy a modell menthetetlen — csak azt, hogy a mostani feature-készlettel, szivárgás nélkül ezen a mintán nincs kimutatható él.
- Ez kutatási projekt, nem tippszolgáltatás.
$ echo $TANULSAG
A szép backteszt gyanú, amíg a szivárgás-teszt nem zöld.
Ez azóta a projekt egyik alapszabálya. A legdrágább hiba nem az, amelyik elszáll, hanem az, amelyik csendben jó számokat ad.