Skip to content
Aabha AI Academy
Skip to reading

Python

Capstone

Compare these references after your own attempt. You can read them at any time; they do not record a live model or vendor run.

"""An explicit small-data cleaning policy; never changes the raw input.

Accepted-record subtotal only. This is an instructional pipeline, not a
financial reporting system. Run `python -m solutions.capstone` from course root.
"""
from io import StringIO
import csv
from pathlib import Path
import pandas as pd
import matplotlib.pyplot as plt

REQUIRED = ["order_id", "date", "category", "units", "unit_price_cents"]
CATEGORIES = {"notebooks", "pens", "markers"}


def clean_orders(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")
    # Exact duplicates compare source field values, before normalization.
    duplicate_mask = raw.duplicated(keep="first")
    duplicates = raw.loc[duplicate_mask].copy()
    work = raw.loc[~duplicate_mask].copy()
    work["order_id"] = work["order_id"].str.strip()
    work["category"] = work["category"].str.strip().str.lower()
    # A same-ID disagreement is not permission to choose a winner.
    conflict_mask = work["order_id"].duplicated(keep=False)
    iso_date_shape = work["date"].str.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
    work["date"] = pd.to_datetime(work["date"], format="%Y-%m-%d", errors="coerce")
    integer_syntax = {}
    for column in ["units", "unit_price_cents"]:
        numeric_text = work[column].str.strip()
        integer_syntax[column] = numeric_text.str.fullmatch(r"[0-9]+")
        work[column] = pd.to_numeric(numeric_text, errors="coerce")
    valid_units = (integer_syntax["units"] & work["units"].notna() & work["units"].gt(0)
                   & work["units"].le(1_000_000) & work["units"].mod(1).eq(0))
    valid_price = (integer_syntax["unit_price_cents"] & work["unit_price_cents"].notna() & work["unit_price_cents"].ge(0)
                   & work["unit_price_cents"].le(1_000_000_000)
                   & work["unit_price_cents"].mod(1).eq(0))
    # Upper bounds are an explicit teaching contract, keeping int64 products safe.
    checks = {
        "missing_order_id": work["order_id"].eq(""),
        "conflicting_order_id": conflict_mask,
        "invalid_date": (work["date"].isna() | ~iso_date_shape
                         | ~work["date"].between("2000-01-01", "2099-12-31")),
        "invalid_category": ~work["category"].isin(CATEGORIES),
        "invalid_units": ~valid_units,
        "invalid_price": ~valid_price,
    }
    reasons = pd.Series("", index=work.index, dtype="string")
    for reason, mask in checks.items():
        reasons.loc[mask] = reasons.loc[mask] + reason + ";"
    valid = reasons.eq("")
    rejected = raw.loc[work.index[~valid]].copy()
    rejected["reason"] = reasons.loc[~valid].str.rstrip(";")
    accepted = work.loc[valid].copy()
    for column in ["units", "unit_price_cents"]:
        accepted[column] = accepted[column].astype("int64")
    accepted["revenue_cents"] = accepted["units"] * accepted["unit_price_cents"]
    accepted = accepted.sort_values(["date", "order_id"]).reset_index(drop=True)
    summary = (accepted.groupby("category", as_index=False)[["units", "revenue_cents"]]
               .sum().sort_values("category").reset_index(drop=True))
    # Python ints avoid fixed-width overflow in the overall audit subtotal.
    subtotal = sum(int(value) for value in accepted["revenue_cents"])
    audit = {"raw_rows": len(raw), "duplicate_rows": len(duplicates),
             "rejected_rows": len(rejected), "accepted_rows": len(accepted),
             "accepted_revenue_cents": subtotal}
    return accepted, rejected, duplicates, summary, audit


def plot_summary(summary):
    fig, ax = plt.subplots(figsize=(7, 4.4))
    fig.subplots_adjust(bottom=0.24, top=0.86, left=0.12, right=0.97)
    ax.bar(summary["category"], summary["revenue_cents"] / 100, color="#0d9488")
    ax.set(title="Accepted-order subtotal by category (synthetic)",
           xlabel="Category", ylabel="Fictional currency units")
    largest = max((float(value) / 100 for value in summary["revenue_cents"]), default=0)
    ax.set_ylim(0, max(1.0, largest * 1.2))
    if summary.empty:
        ax.text(0.5, 0.5, "No accepted records", transform=ax.transAxes, ha="center")
    fig.text(0.02, 0.045, "Rejected records are excluded.\nThis accepted subtotal is not a complete sales total.", fontsize=8.5)
    return fig


def main():
    root = Path(__file__).resolve().parents[1]
    csv_text = (root / "datasets" / "workshop_orders.csv").read_text(encoding="utf-8")
    accepted, rejected, duplicates, summary, audit = clean_orders(csv_text)
    output = root / "outputs"
    output.mkdir(exist_ok=True)
    for name, frame in [("accepted_orders", accepted), ("rejected_orders", rejected),
                        ("duplicate_orders", duplicates), ("category_summary", summary)]:
        frame.to_csv(output / f"{name}.csv", index=False)
    fig = plot_summary(summary)
    fig.savefig(output / "accepted_subtotal.png", dpi=160)
    plt.close(fig)
    print(audit)
    print(summary.to_string(index=False))


if __name__ == "__main__":
    main()

Download this source file