39.1 Probléma és adatszerződés befagyasztása

Érvényességi kapu: egy predikciós szerződés akkor ellenőrizhető, ha minden mezőhöz rögzíti a forrását, a rendelkezésre állási időpontját, a mértékegységét és a nullértékek kezelését; a tanító- és tesztablakot az eredmények megtekintése előtt kell meghatározni.

A záróprojekt első lépése szándékosan nem modellt épít: a problémát és az adatszerződést fagyasztja be. A befagyasztás egy verziózott adatlapot (problem contract) és egy split manifestet jelent, amelyek nélkül a későbbi AUC, log loss vagy feature-fontosság számok nem lennének összehasonlíthatók. A gyakorlatban ez két fájl: contract.json és split.json, mindkettő Git-kommitban.

39.1.1 Cél és konkrét helyzet

A Northstar egy szintetikus webáruház, amely minden rendeléskor 7 napos kézbesítést ígér. A logisztikai csapat a rendelés pillanatában szeretné tudni, hogy az adott rendelés várhatóan késni fog-e, hogy proaktívan szólhasson az ügyfélnek vagy kapacitást csoportosíthasson át. A predikció batchben fut, röviddel a rendelés után; ez order_ts-hez közeli, de attól elkülönült futási időpontot jelent.

Bemenet: a tisztított lab/data/orders.csv és a lab/data/customers.csv. A szándékosan hibás orders_raw.csv nem bemenet; annak tisztítása a Hiány és anomália mint vizsgálati kérdés alfejezetben megtörtént, és a tiszta orders.csv a kontroll. A split kialakításához csak az orders.csv kell; a teljes feature-tábla az II. ellenőrző pont: adatból ellenőrzött feature-tábla alfejezetben előállított 11 oszlopos sémával dolgozik majd tovább.

Megfigyelési egység: egy rendelés (order_id), nem tétel és nem ügyfél. A cél bináris, rendelésszintű. A tételszintű tábla csak aggregálás után kapcsolódhat a rendeléshez, különben a cél és a szállítási díj többszöröződne (Join-kardinalitás és duplázódó célváltozó).

Cél: a modell a rendelés pillanatában ismert mezőkből a late = 1 esemény valószínűségét becsülje, ahol a késés azt jelenti, hogy delivery_days > promised_days, tehát delivery_days > 7.

Előzetes eredménybecslés: a split létrehozása után a tanítóhalmaz késési arányát (prevalenciáját) számítjuk ki. Ez lesz a baseline: ha például p̂=0.25\hat p = 0.25, akkor egy mindig 0.250.25 valószínűséget adó konstans modell log loss-ja Lref=−[p̂lnp̂+(1−p̂)ln(1−p̂)]L_{\mathrm{ref}} = -[\hat p \ln \hat p + (1-\hat p)\ln(1-\hat p)]; ezt kell a valódi modelleknek megverniük. A pontos p̂\hat p érték a futtatásból derül ki, ezért itt nem adunk előre számot.

Elvárt ellenőrzés: a három split halmaz páronként diszjunkt, együtt lefedi az összes rendelést; a tanítóban nincs cutoff utáni címke; a tiltott mezők egyike sem jelenik meg a prediktorlistában.

39.1.2 Fogalmi magyarázat: label, cutoff, observation unit, feature availability, split manifest

Label (célváltozó). Az ii rendelésre DiD_i a tényleges kézbesítési napok száma, pozitív egész nap. A késést az

Yi=𝟏{Di>7} Y_i = \mathbf{1}\{D_i > 7\}

indikátor adja: Yi=1Y_i=1, ha késik, és 00, ha nem. A predikció időpontja tit_i az order_ts. A címke elérhetőségének időpontja ℓi\ell_i a label_available_at, amely a kezdőlaborban megegyezik az actual_delivery_at értékével. A predikció pillanatában YiY_i nem ismert; csak ℓi\ell_i elérése után válik ismertté. A pozitív eset egyes célokhoz korábban is felismerhető lenne, de ez más megfigyelési szerződés; itt konzervatívan a teljes kézbesítést tekintjük címke-időpontnak.

Cutoff (időbeli vágás). A cc cutoff itt 2025-05-01T00:00:00+00:00, ISO 8601, UTC. A modellépítés időpontját modellezi. Három halmazt képezünk:

A három feltétel páronként kizáró és együtt teljes partíció. A purged azért kell, mert vannak olyan rendelések, amelyeket a cutoff előtt feladtak ugyan, de a címkéjük a modellépítéskor még nem érkezett meg. Ezeket sem tanítani, sem tesztelni nem szabad: a teszt a jövőbeli rendeléseket utánozza, ezért csak ti≥ct_i \ge c sorokból állhat. A purged sorok kimaradnak; ez adatveszteség, de a korrekt időbeli validáció ára.

Observation unit. A predikció és az értékelés egysége egy rendelés. Egy rendeléshez egyetlen YiY_i tartozik. Az ügyfélszintű aggregálás későbbi szerződés lehet, de a primer modell nem ügyfélszintű. Ha a tételtáblát rendeléshez joinoljuk, előbb aggregálni kell, mert a line_total_minor összegezhető, de a shipping_fee_minor rendelésenként egyszer szerepel.

Feature availability. Egy xi,kx_{i,k} mező akkor megengedett prediktor, ha értéke a tit_i pillanatban ténylegesen ismert. A befagyasztott lista nyolc mezője ezt teljesíti: distance_km, priority_flag, basket_minor, quantity_sum a rendelés pillanatában adott; account_age_days az order_ts és a korábban ismert signup_date különbsége; segment, region, acquisition_channel statikus ügyféljellemzők. A tiltott prediktorok: delivery_days, actual_delivery_at, label_available_at, target_late, valamint ezek bármely származéka. Ez utóbbi kitétel miatt például egy „ügyfél korábbi rendeléseinek átlagos késési ideje” sem használható, még akkor sem, ha az adott előzmény időben elvileg elérhető lenne.

Split manifest. A manifest egy verziózott fájl, amely minden order_id-hoz hozzárendeli a splitet. A későbbi kódok nem véletlenszerűen vagy újraszámolva hoznak létre splitet, hanem ezt a fájlt olvassák. Ez megakadályozza, hogy egy apró scriptmódosítás eltérő tanító- és teszthalmazt produkáljon. A manifest tartalmazza a cutoffot, a split-szabályt, a forrásfájl hash-ét, a generáló script verzióját és a sorokat.

39.1.3 Teljes minimális példa

39.1.3.1 Kézi ellenőrzés

Legyen a cutoff 2025-05-01T00:00:00+00:00. Nyolc rendelés:

order_id order_ts label_available_at delivery_days
A 2025-04-28 10:00 2025-05-02 09:00 4
B 2025-04-20 08:00 2025-04-27 10:00 7
C 2025-05-02 14:00 2025-05-10 16:00 8
D 2025-04-15 09:00 2025-04-25 08:00 10
E 2025-05-10 07:00 2025-05-18 12:00 8
F 2025-04-30 11:00 2025-05-04 10:00 4
G 2025-04-25 13:00 2025-05-09 11:00 14
H 2025-04-18 16:00 2025-04-24 17:00 6

A besorolás:

A tanítóhalmaz három sorból áll (B, D, H), a késések száma nlate=1n_{\mathrm{late}}=1, tehát a tanító-prevalencia p̂=1/3≈0.333\hat p = 1/3 \approx 0.333. Ez a kézi példa aránya; a teljes Northstar adaton a tényleges érték a futtatásból adódik.

39.1.3.2 DuckDB + pandas split-manifest generálás

Az alábbi script szemléltető, nem futtatott kimenettel; a tényleges futást a lab környezetben a uv run parancs végzi. A script a paraméterezést a ? placeholderrel végzi a cutoffra (Paraméterezett lekérdezés és lekérdezési terv), a fájlnevet konstansként kezeli, és a manifestet a lab/artifacts/split.json útvonalra írja.

# scripts/freeze_split.py -- szemléltető, nem futtatott
from pathlib import Path
import json
import hashlib
import duckdb
import pandas as pd

CUTOFF = pd.Timestamp("2025-05-01T00:00:00+00:00")
ORDERS = Path("lab/data/orders.csv")
OUT = Path("lab/artifacts/split.json")

split_sql = """
SELECT
    order_id,
    order_ts::TIMESTAMPTZ AS order_ts,
    label_available_at::TIMESTAMPTZ AS label_available_at,
    CASE
        WHEN order_ts < ? AND label_available_at < ? THEN 'train'
        WHEN order_ts >= ? THEN 'test'
        ELSE 'purged'
    END AS split
FROM read_csv('lab/data/orders.csv', header = true, auto_detect = true)
ORDER BY order_id
"""

conn = duckdb.connect()
manifest = conn.execute(split_sql, [CUTOFF, CUTOFF, CUTOFF]).fetchdf()
manifest["order_ts"] = pd.to_datetime(manifest["order_ts"], utc=True)
manifest["label_available_at"] = pd.to_datetime(
    manifest["label_available_at"], utc=True
)

# Invariánsok
train = manifest[manifest["split"] == "train"]
test = manifest[manifest["split"] == "test"]
purged = manifest[manifest["split"] == "purged"]

assert manifest["order_id"].is_unique
assert set(manifest["split"].unique()) <= {"train", "test", "purged"}
assert len(manifest) == len(train) + len(test) + len(purged)
assert (train["order_ts"] < CUTOFF).all()
assert (train["label_available_at"] < CUTOFF).all()
assert (test["order_ts"] >= CUTOFF).all()
assert (purged["order_ts"] < CUTOFF).all()
assert (purged["label_available_at"] >= CUTOFF).all()

source_hash = hashlib.sha256(ORDERS.read_bytes()).hexdigest()

payload = {
    "schema_version": "1.0.0",
    "contract_id": "northstar-late-v1",
    "cutoff": CUTOFF.isoformat(),
    "observation_unit": "order_id",
    "split_rule": {
        "train": "order_ts < cutoff AND label_available_at < cutoff",
        "test": "order_ts >= cutoff",
        "purged": "order_ts < cutoff AND label_available_at >= cutoff",
    },
    "source_sha256": source_hash,
    "row_counts": {
        "train": int(len(train)),
        "test": int(len(test)),
        "purged": int(len(purged)),
    },
    "order_ids": manifest.to_dict(orient="records"),
}

OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(payload, indent=2))
print(payload["row_counts"])

A futási parancs Linux/macOS terminálból, a projektgyökérből:

uv run python scripts/freeze_split.py

A kód kimenete a tényleges sorokkal töltődik fel; ebben a szövegben csak a szerkezet ellenőrzött. A lab manifest.json a mellékelt ellenőrző jegyzék szerint mérvadó.

39.1.3.3 A befagyasztott adatlap

A fenti split-szabállyal együtt a projekt adatlapja:

{
  "contract_id": "northstar-late-v1",
  "frozen_at": "2026-10-20",
  "observation_unit": "order",
  "prediction_timestamp": "order_ts",
  "promised_days": 7,
  "target": {
    "name": "late",
    "positive_definition": "delivery_days > promised_days",
    "positive_class": 1,
    "label_available_at": "actual_delivery_at"
  },
  "cutoff": "2025-05-01T00:00:00+00:00",
  "split_rule": {
    "train": "order_ts < cutoff AND label_available_at < cutoff",
    "test": "order_ts >= cutoff",
    "purged": "order_ts < cutoff AND label_available_at >= cutoff"
  },
  "allowed_features": [
    "distance_km",
    "priority_flag",
    "basket_minor",
    "quantity_sum",
    "account_age_days",
    "segment",
    "region",
    "acquisition_channel"
  ],
  "forbidden_predictors": [
    "delivery_days",
    "actual_delivery_at",
    "label_available_at",
    "target_late",
    "bármely ezekből származtatott oszlop predictor szerepben"
  ],
  "notes": [
    "Pénzösszegek EUR-ben, minor unitban (cent); nincs pénznemkonverzió.",
    "A distance_km hiányzó értéke nem 0; a tisztított orders.csv-ben nincs hiány.",
    "A label_available_at a teljes kézbesítés; a pozitív eset korábbi felismerése új szerződést igényel."
  ]
}

Ez a dokumentum a modellépítés belépési kapuja. A contract_id = northstar-late-v1 bármely módosítása új verziót, például v2-t igényel, és a régi futásokkal nem hasonlítható össze automatikusan.

39.1.4 Eredmény értelmezése

A kézi példában a train-prevalencia 1/3≈0.3331/3 \approx 0.333, a purged három sorból áll, a test két sorból. Az időbeli feltételek tehát nem egyszerűen „cutoff előtt train, utána test” felosztást adnak; a purged határsáv a címke elérhetőségét is figyelembe veszi.

A DuckDB-példa invariánsai a manifestre:

Ezek nem toleranciás, hanem egzakt feltételek: az időbélyegeket UTC-ben hasonlítjuk, ezért a határon nincs kerekítési szabadság. A JSON a source_sha256 mezővel rögzíti, melyik orders.csv-ből készült; ha a fájl később változik, a hash eltérése azonnal jelzi, hogy a manifestet újra kell generálni. A split manifestet a Git commit rögzíti, a későbbi modellkísérletek ezt olvassák be ahelyett, hogy újrafuttatnák a split-szabályt. Ez a Cache, backfill és orchestrator alfejezetben tanult bemeneti hash-gondolatot ismétli: a split manifest a modelltanítás egyik legfontosabb bemenete.

39.1.5 Feladatok

  1. Értelmezés. Az alábbi táblában c=2025-05-01T00:00Zc = 2025\text{-}05\text{-}01T00:00Z. Töltsd ki a split oszlopot minden sorra, add meg a train késési arányát, és indokold, miért nem tanulhat a modell a G rendelés delivery_days=14 értékéből.

  2. Megvalósítás. Írj egy assign_split(order_ts, label_available_at, cutoff) függvényt, amely a fenti szabály szerint "train", "test" vagy "purged" értéket ad, majd alkalmazd a táblázat minden sorára. Ellenőrizd a B, G és E sorokat.

  3. Hibakeresés. Egy adathiány miatt kézzel javított manifestben a következő sorok szerepelnek:

    A cutoff változatlanul 2025-05-01. Melyik sor hibás és miért? Mi lehet a következménye mindkét hibának?

  4. Megváltoztatott feltétel. Változtasd a cutoffot 2025-05-03T00:00:00+00:00-ra az eredeti nyolcsoros táblázatban. Számítsd ki újra a splitet és a train-prevalenciát. Melyik sor kerül át másik halmazba?

  5. Feature-availability audit. Egy munkatárs a modellhez javasolja a customer_previous_late_rate mezőt: az ügyfél korábbi rendelései közül a késettek aránya a cutoff előtt. Döntsd el a befagyasztott adatlap alapján, hogy használható-e, és indokold. Fogalmazz meg egy olyan alternatív előzmény-mezőt, amely a jelen szerződésben is megengedett lehet, vagy indokold, miért kell új szerződésverzió.

39.1.6 Megoldások és indoklás

1. feladat. A helyes split:

order_id split indoklás
A purged t<ct<c, de ℓ≥c\ell \ge c
B train t<ct<c és ℓ<c\ell<c; D=7D=7, tehát Y=0Y=0
C test t≥ct\ge c
D train t<ct<c, ℓ<c\ell<c; D=10D=10, Y=1Y=1
E test t≥ct\ge c
F purged t<ct<c, ℓ≥c\ell \ge c
G purged t<ct<c, ℓ≥c\ell \ge c
H train t<ct<c, ℓ<c\ell<c; D=6D=6, Y=0Y=0

A train sorok B, D, H; n=3n=3, nlate=1n_{\mathrm{late}}=1, tehát p̂=1/3\hat p = 1/3. A G rendelés 14 napos kézbesítése pozitív címke lenne, de a modellépítés időpontjában, a cutoffkor ez az információ még nem érkezett meg, mert label_available_at = 2025-05-09. Ha tanítósorba tennénk, a modell olyan címkét látna, amely a valódi predikciós pillanatban nem állt rendelkezésre; ez jövőbeli információ kiszivárgása lenne. A test sem jó, mert a rendelés időpontja a cutoff előtti, tehát nem modellezi a jövőbeli rendeléseket.

2. feladat. Egy lehetséges implementáció:

def assign_split(order_ts, label_available_at, cutoff):
    if order_ts < cutoff:
        if label_available_at < cutoff:
            return "train"
        return "purged"
    return "test"

Ellenőrzés B-re: order_ts=2025-04-20 < cutoff és label_available_at=2025-04-27 < cutoff, tehát "train". G-re: order_ts < cutoff, de label_available_at >= cutoff, tehát "purged". E-re: order_ts >= cutoff, tehát "test". A függvény a három feltételt a rövidzár-kiértékelés miatt páronként kizáróan adja.

3. feladat. Mindkét sor hibás. X1-re order_ts < cutoff, de label_available_at >= cutoff, tehát a splitnek purged-nek kellene lennie, nem train-nek. X2-re ugyanez a helyzet: order_ts < cutoff és label_available_at >= cutoff, tehát purged, nem test. Az X1 tanítósorba kerülése jövőbeli címke-információt szivárogtat a modellbe, ami optimistán torzítja a validációs és teszteredményt. Az X2 tesztsorba kerülése viszont múltbeli rendelést kever a jövőbeli tesztbe, ami nem a valós batch-feltételeket méri, ezért a teszt AUC/log loss nem lesz érvényes becslése a jövőbeli teljesítménynek. A javítás: mindkét sor kerüljön purged-be, vagy a manifestet újra kell generálni a scriptből.

4. feladat. Az új c=2025-05-03c = 2025\text{-}05\text{-}03. Ekkor:

A train sorok: A, B, D, H; n=4n=4, nlate=1n_{\mathrm{late}}=1, tehát p̂=1/4=0.25\hat p = 1/4 = 0.25. A példa mutatja, hogy a cutoff eltolása nem egyszerűen áthelyezi a határt, hanem a purged/test viszonyt is átírja: C a testből purgedbe került, A pedig purgedből trainbe.

5. feladat. A customer_previous_late_rate a delivery_days múltbeli értékeiből képzett szám, tehát a befagyasztott adatlap tiltott prediktora, függetlenül attól, hogy időben elvileg elérhető lenne. A tiltólista ugyanis nemcsak a jövőbeli, hanem a delivery_days bármely származékát kizárja. Ha ilyen mezőt szeretnénk, új szerződésverzió kell, amely pontosan definiálja a leade time-ot, a címke elérhetőségét és a batch-predikciós utánpótlást. A jelen szerződésben megengedett előzmény-alternatíva lehet az ügyfél korábbi rendeléseinek száma az order_ts előtt, mert az order_ts sorozat rendeléskor ismert, és nem a célból származik. Ugyanakkor a befagyasztott nyolcmezős lista ezt sem tartalmazza; annak felvétele is új feature-indoklást és szerződésverziót igényelne.

39.1.7 Gyakori tévedés, visszautalás és önellenőrzés

A leggyakoribb hiba azt hinni, hogy a delivery_days közvetett használata ártatlan, ha az érték a predikciós időpontnál régebbi. Egy ellenpélda: az „ügyfél korábbi rendeléseinek átlagos delivery_dayse” minden számítási lépésben időben elérhető lehet, mégis tiltott a jelen szerződésben, mert a befagyasztott lista a delivery_days minden származékát kizárja. Ráadásul a batch-kiszolgálásnál (Batch inference és idempotens output) külön kellene igazolni, hogy a korábbi címkék ténylegesen megérkeztek a modell futásáig, és a Feature-szelekció és magas dimenzió alfejezetben látott szelekciós optimizmus is fenyegetne. A célváltozó múltbeli értékeiből képzett prediktor a modellkártya (Robusztusság és korlátokkal írt modellkártya) „nem támogatott használat” mezőjébe tartozna, amíg új szerződés nem írja le.

Visszautalva a korábbi fogalmakra: a split-manifest az II. ellenőrző pont: adatból ellenőrzött feature-tábla alfejezetben előállított feature-tábla fölé épül, de a címkét csak a split után szabad csatolni. A predikciós szerződés (Predikciós szerződés és hibateszt) sémaellenőrzője fogja később garantálni, hogy a betanított pipeline ugyanezt a nyolc mezőt várja. A befagyasztás a Modelljelölt, jóváhagyás és registry alfejezetből ismert registry-diszciplína projekt előtti megfelelője: az adminisztratív verzió és a tudományos minőség külön állítás, ezért a contract_id és a modellmetrikák soha nem keverhetők egy sorba.

Önellenőrzés. Válaszolj röviden: (1) Mi a predikciós időbélyeg, és mi a címke elérhetősége? (2) Miért nem lehet a G rendelés a trainben a kézi példában? (3) Melyik feltétel teszi a három splitet páronként kizáróvá? (4) Miért nem elég a cutoff előtti/utáni felosztás?

39.1.8 Rövid összefoglaló

A befagyasztott adatlap rögzíti, hogy a Northstar késésprojekt rendelésszintű bináris célt becsül a rendelés pillanatában. A címke Y=𝟏{delivery_days>7}Y=\mathbf{1}\{delivery\_days>7\}, és csak a label_available_at időpontban ismert. A 2025-05-01 cutoff három diszjunkt halmazt ad: tanító, teszt és purged határsáv. Prediktorként csak a rendeléskor ismert nyolc mező használható; a delivery_days, actual_delivery_at, label_available_at, target_late és származékaik tiltottak. A split manifest verziózott, forrás-hash-sel ellátott fájl. A következő alkalmazás a Feature-pipeline, baseline és tanulás: ebből a manifestből építjük a feature-pipeline-t, a baseline-t és az első tanítást.