31.2 Adatverzió és splitmanifest

31.2.1 Cél és konkrét helyzet

A Northstar prediktív projektben az a kockázat, hogy a modell értékelése után nem tudjuk megmondani, pontosan melyik adatból és melyik felosztásból született egy metrika. A seed=20261020 önmagában nem azonosítja a tényleges CSV-fájlokat, a cutoff pedig nem azonosítja a splitfájl minden sorát. Ebben az alfejezetben olyan manifestet készítünk, amely rögzíti a forrásfájlok tartalomhashét, a splitfájl hashét és a split sorazonosítóit.

Bemenet: a lab projekt data/customers.csv, data/orders.csv és data/order_items.csv fájljai, valamint a starter_experiment.py által is használt időbeli split logika. Megfigyelési egység: egy rendelés (order_id). Kimenet: artifacts/split.json sorazonosítókkal és artifacts/split_manifest.json hash-jegyzékkel.

Előzetes becslés. Az adat 2025 első 180 napjára esik, a cutoff 2025-05-01. Ha a rendelések időben egyenletesen oszlanak el, akkor a teszt aránya körülbelül 60/180=1/360/180 = 1/3, tehát 2400 rendelésből körülbelül 800 teszt sor várható. A purged határsáv felső becslése a cutoff előtti 7 nap alapján 7/180≈3,9%7/180 \approx 3{,}9\%, körülbelül 93 rendelés. A train így nagyságrendileg 2400⋅(1−1/3−7/180)≈15002400 \cdot (1 - 1/3 - 7/180) \approx 1500 sor. Ez csak aránybecslés, nem mért eredmény; a valódi értékek a generált adat eloszlásától függenek.

31.2.2 Fogalmi magyarázat: content hash, snapshot, pointer, DVC és immutable split

31.2.2.1 Content hash

A tartalomhash (content hash) egy fájl bájtjaihoz rendelt, gyakorlatilag egyedi rövid azonosító. Ha a bemenet nn bájtja b1,b2,…,bnb_1, b_2, \dots, b_n, akkor

h=SHA256(b1b2…bn),h = \mathrm{SHA256}(b_1 b_2 \dots b_n),

ahol hh egy 256 bites érték, amelyet 64 karakteres hexadecimális szövegként írunk ki. Az SHA-256 determinisztikus: ugyanarra a bájtsorozatra minden gépen ugyanazt adja. A 256 bites kimeneti tér miatt két különböző fájl gyakorlati ütközése elhanyagolható; ez integritásellenőrzésre, nem matematikai bizonyításra használt eszköz.

Kézi kontroll a Pythonnal:

import hashlib
print(hashlib.sha256(b"abc").hexdigest())
# elméletileg várt, szabványos SHA-256 tesztvektor:
# ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad

A hash a fájl teljes tartalmára vonatkozik. A fájlméret és a sorok száma nem elegendő: két azonos hosszúságú, de eltérő tartalmú fájlnak más a hash-e.

31.2.2.2 Dataset snapshot

A dataset snapshot egy adott időpontban rögzített, megváltoztathatatlanná tett adatpéldány. A snapshot lehet a fájlok teljes másolata, például data/snapshots/2026-10-20/ alatt, vagy egy object store verziója. A content hash a snapshot leltára: igazolja, hogy a később felhasznált fájl ugyanaz, mint amit a manifest rögzített. A Northstar esetben a generate_data.py determinisztikus, ezért a snapshot újragenerálható, de a szállított CSV-ket akkor is a hash-ükkel ellenőrizzük.

31.2.2.3 Storage pointer és DVC

A storage pointer olyan kis rekord, amely nem magát a nagy adatot, hanem annak tartalomhashét és elérési útját tárolja. A mi manifestünkben a path és a sha256 mező pointer: a tényleges bájtok a fájlban maradnak, a manifest csak hivatkozik rájuk.

A DVC (Data Version Control) ezt az elvet viszi tovább. A dvc add data/orders.csv a fájlt tartalomhash alapján a DVC cache-be másolja, és létrehoz egy kis data/orders.csv.dvc pointerfájlt. A Git csak ezt a pointert verziózza, magát a CSV-t a .gitignore kizárja. A dvc push a cache-ből egy távoli storage-ba tölti az adatot; másik gépen git clone után a dvc pull hozza vissza. A DVC pointer tehát két dolgot köt össze: a tartalom azonosítóját és a storage elérési útját. A pontos parancsok és hash-algoritmus DVC-verziófüggők; az itt közölt vázlat a fogalmi működést mutatja, telepített DVC-n a saját verzió dokumentációját kell követni.

31.2.2.4 Immutable split

Az immutable split olyan splitfájl, amelyet a split_id és a hash rögzítése után nem írunk felül. Ha a cutoff vagy a szűrési logika megváltozik, új split_id-t és új fájlt készítünk, a régit pedig megtartjuk. A mi laborunkban a split.json a teljes sorazonosító-listát tartalmazza; a manifest ennek a fájlnak a hash-ét rögzíti. Így a split módosítása a manifest ellenőrzésekor azonnal kiderül.

31.2.3 Teljes minimális munkapélda

A példa a lab projekt gyökeréből fut, ahol a pyproject.toml van. A data/ mappa a CSV-ket, az artifacts/ mappa a kimeneteket tartalmazza. A Python-kód standard library-t használ, ezért a lockolt uv környezetben is futtatható.

31.2.3.1 1. lépés: splitfájl előállítása

A make_split.py a cutoff és a label_available_at alapján három listát készít. A címke akkor ismert, ha label_available_at < cutoff; ez az Adatminőségi kapu és hibás sorok kezelése alfejezetben bevezetett adatminőségi logikától független, de a manifestben rögzített tisztított fájlra épül.

from pathlib import Path
import json
import csv
from datetime import datetime

ROOT = Path(".")
CUTOFF = datetime.fromisoformat("2025-05-01T00:00:00+00:00")
orders_path = ROOT / "data" / "orders.csv"
out_path = ROOT / "artifacts" / "split.json"

train, test, purged = [], [], []
with orders_path.open(newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        order_ts = datetime.fromisoformat(row["order_ts"])
        label_ts = datetime.fromisoformat(row["label_available_at"])
        oid = row["order_id"]
        if order_ts >= CUTOFF:
            test.append(oid)
        elif label_ts < CUTOFF:
            train.append(oid)
        else:
            purged.append(oid)

split_doc = {
    "split_id": "northstar-time-2025-05-01",
    "cutoff": "2025-05-01T00:00:00+00:00",
    "train_order_ids": train,
    "test_order_ids": test,
    "purged_order_ids": purged,
}

new_content = json.dumps(split_doc, indent=2, sort_keys=True) + "\n"
if out_path.exists():
    if out_path.read_text(encoding="utf-8") != new_content:
        raise SystemExit(
            "artifacts/split.json already exists with different content; "
            "use a new split_id instead of overwriting"
        )
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(new_content, encoding="utf-8")
print(f"train={len(train)}, test={len(test)}, purged={len(purged)}")
print(f"wrote {out_path}")

A script akkor ír csak, ha a fájl még nem létezik, vagy a tartalma byte-azonos a kiszámolttal. Ez az immutable split első védelme.

31.2.3.2 2. lépés: hash-manifest előállítása

A make_manifest.py a három forrás-CSV és a split.json SHA-256 hash-ét számolja ki, és a manifestbe írja. A count_rows opcionális kontroll, nem helyettesíti a hash-t.

from pathlib import Path
import hashlib
import json
import csv
from datetime import datetime, timezone

ROOT = Path(".")
SOURCE_FILES = [
    "data/customers.csv",
    "data/orders.csv",
    "data/order_items.csv",
]
SPLIT_PATH = Path("artifacts/split.json")
OUT_PATH = Path("artifacts/split_manifest.json")

def sha256_hex(path: Path, chunk_size: int = 1024 * 1024) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        while chunk := f.read(chunk_size):
            h.update(chunk)
    return h.hexdigest()

def count_rows(path: Path) -> int:
    with path.open(newline="", encoding="utf-8") as f:
        return sum(1 for _ in csv.reader(f)) - 1  # fejléc nélkül

manifest = {
    "manifest_version": 1,
    "created_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
    "dataset": "northstar",
    "source_files": {},
    "split": {
        "path": str(SPLIT_PATH),
        "sha256": sha256_hex(SPLIT_PATH),
    },
    "generator": {
        "script": "generate_data.py",
        "seed": 20261020,
        "customers": 300,
        "orders": 2400,
    },
}

for rel in SOURCE_FILES:
    p = ROOT / rel
    manifest["source_files"][rel] = {
        "sha256": sha256_hex(p),
        "bytes": p.stat().st_size,
        "row_count": count_rows(p),
    }

OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUT_PATH.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
print(f"wrote {OUT_PATH}")

A created_at miatt a manifest maga nem byte-azonos újrafuttatáskor; ezért a manifestet nem hasheljük saját magával. A forrás- és split-hashek már byte-azonosan reprodukálhatók.

31.2.3.3 3. lépés: ellenőrzés

A verify_manifest.py újraszámolja a hash-eket, és összehasonlítja a manifesttel.

from pathlib import Path
import hashlib
import json

ROOT = Path(".")
MANIFEST_PATH = Path("artifacts/split_manifest.json")

def sha256_hex(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()

manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
for rel, info in manifest["source_files"].items():
    actual = sha256_hex(ROOT / rel)
    if actual != info["sha256"]:
        raise SystemExit(f"hash mismatch for {rel}: expected {info['sha256']}, got {actual}")
    if (ROOT / rel).stat().st_size != info["bytes"]:
        raise SystemExit(f"size mismatch for {rel}")
    print(f"{rel}: ok")

split_path = ROOT / manifest["split"]["path"]
if sha256_hex(split_path) != manifest["split"]["sha256"]:
    raise SystemExit("split hash mismatch")
print("split: ok")
print("all hashes match")

A futtatási parancs a lab projekt gyökeréből:

# munkakönyvtár: a lab projekt gyökere, ahol a pyproject.toml van
# Linux/macOS bash; Windows PowerShellben ugyanez a három parancs
uv run --locked python make_split.py
uv run --locked python make_manifest.py
uv run --locked python verify_manifest.py

A --locked azt ellenőrzi, hogy a környezet a uv.lock szerinti függőségeket használja. A scriptek standard library-sek, ezért itt nincs külső csomagfüggőség.

31.2.3.4 Mitől változik meg a futás inputja?

  1. Forrásfájl-módosítás. Ha a data/customers.csv egyetlen bájtja megváltozik, az SHA-256 megváltozik. A bytes és a row_count akár azonos is maradhat, például ha egy south értéket sooth-ra cserélünk, mert mindkettő 5 karakter. A verify_manifest.py ilyenkor hash mismatch hibát ad.
  2. Generálási paraméter változás. Ha a generate_data.py-t más seeddel futtatjuk, a CSV-k tartalma más lesz; a manifest generator.seed mezője a régi értéket mutatja, de a sha256 ellenőrzése akkor is hibát jelez. Ezért a hash a döntő, nem a seed.
  3. Splitlogika-változás. Ha a cutoffot vagy a label-availability szabályt megváltoztatjuk, a split.json más sorazonosítókat tartalmaz, így a split hash-e megváltozik. A manifest régi split hash-e nem egyezik.
  4. Splitfájl átrendezése. Ha valaki a split.json listáinak sorrendjét módosítja, a JSON szövege más, tehát a hash más. A hash a teljes fájlra vonatkozik, nem csak a halmazokra.

31.2.4 Eredmény értelmezése

A split_manifest.json szerkezete:

{
  "manifest_version": 1,
  "created_at": "2026-10-20T12:00:00+00:00",
  "dataset": "northstar",
  "generator": {
    "script": "generate_data.py",
    "seed": 20261020,
    "customers": 300,
    "orders": 2400
  },
  "source_files": {
    "data/customers.csv": {
      "sha256": "…",
      "bytes": 12345,
      "row_count": 300
    },
    "data/orders.csv": {
      "sha256": "…",
      "bytes": 234567,
      "row_count": 2400
    },
    "data/order_items.csv": {
      "sha256": "…",
      "bytes": 345678,
      "row_count": 4796
    }
  },
  "split": {
    "path": "artifacts/split.json",
    "sha256": "…"
  }
}

A pontozott hash-ek csak mintaértékek, nem mért kimenetek; a tényleges értékek a helyi fájloktól függenek. A mezők típusa: sha256 64 karakteres hex string, bytes és row_count egész szám, a listák JSON stringekből állnak. A row_count a fejléc nélküli sorok száma; a bytes a fájl tényleges mérete.

A hash-számítás a labor néhány MB-os fájljain a másodperc tört része alatt lefut; ez nagyságrendi becslés, nem benchmark. A manifest néhány KB méretű, mert nem tartalmazza az adatot, csak a pointereket.

A fájlhash önmagában nem azonosítja az adat előállításának kódját. A manifestben rögzítsük az előállító kód commitját, a sémaverziót és a split létrehozási szabályát is.

31.2.5 Feladatok

31.2.5.1 1. Értelmezés: miért nem elég a seed?

Adott az alábbi manifest-részlet:

"generator": {"script": "generate_data.py", "seed": 20261020}

Magyarázd el, miért nem elegendő a seed önmagában a reprodukálhatósághoz, és milyen hibát vesz észre a source_files.sha256 ellenőrzése akkor is, ha a seed nem változott. Adj példát olyan változásra, amely a seed megtartása mellett is megváltoztatja a CSV tartalmát.

31.2.5.2 2. Megvalósítás: manifest előállítása és ellenőrzése

Futtasd a make_split.py és make_manifest.py scripteket a lab projekt gyökeréből, majd a verify_manifest.py ellenőrzőt. Írd le a parancsokat és a várható kimenetet. Milyen mezőket tartalmaz a létrejött split_manifest.json?

31.2.5.3 3. Hibakeresés: azonos méret, mégis más adat

Egy kolléga a data/customers.csv egy sorában a region értéket south-ról sooth-ra cserélte úgy, hogy a sorok száma és a fájl mérete nem változott. A manifest source_files mezőjében a bytes és a row_count tehát egyezik. Mit ír ki a verify_manifest.py, és miért? Melyik mező fogja el a hibát?

31.2.5.4 4. Transzfer: szűrt split új feltétellel

A csapat csak a consumer szegmensre szűkíti a prediktív modellt. A customers.csv-ből a segment oszlop alapján szűrd az orders.csv-t, és készíts új splitet ugyanazzal a cutofftal. Hogyan változik a split.json, a split_id és a manifest? Mit kell rögzíteni ahhoz, hogy a régi és az új split ne legyen összetéveszthető?

31.2.6 Megoldások és indoklás

31.2.6.1 1. megoldás

A seed csak a generáló folyamat egyik bemeneti paramétere. A generate_data.py kódjának verziója, a Python random implementációjának viselkedése, a CSV quoting vagy az oszlopok sorrendje mind befolyásolja a fájl bájtjait. Ha például a generátor egy új verziója más sorrendben írja az oszlopokat, a seed azonos marad, de a CSV tartalma és hash-e megváltozik. A source_files.sha256 a tényleges bájtokat azonosítja, ezért a manifest ellenőrzése ilyenkor hash mismatch hibát ad. A seed tehát a reprodukáláshoz szükséges, de nem elégséges; a hash a tényleges verziót igazolja.

31.2.6.2 2. megoldás

A parancsok:

cd lab
uv run --locked python make_split.py
uv run --locked python make_manifest.py
uv run --locked python verify_manifest.py

A make_split.py kiírja a három lista hosszát, például train=…, test=…, purged=… formában; a tényleges számok a helyi adattól függenek, az előzetes becslés szerint nagyságrendileg 1500, 800 és 100. A make_manifest.py a wrote artifacts/split_manifest.json üzenetet adja. A verify_manifest.py minden forrásfájlra data/…: ok, majd split: ok és all hashes match üzenetet ír. A manifest mezői: manifest_version, created_at, dataset, source_files (mindhárom CSV sha256, bytes, row_count értékével), split (path, sha256) és generator.

31.2.6.3 3. megoldás

A verify_manifest.py a data/customers.csv sorban hash mismatch for data/customers.csv: expected …, got … üzenetet ír, és SystemExit kóddal kilép. Az ok: a south és a sooth egyaránt 5 karakter, ezért a fájlméret és a sorok száma nem változik, de a fájl bájtjai igen. A sha256 a teljes tartalomra vonatkozik, így a hiba csak a hash mezővel fogható el. Az ellenőrző invariáns: a hash a fájl minden bájtjára érzékeny, a méret és a sorok száma nem tekinthető tartalomazonosítónak.

31.2.6.4 4. megoldás

Az új split előállításához először a customers.csv-ből ki kell választani a segment == "consumer" ügyfelek customer_id halmazát, majd az orders.csv-ből csak azokat a sorokat szűrni, amelyek customer_id-ja ebben a halmazban van. A cutoff és a label-availability logika változatlan. Az új split.json csak a consumer rendelések order_id listáit tartalmazza, ezért a hash-e más. Az új split_id legyen például northstar-consumer-time-2025-05-01, és a fájl új nevet kapjon, például artifacts/split_consumer.json; a régit nem írjuk felül. A manifest split.path és split.sha256 mezője az új fájlra mutat; a forrás-hashek változatlanok maradnak, ha a CSV-ket nem módosítottuk. A szűrési feltételt is rögzíteni kell, különben a split_id-ból nem derül ki, milyen szűrés alapján készült a split; javasolt a manifestbe vagy a split.json-ba egy filter mezőt felvenni, például "filter": {"segment": "consumer"}. Ez biztosítja, hogy a régi és az új split ne legyen összetéveszthető, és a futásrekord pontosan hivatkozni tudjon a megfelelő splitre.

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

A leggyakoribb tévedés: „A DVC pointer vagy a Git commit elérhetővé teszi a hiányzó külső adatot.” Ellenpélda: clone-ozunk egy repót, amelyben data/orders.csv.dvc pointer van, de a data/orders.csv nincs a munkakönyvtárban, és a távoli storage sem elérhető. A git log mutatja a pointer commitját, de a CSV nincs a Gitben, mert a .gitignore kizárta. A dvc pull ilyenkor hibát ad, mert a pointer csak a hash-t és a remote elérési utat tartalmazza; magát az adatot nem. Ugyanez igaz a mi manifestünkre: a sha256 azonosítja a fájlt, de nem pótolja, ha a fájl hiányzik.

Visszautalás a korábbi fogalmakra: az Adatminőségi kapu és hibás sorok kezelése adatminőségi kapuja a tisztítás után fut le; a manifest a kapu által elfogadott, tiszta fájlok hash-ét rögzíti. Ha a kapu szabálya megváltozik, a hash is megváltozik, és a manifest ellenőrzése jelzi. A Futásazonosító és kísérleti táblázat futásrekordjának a split manifestre vagy a split_id-ra kell hivatkoznia, nem csak a seedre. Az Embedding és hasonlósági keresés alfejezetben a koszinusz-hasonlóság szemantikai hasonlóságot mér; a tartalomhash ezzel szemben byte-szintű azonosságot ellenőriz, és nem helyettesíti a szemantikai keresést. Végül a Credible interval és posterior predictive check intervallumai csak akkor értelmesek, ha tudjuk, melyik mintára vonatkoznak; a splitmanifest rögzíti ezt a mintát.

Önellenőrzés: tudod-e újraszámolni a manifestben szereplő hash-eket, és ugyanazt kapod-e? A split.json egy sorának törlése után a verify_manifest.py hibát ad-e? A manifest tartalmazza-e a forrásfájlok hash-ét és a split hash-ét, nem csak a seedet?

31.2.8 Rövid összefoglaló

Az adatverzió nem a seed felírása, hanem a tényleges forrásfájlok tartalomhashének rögzítése. A manifest a CSV-k SHA-256 azonosítóját, méretét, sorainak számát és a splitfájl hash-ét tárolja; a split sorazonosítókat tartalmaz, és a split_id, valamint a hash rögzítése után megváltoztathatatlannak tekintjük. A DVC pointer csak a hashre és a távoli elérési útra mutat; hiányzó külső adatot nem pótol. A splitmanifestet a futásrekord idézi, így minden metrika visszavezethető a pontos bemenetre. Következő alkalmazásként a modellartefaktum azonosítását és betöltési határát vizsgáljuk a Modellartefaktum és betöltési határ alfejezetben.