"""Guided capstone starter. Edit the FIVE named TODO stages below.
Read the bridge checkpoints in lesson 10 first. The ingestion helper is supplied.
Public checks run against YOUR clean_orders; no reference solution is imported.
"""
from io import StringIO
import csv
import pandas as pd
REQUIRED = ["order_id", "date", "category", "units", "unit_price_cents"]
CATEGORIES = {"notebooks", "pens", "markers"}
# Start at 1; move through 5 as each stage works. Set 6 for the full project.
CHECKPOINT_STAGE = 1
STAGE_CSV = 'order_id,date,category,units,unit_price_cents\nQ01,2026-06-01,pens,2,80\nQ01,2026-06-01,pens,2,80\n Q01 ,2026-06-01,pens,3,80\nQ02,bad-date,markers,unknown,150\nQ03,2026-06-03, NOTEBOOKS ,4,250\n'
def prepare_work(raw):
"""Stage 1. Return (work, duplicates, conflicts).
Split exact extra copies BEFORE trimming IDs/categories. Preserve indexes.
conflicts is a Boolean Series on work.index marking every repeated ID.
Checkpoint: full-schema STAGE_CSV -> duplicate label [1], conflicts [0, 2].
"""
raise NotImplementedError("Stage 1: split duplicates and flag conflicts")
def validate_work(work, conflicts):
"""Stage 2. Return (converted, reasons), preserving work.index.
Copy work; check date/numeric source spelling before conversion, then check
parsed calendar/range and numeric bounds. Include ID/category/conflict rules.
reasons is a string Series: empty for valid rows, semicolon-separated names
for failures. Preserve ALL failure reasons on a row. See bridge checkpoint 3.
"""
raise NotImplementedError("Stage 2: create aligned validation reasons")
def split_evidence(raw, converted, reasons):
"""Stage 3. Return (accepted, rejected).
Select accepted converted rows where reasons is empty. Select rejected rows
from raw using the corresponding source labels, then attach their reasons.
Convert accepted unit/price columns to int64; add revenue_cents.
Sort accepted rows by date/order_id; only now may their index be reset.
"""
raise NotImplementedError("Stage 3: retain original rejected evidence")
def summarize_accepted(accepted):
"""Stage 4. One row per category, columns category,units,revenue_cents.
Sum numeric columns and sort categories. Empty accepted input stays empty.
"""
raise NotImplementedError("Stage 4: compute category summary")
def make_audit(raw, duplicates, rejected, accepted):
"""Stage 5. Return a dict with raw_rows, duplicate_rows, rejected_rows,
accepted_rows and accepted_revenue_cents. Sum cents with Python integers.
Verify raw = duplicates + rejected + accepted, even for empty input.
"""
raise NotImplementedError("Stage 5: reconcile counts and subtotal")
def clean_orders(csv_text):
# The orchestration is supplied; the five data decisions above are yours.
raw = read_order_records(csv_text)
work, duplicates, conflicts = prepare_work(raw)
converted, reasons = validate_work(work, conflicts)
accepted, rejected = split_evidence(raw, converted, reasons)
summary = summarize_accepted(accepted)
audit = make_audit(raw, duplicates, rejected, accepted)
return accepted, rejected, duplicates, summary, audit
def preview_stage(stage):
"""Supplied driver for the full-schema five-row checkpoint; no answers."""
raw = read_order_records(STAGE_CSV)
work, duplicates, conflicts = prepare_work(raw)
print("Stage 1 working labels:", work.index.tolist())
print("Stage 1 duplicate labels:", duplicates.index.tolist())
print("Stage 1 conflicts:", conflicts.to_dict())
if stage == 1:
return
converted, reasons = validate_work(work, conflicts)
print("Stage 2 reasons:", reasons.to_dict())
if stage == 2:
return
accepted, rejected = split_evidence(raw, converted, reasons)
print("Stage 3 accepted:\n" + accepted.to_string(index=False))
print("Stage 3 rejected evidence:\n" + rejected.to_string(index=False))
if stage == 3:
return
summary = summarize_accepted(accepted)
print("Stage 4 summary:\n" + summary.to_string(index=False))
if stage == 4:
return
print("Stage 5 audit:", make_audit(raw, duplicates, rejected, accepted))
# SUPPLIED INGESTION SUPPORT. Read the structural checkpoint, then reuse this.
def read_order_records(csv_text):
# Parse strictly before constructing a DataFrame. Some CSV readers can
# reinterpret an extra field as an index; that would lose source evidence.
try:
records = list(csv.reader(StringIO(csv_text), strict=True))
except csv.Error as error:
raise ValueError(f"malformed CSV quoting: {error}") from error
if not records:
raise ValueError("empty CSV: required header is missing")
header = records[0]
missing = set(REQUIRED) - set(header)
if missing:
raise ValueError(f"missing columns: {sorted(missing)}")
if len(header) != len(REQUIRED) or set(header) != set(REQUIRED):
raise ValueError("unexpected or duplicate columns")
if len(records) - 1 > 1000:
raise ValueError("this teaching pipeline supports at most 1000 rows")
for position, record in enumerate(records[1:], start=2):
if len(record) != len(header):
raise ValueError(
f"CSV record {position} has {len(record)} fields; "
f"expected {len(header)}. Original fields: {record!r}"
)
# No indexing inference: every validated source field becomes a table cell.
raw = pd.DataFrame(records[1:], columns=header, dtype="string")
return raw
if __name__ == "__main__":
if 1 <= CHECKPOINT_STAGE <= 5:
preview_stage(CHECKPOINT_STAGE)
elif CHECKPOINT_STAGE == 6:
from pathlib import Path
root = Path(__file__).resolve().parents[1]
text = (root / "datasets" / "workshop_orders.csv").read_text(encoding="utf-8")
accepted, rejected, duplicates, summary, audit = clean_orders(text)
print("AUDIT", audit)
for label, frame in [("ACCEPTED", accepted), ("REJECTED", rejected),
("DUPLICATES", duplicates), ("SUMMARY", summary)]:
print("\n" + label + " CSV")
print(frame.to_csv(index=False))
else:
raise ValueError("CHECKPOINT_STAGE must be 1 through 6")