fix(compare): per-school P8 explanation; disadvantaged cohort from the same yearly row
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 47s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m24s

Review findings: (1) the 'no KS2 baseline (COVID)' explanation was derived
from the basket-wide max year, so a school with an unrelated data gap
borrowed it from a neighbour with 2024/25 data — now judged per school on
its own latest year. (2) The '~N disadvantaged pupils' cohort multiplied
eligible_pupils and disadvantaged_pct resolved independently across years
— now both come from the same yearly row that supplies the displayed
percentage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
Tudor
2026-07-17 17:53:10 +01:00
co-authored by Claude Fable 5
parent f3fa12806b
commit 200a97d0b9
@@ -150,13 +150,15 @@ export function CompareAcademics({
}); });
// DfE stopped publishing Progress 8 from 2024/25: those GCSE year groups // DfE stopped publishing Progress 8 from 2024/25: those GCSE year groups
// sat no KS2 tests (COVID), so there is no baseline to measure progress // sat no KS2 tests (COVID), so there is no baseline to measure progress
// from. A bare "No data" reads as a gap on our side — say why. // from. A bare "No data" reads as a gap on our side — say why. Judged
const latestYear = urns.reduce((max, urn) => { // PER SCHOOL on its own latest data year: a school whose data simply
// stops earlier (an unrelated gap) must not borrow the COVID explanation
// from a neighbour that does have 2024/25 data.
const p8NotPublished = urns.map((urn) => {
const rows = data[String(urn)]?.yearly_data ?? []; const rows = data[String(urn)]?.yearly_data ?? [];
const y = rows.length ? Math.trunc(rows[rows.length - 1].year) : 0; const y = rows.length ? Math.trunc(rows[rows.length - 1].year) : 0;
return Math.max(max, y); return y >= 202425;
}, 0); });
const p8NotPublished = latestYear >= 202425;
const grade5 = latestValues(data, urns, 'english_maths_strong_pass_pct'); const grade5 = latestValues(data, urns, 'english_maths_strong_pass_pct');
const ebacc = latestValues(data, urns, 'ebacc_entry_pct'); const ebacc = latestValues(data, urns, 'ebacc_entry_pct');
const att8Anchor = nationalAverages?.secondary?.attainment_8_score; const att8Anchor = nationalAverages?.secondary?.attainment_8_score;
@@ -198,7 +200,7 @@ export function CompareAcademics({
> >
{banding[i]} {banding[i]}
</Chip> </Chip>
) : p8NotPublished ? ( ) : p8NotPublished[i] ? (
<span className={s.small}> <span className={s.small}>
Not published this GCSE year group sat no KS2 tests (COVID), so DfE has no Not published this GCSE year group sat no KS2 tests (COVID), so DfE has no
baseline to measure progress from baseline to measure progress from
@@ -234,15 +236,21 @@ export function CompareAcademics({
const disadvantagedAnchor = benchmarks?.primary?.disadvantaged_rwm_expected_pct ?? null; const disadvantagedAnchor = benchmarks?.primary?.disadvantaged_rwm_expected_pct ?? null;
// Cohort size behind the disadvantaged figure (spec §8.5): these are small // Cohort size behind the disadvantaged figure (spec §8.5): these are small
// groups where single pupils move the percentage — show roughly how many // groups where single pupils move the percentage — show roughly how many
// pupils the figure rests on. // pupils the figure rests on. Taken from the SAME yearly row that supplies
const eligible = latestValues(data, urns, 'eligible_pupils'); // the displayed percentage: resolving eligible_pupils and the
const disadvantagedShare = latestValues(data, urns, 'disadvantaged_pct'); // disadvantaged share independently could mix years and misstate the
const cohorts = urns.map((_, i) => { // cohort behind the figure.
const n = eligible[i]; const cohorts = urns.map((urn) => {
const share = disadvantagedShare[i]; const rows = data[String(urn)]?.yearly_data ?? [];
if (n == null || share == null) return null; for (let i = rows.length - 1; i >= 0; i--) {
const cohort = Math.round((n * share) / 100); const row = rows[i];
return cohort > 0 ? cohort : null; if (row.rwm_expected_disadvantaged_pct != null) {
if (row.eligible_pupils == null || row.disadvantaged_pct == null) return null;
const cohort = Math.round((row.eligible_pupils * row.disadvantaged_pct) / 100);
return cohort > 0 ? cohort : null;
}
}
return null;
}); });
return ( return (