A Northstar labor data/raw/orders_raw.csv fájlja
szándékosan szennyezett rendelési adat. Egy sor egy rendelés
(order), a kulcsnak szánt mező az order_id. A
laborleírás szerint a fájl 2405 sort és 15 oszlopot tartalmaz, és négy
szándékos hibacsoportot rejt: öt duplikált sor, két hiányzó
distance_km, egy negatív basket_minor és egy
olyan customer_id, amely nem létezik a
customers.csv-ben.
Ebben az alfejezetben a feladat az, hogy ne a pandas automatikus
típusfelismerésére bízzuk az adat értelmezését, hanem explicit sémával
töltsük be, és mezőnként hibajegyzéket készítsünk. A kimenet három
dolog: a betöltött DataFrame, egy mezőnkénti sémajelentés,
és egy javítási szabálygyűjtemény, amely nem felülírja, hanem hibakóddal
elkülöníti a problémás sorokat. Előzetes becslés: 2405 sor, a fenti négy
hibacsoport darabszámai, valamint az, hogy a hibás sorok nem átfedőek. A
futtatott kimenetet itt „elméletileg várt” jelöléssel adjuk; a lab
tényleges futása a manifesttel ellenőrizhető.
Adattáblában egy sor egy megfigyelés (observation), egy
oszlop pedig egy változó (variable). A distance_km
numerikus vagy kvantitatív változó: két érték összege, átlaga vagy
különbsége értelmes, mértékegysége km. A currency ezzel
szemben nominális vagy kategorikus változó: nem rendezhető nagyság
szerint, és nincs értelmes számtani középérték. A region
típusú oszlop ugyancsak nominális. Numerikus oszlopoknál a séma része a
megengedett tartomány; például a basket_minor egész
EUR-cent, és a labor-szerződés szerint nem lehet negatív, a
distance_km-nek 0-nál nagyobbnak kell lennie. Kategorikus
oszlopnál viszont a séma a megengedett kategóriák halmaza:
currency itt kizárólag EUR.
Egy kis háromsoros példán látható a különbség:
| order_id | customer_id | basket_minor | distance_km | currency |
|---|---|---|---|---|
| A1 | C1 | 150 | hiány | EUR |
| A2 | hiány | érvénytelen szöveg | -2 | EUR |
| A3 | C1 | hiány | 10 | EUR |
A basket_minor-t összegezhetjük: 150 + ? + ? csak a
hiányok megoldása után értelmes. A currency-t nem tudjuk
„átlagolni”, legfeljebb azt kérdezhetjük, hány sor EUR.
A pandas pd.read_csv alapértelmezésben először mintát
olvas, és oszloponként megtippeli a típust. Ez csendes kockázat: ha egy
oszlop később kap egy hiányzó értéket, megváltozhat a dtype, és a kód
más ágon fut. Az explicit séma (explicit schema) ehelyett előre
rögzíti minden oszlop nevét, típusát, mértékegységét és érvényes
tartományát.
A pandas 2.2.3-ban a hagyományos int64 nem tud hiányzó
értéket tárolni, ezért a pandas a hiányossá vált egész oszlopot gyakran
float64-re konvertálja. A nullable típusok ezt oldják meg:
az Int64, Float64, string és
boolean dtype a hiányt egységesen a pd.NA
jelölővel tárolja. A category dtype kategorikus oszlophoz
való: memóriában a szintkészletet tárolja, és kódszámokkal hivatkozik a
sorokra. A parse_dates paraméter az ISO-szövegeket
datetime64[ns, UTC] típusú időbélyeggé alakítja, mert a
CSV-ben explicit +00:00 offset szerepel.
A tisztaz orders.csv-hez tehát ilyen betöltés
tartozhat:
import pandas as pd
from pathlib import Path
# Notebook-példa: a munkakönyvtár a lab projektgyökere.
ROOT = Path.cwd().resolve()
RAW = ROOT / "data" / "raw"
orders = pd.read_csv(
RAW / "orders.csv",
dtype={
"order_id": "string",
"customer_id": "string",
"promised_days": "Int64",
"quantity_sum": "Int64",
"item_rows": "Int64",
"basket_minor": "Int64",
"shipping_fee_minor": "Int64",
"currency": "category",
"distance_km": "Float64",
"priority_flag": "Int64",
"delivery_days": "Int64",
"target_late": "Int64",
},
parse_dates=["order_ts", "actual_delivery_at", "label_available_at"],
)A dtype szótárban nem szereplő, de
parse_dates által kezelt oszlopok automatikusan datetime
típust kapnak. A dataset szerződése szerint a tiszta fájl
2400 sort tartalmaz, ezért az első ellenőrzés:
assert orders.shape == (2400, 15)
assert str(orders["distance_km"].dtype) == "Float64"
assert str(orders["order_id"].dtype) == "string"
assert pd.api.types.is_datetime64tz_dtype(orders["order_ts"])Scriptben ne a Path.cwd()-re építsünk, hanem a 2.1-ben tanult
Path(__file__).resolve() alapú projektgyökér-levezetésre;
itt a notebook munkakönyvtára rögzített szerződés.
A nyers CSV-ben három különböző probléma jelenhet meg, és ezeket nem szabad összemosni:
NaN vagy pd.NA jelöli.""
van. Az alapértelmezett read_csv ezt a
keep_default_na=True miatt hiánynak tekinti; ha auditálunk,
érdemes keep_default_na=False-szal betölteni, hogy az üres
szöveg megmaradjon.-2 a
distance_km oszlopban, abc a
basket_minor-ban, vagy ismeretlen
customer_id.A három kategóriát egy mini CSV-n kézzel ellenőrizhetjük:
import io
mini = pd.read_csv(
io.StringIO(
"order_id,customer_id,basket_minor,distance_km,currency\n"
"A1,C1,150,,EUR\n"
"A2,,abc,-2,EUR\n"
"A3,C1,,10,EUR\n"
),
dtype={"order_id": "string", "customer_id": "string", "currency": "category"},
keep_default_na=False,
)Ebben a betöltésben az A1 sorbeli üres
distance_km hiány, az A2 sorbeli üres
customer_id üres szöveg, az A2 sorbeli
abc érvénytelen numerikus szöveg, és az A2
sorbeli -2 érvénytelen tartomány. Ha megnézzük a dtypes-ot,
a basket_minor object vagy string lesz, mert vegyesen
tartalmaz számot és szöveget; a distance_km viszont
float64, mert a hiány NaN-ként fér el benne.
Az explicit Int64 konverziós terv csak az érvénytelen
szöveg elkülönítése után hajtható végre.
A laborfájlra két lépésben dolgozunk. Először explicit nullable
sémával betöltjük az orders_raw.csv-t, hogy a hiány és a
tartományhiba egyaránt látszódjon:
raw = pd.read_csv(
RAW / "orders_raw.csv",
dtype={
"order_id": "string",
"customer_id": "string",
"promised_days": "Int64",
"quantity_sum": "Int64",
"item_rows": "Int64",
"basket_minor": "Int64",
"shipping_fee_minor": "Int64",
"currency": "category",
"distance_km": "Float64",
"priority_flag": "Int64",
"delivery_days": "Int64",
"target_late": "Int64",
},
parse_dates=["order_ts", "actual_delivery_at", "label_available_at"],
)A labor-szerződés szerint a raw fájlban a numerikus oszlopok nem
tartalmaznak érvénytelen szöveget, ezért ez a dtype dict nem dob
konverziós hibát. A hiányos distance_km-ből
pd.NA lesz. Most definiáljunk sémajelentést:
def schema_report(df, rules):
"""Mezőnkénti dtype, hiány, egyediség és tartalomhiba jelentés."""
rows = []
for col in df.columns:
s = df[col]
invalid = 0
if col in rules:
# A szabályt csak a nem hiányzó értékekre alkalmazzuk.
invalid = int((s.notna() & ~rules[col](s)).sum())
rows.append(
{
"column": col,
"dtype": str(s.dtype),
"missing": int(s.isna().sum()),
"unique_non_null": int(s.dropna().nunique()),
"invalid_non_null": invalid,
}
)
return pd.DataFrame(rows).set_index("column")
rules = {
"promised_days": lambda s: s.eq(7),
"quantity_sum": lambda s: s.ge(0),
"item_rows": lambda s: s.ge(1),
"basket_minor": lambda s: s.ge(0),
"shipping_fee_minor": lambda s: s.ge(0),
"distance_km": lambda s: s.ge(0),
"priority_flag": lambda s: s.isin([0, 1]),
"delivery_days": lambda s: s.ge(1),
"target_late": lambda s: s.isin([0, 1]),
}
report = schema_report(raw, rules)
print(report)A hibajegyzéknek a kulcs-utalást is ellenőriznie kell. A
customer_id csak akkor érvényes, ha szerepel a
customers.csv ügyféllistájában:
customers = pd.read_csv(RAW / "customers.csv", dtype={"customer_id": "string"})
unknown_customer = (
raw["customer_id"].notna()
& ~raw["customer_id"].isin(customers["customer_id"])
)
duplicated_row = raw.duplicated()Ezek után hibakód-oszlopot készítünk a sorszintű diagnózishoz:
issue = pd.Series("", index=raw.index, dtype="string")
issue[raw["distance_km"].isna()] = "missing_distance_km"
issue[(raw["basket_minor"] < 0).fillna(False)] = "invalid_basket_minor_negative"
issue[unknown_customer] = "unknown_customer"
# A duplikátum sorszintű hiba; a pandas adatminőségi tárgyalás s023.
issue[duplicated_row] = "duplicate_row"
raw_with_issue = raw.assign(issue_code=issue)A javítási szabály nem csendes javítás, hanem osztályozás:
repair_rules = {
"missing_distance_km": {
"action": "exclude",
"reason": "missing measurement, no safe zero-imputation",
},
"invalid_basket_minor_negative": {
"action": "exclude",
"reason": "violates minor-unit range",
},
"unknown_customer": {
"action": "exclude",
"reason": "fails customer key constraint",
},
"duplicate_row": {
"action": "keep first",
"reason": "row-level duplicate, review in s023",
},
}A nyers fájlt soha nem írjuk felül; a javított vagy szűrt változat a
data/derived alá kerülne.
A teljes munkapélda kimenete automatikusan még nem ellenőrzött, ezért
szemléltető, elméletileg várt jellegű. Az orders_raw.csv
betöltése után a következő invariánsoknak kell teljesülniük:
raw.shape == (2405, 15).raw["distance_km"].isna().sum() == 2: a két szándékos
hiány.raw["basket_minor"].lt(0).sum() == 1: az egy szándékos
negatív érték.unknown_customer.sum() == 1: az egy ismeretlen
ügyfél.raw.duplicated().sum() == 5: a szándékos
duplikátumsorok.missing_distance_km és az
invalid_basket_minor_negative szűrése után a
raw_with_issue["issue_code"].eq("").sum() == 2402.issue_code string dtype, nem
object; a hiányzó distance_km a nullable
Float64-ben pd.NA, nem NaN.A saját gépen futtatva ellenőrizzük
raw_with_issue["issue_code"].value_counts() értékét; ha egy
hibacsoport darabszáma eltér, akkor a lab adata nem az alap generálási
paraméterű, vagy a betöltési útvonal más. A unique_non_null
oszlopok pontos értékét itt nem adjuk meg, mert az nem rögzített a
mellékelt szerződésben; az alábbi report nem teljes sort mutat. A
fontos, hogy a missing és invalid_non_null
oszlopok a várt hibákat tükrözzék.
Értelmezés. A mini CSV-ben miért nem lehet
dtype={"basket_minor": "Int64"} értékkel betölteni a fájlt?
Mi a dtype, ha a dtype szótárt teljesen elhagyjuk? Legalább két
következményt indokolj.
Megvalósítás. Olvasd be a tisztaz
orders.csv-t a fent vázolt explicit sémával és
parse_dates paraméterrel. Írj olyan assert
sorokat, amelyek igazolják: a shape (2400, 15), nincs
hiányzó érték semelyik oszlopban, az order_ts
időzóna-tudatos datetime, és a currency egyetlen
kategóriája EUR. A manifest.json-t most ne
használd.
Hibadiagnózis. Egy osztálytárs ezt írta a raw
fájl auditálásához:
clean = pd.to_numeric(raw["basket_minor"], errors="coerce").
Milyen hibát követ el a hibajegyzék szempontjából? Adj olyan, a coerce
elé illesztett maszkot, amely a nem hiányzó, de nem numerikussá
alakítható szöveget még a konverzió előtt megjelöli, és mutasd meg a
mini CSV abc értékére.
Megváltozott feltétel. A downstream folyamat
mostantól csak a missing_distance_km és
invalid_basket_minor_negative sorokat zárja ki; az
unknown_customer hibákat későbbi lépés vizsgálja. Hozd
létre a clean DataFrame-et ebből a feltételből. Ellenőrizd:
clean.shape[0] == 2402, minden distance_km nem
hiányzó, és minden basket_minor nemnegatív. Milyen további
adathibák maradnak még a clean részben?
1. A basket_minor oszlop a mini CSV-ben
tartalmazza az abc szöveget. Az Int64 kötött
numerikus dtype, így a pandas nem tudja az abc-t erre a
típusra konvertálni, ezért kivétel keletkezik. Ha a dtype szótárt
elhagyjuk, a pandas az oszlopot object vagy
string típusúnak észleli, mert vegyesen van szám és szöveg.
Ez azt jelenti, hogy a későbbi s.basket_minor.sum() nem fut
le numerikus összegzésként, hanem hibát ad vagy string-konkatenációt
végez, ezért az adatprobléma csendesen a betöltés után jelenik meg.
2. A betöltés az explicit dtype dicttel:
orders = pd.read_csv(
RAW / "orders.csv",
dtype={
"order_id": "string",
"customer_id": "string",
"promised_days": "Int64",
"quantity_sum": "Int64",
"item_rows": "Int64",
"basket_minor": "Int64",
"shipping_fee_minor": "Int64",
"currency": "category",
"distance_km": "Float64",
"priority_flag": "Int64",
"delivery_days": "Int64",
"target_late": "Int64",
},
parse_dates=["order_ts", "actual_delivery_at", "label_available_at"],
)
assert orders.shape == (2400, 15)
assert orders.isna().sum().sum() == 0
assert pd.api.types.is_datetime64tz_dtype(orders["order_ts"])
assert set(orders["currency"].cat.categories) == {"EUR"}A parse_dates a datetime64 típusú
oszlopokat időzóna-tudatosan hozza létre, mert a forrás
+00:00 offsettel ír. A cat.categories a
kategóriaszintet adja vissza.
3. A errors="coerce" a nem értelmezhető
szöveget NaN-né alakítja, így az érvénytelen érték
összekeveredik a valódi hiánnyal. A hibajegyzék ekkor aluljelenti az
érvénytelen értékeket, és a downstream kód azt hiheti, hogy csak mérési
hiány van. A javított maszk:
def invalid_text_mask(s):
if s.dtype == "string":
not_missing = s.notna()
else:
not_missing = s.notna()
# A to_numeric a hibás szövegre NaN-t ad, ezért a "nem hiány, de NaN" az invalid.
return not_missing & pd.to_numeric(s, errors="coerce").isna()A mini CSV-ben mini["basket_minor"] vegyes oszlop; a
maszk:
mini["basket_minor"].notna() & pd.to_numeric(mini["basket_minor"], errors="coerce").isna()ez az abc sorra True-t ad, a hiány és a
150 értékre False-ot. A konverziót csak azután
végezzük, hogy az invalid flag megvan.
4. A clean szűrés:
clean = raw_with_issue[
~raw_with_issue["issue_code"].isin(
["missing_distance_km", "invalid_basket_minor_negative"]
)
].copy()
assert clean.shape[0] == 2402
assert clean["distance_km"].notna().all()
assert clean["basket_minor"].ge(0).all()A clean részben továbbra is maradhat az
unknown_customer hiba és az öt duplikált sor; ezeket
későbbi Duplikáció, kulcs és adatminőségi
szerződés és ügyfél-kulcs ellenőrzés kezeli. Ha egy sorra egyszerre
több hibakód is igaz lett volna, a hibakód-értékadás sorrendje felülírta
volna a korábbi kódot; a labori szerződés szerint a hibák külön sorokon
vannak, ezért ez itt nem befolyásolja a 2402-es várt értéket.
A leggyakoribb hiba a csendes errors="coerce"
használata. A mini CSV basket_minor oszlopában az
abc szöveg NaN lesz, és a sémajelentés
hiányként mutatja. A következmény: a javítási szabály tévesen azt írja,
hogy a hiányzó értékeket pótolni lehet, pedig az abc
egyáltalán nem hiány, hanem sérült bemenet. Hasonló csendes összemosás
az üres szöveg kezelése: az alapértelmezett read_csv az
üres stringet is NaN-né alakítja, tehát ha a megfigyelési
folyamat megkülönbözteti a „nem mért” és a „nem válaszolt” esetet, akkor
auditáláskor keep_default_na=False szükséges. Az 5.1-ben a dtype és shape, az 2.1-ben a raw/derived elkülönítés tanította
ugyanezt a célzott, explicit szerződéses szemléletet. Önellenőrzésként
mindig kérdezzünk rá: ha futtatom a sémajelentést, akkor a
missing és az invalid_non_null összege
külön-külön egyezik a kézi labor-leírással?
A betöltéskor megadott dtype önmagában még nem igazolja
a teljes adatkontraktust: a beolvasás után az egyediséget, az
értéktartományokat, a hivatkozási integritást és a dátummezők
feldolgozhatóságát is ellenőrizzük. A hiányzó és az érvénytelen érték
külön hibakategória marad.
Az explicit séma rögzíti az oszlopnevet, dtype-ot, mértékegységet és
megengedett tartományt, így a pd.read_csv inferálása nem
változtatja meg csendben az adattípust. A nullable Int64,
Float64 és string dtype a hiányt
pd.NA-ként tárolja; a category a nominális
szinteket kezeli, a parse_dates az UTC-időbélyegeket
elemzi. A sémajelentés a hiányt, az érvénytelen értéket és az üres
szöveget külön oszlopban számolja. A labor orders_raw.csv
betöltése után 2405 sor, két hiányzó távolság, egy negatív kosárérték és
egy idegen ügyfél várható; a javítási szabály hibakóddal zár ki, nem
coerce-szel. A következő lépésben a kiválasztást és a módosítást
végezzük Copy-on-Write mellett.