39.4 Átadás és új környezetes ismétlés

Bizonyítási szint: az azonos metrikák nem garantálnak bitazonos modellartefaktumot. Az átadási teszt külön rögzítse a környezet azonosságát, az input-hash egyezését, a numerikus toleranciát és a célzott regressziós tesztek eredményét.

39.4.1 Helyzet: a kész projektnek el kell jutnia egy másik gépre

A Független értékelés és hibaelemzés szakaszban befagyasztottuk a tesztprotokollt, elvégeztük az értékelést, szeleteket és kalibrációt számoltunk. A végeredmény egyetlen modelljelölt: a rendelés pillanatában ismert nyolc feature-ből becsüli a late kimenetet, azaz azt, hogy a kézbesítés meghaladja-e a 7 napot. Most következik az átadás (handoff): a projektet egy másik gépen, új környezetben újra kell tudni futtatni, és egy kis friss bemeneten batch predikciót kell készíteni.

Az átadás konkrét bemenete:

Elem Tartalom Megfigyelési egység
Projektkönyvtár northstar-final/ Git-tár —
Modellartefaktum artifacts/model_pipeline.joblib egy betöltött scikit-learn Pipeline
Manifest artifacts/manifest.json hash-ek, verziók, feature-szerződés
Modellkártya model_card.md szándék, korlátok, mért eredmények
Friss input artifacts/fresh_input.parquet 12 rendelés, szándékosan a cutoff utáni tartományból
Környezeti előírás pyproject.toml, uv.lock Python 3.12, uv-alapú szinkronizálás

Az elvárt kimenet: artifacts/fresh_predictions.parquet, minden sorban predicted_late (0/1) és late_probability (0 és 1 közötti valós szám), valamint egy tartalomhash. Az ellenőrzési feltétel: a predikció kizárólag a modellkártyában felsorolt, rendeléskor ismert oszlopokat használja; a tiltott, utólag ismert oszlopok megjelenése azonnal hibát ad. A környezeti ismétlés akkor sikeres, ha uv sync --locked és a tesztek ugyanazzal a kód- és modellverzióval futnak, a tartalomhash pedig a numerikus tolerancián belül egyezik.

Előzetes becslés: a 12 soros friss batch predikció egy CPU-s laptopon néhány másodperc nagyságrendű; a pontos futásidőt ebben az alfejezetben nem nevezzük mértnek, mert a szövegíró ebben a módban nem futtatott kódot. A tartalmi elvárás az, hogy a valószínűségek konzisztensek legyenek a befagyasztott 0.5-ös döntési küszöbbel: predicted_late = 1 pontosan akkor, ha late_probability >= 0.5.

39.4.2 Fogalmi alapok: mit jelent egy átadható csomag?

Az átadás nem egyetlen modellfájl átküldése. Az Átadás és új környezetes ismétlés csomag öt pillérből áll: verzió, hash, modellkártya, batch predikciós parancs és helyi füstteszt. Minden pillér más kérdésre felel.

Verziók (versions). A Python-verzió, a függőségek és a saját kód verziója együtt azonosítja, hogy melyik szoftverállapotról beszélünk. A pyproject.toml verziótartomány, nem lock: a tartomány megengedi a későbbi feloldást. A uv.lock viszont a ténylegesen feloldott függőségeket rögzíti. Ezért az átadásnál mindkettőt csomagoljuk. A saját kód verziója a Git commit hash, például git rev-parse --short HEAD, és nem csak a szép címke. Két különböző commit címkéje lehet ugyanaz; a commit hash nem.

Hashek. A hash egy előre rögzített bemeneti bájtsorozathoz számított, rögzített hosszúságú lenyomat. A projekt a SHA-256-ot használja. A lenyomat ellenőrzi a fájl integritását, de nem mond semmit az adat minőségéről vagy a modell tudásáról. Ha egy fájl átvitele közben egyetlen bájt is megsérül, a hash-ek eltérnek. Példának vegyük a "a" szöveg SHA-256 lenyomatát: ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb. Ez a szöveg hexadecimális alakja, tehát 64 karakter, mindig ugyanakkora; önmagából a tartalmat visszaállítani nem tudjuk.

A manifest.json kulcs-értékként tárolja a fájlnevet és a hozzá tartozó elvárt SHA-256-ot. Az ellenőrző parancs újraszámolja a hash-t, és az eltérést azonnal jelzi. Ez a folyamat nem helyettesíti a kódverziót: ha a kódot módosítjuk, a modell hash-e még egyezhet, de a kísérlet már más.

Modellkártya (model card). A modellkártya embereknek szóló, rövid dokumentum. Kötelező tartalma: rendeltetés, bemeneti séma, kimeneti értelmezés, tanítási adat és időbeli feltétel, mért metrikák a befagyasztott tesztből, valamint érvényességi korlátok. Az Átadás és új környezetes ismétlés projektben a kártya legfontosabb rovata a „Nem valós logisztikai validáció” figyelmeztetés: az adat szintetikus, a modell tanulási bizonyíték.

Batch prediction. A batch egy olyan parancssoros folyamat, amely változatlan bemeneti sémából olvas, és változatlan kimeneti sémát ír. Ezt a munkamenetet CLI-vel, nem notebookban adjuk át. Egy notebook cellasorrendje nem garantált; egy CLI-parancs argumentumai igen. A batch CLI feladata: betölteni a pipeline-t, ellenőrizni a manifest hash-ét, érvényesíteni a bemeneti oszlopokat, majd predikciót írni. A manifestben tárolt feature_order lista adja a betöltési sorrendet. Ezzel a gépek közötti sorrend-eltérést is kizárjuk.

Local test. Az új környezet nem csak a predikciós parancsot futtatja; a legkisebb ellenőrző készletet is le kell tudni futtatni. Ez a gyakorlatban pytest-tesztek csomagja: séma-, tartomány-, determinizmus- és hash-teszt. A helyi füstteszt akkor értelmes, ha a parancs meghívható a projektgyökérből, és nem támaszkodik a fejlesztőgép globális csomagjaira.

39.4.3 Teljes minimális példa: CLI, manifest, batch predikció

A projektben a predikciós belépési pont a src/northstar/cli.py. Az alábbi implementáció szándékosan kicsi és olvasható; a PyProject.toml [project.scripts] bejegyzése teszi a futtatható northstar parancsot elérhetővé.

import argparse
import hashlib
import json
import sys
from pathlib import Path

import joblib
import pandas as pd


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


def check_manifest(manifest_path: Path, root: Path) -> int:
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    expected = manifest["artifacts"]
    failed = []
    for rel_name, meta in expected.items():
        actual = sha256_of_file(root / rel_name)
        if actual != meta["sha256"]:
            failed.append((rel_name, actual, meta["sha256"]))
    if failed:
        print("HASH MISMATCH", file=sys.stderr)
        for rel_name, actual, want in failed:
            print(f"  {rel_name}: got {actual[:16]}... want {want[:16]}...", file=sys.stderr)
        return 1
    print(f"OK {len(expected)}/{len(expected)}")
    return 0


def predict(args: argparse.Namespace) -> int:
    root = Path(args.root).resolve()
    model_path = root / args.model
    manifest_path = root / args.manifest
    input_path = root / args.input
    output_path = root / args.output

    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    if sha256_of_file(model_path) != manifest["artifacts"][args.model]["sha256"]:
        print("Model hash mismatch", file=sys.stderr)
        return 2

    model = joblib.load(model_path)
    feature_order = manifest["feature_order"]
    allowed = set(feature_order)

    frame = pd.read_parquet(input_path)
    forbidden = sorted(set(frame.columns) - allowed)
    if forbidden:
        print(f"Forbidden columns in predictor input: {forbidden}", file=sys.stderr)
        return 3
    missing = sorted(set(feature_order) - set(frame.columns))
    if missing:
        print(f"Missing predictor columns: {missing}", file=sys.stderr)
        return 3

    X = frame[feature_order]
    probabilities = model.predict_proba(X)[:, 1]
    threshold = float(manifest["threshold"])
    predicted = (probabilities >= threshold).astype(int)

    order_key = manifest.get("output_order_column", "order_id")
    out = pd.DataFrame(
        {
            order_key: frame[order_key],
            "predicted_late": predicted,
            "late_probability": probabilities.round(6),
        }
    )
    out.to_parquet(output_path, index=False)

    payload = [
        {
            order_key: str(row[order_key]),
            "predicted_late": int(row["predicted_late"]),
            "late_probability": row["late_probability"],
        }
        for _, row in out.iterrows()
    ]
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    content_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    print(f"Wrote {len(out)} rows to {output_path}")
    print(f"Content hash {content_hash}")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(prog="northstar")
    sub = parser.add_subparsers(dest="command", required=True)

    p_check = sub.add_parser("check", help="Check artifact hashes against manifest")
    p_check.add_argument("--manifest", default="artifacts/manifest.json")
    p_check.add_argument("--root", default=".")

    p_predict = sub.add_parser("predict", help="Batch prediction from frozen pipeline")
    p_predict.add_argument("--input", required=True)
    p_predict.add_argument("--output", required=True)
    p_predict.add_argument("--model", default="artifacts/model_pipeline.joblib")
    p_predict.add_argument("--manifest", default="artifacts/manifest.json")
    p_predict.add_argument("--root", default=".")

    args = parser.parse_args()
    if args.command == "check":
        return check_manifest(Path(args.manifest), Path(args.root))
    if args.command == "predict":
        return predict(args)
    return 1


if __name__ == "__main__":
    raise SystemExit(main())

A hozzá tartozó pyproject.toml-részlet:

[project]
name = "northstar-final"
version = "0.3.0"
requires-python = ">=3.12"
dependencies = [
    "pandas>=2.2,<3",
    "scikit-learn>=1.5,<1.7",
    "joblib>=1.4,<2",
    "pyarrow>=17,<20",
]

[project.scripts]
northstar = "northstar.cli:main"

A uv lock rögzíti a tényleges feloldást. A manifest a modellfájl, a predikciós kód és a feature-séma kötelező közös nevezője:

{
  "created_at_utc": "2025-10-10T09:00:00+00:00",
  "git_commit": "a3f28c1b",
  "python": "3.12.8",
  "threshold": 0.5,
  "feature_order": [
    "distance_km",
    "priority_flag",
    "basket_minor",
    "quantity_sum",
    "account_age_days",
    "segment",
    "region",
    "acquisition_channel"
  ],
  "output_order_column": "order_id",
  "artifacts": {
    "artifacts/model_pipeline.joblib": {
      "sha256": "c9a8e45f32756c3b35acd9e44d6a2fd9f0e44d9ba9b16cc8d1fa6dc4b4a0ff37"
    },
    "artifacts/manifest.json": {
      "sha256": "075bfea16e2f2af0a44c0cbbdaf20631f4ea67cdc8cee95ca14cf1b389d66de3"
    }
  },
  "status": "learning_demo",
  "data_origin": "lab/generate_data.py seed=20261020; synthetic Northstar"
}

A példában szereplő SHA-256 értékek csak szerkezeti szemléltetők: ebben az alfejezetben a szövegíró nem futtatta a projektet, ezért nem nevezhetők a helyi gépen számított tényleges lenyomatnak. A feladatmegoldásnál a valódi manifestet a tanuló gépének kell létrehoznia.

A futtatás platformjelölt munkamenete Linux vagy macOS bash esetén, a projektgyökérből:

# Környezet létrehozása a lock alapján
uv sync --locked

# Hash-ellenőrzés
uv run --locked python -m northstar.cli check --root .

# Batch predikció a friss inputra
uv run --locked python -m northstar.cli predict \
  --input artifacts/fresh_input.parquet \
  --output artifacts/fresh_predictions.parquet \
  --root .

Windows PowerShellben ugyanez a munkakönyvtár C:\work\northstar-final esetén:

uv sync --locked
uv run --locked python -m northstar.cli check --root .
uv run --locked python -m northstar.cli predict `
  --input artifacts/fresh_input.parquet `
  --output artifacts/fresh_predictions.parquet `
  --root .

A --locked az ellenőrzött lockkal való egyezést követeli meg. Ha a lock hiányzik vagy a pyproject.toml megváltozott, a parancs hibát ad; ilyenkor előbb uv lock kell, de az már új környezetállapotot hoz létre, ezért a manifestben rögzíteni kell.

39.4.4 Eredmény értelmezése és kézi kontroll

A predikció értelmezése nem maradhat puszta szám a kódblokk után. A friss batch kimenet egy sorának alakja:

order_id predicted_late late_probability
ORD-20250601-0001 0 0.412351
ORD-20250602-0007 1 0.783102

A kézi kontroll invariánsai:

  1. Sor-szám invariáns. A kimenet sorainak száma megegyezik a bemenet soraival: 12 bemenetből 12 kimenet.
  2. Tartomány-invariáns. 0 <= late_probability <= 1 minden sorra, és predicted_late értéke 0 vagy 1.
  3. Küszöb-invariáns. predicted_late = 1 akkor és csak akkor, ha late_probability >= 0.5.
  4. Feature-szerződés. A bemenet nem tartalmazhat delivery_days, actual_delivery_at, label_available_at vagy target_late oszlopot; a CLI ezt a manifest feature_order listájával ellenőrzi.
  5. Tartalomhash. A hash-t nem a Parquet bináris állományából, hanem a kerekített sorok kanonikus JSON-jéből számoljuk. A Parquet belső metaadata a pyarrow-verziótól függően eltérhet; a tartalomhash ellenben a predikciókra vonatkozik.

A numerikus tolerancia: a valószínűségeket hat tizedesjegyre kerekítjük. Két gépen az azonos modell és azonos input hat tizedes pontossággal egyező valószínűséget ad, ha a lockolt függőségek numerikus szempontból egyeznek. A bitpontos bináris egyezés nem alapfeltevés; a tartalomhash a kerekített értékekkel vethető össze.

39.4.5 Feladatok

  1. Értelmezés. Melyik állítást ellenőrzi a manifest SHA-256, és melyiket nem? (a) a modellfájl átvitele közben nem sérült; (b) a modell a befagyasztott tesztkészleten jól teljesít; (c) a modell betanítása óta a fájl tartalma nem változott. Válaszod indokold.

  2. Megvalósítás. Írj check_fresh alparancsot a CLI-be, amely a fresh_predictions.parquet tartalomhash-ét hasonlítja a manifest expected_fresh_content_hash mezőjéhez. A Parquet állomány helyett a kerekített kanonikus JSON-t hash-eld.

  3. Hibakeresés. Az új gépen a uv run --locked python -m northstar.cli predict hívás ModuleNotFoundError: No module named 'northstar' hibát ad. Adj legalább két lehetséges okot, és mindkettőhöz konkrét ellenőrző parancsot.

  4. Megváltozott feltétel. A friss input véletlenül tartalmazza a delivery_days oszlopot is. A fenti predict függvény hogyan viselkedik? Milyen explicit szerződésváltoztatásra lenne szükség ahhoz, hogy a rendszer a hibát ne hagyja figyelmen kívül, és mit kell a modellkártyában módosítani?

  5. Továbbvitel. A batch kimenetet a döntéshozó gépén más pandas-verzióval olvassák be, és a Parquet hash-egyezés eltűnik. Magyarázd el, miért nem a tartalmi modellpredikció romlott el, és milyen hash-t küldj a partnereknek.

39.4.6 Megoldások és indoklás

1. A SHA-256 az (a) és (c) állításokat támogatja: a letöltött fájl lenyomata egyezik a manifesttel, ezért a fájl tartalma az átvitel során nem változott, és a manifest rögzíti, milyen tartalom volt a befagyasztáskor. A (b) állítást a hash nem ellenőrzi: egy rossz modell is kaphat hash-t, ha a fájl tartalma változatlan. A modellminőséget a befagyasztott teszteredmények, például a Független értékelés és hibaelemzés fagyasztott metrikái igazolják.

2. A tartalomhash újraszámítása:

import hashlib
import json

import pandas as pd


def content_hash_of_predictions(path) -> str:
    frame = pd.read_parquet(path)
    payload = [
        {
            "order_id": str(row.order_id),
            "predicted_late": int(row.predicted_late),
            "late_probability": float(round(row.late_probability, 6)),
        }
        for row in frame.itertuples()
    ]
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

A check_fresh ezt veti össze a manifest értékével. Ha eltér, a folyamat kettes kilépési kóddal hibát ad; ellenkező esetben FRESH OK üzenetet ír.

3. A hiba első okaként a modul nincs telepítve a környezetbe. Ellenőrzés: uv run --locked pip list | Select-String northstar PowerShellben, illetve uv run --locked pip show northstar bashben. Megoldás: a projekt gyökérben uv sync --locked futtatása; ha a csomagot fejlesztői módban kell látni, uv sync --locked --extra dev vagy uv pip install -e .. Második ok: a parancs más munkakönyvtárból fut, és a Python nem találja a src/ könyvtárat. Ellenőrzés: pwd bash, Get-Location PowerShell; a várt gyökérben léteznie kell a src/northstar/__init__.py fájlnak. Harmadik lehetséges ok: a CLI-t a northstar konzolscripttel hívják, de a telepített elavult script másik interpretert használ; ezt uv run --locked which python és uv run --locked python -c "import northstar; print(northstar.__file__)" mutatja ki.

4. A fenti predict függvény forbidden listába teszi a delivery_days oszlopot, mert az nincs a manifest feature_order listájában, és hármas kóddal leáll, mielőtt outputot írna. Ez a kívánt viselkedés: a célmagyarázat jelenléte a prediktor bemenetben szivárgás lenne. Ha mégis szeretnénk használni, az új megfigyelési szerződés kellene: például „későbbi, részleges időablakból származó early label”, új manifest, új feature-lista és új modellkártya. A kártyában a bemeneti sémát és a megfigyelési pillanatot explicit módon módosítani kell; a régi modell betöltése ezzel az inputtal továbbra is hiba kell legyen.

5. A Parquet bináris hash-egyezés hiánya gyakori hamis riasztás. A Parquet belső metaadata a pyarrow verziójától, a létrehozó könyvtár nevétől és a statisztikai blokkoktól függhet. A modell predikcióinak lényege a tartalom: ha a late_probability hat tizedesre kerekített értékei és a predicted_late oszlop egyeznek, a tartalomhash egyezik. Ezért a partnereknek a kerekített kanonikus JSON hash-t küldjük, és a manifestben is ezt a mezőt rögzítjük.

39.4.7 Gyakori tévedés és ellenpélda

A leggyakoribb hiba az, hogy a tanuló a szintetikus Northstar-modell működését valós logisztikai modellvalidációként kezeli. Ellenpélda erre a gondolkodásra a következő: tegyük fel, hogy a friss input 45 sort tartalmaz, és minden sorra a rendelés pillanatában elérhető feature-ök pontosak. A batch CLI gond nélkül készít predikciót. Ettől a modell még nem alkalmas valós döntésre, mert az adat szintetikus, a szegmensek mesterségesek, a hiányzó szelekciós folyamatok nem valós üzleti torzítások, és a modellkártya status mezője learning_demo. Az átadás így is teljes értékű tanulási bizonyíték: demonstrálja a kód, a séma, a hash, a manifest és a CLI együttműködését. A valós bevezetéshez valós adat, új adatszerződés, adatvédelmi vizsgálat és üzleti jóváhagyás kell.

Visszautalás: a Saját data science munkaasztal specifikációja szakaszban a saját munkaasztal minimális eszközkészletét választottuk; a mostani átadás pontosan annak a döntésnek a futtatható következménye. A Kiadás, visszaállítás és terhelési próba szakasz release-fogalma szerint a release nem egy modellfájl, hanem verzióhoz kötött jegyzőkönyv; itt ezt a jegyzőkönyvet a manifest és a modellkártya testesíti meg. Az Adatkezelés és reprodukálhatóság külső modellnél szakasz tanulsága is érvényes: a kód-, modell- és verzióhármas azonosítja a futást, és a köztes gyorsítótáraknak is verzióérzékenynek kell lenniük.

Önellenőrző lista az átadás előtt:

39.4.8 Rövid összefoglaló

Az átadás akkor sikeres, ha a másik gép a projektgyökérből uv sync --locked után változatlan kóddal és modellel futtat hash-ellenőrzést, helyi tesztet és batch predikciót. A manifest SHA-256 lenyomatai a fájlintegritást, a Git commit és a lock a szoftverállapotot, a modellkártya az emberi korlátokat, a CLI pedig a megismételhető predikciós szerződést rögzíti. A Friss inputon készített output tartalomhash-ét kerekített numerikus értékekből számoljuk, hogy a formátum-metaadatok eltérése ne okozzon hamis riasztást. A következő záróprojekt ezt az átadási csomagot oksági kérdésre alkalmazza: a Kísérleti kérdés és előzetes elemzési terv szakaszban a kísérleti kérdést és az előzetes elemzési tervet vesszük fel.