DSC3103 ยท Module 8 of 9

Practice Exam (sample_qtn): Milk Validation & Collisions

Master rule ordering, record_id collisions & row conservation (sample_qtn.pdf)

~56 min to study properly

In the DSC3103 Practice Question (sample_qtn.pdf), farmers deliver milk to collection centres (`data/raw/milk_raw.csv`) and we must validate, clean, join with `data/raw/centres.csv`, and load a Star Schema without ever modifying `data/raw/`.

Checkpoint ยท the examiner asks

In `sample_qtn.pdf`, why must validation rules on `centre` run AFTER tidying text, why are BOTH rows of a `record_id` collision rejected instead of keeping one, and what makes running the pipeline twice safe?

In one minute

In sample_qtn, tidy formatting (strip().title() on centre) before running membership rules, or valid deliveries get flagged as unknown centres. Exact duplicates are double-saved records โ€” keep one. A record_id collision has the same id but different values, so both rows are quarantined in rejected_df. Always assert row conservation: rows_in = kept + rejected + exact_duplicates_removed.

Traps examiners look for
  • Running rule_unknown_centre before tidying centre text.
  • Keeping the first row of a record_id collision.
  • Not asserting the row-conservation invariant at the end.
Read

Study notes

01The task in sample_qtn

Farmers deliver milk to collection centres. You get data/raw/milk_raw.csv (deliveries) and data/raw/centres.csv (valid centres with capacities), and must write clean_milk(df) -> (clean_df, rejected_df):

  • clean_df โ€” deliveries you trust, ready for the star schema;
  • rejected_df โ€” every row you didn't keep, with a reason (invalid_date, invalid_litres, unknown_centre, record_id_collision, โ€ฆ).

Two rules frame everything: never modify data/raw/, and every input row must be accounted for. Examiners check both the logic and that you can prove nothing was lost.

02Tidy first, then validate

Part (a) asks whether validation rules run before or after tidying text. Run them after.

rule_unknown_centre checks each delivery's centre against centres.csv. If a clerk typed " mbarara " or "MBARARA", the raw string doesn't match "Mbarara", so checking first rejects a perfectly valid delivery โ€” and a farmer doesn't get paid.

So the order is: normalise formatting (df["centre"] = df["centre"].str.strip().str.title(), parse dates, coerce numbers) โ†’ then apply membership and domain rules. Tidying fixes representation; validation judges content. Mixing them up turns formatting noise into false rejections.

03Exact duplicates vs record_id collisions

These look similar and are handled oppositely:

Exact duplicaterecord_id collision
What it isthe same row saved twice โ€” every column identicalthe same record_id with different values (45 L vs 120 L, or two farmers)
What you knowboth copies say the same thingat most one is true, and you don't know which
Actionkeep one copy (drop_duplicates(keep="first")), count the removed copiesreject both rows to rejected_df with reason="record_id_collision"

Keeping "the first" collision row is a silent guess that can pay the wrong farmer or the wrong amount. Rejecting both sends the conflict to a human. Order matters: remove exact duplicates first, otherwise two identical rows would look like a collision.

exact = df.duplicated(keep="first")
df = df[~exact]
collision = df.duplicated(subset=["record_id"], keep=False)   # marks BOTH rows

04Row conservation โ€” proving nothing was lost

The invariant you assert at the end:

rows_in = len(clean_df) + len(rejected_df) + exact_duplicates_removed

It catches the most common bugs: a filter that silently drops rows without recording them, a rule applied twice, or a collision handled by keeping one row. Examiners love asking for it because it turns "I think my cleaning is right" into a check that either passes or fails.

Two more habits that score marks: give every rejected row exactly one reason (the first rule it failed, applied in a fixed order), and log the count per reason so the operations team can see why data was rejected, not just how much.

Watch it done

Worked examples

Worked example 1

Account for every row

A raw batch has 640 rows. There are 18 exact duplicate copies, 7 record_ids that collide (2 rows each), 11 rows with litres โ‰ค 0 and 6 with an unknown centre (after tidying). How many rows are in clean_df and rejected_df?

    Worked example 2

    Collision or duplicate?

    Three rows share record_id R-104: (R-104, F-22, Mbarara, 45 L), (R-104, F-22, Mbarara, 45 L), (R-104, F-22, Mbarara, 120 L). What happens to each?

      Vocabulary

      Key terms

      Quarantine tables (`rejected_df`) in production pipelines

      Real cooperative and banking pipelines never silently `dropna()` bad records into a black hole. They split batches into `(clean_df, rejected_df)` with a `reason` column so clerks can fix rejected delivery slips without blocking the daily warehouse build.

      Practise

      Quick check

      0 of 8 correct so far. Wrong answers come with an explanation โ€” and the buddy can unpack them.

      Q1A raw milk file has 1,000 rows. There are 15 exact duplicate rows (to be removed), 6 pairs of `record_id` collisions (12 colliding rows total), and 23 rows with non-positive litres or unknown centres. How many rows are in `clean_df`?

      Q2Why does `rule_unknown_centre` run AFTER `df['centre'] = df['centre'].str.strip().str.title()`?

      Q3Why tidy the centre column before rule_unknown_centre?

      Q4Two rows have the same record_id but different litres. What should clean_milk do?

      Q5Which pandas call marks BOTH rows of a collision?

      Q6What is the row-conservation invariant?

      Q7Why must exact duplicates be removed before checking collisions?

      Q8What should every row in rejected_df have?

      Exam-style

      Written practice

      Short answer4 marks

      Should validation rules run before or after tidying the centre column? Justify with an example.

      Short answer6 marks

      Explain the difference between an exact duplicate and a record_id collision, and how each is handled in clean_milk.

      Long answer14 marks

      Write clean_milk(df) -> (clean_df, rejected_df) in pseudo-code or pandas for sample_qtn, covering tidying, exact duplicates, collisions, date and litres rules, unknown centres, reasons, and the final assertion. Explain why each step is in that position.

      Watch

      Videos

      Fresh picks are chosen by AI every day.
      Hands-on

      Milk Validation Funnel, Rule Ordering & Collision Simulator

      4 guided tasks: predict, experiment, explain โ€” then challenge yourself. Plus 1 coding exercise you run right in the browser.

      Start the lab