2.3 Git adat- és modellprojektekben

2.3.1 Cél és konkrét helyzet

Az 2.2 alfejezetben létrehozott scripts/s006_diagnose.py és a reports/s006 kimenetek már a munkakönyvtárban vannak. A data/raw/orders_raw.csv a Northstar nyers adata, amely a generátorral újra előállítható, ezért nem akarjuk a Git-történetbe zárni. A feladat most az, hogy egy új feldolgozási ellenőrző szkriptet, a hozzá tartozó titkokat kizáró szabályt és egy továbbfejlesztett változatot három külön, visszakereshető commitba (véglegesítésbe) szervezzünk.

Bemenet: a fenti könyvtárállapot és a data/raw/orders_raw.csv. Megfigyelési egység: egy-egy fájlváltozat a Git által rögzített pillanatképekben. Cél: a három commit logikailag különválik, a nyers adat és a .env titokfájl soha nem kerül a repóba. Előzetes becslés: a .gitignore után a repó mérete néhány száz KB alatt marad, mert a szándékosan hibás CSV-t a Git nem tárolja.

2.3.2 Fogalmi magyarázat

A Git három területet különböztet meg: a working tree (munkakönyvtár) a ténylegesen látható fájlokat, a staging area vagy index az előkészített, következő commitba szánt állapotot, a HEAD pedig az aktuális commit által mutatott pillanatképet jelenti. A git add a working tree-ből az indexbe másol, a git commit az indexből új, megváltoztathatatlan pillanatképet hoz létre. Minden commitnak egyedi SHA-azonosítója van; a rövid forma például a3f9c21. A commit üzenet a „miért”-et rögzíti, nemcsak a „mit”-et.

A branch (ág) egy mozgatható névcímke, amely egy commitra mutat. Egy kísérleti változtatást saját ágon érdemes végezni, hogy a főág, például main, továbbra is a legutóbbi ismert jó állapotot mutassa. A git switch -c experiment új ágat hoz létre; a commitok azon az ágon haladnak előre, a main pedig nem mozdul.

A diff (különbség) két állapot szövegszintű összehasonlítása. A git diff a working tree és az index, a git diff --staged az index és a HEAD, a git diff HEAD a working tree és a HEAD közötti eltérést mutatja. A kimenet úgynevezett hunkokba rendeződik. Egy hunk fejlécében a @@ -a,b +c,d @@ azt jelenti, hogy a régi fájl a. sorától b sor, az új fájl c. sorától d sor érintett. A mínuszos sor törlés, a pluszos sor hozzáadás. Például:

@@ -1,6 +1,7 @@
 """Ellenőrzi az orders_raw.csv szerződését a Northstar adatszerződés szerint."""
 from pathlib import Path
 import csv
 import sys
+from collections import Counter

Ez azt mutatja, hogy az új változatban a Counter import bekerült, és a régi 6 sor helyett 7 sor szerepel; tartalmi konfliktus nincs, a környező sorok kontextusként szolgálnak.

A .gitignore fájl olyan mintákat tartalmaz, amelyekre illeszkedő, még nem követett fájlokat a Git figyelmen kívül hagy. A minták például data/raw/ könyvtárat, .env titokfájlt, __pycache__/ mappát, *.pyc bájtkódot vagy *.joblib modellfájlt jelölhetnek. Fontos: a szabály csak a még nem indexelt fájlokra hat; ha egy fájl korábban már bekerült a commitba, a .gitignore önmagában nem távolítja el a követésből.

A Jupyter-notebookok .ipynb kiterjesztésű JSON-fájlok, így a nyers git diff a cellakimenetek és a metaadatok miatt zajos. Jó gyakorlat a notebookot kimenet nélkül commitolni, például jupyter nbconvert --to notebook --ClearOutputPreprocessor.enabled=True --inplace analysis/eda.ipynb paranccsal, vagy a végső logikát külön Python-szkriptbe exportálni. A nbdime nevű eszköz notebookbarát diffet ad, de nem kötelező telepíteni; a tanulási főút a notebookból scriptté alakítást részesíti előnyben.

A nagy binárisok határa: a Git a pillanatképekben a teljes fájltartalmat tárolja, még tömörítve is. Egy 200 MB-os Parquet-fájl minden commitban újabb másolatot jelent; a diff nem értelmes, a repó mérete gyorsan nő. Ezért a nagy nyers adat, a generált feature-tábla, a modellfájl és a notebook checkpoint nem a Git-repó tartalma, hanem külön, verziózott adattároló vagy az újragenerálást leíró kód része.

2.3.3 Teljes minimális munkapélda

Az alábbi munkamenet Linux/macOS Bash héjon fut, munkakönyvtára ~/projects/northstar-lab. A data/raw/orders_raw.csv a 2.2 alatt már létrejött, a scripts/s006_diagnose.py pedig a repóban lesz. A kimenetek szemléltetőek, nem mért futtatási jegyzék; a parancsok a leírt állapotból kiindulva ezt az eredményt adják, de a commit-hash-ek gépenként eltérhetnek.

Először inicializáljuk a repót és rögzítjük a helyi azonosítót:

git init -b main
git config user.name "N. Diák"
git config user.email "diak@example.com"

Létrehozzuk az első ellenőrző szkriptet, amely a Northstar szerződés szerint a data/raw/orders_raw.csv oszlopait és a hiányzó distance_km értékeket vizsgálja:

"""Ellenőrzi az orders_raw.csv szerződését a Northstar adatszerződés szerint."""
from pathlib import Path
import csv
import sys

RAW = Path("data/raw/orders_raw.csv")

def main() -> int:
    if not RAW.exists():
        print(f"missing input: {RAW}", file=sys.stderr)
        return 2
    with RAW.open(newline="") as f:
        reader = csv.DictReader(f)
        fieldnames = reader.fieldnames or []
        rows = list(reader)
    required = {"order_id", "customer_id", "order_ts", "distance_km", "basket_minor"}
    missing_cols = required - set(fieldnames)
    if missing_cols:
        print(f"missing columns: {sorted(missing_cols)}", file=sys.stderr)
        return 3
    missing_distance = sum(1 for r in rows if r["distance_km"] == "")
    print(f"rows={len(rows)} missing_distance_km={missing_distance}")
    return 0 if missing_distance == 0 else 1

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

Ez az első commit. A szakaszos mentés a git add és a git commit páros:

mkdir -p scripts
# a fenti tartalmat a scripts/s007_check_orders.py fájlba mentjük
git add scripts/s007_check_orders.py
git commit -m "Add initial orders_raw contract checker
"

A második commit a .gitignore szabályt rögzíti. A nyers adat és a titokfájl kimarad:

cat > .gitignore <<'EOF'
data/raw/
.env
artifacts/
__pycache__/
*.pyc
EOF
git add .gitignore
git commit -m "Ignore raw data and local secrets"

A harmadik commit kiterjeszti a szkriptet a duplikált order_id és a negatív basket_minor ellenőrzésével. A git diff először a working tree és az index közti változást mutatja; feltesszük, hogy a módosítás már megtörtént. A Counter import hunkja a fenti fogalmi példában látható, a további hunk pedig:

@@ -19,4 +20,8 @@ def main() -> int:
     if missing_cols:
         print(f"missing columns: {sorted(missing_cols)}", file=sys.stderr)
         return 3
     missing_distance = sum(1 for r in rows if r["distance_km"] == "")
-    print(f"rows={len(rows)} missing_distance_km={missing_distance}")
-    return 0 if missing_distance == 0 else 1
+    ids = [r["order_id"] for r in rows]
+    duplicates = {k: v for k, v in Counter(ids).items() if v > 1}
+    negative_basket = sum(1 for r in rows if r["basket_minor"] and int(r["basket_minor"]) < 0)
+    print(f"rows={len(rows)} missing_distance_km={missing_distance} "
+          f"duplicate_order_ids={sum(duplicates.values())} negative_basket_minor={negative_basket}")
+    return 0 if missing_distance == 0 and not duplicates and negative_basket == 0 else 1

A szkript tetején a from collections import Counter sor is megjelenik. A commit:

git add scripts/s007_check_orders.py
git commit -m "Add duplicate and negative basket checks"

A végeredmény ellenőrzése:

git log --oneline --reverse
git ls-files data/raw
git check-ignore -v data/raw/orders_raw.csv

Szemléltető kimenet:

a3f9c21 Add initial orders_raw contract checker
b7e41d2 Ignore raw data and local secrets
c2d88e5 Add duplicate and negative basket checks

A git ls-files data/raw üres kimenete azt igazolja, hogy a nyers fájl nincs a commitokban. A git check-ignore -v például a .gitignore:1:data/raw/ data/raw/orders_raw.csv sort írja ki, tehát a szabály ténylegesen illeszkedik.

2.3.4 Eredmény értelmezése

A három commit logikailag elkülönül: az első a minimális ellenőrzést hozza létre, a második a tárolási határt rögzíti, a harmadik a szerződés szerinti hibák további osztályait fedi le. A git log --oneline --reverse három sort ad; a git ls-files data/raw üres; a git status --porcelain tiszta. A git show --stat HEAD a harmadik commitban csak a scripts/s007_check_orders.py módosítását mutatja, a .gitignore nem érintett. A git diff HEAD~1 -- scripts/s007_check_orders.py kizárólag a harmadik commit hunkjait tartalmazza. A script kimenete elméletileg a Northstar szerződésből várt hibákat jelzi: rows=2405, missing_distance_km=2, negative_basket_minor=1, a duplikátumszám pedig nulla feletti; a pontos szám a helyi futtatásból derül ki. A .gitignore miatt sem a data/raw/orders_raw.csv, sem a .env, sem a generált artifacts/ tartalom nem kerül a repóba.

Titok és adatreprodukció. A .gitignore csak a még nem követett fájlok felvételét akadályozza meg; a már commitolt titkot nem törli a történetből. Ilyen esetben a titkot vissza kell vonni vagy rotálni, és szükség esetén a történetet külön eljárással tisztítani. Személyes vagy licencköteles adatot a Gitből való kizárás önmagában nem tesz jogszerűvé.

A Git kis szöveges forrásfájlok és konfigurációk verziózására alkalmas; adat- és modellartefaktumoknál külön tartalomazonosító és hozzáférési szabály szükséges. Gyakorlati kontroll: git status --short, git ls-files, titokkereső ellenőrzés és a generált állományok kizárása a commit előtt.

2.3.5 Feladatok

  1. Értelmezés. Adott az alábbi diff a scripts/s007_check_orders.py egy későbbi változatáról:

    @@ -24,6 +24,7 @@ def main() -> int:
         negative_basket = sum(1 for r in rows if r["basket_minor"] and int(r["basket_minor"]) < 0)
         print(f"rows={len(rows)} missing_distance_km={missing_distance} "
               f"duplicate_order_ids={sum(duplicates.values())} negative_basket_minor={negative_basket}")
    -    return 0 if missing_distance == 0 and not duplicates and negative_basket == 0 else 1
    +    checks_ok = missing_distance == 0 and not duplicates and negative_basket == 0
    +    return 0 if checks_ok else 1

    Mit fejez ki ez a változtatás, és milyen commitüzenet illik hozzá? Indokolja, miért nem változtatja meg a függvény külső viselkedését.

  2. Megvalósítás. Készítsen három commitot egy új scripts/s007_check_orders.py javításról: (a) a hiányzó oszlopok üzenetét egészítse ki a talált oszlopok listájával; (b) a .gitignore-t bővítse a *.joblib mintával; (c) a szkripthez adjon egy --strict kapcsolót, amely a duplikált sorokat is hibának tekinti. Írja le a teljes Bash-parancssort a fájlmódosításoktól a commitokig, és adja meg a három commitüzenetet.

  3. Hibadiagnózis. A git status azt mutatja, hogy a data/raw/orders_raw.csv a staging area-ban van, pedig a .gitignore első sora data/raw/. Mi lehet az ok, és pontosan milyen parancsokkal állítja helyre a helyzetet? Miért nem elég a .gitignore módosítása?

  4. Transzfer. A projekt átkerül egy Windows gépre, ahol PowerShell fut. Adja meg a három commit PowerShell-parancsokkal, és ellenőrizze, hogy a .gitignore mintái változatlanul működnek-e. Ezután a feltétel megváltozik: a kész modellfájlokat models/*.joblib néven, a köztes adatokat data/interim/*.parquet néven generálja a pipeline. Egészítse ki a .gitignore-t, és egyetlen paranccsal igazolja, hogy egy ilyen fájl valóban figyelmen kívül maradna.

2.3.6 Megoldások és indoklás

  1. A diff a hosszú feltételt egy checks_ok nevű, beszédes változóba emeli ki, majd a return ezt használja. A külső viselkedés azonos, mert a logikai kifejezés értéke nem változott; ez refaktorálás, nem funkcionális javítás. Illő commitüzenet: Refactor check condition into named variable. Az indoklásban a diff kontextussorai mutatják, hogy sem a kiírt sor, sem a feltétel tagjai nem változtak.

  2. A Bash-munkamenet vázlata:

    # (a) a szkript üzenetének módosítása után
    git add scripts/s007_check_orders.py
    git commit -m "Report found columns on schema error"
    
    # (b) a .gitignore kiegészítése után
    printf '*.joblib\n' >> .gitignore
    git add .gitignore
    git commit -m "Ignore serialized model files"
    
    # (c) a --strict kapcsoló implementálása után
    git add scripts/s007_check_orders.py
    git commit -m "Add strict mode treating duplicates as errors"

    Az ellenőrző invariant: git status --porcelain üres, git log --oneline --reverse pontosan három új sort ad, és a git show --stat HEAD csak a szkriptfájlt sorolja fel a harmadik commitban.

  3. Az ok: a data/raw/orders_raw.csv már a .gitignore létrehozása előtt bekerült az indexbe; a .gitignore csak a még nem követett fájlokat szűri. A helyreállítás két lépésből áll: először a fájlt el kell távolítani az indexből, de a munkakönyvtárban meg kell hagyni, git rm -r --cached data/raw paranccsal, majd git commit -m "Stop tracking raw data" véglegesíti. A .gitignore módosítása önmagában azért nem elég, mert a már indexelt fájl továbbra is a commit tartalma maradna. Az ellenőrzés: git ls-files data/raw üres, a git status pedig a fájlt már nem mutatja.

  4. PowerShell-munkamenet:

    # a szkript létrehozása után
    git add scripts/s007_check_orders.py
    git commit -m "Add initial orders_raw contract checker"
    
    # a .gitignore létrehozása után
    git add .gitignore
    git commit -m "Ignore raw data and local secrets"
    
    # a szkript módosítása után
    git add scripts/s007_check_orders.py
    git commit -m "Add duplicate and negative basket checks"

    A .gitignore mintája platformfüggetlen: a data/raw/ és a .env minták Windows alatt is ugyanúgy illeszkednek, mert a perjel a Git mintanyelvében könyvtárelválasztóként funkcionál. A kiegészítés:

    Add-Content .gitignore "models/*.joblib"
    Add-Content .gitignore "data/interim/*.parquet"
    git add .gitignore
    git commit -m "Ignore generated models and interim data"

    Az ellenőrzés egyetlen paranccsal: git check-ignore -v models/example.joblib, amelynek kimenete megmutatja a .gitignore megfelelő sorát; ha nincs kimenet, a minta hibás.

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

Gyakori tévedés, hogy a Git-repót automatikusan nagy adathalmazok és modellbinárisok tárolójának tekintik. Ellenpélda: egy 200 MB-os orders.parquet és egy 500 MB-os model.joblib minden egyes commitban új teljes másolatot igényelne. Három-négy ilyen commit után a repó több GB-os, a git clone és a push percnyi, a diff pedig értelmezhetetlen bináris kimenetet ad. A helyes határ: a repóba a forrás, a kis kontrolladatok és a szerződések kerülnek; a nagy generált fájlokhoz külön verziózási eszköz való, amit az Adatverzió és splitmanifest tárgyal. A visszautalás 1.3-ra: a commit üzenet a tudományos munkanapló része, mert a döntést és a szándékot rögzíti; a manifest hash-ellenőrzése determinisztikus és nem tolerál eltérést, míg a Git-commitok hash-ei a tartalomtól függő, de gépenként is reprodukálható azonosítók. Az 2.2-ra visszatekintve a szkript a hibákat stderr-re írja, a return érték a processz exit code-ja, így a későbbi automatizált futtatás a Git által verziózott szkriptet közvetlenül használhatja.

Önellenőrzés minden commit előtt: git diff --check whitespace-hibát keres, git status --porcelain megmutatja a rendezetlen állapotot. A titkos adatok véletlen bekerülését a git grep -n "api_key" HEAD vagy hasonló keresés jelzi; ha titok került a history-ba, a fájl törlése önmagában nem elég, a titkot érvényteleníteni kell, és a history újraírása külön, kockázatos művelet. A git check-ignore -v a .gitignore szabályok tényleges illeszkedését ellenőrzi.

2.3.8 Rövid összefoglaló

A Git a data science projektben a kód, a konfiguráció és az elemzési döntések visszakereshető története. A working tree, az index és a HEAD hármas elkülönítése lehetővé teszi, hogy a git add és a git commit kis, logikus egységeket rögzítsen; a git diff és a hunkok a változás pontos tartalmát mutatják. A .gitignore a nyers adatot, titkokat, cache- és modellfájlokat zárja ki; a notebookok zajos JSON-diffje helyett kimenet nélküli commit és scriptexport a praktikus. A nagy binárisok nem a Git-tárolóba valók. A következő alfejezetben ezt a helyi repót egy másik géppel kötjük össze, hogy a projekt azonos környezetben folytatható legyen.