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.
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
,
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.
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
Az invariant ellenőrzések:
asof_cutoff értéke megegyezik az adott sor
order_ts értékével;prior_late_rate mindenütt 0 és 1 közötti, és az első
rendelésnél 0;prior_order_count az ügyfél adott rendelése előtti
rendelések száma.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.
prior_order_count,
prior_basket_mean_minor és prior_late_rate
értékeket, és írd le a label window szerepét a nevezőben.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.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.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.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.
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.
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.
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.
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.
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.