29.4 Kódminőség és együttműködő review

29.4.1 Cél és konkrét helyzet

Az Adatteszt és numerikus teszt alfejezetben felépített northstar modul rendelkezik adattesztekkel, de a features.py hibás account_age_days függvényt tartalmaz: a signup_date timezone-naiv, az order_ts viszont UTC-aware, ezért a kivonás futásidőben TypeError-t dob. Ebben az alfejezetben nem maga a hibajavítás a fő cél, hanem az, hogyan lehet a javítást review-zhatóvá tenni: kis, áttekinthető diff, a probléma és a validáció rövid leírása, statikus eszközökön átmenő kód és emberi ellenőrzésre alkalmas összefoglaló.

A bemenet az Adatteszt és numerikus teszt labor src/northstar/features.py fájlja, a megfigyelési egység egy rendeléshez tartozó order_ts és signup_date pár. Az eredmény a kettő különbsége egész napokban, az elvárt ellenőrzés pedig egy determinisztikus pytest eset és a formai ellenőrzések. A lab/ADATOK.md szerint signup_date naptári dátum, order_ts UTC-időbélyeg; a javításnak ezt a szerződést kell kodifikálnia.

Induló feladat: Mielőtt a konkrét javítást látná, becsülje meg, mit tapasztal a felhasználó a hibás függvény futtatásakor, és miért nem elég a linter a hiba elkerüléséhez. Becslése után dolgozza végig a minimális példát.

29.4.2 Három automatikus réteg és egy emberi réteg

A kódminőségi eszközöket négy rétegként érdemes szétválasztani: formatter, linter, type checker és code review. Az első három automatikus, az utolsó emberi.

Egy kézi ellenőrzés az automatikus eszközök értelmezésére:

# A linter F401-et ad, mert a pd nincs használva.
import pandas as pd

def late_target(delivery_days):
    # A linter zöld, pedig a delivery_days a valós címkéből képzett,
    # és prediktorként használva leakage-t okozna.
    return delivery_days > 7

A formatter és linter ezen a kódon nem jelez, a típusellenőrző sem feltétlenül. A függvény mégis tudományosan hibás, mert utólag ismert információt használ. A linter csak a pd import hiányát jelzi, a leakage-t nem.

29.4.3 Teljes minimális munkapélda: feature hibajavítás és review

A kezdőlabor lab/ könyvtárában dolgozunk. A projektstruktúra az Adatteszt és numerikus teszt alfejezetből ismert; az adatfájlokat a lab/ADATOK.md rögzíti. A példa a src/northstar/features.py hibás függvényét javítja.

A hibás kód:

# src/northstar/features.py
import pandas as pd

def account_age_days(order_ts: pd.Timestamp, signup_date: pd.Timestamp) -> int:
    """Return days between signup date and order timestamp."""
    return (order_ts - signup_date).days

A formatter és a linter itt nem jelez, a mypy sem feltétlenül; a pytest viszont TypeError-t ad, mert a naive signup_date és tz-aware order_ts kivonása a pandasban futásidőben hibás.

A javított kód:

# src/northstar/features.py
from __future__ import annotations

import pandas as pd


def account_age_days(order_ts: pd.Timestamp, signup_date: pd.Timestamp) -> int:
    """Return whole days between signup date and order timestamp.

    The contract in lab/ADATOK.md says signup_date is a calendar date and
    order_ts is UTC. The signup date is therefore interpreted as UTC midnight.
    """
    signup_utc = pd.Timestamp(signup_date).tz_localize("UTC")
    delta = order_ts - signup_utc
    return int(delta.days)

A hozzá tartozó teszt:

# tests/test_features.py
from pandas import Timestamp

from northstar.features import account_age_days


def test_account_age_days_interprets_signup_as_utc_midnight():
    result = account_age_days(
        order_ts=Timestamp("2025-02-15T23:30:00+00:00"),
        signup_date=Timestamp("2025-01-01"),
    )
    assert result == 45

A parancsok a lab/ munkakönyvtárból futtathatók; Linux shell és Windows PowerShell esetén is a uv a projekt pyproject.toml-ját keresi:

cd lab
uv sync --group dev
uv run pytest tests/test_features.py
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy src

A uv sync --group dev a pyproject.toml verziótartományait oldja fel; a felhasználó által létrehozott uv.lock után a futtatás --locked opcióval őrizhető. A fenti kimenetek ezért szemléltető, elméletileg várt eredmények, nem a jelen szöveg által futtatott mérési jegyzék. A várt viselkedés: a pytest zöld, a ruff check hibátlan, a formatter stílusellenőrzése sikeres, a mypy az annotációkat elfogadja.

A pyproject.toml minőségi eszközökért felelős részlete:

[project]
name = "northstar-lab"
version = "0.1.0"
requires-python = ">=3.12"

[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.5", "mypy>=1.10"]

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

[tool.mypy]
python_version = "3.12"

A select listában az E és F a hibaszabályokat, az I az importrendezést, az UP a Python-verziófüggő egyszerűsítéseket jelenti; a valós konfiguráció a labor saját lab/pyproject.toml-ja szerint alakul.

A review-zható változtatás leírója:

Problem: account_age_days raised TypeError when order_ts was UTC-aware.
Cause: signup_date was a naive calendar date; pandas refuses aware-naive subtraction.
Fix: interpret signup_date as UTC midnight and keep order_ts unchanged.
Validation: tests/test_features.py::test_account_age_days_interprets_signup_as_utc_midnight
  pytest, ruff check, ruff format --check, mypy src.
Manual check: Timestamp("2025-02-15T23:30+00:00") - Timestamp("2025-01-01 00:00+00:00")
  equals 45 days and 23:30; .days returns 45.
Risk: the UTC midnight interpretation is an explicit change; if the business team
  uses local midnight, the feature is off by the offset. The reviewer must confirm.

A diff:

@@ -1,8 +1,16 @@
 import pandas as pd
 
 def account_age_days(order_ts: pd.Timestamp, signup_date: pd.Timestamp) -> int:
-    return (order_ts - signup_date).days
+    signup_utc = pd.Timestamp(signup_date).tz_localize("UTC")
+    delta = order_ts - signup_utc
+    return int(delta.days)

A formatter a from __future__ import annotations és az import sorrendjét is rendezi; a linter a hiányzó sort jelzi, ha elfelejtik.

29.4.4 Pre-commit és minimális CI

A lokális pre-commit hook a commit pillanatában akadályozza a nyilvánvaló hibákat; a központi CI a pull request szintjén futtatja ugyanezeket, és a review előtt ad automatikus bizonyítékot.

A .pre-commit-config.yaml példa:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.0  # example, pin to the reviewed version
    hooks:
      - id: ruff
      - id: ruff-format
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0  # example, pin to the reviewed version
    hooks:
      - id: mypy
        args: [src]
        pass_filenames: false

A rev értékek itt elrendezést mutatnak; a projektnek a saját, ellenőrzött verziójához kell igazítania őket. A telepítés a uv run pre-commit install parancs után a git commit alkalmával futtatja a hookokat. A pre-commit nem helyettesíti a CI-t: a hookok kikerülhetők, a CI viszont központilag naplózza a futást.

A minimális CI workflow:

name: ci
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v4
      - run: uv sync --locked --group dev
      - run: uv run pytest
      - run: uv run ruff check src tests
      - run: uv run ruff format --check src tests
      - run: uv run mypy src

A --locked csak akkor futtatható, ha az uv.lock a repóban van; a kezdőlaborban ezt a felhasználó hozza létre. Az action verziók is ellenőrzést igényelnek a projekt adott időponti függőségi politikája szerint.

29.4.5 Eredmény értelmezése

A függvény a determinisztikus bemenetre 45 napot ad. Kézi kontroll: 2025-02-15 23:30 UTC - 2025-01-01 00:00 UTC = 45 nap 23:30, a Timedelta.days egész napokra levág, így 45. A javítás megszünteti a TypeError-t; az automatikus eszközök formailag zölden haladnak. A teszt invariáns: a bemeneti signup_date mindig Timestamp naptári dátum, az order_ts mindig tz-aware, az eredmény nemnegatív egész. A függvény skalárt ad vissza, ezért DataFrame-shape nem változik; a dtype int. A statisztikai értelmezés itt nincs, mert nem vezetünk be új matematikát; a feature-re ható tudományos döntést a review hagyja jóvá.

Statikus típusellenőrzés nem bizonyítja a modellezési vagy statisztikai helyességet; a review ellenőrizze az estimandot, az időhelyes feature-öket és a splitet is.

29.4.6 Feladatok

  1. Értelmezés: a linter F401 hibát ad, ha az import pandas as pd nincs használva. Magyarázza meg, miért nem ellenőrzi a linter, hogy a target_late oszlop prediktorként szerepel-e az orders táblában. Adjon olyan features.py részletet, ahol a linter zöld, de a leakage miatt a modell mesterségesen jó lesz.

  2. Megvalósítás: egészítse ki az account_age_days docstringjét azzal, hogy milyen hibát jelez, ha order_ts nem tz-aware. Írja be a típusannotációt a def day_floor(timestamp: pd.Timestamp) -> pd.Timestamp függvényhez, amely a nap kezdetére kerekít, és futtassa elméletileg a ruff check és mypy parancsokat.

  3. Hibakeresés: a ruff check a következő hibát adja: F821 Undefined name 'pd'. A features.py aktuális változatában az import pandas as pd sor a fájl végére került. Hogyan észleli a linter a hibát? A mypy is jelezné-e? Indokolja, és adja meg a javított fájlt.

  4. Transzfer: a Northstar segment értéke consumer vagy business. A build_features függvényben a hibás is_business = (segment == "biz").astype(int) kifejezés a business kategóriát sosem találja el. A teszt mégis átmegy, mert a fixture-ben segment="biz" szerepel. Készítsen review-összefoglalót, amely a cél definícióját kéri számon, nem a lintert, és javítsa a tesztet.

29.4.7 Megoldások és indoklás

  1. A linter statikus kódhibákat, például definiálatlan nevet vagy felesleges importot jelez. Azt nem tudja, hogy a delivery_days a rendelés után ismert, ezért a Feature store és adatkontraktus szükségessége alfejezetben megismert időhelyességi szerződésből kizárt prediktor. Példa:
def build_features(orders):
    # A linter zöld, de a delivery_days a valós címkéből képzett.
    orders["late_hint"] = (orders["delivery_days"] > orders["promised_days"]).astype(int)
    return orders[["distance_km", "late_hint"]]

A linter nem ismeri az időrendet; a cél helyességét a teszt adatszerződés és az emberi review ellenőrzi.

  1. A docstring:
def account_age_days(order_ts: pd.Timestamp, signup_date: pd.Timestamp) -> int:
    """Return whole days between signup date and order timestamp.

    Args:
        order_ts: UTC timestamp of the order.
        signup_date: calendar date of signup, interpreted as UTC midnight.

    Returns:
        Non-negative whole days elapsed.

    Raises:
        ValueError: if order_ts is not timezone-aware.
    """
    if order_ts.tzinfo is None:
        raise ValueError("order_ts must be timezone-aware")
    signup_utc = pd.Timestamp(signup_date).tz_localize("UTC")
    return int((order_ts - signup_utc).days)


def day_floor(timestamp: pd.Timestamp) -> pd.Timestamp:
    """Return the timestamp normalized to the start of its day."""
    return timestamp.normalize()

A ruff check és mypy src elméletileg várt kimenete All checks passed! és Success: no issues found; a teszt numerikus várt értéke a korábbi 45, mert a day_floor a 2025-02-15 00:00-t ad.

  1. A linter a fájlban a pd név használatakor nem talál megfelelő importot a modul szintjén, ezért F821 hibát ad. A mypy ugyanezt jelzi, mert a nevet a statikus környezetben nem látja; a linter viszont kifejezetten a kód szerkezeti hibáira fókuszál, ezért gyakran egyszerűbb réteg. A javítás az import pandas as pd mozgatása a fájl elejére, a from __future__ import annotations után; a javított fájlban a linter és a mypy is hibátlan.

  2. A review összefoglaló:

Request: the business_flag feature is always 0 in staging.
Hypothesis: segment == "biz" never matches the ADATOK.md value "business".
Evidence: the unit test uses a fixture segment="biz" that does not follow
  the data contract; therefore the test passes while production fails.
Fix: update the fixture to "business", change the expression to
  (segment == "business"), and add a contract test that compares the
  distinct segment values in customers.csv with {consumer, business}.

A cél definíciója az adatszerződésben van, nem a linterben. A javítás után a teszt a valódi kategóriaértéket használja, és a hibás biz érték már nem szerepel sem a kódban, sem a fixture-ben.

29.4.8 Gyakori tévedés és ellenpélda

A leggyakoribb tévhit, hogy a „lintermentesség” tudományos helyességet jelent. A formatter, linter és típusellenőrző a kód mechanikai és statikus tulajdonságait ellenőrzi; nem a céldefiníciót, a leakage-t vagy a következtetést. Ellenpélda: a Northstar starter_experiment.py-ban ha valaki a delivery_days oszlopot prediktorként használja, a ruff check és a mypy nem panaszkodik, pedig a modell a célváltozóhoz tartozó, a rendelés pillanatában ismeretlen információt használ, ami a Feature store és adatkontraktus szükségessége kontraktus megsértése. Ugyanígy a GLM: bináris és számlálási cél alfejezetben tárgyalt odds ratio kódolása lehet lintertiszta; ha a kategória-referenciát rosszul választjuk, az értelmezés téves. A review-nak ezért mindig a tudományos kérdést is leltárba kell vennie: a cél definícióját, a predictorok időhelyességét, a splitet és a következtetés határait. A linter segít a hibák csökkentésében, de nem helyettesíti az emberi ítéletet.

Önellenőrzés: Ha egy diff csak formázási változtatásokat tartalmaz, a review-nak nem szabad a céldefiníciót jóváhagyni. Ha a diff a signup_date értelmezését változtatja, a review-nak a lab/ADATOK.md UTC-éjfél szerződését kell hivatkoznia.

29.4.9 Rövid összefoglaló

A formatter, linter és típusellenőrző három automatikus réteg, amelyek a kód formáját, statikus hibáit és típusegyezését javítják; az üzleti definíciót és a tudományos következtetést nem bizonyítják. A példában az account_age_days UTC-éjfél értelmezését teszttel, Ruffal, mypy-val és review-leíróval tettük ellenőrizhetővé. A reviewer a céldefiníciót, az adatszerződést, az időbeli helyességet és a validációt kéri számon; a lintmentesség csak belépési küszöb. A következő alkalmazásban a Feldolgozási gráf és idempotencia a feldolgozási gráfok idempotenciáját és adatminőségi kapuját építi ezekre az automatikus ellenőrzésekre.