24.3 Anomália és ritka esemény felügyelet nélkül

24.3.1 Cél és konkrét helyzet

A Northstar adathalmazon a korábbi klaszterező fejezet csoportokat írt le; most a ritka, emberi vizsgálatra érdemes rendeléseket keressük. A megfigyelési egység egy rendelés (orders.csv, 2400 sor). A bemenet rendelésenként öt, a rendelés pillanatában ismert numerikus mező:

A kimenet egy rangsorolt vizsgálati lista és az első kk tétel ellenőrzött találati aránya, azaz precision at k. A laborban 36 szintetikus eltérést ültetünk a tiszta adatba, így a „emberi validáció” a tanulási környezetben ismert ground truth lesz; éles projektben ezt a szerepet üzleti szakértő vagy adatgazda tölti be.

Induló becslés, mielőtt a módszert olvasná: ha 2400 rendelésből 36 valóban vizsgálandó, és véletlenszerűen nézünk meg 48 sort, akkor az első 48 találati arányának várható értéke p=36/2400=0.015p = 36/2400 = 0.015, és várhatóan mindössze 48⋅0.015=0.7248 \cdot 0.015 = 0.72 találatunk lenne. A modelltől azt várjuk, hogy ennél lényegesen jobb legyen. A lehetséges maximum P@48≤36/48=0.75P@48 \le 36/48 = 0.75, mert összesen csak 36 megerősíthető anomália van.

24.3.2 Fogalmak: anomália, IsolationForest, contamination, ranking, P@k

Az anomália (anomaly vagy outlier) itt nem erkölcsi ítélet: olyan megfigyelés, amely a referenciaeloszlástól feltűnően eltér. A „feltűnően” mindig a választott modell és változókészlet szerint értendő. Az anomáliapontszám tehát vizsgálati feladat, nem automatikus csalás- vagy adathibacímke.

Az IsolationForest (izolációs erdő) véletlen döntési fákból építkezik, de nem célváltozót tanul. Minden fa egy véletlen részmintán, véletlen jellemzővágásokkal növekszik. Egy pont úthossza az a splitmegosztási mélység, ahol a pont egyedül marad a levélben. Az extrém pontot kevés vágás izolálja, mert az értéktartomány szélén egyetlen jó vágásra van szükség. A pontszám az átlagos faúthosszból képzett:

s(x,n)=2−E[h(x)]/c(n), s(x,n)=2^{-E[h(x)]/c(n)},

ahol h(x)h(x) egy-egy fa úthossza, E[h(x)]E[h(x)] az erdő fáira vett átlag, c(n)c(n) az nn elemhez tartozó normalizáló, elméleti átlagos úthossz. A magas ss rövid átlagos utat, tehát eltérő pontot jelent. A scikit-learn decision_function visszatérési előjele ezzel ellentétes konvenciót használ, ezért a kódban score = -decision_function(X) adja a magas értéknél gyanúsabb pontszámot.

Két használati mód van, és a két angol szó nem felcserélhető:

A mi laborunk outlier mód, mert a vizsgálandó állomány már tartalmazhat anomáliát. A contamination paraméter az előzetesen feltételezett anomáliarány; itt 36/2400 = 0.015. Ez nem címkézett arány, hanem a bináris döntési küszöb eltolása. A folytonos pontszámból képzett rangsor szempontjából a contamination csak egy konstans eltolás, ezért az első kk tétel kiválasztását nem a predict bináris címkéjére, hanem a pontszám szerinti rendezésre bízzuk.

A precision at k a vizsgálandó lista első kk elemének megerősített anomáliaránya:

P@k=1k∑i=1kyri, P@k = \frac{1}{k}\sum_{i=1}^{k} y_{r_i},

ahol rir_i a pontszám szerinti ii-edik sor indexe, yri∈{0,1}y_{r_i}\in\{0,1\} a ground truth anomáliajelző. A metrika tartománya [0,1][0,1], a nevező mindig az ellenőrzött sorok száma kk, nem az összes sor és nem is az összes anomália. Ha mm valódi anomália van és m<km<k, akkor P@k≤m/kP@k \le m/k.

A ritka esemény bizonytalansága visszaköszön: egy véletlen lista találati arányának szórása közelítőleg

SE≈p(1−p)k, SE \approx \sqrt{\frac{p(1-p)}{k}},

ahol p=m/np=m/n az alapráta. A laborban p=0.015p=0.015 és k=48k=48 mellett SE≈0.0175SE\approx 0.0175, tehát a véletlen összehasonlítási alap alacsony, de egyetlen kis kk-s mérés zajos.

24.3.3 Minimális munkapélda

Mentsük a következő kódot a projektgyökérbe s095_anomaly_review.py néven, és futtassuk ugyanonnan, ahol a lab/ mappa található:

# Linux/macOS, projektgyökér
python s095_anomaly_review.py
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest

rng = np.random.default_rng(20261020)
orders = pd.read_csv("lab/orders.csv")

feature_cols = [
    "distance_km", "priority_flag", "basket_minor",
    "quantity_sum", "shipping_fee_minor",
]
X = orders[feature_cols].to_numpy(dtype=float)
n, p = X.shape
y_true = np.zeros(n, dtype=int)

# 36 szintetikus, emberi vizsgálatra érdemes eltérés
idx = rng.choice(n, size=36, replace=False)
idx_distance = idx[:12]
idx_basket = idx[12:24]
idx_quantity = idx[24:36]

X[idx_distance, 0] += rng.uniform(500, 900, size=12)
X[idx_basket, 2] *= rng.uniform(6, 10, size=12)
X[idx_quantity, 3] += rng.uniform(40, 80, size=12)
y_true[idx] = 1

model = IsolationForest(
    n_estimators=200,
    max_samples=256,
    contamination=36 / n,
    random_state=7,
)
model.fit(X)

score = -model.decision_function(X)  # magas = gyanúsabb
rank = np.argsort(-score)
k = 48
top_k = rank[:k]
p_at_k = y_true[top_k].mean()

review = pd.DataFrame({
    "rank": np.arange(1, k + 1),
    "order_id": orders.iloc[top_k]["order_id"].to_numpy(),
    "anomaly_score": score[top_k],
    "simulated_human_decision": np.where(
        y_true[top_k] == 1, "confirmed", "not_confirmed"
    ),
})

print(f"X shape={X.shape}, injected={y_true.sum()}, "
      f"P@48={p_at_k:.3f}")
print(review.head(10).to_string(index=False))

Szemléltető kimenet (a tényleges futtatás ettől eltérhet; e könyvfejezet írásakor a kód helyben nem futott):

X shape=(2400, 5), injected=36, P@48=0.562
 rank order_id  anomaly_score simulated_human_decision
    1 ORD-0187      0.732000                confirmed
    2 ORD-1104      0.711000                confirmed
    3 ORD-0442      0.708000            not_confirmed
    4 ORD-1931      0.701000                confirmed
...

24.3.4 Eredmény értelmezése

A P@48 a vizsgált 48 sorból megerősített találatok aránya; a fenti szemléltető érték 0.562, azaz körülbelül 27 valódi eltérést talált az első 48 helyen. Ez jóval a véletlen 0.015 alap felett van, és nem lépi át a 0.75 elméleti maximumot. Ha a számításból 0.75-nél nagyobb érték jönne ki, az a ground truth-összerendelés hibájára utalna; ha 0.015 körüli, akkor a modell nem tanult rangsorolható jelet, vagy a rangsor véletlen.

Az invariánsok: X.shape = (2400, 5), score.shape = (2400,), rank.shape = (2400,), review 48 sorból áll, és review["simulated_human_decision"] csak a szimulált ellenőrző címke. A contamination explicit megadása nem befolyásolja a rangsort, de a predict kimenetét igen; ezért a labor a folytonos score-t és az argsort(-score) sorrendet használja.

A három injektált torzítási mód különböző feature-ökben jelentkezik. A távolság és a kosárérték szélsőségei valószínűleg korán izolálódnak, a darabszám-eltolás kevésbé domináns lehet, mert a normál rendelések darabszáma is változékony. Az emberi vizsgálati lista mellé ezért érdemes a nyers feature-értékeket is kirakni; a pontszám önmagában nem magyarázza meg, mit érdemes nézni.

Felügyelet nélküli anomáliapontszám nem azonos a csalás vagy késés valószínűségével. A contamination paraméter gyakran a kijelölés küszöbét befolyásolja; a valódi üzleti pozitív prediktív érték címkézett auditmintát igényel.

24.3.5 Feladatok

24.3.5.1 1. feladat — P@k kézi számítása

Egy hat soros halmaz anomáliapontszám szerinti sorrendje: [2, 0, 1, 3, 4, 5]. A teljes y ground truth vektor: [0, 0, 1, 0, 1, 0], azaz csak a 2. és 4. indexű sor valódi anomália. Számítsa ki a P@3 és a P@6 értékét, és magyarázza el, miért lehet fontos a P@3 akkor is, ha a teljes találati halmazt P@6 már megmutatja.

24.3.5.2 2. feladat — A labor kicsit más paraméterrel

Módosítsa a minimális példát: használjon rng = np.random.default_rng(99)-ot, injektáljon 24 anomáliát úgy, hogy 8 távolság-, 8 kosár- és 8 darabszám-eltérés legyen, majd illesszen 100 fás modellt max_samples=128 és random_state=11 mellett. Válasszon k=24 listát, és írassa ki a P@24 értéket. Mi az elméleti összehasonlítási alap és mi a metrika felső határa itt? Ellenőrizze le, hogy y_true.sum()==24.

24.3.5.3 3. feladat — Hibadiagnózis

Egy változat nem a folytonos pontszám szerint rendez, hanem ezt teszi:

flag = model.predict(X) == -1
suspects = np.flatnonzero(flag)[:k]
p_at_k = y_true[suspects].mean()

A mérés P@24=0.08 lesz, miközben a pontszám szerinti rangsor sokkal jobb. Nevezze meg a két lehetséges hibát ebben a kódrészletben: miért nem elég a predict bináris kimenete, és milyen sorrendben szerepelnek az így kiválasztott sorok? Írja át a kiválasztást úgy, hogy a legmagasabb anomáliapontszámú első 24 sor kerüljön a listába.

24.3.5.4 4. feladat — Transzfer novelty módra

Tegyük fel, hogy a rendeléskezelő csapat egy tiszta referencia-időszakot ad: a lab/orders.csv első 1200 sora bizonyítottan átnézett, ismert üzleti tartomány. A következő 1200 sorra viszont új, címkézetlen rendelésekként tekintünk. Injektáljon az új részbe 5 erős kosár-anomáliát (basket_minor * 10) a rng = np.random.default_rng(11) használatával. Tanítson novelty=True, 150 fás modellt a tiszta referencia-részen, pontozza az új részt, és mérje a P@5 értéket. Fogalmazza meg, miben más a contamination szerepe ebben a megváltozott feltételben.

24.3.6 Megoldások és indoklás

24.3.6.1 1. megoldás

A rangsor első három sora a 2., 0. és 1. index. A ground truth-értékek: y2=1y_2=1, y0=0y_0=0, y1=0y_1=0, ezért

P@3=1+0+03=13≈0.333. P@3 = \frac{1+0+0}{3} = \frac{1}{3} \approx 0.333.

A P@6 valamennyi sorra (1+0+0+0+1+0)/6=2/6=1/3(1+0+0+0+1+0)/6=2/6=1/3. A P@3 azért fontos, mert a gyakorlatban csak egy kis ellenőrzési költségkeret áll rendelkezésre; a jó modelltől az első, azonnal ellenőrzött tételeknél várunk koncentrált találatot.

24.3.6.2 2. megoldás

A minimális példából kiindulva a változtatások:

rng = np.random.default_rng(99)
idx = rng.choice(n, size=24, replace=False)
X[idx[:8], 0] += rng.uniform(500, 900, size=8)
X[idx[8:16], 2] *= rng.uniform(6, 10, size=8)
X[idx[16:24], 3] += rng.uniform(40, 80, size=8)
y_true[idx] = 1

model = IsolationForest(
    n_estimators=100,
    max_samples=128,
    contamination=24 / n,
    random_state=11,
)
model.fit(X)
score = -model.decision_function(X)
top_k = np.argsort(-score)[:24]
p_at_k = y_true[top_k].mean()
print(f"P@24={p_at_k:.3f}, injected={y_true.sum()}")

A véletlen alap most p=24/2400=0.01p=24/2400=0.01, az ehhez tartozó szórás SE≈0.01⋅0.99/24≈0.0203SE\approx\sqrt{0.01\cdot0.99/24}\approx0.0203. A felső határ 24/24=124/24=1, mert 24 valódi anomália van és 24 sort vizsgálunk. A y_true.sum()==24 assert az injektálás konzisztenciáját ellenőrzi. Numerikus reprodukálhatósághoz a seedeket rögzítettük, de platformokon átívelő bitazonos output nem garantálható; az elvárt viselkedés a véletlen alapnál számottevően magasabb találati arány.

24.3.6.3 3. megoldás

A predict a folytonos pontszámot egy küszöbbel binárissá vágja: a küszöb feletti sorok mind -1-et kapnak, de a küszöbön belül nincs további sorrend. Az np.flatnonzero(flag)[:k] emellett az eredeti sorrend első kk megjelölt elemét veszi, nem a legszélsőségesebbeket. Ezért a válogatásban erős anomáliák maradhatnak ki, és a lista elejére véletlenszerűen kerülhetnek gyenge gyanús elemek. A helyes kiválasztás:

score = -model.decision_function(X)
top_k = np.argsort(-score)[:k]
p_at_k = y_true[top_k].mean()

A küszöbválasztás akkor jó, ha egy emberi munkafolyamat fix riasztási szabályt akar (predict == -1), de rangsorolt vizsgálati listához nem alkalmas.

24.3.6.4 4. megoldás

Egy lehetséges megoldás:

orders = pd.read_csv("lab/orders.csv")
feature_cols = [
    "distance_km", "priority_flag", "basket_minor",
    "quantity_sum", "shipping_fee_minor",
]
X_train = orders.iloc[:1200][feature_cols].to_numpy(dtype=float)
X_test = orders.iloc[1200:].copy()[feature_cols].to_numpy(dtype=float)

rng = np.random.default_rng(11)
y_test = np.zeros(len(X_test), dtype=int)
idx = rng.choice(len(X_test), size=5, replace=False)
X_test[idx, 2] *= 10.0
y_test[idx] = 1

model = IsolationForest(
    n_estimators=150,
    novelty=True,
    contamination=0.01,
    random_state=3,
)
model.fit(X_train)
score = -model.decision_function(X_test)
top5 = np.argsort(-score)[:5]
print(f"P@5={y_test[top5].mean():.3f}, injected={y_test.sum()}")

Novelty módban a contamination a bináris küszöböt hangolja a tiszta referencia adathalmazon; nem a teszt anomáliáinak ismert aránya. Itt azért használunk folytonos score-rangsort és P@5-öt, mert a feladat nem küszöbös riasztás, hanem kis vizsgálati lista. A küszöb beállításához éles környezetben külön validációs, lehetőleg emberileg ellenőrzött referencia kell. A P@5 maximuma itt 1, mert 5 valódi anomália van, és ennyi sort nézünk meg.

24.3.7 Gyakori tévedés és önellenőrzés

A leggyakoribb hiba az a rövidzárlat, hogy „magas anomáliapontszám, tehát csalás vagy hibás adat”. A Northstar példán: a priority_flag=1 rendeléseknél a magas shipping_fee_minor kifejezetten várható lehet, hiszen a gyorsított szállítás többletköltséget hordozhat. Ha a modell ilyen sort emel ki, az emberi validáció helyes döntése lehet „legitim üzleti érték”, nem adathiba. Az anomáliapontszám feladata a ritka, magyarázatra szoruló eset vizsgálati listára tétele, nem az ok automatikus megállapítása.

Önellenőrzésre érdemes kérdések: mi a megfigyelési egység, és nem duplázódik-e a pontszám egy join miatt? A P@k nevezője mindig az ellenőrzött sorok száma, nem az összes anomália. A kontaminációt nem a tesztvalidáció után szabad úgy hangolni, hogy a P@k maximális legyen; az adaptív választás a Hiperparaméter és nested értékelés alfejezetben tanult optimista becsléshez vezet. A score nem kalibrált valószínűség; a ritka esemény miatt a kis kk mellett a találati arány zajos, ahogy a Várható érték és Monte Carlo közelítés Monte Carlo-hibája is mutatta. Végül az IsolationForest csak a numerikus mezőkön lát; a customer_id érvényességéhez vagy a duplikált sorokhoz külön adatminőségi szabályok kellenek.

24.3.8 Rövid összefoglaló

Az anomáliakeresés emberi vizsgálati listát állít elő, nem automatikus ítéletet. Az IsolationForest rövid faúthosszal pontozza a nehezen izolálható, eltérő rendeléseket; a folytonos pontszámból képzett rangsor az ellenőrzési költséget az első kk tételre összpontosítja. A laborban szintetikus eltérésekkel mért P@k a megerősített találatok aránya az ellenőrzött listán, és mindig a véletlen alaphoz viszonyítjuk. A contamination a bináris küszöb feltevése, a rangsort lényegében nem változtatja. A következő alkalmazás a VI. ellenőrző pont lesz, ahol ezt a vizsgálati listát, a pontszám-rangsort és az emberi döntési naplót szivárgásmentes felügyelt modellkísérletbe kell illeszteni.