Fix Ofsted transitional inspections, phase tab exclusions, and FSM benchmark comparison #45

Merged
tudor merged 1 commits from fix/compare-expert-fixes into main 2026-07-15 16:30:00 +00:00
9 changed files with 69 additions and 26 deletions
+1
View File
@@ -577,6 +577,7 @@ def compute_benchmarks(df: pd.DataFrame) -> dict:
"eal_pct": _median(sub, "eal_pct"), "eal_pct": _median(sub, "eal_pct"),
"sen_support_pct": _median(sub, "sen_support_pct"), "sen_support_pct": _median(sub, "sen_support_pct"),
"disadvantaged_pct": _median(sub, "disadvantaged_pct"), "disadvantaged_pct": _median(sub, "disadvantaged_pct"),
"fsm_pct": _median(sub, "fsm_pct"),
"median_pupils": median_pupils, "median_pupils": median_pupils,
} }
if with_disadvantaged: if with_disadvantaged:
+12 -9
View File
@@ -17,33 +17,33 @@ def _df():
# weighted = (40*100 + 60*300) / 400 = 55.0 ; unweighted mean = 50.0 # weighted = (40*100 + 60*300) / 400 = 55.0 ; unweighted mean = 50.0
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=100, dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=100,
rwm_expected_disadvantaged_pct=40.0, eal_pct=10.0, rwm_expected_disadvantaged_pct=40.0, eal_pct=10.0,
sen_support_pct=10.0, disadvantaged_pct=20.0, total_pupils=200), sen_support_pct=10.0, disadvantaged_pct=20.0, fsm_pct=15.0, total_pupils=200),
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=300, dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=300,
rwm_expected_disadvantaged_pct=60.0, eal_pct=20.0, rwm_expected_disadvantaged_pct=60.0, eal_pct=20.0,
sen_support_pct=14.0, disadvantaged_pct=24.0, total_pupils=280), sen_support_pct=14.0, disadvantaged_pct=24.0, fsm_pct=17.0, total_pupils=280),
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=np.nan, dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=np.nan,
rwm_expected_disadvantaged_pct=99.0, eal_pct=30.0, rwm_expected_disadvantaged_pct=99.0, eal_pct=30.0,
sen_support_pct=18.0, disadvantaged_pct=30.0, total_pupils=300), sen_support_pct=18.0, disadvantaged_pct=30.0, fsm_pct=19.0, total_pupils=300),
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=50, dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=50,
rwm_expected_disadvantaged_pct=np.nan, eal_pct=np.nan, rwm_expected_disadvantaged_pct=np.nan, eal_pct=np.nan,
sen_support_pct=np.nan, disadvantaged_pct=np.nan, total_pupils=np.nan), sen_support_pct=np.nan, disadvantaged_pct=np.nan, fsm_pct=np.nan, total_pupils=np.nan),
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=40, dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=40,
rwm_expected_disadvantaged_pct=np.nan, eal_pct=40.0, rwm_expected_disadvantaged_pct=np.nan, eal_pct=40.0,
sen_support_pct=20.0, disadvantaged_pct=40.0, total_pupils=350), sen_support_pct=20.0, disadvantaged_pct=40.0, fsm_pct=21.0, total_pupils=350),
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=60, dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=60,
rwm_expected_disadvantaged_pct=np.nan, eal_pct=50.0, rwm_expected_disadvantaged_pct=np.nan, eal_pct=50.0,
sen_support_pct=22.0, disadvantaged_pct=44.0, total_pupils=400), sen_support_pct=22.0, disadvantaged_pct=44.0, fsm_pct=23.0, total_pupils=400),
# Two secondary schools (attainment_8 non-null) # Two secondary schools (attainment_8 non-null)
dict(year=LATEST, attainment_8_score=45.0, eligible_pupils=180, dict(year=LATEST, attainment_8_score=45.0, eligible_pupils=180,
rwm_expected_disadvantaged_pct=np.nan, eal_pct=15.0, rwm_expected_disadvantaged_pct=np.nan, eal_pct=15.0,
sen_support_pct=12.0, disadvantaged_pct=22.0, total_pupils=1000), sen_support_pct=12.0, disadvantaged_pct=22.0, fsm_pct=12.0, total_pupils=1000),
dict(year=LATEST, attainment_8_score=50.0, eligible_pupils=200, dict(year=LATEST, attainment_8_score=50.0, eligible_pupils=200,
rwm_expected_disadvantaged_pct=np.nan, eal_pct=25.0, rwm_expected_disadvantaged_pct=np.nan, eal_pct=25.0,
sen_support_pct=16.0, disadvantaged_pct=26.0, total_pupils=1200), sen_support_pct=16.0, disadvantaged_pct=26.0, fsm_pct=14.0, total_pupils=1200),
# An older-year primary row that must NOT influence anything # An older-year primary row that must NOT influence anything
dict(year=202324, attainment_8_score=np.nan, eligible_pupils=500, dict(year=202324, attainment_8_score=np.nan, eligible_pupils=500,
rwm_expected_disadvantaged_pct=1.0, eal_pct=99.0, rwm_expected_disadvantaged_pct=1.0, eal_pct=99.0,
sen_support_pct=99.0, disadvantaged_pct=99.0, total_pupils=9999), sen_support_pct=99.0, disadvantaged_pct=99.0, fsm_pct=99.0, total_pupils=9999),
] ]
return pd.DataFrame(rows) return pd.DataFrame(rows)
@@ -59,6 +59,8 @@ def test_medians_ignore_nan_and_older_years():
assert b["year"] == LATEST assert b["year"] == LATEST
# eal medians over [10,20,30,40,50] = 30 # eal medians over [10,20,30,40,50] = 30
assert b["primary"]["eal_pct"] == 30.0 assert b["primary"]["eal_pct"] == 30.0
# fsm medians over [15,17,19,21,23] = 19
assert b["primary"]["fsm_pct"] == 19.0
# median pupils over [200,280,300,350,400] = 300 # median pupils over [200,280,300,350,400] = 300
assert b["primary"]["median_pupils"] == 300 assert b["primary"]["median_pupils"] == 300
@@ -66,6 +68,7 @@ def test_medians_ignore_nan_and_older_years():
def test_secondary_block_has_no_disadvantaged_rwm(): def test_secondary_block_has_no_disadvantaged_rwm():
b = compute_benchmarks(_df()) b = compute_benchmarks(_df())
assert "disadvantaged_rwm_expected_pct" not in b["secondary"] assert "disadvantaged_rwm_expected_pct" not in b["secondary"]
assert b["secondary"]["fsm_pct"] == 13.0
assert b["secondary"]["median_pupils"] == 1100 assert b["secondary"]["median_pupils"] == 1100
@@ -126,6 +126,13 @@ describe('ofstedDisplay', () => {
expect(ofstedDisplay(ofsted({})).kind).toBe('none'); expect(ofstedDisplay(ofsted({})).kind).toBe('none');
}); });
it('identifies transitional inspections without overall grades', () => {
const transitional = ofstedDisplay(
ofsted({ overall_effectiveness: null, inspection_date: '2024-11-05' }),
);
expect(transitional.kind).toBe('transitional');
});
it('uses the four legacy grade words', () => { it('uses the four legacy grade words', () => {
expect(OFSTED_LEGACY_GRADES).toEqual({ expect(OFSTED_LEGACY_GRADES).toEqual({
1: 'Outstanding', 1: 'Outstanding',
+15 -11
View File
@@ -142,19 +142,23 @@ export function ComparisonView({
}); });
}, [urnKey, isInitialized]); }, [urnKey, isInitialized]);
// Classify schools by phase using comparison data const primarySchools = selectedSchools.filter((school) => {
const classifySchool = (school: School): 'primary' | 'secondary' => {
const info = comparisonData?.[school.urn]?.school_info; const info = comparisonData?.[school.urn]?.school_info;
if (info?.attainment_8_score != null) return 'secondary'; const hasPrimaryData =
if (info?.rwm_expected_pct != null) return 'primary'; info?.rwm_expected_pct != null ||
// Fallback: check yearly data comparisonData?.[school.urn]?.yearly_data?.some((d) => d.rwm_expected_pct != null);
const yearlyData = comparisonData?.[school.urn]?.yearly_data; if (hasPrimaryData) return true;
if (yearlyData?.some((d) => d.attainment_8_score != null)) return 'secondary'; return school.phase?.toLowerCase().includes('primary') || false;
return 'primary'; });
};
const primarySchools = selectedSchools.filter((s) => classifySchool(s) === 'primary'); const secondarySchools = selectedSchools.filter((school) => {
const secondarySchools = selectedSchools.filter((s) => classifySchool(s) === 'secondary'); const info = comparisonData?.[school.urn]?.school_info;
const hasSecondaryData =
info?.attainment_8_score != null ||
comparisonData?.[school.urn]?.yearly_data?.some((d) => d.attainment_8_score != null);
if (hasSecondaryData) return true;
return school.phase?.toLowerCase().includes('secondary') || false;
});
// Auto-select tab with more schools and sync the metric to match the phase. // Auto-select tab with more schools and sync the metric to match the phase.
useEffect(() => { useEffect(() => {
@@ -83,6 +83,14 @@ export function CompareAtAGlance({
{display.carriedForward && <span className={s.small}>Grade carried forward</span>} {display.carriedForward && <span className={s.small}>Grade carried forward</span>}
</> </>
)} )}
{display.kind === 'transitional' && (
<>
<span className={s.badge} style={{ backgroundColor: '#e2e8f0', color: '#475569' }}>
No overall grade
</span>
<span className={s.small}>Sub-judgements only</span>
</>
)}
{display.kind === 'none' && <span className={s.small}>No inspection in our dataset</span>} {display.kind === 'none' && <span className={s.small}>No inspection in our dataset</span>}
</Cell> </Cell>
); );
@@ -31,13 +31,14 @@ export function CompareCommunity({
const bench = isSecondary ? benchmarks?.secondary : benchmarks?.primary; const bench = isSecondary ? benchmarks?.secondary : benchmarks?.primary;
const fsmChip = (value: number | null) => { const fsmChip = (value: number | null) => {
if (value == null || bench?.disadvantaged_pct == null) return null; const anchor = bench?.fsm_pct ?? bench?.disadvantaged_pct ?? null;
const v = verdict(value, bench.disadvantaged_pct, 3); if (value == null || anchor == null) return null;
const v = verdict(value, anchor, 3);
return ( return (
<Chip tone="neutral"> <Chip tone="neutral">
{v === 'above' && 'Above the state-school average'} {v === 'above' && `Above the state-school average (${Math.round(anchor)}%)`}
{v === 'close' && 'About the state-school average'} {v === 'close' && `About the state-school average (${Math.round(anchor)}%)`}
{v === 'below' && 'Below the state-school average'} {v === 'below' && `Below the state-school average (${Math.round(anchor)}%)`}
</Chip> </Chip>
); );
}; };
@@ -51,6 +51,18 @@ function ResultCell({ display }: { display: OfstedDisplay }) {
</> </>
); );
} }
if (display.kind === 'transitional') {
return (
<>
<span className={s.badge} style={{ backgroundColor: '#e2e8f0', color: '#475569' }}>
No overall grade
</span>
<span className={s.small}>
Inspected under transitional framework (sub-judgements only)
</span>
</>
);
}
return ( return (
<> <>
<span className={`${s.badge} ${display.grade <= 2 ? s.badgeGood : display.grade === 3 ? s.badgeWarn : s.badgeBad}`}> <span className={`${s.badge} ${display.grade <= 2 ? s.badgeGood : display.grade === 3 ? s.badgeWarn : s.badgeBad}`}>
+7 -1
View File
@@ -102,6 +102,7 @@ export type OfstedDisplay =
| { kind: 'none' } | { kind: 'none' }
| { kind: 'graded'; grade: number; gradeLabel: string; carriedForward: false } | { kind: 'graded'; grade: number; gradeLabel: string; carriedForward: false }
| { kind: 'carried_forward'; grade: number; gradeLabel: string; carriedForward: true } | { kind: 'carried_forward'; grade: number; gradeLabel: string; carriedForward: true }
| { kind: 'transitional' }
| { kind: 'report_card'; summary: ReportCardSummary }; | { kind: 'report_card'; summary: ReportCardSummary };
export function ofstedDisplay( export function ofstedDisplay(
@@ -117,7 +118,12 @@ export function ofstedDisplay(
const grade = ofsted.overall_effectiveness; const grade = ofsted.overall_effectiveness;
const gradeLabel = grade != null ? OFSTED_LEGACY_GRADES[grade] : undefined; const gradeLabel = grade != null ? OFSTED_LEGACY_GRADES[grade] : undefined;
if (grade == null || gradeLabel === undefined) return { kind: 'none' }; if (grade == null || gradeLabel === undefined) {
if (ofsted.inspection_date) {
return { kind: 'transitional' };
}
return { kind: 'none' };
}
if (ofsted.grade_source === 'ungraded_carried_forward') { if (ofsted.grade_source === 'ungraded_carried_forward') {
return { kind: 'carried_forward', grade, gradeLabel, carriedForward: true }; return { kind: 'carried_forward', grade, gradeLabel, carriedForward: true };
+1
View File
@@ -357,6 +357,7 @@ export interface BenchmarkBlock {
eal_pct: number | null; eal_pct: number | null;
sen_support_pct: number | null; sen_support_pct: number | null;
disadvantaged_pct: number | null; disadvantaged_pct: number | null;
fsm_pct?: number | null;
median_pupils: number | null; median_pupils: number | null;
/** Primary only — weighted by cohort size. */ /** Primary only — weighted by cohort size. */
disadvantaged_rwm_expected_pct?: number | null; disadvantaged_rwm_expected_pct?: number | null;