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
,
azaz
;
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.
Legyen az az -edik megfigyelési egység -edik változójának értéke, ahol és . A missingness indicator (hiányzásjelző)
Itt bináris, dimenzió nélküli, az mátrix alakja , tehát ugyanaz, mint a nyers adaté. A -edik oszlop hiányzási aránya
ahol
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
ahol az adott csoport indexhalmaza, 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:
distance_km-ek például egy technikai kihagyás
véletlen eredményei lehetnének.distance_km hiánya gyakoribb lehet bizonyos
régiókban, de a tényleges távolságtól független.Kis kézi kontroll: hat ügyfél, a region megfigyelt, a
distance_km két sorban hiányzik.
A feltételes arány priority_flag=0 esetén
,
priority_flag=1 esetén ugyancsak
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ó.
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 , tehát
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
ahol és az alsó és felső kvartilis, az IQR pedig . 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.
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.pyWindows 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ó.
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.
É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?
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.
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.
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.
A megfigyelt táblából csak az
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?”
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á.
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.
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.
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 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 km, a déli pedig 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 -val töltés nem ártalmatlan skálázás, hanem a mérési szerződés megsértése.
A hiányzásjelző
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.