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 , 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 , körülbelül 93 rendelés. A train így nagyságrendileg 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.
A tartalomhash (content hash) egy fájl bájtjaihoz rendelt, gyakorlatilag egyedi rövid azonosító. Ha a bemenet bájtja , akkor
ahol 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:
# ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015adA 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.
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.
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.
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.
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ó.
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.
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.
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.pyA --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.
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.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.split.json
más sorazonosítókat tartalmaz, így a split hash-e megváltozik. A
manifest régi split hash-e nem egyezik.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.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.
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.
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?
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?
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ő?
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.
A parancsok:
cd lab
uv run --locked python make_split.py
uv run --locked python make_manifest.py
uv run --locked python verify_manifest.pyA 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.
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.
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.
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?
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.