Method — why these numbers can be trusted

The full account is docs/PROJECT-REPORT.md in the repository; this page is the condensed version a reviewer needs before quoting a number.

Two layers, one provenance rule

The as-printed layer records exactly what the handbooks print — defects included — for audit and replication. The final-clean layer (everything this explorer shows) resolves those defects through documented rules and a reviewable adjudication register. Every row of every table carries the handbook edition and the PDF page it came from. Source PDFs are immutable; outputs are never hand-edited; re-running one year or faculty provably cannot alter another's rows.

The ideal student

Handbooks list more than any student takes. Credit load and cost are computed for a deterministic, documented selection: every core course; the first-listed branch of any choice (alternates retained); elective slots at their stated credits, priced at a median and always flagged as estimates; minima taken exactly. It is a convention, not a behavioural claim — and it is a boolean on every row, so "what if they took the other branch" is a filter, not a re-count.

Triangulated validation

   curriculum (courses + credits)  x  course fees   ->  computed year cost
        compared with                                   compared with
   the handbook's printed                      the fees book's published
   "Total credits per year"                    typical fee for that year
        and, summed across all study years, compared with
   the faculty rules' printed minimum credits for the whole degree

The third leg — the rules layer — was extracted from the faculty-rules sections (387 printed statements with page and verbatim quote). It dates the credit re-think authoritatively and catches what per-year checks cannot: a curriculum below its own faculty's floor, or a stated total the tables no longer support.

How discrepancies are resolved

Rule What it does Confidence
R0 computed equals stated — nothing to do high
R3 a register entry applies (rationale + page evidence) high
R1a the gap exactly equals the non-taken choice branches — the printed total counted both high
R2a identical course set reconciles in ≥2 sibling editions medium
R1b / R2b misprint / extraction-gap detectors — file suggestions, never auto-resolve
R4 nothing applies — carry computed, flag unresolved, queue for review low

"Computed" is the default because, where both sides can be tested, the rows win: summing each taken course's fee reproduces UCT's own published programme fees to the rand for most programmes. A mismatch is a finding, not an error — most turn out to be defects in the books themselves, which is precisely what a curriculum-review dataset should surface.

Real (2025-rand) fees

Nominal fees rise ~4.3–5.8% per edition from price escalation alone. The dataset derives its own deflator from the fees books: the median year-over-year fee ratio across all course codes present in consecutive editions (~3,900 matches per pair, spread of a few hundredths of a percentage point), chained and rebased to 2025 = 1.0. Real series (v_fee_real, and the "2025 rands" charts on the fees and programme pages) divide nominal fees by this index — the correct series for "fee income gain/loss", since credit changes are not proportional to fee changes.

Reading the labels

  • no_anchor — Science/Humanities majors: no per-year total exists to reconcile against, by design. Never read a major's sum as a degree total.
  • flat_annual / degree_flat — Law publishes one fee per stream, Science/Humanities one per degree: per-year fee divergence is structural.
  • confidence — high (arithmetic or adjudicated), medium (cross-edition), low (default policy; see the adjudication queue).
  • Estimated fees — elective-slot costs are median-based and carried separately, so exact-figure analyses can exclude them.

Reproducing this site

python run_pipeline.py --years all        # rebuild the dataset from the PDFs
python analysis/build_database.py         # rebuild handbooks.duckdb
cd analysis/explorer && npm run sources && npm run dev