feat(ofsted): fall back to ungraded inspection outcome for school grade
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 18s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 48s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m30s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s

Ungraded (Section 8) inspections don't assign a fresh grade — the export
only gives free text like "School remains Good". Parse that text into a
grade (remains Outstanding -> 1, remains Good -> 2, else null) and use it
as a last-resort fallback when no graded overall effectiveness exists.

Also retain schools that have only an ungraded inspection (no graded date)
by coalescing the inspection date, so ~8.5k previously-dropped schools now
carry a grade.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tudor
2026-07-01 21:44:01 +01:00
co-authored by Claude Opus 4.8
parent eae62a4b42
commit 2332ee6347
8 changed files with 61 additions and 4 deletions
+2 -1
View File
@@ -204,7 +204,8 @@ _MAIN_QUERY = text("""
LEFT JOIN ( LEFT JOIN (
SELECT DISTINCT ON (urn) SELECT DISTINCT ON (urn)
urn, urn,
overall_effectiveness AS ofsted_grade, -- Fall back to the ungraded-inspection grade when no graded grade exists.
COALESCE(overall_effectiveness, ungraded_grade) AS ofsted_grade,
inspection_date AS ofsted_date, inspection_date AS ofsted_date,
framework AS ofsted_framework framework AS ofsted_framework
FROM marts.fact_ofsted_inspection FROM marts.fact_ofsted_inspection
+4
View File
@@ -133,6 +133,10 @@ class FactOfstedInspection(Base):
leadership_management = Column(Integer) leadership_management = Column(Integer)
early_years_provision = Column(Integer) early_years_provision = Column(Integer)
sixth_form_provision = Column(Integer) sixth_form_provision = Column(Integer)
# Ungraded (Section 8) inspection: raw outcome text and the grade parsed from
# it (fallback for schools with no graded overall effectiveness).
ungraded_outcome = Column(String(100))
ungraded_grade = Column(Integer)
rc_safeguarding_met = Column(Boolean) rc_safeguarding_met = Column(Boolean)
rc_inclusion = Column(Integer) rc_inclusion = Column(Integer)
rc_curriculum_teaching = Column(Integer) rc_curriculum_teaching = Column(Integer)
@@ -59,6 +59,15 @@ COLUMN_PRIORITY = {
"Latest OEIF sixth form provision", "Latest OEIF sixth form provision",
"Sixth form provision (where applicable)", "Sixth form provision (where applicable)",
], ],
# Ungraded (Section 8) inspection — no fresh grade is assigned; the outcome
# is free text like "School remains Good". Used as a last-resort fallback
# for the school's grade when no graded overall effectiveness is available.
"ungraded_outcome": [
"Ungraded inspection overall outcome",
],
"ungraded_inspection_date": [
"Date of latest ungraded inspection",
],
} }
@@ -100,6 +109,8 @@ class OfstedInspectionsStream(Stream):
th.Property("effectiveness_of_leadership_and_management", th.StringType), th.Property("effectiveness_of_leadership_and_management", th.StringType),
th.Property("early_years_provision", th.StringType), th.Property("early_years_provision", th.StringType),
th.Property("sixth_form_provision", th.StringType), th.Property("sixth_form_provision", th.StringType),
th.Property("ungraded_outcome", th.StringType),
th.Property("ungraded_inspection_date", th.StringType),
th.Property("report_url", th.StringType), th.Property("report_url", th.StringType),
).to_dict() ).to_dict()
@@ -0,0 +1,20 @@
-- Macro: Parse an Ofsted ungraded (Section 8) inspection outcome into a grade.
--
-- Ungraded inspections do not assign a fresh grade; the outcome is free text
-- describing whether the school kept its previous judgement, e.g.
-- "School remains Good", "School remains Good (Concerns) - S5 Next",
-- "School remains Outstanding", "School remains Outstanding (Concerns) - S5 Next".
-- These are the only Good/Outstanding signal we can recover, so we match on the
-- "remains <grade>" phrase (tolerant of any trailing qualifier).
--
-- Outcomes that don't establish a Good/Outstanding status — "Standards
-- maintained", "Improved significantly", "Some aspects not as strong" — map to
-- null. Grades align with parse_ofsted_grade (1 = Outstanding, 2 = Good).
{% macro parse_ungraded_outcome(column) %}
case
when {{ column }} ilike '%remains Outstanding%' then 1
when {{ column }} ilike '%remains Good%' then 2
else null
end
{% endmacro %}
@@ -23,6 +23,8 @@ select
leadership_management, leadership_management,
early_years_provision, early_years_provision,
sixth_form_provision, sixth_form_provision,
ungraded_outcome,
ungraded_grade,
rc_safeguarding_met, rc_safeguarding_met,
rc_inclusion, rc_inclusion,
rc_curriculum_teaching, rc_curriculum_teaching,
@@ -60,7 +60,9 @@ select
-- Latest Ofsted (populated after monthly Ofsted pipeline runs) -- Latest Ofsted (populated after monthly Ofsted pipeline runs)
{% if ofsted_relation is not none %} {% if ofsted_relation is not none %}
o.overall_effectiveness as ofsted_grade, -- Prefer the graded overall effectiveness; fall back to the grade parsed
-- from the latest ungraded (Section 8) outcome when no graded grade exists.
coalesce(o.overall_effectiveness, o.ungraded_grade) as ofsted_grade,
o.inspection_date as ofsted_date, o.inspection_date as ofsted_date,
o.framework as ofsted_framework o.framework as ofsted_framework
{% else %} {% else %}
@@ -12,6 +12,8 @@ select
leadership_management, leadership_management,
early_years_provision, early_years_provision,
sixth_form_provision, sixth_form_provision,
ungraded_outcome,
ungraded_grade,
rc_safeguarding_met, rc_safeguarding_met,
rc_inclusion, rc_inclusion,
rc_curriculum_teaching, rc_curriculum_teaching,
@@ -8,7 +8,13 @@ with source as (
renamed as ( renamed as (
select select
cast(urn as integer) as urn, cast(urn as integer) as urn,
to_date(nullif(trim(inspection_date), 'NULL'), 'DD/MM/YYYY') as inspection_date, -- Inspection event date: the graded inspection when present, otherwise the
-- ungraded (Section 8) inspection so schools with only an ungraded
-- inspection are still retained.
coalesce(
to_date(nullif(trim(inspection_date), 'NULL'), 'DD/MM/YYYY'),
to_date(nullif(trim(ungraded_inspection_date), 'NULL'), 'DD/MM/YYYY')
) as inspection_date,
inspection_type, inspection_type,
event_type_grouping as framework, event_type_grouping as framework,
@@ -21,6 +27,12 @@ renamed as (
{{ safe_numeric('early_years_provision') }}::integer as early_years_provision, {{ safe_numeric('early_years_provision') }}::integer as early_years_provision,
{{ safe_numeric('sixth_form_provision') }}::integer as sixth_form_provision, {{ safe_numeric('sixth_form_provision') }}::integer as sixth_form_provision,
-- Ungraded (Section 8) inspection outcome — free text, plus a grade
-- parsed from it (1/2/null) used as a last-resort fallback for schools
-- with no graded overall effectiveness.
nullif(trim(ungraded_outcome), 'NULL') as ungraded_outcome,
{{ parse_ungraded_outcome('ungraded_outcome') }}::integer as ungraded_grade,
-- Report Card fields (post-Nov 2025 framework) -- Report Card fields (post-Nov 2025 framework)
-- TODO: add rc_* columns to tap-uk-ofsted schema once CSV column names are confirmed -- TODO: add rc_* columns to tap-uk-ofsted schema once CSV column names are confirmed
null::text as rc_safeguarding_met, null::text as rc_safeguarding_met,
@@ -36,7 +48,10 @@ renamed as (
report_url report_url
from source from source
where urn is not null where urn is not null
and nullif(trim(inspection_date), 'NULL') is not null and (
nullif(trim(inspection_date), 'NULL') is not null
or nullif(trim(ungraded_inspection_date), 'NULL') is not null
)
) )
select * from renamed select * from renamed