diff --git a/nextjs-app/__tests__/components/LeafletMapInner.test.tsx b/nextjs-app/__tests__/components/LeafletMapInner.test.tsx
index 5a47ae0..e910061 100644
--- a/nextjs-app/__tests__/components/LeafletMapInner.test.tsx
+++ b/nextjs-app/__tests__/components/LeafletMapInner.test.tsx
@@ -105,3 +105,20 @@ it('puts the card back when the pins are rebuilt for a reason other than the sch
expect(container.querySelector('.sc-popup')).toHaveTextContent('Southmead Primary School');
expect(container.querySelectorAll('.sc-pin--selected')).toHaveLength(1);
});
+
+it('compares a state secondary with its LA average on the card, but not an independent one', () => {
+ const state: School = { ...base, urn: 3, school_name: 'Holland Park School', school_type: 'Academy converter',
+ phase: 'Secondary', local_authority: 'Kensington and Chelsea', attainment_8_score: 60,
+ latitude: 51.5, longitude: -0.2, distance: 0.3 };
+ const independent: School = { ...state, urn: 4, school_name: 'Abbey Gate College',
+ school_type: 'Other independent school', attainment_8_score: 20.4 };
+ const laAverages = { 'Kensington and Chelsea': 54.5 };
+
+ const { container, rerender } = renderMap({ schools: [state, independent], laAverages, selectedUrn: 3 });
+ expect(container.querySelector('.sc-popup')).toHaveTextContent('60.0 Att 8 +5.5 vs LA');
+
+ rerender({ schools: [state, independent], laAverages, selectedUrn: 4 });
+ const card = container.querySelector('.sc-popup')!;
+ expect(card).toHaveTextContent('20.4 Att 8');
+ expect(card).not.toHaveTextContent(/vs LA/);
+});
diff --git a/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx b/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx
index 9503a84..2ed339b 100644
--- a/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx
+++ b/nextjs-app/__tests__/components/SecondarySchoolRow.test.tsx
@@ -116,3 +116,38 @@ describe('SecondarySchoolRow shares the school page flags', () => {
expect(screen.getByText('Fee-paying')).toBeInTheDocument();
});
});
+
+describe('SecondarySchoolRow LA comparison', () => {
+ it('compares a state school with its LA average', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/\+5\.5 vs LA avg/)).toBeInTheDocument();
+ });
+
+ it('keeps the comparison for a school whose type is unknown', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/\+5\.5 vs LA avg/)).toBeInTheDocument();
+ });
+
+ it("shows an independent school's Attainment 8 without an LA comparison", () => {
+ // DfE's LA average covers state-funded schools; an independent's
+ // Attainment 8 leaves out IGCSEs (audit H2).
+ render(
+ ,
+ );
+ expect(screen.getByText('20.4')).toBeInTheDocument();
+ expect(screen.queryByText(/vs LA avg/)).not.toBeInTheDocument();
+ });
+});
diff --git a/nextjs-app/__tests__/lib/utils.test.ts b/nextjs-app/__tests__/lib/utils.test.ts
index ab03561..ea9f983 100644
--- a/nextjs-app/__tests__/lib/utils.test.ts
+++ b/nextjs-app/__tests__/lib/utils.test.ts
@@ -391,3 +391,20 @@ describe('singleSexLabel', () => {
expect(singleSexLabel(undefined)).toBeNull();
});
});
+
+describe('isIndependentSchool', () => {
+ const { isIndependentSchool } = require('@/lib/utils');
+
+ it('matches both GIAS independent types', () => {
+ expect(isIndependentSchool({ school_type: 'Other independent school' })).toBe(true);
+ expect(isIndependentSchool({ school_type: 'Other independent special school' })).toBe(true);
+ });
+
+ it('does not match state-funded types or a missing type', () => {
+ for (const t of ['Academy converter', 'Community school', 'Free schools', 'Non-maintained special school']) {
+ expect(isIndependentSchool({ school_type: t })).toBe(false);
+ }
+ expect(isIndependentSchool({ school_type: null })).toBe(false);
+ expect(isIndependentSchool({})).toBe(false);
+ });
+});
diff --git a/nextjs-app/components/LeafletMapInner.tsx b/nextjs-app/components/LeafletMapInner.tsx
index e7d1bec..ded0513 100644
--- a/nextjs-app/components/LeafletMapInner.tsx
+++ b/nextjs-app/components/LeafletMapInner.tsx
@@ -9,7 +9,7 @@ import { useEffect, useRef, useState } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import type { School } from '@/lib/types';
-import { schoolUrl, isSpecialSchool, buildOfstedListBadge, listRwmValue } from '@/lib/utils';
+import { schoolUrl, isSpecialSchool, isIndependentSchool, buildOfstedListBadge, listRwmValue } from '@/lib/utils';
interface LeafletMapInnerProps {
schools: School[];
@@ -70,7 +70,7 @@ function metricHtml(school: School, { nationalAvgRwm, laAverages }: CardContext)
const score = school.attainment_8_score;
const laAvg = school.local_authority ? (laAverages?.[school.local_authority] ?? null) : null;
let delta = '';
- if (!special && laAvg != null) {
+ if (!special && !isIndependentSchool(school) && laAvg != null) {
const diff = Math.round((score - laAvg) * 10) / 10;
// Att8 runs 0–90 in 0.1 steps; ±0.5 is meaningful, where RWM needs ±2.
const cls = diff >= 0.5 ? 'sc-up' : diff <= -0.5 ? 'sc-down' : '';
diff --git a/nextjs-app/components/SecondarySchoolRow.tsx b/nextjs-app/components/SecondarySchoolRow.tsx
index 2c5181e..bc3f48e 100644
--- a/nextjs-app/components/SecondarySchoolRow.tsx
+++ b/nextjs-app/components/SecondarySchoolRow.tsx
@@ -11,7 +11,7 @@
'use client';
import type { School } from '@/lib/types';
-import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, isProposedToClose, isSpecialSchool } from '@/lib/utils';
+import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, isProposedToClose, isSpecialSchool, isIndependentSchool } from '@/lib/utils';
import { schoolFlags, schoolTypeLabel } from '@/lib/schoolFacts';
import styles from './SecondarySchoolRow.module.css';
@@ -45,10 +45,12 @@ export function SecondarySchoolRow({
const att8 = school.attainment_8_score;
// The school's own Attainment 8 is a same-school figure — shown whenever it
// exists (special schools included; their type tag on line 2 gives context).
- // Only the vs-LA-average delta, a benchmark comparison, is dropped for
- // special schools / PRUs / AP, whose pupils aren't measured against it fairly.
+ // Only the vs-LA-average delta, a benchmark comparison, is dropped: for
+ // special schools / PRUs / AP, whose pupils aren't measured against it
+ // fairly, and for independent schools, because DfE's LA average covers
+ // state-funded schools and an independent's Attainment 8 leaves out IGCSEs.
const laDelta =
- att8 != null && !isSpecialSchool(school) && laAvgAttainment8 != null
+ att8 != null && !isSpecialSchool(school) && !isIndependentSchool(school) && laAvgAttainment8 != null
? att8 - laAvgAttainment8
: null;
diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts
index 75c873a..9d3440d 100644
--- a/nextjs-app/lib/utils.ts
+++ b/nextjs-app/lib/utils.ts
@@ -758,6 +758,16 @@ export function isSpecialSchool(school: { school_type?: string | null }): boolea
return /\bspecial\b/.test(t) || /pupil referral/.test(t) || /alternative provision/.test(t);
}
+/**
+ * Independent (fee-paying) schools: GIAS types "Other independent school" and
+ * "Other independent special school". DfE's Attainment 8 for them leaves out
+ * IGCSEs, and DfE's LA averages cover state-funded schools only, so callers
+ * drop the "vs LA avg" comparison for them (audit H2).
+ */
+export function isIndependentSchool(school: { school_type?: string | null }): boolean {
+ return /\bindependent\b/i.test(school.school_type ?? '');
+}
+
/**
* Whether GIAS records a religious character. "None", "Does not apply" and
* "Not applicable" are the register's ways of saying it has none; the place