Roadmap To Be A Data Engineer / Lesson 11

Lesson 11 Medallion Fundamentals §4 Modelling (+§1, §6) About 10 min read

Cleaned Eight Times

Eight reports clean the same data eight ways. Bronze, silver and gold.

1The scene

Two slides in one board meeting, both labelled premium-tier GMV, August, both pulled from the warehouse that morning. Board deck: €2,336,146.73. Finance close: €2,319,895.35. Gap €16,251.38 (0.70%) — close enough that the room spends eleven minutes on it and moves on. Neither is right; the data says €2,307,800.01.

Behind both slides: one table, bronze.listing_sold_raw, 69,130 rows over 92 days from three feeds (own app, partner API, wholesaler CSV). No cleaned layer. Every report cleans the raw table on the way past, in its own SQL.

2The five rules the data needs

Rule Fixes Changes Reports doing it Error if skipped (premium GMV)
R1 locale price parse feed C sends "1.284,19"; SAFE_CAST → NULL for 9,288 rows values 7/8 −34.055%
R2 dedup on business key 472 at-least-once retries rows 5/8 +1.029%
R3 grade normalisation 3 vocabularies → 6 grades values 5/8 0.000%
R4 exclude test sellers 491 QA/load-test rows rows 6/8 +0.217%
R5 event date not ingest date feed C lands T+2; feed A 1.5% retry backlog which period a row lands in 4/8 +0.524%

Five rules × eight reports = 27 separate implementations. A price-parser fix must be found in 7 places and added to the eighth.

Feed shares: A 61.4% of rows / 48.6% of GMV / 12.0% premium · B 24.8 / 25.2 / 18.6 · C 13.8 / 26.1 / 45.8 — the messiest feed carries the premium money.

Also: silver removes 1.92% of rows and rewrites the values of 38.2%. Row-count reconciliation between bronze and silver proves almost nothing.

3Eight reports, seven answers

August premium GMV, sorted:

Report Rules Answer Error Distinct grades Weeks in own band
Ops daily trading tile R2 R4 R5 €1,521,873.45 −34.06% 12 4/4
Category analyst notebook all five €2,307,800.01 0.00% 7 3/4
Pricing-model feature table R1 R2 R3 R5 €2,312,810.70 +0.22% 7 3/4
Finance monthly close R1 R2 R4 €2,319,895.35 +0.52% 19 3/4
Board deck (dedups on raw payload) R1 R2 R3 R4 €2,336,146.73 +1.23% 7 3/4
Marketing tier report R1 R3 R5 €2,336,557.69 +1.25% 7 2/4
Brand-partner export R1 R3 R4 €2,344,252.23 +1.58% 7 3/4
Exec KPI sheet R1 R4 €2,344,252.23 +1.58% 19 3/4

Spread €822,378.78; excluding the catastrophic one, €36,452.22. 7 distinct answers from 8 reports.

Three results worth more than the spread: – Rule count does not predict accuracy. corr(rules implemented, |error|) = −0.203. The 2-rule Exec sheet is 20× more accurate than the 3-rule Ops tile. R1 alone owns 95.1% of the error surface. – The rule that moves no money breaks every breakdown. R3 changes totals by exactly €0.00 and is the difference between 7 and 19 distinct condition values. Two reports return the identical euro figure to the cent while disagreeing about the world. – Nobody is deliberately wrong. Eight competent people fixed what they noticed; the architecture gave them eight places to fix five problems.

4The finding nobody was looking for — the band test inverts

Every report band-tested against its own weekly history (weeks of 8 Jun – 26 Jul), scored on the four full weeks of August:

  • Ops daily tile (34% wrong): 4/4 — the best score in the company.
  • silver (correct): 3/4.
  • Marketing tier report (1.25% wrong): 2/4 — worse than the truth.

Mechanism: dropping feed C removed 26.1% of the volume and raised the Ops tile’s relative variance, so its own tolerance band is 1.61× wider (13.96 pp vs 8.69 pp indexed). Being wrong made its monitor less sensitive.

A report can only be tested against its own history — and its history was produced by the same wrong code. A systematic cleaning error is not a change, so nothing that looks for change can see it.

This is the structural break from Lessons 06/08/09/10: those were events (before/after), which every drift, threshold, freshness and anomaly test is built to find. Duplicated cleaning logic produces a constant. Totals reconcile to within 1.6% across the seven non-catastrophic reports — signed off as a rounding difference. So the fix cannot be a test; it has to be that there is only one number to test.

5The mechanism — three layers, three rules

Layer Job May enter Must never enter Readers
bronze keep what arrived, forever every row as sent, incl. dupes, test rows, unparseable values, unmapped codes + loader metadata any cleaning, renaming, filtering, UPDATE/DELETE only the silver job
silver one row per real business event, in the warehouse’s vocabulary types, one grain, dedup on business key, conformed vocabularies, quarantine table, UNKNOWN member metric definitions, tier logic, fiscal calendar — anything arguable analysts + every gold job
gold answer one consumption pattern well and cheaply business definitions, aggregates, star schemas, SLA marts cleaning (a repairing CASE in gold is a message from silver) dashboards, exports, reverse-ETL

Where each rule belongs: R1, R2, R4 → silver. R3 → silver (translation), but “which grades are sellable” → gold. R5 → silver stamps event_date; gold owns the calendar. “Premium tier” → gold, only gold.

If two reasonable people could disagree about it, it belongs in gold. If they couldn’t, silver. If it hasn’t been decided yet, bronze — untouched.

6Order matters: normalise, then deduplicate

The wholesaler’s retry path re-serialises the payload: "1.284,19" comes back as "1284.19", " b " as "B". SELECT DISTINCT * therefore collapses 252/252 byte-identical feed-A retries and 0/220 feed-C ones — it catches the duplicates that cost nothing and misses the ones that cost €19,483.92 in August, 80.3% of it inside the premium tier that is 18.2% of rows and 100% of the slide.

Dedup can only run after values are canonical, because until then two records of the same event are not equal. The ordering runs the other way too: quarantine before dedup and the same reject is counted twice — which is why the waterfall uses disjoint buckets (2 rows are both a test seller and an unparseable price; subtract each bucket independently and the arithmetic stops reconciling).

7The waterfall

69,130 bronze → −472 dupes → −491 test sellers → −366 quarantined → 67,801 silver → 276 rows in gold.gmv_daily_tier (245.7× smaller; Lesson 10’s lever #1).

Rejects go to silver.listing_sold_quarantine with the failing rule and raw payload — the money is still missing from every report, but visibly, as a number somebody owns. The 190 rows with an unmapped condition code ("AA") keep their (valid) price and point at an UNKNOWN member of the dimension (Lesson 05) rather than being dropped.

8Medallion theatre — the traps

Three schemas named bronze/silver/gold with no rule about what may enter each · cleaning in bronze (you can never replay) · business logic in silver (every consumer needs an exception, so they go back to raw) · “just this once” reads of bronze (cleaning site 28) · a silver per team (silver_finance = bronze with better manners) · gold on gold on gold · no quarantine (rejects vanish into WHERE) · no contract at the bronze→silver boundary (Lesson 08 drift walks straight through) · skipping gold (every tile re-derives the metric and re-scans the big table — Lesson 10).

9What to ask the team

  1. Which table does every report read? (More than one name, or a raw table, and you have this problem.)
  2. Where is the price parser — the file, not the approach? Count the places. That count is your 27.
  3. What happens to a row we cannot parse? What was last month’s quarantined euro total?
  4. Do we deduplicate before or after normalising? What does the retry path do to the payload?
  5. Name a business definition that lives in silver. (There will be one.)
  6. If “premium tier” changed tomorrow, how many objects change? One is the goal.

10Hands-on (≈45 min)

Run assay.py (stdlib only, deterministic) → reproduce €2,307,800.01 and the seven answers. Load bronze into DuckDB as-is; write exactly two SQL files, silver.sql (all five rules) and gold_gmv_daily_tier.sql — with the rule that gold_*.sql may not contain CAST, CASE, DISTINCT or TEST. Prove notebook = gold mart to the cent. Add the quarantine table (366 rows) and the UNKNOWN member (190). Then break the dedup in silver on purpose and watch exactly €19,483.92 of phantom revenue appear in every downstream number at once — one mistake, one place, one fix.

11Vocabulary

medallion architecture · bronze / silver / gold · raw zone · conformed vocabulary · grain · business key · quarantine table · unknown member · idempotent dedup · layer contract · single source of truth · cleaning site

12Reproducibility

All figures from assay.py — standard library, no RNG, no seed, deterministic integer hashing — shipped in full as an appendix inside the artifact. Verified by extracting the source back out of the rendered HTML, running it, and diffing 729 figure keys against the published values: zero differences.

Back to top