25.4 Feature store és adatkontraktus szükségessége

25.4.1 Helyzet, bemenet és döntési kérdés

Egy négytagú adatcsapat a Northstar prediktív feladaton dolgozik: rendeléskor kell megbecsülni annak a valószínűségét, hogy a kézbesítés 7 napnál később történik. A modell a rendelés pillanatában ismert nyolc feature-t használja: distance_km, priority_flag, basket_minor, quantity_sum, account_age_days, segment, region, acquisition_channel. A cél target_late, értéke 1, ha delivery_days > 7, különben 0.

A bemenet a mellékelt lab/data/customers.csv (300 ügyfél), lab/data/orders.csv (2400 rendelés) és lab/data/order_items.csv (4796 tételsor). A megfigyelési egység egy rendelés, a predikció időpontja order_ts. A modellépítés a starter szerződés szerint a 2025-05-01 cutoff előtti, címkeszempontból már teljesen megfigyelt rendeléseket használja. A jelen alfejezet kérdése nem új modell, hanem eszközdöntés: elég-e a verziózott Parquet snapshot és a verziózott SQL, vagy külön feature store-t kell üzemeltetni?

Induló becslés: a nyers input néhány MB alatt van; egy cutoffszűrés után a tanuló feature-tábla nagyságrendileg 100–300 sor; a számítás napi batchben másodpercek alatt várható. Egyetlen modell, egy csapat, nincs 100 ms alatti online kiszolgálási SLA. Ebből az első becslés az, hogy Parquet + verziózott SQL + feature-szerződés elegendő, és a külön feature store csak későbbi feltételváltozáskor indokolt. A pontos döntéshez az alábbi fogalmakat kell megkülönböztetni.

25.4.2 Offline és online feature

A feature a modell bemeneti oszlopa egy adott megfigyelési egységre, egy adott predikciós időpontban. A feladat szempontjából nem az a fontos, hogy a mérnök hogyan nevezi, hanem az, hogy mikor áll rendelkezésre.

A kettő nem a feature belső tulajdonsága, hanem a felhasználás időzítési szerződése. Ugyanaz a account_age_days lehet offline feature a modelltanításkor és online feature akkor, ha egy webszolgáltatás a rendelés pillanatában kéri le. A döntő kérdés: a forrásadathoz képest mekkora késéssel áll elő, és ez belefér-e a fogyasztó időkorlátjába.

A orders.csv szerződése szerint a rendelés sora tartalmazza a rendelés pillanatában ismert mezőket; a célváltozó és a belőle képzett oszlopok (delivery_days, actual_delivery_at, label_available_at) viszont később ismertek meg, ezért predictorba nem kerülhetnek. Ez ugyanaz a szerződés, amelyet a Predikciós szerződés és hibateszt predikciós séma-ellenőrzésként vezetett be.

25.4.3 Version és freshness

A feature-táblának nem elég „helyesnek” lennie egyszer; a tanítás és a későbbi scoring más-más időpontban fut, ezért a fogalmakat verzióhoz kell kötni.

Li,f=ti,eval−ti,f,src,L_{i,f}=t_{i,\text{eval}}-t_{i,f,\text{src}},

ahol mindkét tag időbélyeg, a különbség órában vagy napban értendő, a kivonás soronkénti. Ha Li,f>TfL_{i,f}>T_f, ahol TfT_f a szerződéses küszöb, a feature elavultnak minősül. A képlet diagnosztikai jelzés, nem valószínűség. A signup_date esetében a forrás a rendelés előtt már ismert, ezért Li,f=0L_{i,f}=0; egy last_14d_order_count jövőbeli feature-nél viszont a rendelési tábla töltési késése már releváns lenne.

A verzió nélkül a „feature store” kifejezés gyakran csak egy közös mappa, ahonnan mindenki mást olvas. A Predikciós szerződés és hibateszt alfejezetben bevezetett predikciós szerződés itt folytatódik: a tanításkor és a scoringnál ugyanazt a sémát kell érvényesíteni, különben a modell csendben eltolódik.

25.4.4 Backfill és a feature store áttekintése

A backfill múltbeli időszakok feature-értékeinek újraszámítása, például amikor a account_age_days definícióját finomítjuk, vagy új feature-t vezetünk be. A kulcskövetelmény az időhelyes join: múltbeli rendelésre nem használhatunk olyan ügyféltulajdonságot, amelyet a rendelés pillanatában még nem ismertek. A Northstar account_age_days esetében ez egyszerű, mert a signup_date a rendeléskor ismert. Egy későbbi last_14d_order_count feature-nél viszont a joinnak rendelésenként az order_ts előtti ablakot szabad csak néznie.

A feature store ilyen értelemben nem egyetlen szoftver, hanem tervezési minta, amely jellemzően négy részt kapcsol össze:

  1. Registry: név, definíció, verzió, tulajdonos, frissességi szerződés és érvényességi intervallum.
  2. Offline tárolás és transzformáció: verziózott batch feature-táblák, gyakran Parquet vagy analitikus SQL.
  3. Online tárolás és kiszolgálás: alacsony késleltetésű olvasás a scoring hívás idejére.
  4. Materializáció: az offline számításból az online tárolóba írás, verziók szerint.

A döntési feljegyzéshez érdemes feltételként kezelni, nem általános igazságként:

Igény Parquet + verziózott SQL Külön feature store
Kis adat, 1-2 modell, napi batch elegendő, kisebb fenntartási teher általában túlzó
Több csapat, közös definíciók széteshet, szóbeli egyeztetés kell registry és tulajdonosi határok hasznosak
200 ms alatti scoring a fájl vagy adatbázis nem garantálja online tároló + gyorsítótár indokolt
Skálázódó backfill és audit manifest + asof query is működhet centralizált újraszámítás segíthet

A konkrét termékválasztás itt szándékosan nem része a tananyagnak; egy Feast, Tecton vagy felhőszolgáltatás igénybevétele aktuális, verzióhoz kötött dokumentációt és mérést igényelne.

25.4.5 Teljes minimális példa: verziózott Parquet snapshot

A példa egyetlen gépen, CPU-n fut. Munkakönyvtár a projektgyökér lab/ mappája. A kód a lab pyproject.toml Python-környezetét használja; a Parquet íráshoz pyarrow szükséges. A kódot itt nem futtattuk, a feltüntetett kimenet szemléltető.

from pathlib import Path
import hashlib
import json

import pandas as pd

ROOT = Path("artifacts/feature_snapshot")
ROOT.mkdir(parents=True, exist_ok=True)

customers = pd.read_csv("data/customers.csv")
orders = pd.read_csv("data/orders.csv")

CUTOFF = pd.Timestamp("2025-05-01T00:00:00+00:00")

# Időhelyes join: a signup_date a rendeléskor már ismert,
# tehát nyugodtan használható az account_age_days-hoz.
orders_c = orders.merge(
    customers[["customer_id", "signup_date"]],
    on="customer_id",
    how="left",
    validate="many_to_one",
)

order_ts = pd.to_datetime(orders_c["order_ts"], utc=True)
signup_ts = pd.to_datetime(orders_c["signup_date"], utc=True)
orders_c["account_age_days"] = (order_ts - signup_ts).dt.days

# Kézi kontroll: 2025-01-01-től 2025-03-31-ig 89 nap,
# 2025-02-10-től 2025-03-31-ig 49 nap telik el.
check = pd.DataFrame(
    {
        "signup_date": ["2025-01-01", "2025-02-10"],
        "order_ts": [
            "2025-03-31T12:00:00+00:00",
            "2025-03-31T12:00:00+00:00",
        ],
    }
)
check_a = (pd.to_datetime(check["order_ts"], utc=True)
           - pd.to_datetime(check["signup_date"], utc=True)).dt.days
assert list(check_a) == [89, 49]

FEATURE_NAMES = [
    "distance_km",
    "priority_flag",
    "basket_minor",
    "quantity_sum",
    "account_age_days",
    "segment",
    "region",
    "acquisition_channel",
]
TARGET = "target_late"
ORDER_KEY = "order_id"

# Astarter_cutoffba illeszkedő, címkével már rendelkező tanulósorok.
label_avail = pd.to_datetime(orders_c["label_available_at"], utc=True)
training = orders_c[
    (order_ts < CUTOFF) & (label_avail < CUTOFF)
].copy()

snapshot = training[[ORDER_KEY, "order_ts"] + FEATURE_NAMES + [TARGET]].copy()

# Sémaszerződés: ugyanaz, amit a s084 predikciós validátora használ.
contract = {
    "schema_version": "features_v2",
    "created_utc": "2026-10-20T10:00:00+00:00",  # példa, nem mért futás
    "observation_unit": "order_id",
    "prediction_time_feature": "order_ts",
    "features": FEATURE_NAMES,
    "target": TARGET,
    "categorical_levels": {
        "segment": ["consumer", "business"],
        "region": ["north", "central", "south"],
        "acquisition_channel": ["search", "referral", "direct"],
    },
    "allow_missing_in_snapshot": False,
}
(ROOT / "feature_contract.json").write_text(
    json.dumps(contract, indent=2), encoding="utf-8"
)

snapshot.to_parquet(ROOT / "train_features_v2.parquet", index=False)

# Logikai összegzés. A hash itt artifact-ujjlenyomat, nem
# környezetközi bitazonos ígéret, mert a writer verziója eltérhet.
logical_summary = {
    "rows": len(snapshot),
    "columns": list(snapshot.columns),
    "missing_feature_values": int(snapshot[FEATURE_NAMES].isna().sum().sum()),
    "segment_levels": sorted(snapshot["segment"].dropna().unique().tolist()),
    "feature_hash_sha256_12": hashlib.sha256(
        snapshot[FEATURE_NAMES].to_csv(index=False).encode("utf-8")
    ).hexdigest()[:12],
}
print(logical_summary)

Futtatás a projektgyökérből Linux/macOS terminálon vagy PowerShellben:

cd lab
python feature_store_snapshot.py

Szemléltető, nem futtatott kimenet:

{'rows': 184, 'columns': ['order_id', 'order_ts', 'distance_km',
 'priority_flag', 'basket_minor', 'quantity_sum', 'account_age_days',
 'segment', 'region', 'acquisition_channel', 'target_late'],
 'missing_feature_values': 0,
 'segment_levels': ['business', 'consumer'],
 'feature_hash_sha256_12': '3f9a2c...'}

A sor- és hashértékek a tényleges futtatástól függnek; itt csak a logikai invariánsok ellenőrzendők.

SQL-verzió ugyanarra a backfillre DuckDB dialektusban, a lab/ munkakönyvtárban:

SELECT o.order_id,
       o.order_ts,
       o.distance_km,
       o.priority_flag,
       o.basket_minor,
       o.quantity_sum,
       date_diff('day', c.signup_date::DATE, o.order_ts::DATE) AS account_age_days,
       o.segment,
       o.region,
       o.acquisition_channel,
       o.target_late
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_ts < '2025-05-01T00:00:00+00:00'
  AND o.label_available_at < '2025-05-01T00:00:00+00:00';

A label_available_at nem kerül a feature-oszlopok közé; csak a cutoffszűrés feltétele.

25.4.6 A kimenet értelmezése

A tábla egy sora egy rendelés, a sorrendet az order_id azonosítja. A numerikus oszlopok dtype-ja int64 vagy float64, a kategóriák szövegesek. A feature_contract.json features listájának pontosan a nyolc megengedett oszlopot kell tartalmaznia, és a céloszlopnak a snapshotban a sémaszerződés szerinti utolsó pozícióban kell állnia. Ez ugyanaz a minimális predikciós szerződés, amelyet a Predikciós szerződés és hibateszt a scoring oldalon kért.

A kézi kontroll arra figyelmeztet, hogy a account_age_days a két ismert dátummal 89 és 49 nap; ha a backfill során ezektől eltérő érték jön, a hiba a forrásidőpont-feldolgozásban keresendő. A snapshot hiányzó értékmentes, tehát a Hiány és anomália mint vizsgálati kérdés tartományellenőrzés itt nem pótolhatja a forrásfolyamat minőségi kapuját, csak jelzi a tünetet. A hash az artifact átadásához használható; numerikus reprodukálhatóságra azonban a példa nem tesz bitazonos ígéretet.

25.4.7 Feladatok

  1. Értelmezés — döntési feljegyzés. A Northstar adat 300 ügyfélből, 2400 rendelésből és 4796 tételsorból áll. Egyetlen modell készül, napi batch futással, nincs 200 ms alatti online SLA, és a csapat négy fő. Írj 5–7 mondatos feljegyzést arról, miért elég a verziózott Parquet snapshot + SQL, és nevezz meg legalább egy konkrét feltételváltozást, amely külön feature store-t indokolna.

  2. Implementálás — sémavalidátor. Írj validate_feature_table(df, contract) függvényt, amely a fenti feature_contract.json szerkezetet használja. Ellenőrizze az oszlopok sorrendjét (order_id, order_ts, feature-k, target_late), a numerikus oszlopok dtype-ját, a kategóriaszinteket és a hiányzó értékeket. Hibák esetén listát adjon vissza, érvényes táblára üres listát.

  3. Hibadiagnózis. Egy módosított backfill futtatása után 12 tanulósorban account_age_days < 0. Mit mond ez az adatfolyamatról, és miért nem oldja meg a probléma kizárólag a feature store registry frissítése? Melyik réteg feladata a hiba blokkolása?

  4. Megváltozott feltétel. A projekt hat hónappal később 2 adatcsapat-tal és 3 ML-mérnökkel bővül. 20 modell használ közös feature-öket, közöttük last_14d_order_count és mean_basket_7d. A scoring követelmény P95 < 200 ms, és a feature-ket több rendszer is fogyasztja. Fogalmazd meg, miért válik indokolttá a külön feature store, és mi marad változatlanul a csapat felelőssége a leakage elkerülésében.

25.4.8 Megoldások és indoklás

1. megoldás. A döntési feljegyzés lényege: az adat kicsi, egy fogyasztó van, a kiszolgálás batch, ezért a Parquet snapshot + verziós szerződés + SQL-transzformáció teljes auditnyomot ad kisebb üzemeltetési teherrel. A külön feature store-t a következő feltétel indokolja: több csapat ugyanazt a feature-t több modellben szeretné újrahasznosítani, alacsony késleltetésű online scoring jelenik meg, vagy a backfill és a frissességi audit már nem követhető kézzel. Ellenőrzés: a feature_contract.json és a snapshot hash ugyanaz a tanítás és az értékelés artifactjain.

2. megoldás. Egy lehetséges implementáció:

import pandas as pd

def validate_feature_table(df, contract, with_target=True):
    expected = ["order_id", "order_ts"] + list(contract["features"])
    if with_target:
        expected = expected + [contract["target"]]

    errors = []
    if list(df.columns) != expected:
        errors.append(
            f"column_order: {list(df.columns)} != {expected}"
        )

    numeric = {
        "distance_km", "priority_flag", "basket_minor",
        "quantity_sum", "account_age_days",
    }
    for col in numeric & set(contract["features"]):
        if not pd.api.types.is_numeric_dtype(df[col]):
            errors.append(f"{col}: not numeric dtype")

    for col, allowed in contract["categorical_levels"].items():
        actual = set(df[col].dropna().unique())
        unknown = actual - set(allowed)
        if unknown:
            errors.append(f"{col}: unknown levels {sorted(unknown)}")

    if not contract.get("allow_missing_in_snapshot", False):
        n_missing_rows = df[contract["features"]].isna().any(axis=1).sum()
        if n_missing_rows:
            errors.append(f"rows with missing feature: {n_missing_rows}")

    return errors

Invariáns: érvényes táblára [], egy elcserélt oszlopra az első hibabejegyzés, ismeretlen segment értékre a harmadik hibabejegyzés jelenik meg. A függvény nem tanít modellt és nem imputál; a Hiány és anomália mint vizsgálati kérdés minőségi kapu kiegészítője.

3. megoldás. A negatív account_age_days azt jelzi, hogy a forrásban a signup_date későbbi, mint az order_ts, ami sérti a Northstar szerződést: a regisztráció minden rendelés előtt van. A lehetséges ok hibás időzóna-értelmezés, adatpatch vagy rossz forrás snapshot. A feature store nem tudja visszamenőleg helyesbíteni a forrást; a registry frissítése csak a tünetet takarhatná el. A blokkolás a belépési adatminőségi kapu dolga: a snapshot készítése előtt tartományellenőrzést kell futtatni, például min(account_age_days) >= 0 feltétellel. A javítás után a 12 sor eltűnik a hibajelentésből.

4. megoldás. A külön feature store indokolt, mert a közös definíciók és a 200 ms-os P95 követelmény már nem teljesíthető megbízhatóan közös Parquet-fájlokkal, továbbá a last_14d_order_count és mean_basket_7d minden scoring hívásra friss, időablakos aggregátumot igényel. Az offline réteg továbbra is verziózott batchben számolhat; az online réteg gyorsítótárként materializál. Ami nem változik: a predikció pillanatában nem ismert oszlopok továbbra sem kerülhetnek feature-be, a foldon belüli preprocessing-t a VI. ellenőrző pont: szivárgásmentes modellkísérlet szabálya szerint a training részen belül kell tartani, és a Predikciós szerződés és hibateszt szerződésnek a tanítás és a scoring oldalon azonosnak kell lennie.

25.4.9 Gyakori tévedés ellenpéldával

Veszélyes állítás: „Mivel a feature store központosított, a belőle olvasó modellek nem szenvedhetnek leakage-tól.” Ez nem igaz. Tegyük fel, hogy a központi tárolóba valaki felveszi a delivery_days oszlopot, mert az üzleti elemzőknek hasznos. A predikciós modell feature-listája véletlenül *-gal olvas, vagy egy későbbi szelekciós lépés automatikusan beválasztja. A modell tanítási pontossága mesterségesen magas lesz, a scoring viszont a valós rendelés pillanatában nem látja a célváltozót, ezért élesben látványosan gyengül. A központosítás itt nem csökkenti, hanem növeli a hiba hatókörét. A korábbi tanulságok érvényesek maradnak: a VI. ellenőrző pont: szivárgásmentes modellkísérlet szerint a preprocessing és a szelekció a training foldon belül él, a Predikciós szerződés és hibateszt szerint a célváltozónak és a későn érkező oszlopoknak kívül kell maradniuk a sémaengedélyen, a Hiány és anomália mint vizsgálati kérdés szerint pedig a hiányzás és az anomália a forrásfolyamat vizsgálati kérdése, nem a tároló utólagos „javítása”.

25.4.10 Önellenőrzés

Dátum-e van a döntési feljegyzésen, és a feature-táblának van-e egyértelmű sémaszerződése? A célváltozó kimaradt-e a feature-listából? A backfill minden sorára ugyanaz az asof feltétel érvényesül, és a join a predikció pillanatában ismert adatot használ? A tanításnál és a scoringnál azonos a schema_version és a kategóriaszintek? A frissességi késés mértéke és mértékegysége rögzített? A Parquet snapshotnak van hash-e és sorazonosítója? Ha ezekre igen a válasz, a Parquet + SQL döntés védhető; külön feature store a méret, késleltetés vagy csapatproblémából következzen, ne a „jó gyakorlat” címkéből.

A feature store választását a point-in-time helyesség, online/offline konzisztencia, hozzáférésvezérlés és üzemeltetési költség alapján indokoljuk; kis csapatnál a verziózott snapshot és SQL is lehet jó megoldás.

25.4.11 Rövid összefoglaló

A feature store döntés nem általános modernségi kérdés, hanem konzisztencia-, késés- és csapatméret-alapú mérnöki választás. A Northstar projektben a kis adat, egy modell és a batch scoring miatt elég a verziózott Parquet snapshot, a JSON sémaszerződés és a verziózott SQL. A központi tároló akkor indokolt, ha több csapat és modell közös feature-definíciót használ, alacsony késleltetésű online kiszolgálás kell, vagy a backfill audit skálázódni nem tud. A verzió, a frissességi késés és az időhelyes join mindkét megoldásban kötelező; a feature store önmagában nem szünteti meg a leakage-t vagy a hibás céldefiníciót. Következő alkalmazás: a reprezentációs fejezet lezárása után a modellmagyarázat és a szeletelt értékelés Globális és lokális magyarázat, ahol ugyanezt a feature-szerződést használjuk a hibaszeletek értelmezéséhez.