25.1 Időhelyes aggregátum és targetfüggetlen feature

25.1.1 Cél és konkrét helyzet

A Northstar késésbecslő projektben a Python-verzió, csomagok és izolált környezet már előállított egy rendelésszintű feature-táblát. Most új jelölt feature-öket adunk hozzá: az ügyfél előéletéből számolt összegzőket. A bemenet a lab/data/orders.csv 2400 rendelése; a megfigyelési egység a rendelés. A cél: minden rendeléshez hozzárendelni három ügyféltörténeti jellemzőt, és minden sorhoz egy asof_cutoff audit oszlopot, amely rögzíti, hogy a feature kiszámításakor mi volt az időpont. Az induló becslés: az első rendelésnél minden előzményfeature 0, a későbbi rendeléseknél változhat (a rendelésszám nő, de az átlag és a késési arány csökkenhet is); az eredményt a 2400 soros shape és a prior_late_rate∈[0,1] invariant ellenőrzi.

25.1.2 Fogalmi magyarázat

Point-in-time join (időponthoz kötött join, as-of join): olyan összekapcsolás, amelyben a jobb oldali tábla rekordjai közül mindig azt választjuk, amelyik időben legfrissebb, de nem későbbi a bal oldali időpontnál. Például ha egy eseményhez az ügyfél akkori szegmensét kell hozzárendelni, akkor nem a mai customers.csv aktuális segment értékét nézzük, hanem azt, amelyik az esemény időpontjában érvényes volt. SQL-ben ön-join, pandasban merge_asof vagy rendezett csoportos ablak valósítja meg.

Rolling aggregation (gördülő aggregálás): időben csúszó ablakon számított összegzés. Itt a pontos feladat nem fix szélességű ablak, hanem az adott rendelésig bezárólag minden korábbi rendelés: ez az ún. expanding ablak. A rolling("7D") például csak az utolsó 7 napot venné, ami más üzleti kérdés lenne. A féloldalas [start, end) időablak és a join-kardinalitás a Paraméterezett lekérdezés és lekérdezési terv, illetve az II. ellenőrző pont: adatból ellenőrzött feature-tábla tanulsága: a szűrésnek a predikció időpontjánál szigorúan korábbi rekordokat kell tartalmaznia.

Entity history (egyedtörténet): egy ügyfél customer_id-hez tartozó összes esemény időrendben. A feature-ök ebből a történetből származnak, de a jelenlegi rendeléshez csak a múltbeli rész használható. A Northstar orders.csv éppen ilyen: egy rendelés egy esemény, és az ügyfélhez több rendelés tartozhat.

Label window (címke-megfigyelési ablak): az az időtartam, amely alatt a célváltozó megfigyelhetővé válik. A kezdőprojektben label_available_at = actual_delivery_at: a target_late címke csak a kézbesítés pillanatában ismert. Ha tehát egy múltbeli rendelés késési állapotát akarjuk feature-ként használni, akkor azt csak akkor vehetjük be, ha label_available_at legkésőbb az aktuális predikció időpontjában rendelkezésre áll (a <= vagy < döntést az időbélyeg pontossága és az események sorrendje szerint rögzítsük). A rendeléskosár (basket_minor) ezzel szemben már a rendelés pillanatában ismert, így ahhoz nincs szükség label windowra.

Kézi ellenőrzésként vegyünk egy kitalált ügyfelet négy rendeléssel:

Rendelés order_ts basket_minor label_available_at target_late
1. jan. 10. 5000 jan. 17. 0
2. jan. 20. 7000 jan. 28. 1
3. febr. 1. 3000 febr. 9. 0
4. febr. 15. 6000 febr. 23. 1

A 4. rendelés predikciója febr. 15-én történik. Az előzmény mind a három korábbi rendelés; a kosárátlag (5000+7000+3000)/3=5000(5000+7000+3000)/3 = 5000, a rendelésszám 3. A késési arányhoz olyan múltbeli rendelések kellenek, amelyek címkéje febr. 15. előtt ismert: mindhárom label_available_at kisebb, ezért prior_late_rate = (0+1+0)/3 = 0.333. Ha lenne egy 5. rendelés febr. 20-án, akkor annak számításakor a 4. rendelés label_available_at értéke febr. 23., tehát még nem ismert; így az 5. rendelés prior_late_rate nevezőjébe a 4. rendelés nem kerülhet be.

25.1.3 Teljes minimális munkapélda

A példa a lab/ munkakönyvtárból fut, ahol a CSV-k a data/ almappában vannak. Ha a fájl máshol található, a DATA_DIR változó módosítandó. A kód pandas 2.2.3-mal íródott; a groupby.apply include_groups=False paramétere verzióérzékeny.

import pandas as pd
from pathlib import Path

DATA_DIR = Path("data")  # igazítsa a saját lab elérési útjához

orders = pd.read_csv(
    DATA_DIR / "orders.csv",
    parse_dates=["order_ts", "actual_delivery_at", "label_available_at"],
)
orders = orders.sort_values(["customer_id", "order_ts"]).reset_index(drop=True)

def add_prior_features(name: str, grp: pd.DataFrame) -> pd.DataFrame:
    rows = []
    for i in range(len(grp)):
        row = grp.iloc[i]
        past = grp.iloc[:i]  # rendezett előzmények; azonos időpontnál tie-break szükséges
        past = past[past["order_ts"] < row["order_ts"]]
        observed = past[past["label_available_at"] < row["order_ts"]]
        rows.append({
            "customer_id": name,
            "order_id": row["order_id"],
            "order_ts": row["order_ts"],
            "asof_cutoff": row["order_ts"],
            "prior_order_count": len(past),
            "prior_basket_mean_minor": (
                past["basket_minor"].mean() if len(past) else 0.0
            ),
            "prior_late_rate": (
                observed["target_late"].mean() if len(observed) else 0.0
            ),
        })
    return pd.DataFrame(rows)

features = (
    orders.groupby("customer_id", group_keys=False)
    .apply(add_prior_features, include_groups=False)
    .reset_index(drop=True)
)

print(features.shape)
print(features[["customer_id", "prior_order_count"]]
      .sort_values("prior_order_count", ascending=False)
      .head(3))

A fenti kód várható kimenete (szemléltető, az adott CSV-től függ, nem futtatott):

(2400, 7)
  customer_id  prior_order_count
0     CUS-042                  7
1     CUS-118                  6
2     CUS-203                  6

25.1.4 Eredmény értelmezése

Az invariant ellenőrzések:

A prior_basket_mean_minor gyakorlati értelmezése: az ügyfél átlagos kosárértéke centben a múltban. A prior_late_rate azt mutatja, hogy az ügyfél korábbi, már megfigyelt rendelései milyen arányban késtek. Az audit oszlop lehetővé teszi, hogy utólag bármelyik sorra ellenőrizzük: a feature csak az asof_cutoff előtti adatokat használta. A feature-ök a Predikciós szerződés és hibateszt predikciós szerződésbe új oszlopként, megnövelt sémaverzióval kerülhetnek be.

25.1.5 Feladatok

  1. Értelmezés. A fenti kézi táblázatban add meg a 3. rendeléshez tartozó prior_order_count, prior_basket_mean_minor és prior_late_rate értékeket, és írd le a label window szerepét a nevezőben.
  2. Megvalósítás. Egészítsd ki a add_prior_features függvényt egy prior_priority_ratio feature-rel, amely a korábbi rendelések priority_flag oszlopának átlaga. A hiányzó múlt esetén 0.0 legyen az érték. Írd meg a módosított sort vagy függvényt.
  3. Hibadiagnózis. Egy kolléga a prior_late_rate-et így számolja: past["target_late"].mean(), vagyis nem szűri a label_available_at feltételt. A fenti kézi táblázatba illessz be egy ötödik rendelést order_ts = febr. 20. 14:00 értékkel, és mutasd meg, mennyi lenne a hibás és mennyi a helyes prior_late_rate az ötödik rendelésre. Nevezd meg, milyen típusú adatszivárgás történt.
  4. Megváltozott feltétel. A csapat új üzemeltetési szabályt vezet be: a predikciót nem a rendelés pillanatában, hanem 1 órával később futtatja le. Írd át a add_prior_features függvényt úgy, hogy az asof_cutoff és a label window feltétel is row["order_ts"] + pd.Timedelta("1h") legyen. Indokold, hogy a Northstar adatokon várhatóan változik-e a prior_late_rate, ha a kézbesítési idők napokban mérhetők.

25.1.6 Megoldások és indoklás

  1. A 3. rendelésnél prior_order_count = 2, prior_basket_mean_minor = (5000 + 7000) / 2 = 6000. A label window szűrés szerint az első két rendelés label_available_at értéke jan. 17. és jan. 28., mindkettő korábbi febr. 1.-nél, ezért mindkettő címkéje ismert; prior_late_rate = (0 + 1) / 2 = 0.5. A nevezőbe csak a cutoff előtt megfigyelt múltbeli rendelések kerülnek, ezért a nevező nem feltétlenül egyenlő az összes múltbeli rendelés számával.

  2. A rows.append szótárban a következő sort kell hozzáadni:

"prior_priority_ratio": (
    past["priority_flag"].mean() if len(past) else 0.0
),

A priority_flag 0/1 értékű, ezért az átlag a korábbi prioritásos rendelések aránya; a hiányzó múltnál a 0.0 konzervatív kitöltés.

  1. Az ötödik rendelés order_ts értéke febr. 20. 14:00. A múltbeli rendelések között ott van a 4. rendelés, amelynek label_available_at = febr. 23., tehát a predikció időpontjában még nem ismert a késési állapota. A hibás számítás mind a négy múltbeli rendelést beszámítja: (0+1+0+1)/4 = 0.5. A helyes szűrés csak az első hármat veszi, mert a 4. címkéje később érkezik meg: (0+1+0)/3 ≈ 0.333. A hiba a jövőbeli címke beszivárgása (label leakage), mert a modell olyan információt kapna, amely a valódi predikciós időpontban nem állt rendelkezésre.

  2. A módosított függvény:

def add_prior_features_1h(name: str, grp: pd.DataFrame) -> pd.DataFrame:
    rows = []
    for i in range(len(grp)):
        row = grp.iloc[i]
        cutoff = row["order_ts"] + pd.Timedelta("1h")
        past = grp.iloc[:i]
        observed = past[past["label_available_at"] < cutoff]
        rows.append({
            "customer_id": name,
            "order_id": row["order_id"],
            "order_ts": row["order_ts"],
            "asof_cutoff": cutoff,
            "prior_order_count": len(past),
            "prior_basket_mean_minor": (
                past["basket_minor"].mean() if len(past) else 0.0
            ),
            "prior_late_rate": (
                observed["target_late"].mean() if len(observed) else 0.0
            ),
        })
    return pd.DataFrame(rows)

Az asof_cutoff most már nem egyezik meg az order_ts-szel, hanem 1 órával nagyobb. A prior_basket_mean_minor csak akkor marad változatlan, ha a plusz egy órában nem érkezik új rendelés, amely az új predikciós időpontban már ismert; a demonstráció az eredeti past halmazt tartja fixen. A prior_late_rate csak akkor változik, ha egy múltbeli rendelés label_available_at értéke az order_ts és az order_ts + 1h közé esik. Mivel egy korábbi rendelés kézbesítése bármely órára eshet, ilyen köztes eset napokban mért szállítás mellett is előfordulhat; a módosítás azonban elvileg és más adatokon fontos lehet. A pontos ellenőrzéshez le kell futtatni, és meg kell keresni azokat a sorokat, ahol a két implementáció prior_late_rate értéke eltér.

25.1.7 Gyakori tévedés, önellenőrzés

A leggyakoribb hiba: a történeti adatbázis mai állapotát tekintjük a múltban ismert állapotnak. Ha egy korábbi rendelés delivery_days vagy target_late értékét utólag javítják, akkor a mai tábla már a javított értéket tartalmazza, de a korábbi predikció idején a régi, esetleg hibás értéket látta volna a modell. A point-in-time join éppen azt biztosítja, hogy minden eseményhez az adott időpontban érvényes rekordot rendeljük. Ez a gondolat egyben visszautal Tanítás, validáció és érintetlen teszt alfejezetre: a split is időponthoz kötött feltételeket használ, és Sokaság, minta és adatfelvételi folyamat alfejezetre: a múltból származó adat nem feltétlenül reprezentatív a jövőre. Önellenőrzés: minden feature-höz fel kell tudnom írni, mit tudtam volna a asof_cutoff pillanatában. Ha egy feature képlete bármilyen későbbi információt tartalmaz, a munkanaplóban meg kell jelölni, és a kísérletből ki kell zárni.

25.1.8 Rövid összefoglaló

A point-in-time join minden eseményhez az adott időpontban érvényes múltbeli rekordokat rendeli. Az ügyféltörténetből számolt expanding aggregátumok (rendelésszám, kosárátlag) a rendelés pillanatában ismertek. A késési arányhoz label window kell: csak azok a múltbeli rendelések számítanak, amelyek label_available_at értéke a predikciós cutoff előtt van. Az asof_cutoff audit oszlop minden sorra rögzíti a feature-számítás időpontját, így a szivárgás utólag ellenőrizhető. A következő alkalmazás a Kategória, hiány és ferde változó alfejezetben ezeket a new feature-öket skálázza, kódolja, és megvizsgálja a hiányzó értékek és a ferde eloszlások hatását.