Ebben az ellenőrző pontban a Northstar lab három tiszta forrásfájljából egyetlen rendelésszintű, modellépítésre alkalmas feature-táblát készítünk. A bemenet:
customers.csv — 300 ügyfél, egy sor ügyfelenként,
kulcs: customer_id;orders.csv — 2400 rendelés, egy sor rendelésenként,
kulcs: order_id;order_items.csv — 4796 rendelési tétel, kulcs:
(order_id, line_id).A megfigyelési egység a kimenetben egy rendelés. Az eredmény egy
orders_features.parquet, egy
data_quality_report.json, és egy független
pandas-ellenőrzés, amely ugyanazt a táblát állítja elő, mint a DuckDB
SQL. A feature-tábla csak a rendelés pillanatában ismert oszlopokat
tartalmazhatja; a delivery_days,
actual_delivery_at, label_available_at és
target_late mezőket ki kell zárni, mert azok a későbbi
célváltozó meghatározásához tartoznak.
Előzetes becslés: a kimenet pontosan 2400 sorból áll, az
order_id 2400 egyedi értéket ad, a customer join nem veszít
sort, és a tiszta lab adatban a prediktor oszlopok NULL-száma nulla. Az
order_items aggregátumainak rendelésszintű összege meg kell
egyezzen az orders.csv basket_minor és
quantity_sum értékeivel. Mielőtt tovább olvasol, becsüld
meg: ha véletlenül a tételszintű táblát közvetlenül joinolnád a
rendelésekhez, hány sor keletkezne a 2400 rendelésből, és hogyan
változna a szállítási díj összege?
Az ellenőrző pont feladata az eddigi építőelemek összekapcsolása: a Séma, dtypes és hiányzó értékek–Dátumok, időzónák és kategóriák pandas-séma, a Reláció, NULL és elemzési szemcsézettség–Paraméterezett lekérdezés és lekérdezési terv SQL-join és -aggregáció, valamint a DuckDB: helyi analitikus SQL szerver nélkül DuckDB-fájlscan és Relation viselkedés. Az I. ellenőrző pont: megismételhető kutatói munkaasztal alfejezetben felállított szabály szerint a kimenethez tartozik egy futási jegyzék és kontrollösszeg is, nem elég „ránézésre jó” táblát írni.
A séma explicit oszlopszerződés: minden oszlophoz megadjuk a nevet,
típust, szemcsézettséget és mértékegységet. A CSV beolvasásnál az
automatikus típusfelismerés kényelmes, de egy üres mező vagy egy furcsa
dátum felboríthatja az utána következő joinokat. A DuckDB
read_csv hívásában ezért explicit oszloptípusokat adunk
meg. A Parquet fájl magában hordozza a sémát, ezért a kimenet olvasója
nem találgathat: a pyarrow.parquet.read_schema megmutatja a
rögzített típusokat.
A kimeneti tábla oszlopai:
| Oszlop | Típus | Jelentés |
|---|---|---|
order_id |
string | rendelés egyedi kulcsa, nem predictor |
customer_id |
string | ügyfélkulcs, nem predictor |
order_ts |
timestamp | predikciós időpont, splithez kell |
distance_km |
float64 | szállítási távolság kilométerben |
priority_flag |
int | 0/1 prioritásjelző |
basket_minor |
int | kosárérték EUR-centben |
quantity_sum |
int | összes darabszám a rendelésben |
account_age_days |
int | a rendelés napján ismert ügyfélkor napokban |
segment, region,
acquisition_channel |
string | kategorikus ügyféljellemzők |
A basket_minor és a quantity_sum pénz-,
illetve darabszám-egysége egész; a distance_km folytonos
lebegőpontos érték. A segment, region és
acquisition_channel kategóriák, nem rendezett numerikus
kódok, tehát stringként maradnak.
A három forrás három különböző szemcsézettséget képvisel. A
customers egy sor ügyfelenként, az orders egy
sor rendelésenként, az order_items pedig egy sor
tételenként. A cél egy sor rendelésenként. A tételszintű táblát ezért a
join előtt rendelésszintre kell aggregálni:
SELECT
order_id,
COUNT(*) AS item_rows_calc,
SUM(quantity) AS quantity_calc,
SUM(line_total_minor) AS basket_minor_calc,
SUM(quantity * unit_price_minor - discount_minor) AS basket_minor_recomputed
FROM order_items
GROUP BY order_idHa ezt a lépést kihagyjuk, a tételtábla 4796 sora a 2400
rendeléssorhoz kapcsolva akár 4796 soros kimenetet adhat, és a
rendelésszintű oszlopok, például a shipping_fee_minor vagy
a distance_km, tételekként többszöröződnek. A Join-kardinalitás és duplázódó célváltozó
alfejezetben látott one-to-many join pontosan ezért volt kiemelt
hibaforrás: a szállítási díj egyszer létezik rendelésenként, nem szabad
tételszintre szorozni.
A tisztaz orders.csv prediktor mezőiben a lab szerződés
szerint nincs hiányzó érték. Ezt nem hisszük el, hanem ellenőrizzük. A
szabály: a feature-tábla nem tartalmazhat dokumentálatlan NULL-t a
prediktor oszlopokban. Ha egy hiányzó distance_km bukkan
fel, a szkript ne töltsön be hallgatólagosan nullát vagy átlagot, hanem
álljon meg, és írja be a jelentésbe a null_rows > 0
állapotot. A NULL nem technikai kellemetlenség, hanem adatminőségi
esemény, amelynek oka vagy dokumentált imputálási döntése kell
legyen.
A modell a rendelés pillanatában becsül. A feature-tábla minden
sorának ezért van egy feature_time értéke: az
order_ts. Az account_age_days kiszámítása:
ahol
a rendelés,
az ügyfél. A lab szintetikus adatában a signup_date
statikus, ezért ez az érték mindig a rendelés előtt ismert. Ha a valós
üzleti adatban az ügyfél szegmense vagy régiója időben változna, akkor a
joinhoz havi pillanatfelvétel kellene; erről a 4. feladat szól. A
későbbi tanító-teszt split további label_available_at
szűrést igényel, de az már a modellépítés feladata, nem a
feature-tábláé.
A kontrollösszeg egy numerikus oszlop összege a tábla összes sorára:
A jelentésbe a basket_minor, quantity_sum,
basket_minor_calc, quantity_calc és a
discount_minor_total
értéke kerül. Az invariant: basket_minor összege az
orders.csv-ben megegyezik a line_total_minor
rendelésszintű összegével az order_items.csv-ben, és
ugyanez igaz a quantity_sum és a tételenkénti
quantity összegére. A kontrollösszeg nem bizonyítja, hogy
minden sor helyes, de azonnal jelzi, ha egy join megdupláz vagy elveszít
sorokat. A Parquet fájl SHA-256 lenyomatát is rögzítjük, hogy a átadott
fájl később azonosítható legyen.
Ugyanazt a logikai műveletet három eszközzel is el lehet végezni:
| Szempont | DuckDB SQL | pandas | Polars |
|---|---|---|---|
| Fő erősség | deklaratív join + aggregáció fájlból | interaktív oszlopkezelés, dtype-kontroll | lazy terv, oszlopos kiértékelés |
| Előzmény | Reláció, NULL és elemzési szemcsézettség–DuckDB: helyi analitikus SQL szerver nélkül | Séma, dtypes és hiányzó értékek–Dátumok, időzónák és kategóriák | Polars lazy terv és streaming korlát |
| Szerep itt | fő előállító | független ellenőrzés | opcionális harmadik |
A DuckDB-t választjuk fő előállítónak, mert a fájlokat közvetlenül,
pandas köztes DataFrame nélkül olvassa, és a COPY TO
utasítás egy lépésben ír Parquet-et. A pandas az ellenőrzés nyelve, mert
a könyv fő táblás felülete; ha két különböző implementáció azonos
kimenetet ad, csökken a fordítási hiba esélye. A Polars ebben a
fejezetben nem előfeltétel, de a Polars lazy terv és
streaming korlát lazy terve ugyanehhez a kontrollhoz
használható.
A lab gyökerében hozzuk létre a make_orders_features.py
szkriptet. A fájlokat a lab gyökeréből olvassuk, a kimenetet az
artifacts/ mappába írjuk. A kód célja: a három CSV
beolvasása explicit sémával, a tételtábla aggregálása, a feature-tábla
összeállítása, a kontrollok kiszámítása, a Parquet és a JSON jelentés
kiírása, végül egy független pandas-újraszámolás, amely
assert_frame_equal hívással veti össze a két kimenetet.
import hashlib
import json
from pathlib import Path
import duckdb
import pandas as pd
import pyarrow.parquet as pq
from pandas.testing import assert_frame_equal
ROOT = Path(__file__).resolve().parent
ARTIFACTS = ROOT / "artifacts"
ARTIFACTS.mkdir(exist_ok=True)
PARQUET = ARTIFACTS / "orders_features.parquet"
REPORT = ARTIFACTS / "data_quality_report.json"
# --- 1. DuckDB: explicit séma és main query -----------------------------
con = duckdb.connect()
con.execute("""
CREATE OR REPLACE TEMP TABLE customers_raw AS
SELECT *
FROM read_csv('customers.csv', columns = {
'customer_id': 'VARCHAR',
'signup_date': 'DATE',
'segment': 'VARCHAR',
'region': 'VARCHAR',
'acquisition_channel': 'VARCHAR'
});
""")
con.execute("""
CREATE OR REPLACE TEMP TABLE orders_raw AS
SELECT *
FROM read_csv('orders.csv', columns = {
'order_id': 'VARCHAR',
'customer_id': 'VARCHAR',
'order_ts': 'TIMESTAMP',
'quantity_sum': 'BIGINT',
'item_rows': 'BIGINT',
'basket_minor': 'BIGINT',
'shipping_fee_minor': 'BIGINT',
'currency': 'VARCHAR',
'distance_km': 'DOUBLE',
'priority_flag': 'TINYINT'
});
""")
con.execute("""
CREATE OR REPLACE TEMP TABLE items_raw AS
SELECT *
FROM read_csv('order_items.csv', columns = {
'order_id': 'VARCHAR',
'line_id': 'BIGINT',
'product_id': 'VARCHAR',
'quantity': 'BIGINT',
'unit_price_minor': 'BIGINT',
'discount_minor': 'BIGINT',
'line_total_minor': 'BIGINT',
'currency': 'VARCHAR'
});
""")
con.execute("""
CREATE OR REPLACE TEMP TABLE item_agg AS
SELECT
order_id,
COUNT(*) AS item_rows_calc,
SUM(quantity) AS quantity_calc,
SUM(line_total_minor) AS basket_minor_calc,
SUM(quantity * unit_price_minor - discount_minor) AS basket_minor_recomputed,
SUM(discount_minor) AS discount_minor_total,
SUM(quantity * unit_price_minor) AS total_list_minor
FROM items_raw
GROUP BY order_id;
""")
con.execute("""
CREATE OR REPLACE TEMP TABLE orders_features AS
SELECT
o.order_id,
o.customer_id,
o.order_ts,
o.distance_km,
o.priority_flag,
o.basket_minor,
o.quantity_sum,
date_diff('day', c.signup_date, CAST(o.order_ts AS DATE)) AS account_age_days,
c.segment,
c.region,
c.acquisition_channel
FROM orders_raw o
INNER JOIN customers_raw c USING (customer_id)
INNER JOIN item_agg i USING (order_id)
WHERE o.currency = 'EUR'
AND o.distance_km IS NOT NULL;
""")
con.execute(
f"COPY (SELECT * FROM orders_features ORDER BY order_id) "
f"TO '{PARQUET}' (FORMAT PARQUET, COMPRESSION ZSTD);"
)
# --- 2. A jelentés kontrolljai ----------------------------------------
source_orders = con.sql("""
SELECT COUNT(*) AS n,
SUM(basket_minor) AS basket_sum,
SUM(quantity_sum) AS q_sum,
SUM(shipping_fee_minor) AS shipping_sum
FROM orders_raw
""").fetchone()
final_rows = con.sql("""
SELECT COUNT(*) AS n,
COUNT(DISTINCT order_id) AS unique_order_ids,
SUM(basket_minor) AS basket_sum,
SUM(quantity_sum) AS q_sum
FROM orders_features
""").fetchone()
item_totals = con.sql("""
SELECT SUM(basket_minor_calc) AS basket_calc,
SUM(quantity_calc) AS q_calc,
SUM(basket_minor_recomputed) AS basket_recomputed
FROM item_agg
""").fetchone()
lost_customer_join = con.sql("""
SELECT COUNT(*) FROM orders_raw o
WHERE NOT EXISTS (SELECT 1 FROM customers_raw c WHERE c.customer_id = o.customer_id)
""").fetchone()[0]
lost_item_join = con.sql("""
SELECT COUNT(*) FROM orders_raw o
WHERE NOT EXISTS (SELECT 1 FROM item_agg i WHERE i.order_id = o.order_id)
""").fetchone()[0]
# --- 3. pandas független ellenőrzés ------------------------------------
customers = pd.read_csv(
ROOT / "customers.csv",
dtype={"customer_id": "string", "segment": "string",
"region": "string", "acquisition_channel": "string"},
parse_dates=["signup_date"],
)
orders = pd.read_csv(
ROOT / "orders.csv",
dtype={"order_id": "string", "customer_id": "string",
"currency": "string", "priority_flag": "int64",
"distance_km": "float64"},
)
orders["order_ts"] = pd.to_datetime(orders["order_ts"], utc=True)
items = pd.read_csv(
ROOT / "order_items.csv",
dtype={"order_id": "string", "line_id": "int64",
"product_id": "string", "quantity": "int64",
"unit_price_minor": "int64", "discount_minor": "int64",
"line_total_minor": "int64", "currency": "string"},
)
item_agg_pd = items.groupby("order_id").agg(
item_rows_calc=("order_id", "size"),
quantity_calc=("quantity", "sum"),
basket_minor_calc=("line_total_minor", "sum"),
)
feat_pandas = (
orders
.merge(customers[["customer_id", "signup_date", "segment",
"region", "acquisition_channel"]],
on="customer_id", how="inner")
)
feat_pandas = feat_pandas[
feat_pandas["order_id"].isin(item_agg_pd.index)
]
feat_pandas["account_age_days"] = (
feat_pandas["order_ts"].dt.tz_localize(None).dt.normalize()
- feat_pandas["signup_date"]
).dt.days
feat_pandas = feat_pandas[[
"order_id", "customer_id", "order_ts", "distance_km",
"priority_flag", "basket_minor", "quantity_sum",
"account_age_days", "segment", "region", "acquisition_channel"
]].sort_values("order_id").reset_index(drop=True)
duckdb_feat = pd.read_parquet(PARQUET, engine="pyarrow")
controls_passed = (
source_orders[0] == final_rows[0]
and final_rows[1] == final_rows[0]
and source_orders[1] == item_totals[0]
and source_orders[2] == item_totals[1]
and item_totals[0] == item_totals[2]
and lost_customer_join == 0
and lost_item_join == 0
)
# --- 4. A pandas és a DuckDB kimenet egyezése ---------------------------
compare_cols = feat_pandas.columns.tolist()
assert_frame_equal(
duckdb_feat[compare_cols].reset_index(drop=True),
feat_pandas,
check_dtype=False,
check_exact=False,
rtol=1e-13,
atol=1e-9,
)
# --- 5. A jelentés és a hash kiírása ------------------------------------
report = {
"generated_at": pd.Timestamp.now(tz="UTC").isoformat(),
"input_files": ["customers.csv", "orders.csv", "order_items.csv"],
"granularity": "one row per order",
"output_file": str(PARQUET.name),
"output_row_count": int(final_rows[0]),
"unique_order_ids": int(final_rows[1]),
"lost_customer_join": int(lost_customer_join),
"lost_item_join": int(lost_item_join),
"controls_passed": bool(controls_passed),
"control_totals": {
"orders_basket_minor": int(source_orders[1]),
"items_basket_minor_calc": int(item_totals[0]),
"items_basket_minor_recomputed": int(item_totals[2]),
"orders_quantity_sum": int(source_orders[2]),
"items_quantity_calc": int(item_totals[1]),
},
"parquet_sha256": hashlib.sha256(PARQUET.read_bytes()).hexdigest(),
"schema": {field.name: str(field.type)
for field in pq.read_schema(PARQUET)},
}
REPORT.write_text(json.dumps(report, indent=2), encoding="utf-8")
print("controls_passed =", controls_passed)
print("parquet_sha256 =", report["parquet_sha256"])Futtatás a lab projektkörnyezetben, munkakönyvtár:
lab/:
# POSIX shell és Windows PowerShell esetén is:
uv sync --locked
uv run python make_orders_features.pyHa a uv.lock még nem létezik, előbb az I. ellenőrző pont: megismételhető kutatói
munkaasztal alfejezetben megismert módon uv lock, majd
a fenti uv sync --locked hívás jön. A kód itt bemutatott
kimenete szemléltető: e fejezet írásakor nem futtattunk Python-kódot,
ezért a konkrét összeg és hash a tanuló saját futásából származik.
A fenti szkript várható logikája a lab szerződés alapján:
output_row_count = 2400,
unique_order_ids = 2400,
lost_customer_join = 0, lost_item_join = 0, és
a controls_passed értéke True. A szemléltető
JSON innentől csak a formátumot mutatja; a számok a lab futása után
lesznek mértek:
{
"granularity": "one row per order",
"output_row_count": 2400,
"unique_order_ids": 2400,
"lost_customer_join": 0,
"lost_item_join": 0,
"controls_passed": true
}Kézi kontrollként vegyünk egyetlen rendelést három tétellel. Legyen a
rendelés basket_minor = 410,
shipping_fee_minor = 500, és a tételek:
quantity=2, unit_price_minor=100,
discount_minor=0 → line_total=200;quantity=2, unit_price_minor=80,
discount_minor=10 → line_total=150;quantity=1, unit_price_minor=70,
discount_minor=10 → line_total=60.Az összeg: quantity_calc = 5,
basket_minor_calc = 410, tehát a rendelésszintű
basket_minor egyezik. Ha a tételtáblát aggregálás nélkül
joinolnánk, a három tétel miatt három sor keletkezne, és a
shipping_fee_minor összege 3 * 500 = 1500
lenne a helyes 500 helyett. A kontrollösszeg ezért azonnal
elárulja a hibás join-kardinalitást.
A pandas ellenőrzés nem csupán a kontrollösszegeket nézi, hanem a
teljes 11 oszlopos táblát összehasonlítja a DuckDB Parquet kimenetével,
order_id szerint rendezve. Az egész számlálóknál a
tolerancia nulla, a lebegőpontos distance_km-nél
rtol=1e-13 és atol=1e-9 engedélyezett, mert a
két motor a lebegőpontos műveletek sorrendjében kismértékben eltérhet. A
check_dtype=False azért szerepel, mert a pandas
category vagy int64 és a DuckDB
INTEGER közötti reprezentációs különbség nem jelent
tartalmi hibát.
A data_quality_report.json a séma mellett rögzíti a
Parquet SHA-256 lenyomatát is. Így később bárki ellenőrizheti, hogy a
fájl az átadás óta nem változott. Ez az I. ellenőrző
pont: megismételhető kutatói munkaasztal futási jegyzékkel
felállított elv Parquet-szintű megfelelője.
Értelmezés. A fenti háromtételes kézi példában
mi a quantity_calc, basket_minor_calc,
discount_minor_total és a tételszintű
shipping_fee_minor összege, ha valaki az
order_items táblát közvetlenül joinolja, aggregálás nélkül?
Melyik rendelésszintű érték romlik el, és miért?
Implementálás. Írj DuckDB SQL lekérdezést, amely egyetlen rendelésre a tételsorokból kiszámítja a mennyiséggel súlyozott átlagos egységárat:
ahol
a tétel darabszáma,
az egységár EUR-centben. A nevező biztosítására használj
NULLIF-et, és a háromtételes példán ellenőrizd, hogy az
eredmény 86.
Hibakeresés. A jelentésben
lost_item_join = 5 és controls_passed = false
jelenik meg, miközben a lost_customer_join = 0. Mi lehet a
hiba forrása, és milyen lekérdezéssel lokalizálod pontosan az öt hiányzó
rendelést? A tisztaz orders.csv 2400 sora ellenére a
kimenet csak 2395 sor.
Megváltozott feltétel. Új forrás érkezik:
customers_monthly_snapshot.csv oszlopai
customer_id, snapshot_month, segment, region, acquisition_channel,
ahol snapshot_month minden hónap első napja. Az ügyfél
szegmense hónapról hónapra változhat. Hogyan módosul a join, hogy minden
rendelés a saját rendelési hónapjában érvényes szegmenst kapja?
Fogalmazd meg a feltételt, és mondd meg, mi történik, ha egy ügyfélnek
hiányzik a snapshotja abban a hónapban.
Transzfer. A pandas ellenőrző részben cseréld le
az isin szűrést egy
merge(..., how="left", indicator=True) alapú ellenőrzésre.
Melyik _merge kategória darabszámának kell nullának lennie,
és hogyan viszed be ezt a report JSON-ba külön
pandas_lost_item_join mezőként?
1. megoldás. A tételszintű táblában a három sor
quantity_calc összege 2 + 2 + 1 = 5, a
basket_minor_calc összege
200 + 150 + 60 = 410, a
discount_minor_total = 0 + 10 + 10 = 20. Ha a táblát
aggregálatlanul joinoljuk a rendeléshez, a rendelésszintű
shipping_fee_minor három soron is megjelenik, így az
összege 1500 lesz a valódi 500 helyett. Ez a
szállítási díj többszöröződése; a basket_minor a join
előtti rendelésértékkel maradna, de ha tévesen tételenként is
hozzáadnánk, akkor 410 + 1500 = 1910 hamis kosárösszeg
keletkezne. A hiba gyökere a one-to-many join kardinalitásának figyelmen
kívül hagyása.
2. megoldás.
SELECT
oi.order_id,
ROUND(
SUM(oi.quantity * oi.unit_price_minor)
/ NULLIF(SUM(oi.quantity), 0),
2
) AS weighted_unit_price_minor
FROM order_items oi
WHERE oi.order_id = 'O1'
GROUP BY oi.order_id;A háromtételes példában a számláló
2*100 + 2*80 + 1*70 = 430, a nevező 5, így az
eredmény 86.00. A NULLIF arra az elvi esetre
véd, ha a nevező nulla; ilyenkor az osztás NULL-t ad,
amelyet a jelentés null-szűrése megfog. A ROUND itt a
megjelenítést és a következő összehasonlítást stabilizálja, de a nyers
arányt is érdemes megtartani.
3. megoldás. A lost_item_join = 5 azt
jelenti, hogy öt rendeléshez nincs egyetlen tételsor sem az
order_items.csv-ben, vagy az order_id értékük
eltér például szóköz vagy kis/nagybetű miatt. A lokalizáló anti
join:
SELECT o.order_id, o.customer_id, o.order_ts
FROM orders_raw o
LEFT JOIN item_agg i USING (order_id)
WHERE i.order_id IS NULL
ORDER BY o.order_id;Ez pontosan az öt hiányzó rendelést adja. Ha az
order_items.csv-ben ezek az azonosítók más formában
szerepelnek, a hiba sztring-összehasonlítási eltérés; ha egyáltalán
nincsenek ott, akkor adathiány. A controls_passed = false
azért következik be, mert a basket_minor és a
quantity_sum összege a 2395 soros kimenetben kisebb, mint a
2400 soros forrásban. A javítás nem a kontroll kikapcsolása, hanem a
hiányzó tételek pótlása vagy a rendelések explicit kizárása dokumentált
döntéssel.
4. megoldás. A havi snapshot joinja:
SELECT ...
FROM orders_raw o
INNER JOIN customers_monthly_snapshot cs
ON cs.customer_id = o.customer_id
AND cs.snapshot_month = CAST(date_trunc('month', o.order_ts) AS DATE)A date_trunc('month', o.order_ts) a rendelés időbélyegét
a hónap első napjára kerekíti; DATE-re castolva egyezhet a
snapshot_month oszloppal. A feltétel biztosítja, hogy a
rendelés pillanatában érvényes szegmenst használjuk. Ha egy ügyfélnek az
adott hónapban hiányzik a snapshotja, az INNER JOIN eldobja
a rendelést, és a jelentésben új lost_snapshot_join
kontroll jelenik meg. Ilyenkor vagy LEFT JOIN-nal jelezzük
a hiányt és dokumentált alapértelmezést adunk, vagy a
snapshot-előállítást javítjuk. A csendes INNER JOIN-nal
való eldobás nem elfogadható audit nélkül.
5. megoldás. A pandas ellenőrzés:
check = (
orders[["order_id"]]
.merge(item_agg_pd.reset_index()[["order_id"]],
on="order_id", how="left", indicator=True)
)
lost = (check["_merge"] == "left_only").sum()
report["pandas_lost_item_join"] = int(lost)A left_only sorok azok, amelyek az orders
táblában szerepelnek, de az aggregált tételtáblában nincsenek. Ennek a
számnak nullának kell lennie. Ha nem nulla, a pandas és a DuckDB
lost_item_join értékének is egyeznie kell; az egyezés
hiánya arra utal, hogy a két implementáció bemenete vagy szűrése
eltér.
A leggyakoribb tévedés ezen az ellenőrző ponton az, hogy „a DuckDB és
a pandas ugyanazt adta, tehát a mutató helyes”. A két végrehajtó
egyezése csak a fordítási hibát csökkenti, a mutató definícióját nem
igazolja. Ha mindkét implementáció azt a hibás szabályt használja, hogy
a basket_minor-hoz minden tétel után hozzáadja a
shipping_fee_minor-t, a két kimenet továbbra is egyezni
fog, csak mindkettő hamis lesz. Ez a Paraméterezett
lekérdezés és lekérdezési terv alfejezetben látott
EXPLAIN-csapda rokona: a terv és a determinisztikus egyezés
a végrehajtást írja le, nem a definíció helyességét. A védőháló a
független szemantikai invariant: a basket_minor a
line_total_minor összege, a shipping_fee_minor
pedig rendelésszintű, tehát nem tartozik a kosárértékhez. Ezt a lab
ADATOK.md szerződése rögzíti, és a kontrollösszeg e
szerződés ellen dolgozik.
Mielőtt továbblépnél, tedd fel magadnak a következő kérdéseket. A
kimeneti tábla minden sora pontosan egy rendelés? A
customer_id join után nincs duplikált
order_id? A prediktor oszlopok NULL-száma nulla, vagy ha
nem, a jelentés dokumentálta? A basket_minor és a
quantity_sum összege egyezik az order_items
aggregátumával? A delivery_days,
actual_delivery_at, label_available_at és
target_late oszlopok hiányoznak a feature-táblából? A
Parquet SHA-256 lenyomata rögzítve van? Ha bármelyik válasz nem, az
ellenőrző pont még nincs kész.
A feature-tábla minden mezőjéről igazolni kell, hogy a predikció pillanatában hozzáférhető volt. A feature „as-of” ideje és a tanítócímke megérkezésének ideje külön időtengely: egy késve rendelkezésre álló címke a releváns cutoff előtt nem használható tanításhoz.
A II. ellenőrző pont rendelésszintű, időhelyes feature-táblát állít
elő három Northstar forrásból. A tételtáblát a join előtt aggregáljuk,
hogy a szállítási díj és a rendelésszintű összegek ne duplázódjanak. A
DuckDB explicit CSV-sémát, inner joinokat és Parquet-kimenetet ad; a
kontrollösszegek a basket_minor, a
quantity_sum és a tételaggregátumok egyezését ellenőrzik.
Egy független pandas implementáció a teljes 11 oszlopos táblát
újraszámolja, és toleranciás keret-összehasonlítást végez. A jelentés
rögzíti a sor- és kulcsszámot, a joinveszteséget, a NULL-állapotot és a
fájl SHA-256 lenyomatát. A következő alkalmazás a Tanítás, validáció és érintetlen teszt
modellépítés: ebből az érintetlen feature-táblából kell időhelyes
tanító-teszt splitet készíteni, a célváltozót csak a split után
csatolva.