13.4 Hiány és anomália mint vizsgálati kérdés

13.4.1 Cél és konkrét helyzet

A Northstar-labor nyers rendeléstáblája, a lab/orders_raw.csv, szándékosan tartalmaz adatminőségi hibákat. Az adatszerződés szerint az alapgenerálásnál 2405 sor van: 5 extra duplikált sor, 2 hiányzó distance_km, 1 negatív basket_minor és 1 olyan sor, amelynek customer_id értéke nem található a customers.csv-ben. A megfigyelési egység egy rendelés, a hivatkozási kulcs az order_id, a pénzösszegek egész EUR-centben értendők. A feladat nem az, hogy gyorsan „megtisztítsuk” a táblát, hanem az, hogy a hiányt és anomáliát vizsgálati kérdésként kezeljük: készítsünk missingness-jelentést, kössük a hibákat a mérési szerződéshez, és hasonlítsunk össze két alternatív tisztítást anélkül, hogy automatikusan törölnénk minden gyanús sort.

Előzetes becslés: a hiányzási arány a distance_km oszlopban 2/2405≈0,000832/2405 \approx 0{,}00083, azaz 0,083%0{,}083\%; a duplikátumok eltávolítása 2400 sort hagy, mert az 5 extra sor másodpéldány. A 4 hibás sor törléses kezelése 2396 elemű elemzési táblát eredményez, tehát a tiszta orders.csv 2400 soros kontrolljához képest 4 rendelésnyi információvesztést.

13.4.2 Hiányzási mutató és mechanizmusfeltételezés

Legyen az xijx_{ij} az ii-edik megfigyelési egység jj-edik változójának értéke, ahol i=1,…,ni=1,\dots,n és j=1,…,pj=1,\dots,p. A missingness indicator (hiányzásjelző)

Rij={1,ha xij hiányzik,0,egyébként. R_{ij} = \begin{cases} 1, & \text{ha } x_{ij} \text{ hiányzik},\\ 0, & \text{egyébként}. \end{cases}

Itt RijR_{ij} bináris, dimenzió nélküli, az RR mátrix alakja (n,p)(n,p), tehát ugyanaz, mint a nyers adaté. A jj-edik oszlop hiányzási aránya

R‾j=1n∑i=1nRij=nmiss,jn, \bar R_j = \frac{1}{n}\sum_{i=1}^n R_{ij} = \frac{n_{\text{miss},j}}{n},

ahol nmiss,jn_{\text{miss},j} a hiányzó elemek darabszáma. Ha a hiányt egy csoportra, például priority_flag szerint szűkítve nézzük, a feltételes arány

R‾j(g)=∑i∈gRijng, \bar R_j^{(g)} = \frac{\sum_{i \in g} R_{ij}}{n_g},

ahol gg az adott csoport indexhalmaza, ngn_g a csoport elemszáma. A nevező itt a csoport összes sora, nem a hiányzók száma. A laborban ez a nevező különösen fontos, mert a duplikátumok megsokszorozhatják ugyanazt a rendelést.

A megfigyelt hiányzási minta önmagában nem mondja meg, miért hiányzik az adat. Három szokásos feltételezés:

Kis kézi kontroll: hat ügyfél, a region megfigyelt, a distance_km két sorban hiányzik.

R=[0,0,1,0,0,1]T,R‾=26=13.R = [0,0,1,0,0,1]^T,\qquad \bar R = \frac{2}{6} = \frac{1}{3}.

A feltételes arány priority_flag=0 esetén 1/31/3, priority_flag=1 esetén ugyancsak 1/31/3 lehet. Ez a megfigyelt táblán MCAR-mintázatnak látszik, de ugyanilyen mintát adhat egy MNAR-mechanizmus is, ha a 2 elveszett távolság éppen a legnagyobb távolságú megrendeléseké. A mechanizmus tehát nem a táblából, hanem a mérési folyamatból igazolható.

13.4.3 Anomália, tartományellenőrzés és outlier

A range check (tartományellenőrzés) a szerződésben rögzített, lehetséges értéktartományt vizsgálja. Formálisan az érvényes tartomány [aij,bij][a_{ij}, b_{ij}], tehát

aij≤xij≤bij. a_{ij} \le x_{ij} \le b_{ij}.

A laborban ilyen szabály például: distance_km >= 0, basket_minor >= 0, quantity_sum > 0, promised_days == 7, delivery_days > 0. A negatív basket_minor tartománysértés, mert a szerződés szerint a kosárérték nem lehet negatív. A distance_km hiánya viszont nem tartománysértés, hanem hiány.

A kiugró (outlier) olyan megfigyelt érték, amely a többitől szokatlanul távol esik, de a tartománysértéssel ellentétben lehetséges érték. A boxplot egy gyakori jelölési szabályt használ: a whisker határok

Q1−1,5(Q3−Q1)ésQ3+1,5(Q3−Q1),Q_1 - 1{,}5\,(Q_3 - Q_1) \qquad\text{és}\qquad Q_3 + 1{,}5\,(Q_3 - Q_1),

ahol Q1Q_1 és Q3Q_3 az alsó és felső kvartilis, az IQR pedig Q3−Q1Q_3 - Q_1. Ez az 1,5-ös szabály csak vizuális jelölés; egy nagyobb távolság lehet valós, üzletileg fontos rendelés, nem automatikus törlendő hiba.

13.4.4 Teljes minimális munkapélda

A példa a laborfájlok neveit használja; az útvonalat a projektgyökérhez igazítsuk. A pandas 2.2.3, NumPy, Matplotlib és Seaborn a lab/pyproject.toml alapfüggőségei közül való. A bemutatott kimenet szemléltető, mert e szöveg írása közben nem futott kód; a laborban a tanuló futtatásából származik a mért eredmény.

# Linux/macOS, munkakönyvtár a labor projektgyökere
uv run python missingness_report.py

Windows PowerShellben a futtatás előtt Set-Location C:\path\to\northstar-lab, majd ugyanez a uv run parancs.

"""Missingness-jelentés és két alternatív tisztítás érzékenységi összevetése."""
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

LAB = Path("lab")
RAW = pd.read_csv(LAB / "orders_raw.csv")
CUSTOMERS = pd.read_csv(LAB / "customers.csv")
REFERENCE = pd.read_csv(LAB / "orders.csv")  # kontroll, nem a tisztítás inputja

# 1. Szerződésellenőrzés: a labor alapméretei
print(RAW.shape)
# Elméletileg várt: (2405, 15)

duplicated_mask = RAW.duplicated()
missing_distance = RAW["distance_km"].isna()
negative_basket = RAW["basket_minor"] < 0
known_customer = RAW["customer_id"].isin(CUSTOMERS["customer_id"])

print(duplicated_mask.sum())       # várt: 5
print(missing_distance.sum())      # várt: 2
print(negative_basket.sum())       # várt: 1
print((~known_customer).sum())     # várt: 1

# 2. Missingness-jelentés
R = RAW.isna().astype(int)          # shape: (2405, 15)
missing_report = pd.DataFrame({
    "n_missing": R.sum(),
    "missing_rate": R.mean(),
})
print(missing_report.loc[missing_report["n_missing"] > 0])
# A distance_km hiányzási aránya: 2/2405, a többi oszlopé 0.

# 3. Tartomány- és hivatkozási ellenőrzés
range_checks = {
    "distance_km_negative": RAW["distance_km"] < 0,
    "basket_minor_negative": RAW["basket_minor"] < 0,
    "quantity_sum_nonpositive": RAW["quantity_sum"] <= 0,
    "promised_days_not_7": RAW["promised_days"] != 7,
    "delivery_days_nonpositive": RAW["delivery_days"] <= 0,
    "customer_unknown": ~known_customer,
}
range_report = pd.DataFrame({
    "n_violations": {k: int(mask.sum()) for k, mask in range_checks.items()}
})
print(range_report)
# A labor szerint a negatív basket és az ismeretlen customer jelenik meg.

# 4. Két alternatív tisztítás
base = RAW.loc[~duplicated_mask].copy()  # 2400 sor, 5 duplikált másodpéldány nélkül

bad_mask = (
    base["distance_km"].isna()
    | (base["basket_minor"] < 0)
    | ~base["customer_id"].isin(CUSTOMERS["customer_id"])
)

clean_drop = base.loc[~bad_mask].copy()      # A: hibás sorok listwise törlése

clean_keep = base.copy()                     # B: megtartás és hibajelző
clean_keep["distance_na"] = base["distance_km"].isna().astype(int)
clean_keep.loc[base["basket_minor"] < 0, "basket_minor"] = np.nan
clean_keep["unknown_customer"] = ~base["customer_id"].isin(CUSTOMERS["customer_id"])

# A másolás után a basket_minor float64 lehet, mert a NaN miatt.
# A distance_km hiánya és a target_late megfigyelése B-ben megmarad.

sensitivity = pd.DataFrame({
    "variant": ["raw_with_duplicates", "drop_bad_rows", "keep_and_flag"],
    "n_rows": [len(RAW), len(clean_drop), len(clean_keep)],
    "late_rate": [
        RAW["target_late"].mean(),
        clean_drop["target_late"].mean(),
        clean_keep["target_late"].mean(),
    ],
    "distance_mean": [
        RAW["distance_km"].mean(),
        clean_drop["distance_km"].mean(),
        clean_keep["distance_km"].mean(skipna=True),
    ],
})
print(sensitivity)

# 5. Ábra: a távolság eloszlása a megtartó változatban
fig, ax = plt.subplots()
sns.boxplot(data=clean_keep, x="priority_flag", y="distance_km", ax=ax)
ax.set_title("Szemléltető: distance_km priority_flag szerint")
fig.tight_layout()
plt.show()

A kód R = RAW.isna().astype(int) sora közvetlenül előállítja a hiányzásjelző mátrixot. A range_report egyes szabályai a szerződött tartományokat ellenőrzik; a negatív distance_km szabály a 2 hiányzó sort nem jelöli, mert a NaN < 0 értéke False. A clean_keep változat a negatív kosárértéket hiányként jelöli, de a sor többi értékét megtartja, így a target_late arány a 2400 soros alapon számítható.

13.4.5 Eredmény értelmezése

A labor szerződött invariánsai adják a kézi kontrollt: a nyers tábla 2405 sor, a duplikált sorokból 5, a distance_km hiányból 2, a negatív kosárból 1, az ismeretlen ügyfélből 1 található. A RAW.drop_duplicates() 2400 sort ad, de ez a 2400 sor még nem azonos a tiszta orders.csv-vel, mert a 4 hibás sor javítatlan marad. A drop_bad_rows változat 2396 sort hagy; ez a 4 hibás sor eltávolításának következménye. A distance_km hiányzási arányának nevezőjét a duplikátumok nélkül kell értelmezni: 2 hiány 2400 rendelésből, nem 2405 sorból.

A két alternatíva közötti érdemi különbség a 4 hibás sor. Az A változat konzervatív, de a hibás sorok eldobásával minden oszlopra nézve elveszít 4 rendelést. A B változat a hibajelzőket megtartja, ezért a target_late arányt a teljes 2400 rendelésen becsüli, de a distance_km-et érintő elemzéseknél a 2 hiány továbbra is kimarad. A distance_km átlagban a nyers tábla és a megtartó változat között kicsi a különbség, mert a negative basket és az ismeretlen ügyfél nem érinti a távolságot. Az A és B közötti late rate különbsége nem a módszer által garantált irányú: azt a konkrét sorok target_late értéke határozza meg, ezért az összevetést mindig a labor tényleges futtatásából kell kiolvasni.

A 2 hiányzó distance_km statisztikai értelemben túl kevés ahhoz, hogy a hiányzást priority_flag vagy más változó szerinti csoportosítással megbízhatóan mechanizmushoz kössük. A missingness-jelentés ezért itt főként dokumentációs eszköz, nem mechanizmusteszt.

Az MCAR, MAR és MNAR kategóriák a hiányzási mechanizmus feltevései, nem olyan címkék, amelyek az egyetlen megfigyelt táblából bizonyossággal megállapíthatók. A teljes esetek elemzése ezért érzékenységvizsgálatot igényelhet.

13.4.6 Feladatok

  1. Értelmezés. A megfigyelt táblán a distance_km hiánya a priority_flag=1 csoportban sokkal gyakoribbnak látszik, mint a másikban. Miért nem jelenti ez automatikusan azt, hogy a hiány MAR vagy MNAR? Milyen mérési folyamatból származó, táblán kívüli ellenőrzést kérne?

  2. Implementálás. Írjon missingness_report(df) függvényt, amely a bemenő DataFrame minden oszlopára visszaadja a hiányzó darabszámot és a hiányzási arányt. Ezután a distance_km-re adjon feltételes hiányzási arányt priority_flag szerint, és adja meg pontosan a nevezőt.

  3. Hibakeresés. Egy kolléga a nyers adatot így tisztítja: orders_raw.dropna().copy(), majd a hiányzó distance_km-et nullára cseréli: df["distance_km"].fillna(0). Miért hibás mindkét lépés? A labor szerződésére hivatkozva indokolja, miért nem tekinthető a 0 távolságnak.

  4. Megváltozott feltétel. Tegyük fel, hogy a distance_km hiánya éppen akkor valószínűbb, ha a csomag késni fog, és a hiány a nagy távolságú rendeléseknél gyakoribb. Melyik alternatív tisztítás torzítaná kevésbé a target_late arány becslését? A távolságra épülő modellnél ugyanez a választás elegendő-e? Válaszát a 4 hibás sor szerepére alapozza.

13.4.7 Megoldások és indoklás

  1. A megfigyelt táblából csak az RR minta látszik, a hiányzó tényleges distance_km értékek nem. Ugyanazt a feltételes arányt előállíthatja az, hogy a prioritásos rendelések feldolgozása más rendszerben történt (MAR), de az is, hogy a kritikus távolságoknál hiányos a mérés (MNAR). A generátor forráskódja a laborban megmondja a tényleges mechanizmust, egy üzleti rendszerben viszont a napló, a mérési dokumentáció vagy egy reprodukált bevitel kell. Az ellenőrző kérdés: „Mikor és melyik rendszer írta ezt a mezőt üresen?”

  2. A függvény:

def missingness_report(df):
    n = len(df)
    return pd.DataFrame({
        "column": df.columns,
        "n_missing": df.isna().sum().to_numpy(),
        "missing_rate": (df.isna().sum() / n).to_numpy(),
    })

report = missingness_report(RAW)

clean_base = RAW.drop_duplicates().copy()
clean_base["distance_na"] = clean_base["distance_km"].isna().astype(int)
conditional = (
    clean_base
    .groupby("priority_flag")["distance_na"]
    .agg(n_missing="sum", n_group="count", missing_rate="mean")
)

A nevező a priority_flag adott csoportjának összes rendelése, nem a hiányzó értékek száma. A duplikátummentes clean_base használata azért lényeges, mert a duplikátum mindkét csoport nevezőjét megváltoztathatná.

  1. A dropna() az egész DataFrame minden oszlopára törölne minden olyan sort, ahol bármelyik mező hiányzik. A laborban most csak a distance_km hiányos, ezért 2 sort dobna el; viszont a művelet a jövőbeli oszlopok hiányára is automatikusan vonatkozna, és nem dokumentálná, hogy melyik mező miatt vesztettünk sort. A fillna(0) ettől súlyosabb: új adatot talál ki. A distance_km = 0 egy konkrét, ténylegesen kis távolságú rendelést jelent; a szerződésben a nullát nem szabad távolságnak tekinteni. A hiányt NaN-ként kell megtartani, vagy a mechanizmusfeltételezés szerint külön kezelni.

  2. A target_late arány becsléséhez a keep_and_flag jobb, mert a célváltozó a 4 hibás soron is megfigyelt, és a teljes 2400 rendelésen számol. A drop_bad_rows 4 rendelést eldob; ha a 4 hiba éppen a késő kézbesítéssel függ össze, az arány torzulhat. A távolságmodellnél ez kevés: a 2 hiányzó distance_km a megtartó változatban is hiányként marad, tehát az elemzés csak 2398 soron használhatja a távolságot. A korrekt megoldás ilyenkor a hiányzó távolság pótlása a mérési folyamatból, nem a sor csendes törlése, vagy tudatos MNAR-modell és bizonytalanság jelentése.

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

Gyakori tévedés, hogy a megfigyelt hiányzási mintából azonosítani lehet a mechanizmust. Vegyünk két régiót, mindkettőben 3 rendelést:

region priority_flag distance_km
north 0 12
north 1 15
north 0 hiány
south 1 20
south 0 24
south 1 hiány

A táblában mindkét régióban 1/31/3 a hiányzási arány. Ez MCAR-ral éppúgy konzisztens, mint azzal, hogy az északi hiányzó távolság valójában 55 km, a déli pedig 6060 km, és a nagy távolságokat nem rögzítette a rendszer. A tábla ugyanaz; a mechanizmus mégis különböző. Ezért a hiányzási jelentésnek mindig fel kell tüntetnie a feltételezést, nem csak a százalékot. A Diagnózis és első teljes munkamenet diagnosztikai felosztása itt is segít: ez adatértelmezési kérdés, nem pusztán programozási hiány. A Stabil numerika és érzékenység numerikus fegyelme pedig arra figyelmeztet, hogy a 00-val töltés nem ártalmatlan skálázás, hanem a mérési szerződés megsértése.

13.4.9 Rövid összefoglaló

A hiányzásjelző RR bináris mátrix, amelyből arányokat és csoportos feltételes arányokat számítunk; a mechanizmus azonban MCAR, MAR vagy MNAR feltételezés, nem a táblából bizonyított tény. A tartományellenőrzés a szerződésben lehetetlen értékeket jelöli, a kiugró ezzel szemben lehetséges, de szokatlan érték. A laborban 5 duplikált sor, 2 hiányzó distance_km, 1 negatív basket_minor és 1 ismeretlen ügyfél szerepel; a törléses és megtartásos tisztítás eltérő információszerkezetű, ezért érzékenységi összevetése kötelező. A 0-val töltés új adatot gyárt, a dropna() pedig nem dokumentálja a törlés okát. A következő Esemény, feltételes valószínűség és Bayes a bináris jelzőket eseményként és feltételes valószínűséggel vizsgálja.