# Exam Results Taxonomy — Phase Grouping and Sixth-Form Separation

**Date:** 2026-07-07
**Status:** Approved design (taxonomy/analysis only — no implementation in this doc's scope)

## Purpose

Classify every exam-result metric SchoolCompare displays today into four phase
groups — **Primary**, **Secondary**, **Sixth form**, **Other** — and define an
authoritative rule for separating schools that have a sixth form from those
that don't. This document is the reference for:

1. How the UI should group results sections and rankings by phase.
2. The future KS5 (A-level) ingestion work — the Sixth form group lists the
   concrete DfE metrics as placeholders with source columns.
3. Replacing the fragile `age_range contains "18"` heuristic with the GIAS
   `OfficialSixthForm` flag.

## 1. Grouping principle

Metrics are grouped by **the key stage of the assessment**, not by the phase
of the school displaying them. An all-through school (4–18) shows metrics in
all three exam groups; a pure primary shows only the Primary group.

| Group | Assessments | Key stage | Taken at age | Data status |
|---|---|---|---|---|
| **Primary** | KS2 SATs (reading, writing TA, maths, GPS, science TA) | KS2 | 10–11 (Year 6) | ✅ Live — `marts.fact_ks2_performance` |
| **Secondary** | GCSEs, Attainment 8 / Progress 8, EBacc | KS4 | 15–16 (Year 11) | ✅ Live — `marts.fact_ks4_performance` |
| **Sixth form** | A levels, applied general, tech levels | KS5 (16–18) | 17–18 (Year 12–13) | ⏳ Not ingested — placeholders in §4 |
| **Other** | Non-exam context displayed alongside results | n/a | n/a | ✅ Live — various marts |

Not covered (not displayed today, candidates for future "Other"/Primary):
EYFS Good Level of Development, Year 1 Phonics check, Year 4 Multiplication
Tables Check, KS1 assessments (no longer published at school level by DfE).

## 2. Metric-by-metric mapping (current site)

Every key in `backend/schemas.py` `METRIC_DEFINITIONS` — the single source of
truth for what the site displays — mapped to its phase group. `category` is
the existing schema category; source columns are the DfE names used at
ingestion (legacy performance-tables CSV for KS2, EES for KS4).

### Primary (KS2 SATs)

| Metric key | Category | DfE source column |
|---|---|---|
| `rwm_expected_pct` | expected | `PTRWM_EXP` |
| `reading_expected_pct` | expected | `PTREAD_EXP` |
| `writing_expected_pct` | expected | `PTWRITTA_EXP` |
| `maths_expected_pct` | expected | `PTMAT_EXP` |
| `gps_expected_pct` | expected | `PTGPS_EXP` |
| `science_expected_pct` | expected | `PTSCITA_EXP` |
| `rwm_high_pct` | higher | `PTRWM_HIGH` |
| `reading_high_pct` | higher | `PTREAD_HIGH` |
| `writing_high_pct` | higher | `PTWRITTA_HIGH` |
| `maths_high_pct` | higher | `PTMAT_HIGH` |
| `gps_high_pct` | higher | `PTGPS_HIGH` |
| `reading_progress` | progress | `READPROG` |
| `writing_progress` | progress | `WRITPROG` |
| `maths_progress` | progress | `MATPROG` |
| `reading_avg_score` | average | `READ_AVERAGE` |
| `maths_avg_score` | average | `MAT_AVERAGE` |
| `gps_avg_score` | average | `GPS_AVERAGE` |
| `rwm_expected_boys_pct` | gender | `PTRWM_EXP_B` |
| `rwm_expected_girls_pct` | gender | `PTRWM_EXP_G` |
| `rwm_high_boys_pct` | gender | `PTRWM_HIGH_B` |
| `rwm_high_girls_pct` | gender | `PTRWM_HIGH_G` |
| `rwm_expected_disadvantaged_pct` | equity | `PTRWM_EXP_FSM6CLA1A` |
| `rwm_expected_non_disadvantaged_pct` | equity | `PTRWM_EXP_NotFSM6CLA1A` |
| `disadvantaged_gap` | equity | `DIFFN_RWM_EXP` |
| `reading_absence_pct` | absence | `PTREAD_AT` |
| `gps_absence_pct` | absence | `PTGPS_AT` |
| `maths_absence_pct` | absence | `PTMAT_AT` |
| `writing_absence_pct` | absence | `PTWRITTA_AD` |
| `science_absence_pct` | absence | `PTSCITA_AD` |
| `rwm_expected_3yr_pct` | trends | `PTRWM_EXP_3YR` |
| `reading_avg_3yr` | trends | `READ_AVERAGE_3YR` |
| `maths_avg_3yr` | trends | `MAT_AVERAGE_3YR` |

The absence metrics measure absence *from KS2 tests*, so they belong to
Primary even though they are not attainment scores. National comparators for
this group come from `marts.fact_ks2_national_averages`.

### Secondary (KS4 / GCSE)

| Metric key | Category | EES source column |
|---|---|---|
| `attainment_8_score` | gcse | `attainment8_average` |
| `progress_8_score` | gcse | `progress8_average` |
| `english_maths_standard_pass_pct` | gcse | `engmath_94_percent` |
| `english_maths_strong_pass_pct` | gcse | `engmath_95_percent` |
| `ebacc_entry_pct` | gcse | `ebacc_entering_percent` |
| `ebacc_standard_pass_pct` | gcse | `ebacc_94_percent` |
| `ebacc_strong_pass_pct` | gcse | `ebacc_95_percent` |
| `ebacc_avg_score` | gcse | `ebacc_aps_average` |
| `gcse_grade_91_pct` | gcse | `gcse_91_percent` |

Also stored in `marts.fact_ks4_performance` (and `fact_performance`) but not
yet in `METRIC_DEFINITIONS` — Secondary group members when surfaced:
`progress_8_lower_ci`, `progress_8_upper_ci`, `progress_8_english`,
`progress_8_maths`, `progress_8_ebacc`, `progress_8_open`,
`prior_attainment_avg` (KS2 baseline of the GCSE cohort), `sen_pct`.

### Sixth form (KS5)

No metrics today. The secondary school detail view renders a static note
("Post-16 destination data coming soon") when the school has a sixth form.
Placeholders for ingestion are specified in §4.

### Other (non-exam context)

Displayed alongside results but not tied to any assessment:

| Metric key / surface | Category | Source |
|---|---|---|
| `disadvantaged_pct` | context | KS2 CSV `PTFSM6CLA1A` |
| `eal_pct` | context | KS2 CSV `PTEALGRP2` |
| `sen_support_pct` | context | KS2 CSV `PSENELK` (KS4 fallback `sen_no_ehcp_pupil_percent`) |
| `stability_pct` | context | KS2 CSV `PTMOBN` |
| Ofsted grades incl. `sixth_form_provision` / `rc_sixth_form` | — | `marts.fact_ofsted_inspection` |
| Admissions (offers, oversubscription) | — | `marts.fact_admissions` |
| Finance (per-pupil spend, cost shares) | — | `marts.fact_finance` |
| Deprivation (IDACI) | — | `marts.fact_deprivation` |
| Pupil characteristics (census) | — | `marts.fact_pupil_characteristics` |

Note: the context metrics are cohort characteristics of the KS2 cohort at
source, but they are presented (and should stay presented) as school-level
context, so they group as Other, not Primary.

## 3. Sixth-form separation

### Definition (authoritative)

> A school **has a sixth form** iff GIAS `OfficialSixthForm (name)` =
> `"Has a sixth form"` for its URN.

GIAS values are `Has a sixth form`, `Does not have a sixth form`, and
`Not applicable` / blank. `Not applicable` (nurseries, primaries, PRUs) maps
to **false**. This field is the DfE's registry flag, updated continuously,
and is the only source that correctly classifies:

- 16–19 sixth-form colleges and UTCs (age ranges like `14-19`, `16-19` that
  the current substring heuristic misclassifies as *no* sixth form);
- schools whose statutory age range extends to 18 on paper but which have no
  registered post-16 provision.

### Pipeline change (implemented 2026-07-07)

1. `stg_gias_establishments.sql`: add
   `"OfficialSixthForm (name)" as official_sixth_form`.
2. `dim_school.sql` (+ `models.py` `DimSchool`, `_marts_schema.yml`): add
   `has_sixth_form boolean` = `official_sixth_form = 'Has a sixth form'`.
3. Expose `has_sixth_form` on the school API payloads.

Implemented in `feat/gias-sixth-form-flag` — see
`docs/superpowers/plans/2026-07-07-gias-sixth-form-flag.md`.

### Current heuristic — audit of `age_range` ~ "18" sites

All must migrate to the `has_sixth_form` flag once exposed:

| Site | Current behaviour |
|---|---|
| `backend/app.py:419-422` | `/api/schools?has_sixth_form=yes\|no` filters on `age_range.str.contains("18")` |
| `nextjs-app/components/SecondarySchoolDetailView.tsx:101` | "Sixth form" badge + coming-soon note from `age_range?.includes('18')` |
| `nextjs-app/components/FilterBar.tsx:370-372` | Filter labels hard-code "(11-18)" / "(11-16)" — labels should drop the age-range parenthetical since sixth form ≠ age range |

Fallback rule: if GIAS is blank for a URN (rare; new establishments), fall
back to the age-range heuristic and log the URN.

### UI separation rules

- **School page**: schools with `has_sixth_form = true` show a Sixth form
  results section (placeholder until KS5 data lands); schools without never
  show it. Badge on the header as today, but driven by the flag.
- **Search/rankings filter**: "With sixth form" / "Without sixth form" uses
  the flag; applies to secondary and all-through phases.
- **Comparison**: when comparing a with-sixth-form school against one
  without, the Sixth form group renders "No sixth form" for the latter
  rather than blank cells, making the structural difference explicit.

## 4. Sixth form placeholders — future KS5 ingestion spec

Source: DfE "A level and other 16 to 18 results" (EES, preferred — matches
the KS4 EES tap) or legacy performance-tables `england_ks5final.csv`.
Column names below are from the legacy KS5 CSV; verify against the EES
release chosen at ingestion time.

| Proposed metric key | Name | Legacy source column | Type |
|---|---|---|---|
| `alevel_aps_per_entry` | A level average points per entry | `TALLPPE_ALEV_1618` | score |
| `alevel_avg_grade` | A level average grade (e.g. B-) | `TALLPPEGRD_ALEV_1618` | grade |
| `academic_aps_per_entry` | Academic qualifications APS per entry | `TALLPPE_ACAD_1618` | score |
| `applied_general_aps_per_entry` | Applied general APS per entry | `TALLPPE_AGEN_1618` | score |
| `tech_level_aps_per_entry` | Tech level APS per entry | `TALLPPE_TLEV_1618` | score |
| `english_progress_1618` | English progress (16–18, unfinished GCSE 4+) | `PROGENG_1618` | score |
| `maths_progress_1618` | Maths progress (16–18) | `PROGMAT_1618` | score |
| `ks5_cohort_size` | Students at end of 16–18 study | `TALLPUP_1618` | count |
| `alevel_3plus_aab_pct` | % achieving AAB+ in ≥2 facilitating subjects | `TAAB2FAC_1618` | percentage |
| `ks5_retention_pct` | Retention (completed main programme) | study-programme retention measure | percentage |
| `ks5_destinations_pct` | Sustained education/employment destination | 16–18 destination measures dataset | percentage |

Proposed landing shape mirrors KS4: `stg_ees_ks5.sql` →
`int_ks5_with_lineage.sql` → `marts.fact_ks5_performance` (one row per URN
per year), joined into `fact_performance`, with a `category: "sixth_form"`
(or `"alevel"`) block added to `METRIC_DEFINITIONS`.

## 5. Out of scope

- Any implementation (pipeline, API, or UI changes) — this is the taxonomy
  reference; implementation work items are §3 "Pipeline change", the
  heuristic migration audit, and §4 ingestion, each to be planned separately.
- Middle schools (deemed secondary/primary): they follow the assessment-based
  grouping automatically — no special casing.
- Independent schools: no DfE performance data published; unaffected.
