initial commit

This commit is contained in:
Tudor Sitaru
2026-01-06 13:52:00 +00:00
commit c65eb1a00f
37 changed files with 402537 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

44
.dockerignore Normal file
View File

@@ -0,0 +1,44 @@
# Virtual environment
venv/
.venv/
env/
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# IDE
.idea/
.vscode/
*.swp
*.swo
.cursor/
# Git
.git/
.gitignore
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Scripts (not needed in container)
scripts/
# Documentation
README.md
*.md
# OS files
.DS_Store
Thumbs.db
# Testing
.pytest_cache/
.coverage
htmlcov/

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
venv

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
# syntax=docker/dockerfile:1
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install curl for healthcheck
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install dependencies first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY backend/ ./backend/
COPY frontend/ ./frontend/
COPY data/ ./data/
# Expose the application port
EXPOSE 80
# Run the application
CMD ["uvicorn", "backend.app:app", "--host", "0.0.0.0", "--port", "80"]

189
README.md Normal file
View File

@@ -0,0 +1,189 @@
# Primary School Compass 🧒📚
A modern web application for comparing **primary school (KS2)** performance data in **Wandsworth and Merton** over the last 5 years. Built with FastAPI and vanilla JavaScript with Chart.js visualizations.
![Python](https://img.shields.io/badge/Python-3.9+-blue)
![FastAPI](https://img.shields.io/badge/FastAPI-0.109-green)
![License](https://img.shields.io/badge/License-MIT-yellow)
## Features
- 📊 **Interactive Charts** - Visualize KS2 performance trends over time
- 🔍 **Smart Search** - Find primary schools by name in Wandsworth & Merton
- ⚖️ **Side-by-Side Comparison** - Compare up to 5 schools simultaneously
- 🏆 **Rankings** - View top-performing primary schools by various KS2 metrics
- 📱 **Responsive Design** - Works beautifully on desktop and mobile
## Key Metrics (KS2)
The application tracks these Key Stage 2 performance indicators:
| Metric | Description |
|--------|-------------|
| **Reading Progress** | Progress in reading from KS1 to KS2 |
| **Writing Progress** | Progress in writing from KS1 to KS2 |
| **Maths Progress** | Progress in maths from KS1 to KS2 |
| **Reading Expected %** | Percentage meeting expected standard in reading |
| **Writing Expected %** | Percentage meeting expected standard in writing |
| **Maths Expected %** | Percentage meeting expected standard in maths |
| **RWM Combined %** | Percentage meeting expected standard in all three subjects |
## Quick Start
### 1. Clone and Setup
```bash
cd school_results
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
```
### 2. Run the Application
```bash
# Start the server
python -m uvicorn backend.app:app --reload --port 8000
```
Then open http://localhost:8000 in your browser.
The app will run with **sample data** by default, showing **110 primary schools** (66 in Wandsworth, 44 in Merton) with 5 years of KS2 performance data.
### 3. (Optional) Use Real Data
To use real UK school performance data:
1. Visit [Compare School Performance - Download Data](https://www.compare-school-performance.service.gov.uk/download-data)
2. Download **Key Stage 2** data for the years you want (2019-2024)
- Select "Key Stage 2" as the data type
3. Place the CSV files in the `data/` folder
4. Restart the server - it will automatically load and filter to Wandsworth & Merton schools
**Note:** The app only displays schools in Wandsworth and Merton. Data from other areas will be filtered out.
See the helper script for more details:
```bash
python scripts/download_data.py
```
## Project Structure
```
school_results/
├── backend/
│ └── app.py # FastAPI application with all API endpoints
├── frontend/
│ ├── index.html # Main HTML page
│ ├── styles.css # Styling (warm, editorial design)
│ └── app.js # Frontend JavaScript
├── data/
│ └── .gitkeep # Place CSV data files here
├── scripts/
│ └── download_data.py # Helper for downloading/processing data
├── requirements.txt # Python dependencies
└── README.md
```
## API Endpoints
| Endpoint | Description |
|----------|-------------|
| `GET /api/schools` | List schools with optional search/filter |
| `GET /api/schools/{urn}` | Get detailed data for a specific school |
| `GET /api/compare?urns=...` | Compare multiple schools |
| `GET /api/rankings` | Get school rankings by metric |
| `GET /api/filters` | Get available filter options |
| `GET /api/metrics` | Get available performance metrics |
### Example API Usage
```bash
# Search for schools
curl "http://localhost:8000/api/schools?search=academy"
# Get school details
curl "http://localhost:8000/api/schools/100001"
# Compare schools
curl "http://localhost:8000/api/compare?urns=100001,100002,100003"
# Get rankings
curl "http://localhost:8000/api/rankings?metric=rwm_expected_pct&year=2024"
```
## Data Format
If using your own CSV data, ensure it includes these columns (or similar):
| Column | Type | Description |
|--------|------|-------------|
| URN | Integer | Unique Reference Number |
| SCHNAME | String | School name |
| LA | String | Local Authority (must be Wandsworth or Merton) |
| READPROG | Float | Reading progress score |
| WRITPROG | Float | Writing progress score |
| MATPROG | Float | Maths progress score |
| PTRWM_EXP | Float | % meeting expected standard in RWM |
| PTREAD_EXP | Float | % meeting expected standard in reading |
| PTWRIT_EXP | Float | % meeting expected standard in writing |
| PTMAT_EXP | Float | % meeting expected standard in maths |
The application normalizes column names automatically and filters to only show Wandsworth and Merton schools.
## Technology Stack
- **Backend**: FastAPI (Python) - High-performance async API framework
- **Frontend**: Vanilla JavaScript with Chart.js
- **Styling**: Custom CSS with CSS variables for theming
- **Data**: Pandas for CSV processing
## Design Philosophy
The UI features a warm, editorial design inspired by quality publications:
- **Typography**: DM Sans for body text, Playfair Display for headings
- **Color Palette**: Warm cream background with coral and teal accents
- **Interactions**: Smooth animations and hover effects
- **Charts**: Clean, readable data visualizations
## Development
```bash
# Run with auto-reload
python -m uvicorn backend.app:app --reload --port 8000
# Or run directly
python backend/app.py
```
## Coverage
This application is specifically designed for:
- **School Phase**: Primary schools only (Key Stage 2)
- **Geographic Area**: Wandsworth and Merton (London boroughs)
- **Time Period**: Last 5 years of data (2020-2024)
Note: 2021 data shows as unavailable because SATs were cancelled due to COVID-19.
## Data Source
Data is sourced from the UK Government's [Compare School Performance](https://www.compare-school-performance.service.gov.uk/) service, which provides official school performance data for England.
**Important**: When using real data, please comply with the [terms of use](https://www.compare-school-performance.service.gov.uk/download-data) and data protection regulations.
## License
MIT License - feel free to use this project for educational purposes.
---
Built with ❤️ for Wandsworth & Merton families

549
backend/app.py Normal file
View File

@@ -0,0 +1,549 @@
"""
Primary School Performance Comparison API
Serves primary school (KS2) performance data for Wandsworth and Merton.
Uses real data from UK Government Compare School Performance downloads.
"""
from fastapi import FastAPI, HTTPException, Query
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
import pandas as pd
import numpy as np
from pathlib import Path
from typing import Optional
import os
import re
# Local Authority codes for Wandsworth and Merton
LA_CODES = {
212: "Wandsworth",
315: "Merton"
}
ALLOWED_LA_CODES = list(LA_CODES.keys())
app = FastAPI(
title="Primary School Performance API - Wandsworth & Merton",
description="API for comparing primary school (KS2) performance data in Wandsworth and Merton",
version="1.0.0"
)
# CORS middleware for development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Data directory
DATA_DIR = Path(__file__).parent.parent / "data"
FRONTEND_DIR = Path(__file__).parent.parent / "frontend"
# Cache for loaded data - cleared on reload (updated for 2016-2017 data)
_data_cache: Optional[pd.DataFrame] = None
def convert_to_native(value):
"""Convert numpy types to native Python types for JSON serialization."""
if pd.isna(value):
return None
if isinstance(value, (np.integer,)):
return int(value)
if isinstance(value, (np.floating,)):
if np.isnan(value) or np.isinf(value):
return None
return float(value)
if isinstance(value, np.ndarray):
return value.tolist()
if value == "SUPP" or value == "NE" or value == "NA" or value == "NP":
return None
return value
def clean_for_json(df: pd.DataFrame) -> list:
"""Convert DataFrame to list of dicts, replacing NaN/inf with None for JSON serialization."""
records = df.to_dict(orient="records")
cleaned = []
for record in records:
clean_record = {}
for key, value in record.items():
clean_record[key] = convert_to_native(value)
cleaned.append(clean_record)
return cleaned
def parse_numeric(value):
"""Parse a value to numeric, handling SUPP, NE, NA, %, etc."""
if pd.isna(value):
return None
if isinstance(value, (int, float)):
if np.isnan(value) or np.isinf(value):
return None
return value
if isinstance(value, str):
value = value.strip()
if value in ["SUPP", "NE", "NA", "NP", "NEW", "LOW", ""]:
return None
# Remove % sign if present
if value.endswith('%'):
value = value[:-1]
try:
return float(value)
except ValueError:
return None
return None
def extract_year_from_folder(folder_name: str) -> Optional[int]:
"""Extract the end year from folder name like '2023-2024' -> 2024."""
match = re.search(r'(\d{4})-(\d{4})', folder_name)
if match:
return int(match.group(2))
return None
def load_school_data() -> pd.DataFrame:
"""Load and combine all school data from CSV files in year folders."""
global _data_cache
if _data_cache is not None:
return _data_cache
all_data = []
# Look for year folders in data directory
if DATA_DIR.exists():
for year_folder in DATA_DIR.iterdir():
if year_folder.is_dir() and re.match(r'\d{4}-\d{4}', year_folder.name):
year = extract_year_from_folder(year_folder.name)
if year is None:
continue
# Look for KS2 data file
ks2_file = year_folder / "england_ks2final.csv"
if ks2_file.exists():
try:
print(f"Loading data from {ks2_file}")
df = pd.read_csv(ks2_file, low_memory=False)
# Filter to Wandsworth (212) and Merton (315)
# Handle both string and integer columns
if df['LEA'].dtype == 'object':
df['LEA'] = pd.to_numeric(df['LEA'], errors='coerce')
if df['URN'].dtype == 'object':
df['URN'] = pd.to_numeric(df['URN'], errors='coerce')
df = df[df['LEA'].isin(ALLOWED_LA_CODES)]
# Filter to schools only (RECTYPE == 1 means school level data)
if 'RECTYPE' in df.columns:
df = df[df['RECTYPE'] == 1]
# Add year and local authority name
df['year'] = year
df['local_authority'] = df['LEA'].map(LA_CODES)
# Standardize column names for our API
df = df.rename(columns={
'URN': 'urn',
'SCHNAME': 'school_name',
'ADDRESS1': 'address1',
'ADDRESS2': 'address2',
'TOWN': 'town',
'PCODE': 'postcode',
'NFTYPE': 'school_type_code',
'RELDENOM': 'religious_denomination',
'AGERANGE': 'age_range',
'TOTPUPS': 'total_pupils',
'TELIG': 'eligible_pupils',
# Core KS2 metrics
'PTRWM_EXP': 'rwm_expected_pct',
'PTRWM_HIGH': 'rwm_high_pct',
'READPROG': 'reading_progress',
'WRITPROG': 'writing_progress',
'MATPROG': 'maths_progress',
'PTREAD_EXP': 'reading_expected_pct',
'PTWRITTA_EXP': 'writing_expected_pct',
'PTMAT_EXP': 'maths_expected_pct',
'READ_AVERAGE': 'reading_avg_score',
'MAT_AVERAGE': 'maths_avg_score',
'PTREAD_HIGH': 'reading_high_pct',
'PTWRITTA_HIGH': 'writing_high_pct',
'PTMAT_HIGH': 'maths_high_pct',
# GPS (Grammar, Punctuation & Spelling)
'PTGPS_EXP': 'gps_expected_pct',
'PTGPS_HIGH': 'gps_high_pct',
'GPS_AVERAGE': 'gps_avg_score',
# Science
'PTSCITA_EXP': 'science_expected_pct',
# School context
'PTFSM6CLA1A': 'disadvantaged_pct',
'PTEALGRP2': 'eal_pct',
'PSENELK': 'sen_support_pct',
'PSENELE': 'sen_ehcp_pct',
'PTMOBN': 'stability_pct',
# Gender breakdown
'PTRWM_EXP_B': 'rwm_expected_boys_pct',
'PTRWM_EXP_G': 'rwm_expected_girls_pct',
'PTRWM_HIGH_B': 'rwm_high_boys_pct',
'PTRWM_HIGH_G': 'rwm_high_girls_pct',
# Disadvantaged performance
'PTRWM_EXP_FSM6CLA1A': 'rwm_expected_disadvantaged_pct',
'PTRWM_EXP_NotFSM6CLA1A': 'rwm_expected_non_disadvantaged_pct',
'DIFFN_RWM_EXP': 'disadvantaged_gap',
# 3-year averages
'PTRWM_EXP_3YR': 'rwm_expected_3yr_pct',
'READ_AVERAGE_3YR': 'reading_avg_3yr',
'MAT_AVERAGE_3YR': 'maths_avg_3yr',
})
# Create address field
def make_address(row):
parts = []
if pd.notna(row.get('address1')) and row.get('address1'):
parts.append(str(row['address1']))
if pd.notna(row.get('town')) and row.get('town'):
parts.append(str(row['town']))
if pd.notna(row.get('postcode')) and row.get('postcode'):
parts.append(str(row['postcode']))
return ', '.join(parts) if parts else ''
df['address'] = df.apply(make_address, axis=1)
# Map school type codes to names
school_type_map = {
'AC': 'Academy', 'ACC': 'Academy Converter', 'ACS': 'Academy Sponsor Led',
'CY': 'Community School', 'VA': 'Voluntary Aided', 'VC': 'Voluntary Controlled',
'FD': 'Foundation', 'F': 'Foundation', 'FS': 'Free School',
}
df['school_type'] = df['school_type_code'].map(school_type_map).fillna('Other')
# Parse numeric columns
numeric_cols = [
# Core metrics
'rwm_expected_pct', 'rwm_high_pct', 'reading_progress',
'writing_progress', 'maths_progress', 'reading_expected_pct',
'writing_expected_pct', 'maths_expected_pct', 'reading_avg_score',
'maths_avg_score', 'reading_high_pct', 'writing_high_pct', 'maths_high_pct',
# GPS & Science
'gps_expected_pct', 'gps_high_pct', 'gps_avg_score', 'science_expected_pct',
# School context
'total_pupils', 'eligible_pupils', 'disadvantaged_pct', 'eal_pct',
'sen_support_pct', 'sen_ehcp_pct', 'stability_pct',
# Gender breakdown
'rwm_expected_boys_pct', 'rwm_expected_girls_pct',
'rwm_high_boys_pct', 'rwm_high_girls_pct',
# Disadvantaged performance
'rwm_expected_disadvantaged_pct', 'rwm_expected_non_disadvantaged_pct', 'disadvantaged_gap',
# 3-year averages
'rwm_expected_3yr_pct', 'reading_avg_3yr', 'maths_avg_3yr',
]
for col in numeric_cols:
if col in df.columns:
df[col] = df[col].apply(parse_numeric)
all_data.append(df)
print(f" Loaded {len(df)} schools for year {year}")
except Exception as e:
print(f"Error loading {ks2_file}: {e}")
if all_data:
_data_cache = pd.concat(all_data, ignore_index=True)
print(f"\nTotal records loaded: {len(_data_cache)}")
print(f"Unique schools: {_data_cache['urn'].nunique()}")
print(f"Years: {sorted(_data_cache['year'].unique())}")
else:
print("No data files found. Creating empty DataFrame.")
_data_cache = pd.DataFrame()
return _data_cache
@app.get("/")
async def root():
"""Serve the frontend."""
return FileResponse(FRONTEND_DIR / "index.html")
@app.get("/api/schools")
async def get_schools(
search: Optional[str] = Query(None, description="Search by school name"),
local_authority: Optional[str] = Query(None, description="Filter by local authority (Wandsworth or Merton)"),
school_type: Optional[str] = Query(None, description="Filter by school type"),
):
"""Get list of unique primary schools in Wandsworth and Merton."""
df = load_school_data()
if df.empty:
return {"schools": []}
# Get unique schools (latest year data for each)
latest_year = df.groupby('urn')['year'].max().reset_index()
df_latest = df.merge(latest_year, on=['urn', 'year'])
school_cols = ["urn", "school_name", "local_authority", "school_type", "address", "town", "postcode"]
available_cols = [c for c in school_cols if c in df_latest.columns]
schools_df = df_latest[available_cols].drop_duplicates(subset=['urn'])
# Apply filters
if search:
search_lower = search.lower()
mask = schools_df["school_name"].str.lower().str.contains(search_lower, na=False)
if "address" in schools_df.columns:
mask = mask | schools_df["address"].str.lower().str.contains(search_lower, na=False)
schools_df = schools_df[mask]
if local_authority:
schools_df = schools_df[schools_df["local_authority"].str.lower() == local_authority.lower()]
if school_type:
schools_df = schools_df[schools_df["school_type"].str.lower() == school_type.lower()]
return {"schools": clean_for_json(schools_df)}
@app.get("/api/schools/{urn}")
async def get_school_details(urn: int):
"""Get detailed KS2 data for a specific primary school across all years."""
df = load_school_data()
if df.empty:
raise HTTPException(status_code=404, detail="No data available")
school_data = df[df["urn"] == urn]
if school_data.empty:
raise HTTPException(status_code=404, detail="School not found")
# Sort by year
school_data = school_data.sort_values("year")
# Get latest info for the school
latest = school_data.iloc[-1]
return {
"school_info": {
"urn": urn,
"school_name": latest.get("school_name", ""),
"local_authority": latest.get("local_authority", ""),
"school_type": latest.get("school_type", ""),
"address": latest.get("address", ""),
"phase": "Primary",
},
"yearly_data": clean_for_json(school_data)
}
@app.get("/api/compare")
async def compare_schools(urns: str = Query(..., description="Comma-separated URNs")):
"""Compare multiple primary schools side by side."""
df = load_school_data()
if df.empty:
raise HTTPException(status_code=404, detail="No data available")
try:
urn_list = [int(u.strip()) for u in urns.split(",")]
except ValueError:
raise HTTPException(status_code=400, detail="Invalid URN format")
comparison_data = df[df["urn"].isin(urn_list)]
if comparison_data.empty:
raise HTTPException(status_code=404, detail="No schools found")
result = {}
for urn in urn_list:
school_data = comparison_data[comparison_data["urn"] == urn].sort_values("year")
if not school_data.empty:
latest = school_data.iloc[-1]
result[str(urn)] = {
"school_info": {
"urn": urn,
"school_name": latest.get("school_name", ""),
"local_authority": latest.get("local_authority", ""),
"address": latest.get("address", ""),
},
"yearly_data": clean_for_json(school_data)
}
return {"comparison": result}
@app.get("/api/filters")
async def get_filter_options():
"""Get available filter options (local authorities, school types, years)."""
df = load_school_data()
if df.empty:
return {
"local_authorities": ["Wandsworth", "Merton"],
"school_types": [],
"years": [],
}
return {
"local_authorities": sorted(df["local_authority"].dropna().unique().tolist()),
"school_types": sorted(df["school_type"].dropna().unique().tolist()),
"years": sorted(df["year"].dropna().unique().tolist()),
}
@app.get("/api/metrics")
async def get_available_metrics():
"""Get list of available KS2 performance metrics for primary schools."""
df = load_school_data()
# Define KS2 metric metadata organized by category
metric_info = {
# Expected Standard
"rwm_expected_pct": {"name": "RWM Combined %", "description": "% meeting expected standard in reading, writing and maths", "type": "percentage", "category": "expected"},
"reading_expected_pct": {"name": "Reading Expected %", "description": "% meeting expected standard in reading", "type": "percentage", "category": "expected"},
"writing_expected_pct": {"name": "Writing Expected %", "description": "% meeting expected standard in writing", "type": "percentage", "category": "expected"},
"maths_expected_pct": {"name": "Maths Expected %", "description": "% meeting expected standard in maths", "type": "percentage", "category": "expected"},
"gps_expected_pct": {"name": "GPS Expected %", "description": "% meeting expected standard in grammar, punctuation & spelling", "type": "percentage", "category": "expected"},
"science_expected_pct": {"name": "Science Expected %", "description": "% meeting expected standard in science", "type": "percentage", "category": "expected"},
# Higher Standard
"rwm_high_pct": {"name": "RWM Combined Higher %", "description": "% achieving higher standard in RWM combined", "type": "percentage", "category": "higher"},
"reading_high_pct": {"name": "Reading Higher %", "description": "% achieving higher standard in reading", "type": "percentage", "category": "higher"},
"writing_high_pct": {"name": "Writing Higher %", "description": "% achieving greater depth in writing", "type": "percentage", "category": "higher"},
"maths_high_pct": {"name": "Maths Higher %", "description": "% achieving higher standard in maths", "type": "percentage", "category": "higher"},
"gps_high_pct": {"name": "GPS Higher %", "description": "% achieving higher standard in GPS", "type": "percentage", "category": "higher"},
# Progress Scores
"reading_progress": {"name": "Reading Progress", "description": "Progress in reading from KS1 to KS2", "type": "score", "category": "progress"},
"writing_progress": {"name": "Writing Progress", "description": "Progress in writing from KS1 to KS2", "type": "score", "category": "progress"},
"maths_progress": {"name": "Maths Progress", "description": "Progress in maths from KS1 to KS2", "type": "score", "category": "progress"},
# Average Scores
"reading_avg_score": {"name": "Reading Avg Score", "description": "Average scaled score in reading", "type": "score", "category": "average"},
"maths_avg_score": {"name": "Maths Avg Score", "description": "Average scaled score in maths", "type": "score", "category": "average"},
"gps_avg_score": {"name": "GPS Avg Score", "description": "Average scaled score in GPS", "type": "score", "category": "average"},
# Gender Performance
"rwm_expected_boys_pct": {"name": "RWM Expected % (Boys)", "description": "% of boys meeting expected standard", "type": "percentage", "category": "gender"},
"rwm_expected_girls_pct": {"name": "RWM Expected % (Girls)", "description": "% of girls meeting expected standard", "type": "percentage", "category": "gender"},
"rwm_high_boys_pct": {"name": "RWM Higher % (Boys)", "description": "% of boys at higher standard", "type": "percentage", "category": "gender"},
"rwm_high_girls_pct": {"name": "RWM Higher % (Girls)", "description": "% of girls at higher standard", "type": "percentage", "category": "gender"},
# Disadvantaged Performance
"rwm_expected_disadvantaged_pct": {"name": "RWM Expected % (Disadvantaged)", "description": "% of disadvantaged pupils meeting expected", "type": "percentage", "category": "equity"},
"rwm_expected_non_disadvantaged_pct": {"name": "RWM Expected % (Non-Disadvantaged)", "description": "% of non-disadvantaged pupils meeting expected", "type": "percentage", "category": "equity"},
"disadvantaged_gap": {"name": "Disadvantaged Gap", "description": "Gap between disadvantaged and national non-disadvantaged", "type": "score", "category": "equity"},
# School Context
"disadvantaged_pct": {"name": "% Disadvantaged Pupils", "description": "% of pupils eligible for free school meals or looked after", "type": "percentage", "category": "context"},
"eal_pct": {"name": "% EAL Pupils", "description": "% of pupils with English as additional language", "type": "percentage", "category": "context"},
"sen_support_pct": {"name": "% SEN Support", "description": "% of pupils with SEN support", "type": "percentage", "category": "context"},
"stability_pct": {"name": "% Pupil Stability", "description": "% of non-mobile pupils (stayed at school)", "type": "percentage", "category": "context"},
# 3-Year Averages
"rwm_expected_3yr_pct": {"name": "RWM Expected % (3-Year Avg)", "description": "3-year average % meeting expected", "type": "percentage", "category": "trends"},
"reading_avg_3yr": {"name": "Reading Score (3-Year Avg)", "description": "3-year average reading score", "type": "score", "category": "trends"},
"maths_avg_3yr": {"name": "Maths Score (3-Year Avg)", "description": "3-year average maths score", "type": "score", "category": "trends"},
}
available = []
for col, info in metric_info.items():
if df.empty or col in df.columns:
available.append({"key": col, **info})
return {"metrics": available}
@app.get("/api/rankings")
async def get_rankings(
metric: str = Query("rwm_expected_pct", description="KS2 metric to rank by"),
year: Optional[int] = Query(None, description="Specific year (defaults to most recent)"),
limit: int = Query(20, description="Number of schools to return"),
):
"""Get primary school rankings by a specific KS2 metric."""
df = load_school_data()
if df.empty:
return {"metric": metric, "year": None, "rankings": []}
if metric not in df.columns:
raise HTTPException(status_code=400, detail=f"Metric '{metric}' not available")
# Filter by year
if year:
df = df[df["year"] == year]
else:
# Use most recent year
max_year = df["year"].max()
df = df[df["year"] == max_year]
# Sort and rank (exclude rows with no data for this metric)
df = df.dropna(subset=[metric])
# For progress scores, higher is better. For percentages, higher is also better.
df = df.sort_values(metric, ascending=False).head(limit)
# Return only relevant fields for rankings
ranking_cols = [
"urn", "school_name", "local_authority", "school_type", "address", "year", "total_pupils",
# Core expected
"rwm_expected_pct", "reading_expected_pct", "writing_expected_pct", "maths_expected_pct",
"gps_expected_pct", "science_expected_pct",
# Core higher
"rwm_high_pct", "reading_high_pct", "writing_high_pct", "maths_high_pct", "gps_high_pct",
# Progress & averages
"reading_progress", "writing_progress", "maths_progress",
"reading_avg_score", "maths_avg_score", "gps_avg_score",
# Gender
"rwm_expected_boys_pct", "rwm_expected_girls_pct", "rwm_high_boys_pct", "rwm_high_girls_pct",
# Equity
"rwm_expected_disadvantaged_pct", "rwm_expected_non_disadvantaged_pct", "disadvantaged_gap",
# Context
"disadvantaged_pct", "eal_pct", "sen_support_pct", "stability_pct",
# 3-year
"rwm_expected_3yr_pct", "reading_avg_3yr", "maths_avg_3yr",
]
available_cols = [c for c in ranking_cols if c in df.columns]
df = df[available_cols]
return {
"metric": metric,
"year": int(df["year"].iloc[0]) if not df.empty else None,
"rankings": clean_for_json(df)
}
@app.get("/api/data-info")
async def get_data_info():
"""Get information about loaded data."""
df = load_school_data()
if df.empty:
return {
"status": "no_data",
"message": "No data files found in data folder. Please download KS2 data from the government website.",
"data_folder": str(DATA_DIR),
}
years = [int(y) for y in sorted(df["year"].unique())]
schools_per_year = {str(int(k)): int(v) for k, v in df.groupby("year")["urn"].nunique().to_dict().items()}
la_counts = {str(k): int(v) for k, v in df["local_authority"].value_counts().to_dict().items()}
return {
"status": "loaded",
"total_records": int(len(df)),
"unique_schools": int(df["urn"].nunique()),
"years_available": years,
"schools_per_year": schools_per_year,
"local_authorities": la_counts,
}
# Mount static files
@app.on_event("startup")
async def startup():
"""Setup static file serving and load data on startup."""
if FRONTEND_DIR.exists():
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
# Pre-load data
load_school_data()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

BIN
data/.DS_Store vendored Normal file

Binary file not shown.

3
data/.gitkeep Normal file
View File

@@ -0,0 +1,3 @@
# Place your CSV data files here
# Download from: https://www.compare-school-performance.service.gov.uk/download-data

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

24
data/meta/census_meta.csv Normal file
View File

@@ -0,0 +1,24 @@
Field Number,Field Reference,Field Name,Values,Data Format,LA level field?,National level field?
1,URN,School Unique Reference Number,999999,I6,No,No
2,LA,LA number,999,I3,Yes,No
3,ESTAB,ESTAB number,9999,I4,No,No
4,SCHOOLTYPE,Type of school,String,,No,No
5,NOR,Total number of pupils on roll,9999 or NA,,Yes,Yes
6,NORG,Number of girls on roll,9999 or NA,,Yes,Yes
7,NORB,Number of boys on roll,9999 or NA,,Yes,Yes
8,PNORG,Percentage of girls on roll,99.9 or NA,,Yes,Yes
9,PNORB,Percentage of boys on roll,99.9 or NA,,Yes,Yes
10,TSENELSE,Number of eligible pupils with an EHC plan,9999 or NA,A4,Yes,Yes
11,PSENELSE,Percentage of eligible pupils with an EHC plan,99.9 or NA,A4,Yes,Yes
12,TSENELK,Number of eligible pupils with SEN support,9999 or NA,A4,Yes,Yes
13,PSENELK,Percentage of eligible pupils with SEN support,99.9 or NA,A4,Yes,Yes
14,NUMEAL,No. pupils where English not first language,9999 or NA,A4,Yes,Yes
15,NUMENGFL,No. pupils with English first language,9999 or NA,A4,Yes,Yes
16,NUMUNCFL,No. pupils where first language is unclassified,9999 or NA,A4,Yes,Yes
17,PNUMEAL,% pupils where English not first language,99.9 or NA,A4,Yes,Yes
18,PNUMENGFL,% pupils with English first language,99.9 or NA,A4,Yes,Yes
19,PNUMUNCFL,% pupils where first language is unclassified,99.9 or NA,A4,Yes,Yes
20,NUMFSM,No. pupils eligible for free school meals,9999 or NA,A4,Yes,Yes
21,NUMFSMEVER,Number of pupils eligible for FSM at any time during the past 6 years,9999 or NA,A6,Yes,Yes
22,NORFSMEVER,Total pupils for FSMEver,9999 or NA,,Yes,Yes
23,PNUMFSMEVER,Percentage of pupils eligible for FSM at any time during the past 6 years,99.9 or NA,A4,Yes,Yes
1 Field Number Field Reference Field Name Values Data Format LA level field? National level field?
2 1 URN School Unique Reference Number 999999 I6 No No
3 2 LA LA number 999 I3 Yes No
4 3 ESTAB ESTAB number 9999 I4 No No
5 4 SCHOOLTYPE Type of school String No No
6 5 NOR Total number of pupils on roll 9999 or NA Yes Yes
7 6 NORG Number of girls on roll 9999 or NA Yes Yes
8 7 NORB Number of boys on roll 9999 or NA Yes Yes
9 8 PNORG Percentage of girls on roll 99.9 or NA Yes Yes
10 9 PNORB Percentage of boys on roll 99.9 or NA Yes Yes
11 10 TSENELSE Number of eligible pupils with an EHC plan 9999 or NA A4 Yes Yes
12 11 PSENELSE Percentage of eligible pupils with an EHC plan 99.9 or NA A4 Yes Yes
13 12 TSENELK Number of eligible pupils with SEN support 9999 or NA A4 Yes Yes
14 13 PSENELK Percentage of eligible pupils with SEN support 99.9 or NA A4 Yes Yes
15 14 NUMEAL No. pupils where English not first language 9999 or NA A4 Yes Yes
16 15 NUMENGFL No. pupils with English first language 9999 or NA A4 Yes Yes
17 16 NUMUNCFL No. pupils where first language is unclassified 9999 or NA A4 Yes Yes
18 17 PNUMEAL % pupils where English not first language 99.9 or NA A4 Yes Yes
19 18 PNUMENGFL % pupils with English first language 99.9 or NA A4 Yes Yes
20 19 PNUMUNCFL % pupils where first language is unclassified 99.9 or NA A4 Yes Yes
21 20 NUMFSM No. pupils eligible for free school meals 9999 or NA A4 Yes Yes
22 21 NUMFSMEVER Number of pupils eligible for FSM at any time during the past 6 years 9999 or NA A6 Yes Yes
23 22 NORFSMEVER Total pupils for FSMEver 9999 or NA Yes Yes
24 23 PNUMFSMEVER Percentage of pupils eligible for FSM at any time during the past 6 years 99.9 or NA A4 Yes Yes

312
data/meta/ks2_meta.csv Normal file
View File

@@ -0,0 +1,312 @@
Column,Field Name,Label/Description
1,RECTYPE,Record type
2,AlphaIND,Alphabetic index
3,LEA,Local authority number
4,ESTAB,Establishment number
5,URN,School unique reference number
6,SCHNAME,School/Local authority name
7,ADDRESS1,School address (1)
8,ADDRESS2,School address (2)
9,ADDRESS3,School address (3)
10,TOWN,School town
11,PCODE,School postcode
12,TELNUM,School telephone number
13,PCON_CODE,School parliamentary constituency code
14,PCON_NAME,School parliamentary constituency name
15,URN_AC,Converter academy: URN
16,SCHNAME_AC,Converter academy: name
17,OPEN_AC,Converter academy: open date
18,NFTYPE,School type
19,ICLOSE,Closed Flag
20,RELDENOM,Religious denomination
21,AGERANGE,Age range
22,TAB15,School published in secondary school (key stage 4) performance tables
23,TAB1618,School published in school and college (key stage 5) performance tables
24,TOTPUPS,Total number of pupils (including part-time pupils)
25,TPUPYEAR,Number of pupils aged 11
26,TELIG,Published eligible pupil number
27,BELIG,Eligible boys on school roll at time of tests
28,GELIG,Eligible girls on school roll at time of tests
29,PBELIG,Percentage of eligible boys on school roll at time of tests
30,PGELIG,Percentage of eligible girls on school roll at time of tests
31,TKS1AVERAGE,Cohort level key stage 1 average points score [not populated in 2025]
32,TKS1GROUP_L,Number of pupils in cohort with low KS1 attainment [not populated in 2025]
33,PTKS1GROUP_L,Percentage of pupils in cohort with low KS1 attainment [not populated in 2025]
34,TKS1GROUP_M,Number of pupils in cohort with medium KS1 attainment [not populated in 2025]
35,PTKS1GROUP_M,Percentage of pupils in cohort with medium KS1 attainment [not populated in 2025]
36,TKS1GROUP_H,Number of pupils in cohort high KS1 attainment [not populated in 2025]
37,PTKS1GROUP_H,Percentage of pupils in cohort with high KS1 attainment [not populated in 2025]
38,TKS1GROUP_NA,No. of pupils in KS1 group not calculable [not populated in 2025]
39,PTKS1GROUP_NA,Percentage of pupils in KS1group not calculable [not populated in 2025]
40,TFSM6CLA1A,Number of key stage 2 disadvantaged pupils (those who were eligible for free school meals in last 6 years or are looked after by the LA for a day or more or who have been adopted from care)
41,PTFSM6CLA1A,Percentage of key stage 2 disadvantaged pupils
42,TNotFSM6CLA1A,Number of key stage 2 pupils who are not disadvantaged
43,PTNotFSM6CLA1A,Percentage of key stage 2 pupils who are not disadvantaged
44,TEALGRP2,Number of eligible pupils with English as additional language (EAL)
45,PTEALGRP2,Percentage of eligible pupils with English as additional language (EAL)
46,TMOBN,Number of eligible pupils classified as non-mobile
47,PTMOBN,Percentage of eligible pupils classified as non-mobile
48,PTRWM_EXP,"Percentage of pupils reaching the expected standard in reading, writing and maths"
49,PTRWM_HIGH,Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing
50,READPROG,Reading progress measure [not populated in 2025]
51,READPROG_LOWER,Reading progress measure - lower confidence limit [not populated in 2025]
52,READPROG_UPPER,Reading progress measure - upper confidence limit [not populated in 2025]
53,READCOV,Reading progress measure - coverage [not populated in 2025]
54,WRITPROG,Writing progress measure [not populated in 2025]
55,WRITPROG_LOWER,Writing progress measure - lower confidence limit [not populated in 2025]
56,WRITPROG_UPPER,Writing progress measure - upper confidence limit [not populated in 2025]
57,WRITCOV,Writing progress measure - coverage [not populated in 2025]
58,MATPROG,Maths progress measure [not populated in 2025]
59,MATPROG_LOWER,Maths progress measure - lower confidence limit [not populated in 2025]
60,MATPROG_UPPER,Maths progress measure - upper confidence limit [not populated in 2025]
61,MATCOV,Maths progress measure - coverage [not populated in 2025]
62,PTREAD_EXP,Percentage of pupils reaching the expected standard in reading
63,PTREAD_HIGH,Percentage of pupils achieving a high score in reading
64,PTREAD_AT,Percentage of pupils absent from or not able to access the test in reading
65,READ_AVERAGE,Average scaled score in reading
66,PTGPS_EXP,"Percentage of pupils reaching the expected standard in grammar, punctuation and spelling"
67,PTGPS_HIGH,"Percentage of pupils achieving a high score in grammar, punctuation and spelling"
68,PTGPS_AT,"Percentage of pupils absent from or not able to access the test in grammar, punctuation and spelling"
69,GPS_AVERAGE,"Average scaled score in grammar, punctuation and spelling"
70,PTMAT_EXP,Percentage of pupils reaching the expected standard in maths
71,PTMAT_HIGH,Percentage of pupils achieving a high score in maths
72,PTMAT_AT,Percentage of pupils absent from or not able to access the test in maths
73,MAT_AVERAGE,Average scaled score in maths
74,PTWRITTA_EXP,Percentage of pupils reaching the expected standard in writing
75,PTWRITTA_HIGH,Percentage of pupils working at greater depth within the expected standard in writing
76,PTWRITTA_WTS,Percentage of pupils working towards the expected standard in writing
77,PTWRITTA_AD,Percentage of pupils absent or disapplied in writing TA
78,PTSCITA_EXP,Percentage of pupils reaching the expected standard in science TA
79,PTSCITA_AD,Percentage of pupils absent or disapplied in science TA
80,PTRWM_EXP_B,"Percentage of boys reaching the expected standard in reading, writing and maths"
81,PTRWM_EXP_G,"Percentage of girls reaching the expected standard in reading, writing and maths"
82,PTRWM_EXP_L,"Percentage of pupils with low prior attainment reaching the expected standard in reading, writing and maths [not populated in 2025]"
83,PTRWM_EXP_M,"Percentage of pupils with medium prior attainment reaching the expected standard in reading, writing and maths [not populated in 2025]"
84,PTRWM_EXP_H,"Percentage of pupils with high prior attainment reaching the expected standard in reading, writing and maths [not populated in 2025]"
85,PTRWM_EXP_FSM6CLA1A,"Percentage of disadvantaged pupils reaching the expected standard in reading, writing and maths"
86,PTRWM_EXP_NotFSM6CLA1A,"Percentage of non-disadvantaged pupils reaching the expected standard in reading, writing and maths"
87,DIFFN_RWM_EXP,"Difference between school percentage of disavantaged pupils and national percentage of other pupils reaching the expected standard in reading, writing and maths "
88,PTRWM_EXP_EAL,"Percentage of EAL pupils reaching the expected standard in reading, writing and maths"
89,PTRWM_EXP_MOBN,"Percentage of non-mobile pupils reaching the expected standard in reading, writing and maths"
90,PTRWM_HIGH_B,Percentage of boys achieving a high score in reading and maths and working at greater depth in writing
91,PTRWM_HIGH_G,"Percentage of girls reaching the HIGHected standard in reading, writing and maths"
92,PTRWM_HIGH_L,Percentage of pupils with low prior attainment achieving a high score in reading and maths and working at greater depth in writing [not populated in 2025]
93,PTRWM_HIGH_M,Percentage of pupils with medium prior attainment achieving a high score in reading and maths and working at greater depth in writing [not populated in 2025]
94,PTRWM_HIGH_H,Percentage of pupils with high prior attainment achieving a high score in reading and maths and working at greater depth in writing [not populated in 2025]
95,PTRWM_HIGH_FSM6CLA1A,Percentage of disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing
96,PTRWM_HIGH_NotFSM6CLA1A,Percentage of non-disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing
97,DIFFN_RWM_HIGH,"Difference between school percentage of disavantaged pupils and national percentage of other pupils achieving a high score in reading, writing and maths "
98,PTRWM_HIGH_EAL,Percentage of EAL pupils achieving a high score in reading and maths and working at greater depth in writing
99,PTRWM_HIGH_MOBN,Percentage of non-mobile pupils achieving a high score in reading and maths and working at greater depth in writing
100,READPROG_B,Reading progress measure for boys [not populated in 2025]
101,READPROG_B_LOWER,Reading progress measure for boys - lower confidence limit [not populated in 2025]
102,READPROG_B_UPPER,Reading progress measure for boys - upper confidence limit [not populated in 2025]
103,READPROG_G,Reading progress measure for girls [not populated in 2025]
104,READPROG_G_LOWER,Reading progress measure for girls - lower confidence limit [not populated in 2025]
105,READPROG_G_UPPER,Reading progress measure for girls - upper confidence limit [not populated in 2025]
106,READPROG_L,Reading progress measure for pupils with low prior attainment [not populated in 2025]
107,READPROG_L_LOWER,Reading progress measure for pupils with low prior attainment - lower confidence limit [not populated in 2025]
108,READPROG_L_UPPER,Reading progress measure for pupils with low prior attainment - upper confidence limit [not populated in 2025]
109,READPROG_M,Reading progress measure for pupils with medium prior attainment [not populated in 2025]
110,READPROG_M_LOWER,Reading progress measure for pupils with medium prior attainment - lower confidence limit [not populated in 2025]
111,READPROG_M_UPPER,Reading progress measure for pupils with medium prior attainment - upper confidence limit [not populated in 2025]
112,READPROG_H,Reading progress measure for pupils with high prior attainment [not populated in 2025]
113,READPROG_H_LOWER,Reading progress measure for pupils with high prior attainment - lower confidence limit [not populated in 2025]
114,READPROG_H_UPPER,Reading progress measure for pupils with high prior attainment - upper confidence limit [not populated in 2025]
115,READPROG_FSM6CLA1A,Reading progress measure for disadvantaged pupils [not populated in 2025]
116,READPROG_FSM6CLA1A_LOWER,Reading progress measure for disadvantaged pupils - lower confidence limit [not populated in 2025]
117,READPROG_FSM6CLA1A_UPPER,Reading progress measure for disadvantaged pupils - upper confidence limit [not populated in 2025]
118,READPROG_NotFSM6CLA1A,Reading progress measure for non-disadvantaged pupils [not populated in 2025]
119,READPROG_NotFSM6CLA1A_LOWER,Reading progress measure for non-disadvantaged pupils - lower confidence limit [not populated in 2025]
120,READPROG_NotFSM6CLA1A_UPPER,Reading progress measure for non-disadvantaged pupils - upper confidence limit [not populated in 2025]
121,DIFFN_READPROG,Difference between reading progress measure for disadvantaged pupils in school and other pupils nationally [not populated in 2025]
122,READPROG_EAL,Reading progress measure for EAL pupils [not populated in 2025]
123,READPROG_EAL_LOWER,Reading progress measure for EAL pupils - lower confidence limit [not populated in 2025]
124,READPROG_EAL_UPPER,Reading progress measure for EAL pupils - upper confidence limit [not populated in 2025]
125,READPROG_MOBN,Reading progress measure for non-mobile pupils [not populated in 2025]
126,READPROG_MOBN_LOWER,Reading progress measure for non-mobile pupils - lower confidence limit [not populated in 2025]
127,READPROG_MOBN_UPPER,Reading progress measure for non-mobile pupils - upper confidence limit [not populated in 2025]
128,WRITPROG_B,Writing progress measure for boys [not populated in 2025]
129,WRITPROG_B_LOWER,Writing progress measure for boys - lower confidence limit [not populated in 2025]
130,WRITPROG_B_UPPER,Writing progress measure for boys - upper confidence limit [not populated in 2025]
131,WRITPROG_G,Writing progress measure for girls [not populated in 2025]
132,WRITPROG_G_LOWER,Writing progress measure for girls - lower confidence limit [not populated in 2025]
133,WRITPROG_G_UPPER,Writing progress measure for girls - upper confidence limit [not populated in 2025]
134,WRITPROG_L,Writing progress measure for pupils with low prior attainment [not populated in 2025]
135,WRITPROG_L_LOWER,Writing progress measure for pupils with low prior attainment - lower confidence limit [not populated in 2025]
136,WRITPROG_L_UPPER,Writing progress measure for pupils with low prior attainment - upper confidence limit [not populated in 2025]
137,WRITPROG_M,Writing progress measure for pupils with medium prior attainment [not populated in 2025]
138,WRITPROG_M_LOWER,Writing progress measure for pupils with medium prior attainment - lower confidence limit [not populated in 2025]
139,WRITPROG_M_UPPER,Writing progress measure for pupils with medium prior attainment - upper confidence limit [not populated in 2025]
140,WRITPROG_H,Writing progress measure for pupils with high prior attainment [not populated in 2025]
141,WRITPROG_H_LOWER,Writing progress measure for pupils with high prior attainment - lower confidence limit [not populated in 2025]
142,WRITPROG_H_UPPER,Writing progress measure for pupils with high prior attainment - upper confidence limit [not populated in 2025]
143,WRITPROG_FSM6CLA1A,Writing progress measure for disadvantaged pupils [not populated in 2025]
144,WRITPROG_FSM6CLA1A_LOWER,Writing progress measure for disadvantaged pupils - lower confidence limit [not populated in 2025]
145,WRITPROG_FSM6CLA1A_UPPER,Writing progress measure for disadvantaged pupils - upper confidence limit [not populated in 2025]
146,WRITPROG_NotFSM6CLA1A,Writing progress measure for non-disadvantaged pupils [not populated in 2025]
147,WRITPROG_NotFSM6CLA1A_LOWER,Writing progress measure for non-disadvantaged pupils - lower confidence limit [not populated in 2025]
148,WRITPROG_NotFSM6CLA1A_UPPER,Writing progress measure for non-disadvantaged pupils - upper confidence limit [not populated in 2025]
149,DIFFN_WRITPROG,Difference between writing progress measure for disadvantaged pupils in school and other pupils nationally [not populated in 2025]
150,WRITPROG_EAL,Writing progress measure for EAL pupils [not populated in 2025]
151,WRITPROG_EAL_LOWER,Writing progress measure for EAL pupils - lower confidence limit [not populated in 2025]
152,WRITPROG_EAL_UPPER,Writing progress measure for EAL pupils - upper confidence limit [not populated in 2025]
153,WRITPROG_MOBN,Writing progress measure for non-mobile pupils [not populated in 2025]
154,WRITPROG_MOBN_LOWER,Writing progress measure for non-mobile pupils - lower confidence limit [not populated in 2025]
155,WRITPROG_MOBN_UPPER,Writing progress measure for non-mobile pupils - upper confidence limit [not populated in 2025]
156,MATPROG_B,Maths progress measure for boys [not populated in 2025]
157,MATPROG_B_LOWER,Maths progress measure for boys - lower confidence limit [not populated in 2025]
158,MATPROG_B_UPPER,Maths progress measure for boys - upper confidence limit [not populated in 2025]
159,MATPROG_G,Maths progress measure for girls [not populated in 2025]
160,MATPROG_G_LOWER,Maths progress measure for girls - lower confidence limit [not populated in 2025]
161,MATPROG_G_UPPER,Maths progress measure for girls - upper confidence limit [not populated in 2025]
162,MATPROG_L,Maths progress measure for pupils with low prior attainment [not populated in 2025]
163,MATPROG_L_LOWER,Maths progress measure for pupils with low prior attainment - lower confidence limit [not populated in 2025]
164,MATPROG_L_UPPER,Maths progress measure for pupils with low prior attainment - upper confidence limit [not populated in 2025]
165,MATPROG_M,Maths progress measure for pupils with medium prior attainment [not populated in 2025]
166,MATPROG_M_LOWER,Maths progress measure for pupils with medium prior attainment - lower confidence limit [not populated in 2025]
167,MATPROG_M_UPPER,Maths progress measure for pupils with medium prior attainment - upper confidence limit [not populated in 2025]
168,MATPROG_H,Maths progress measure for pupils with high prior attainment [not populated in 2025]
169,MATPROG_H_LOWER,Maths progress measure for pupils with high prior attainment - lower confidence limit [not populated in 2025]
170,MATPROG_H_UPPER,Maths progress measure for pupils with high prior attainment - upper confidence limit [not populated in 2025]
171,MATPROG_FSM6CLA1A,Maths progress measure for disadvantaged pupils [not populated in 2025]
172,MATPROG_FSM6CLA1A_LOWER,Maths progress measure for disadvantaged pupils - lower confidence limit [not populated in 2025]
173,MATPROG_FSM6CLA1A_UPPER,Maths progress measure for disadvantaged pupils - upper confidence limit [not populated in 2025]
174,MATPROG_NotFSM6CLA1A,Maths progress measure for non-disadvantaged pupils [not populated in 2025]
175,MATPROG_NotFSM6CLA1A_LOWER,Maths progress measure for non-disadvantaged pupils - lower confidence limit [not populated in 2025]
176,MATPROG_NotFSM6CLA1A_UPPER,Maths progress measure for non-disadvantaged pupils - upper confidence limit [not populated in 2025]
177,DIFFN_MATPROG,Difference between maths progress measure for disadvantaged pupils in school and other pupils nationally [not populated in 2025]
178,MATPROG_EAL,Maths progress measure for EAL pupils [not populated in 2025]
179,MATPROG_EAL_LOWER,Maths progress measure for EAL pupils - lower confidence limit [not populated in 2025]
180,MATPROG_EAL_UPPER,Maths progress measure for EAL pupils - upper confidence limit [not populated in 2025]
181,MATPROG_MOBN,Maths progress measure for non-mobile pupils [not populated in 2025]
182,MATPROG_MOBN_LOWER,Maths progress measure for non-mobile pupils - lower confidence limit [not populated in 2025]
183,MATPROG_MOBN_UPPER,Maths progress measure for non-mobile pupils - upper confidence limit [not populated in 2025]
184,READ_AVERAGE_B,Average scaled score in reading for boys
185,READ_AVERAGE_G,Average scaled score in reading for girls
186,READ_AVERAGE_L,Average scaled score in reading for pupils with low prior attainment [not populated in 2025]
187,READ_AVERAGE_M,Average scaled score in reading for pupils with medium prior attainment [not populated in 2025]
188,READ_AVERAGE_H,Average scaled score in reading for pupils with high prior attainment [not populated in 2025]
189,READ_AVERAGE_FSM6CLA1A,Average scaled score in reading for disadvantaged pupils
190,READ_AVERAGE_NotFSM6CLA1A,Average scaled score in reading for non-disadvantaged pupils
191,READ_AVERAGE_EAL,Average scaled score in reading for EAL pupils
192,READ_AVERAGE_MOBN,Average scaled score in reading for MOBN pupils
193,MAT_AVERAGE_B,Average scaled score in maths for boys
194,MAT_AVERAGE_G,Average scaled score in maths for girls
195,MAT_AVERAGE_L,Average scaled score in maths for pupils with low prior attainment [not populated in 2025]
196,MAT_AVERAGE_M,Average scaled score in maths for pupils with medium prior attainment [not populated in 2025]
197,MAT_AVERAGE_H,Average scaled score in maths for pupils with high prior attainment [not populated in 2025]
198,MAT_AVERAGE_FSM6CLA1A,Average scaled score in maths for disadvantaged pupils
199,MAT_AVERAGE_NotFSM6CLA1A,Average scaled score in maths for non-disadvantaged pupils
200,MAT_AVERAGE_EAL,Average scaled score in maths for EAL pupils
201,MAT_AVERAGE_MOBN,Average scaled score in maths for MOBN pupils
202,GPS_AVERAGE_B,Average scaled score in GPS for boys
203,GPS_AVERAGE_G,Average scaled score in GPS for girls
204,GPS_AVERAGE_L,Average scaled score in GPS for pupils with low prior attainment [not populated in 2025]
205,GPS_AVERAGE_M,Average scaled score in GPS for pupils with medium prior attainment [not populated in 2025]
206,GPS_AVERAGE_H,Average scaled score in GPS for pupils with high prior attainment [not populated in 2025]
207,GPS_AVERAGE_FSM6CLA1A,Average scaled score in GPS for disadvantaged pupils
208,GPS_AVERAGE_NotFSM6CLA1A,Average scaled score in GPS for non-disadvantaged pupils
209,GPS_AVERAGE_EAL,Average scaled score in GPS for EAL pupils
210,GPS_AVERAGE_MOBN,Average scaled score in GPS for MOBN pupils
211,PTREAD_EXP_L,Percentage of pupils with low prior attainment reaching the expected standard in reading [not populated in 2025]
212,PTREAD_EXP_M,Percentage of pupils with medium prior attainment reaching the expected standard in reading [not populated in 2025]
213,PTREAD_EXP_H,Percentage of pupils with high prior attainment reaching the expected standard in reading [not populated in 2025]
214,PTREAD_EXP_FSM6CLA1A,Percentage of disadvantaged pupils reaching the expected standard in reading
215,PTREAD_EXP_NotFSM6CLA1A,Percentage of non-disadvantaged pupils reaching the expected standard in reading
216,PTGPS_EXP_L,"Percentage of pupils with low prior attainment reaching the expected standard in grammar, punctuation and spelling [not populated in 2025]"
217,PTGPS_EXP_M,"Percentage of pupils with medium prior attainment reaching the expected standard in grammar, punctuation and spelling [not populated in 2025]"
218,PTGPS_EXP_H,"Percentage of pupils with high prior attainment reaching the expected standard in grammar, punctuation and spelling [not populated in 2025]"
219,PTGPS_EXP_FSM6CLA1A,"Percentage of disadvantaged pupils reaching the expected standard in grammar, punctuation and spelling"
220,PTGPS_EXP_NotFSM6CLA1A,"Percentage of non-disadvantaged pupils reaching the expected standard in grammar, punctuation and spelling"
221,PTMAT_EXP_L,Percentage of pupils with low prior attainment reaching the expected standard in maths [not populated in 2025]
222,PTMAT_EXP_M,Percentage of pupils with medium prior attainment reaching the expected standard in maths [not populated in 2025]
223,PTMAT_EXP_H,Percentage of pupils with high prior attainment reaching the expected standard in maths [not populated in 2025]
224,PTMAT_EXP_FSM6CLA1A,Percentage of disadvantaged pupils reaching the expected standard in maths
225,PTMAT_EXP_NotFSM6CLA1A,Percentage of non-disadvantaged pupils reaching the expected standard in maths
226,PTWRITTA_EXP_L,Percentage of pupils with low prior attainment reaching the expected standard in writing [not populated in 2025]
227,PTWRITTA_EXP_M,Percentage of pupils with medium prior attainment reaching the expected standard in writing [not populated in 2025]
228,PTWRITTA_EXP_H,Percentage of pupils with high prior attainment reaching the expected standard in writing [not populated in 2025]
229,PTWRITTA_EXP_FSM6CLA1A,Percentage of disadvantaged pupils reaching the expected standard in writing
230,PTWRITTA_EXP_NotFSM6CLA1A,Percentage of non-disadvantaged pupils reaching the expected standard in writing
231,PTREAD_HIGH_L,Percentage of pupils with low prior attainment achieving a high score in reading [not populated in 2025]
232,PTREAD_HIGH_M,Percentage of pupils with medium prior attainment achieving a high score in reading [not populated in 2025]
233,PTREAD_HIGH_H,Percentage of pupils with high prior attainment achieving a high score in reading [not populated in 2025]
234,PTREAD_HIGH_FSM6CLA1A,Percentage of disadvantaged pupils achieving a high score in reading
235,PTREAD_HIGH_NotFSM6CLA1A,Percentage of non-disadvantaged pupils achieving a high score in reading
236,PTGPS_HIGH_L,"Percentage of pupils with low prior attainment achieving a high score in grammar, punctuation and spelling [not populated in 2025]"
237,PTGPS_HIGH_M,"Percentage of pupils with medium prior attainment achieving a high score in grammar, punctuation and spelling [not populated in 2025]"
238,PTGPS_HIGH_H,"Percentage of pupils with high prior attainment achieving a high score in grammar, punctuation and spelling [not populated in 2025]"
239,PTGPS_HIGH_FSM6CLA1A,"Percentage of disadvantaged pupils achieving a high score in grammar, punctuation and spelling"
240,PTGPS_HIGH_NotFSM6CLA1A,"Percentage of non-disadvantaged pupils achieving a high score in grammar, punctuation and spelling"
241,PTMAT_HIGH_L,Percentage of pupils with low prior attainment achieving a high score in maths [not populated in 2025]
242,PTMAT_HIGH_M,Percentage of pupils with medium prior attainment achieving a high score in maths [not populated in 2025]
243,PTMAT_HIGH_H,Percentage of pupils with high prior attainment achieving a high score in maths [not populated in 2025]
244,PTMAT_HIGH_FSM6CLA1A,Percentage of disadvantaged pupils achieving a high score in maths
245,PTMAT_HIGH_NotFSM6CLA1A,Percentage of non-disadvantaged pupils achieving a high score in maths
246,PTWRITTA_HIGH_L,Percentage of pupils with low prior attainment working at greater depth in writing [not populated in 2025]
247,PTWRITTA_HIGH_M,Percentage of pupils with medium prior attainment working at greater depth in writing [not populated in 2025]
248,PTWRITTA_HIGH_H,Percentage of pupils with high prior attainment working at greater depth in writing [not populated in 2025]
249,PTWRITTA_HIGH_FSM6CLA1A,Percentage of disadvantaged pupils working at greater depth in writing
250,PTWRITTA_HIGH_NotFSM6CLA1A,Percentage of non-disadvantaged pupils working at greater depth in writing
251,TEALGRP1,Number of eligible pupils with English as first language
252,PTEALGRP1,Percentage of eligible pupils with English as first language
253,TEALGRP3,Number of eligible pupils with unclassified language
254,PTEALGRP3,Percentage of eligible pupils with unclassified language
255,TSENELE,Number of eligible pupils with EHC plan
256,PSENELE,Percentage of eligible pupils with EHC plan
257,TSENELK,Number of eligible pupils with SEN support
258,PSENELK,Percentage of eligible pupils with SEN support
259,TSENELEK,Number of eligible pupils with SEN (EHC plan or SEN support)
260,PSENELEK,Percentage of eligible pupils with SEN (EHC plan or SEN support)
261,TELIG_24,Number of eligible pupils 2024
262,PTFSM6CLA1A_24,Percentage of key stage 2 disadvantaged pupils one year prior
263,PTNOTFSM6CLA1A_24,Percentage of key stage 2 pupils who are not disadvantaged one year prior
264,PTRWM_EXP_24,"Percentage of pupils reaching the expected standard in reading, writing and maths one year prior"
265,PTRWM_HIGH_24,Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing one year prior
266,PTRWM_EXP_FSM6CLA1A_24,"Percentage of disadvantaged pupils reaching the expected standard in reading, writing and maths one year prior"
267,PTRWM_HIGH_FSM6CLA1A_24,Percentage of disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing one year prior
268,PTRWM_EXP_NotFSM6CLA1A_24,"Percentage of non-disadvantaged pupils reaching the expected standard in reading, writing and maths one year prior"
269,PTRWM_HIGH_NotFSM6CLA1A_24,Percentage of non-disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing one year prior
270,READPROG_24,Reading progress measure - one year prior [not populated in 2025]
271,READPROG_LOWER_24,Reading progress measure - lower confidence limit - one year prior [not populated in 2025]
272,READPROG_UPPER_24,Reading progress measure - upper confidence limit - one year prior [not populated in 2025]
273,WRITPROG_24,Writing progress measure - one year prior [not populated in 2025]
274,WRITPROG_LOWER_24,Writing progress measure - lower confidence limit - one year prior [not populated in 2025]
275,WRITPROG_UPPER_24,Writing progress measure - upper confidence limit - one year prior [not populated in 2025]
276,MATPROG_24,Maths progress measure - one year prior [not populated in 2025]
277,MATPROG_LOWER_24,Maths progress measure - lower confidence limit - one year prior [not populated in 2025]
278,MATPROG_UPPER_24,Maths progress measure - upper confidence limit - one year prior [not populated in 2025]
279,READ_AVERAGE_24,Average scaled score in reading - one year prior
280,MAT_AVERAGE_24,Average scaled score in maths - one year prior
281,TELIG_23,Number of eligible pupils 2023
282,PTFSM6CLA1A_23,Percentage of key stage 2 disadvantaged pupils - two years prior
283,PTNOTFSM6CLA1A_23,Percentage of key stage 2 pupils who are not disadvantaged - two years prior
284,PTRWM_EXP_23,"Percentage of pupils reaching the expected standard in reading, writing and maths - two years prior"
285,PTRWM_HIGH_23,Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing - two years prior
286,PTRWM_EXP_FSM6CLA1A_23,"Percentage of disadvantaged pupils reaching the expected standard in reading, writing and maths - two years prior"
287,PTRWM_HIGH_FSM6CLA1A_23,Percentage of disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing - two years prior
288,PTRWM_EXP_NotFSM6CLA1A_23,"Percentage of non-disadvantaged pupils reaching the expected standard in reading, writing and maths - two years prior"
289,PTRWM_HIGH_NotFSM6CLA1A_23,Percentage of non-disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing - two years prior
290,READPROG_23,Reading progress measure - two years prior
291,READPROG_LOWER_23,Reading progress measure - lower confidence limit - two years prior
292,READPROG_UPPER_23,Reading progress measure - upper confidence limit - two years prior
293,WRITPROG_23,Writing progress measure - two years prior
294,WRITPROG_LOWER_23,Writing progress measure - lower confidence limit - two years prior
295,WRITPROG_UPPER_23,Writing progress measure - upper confidence limit - two years prior
296,MATPROG_23,Maths progress measure - two years prior
297,MATPROG_LOWER_23,Maths progress measure - lower confidence limit - two years prior
298,MATPROG_UPPER_23,Maths progress measure - upper confidence limit - two years prior
299,READ_AVERAGE_23,Average scaled score in reading - two years prior
300,MAT_AVERAGE_23,Average scaled score in maths - two years prior
301,TELIG_3YR,Total number of pupils at the end of Key Stage 2 over the past three years
302,PTRWM_EXP_3YR,"Percentage of pupils reaching the expected standard in reading, writing and maths - 3 year total"
303,PTRWM_HIGH_3YR,Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing - 3 year total
304,READ_AVERAGE_3YR,Average scaled score in reading - 3 year average
305,MAT_AVERAGE_3YR,Average scaled score in maths - 3 year average
306,READPROG_UNADJUSTED,Unadjusted reading progress measure [not populated in 2025]
307,WRITPROG_UNADJUSTED,Unadjusted writing progress measure [not populated in 2025]
308,MATPROG_UNADJUSTED,Unadjusted maths progress measure [not populated in 2025]
309,READPROG_DESCR,Reading progress measure 'description' [not populated in 2025]
310,WRITPROG_DESCR,Writing progress measure 'description' [not populated in 2025]
311,MATPROG_DESCR,Maths progress measure 'description' [not populated in 2025]
1 Column Field Name Label/Description
2 1 RECTYPE Record type
3 2 AlphaIND Alphabetic index
4 3 LEA Local authority number
5 4 ESTAB Establishment number
6 5 URN School unique reference number
7 6 SCHNAME School/Local authority name
8 7 ADDRESS1 School address (1)
9 8 ADDRESS2 School address (2)
10 9 ADDRESS3 School address (3)
11 10 TOWN School town
12 11 PCODE School postcode
13 12 TELNUM School telephone number
14 13 PCON_CODE School parliamentary constituency code
15 14 PCON_NAME School parliamentary constituency name
16 15 URN_AC Converter academy: URN
17 16 SCHNAME_AC Converter academy: name
18 17 OPEN_AC Converter academy: open date
19 18 NFTYPE School type
20 19 ICLOSE Closed Flag
21 20 RELDENOM Religious denomination
22 21 AGERANGE Age range
23 22 TAB15 School published in secondary school (key stage 4) performance tables
24 23 TAB1618 School published in school and college (key stage 5) performance tables
25 24 TOTPUPS Total number of pupils (including part-time pupils)
26 25 TPUPYEAR Number of pupils aged 11
27 26 TELIG Published eligible pupil number
28 27 BELIG Eligible boys on school roll at time of tests
29 28 GELIG Eligible girls on school roll at time of tests
30 29 PBELIG Percentage of eligible boys on school roll at time of tests
31 30 PGELIG Percentage of eligible girls on school roll at time of tests
32 31 TKS1AVERAGE Cohort level key stage 1 average points score [not populated in 2025]
33 32 TKS1GROUP_L Number of pupils in cohort with low KS1 attainment [not populated in 2025]
34 33 PTKS1GROUP_L Percentage of pupils in cohort with low KS1 attainment [not populated in 2025]
35 34 TKS1GROUP_M Number of pupils in cohort with medium KS1 attainment [not populated in 2025]
36 35 PTKS1GROUP_M Percentage of pupils in cohort with medium KS1 attainment [not populated in 2025]
37 36 TKS1GROUP_H Number of pupils in cohort high KS1 attainment [not populated in 2025]
38 37 PTKS1GROUP_H Percentage of pupils in cohort with high KS1 attainment [not populated in 2025]
39 38 TKS1GROUP_NA No. of pupils in KS1 group not calculable [not populated in 2025]
40 39 PTKS1GROUP_NA Percentage of pupils in KS1group not calculable [not populated in 2025]
41 40 TFSM6CLA1A Number of key stage 2 disadvantaged pupils (those who were eligible for free school meals in last 6 years or are looked after by the LA for a day or more or who have been adopted from care)
42 41 PTFSM6CLA1A Percentage of key stage 2 disadvantaged pupils
43 42 TNotFSM6CLA1A Number of key stage 2 pupils who are not disadvantaged
44 43 PTNotFSM6CLA1A Percentage of key stage 2 pupils who are not disadvantaged
45 44 TEALGRP2 Number of eligible pupils with English as additional language (EAL)
46 45 PTEALGRP2 Percentage of eligible pupils with English as additional language (EAL)
47 46 TMOBN Number of eligible pupils classified as non-mobile
48 47 PTMOBN Percentage of eligible pupils classified as non-mobile
49 48 PTRWM_EXP Percentage of pupils reaching the expected standard in reading, writing and maths
50 49 PTRWM_HIGH Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing
51 50 READPROG Reading progress measure [not populated in 2025]
52 51 READPROG_LOWER Reading progress measure - lower confidence limit [not populated in 2025]
53 52 READPROG_UPPER Reading progress measure - upper confidence limit [not populated in 2025]
54 53 READCOV Reading progress measure - coverage [not populated in 2025]
55 54 WRITPROG Writing progress measure [not populated in 2025]
56 55 WRITPROG_LOWER Writing progress measure - lower confidence limit [not populated in 2025]
57 56 WRITPROG_UPPER Writing progress measure - upper confidence limit [not populated in 2025]
58 57 WRITCOV Writing progress measure - coverage [not populated in 2025]
59 58 MATPROG Maths progress measure [not populated in 2025]
60 59 MATPROG_LOWER Maths progress measure - lower confidence limit [not populated in 2025]
61 60 MATPROG_UPPER Maths progress measure - upper confidence limit [not populated in 2025]
62 61 MATCOV Maths progress measure - coverage [not populated in 2025]
63 62 PTREAD_EXP Percentage of pupils reaching the expected standard in reading
64 63 PTREAD_HIGH Percentage of pupils achieving a high score in reading
65 64 PTREAD_AT Percentage of pupils absent from or not able to access the test in reading
66 65 READ_AVERAGE Average scaled score in reading
67 66 PTGPS_EXP Percentage of pupils reaching the expected standard in grammar, punctuation and spelling
68 67 PTGPS_HIGH Percentage of pupils achieving a high score in grammar, punctuation and spelling
69 68 PTGPS_AT Percentage of pupils absent from or not able to access the test in grammar, punctuation and spelling
70 69 GPS_AVERAGE Average scaled score in grammar, punctuation and spelling
71 70 PTMAT_EXP Percentage of pupils reaching the expected standard in maths
72 71 PTMAT_HIGH Percentage of pupils achieving a high score in maths
73 72 PTMAT_AT Percentage of pupils absent from or not able to access the test in maths
74 73 MAT_AVERAGE Average scaled score in maths
75 74 PTWRITTA_EXP Percentage of pupils reaching the expected standard in writing
76 75 PTWRITTA_HIGH Percentage of pupils working at greater depth within the expected standard in writing
77 76 PTWRITTA_WTS Percentage of pupils working towards the expected standard in writing
78 77 PTWRITTA_AD Percentage of pupils absent or disapplied in writing TA
79 78 PTSCITA_EXP Percentage of pupils reaching the expected standard in science TA
80 79 PTSCITA_AD Percentage of pupils absent or disapplied in science TA
81 80 PTRWM_EXP_B Percentage of boys reaching the expected standard in reading, writing and maths
82 81 PTRWM_EXP_G Percentage of girls reaching the expected standard in reading, writing and maths
83 82 PTRWM_EXP_L Percentage of pupils with low prior attainment reaching the expected standard in reading, writing and maths [not populated in 2025]
84 83 PTRWM_EXP_M Percentage of pupils with medium prior attainment reaching the expected standard in reading, writing and maths [not populated in 2025]
85 84 PTRWM_EXP_H Percentage of pupils with high prior attainment reaching the expected standard in reading, writing and maths [not populated in 2025]
86 85 PTRWM_EXP_FSM6CLA1A Percentage of disadvantaged pupils reaching the expected standard in reading, writing and maths
87 86 PTRWM_EXP_NotFSM6CLA1A Percentage of non-disadvantaged pupils reaching the expected standard in reading, writing and maths
88 87 DIFFN_RWM_EXP Difference between school percentage of disavantaged pupils and national percentage of other pupils reaching the expected standard in reading, writing and maths
89 88 PTRWM_EXP_EAL Percentage of EAL pupils reaching the expected standard in reading, writing and maths
90 89 PTRWM_EXP_MOBN Percentage of non-mobile pupils reaching the expected standard in reading, writing and maths
91 90 PTRWM_HIGH_B Percentage of boys achieving a high score in reading and maths and working at greater depth in writing
92 91 PTRWM_HIGH_G Percentage of girls reaching the HIGHected standard in reading, writing and maths
93 92 PTRWM_HIGH_L Percentage of pupils with low prior attainment achieving a high score in reading and maths and working at greater depth in writing [not populated in 2025]
94 93 PTRWM_HIGH_M Percentage of pupils with medium prior attainment achieving a high score in reading and maths and working at greater depth in writing [not populated in 2025]
95 94 PTRWM_HIGH_H Percentage of pupils with high prior attainment achieving a high score in reading and maths and working at greater depth in writing [not populated in 2025]
96 95 PTRWM_HIGH_FSM6CLA1A Percentage of disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing
97 96 PTRWM_HIGH_NotFSM6CLA1A Percentage of non-disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing
98 97 DIFFN_RWM_HIGH Difference between school percentage of disavantaged pupils and national percentage of other pupils achieving a high score in reading, writing and maths
99 98 PTRWM_HIGH_EAL Percentage of EAL pupils achieving a high score in reading and maths and working at greater depth in writing
100 99 PTRWM_HIGH_MOBN Percentage of non-mobile pupils achieving a high score in reading and maths and working at greater depth in writing
101 100 READPROG_B Reading progress measure for boys [not populated in 2025]
102 101 READPROG_B_LOWER Reading progress measure for boys - lower confidence limit [not populated in 2025]
103 102 READPROG_B_UPPER Reading progress measure for boys - upper confidence limit [not populated in 2025]
104 103 READPROG_G Reading progress measure for girls [not populated in 2025]
105 104 READPROG_G_LOWER Reading progress measure for girls - lower confidence limit [not populated in 2025]
106 105 READPROG_G_UPPER Reading progress measure for girls - upper confidence limit [not populated in 2025]
107 106 READPROG_L Reading progress measure for pupils with low prior attainment [not populated in 2025]
108 107 READPROG_L_LOWER Reading progress measure for pupils with low prior attainment - lower confidence limit [not populated in 2025]
109 108 READPROG_L_UPPER Reading progress measure for pupils with low prior attainment - upper confidence limit [not populated in 2025]
110 109 READPROG_M Reading progress measure for pupils with medium prior attainment [not populated in 2025]
111 110 READPROG_M_LOWER Reading progress measure for pupils with medium prior attainment - lower confidence limit [not populated in 2025]
112 111 READPROG_M_UPPER Reading progress measure for pupils with medium prior attainment - upper confidence limit [not populated in 2025]
113 112 READPROG_H Reading progress measure for pupils with high prior attainment [not populated in 2025]
114 113 READPROG_H_LOWER Reading progress measure for pupils with high prior attainment - lower confidence limit [not populated in 2025]
115 114 READPROG_H_UPPER Reading progress measure for pupils with high prior attainment - upper confidence limit [not populated in 2025]
116 115 READPROG_FSM6CLA1A Reading progress measure for disadvantaged pupils [not populated in 2025]
117 116 READPROG_FSM6CLA1A_LOWER Reading progress measure for disadvantaged pupils - lower confidence limit [not populated in 2025]
118 117 READPROG_FSM6CLA1A_UPPER Reading progress measure for disadvantaged pupils - upper confidence limit [not populated in 2025]
119 118 READPROG_NotFSM6CLA1A Reading progress measure for non-disadvantaged pupils [not populated in 2025]
120 119 READPROG_NotFSM6CLA1A_LOWER Reading progress measure for non-disadvantaged pupils - lower confidence limit [not populated in 2025]
121 120 READPROG_NotFSM6CLA1A_UPPER Reading progress measure for non-disadvantaged pupils - upper confidence limit [not populated in 2025]
122 121 DIFFN_READPROG Difference between reading progress measure for disadvantaged pupils in school and other pupils nationally [not populated in 2025]
123 122 READPROG_EAL Reading progress measure for EAL pupils [not populated in 2025]
124 123 READPROG_EAL_LOWER Reading progress measure for EAL pupils - lower confidence limit [not populated in 2025]
125 124 READPROG_EAL_UPPER Reading progress measure for EAL pupils - upper confidence limit [not populated in 2025]
126 125 READPROG_MOBN Reading progress measure for non-mobile pupils [not populated in 2025]
127 126 READPROG_MOBN_LOWER Reading progress measure for non-mobile pupils - lower confidence limit [not populated in 2025]
128 127 READPROG_MOBN_UPPER Reading progress measure for non-mobile pupils - upper confidence limit [not populated in 2025]
129 128 WRITPROG_B Writing progress measure for boys [not populated in 2025]
130 129 WRITPROG_B_LOWER Writing progress measure for boys - lower confidence limit [not populated in 2025]
131 130 WRITPROG_B_UPPER Writing progress measure for boys - upper confidence limit [not populated in 2025]
132 131 WRITPROG_G Writing progress measure for girls [not populated in 2025]
133 132 WRITPROG_G_LOWER Writing progress measure for girls - lower confidence limit [not populated in 2025]
134 133 WRITPROG_G_UPPER Writing progress measure for girls - upper confidence limit [not populated in 2025]
135 134 WRITPROG_L Writing progress measure for pupils with low prior attainment [not populated in 2025]
136 135 WRITPROG_L_LOWER Writing progress measure for pupils with low prior attainment - lower confidence limit [not populated in 2025]
137 136 WRITPROG_L_UPPER Writing progress measure for pupils with low prior attainment - upper confidence limit [not populated in 2025]
138 137 WRITPROG_M Writing progress measure for pupils with medium prior attainment [not populated in 2025]
139 138 WRITPROG_M_LOWER Writing progress measure for pupils with medium prior attainment - lower confidence limit [not populated in 2025]
140 139 WRITPROG_M_UPPER Writing progress measure for pupils with medium prior attainment - upper confidence limit [not populated in 2025]
141 140 WRITPROG_H Writing progress measure for pupils with high prior attainment [not populated in 2025]
142 141 WRITPROG_H_LOWER Writing progress measure for pupils with high prior attainment - lower confidence limit [not populated in 2025]
143 142 WRITPROG_H_UPPER Writing progress measure for pupils with high prior attainment - upper confidence limit [not populated in 2025]
144 143 WRITPROG_FSM6CLA1A Writing progress measure for disadvantaged pupils [not populated in 2025]
145 144 WRITPROG_FSM6CLA1A_LOWER Writing progress measure for disadvantaged pupils - lower confidence limit [not populated in 2025]
146 145 WRITPROG_FSM6CLA1A_UPPER Writing progress measure for disadvantaged pupils - upper confidence limit [not populated in 2025]
147 146 WRITPROG_NotFSM6CLA1A Writing progress measure for non-disadvantaged pupils [not populated in 2025]
148 147 WRITPROG_NotFSM6CLA1A_LOWER Writing progress measure for non-disadvantaged pupils - lower confidence limit [not populated in 2025]
149 148 WRITPROG_NotFSM6CLA1A_UPPER Writing progress measure for non-disadvantaged pupils - upper confidence limit [not populated in 2025]
150 149 DIFFN_WRITPROG Difference between writing progress measure for disadvantaged pupils in school and other pupils nationally [not populated in 2025]
151 150 WRITPROG_EAL Writing progress measure for EAL pupils [not populated in 2025]
152 151 WRITPROG_EAL_LOWER Writing progress measure for EAL pupils - lower confidence limit [not populated in 2025]
153 152 WRITPROG_EAL_UPPER Writing progress measure for EAL pupils - upper confidence limit [not populated in 2025]
154 153 WRITPROG_MOBN Writing progress measure for non-mobile pupils [not populated in 2025]
155 154 WRITPROG_MOBN_LOWER Writing progress measure for non-mobile pupils - lower confidence limit [not populated in 2025]
156 155 WRITPROG_MOBN_UPPER Writing progress measure for non-mobile pupils - upper confidence limit [not populated in 2025]
157 156 MATPROG_B Maths progress measure for boys [not populated in 2025]
158 157 MATPROG_B_LOWER Maths progress measure for boys - lower confidence limit [not populated in 2025]
159 158 MATPROG_B_UPPER Maths progress measure for boys - upper confidence limit [not populated in 2025]
160 159 MATPROG_G Maths progress measure for girls [not populated in 2025]
161 160 MATPROG_G_LOWER Maths progress measure for girls - lower confidence limit [not populated in 2025]
162 161 MATPROG_G_UPPER Maths progress measure for girls - upper confidence limit [not populated in 2025]
163 162 MATPROG_L Maths progress measure for pupils with low prior attainment [not populated in 2025]
164 163 MATPROG_L_LOWER Maths progress measure for pupils with low prior attainment - lower confidence limit [not populated in 2025]
165 164 MATPROG_L_UPPER Maths progress measure for pupils with low prior attainment - upper confidence limit [not populated in 2025]
166 165 MATPROG_M Maths progress measure for pupils with medium prior attainment [not populated in 2025]
167 166 MATPROG_M_LOWER Maths progress measure for pupils with medium prior attainment - lower confidence limit [not populated in 2025]
168 167 MATPROG_M_UPPER Maths progress measure for pupils with medium prior attainment - upper confidence limit [not populated in 2025]
169 168 MATPROG_H Maths progress measure for pupils with high prior attainment [not populated in 2025]
170 169 MATPROG_H_LOWER Maths progress measure for pupils with high prior attainment - lower confidence limit [not populated in 2025]
171 170 MATPROG_H_UPPER Maths progress measure for pupils with high prior attainment - upper confidence limit [not populated in 2025]
172 171 MATPROG_FSM6CLA1A Maths progress measure for disadvantaged pupils [not populated in 2025]
173 172 MATPROG_FSM6CLA1A_LOWER Maths progress measure for disadvantaged pupils - lower confidence limit [not populated in 2025]
174 173 MATPROG_FSM6CLA1A_UPPER Maths progress measure for disadvantaged pupils - upper confidence limit [not populated in 2025]
175 174 MATPROG_NotFSM6CLA1A Maths progress measure for non-disadvantaged pupils [not populated in 2025]
176 175 MATPROG_NotFSM6CLA1A_LOWER Maths progress measure for non-disadvantaged pupils - lower confidence limit [not populated in 2025]
177 176 MATPROG_NotFSM6CLA1A_UPPER Maths progress measure for non-disadvantaged pupils - upper confidence limit [not populated in 2025]
178 177 DIFFN_MATPROG Difference between maths progress measure for disadvantaged pupils in school and other pupils nationally [not populated in 2025]
179 178 MATPROG_EAL Maths progress measure for EAL pupils [not populated in 2025]
180 179 MATPROG_EAL_LOWER Maths progress measure for EAL pupils - lower confidence limit [not populated in 2025]
181 180 MATPROG_EAL_UPPER Maths progress measure for EAL pupils - upper confidence limit [not populated in 2025]
182 181 MATPROG_MOBN Maths progress measure for non-mobile pupils [not populated in 2025]
183 182 MATPROG_MOBN_LOWER Maths progress measure for non-mobile pupils - lower confidence limit [not populated in 2025]
184 183 MATPROG_MOBN_UPPER Maths progress measure for non-mobile pupils - upper confidence limit [not populated in 2025]
185 184 READ_AVERAGE_B Average scaled score in reading for boys
186 185 READ_AVERAGE_G Average scaled score in reading for girls
187 186 READ_AVERAGE_L Average scaled score in reading for pupils with low prior attainment [not populated in 2025]
188 187 READ_AVERAGE_M Average scaled score in reading for pupils with medium prior attainment [not populated in 2025]
189 188 READ_AVERAGE_H Average scaled score in reading for pupils with high prior attainment [not populated in 2025]
190 189 READ_AVERAGE_FSM6CLA1A Average scaled score in reading for disadvantaged pupils
191 190 READ_AVERAGE_NotFSM6CLA1A Average scaled score in reading for non-disadvantaged pupils
192 191 READ_AVERAGE_EAL Average scaled score in reading for EAL pupils
193 192 READ_AVERAGE_MOBN Average scaled score in reading for MOBN pupils
194 193 MAT_AVERAGE_B Average scaled score in maths for boys
195 194 MAT_AVERAGE_G Average scaled score in maths for girls
196 195 MAT_AVERAGE_L Average scaled score in maths for pupils with low prior attainment [not populated in 2025]
197 196 MAT_AVERAGE_M Average scaled score in maths for pupils with medium prior attainment [not populated in 2025]
198 197 MAT_AVERAGE_H Average scaled score in maths for pupils with high prior attainment [not populated in 2025]
199 198 MAT_AVERAGE_FSM6CLA1A Average scaled score in maths for disadvantaged pupils
200 199 MAT_AVERAGE_NotFSM6CLA1A Average scaled score in maths for non-disadvantaged pupils
201 200 MAT_AVERAGE_EAL Average scaled score in maths for EAL pupils
202 201 MAT_AVERAGE_MOBN Average scaled score in maths for MOBN pupils
203 202 GPS_AVERAGE_B Average scaled score in GPS for boys
204 203 GPS_AVERAGE_G Average scaled score in GPS for girls
205 204 GPS_AVERAGE_L Average scaled score in GPS for pupils with low prior attainment [not populated in 2025]
206 205 GPS_AVERAGE_M Average scaled score in GPS for pupils with medium prior attainment [not populated in 2025]
207 206 GPS_AVERAGE_H Average scaled score in GPS for pupils with high prior attainment [not populated in 2025]
208 207 GPS_AVERAGE_FSM6CLA1A Average scaled score in GPS for disadvantaged pupils
209 208 GPS_AVERAGE_NotFSM6CLA1A Average scaled score in GPS for non-disadvantaged pupils
210 209 GPS_AVERAGE_EAL Average scaled score in GPS for EAL pupils
211 210 GPS_AVERAGE_MOBN Average scaled score in GPS for MOBN pupils
212 211 PTREAD_EXP_L Percentage of pupils with low prior attainment reaching the expected standard in reading [not populated in 2025]
213 212 PTREAD_EXP_M Percentage of pupils with medium prior attainment reaching the expected standard in reading [not populated in 2025]
214 213 PTREAD_EXP_H Percentage of pupils with high prior attainment reaching the expected standard in reading [not populated in 2025]
215 214 PTREAD_EXP_FSM6CLA1A Percentage of disadvantaged pupils reaching the expected standard in reading
216 215 PTREAD_EXP_NotFSM6CLA1A Percentage of non-disadvantaged pupils reaching the expected standard in reading
217 216 PTGPS_EXP_L Percentage of pupils with low prior attainment reaching the expected standard in grammar, punctuation and spelling [not populated in 2025]
218 217 PTGPS_EXP_M Percentage of pupils with medium prior attainment reaching the expected standard in grammar, punctuation and spelling [not populated in 2025]
219 218 PTGPS_EXP_H Percentage of pupils with high prior attainment reaching the expected standard in grammar, punctuation and spelling [not populated in 2025]
220 219 PTGPS_EXP_FSM6CLA1A Percentage of disadvantaged pupils reaching the expected standard in grammar, punctuation and spelling
221 220 PTGPS_EXP_NotFSM6CLA1A Percentage of non-disadvantaged pupils reaching the expected standard in grammar, punctuation and spelling
222 221 PTMAT_EXP_L Percentage of pupils with low prior attainment reaching the expected standard in maths [not populated in 2025]
223 222 PTMAT_EXP_M Percentage of pupils with medium prior attainment reaching the expected standard in maths [not populated in 2025]
224 223 PTMAT_EXP_H Percentage of pupils with high prior attainment reaching the expected standard in maths [not populated in 2025]
225 224 PTMAT_EXP_FSM6CLA1A Percentage of disadvantaged pupils reaching the expected standard in maths
226 225 PTMAT_EXP_NotFSM6CLA1A Percentage of non-disadvantaged pupils reaching the expected standard in maths
227 226 PTWRITTA_EXP_L Percentage of pupils with low prior attainment reaching the expected standard in writing [not populated in 2025]
228 227 PTWRITTA_EXP_M Percentage of pupils with medium prior attainment reaching the expected standard in writing [not populated in 2025]
229 228 PTWRITTA_EXP_H Percentage of pupils with high prior attainment reaching the expected standard in writing [not populated in 2025]
230 229 PTWRITTA_EXP_FSM6CLA1A Percentage of disadvantaged pupils reaching the expected standard in writing
231 230 PTWRITTA_EXP_NotFSM6CLA1A Percentage of non-disadvantaged pupils reaching the expected standard in writing
232 231 PTREAD_HIGH_L Percentage of pupils with low prior attainment achieving a high score in reading [not populated in 2025]
233 232 PTREAD_HIGH_M Percentage of pupils with medium prior attainment achieving a high score in reading [not populated in 2025]
234 233 PTREAD_HIGH_H Percentage of pupils with high prior attainment achieving a high score in reading [not populated in 2025]
235 234 PTREAD_HIGH_FSM6CLA1A Percentage of disadvantaged pupils achieving a high score in reading
236 235 PTREAD_HIGH_NotFSM6CLA1A Percentage of non-disadvantaged pupils achieving a high score in reading
237 236 PTGPS_HIGH_L Percentage of pupils with low prior attainment achieving a high score in grammar, punctuation and spelling [not populated in 2025]
238 237 PTGPS_HIGH_M Percentage of pupils with medium prior attainment achieving a high score in grammar, punctuation and spelling [not populated in 2025]
239 238 PTGPS_HIGH_H Percentage of pupils with high prior attainment achieving a high score in grammar, punctuation and spelling [not populated in 2025]
240 239 PTGPS_HIGH_FSM6CLA1A Percentage of disadvantaged pupils achieving a high score in grammar, punctuation and spelling
241 240 PTGPS_HIGH_NotFSM6CLA1A Percentage of non-disadvantaged pupils achieving a high score in grammar, punctuation and spelling
242 241 PTMAT_HIGH_L Percentage of pupils with low prior attainment achieving a high score in maths [not populated in 2025]
243 242 PTMAT_HIGH_M Percentage of pupils with medium prior attainment achieving a high score in maths [not populated in 2025]
244 243 PTMAT_HIGH_H Percentage of pupils with high prior attainment achieving a high score in maths [not populated in 2025]
245 244 PTMAT_HIGH_FSM6CLA1A Percentage of disadvantaged pupils achieving a high score in maths
246 245 PTMAT_HIGH_NotFSM6CLA1A Percentage of non-disadvantaged pupils achieving a high score in maths
247 246 PTWRITTA_HIGH_L Percentage of pupils with low prior attainment working at greater depth in writing [not populated in 2025]
248 247 PTWRITTA_HIGH_M Percentage of pupils with medium prior attainment working at greater depth in writing [not populated in 2025]
249 248 PTWRITTA_HIGH_H Percentage of pupils with high prior attainment working at greater depth in writing [not populated in 2025]
250 249 PTWRITTA_HIGH_FSM6CLA1A Percentage of disadvantaged pupils working at greater depth in writing
251 250 PTWRITTA_HIGH_NotFSM6CLA1A Percentage of non-disadvantaged pupils working at greater depth in writing
252 251 TEALGRP1 Number of eligible pupils with English as first language
253 252 PTEALGRP1 Percentage of eligible pupils with English as first language
254 253 TEALGRP3 Number of eligible pupils with unclassified language
255 254 PTEALGRP3 Percentage of eligible pupils with unclassified language
256 255 TSENELE Number of eligible pupils with EHC plan
257 256 PSENELE Percentage of eligible pupils with EHC plan
258 257 TSENELK Number of eligible pupils with SEN support
259 258 PSENELK Percentage of eligible pupils with SEN support
260 259 TSENELEK Number of eligible pupils with SEN (EHC plan or SEN support)
261 260 PSENELEK Percentage of eligible pupils with SEN (EHC plan or SEN support)
262 261 TELIG_24 Number of eligible pupils 2024
263 262 PTFSM6CLA1A_24 Percentage of key stage 2 disadvantaged pupils one year prior
264 263 PTNOTFSM6CLA1A_24 Percentage of key stage 2 pupils who are not disadvantaged one year prior
265 264 PTRWM_EXP_24 Percentage of pupils reaching the expected standard in reading, writing and maths one year prior
266 265 PTRWM_HIGH_24 Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing one year prior
267 266 PTRWM_EXP_FSM6CLA1A_24 Percentage of disadvantaged pupils reaching the expected standard in reading, writing and maths one year prior
268 267 PTRWM_HIGH_FSM6CLA1A_24 Percentage of disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing one year prior
269 268 PTRWM_EXP_NotFSM6CLA1A_24 Percentage of non-disadvantaged pupils reaching the expected standard in reading, writing and maths one year prior
270 269 PTRWM_HIGH_NotFSM6CLA1A_24 Percentage of non-disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing one year prior
271 270 READPROG_24 Reading progress measure - one year prior [not populated in 2025]
272 271 READPROG_LOWER_24 Reading progress measure - lower confidence limit - one year prior [not populated in 2025]
273 272 READPROG_UPPER_24 Reading progress measure - upper confidence limit - one year prior [not populated in 2025]
274 273 WRITPROG_24 Writing progress measure - one year prior [not populated in 2025]
275 274 WRITPROG_LOWER_24 Writing progress measure - lower confidence limit - one year prior [not populated in 2025]
276 275 WRITPROG_UPPER_24 Writing progress measure - upper confidence limit - one year prior [not populated in 2025]
277 276 MATPROG_24 Maths progress measure - one year prior [not populated in 2025]
278 277 MATPROG_LOWER_24 Maths progress measure - lower confidence limit - one year prior [not populated in 2025]
279 278 MATPROG_UPPER_24 Maths progress measure - upper confidence limit - one year prior [not populated in 2025]
280 279 READ_AVERAGE_24 Average scaled score in reading - one year prior
281 280 MAT_AVERAGE_24 Average scaled score in maths - one year prior
282 281 TELIG_23 Number of eligible pupils 2023
283 282 PTFSM6CLA1A_23 Percentage of key stage 2 disadvantaged pupils - two years prior
284 283 PTNOTFSM6CLA1A_23 Percentage of key stage 2 pupils who are not disadvantaged - two years prior
285 284 PTRWM_EXP_23 Percentage of pupils reaching the expected standard in reading, writing and maths - two years prior
286 285 PTRWM_HIGH_23 Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing - two years prior
287 286 PTRWM_EXP_FSM6CLA1A_23 Percentage of disadvantaged pupils reaching the expected standard in reading, writing and maths - two years prior
288 287 PTRWM_HIGH_FSM6CLA1A_23 Percentage of disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing - two years prior
289 288 PTRWM_EXP_NotFSM6CLA1A_23 Percentage of non-disadvantaged pupils reaching the expected standard in reading, writing and maths - two years prior
290 289 PTRWM_HIGH_NotFSM6CLA1A_23 Percentage of non-disadvantaged pupils achieving a high score in reading and maths and working at greater depth in writing - two years prior
291 290 READPROG_23 Reading progress measure - two years prior
292 291 READPROG_LOWER_23 Reading progress measure - lower confidence limit - two years prior
293 292 READPROG_UPPER_23 Reading progress measure - upper confidence limit - two years prior
294 293 WRITPROG_23 Writing progress measure - two years prior
295 294 WRITPROG_LOWER_23 Writing progress measure - lower confidence limit - two years prior
296 295 WRITPROG_UPPER_23 Writing progress measure - upper confidence limit - two years prior
297 296 MATPROG_23 Maths progress measure - two years prior
298 297 MATPROG_LOWER_23 Maths progress measure - lower confidence limit - two years prior
299 298 MATPROG_UPPER_23 Maths progress measure - upper confidence limit - two years prior
300 299 READ_AVERAGE_23 Average scaled score in reading - two years prior
301 300 MAT_AVERAGE_23 Average scaled score in maths - two years prior
302 301 TELIG_3YR Total number of pupils at the end of Key Stage 2 over the past three years
303 302 PTRWM_EXP_3YR Percentage of pupils reaching the expected standard in reading, writing and maths - 3 year total
304 303 PTRWM_HIGH_3YR Percentage of pupils achieving a high score in reading and maths and working at greater depth in writing - 3 year total
305 304 READ_AVERAGE_3YR Average scaled score in reading - 3 year average
306 305 MAT_AVERAGE_3YR Average scaled score in maths - 3 year average
307 306 READPROG_UNADJUSTED Unadjusted reading progress measure [not populated in 2025]
308 307 WRITPROG_UNADJUSTED Unadjusted writing progress measure [not populated in 2025]
309 308 MATPROG_UNADJUSTED Unadjusted maths progress measure [not populated in 2025]
310 309 READPROG_DESCR Reading progress measure 'description' [not populated in 2025]
311 310 WRITPROG_DESCR Writing progress measure 'description' [not populated in 2025]
312 311 MATPROG_DESCR Maths progress measure 'description' [not populated in 2025]

View File

@@ -0,0 +1,154 @@
LEA,LA Name,REGION,REGION NAME
841,Darlington,1,North East A
840,County Durham,1,North East A
805,Hartlepool,1,North East A
806,Middlesbrough,1,North East A
807,Redcar and Cleveland,1,North East A
808,Stockton-on-Tees,1,North East A
390,Gateshead,3,North East B
391,Newcastle upon Tyne,3,North East B
392,North Tyneside,3,North East B
929,Northumberland,3,North East B
393,South Tyneside,3,North East B
394,Sunderland,3,North East B
889,Blackburn with Darwen,6,North West A
890,Blackpool,6,North West A
942,Cumberland,6,North West A
943,Westmorland and Furness ,6,North West A
888,Lancashire,6,North West A
350,Bolton,7,North West B
351,Bury,7,North West B
352,Manchester,7,North West B
353,Oldham,7,North West B
354,Rochdale,7,North West B
355,Salford,7,North West B
356,Stockport,7,North West B
357,Tameside,7,North West B
358,Trafford,7,North West B
359,Wigan,7,North West B
895,Cheshire East,9,North West C
896,Cheshire West and Chester,9,North West C
876,Halton,9,North West C
340,Knowsley,9,North West C
341,Liverpool,9,North West C
343,Sefton,9,North West C
342,St. Helens,9,North West C
877,Warrington,9,North West C
344,Wirral,9,North West C
811,East Riding of Yorkshire,10,North Yorkshire and The Humber
810,"Kingston Upon Hull, City of",10,North Yorkshire and The Humber
812,North East Lincolnshire,10,North Yorkshire and The Humber
813,North Lincolnshire,10,North Yorkshire and The Humber
815,North Yorkshire,10,North Yorkshire and The Humber
816,York,10,North Yorkshire and The Humber
370,Barnsley,12,South and West Yorkshire
380,Bradford,12,South and West Yorkshire
381,Calderdale,12,South and West Yorkshire
371,Doncaster,12,South and West Yorkshire
382,Kirklees,12,South and West Yorkshire
383,Leeds,12,South and West Yorkshire
372,Rotherham,12,South and West Yorkshire
373,Sheffield,12,South and West Yorkshire
384,Wakefield,12,South and West Yorkshire
831,Derby,14,East Midlands A
830,Derbyshire,14,East Midlands A
892,Nottingham,14,East Midlands A
891,Nottinghamshire,14,East Midlands A
856,Leicester,16,East Midlands B
855,Leicestershire,16,East Midlands B
925,Lincolnshire,16,East Midlands B
940,North Northamptonshire,16,East Midlands B
941,West Northamptonshire,16,East Midlands B
857,Rutland,16,East Midlands B
893,Shropshire,20,West Midlands A
860,Staffordshire,20,West Midlands A
861,Stoke-on-Trent,20,West Midlands A
894,Telford and Wrekin,20,West Midlands A
884,"Herefordshire, County of",22,West Midlands B
885,Worcestershire,22,West Midlands B
330,Birmingham,24,West Midlands C
331,Coventry,24,West Midlands C
332,Dudley,24,West Midlands C
333,Sandwell,24,West Midlands C
334,Solihull,24,West Midlands C
335,Walsall,24,West Midlands C
937,Warwickshire,24,West Midlands C
336,Wolverhampton,24,West Midlands C
822,Bedford,25,East of England A
873,Cambridgeshire,25,East of England A
823,Central Bedfordshire,25,East of England A
919,Hertfordshire,25,East of England A
821,Luton,25,East of England A
874,Peterborough,25,East of England A
881,Essex,27,East of England B
926,Norfolk,27,East of England B
882,Southend-on-Sea,27,East of England B
935,Suffolk,27,East of England B
883,Thurrock,27,East of England B
202,Camden,31,London Central
206,Islington,31,London Central
207,Kensington and Chelsea,31,London Central
208,Lambeth,31,London Central
210,Southwark,31,London Central
212,Wandsworth,31,London Central
213,Westminster,31,London Central
301,Barking and Dagenham,32,London East
303,Bexley,32,London East
201,City of London,32,London East
203,Greenwich,32,London East
204,Hackney,32,London East
311,Havering,32,London East
209,Lewisham,32,London East
316,Newham,32,London East
317,Redbridge,32,London East
211,Tower Hamlets,32,London East
302,Barnet,33,London North
308,Enfield,33,London North
309,Haringey,33,London North
320,Waltham Forest,33,London North
305,Bromley,34,London South
306,Croydon,34,London South
314,Kingston upon Thames,34,London South
315,Merton,34,London South
318,Richmond upon Thames,34,London South
319,Sutton,34,London South
304,Brent,35,London West
307,Ealing,35,London West
205,Hammersmith and Fulham,35,London West
310,Harrow,35,London West
312,Hillingdon,35,London West
313,Hounslow,35,London West
867,Bracknell Forest,36,South East A
825,Buckinghamshire,36,South East A
826,Milton Keynes,36,South East A
931,Oxfordshire,36,South East A
870,Reading,36,South East A
871,Slough,36,South East A
869,West Berkshire,36,South East A
868,Windsor and Maidenhead,36,South East A
872,Wokingham,36,South East A
850,Hampshire,37,South East B
921,Isle of Wight,37,South East B
851,Portsmouth,37,South East B
852,Southampton,37,South East B
936,Surrey,38,South East C
938,West Sussex,38,South East C
846,Brighton and Hove,39,South East D
845,East Sussex,39,South East D
886,Kent,39,South East D
887,Medway,39,South East D
839,"Bournemouth, Christchurch and Poole",43,South West A
908,Cornwall,43,South West A
878,Devon,43,South West A
838,Dorset,43,South West A
420,Isles of Scilly,43,South West A
879,Plymouth,43,South West A
933,Somerset,43,South West A
880,Torbay,43,South West A
800,Bath and North East Somerset,45,South West B
801,"Bristol, City of",45,South West B
916,Gloucestershire,45,South West B
802,North Somerset,45,South West B
803,South Gloucestershire,45,South West B
866,Swindon,45,South West B
865,Wiltshire,45,South West B
1 LEA LA Name REGION REGION NAME
2 841 Darlington 1 North East A
3 840 County Durham 1 North East A
4 805 Hartlepool 1 North East A
5 806 Middlesbrough 1 North East A
6 807 Redcar and Cleveland 1 North East A
7 808 Stockton-on-Tees 1 North East A
8 390 Gateshead 3 North East B
9 391 Newcastle upon Tyne 3 North East B
10 392 North Tyneside 3 North East B
11 929 Northumberland 3 North East B
12 393 South Tyneside 3 North East B
13 394 Sunderland 3 North East B
14 889 Blackburn with Darwen 6 North West A
15 890 Blackpool 6 North West A
16 942 Cumberland 6 North West A
17 943 Westmorland and Furness 6 North West A
18 888 Lancashire 6 North West A
19 350 Bolton 7 North West B
20 351 Bury 7 North West B
21 352 Manchester 7 North West B
22 353 Oldham 7 North West B
23 354 Rochdale 7 North West B
24 355 Salford 7 North West B
25 356 Stockport 7 North West B
26 357 Tameside 7 North West B
27 358 Trafford 7 North West B
28 359 Wigan 7 North West B
29 895 Cheshire East 9 North West C
30 896 Cheshire West and Chester 9 North West C
31 876 Halton 9 North West C
32 340 Knowsley 9 North West C
33 341 Liverpool 9 North West C
34 343 Sefton 9 North West C
35 342 St. Helens 9 North West C
36 877 Warrington 9 North West C
37 344 Wirral 9 North West C
38 811 East Riding of Yorkshire 10 North Yorkshire and The Humber
39 810 Kingston Upon Hull, City of 10 North Yorkshire and The Humber
40 812 North East Lincolnshire 10 North Yorkshire and The Humber
41 813 North Lincolnshire 10 North Yorkshire and The Humber
42 815 North Yorkshire 10 North Yorkshire and The Humber
43 816 York 10 North Yorkshire and The Humber
44 370 Barnsley 12 South and West Yorkshire
45 380 Bradford 12 South and West Yorkshire
46 381 Calderdale 12 South and West Yorkshire
47 371 Doncaster 12 South and West Yorkshire
48 382 Kirklees 12 South and West Yorkshire
49 383 Leeds 12 South and West Yorkshire
50 372 Rotherham 12 South and West Yorkshire
51 373 Sheffield 12 South and West Yorkshire
52 384 Wakefield 12 South and West Yorkshire
53 831 Derby 14 East Midlands A
54 830 Derbyshire 14 East Midlands A
55 892 Nottingham 14 East Midlands A
56 891 Nottinghamshire 14 East Midlands A
57 856 Leicester 16 East Midlands B
58 855 Leicestershire 16 East Midlands B
59 925 Lincolnshire 16 East Midlands B
60 940 North Northamptonshire 16 East Midlands B
61 941 West Northamptonshire 16 East Midlands B
62 857 Rutland 16 East Midlands B
63 893 Shropshire 20 West Midlands A
64 860 Staffordshire 20 West Midlands A
65 861 Stoke-on-Trent 20 West Midlands A
66 894 Telford and Wrekin 20 West Midlands A
67 884 Herefordshire, County of 22 West Midlands B
68 885 Worcestershire 22 West Midlands B
69 330 Birmingham 24 West Midlands C
70 331 Coventry 24 West Midlands C
71 332 Dudley 24 West Midlands C
72 333 Sandwell 24 West Midlands C
73 334 Solihull 24 West Midlands C
74 335 Walsall 24 West Midlands C
75 937 Warwickshire 24 West Midlands C
76 336 Wolverhampton 24 West Midlands C
77 822 Bedford 25 East of England A
78 873 Cambridgeshire 25 East of England A
79 823 Central Bedfordshire 25 East of England A
80 919 Hertfordshire 25 East of England A
81 821 Luton 25 East of England A
82 874 Peterborough 25 East of England A
83 881 Essex 27 East of England B
84 926 Norfolk 27 East of England B
85 882 Southend-on-Sea 27 East of England B
86 935 Suffolk 27 East of England B
87 883 Thurrock 27 East of England B
88 202 Camden 31 London Central
89 206 Islington 31 London Central
90 207 Kensington and Chelsea 31 London Central
91 208 Lambeth 31 London Central
92 210 Southwark 31 London Central
93 212 Wandsworth 31 London Central
94 213 Westminster 31 London Central
95 301 Barking and Dagenham 32 London East
96 303 Bexley 32 London East
97 201 City of London 32 London East
98 203 Greenwich 32 London East
99 204 Hackney 32 London East
100 311 Havering 32 London East
101 209 Lewisham 32 London East
102 316 Newham 32 London East
103 317 Redbridge 32 London East
104 211 Tower Hamlets 32 London East
105 302 Barnet 33 London North
106 308 Enfield 33 London North
107 309 Haringey 33 London North
108 320 Waltham Forest 33 London North
109 305 Bromley 34 London South
110 306 Croydon 34 London South
111 314 Kingston upon Thames 34 London South
112 315 Merton 34 London South
113 318 Richmond upon Thames 34 London South
114 319 Sutton 34 London South
115 304 Brent 35 London West
116 307 Ealing 35 London West
117 205 Hammersmith and Fulham 35 London West
118 310 Harrow 35 London West
119 312 Hillingdon 35 London West
120 313 Hounslow 35 London West
121 867 Bracknell Forest 36 South East A
122 825 Buckinghamshire 36 South East A
123 826 Milton Keynes 36 South East A
124 931 Oxfordshire 36 South East A
125 870 Reading 36 South East A
126 871 Slough 36 South East A
127 869 West Berkshire 36 South East A
128 868 Windsor and Maidenhead 36 South East A
129 872 Wokingham 36 South East A
130 850 Hampshire 37 South East B
131 921 Isle of Wight 37 South East B
132 851 Portsmouth 37 South East B
133 852 Southampton 37 South East B
134 936 Surrey 38 South East C
135 938 West Sussex 38 South East C
136 846 Brighton and Hove 39 South East D
137 845 East Sussex 39 South East D
138 886 Kent 39 South East D
139 887 Medway 39 South East D
140 839 Bournemouth, Christchurch and Poole 43 South West A
141 908 Cornwall 43 South West A
142 878 Devon 43 South West A
143 838 Dorset 43 South West A
144 420 Isles of Scilly 43 South West A
145 879 Plymouth 43 South West A
146 933 Somerset 43 South West A
147 880 Torbay 43 South West A
148 800 Bath and North East Somerset 45 South West B
149 801 Bristol, City of 45 South West B
150 916 Gloucestershire 45 South West B
151 802 North Somerset 45 South West B
152 803 South Gloucestershire 45 South West B
153 866 Swindon 45 South West B
154 865 Wiltshire 45 South West B

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
services:
app:
build: .
ports:
- "80:80"
volumes:
# Mount data directory for easy updates without rebuilding
- ./data:/app/data:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/api/data-info"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

831
frontend/app.js Normal file
View File

@@ -0,0 +1,831 @@
/**
* School Performance Compass - Frontend Application
* Interactive UK School Data Visualization
*/
const API_BASE = '';
// State
let allSchools = [];
let selectedSchools = [];
let comparisonChart = null;
let schoolDetailChart = null;
let currentSchoolData = null;
// Chart colors
const CHART_COLORS = [
'#e07256', // coral
'#2d7d7d', // teal
'#c9a227', // gold
'#7b68a6', // purple
'#3498db', // blue
'#27ae60', // green
'#e74c3c', // red
'#9b59b6', // violet
];
// DOM Elements
const elements = {
schoolSearch: document.getElementById('school-search'),
localAuthorityFilter: document.getElementById('local-authority-filter'),
typeFilter: document.getElementById('type-filter'),
schoolsGrid: document.getElementById('schools-grid'),
compareSearch: document.getElementById('compare-search'),
compareResults: document.getElementById('compare-results'),
selectedSchools: document.getElementById('selected-schools'),
chartsSection: document.getElementById('charts-section'),
metricSelect: document.getElementById('metric-select'),
comparisonChart: document.getElementById('comparison-chart'),
comparisonTable: document.getElementById('comparison-table'),
tableHeader: document.getElementById('table-header'),
tableBody: document.getElementById('table-body'),
rankingMetric: document.getElementById('ranking-metric'),
rankingYear: document.getElementById('ranking-year'),
rankingsList: document.getElementById('rankings-list'),
modal: document.getElementById('school-modal'),
modalClose: document.getElementById('modal-close'),
modalSchoolName: document.getElementById('modal-school-name'),
modalMeta: document.getElementById('modal-meta'),
modalStats: document.getElementById('modal-stats'),
schoolDetailChart: document.getElementById('school-detail-chart'),
addToCompare: document.getElementById('add-to-compare'),
};
// Initialize
document.addEventListener('DOMContentLoaded', init);
async function init() {
await loadFilters();
await loadSchools();
await loadRankingYears();
await loadRankings();
setupEventListeners();
}
// API Functions
async function fetchAPI(endpoint) {
try {
const response = await fetch(`${API_BASE}${endpoint}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error(`API Error (${endpoint}):`, error);
return null;
}
}
async function loadFilters() {
const data = await fetchAPI('/api/filters');
if (!data) return;
// Populate school type filter
data.school_types.forEach(type => {
const option = document.createElement('option');
option.value = type;
option.textContent = type;
elements.typeFilter.appendChild(option);
});
}
async function loadSchools() {
const params = new URLSearchParams();
const search = elements.schoolSearch.value.trim();
if (search) params.append('search', search);
const localAuthority = elements.localAuthorityFilter.value;
if (localAuthority) params.append('local_authority', localAuthority);
const type = elements.typeFilter.value;
if (type) params.append('school_type', type);
const queryString = params.toString();
const endpoint = `/api/schools${queryString ? '?' + queryString : ''}`;
const data = await fetchAPI(endpoint);
if (!data) {
showEmptyState(elements.schoolsGrid, 'Unable to load schools');
return;
}
allSchools = data.schools;
renderSchools(allSchools);
}
async function loadSchoolDetails(urn) {
const data = await fetchAPI(`/api/schools/${urn}`);
return data;
}
async function loadComparison() {
if (selectedSchools.length === 0) return null;
const urns = selectedSchools.map(s => s.urn).join(',');
const data = await fetchAPI(`/api/compare?urns=${urns}`);
return data;
}
async function loadRankingYears() {
const data = await fetchAPI('/api/filters');
if (!data) return;
elements.rankingYear.innerHTML = '';
data.years.sort((a, b) => b - a).forEach(year => {
const option = document.createElement('option');
option.value = year;
option.textContent = `${year}`;
elements.rankingYear.appendChild(option);
});
}
async function loadRankings() {
const metric = elements.rankingMetric.value;
const year = elements.rankingYear.value;
let endpoint = `/api/rankings?metric=${metric}&limit=20`;
if (year) endpoint += `&year=${year}`;
const data = await fetchAPI(endpoint);
if (!data) {
showEmptyState(elements.rankingsList, 'Unable to load rankings');
return;
}
renderRankings(data.rankings, metric);
}
// Render Functions
function renderSchools(schools) {
if (schools.length === 0) {
showEmptyState(elements.schoolsGrid, 'No primary schools found matching your criteria');
return;
}
elements.schoolsGrid.innerHTML = schools.map(school => `
<div class="school-card" data-urn="${school.urn}">
<h3 class="school-name">${escapeHtml(school.school_name)}</h3>
<div class="school-meta">
<span class="school-tag">${escapeHtml(school.local_authority || '')}</span>
<span class="school-tag type">${escapeHtml(school.school_type || '')}</span>
</div>
<div class="school-address">${escapeHtml(school.address || '')}</div>
<div class="school-stats">
<div class="stat">
<div class="stat-value">Primary</div>
<div class="stat-label">Phase</div>
</div>
<div class="stat">
<div class="stat-value">KS2</div>
<div class="stat-label">Data</div>
</div>
</div>
</div>
`).join('');
// Add click handlers
elements.schoolsGrid.querySelectorAll('.school-card').forEach(card => {
card.addEventListener('click', () => {
const urn = parseInt(card.dataset.urn);
openSchoolModal(urn);
});
});
}
function renderRankings(rankings, metric) {
if (rankings.length === 0) {
showEmptyState(elements.rankingsList, 'No ranking data available for this year/metric');
return;
}
const metricLabels = {
// Expected
rwm_expected_pct: 'RWM %',
reading_expected_pct: 'Reading %',
writing_expected_pct: 'Writing %',
maths_expected_pct: 'Maths %',
gps_expected_pct: 'GPS %',
science_expected_pct: 'Science %',
// Higher
rwm_high_pct: 'RWM Higher %',
reading_high_pct: 'Reading Higher %',
writing_high_pct: 'Writing Higher %',
maths_high_pct: 'Maths Higher %',
gps_high_pct: 'GPS Higher %',
// Progress
reading_progress: 'Reading Progress',
writing_progress: 'Writing Progress',
maths_progress: 'Maths Progress',
// Averages
reading_avg_score: 'Reading Avg',
maths_avg_score: 'Maths Avg',
gps_avg_score: 'GPS Avg',
// Gender
rwm_expected_boys_pct: 'Boys RWM %',
rwm_expected_girls_pct: 'Girls RWM %',
rwm_high_boys_pct: 'Boys Higher %',
rwm_high_girls_pct: 'Girls Higher %',
// Equity
rwm_expected_disadvantaged_pct: 'Disadvantaged %',
rwm_expected_non_disadvantaged_pct: 'Non-Disadv %',
disadvantaged_gap: 'Disadv Gap',
// Context
disadvantaged_pct: '% Disadvantaged',
eal_pct: '% EAL',
sen_support_pct: '% SEN',
stability_pct: '% Stable',
// 3-Year
rwm_expected_3yr_pct: 'RWM 3yr %',
reading_avg_3yr: 'Reading 3yr',
maths_avg_3yr: 'Maths 3yr',
};
elements.rankingsList.innerHTML = rankings.map((school, index) => {
const value = school[metric];
if (value === null || value === undefined) return '';
const isProgress = metric.includes('progress');
const isScore = metric.includes('_avg_');
let displayValue;
if (isProgress) {
displayValue = (value >= 0 ? '+' : '') + value.toFixed(1);
} else if (isScore) {
displayValue = value.toFixed(0);
} else {
displayValue = value.toFixed(0) + '%';
}
return `
<div class="ranking-item" data-urn="${school.urn}">
<div class="ranking-position ${index < 3 ? 'top-3' : ''}">${index + 1}</div>
<div class="ranking-info">
<div class="ranking-name">${escapeHtml(school.school_name)}</div>
<div class="ranking-location">${escapeHtml(school.local_authority || '')}</div>
</div>
<div class="ranking-score">
<div class="ranking-score-value">${displayValue}</div>
<div class="ranking-score-label">${metricLabels[metric] || metric}</div>
</div>
</div>
`;
}).filter(Boolean).join('');
// Add click handlers
elements.rankingsList.querySelectorAll('.ranking-item').forEach(item => {
item.addEventListener('click', () => {
const urn = parseInt(item.dataset.urn);
openSchoolModal(urn);
});
});
}
function renderSelectedSchools() {
if (selectedSchools.length === 0) {
elements.selectedSchools.innerHTML = `
<div class="empty-selection">
<div class="empty-icon">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="6" y="10" width="36" height="28" rx="2"/>
<path d="M6 18h36"/>
<circle cx="14" cy="14" r="2" fill="currentColor"/>
<circle cx="22" cy="14" r="2" fill="currentColor"/>
</svg>
</div>
<p>Search and add schools to compare</p>
</div>
`;
elements.chartsSection.style.display = 'none';
return;
}
elements.selectedSchools.innerHTML = selectedSchools.map((school, index) => `
<div class="selected-school-tag" style="border-left: 3px solid ${CHART_COLORS[index % CHART_COLORS.length]}">
<span>${escapeHtml(school.school_name)}</span>
<button class="remove" data-urn="${school.urn}" title="Remove">
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 4L4 12M4 4l8 8"/>
</svg>
</button>
</div>
`).join('');
// Add remove handlers
elements.selectedSchools.querySelectorAll('.remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const urn = parseInt(btn.dataset.urn);
removeFromComparison(urn);
});
});
elements.chartsSection.style.display = 'block';
updateComparisonChart();
}
async function updateComparisonChart() {
if (selectedSchools.length === 0) return;
const data = await loadComparison();
if (!data) return;
const metric = elements.metricSelect.value;
const metricLabels = {
// Expected Standard
rwm_expected_pct: 'Reading, Writing & Maths Combined (%)',
reading_expected_pct: 'Reading Expected Standard (%)',
writing_expected_pct: 'Writing Expected Standard (%)',
maths_expected_pct: 'Maths Expected Standard (%)',
gps_expected_pct: 'GPS Expected Standard (%)',
science_expected_pct: 'Science Expected Standard (%)',
// Higher Standard
rwm_high_pct: 'RWM Combined Higher Standard (%)',
reading_high_pct: 'Reading Higher Standard (%)',
writing_high_pct: 'Writing Greater Depth (%)',
maths_high_pct: 'Maths Higher Standard (%)',
gps_high_pct: 'GPS Higher Standard (%)',
// Progress
reading_progress: 'Reading Progress Score',
writing_progress: 'Writing Progress Score',
maths_progress: 'Maths Progress Score',
// Averages
reading_avg_score: 'Reading Average Scaled Score',
maths_avg_score: 'Maths Average Scaled Score',
gps_avg_score: 'GPS Average Scaled Score',
// Gender
rwm_expected_boys_pct: 'RWM Expected % (Boys)',
rwm_expected_girls_pct: 'RWM Expected % (Girls)',
rwm_high_boys_pct: 'RWM Higher % (Boys)',
rwm_high_girls_pct: 'RWM Higher % (Girls)',
// Equity
rwm_expected_disadvantaged_pct: 'RWM Expected % (Disadvantaged)',
rwm_expected_non_disadvantaged_pct: 'RWM Expected % (Non-Disadvantaged)',
disadvantaged_gap: 'Disadvantaged Gap vs National',
// Context
disadvantaged_pct: '% Disadvantaged Pupils',
eal_pct: '% EAL Pupils',
sen_support_pct: '% SEN Support',
stability_pct: '% Pupil Stability',
// 3-Year
rwm_expected_3yr_pct: 'RWM Expected % (3-Year Avg)',
reading_avg_3yr: 'Reading Score (3-Year Avg)',
maths_avg_3yr: 'Maths Score (3-Year Avg)',
};
// Prepare chart data - iterate in same order as selectedSchools for color consistency
const datasets = [];
const allYears = new Set();
selectedSchools.forEach((school, index) => {
const schoolData = data.comparison[school.urn];
if (!schoolData) return;
const yearlyData = schoolData.yearly_data;
yearlyData.forEach(d => allYears.add(d.year));
const sortedData = yearlyData.sort((a, b) => a.year - b.year);
datasets.push({
label: schoolData.school_info.school_name,
data: sortedData.map(d => ({ x: d.year, y: d[metric] })),
borderColor: CHART_COLORS[index % CHART_COLORS.length],
backgroundColor: CHART_COLORS[index % CHART_COLORS.length] + '20',
borderWidth: 3,
pointRadius: 5,
pointHoverRadius: 7,
tension: 0.3,
fill: false,
});
});
const years = Array.from(allYears).sort();
// Destroy existing chart
if (comparisonChart) {
comparisonChart.destroy();
}
// Create new chart
const ctx = elements.comparisonChart.getContext('2d');
comparisonChart = new Chart(ctx, {
type: 'line',
data: {
labels: years,
datasets: datasets,
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2,
plugins: {
legend: {
position: 'bottom',
labels: {
font: { family: "'DM Sans', sans-serif", size: 12 },
padding: 20,
usePointStyle: true,
},
},
title: {
display: true,
text: metricLabels[metric],
font: { family: "'Playfair Display', serif", size: 18, weight: 600 },
padding: { bottom: 20 },
},
tooltip: {
backgroundColor: '#1a1612',
titleFont: { family: "'DM Sans', sans-serif" },
bodyFont: { family: "'DM Sans', sans-serif" },
padding: 12,
cornerRadius: 8,
},
},
scales: {
x: {
title: {
display: true,
text: 'Academic Year',
font: { family: "'DM Sans', sans-serif", weight: 500 },
},
grid: { display: false },
},
y: {
title: {
display: true,
text: metricLabels[metric],
font: { family: "'DM Sans', sans-serif", weight: 500 },
},
grid: { color: '#e5dfd5' },
},
},
interaction: {
intersect: false,
mode: 'index',
},
},
});
// Update comparison table
updateComparisonTable(data.comparison, metric, years);
}
function updateComparisonTable(comparison, metric, years) {
// Build header
let headerHtml = '<th>School</th>';
years.forEach(year => {
headerHtml += `<th>${year}</th>`;
});
headerHtml += '<th>Change</th>';
elements.tableHeader.innerHTML = headerHtml;
// Build body - iterate in same order as selectedSchools for color consistency
let bodyHtml = '';
selectedSchools.forEach((school, index) => {
const schoolData = comparison[school.urn];
if (!schoolData) return;
const yearlyMap = {};
schoolData.yearly_data.forEach(d => {
yearlyMap[d.year] = d[metric];
});
const firstValue = yearlyMap[years[0]];
const lastValue = yearlyMap[years[years.length - 1]];
const change = firstValue && lastValue ? (lastValue - firstValue).toFixed(2) : 'N/A';
const changeClass = parseFloat(change) >= 0 ? 'positive' : 'negative';
const color = CHART_COLORS[index % CHART_COLORS.length];
bodyHtml += `<tr>`;
bodyHtml += `<td><strong style="border-left: 3px solid ${color}; padding-left: 8px;">${escapeHtml(schoolData.school_info.school_name)}</strong></td>`;
years.forEach(year => {
const value = yearlyMap[year];
bodyHtml += `<td>${value !== undefined ? formatMetricValue(value, metric) : '-'}</td>`;
});
bodyHtml += `<td class="${changeClass}">${change !== 'N/A' ? (parseFloat(change) >= 0 ? '+' : '') + change : change}</td>`;
bodyHtml += `</tr>`;
});
elements.tableBody.innerHTML = bodyHtml;
}
function formatMetricValue(value, metric) {
if (value === null || value === undefined) return '-';
if (metric.includes('progress')) {
return (value >= 0 ? '+' : '') + value.toFixed(1);
}
if (metric.includes('pct')) {
return value.toFixed(0) + '%';
}
return value.toFixed(1);
}
async function openSchoolModal(urn) {
const data = await loadSchoolDetails(urn);
if (!data) return;
currentSchoolData = data;
elements.modalSchoolName.textContent = data.school_info.school_name;
elements.modalMeta.innerHTML = `
<span class="school-tag">${escapeHtml(data.school_info.local_authority || '')}</span>
<span class="school-tag type">${escapeHtml(data.school_info.school_type || '')}</span>
<span class="school-tag">Primary</span>
`;
// Get latest year data with actual results (skip 2021 - no SATs)
const sortedData = data.yearly_data.sort((a, b) => b.year - a.year);
const latest = sortedData.find(d => d.rwm_expected_pct !== null) || sortedData[0];
elements.modalStats.innerHTML = `
<div class="modal-stats-section">
<h4>KS2 Results (${latest.year})</h4>
<div class="modal-stats-grid">
<div class="modal-stat">
<div class="modal-stat-value">${formatMetricValue(latest.rwm_expected_pct, 'rwm_expected_pct')}</div>
<div class="modal-stat-label">RWM Expected</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value">${formatMetricValue(latest.rwm_high_pct, 'rwm_high_pct')}</div>
<div class="modal-stat-label">RWM Higher</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value">${formatMetricValue(latest.gps_expected_pct, 'gps_expected_pct')}</div>
<div class="modal-stat-label">GPS Expected</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value">${formatMetricValue(latest.science_expected_pct, 'science_expected_pct')}</div>
<div class="modal-stat-label">Science Expected</div>
</div>
</div>
</div>
<div class="modal-stats-section">
<h4>Progress Scores</h4>
<div class="modal-stats-grid">
<div class="modal-stat">
<div class="modal-stat-value ${getProgressClass(latest.reading_progress)}">${formatMetricValue(latest.reading_progress, 'reading_progress')}</div>
<div class="modal-stat-label">Reading</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value ${getProgressClass(latest.writing_progress)}">${formatMetricValue(latest.writing_progress, 'writing_progress')}</div>
<div class="modal-stat-label">Writing</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value ${getProgressClass(latest.maths_progress)}">${formatMetricValue(latest.maths_progress, 'maths_progress')}</div>
<div class="modal-stat-label">Maths</div>
</div>
</div>
</div>
<div class="modal-stats-section">
<h4>School Context</h4>
<div class="modal-stats-grid">
<div class="modal-stat">
<div class="modal-stat-value">${latest.total_pupils || '-'}</div>
<div class="modal-stat-label">Total Pupils</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value">${formatMetricValue(latest.disadvantaged_pct, 'disadvantaged_pct')}</div>
<div class="modal-stat-label">% Disadvantaged</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value">${formatMetricValue(latest.eal_pct, 'eal_pct')}</div>
<div class="modal-stat-label">% EAL</div>
</div>
<div class="modal-stat">
<div class="modal-stat-value">${formatMetricValue(latest.sen_support_pct, 'sen_support_pct')}</div>
<div class="modal-stat-label">% SEN Support</div>
</div>
</div>
</div>
`;
function getProgressClass(value) {
if (value === null || value === undefined) return '';
return value >= 0 ? 'positive' : 'negative';
}
// Create chart - filter out years with no data (2021)
if (schoolDetailChart) {
schoolDetailChart.destroy();
}
const validData = sortedData.filter(d => d.rwm_expected_pct !== null).reverse();
const years = validData.map(d => d.year);
const ctx = elements.schoolDetailChart.getContext('2d');
schoolDetailChart = new Chart(ctx, {
type: 'bar',
data: {
labels: years,
datasets: [
{
label: 'Reading %',
data: validData.map(d => d.reading_expected_pct),
backgroundColor: '#2d7d7d',
borderRadius: 4,
},
{
label: 'Writing %',
data: validData.map(d => d.writing_expected_pct),
backgroundColor: '#c9a227',
borderRadius: 4,
},
{
label: 'Maths %',
data: validData.map(d => d.maths_expected_pct),
backgroundColor: '#e07256',
borderRadius: 4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2,
plugins: {
legend: {
position: 'bottom',
labels: {
font: { family: "'DM Sans', sans-serif" },
usePointStyle: true,
},
},
title: {
display: true,
text: 'KS2 Attainment Over Time (% meeting expected standard)',
font: { family: "'Playfair Display', serif", size: 16, weight: 600 },
},
},
scales: {
y: {
beginAtZero: true,
max: 100,
grid: { color: '#e5dfd5' },
},
x: {
grid: { display: false },
},
},
},
});
// Update add to compare button
const isSelected = selectedSchools.some(s => s.urn === data.school_info.urn);
elements.addToCompare.textContent = isSelected ? 'Remove from Compare' : 'Add to Compare';
elements.addToCompare.dataset.urn = data.school_info.urn;
elements.modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal() {
elements.modal.classList.remove('active');
document.body.style.overflow = '';
currentSchoolData = null;
}
function addToComparison(school) {
if (selectedSchools.some(s => s.urn === school.urn)) return;
if (selectedSchools.length >= 5) {
alert('Maximum 5 schools can be compared at once');
return;
}
selectedSchools.push(school);
renderSelectedSchools();
}
function removeFromComparison(urn) {
selectedSchools = selectedSchools.filter(s => s.urn !== urn);
renderSelectedSchools();
}
function showEmptyState(container, message) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="24" cy="24" r="20"/>
<path d="M16 20h16M16 28h10"/>
</svg>
<p>${escapeHtml(message)}</p>
</div>
`;
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Event Listeners
function setupEventListeners() {
// Navigation
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const view = link.dataset.view;
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById(`${view}-view`).classList.add('active');
});
});
// Search and filters
let searchTimeout;
elements.schoolSearch.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(loadSchools, 300);
});
elements.localAuthorityFilter.addEventListener('change', loadSchools);
elements.typeFilter.addEventListener('change', loadSchools);
// Compare search
let compareSearchTimeout;
elements.compareSearch.addEventListener('input', async () => {
clearTimeout(compareSearchTimeout);
const query = elements.compareSearch.value.trim();
if (query.length < 2) {
elements.compareResults.classList.remove('active');
return;
}
compareSearchTimeout = setTimeout(async () => {
const data = await fetchAPI(`/api/schools?search=${encodeURIComponent(query)}`);
if (!data) return;
const results = data.schools.filter(s => !selectedSchools.some(sel => sel.urn === s.urn));
if (results.length === 0) {
elements.compareResults.innerHTML = '<div class="compare-result-item"><span class="name">No schools found</span></div>';
} else {
elements.compareResults.innerHTML = results.slice(0, 10).map(school => `
<div class="compare-result-item" data-urn="${school.urn}" data-name="${escapeHtml(school.school_name)}">
<div class="name">${escapeHtml(school.school_name)}</div>
<div class="location">${escapeHtml(school.local_authority || '')}${school.postcode ? ' • ' + escapeHtml(school.postcode) : ''}</div>
</div>
`).join('');
elements.compareResults.querySelectorAll('.compare-result-item').forEach(item => {
item.addEventListener('click', () => {
const urn = parseInt(item.dataset.urn);
const school = data.schools.find(s => s.urn === urn);
if (school) {
addToComparison(school);
elements.compareSearch.value = '';
elements.compareResults.classList.remove('active');
}
});
});
}
elements.compareResults.classList.add('active');
}, 300);
});
elements.compareSearch.addEventListener('blur', () => {
setTimeout(() => elements.compareResults.classList.remove('active'), 200);
});
elements.compareSearch.addEventListener('focus', () => {
if (elements.compareSearch.value.trim().length >= 2) {
elements.compareResults.classList.add('active');
}
});
// Metric selector
elements.metricSelect.addEventListener('change', updateComparisonChart);
// Rankings
elements.rankingMetric.addEventListener('change', loadRankings);
elements.rankingYear.addEventListener('change', loadRankings);
// Modal
elements.modalClose.addEventListener('click', closeModal);
elements.modal.querySelector('.modal-backdrop').addEventListener('click', closeModal);
elements.addToCompare.addEventListener('click', () => {
if (!currentSchoolData) return;
const urn = currentSchoolData.school_info.urn;
const isSelected = selectedSchools.some(s => s.urn === urn);
if (isSelected) {
removeFromComparison(urn);
elements.addToCompare.textContent = 'Add to Compare';
} else {
addToComparison(currentSchoolData.school_info);
elements.addToCompare.textContent = 'Remove from Compare';
}
});
// Keyboard
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
}

261
frontend/index.html Normal file
View File

@@ -0,0 +1,261 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Primary School Compass | Wandsworth & Merton</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="noise-overlay"></div>
<header class="header">
<div class="header-content">
<div class="logo">
<div class="logo-icon">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="18" stroke="currentColor" stroke-width="2"/>
<path d="M20 8L20 32M12 14L28 14M10 20L30 20M12 26L28 26" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="20" cy="20" r="4" fill="currentColor"/>
</svg>
</div>
<div class="logo-text">
<span class="logo-title">Primary School Compass</span>
<span class="logo-subtitle">Wandsworth & Merton</span>
</div>
</div>
<nav class="nav">
<a href="#" class="nav-link active" data-view="dashboard">Dashboard</a>
<a href="#" class="nav-link" data-view="compare">Compare</a>
<a href="#" class="nav-link" data-view="rankings">Rankings</a>
</nav>
</div>
</header>
<main class="main">
<!-- Dashboard View -->
<section id="dashboard-view" class="view active">
<div class="hero">
<h1 class="hero-title">Primary Schools in Wandsworth & Merton</h1>
<p class="hero-subtitle">Compare KS2 performance data from the last 5 years across local primary schools</p>
</div>
<div class="search-section">
<div class="search-container">
<input type="text" id="school-search" class="search-input" placeholder="Search primary schools by name...">
<div class="search-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
</div>
</div>
<div class="filter-row">
<select id="local-authority-filter" class="filter-select">
<option value="">All Areas</option>
<option value="Wandsworth">Wandsworth</option>
<option value="Merton">Merton</option>
</select>
<select id="type-filter" class="filter-select">
<option value="">All School Types</option>
</select>
</div>
</div>
<div class="schools-grid" id="schools-grid">
<!-- School cards populated by JS -->
</div>
</section>
<!-- Compare View -->
<section id="compare-view" class="view">
<div class="compare-header">
<h2 class="section-title">Compare Primary Schools</h2>
<p class="section-subtitle">Select schools to compare their KS2 performance over time</p>
</div>
<div class="selected-schools" id="selected-schools">
<div class="empty-selection">
<div class="empty-icon">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="6" y="10" width="36" height="28" rx="2"/>
<path d="M6 18h36"/>
<circle cx="14" cy="14" r="2" fill="currentColor"/>
<circle cx="22" cy="14" r="2" fill="currentColor"/>
</svg>
</div>
<p>Search and add schools to compare</p>
</div>
</div>
<div class="compare-search-section">
<input type="text" id="compare-search" class="search-input" placeholder="Add a school to compare...">
<div id="compare-results" class="compare-results"></div>
</div>
<div class="charts-section" id="charts-section" style="display: none;">
<div class="metric-selector">
<label>Select KS2 Metric:</label>
<select id="metric-select" class="filter-select">
<optgroup label="Expected Standard">
<option value="rwm_expected_pct">Reading, Writing & Maths Combined %</option>
<option value="reading_expected_pct">Reading Expected %</option>
<option value="writing_expected_pct">Writing Expected %</option>
<option value="maths_expected_pct">Maths Expected %</option>
<option value="gps_expected_pct">GPS Expected %</option>
<option value="science_expected_pct">Science Expected %</option>
</optgroup>
<optgroup label="Higher Standard">
<option value="rwm_high_pct">RWM Combined Higher %</option>
<option value="reading_high_pct">Reading Higher %</option>
<option value="writing_high_pct">Writing Higher %</option>
<option value="maths_high_pct">Maths Higher %</option>
<option value="gps_high_pct">GPS Higher %</option>
</optgroup>
<optgroup label="Progress Scores">
<option value="reading_progress">Reading Progress</option>
<option value="writing_progress">Writing Progress</option>
<option value="maths_progress">Maths Progress</option>
</optgroup>
<optgroup label="Average Scores">
<option value="reading_avg_score">Reading Avg Score</option>
<option value="maths_avg_score">Maths Avg Score</option>
<option value="gps_avg_score">GPS Avg Score</option>
</optgroup>
<optgroup label="Gender Performance">
<option value="rwm_expected_boys_pct">RWM Expected % (Boys)</option>
<option value="rwm_expected_girls_pct">RWM Expected % (Girls)</option>
<option value="rwm_high_boys_pct">RWM Higher % (Boys)</option>
<option value="rwm_high_girls_pct">RWM Higher % (Girls)</option>
</optgroup>
<optgroup label="Equity (Disadvantaged)">
<option value="rwm_expected_disadvantaged_pct">RWM Expected % (Disadvantaged)</option>
<option value="rwm_expected_non_disadvantaged_pct">RWM Expected % (Non-Disadvantaged)</option>
<option value="disadvantaged_gap">Disadvantaged Gap vs National</option>
</optgroup>
<optgroup label="School Context">
<option value="disadvantaged_pct">% Disadvantaged Pupils</option>
<option value="eal_pct">% EAL Pupils</option>
<option value="sen_support_pct">% SEN Support</option>
<option value="stability_pct">% Pupil Stability</option>
</optgroup>
<optgroup label="3-Year Trends">
<option value="rwm_expected_3yr_pct">RWM Expected % (3-Year Avg)</option>
<option value="reading_avg_3yr">Reading Score (3-Year Avg)</option>
<option value="maths_avg_3yr">Maths Score (3-Year Avg)</option>
</optgroup>
</select>
</div>
<div class="chart-container">
<canvas id="comparison-chart"></canvas>
</div>
<div class="data-table-container">
<table class="data-table" id="comparison-table">
<thead>
<tr id="table-header"></tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
</div>
</section>
<!-- Rankings View -->
<section id="rankings-view" class="view">
<div class="rankings-header">
<h2 class="section-title">Primary School Rankings</h2>
<p class="section-subtitle">Top performing schools in Wandsworth & Merton by KS2 metric</p>
</div>
<div class="rankings-controls">
<select id="ranking-metric" class="filter-select">
<optgroup label="Expected Standard">
<option value="rwm_expected_pct">Reading, Writing & Maths Combined %</option>
<option value="reading_expected_pct">Reading Expected %</option>
<option value="writing_expected_pct">Writing Expected %</option>
<option value="maths_expected_pct">Maths Expected %</option>
<option value="gps_expected_pct">GPS Expected %</option>
<option value="science_expected_pct">Science Expected %</option>
</optgroup>
<optgroup label="Higher Standard">
<option value="rwm_high_pct">RWM Combined Higher %</option>
<option value="reading_high_pct">Reading Higher %</option>
<option value="writing_high_pct">Writing Higher %</option>
<option value="maths_high_pct">Maths Higher %</option>
<option value="gps_high_pct">GPS Higher %</option>
</optgroup>
<optgroup label="Progress Scores">
<option value="reading_progress">Reading Progress</option>
<option value="writing_progress">Writing Progress</option>
<option value="maths_progress">Maths Progress</option>
</optgroup>
<optgroup label="Average Scores">
<option value="reading_avg_score">Reading Avg Score</option>
<option value="maths_avg_score">Maths Avg Score</option>
<option value="gps_avg_score">GPS Avg Score</option>
</optgroup>
<optgroup label="Gender Performance">
<option value="rwm_expected_boys_pct">RWM Expected % (Boys)</option>
<option value="rwm_expected_girls_pct">RWM Expected % (Girls)</option>
<option value="rwm_high_boys_pct">RWM Higher % (Boys)</option>
<option value="rwm_high_girls_pct">RWM Higher % (Girls)</option>
</optgroup>
<optgroup label="Equity (Disadvantaged)">
<option value="rwm_expected_disadvantaged_pct">RWM Expected % (Disadvantaged)</option>
<option value="rwm_expected_non_disadvantaged_pct">RWM Expected % (Non-Disadvantaged)</option>
</optgroup>
<optgroup label="3-Year Trends">
<option value="rwm_expected_3yr_pct">RWM Expected % (3-Year Avg)</option>
</optgroup>
</select>
<select id="ranking-year" class="filter-select">
<!-- Populated by JS -->
</select>
</div>
<div class="rankings-list" id="rankings-list">
<!-- Rankings populated by JS -->
</div>
</section>
</main>
<!-- School Detail Modal -->
<div class="modal" id="school-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<button class="modal-close" id="modal-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
<div class="modal-header">
<h2 id="modal-school-name"></h2>
<div class="modal-meta" id="modal-meta"></div>
</div>
<div class="modal-body">
<div class="modal-chart-container">
<canvas id="school-detail-chart"></canvas>
</div>
<div class="modal-stats" id="modal-stats"></div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="add-to-compare">Add to Compare</button>
</div>
</div>
</div>
<footer class="footer">
<p>Data source: <a href="https://www.compare-school-performance.service.gov.uk/download-data" target="_blank">UK Government - Compare School Performance</a></p>
<p class="footer-note">Primary school (KS2) data for Wandsworth and Merton. Data from 2019-2020, 2020-2021, 2021-2022 unavailable due to COVID-19 disruption.</p>
</footer>
<script src="/static/app.js"></script>
</body>
</html>

931
frontend/styles.css Normal file
View File

@@ -0,0 +1,931 @@
/*
* School Performance Compass
* A warm, editorial design inspired by quality publications
*/
:root {
/* Warm, sophisticated palette */
--bg-primary: #faf7f2;
--bg-secondary: #f3ede4;
--bg-card: #ffffff;
--bg-accent: #1a1612;
--text-primary: #1a1612;
--text-secondary: #5c564d;
--text-muted: #8a847a;
--text-inverse: #faf7f2;
--accent-coral: #e07256;
--accent-coral-dark: #c45a3f;
--accent-teal: #2d7d7d;
--accent-teal-light: #3a9e9e;
--accent-gold: #c9a227;
--accent-navy: #2c3e50;
/* Chart colors */
--chart-1: #e07256;
--chart-2: #2d7d7d;
--chart-3: #c9a227;
--chart-4: #7b68a6;
--chart-5: #3498db;
--border-color: #e5dfd5;
--shadow-soft: 0 2px 8px rgba(26, 22, 18, 0.06);
--shadow-medium: 0 4px 20px rgba(26, 22, 18, 0.1);
--shadow-strong: 0 8px 40px rgba(26, 22, 18, 0.15);
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--radius-xl: 24px;
--transition: 0.2s ease;
--transition-slow: 0.4s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
/* Subtle noise texture overlay */
.noise-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.03;
z-index: 1000;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
}
/* Header */
.header {
background: var(--bg-card);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1400px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo-icon {
width: 40px;
height: 40px;
color: var(--accent-coral);
}
.logo-text {
display: flex;
flex-direction: column;
}
.logo-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.logo-subtitle {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.nav {
display: flex;
gap: 0.5rem;
}
.nav-link {
padding: 0.6rem 1.2rem;
text-decoration: none;
color: var(--text-secondary);
font-weight: 500;
font-size: 0.9rem;
border-radius: var(--radius-md);
transition: var(--transition);
}
.nav-link:hover {
color: var(--text-primary);
background: var(--bg-secondary);
}
.nav-link.active {
background: var(--bg-accent);
color: var(--text-inverse);
}
/* Main Content */
.main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.view {
display: none;
animation: fadeIn 0.3s ease;
}
.view.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Hero Section */
.hero {
text-align: center;
padding: 3rem 0 2rem;
}
.hero-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.75rem;
line-height: 1.2;
}
.hero-subtitle {
font-size: 1.1rem;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
}
/* Search Section */
.search-section {
max-width: 700px;
margin: 2rem auto 3rem;
}
.search-container {
position: relative;
margin-bottom: 1rem;
}
.search-input {
width: 100%;
padding: 1rem 1.25rem 1rem 3.5rem;
font-size: 1rem;
font-family: inherit;
border: 2px solid var(--border-color);
border-radius: var(--radius-lg);
background: var(--bg-card);
color: var(--text-primary);
transition: var(--transition);
}
.search-input:focus {
outline: none;
border-color: var(--accent-coral);
box-shadow: 0 0 0 4px rgba(224, 114, 86, 0.1);
}
.search-input::placeholder {
color: var(--text-muted);
}
.search-icon {
position: absolute;
left: 1.25rem;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
color: var(--text-muted);
}
.filter-row {
display: flex;
gap: 1rem;
justify-content: center;
}
.filter-select {
padding: 0.6rem 2rem 0.6rem 1rem;
font-size: 0.9rem;
font-family: inherit;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: var(--bg-card);
color: var(--text-primary);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%235c564d' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
transition: var(--transition);
}
.filter-select:focus {
outline: none;
border-color: var(--accent-teal);
}
/* Schools Grid */
.schools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.school-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.5rem;
cursor: pointer;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.school-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--accent-coral);
transform: scaleY(0);
transition: var(--transition);
}
.school-card:hover {
border-color: var(--accent-coral);
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
.school-card:hover::before {
transform: scaleY(1);
}
.school-name {
font-family: 'Playfair Display', Georgia, serif;
font-size: 1.15rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.school-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.school-tag {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
background: var(--bg-secondary);
color: var(--text-secondary);
border-radius: var(--radius-sm);
}
.school-tag.type {
background: rgba(45, 125, 125, 0.1);
color: var(--accent-teal);
}
.school-address {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.school-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.stat {
text-align: center;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
}
.stat-value.positive {
color: var(--accent-teal);
}
.stat-value.negative {
color: var(--accent-coral);
}
.stat-label {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Section Titles */
.section-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.section-subtitle {
font-size: 1rem;
color: var(--text-secondary);
margin-bottom: 2rem;
}
/* Compare View */
.compare-header {
text-align: center;
padding: 2rem 0;
}
.selected-schools {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
min-height: 100px;
padding: 1.5rem;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
justify-content: center;
align-items: center;
}
.empty-selection {
text-align: center;
color: var(--text-muted);
}
.empty-icon {
width: 48px;
height: 48px;
margin: 0 auto 0.5rem;
color: var(--text-muted);
}
.selected-school-tag {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: 0.9rem;
color: var(--text-primary);
animation: slideIn 0.2s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.selected-school-tag .remove {
width: 18px;
height: 18px;
border: none;
background: var(--bg-secondary);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.selected-school-tag .remove:hover {
background: var(--accent-coral);
color: white;
}
.compare-search-section {
max-width: 500px;
margin: 0 auto 2rem;
position: relative;
}
.compare-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-medium);
max-height: 300px;
overflow-y: auto;
z-index: 50;
display: none;
}
.compare-results.active {
display: block;
}
.compare-result-item {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 1px solid var(--border-color);
transition: var(--transition);
}
.compare-result-item:last-child {
border-bottom: none;
}
.compare-result-item:hover {
background: var(--bg-secondary);
}
.compare-result-item .name {
font-weight: 500;
color: var(--text-primary);
}
.compare-result-item .location {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Charts Section */
.charts-section {
margin-top: 2rem;
}
.metric-selector {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.metric-selector label {
font-weight: 500;
color: var(--text-secondary);
}
.chart-container {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 2rem;
margin-bottom: 2rem;
}
/* Data Table */
.data-table-container {
overflow-x: auto;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.data-table th {
background: var(--bg-secondary);
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tr:hover td {
background: rgba(224, 114, 86, 0.03);
}
/* Rankings View */
.rankings-header {
text-align: center;
padding: 2rem 0;
}
.rankings-controls {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
}
.rankings-list {
max-width: 800px;
margin: 0 auto;
}
.ranking-item {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1.25rem 1.5rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: 0.75rem;
transition: var(--transition);
cursor: pointer;
}
.ranking-item:hover {
border-color: var(--accent-coral);
box-shadow: var(--shadow-soft);
}
.ranking-position {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-weight: 700;
font-size: 1rem;
}
.ranking-position.top-3 {
background: linear-gradient(135deg, var(--accent-gold), #d4af37);
color: white;
}
.ranking-position:not(.top-3) {
background: var(--bg-secondary);
color: var(--text-secondary);
}
.ranking-info {
flex: 1;
}
.ranking-name {
font-family: 'Playfair Display', Georgia, serif;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.ranking-location {
font-size: 0.85rem;
color: var(--text-muted);
}
.ranking-score {
text-align: right;
}
.ranking-score-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--accent-teal);
}
.ranking-score-label {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 2rem;
}
.modal.active {
display: flex;
}
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(26, 22, 18, 0.6);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
background: var(--bg-card);
border-radius: var(--radius-xl);
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-strong);
animation: modalIn 0.3s ease;
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.95) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-close {
position: absolute;
top: 1rem;
right: 1rem;
width: 40px;
height: 40px;
border: none;
background: var(--bg-secondary);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
z-index: 10;
}
.modal-close:hover {
background: var(--accent-coral);
color: white;
}
.modal-close svg {
width: 20px;
height: 20px;
}
.modal-header {
padding: 2rem 2rem 1rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
font-family: 'Playfair Display', Georgia, serif;
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
padding-right: 3rem;
}
.modal-meta {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.modal-body {
padding: 2rem;
}
.modal-chart-container {
margin-bottom: 2rem;
}
.modal-stats {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.modal-stats-section {
padding: 1rem;
background: var(--bg-secondary);
border-radius: var(--radius-md);
}
.modal-stats-section h4 {
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.modal-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 0.75rem;
}
.modal-stat {
text-align: center;
padding: 1rem;
background: var(--bg-secondary);
border-radius: var(--radius-md);
}
.modal-stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}
.modal-stat-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
margin-top: 0.25rem;
}
.modal-footer {
padding: 1.5rem 2rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
font-family: inherit;
font-weight: 600;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition);
}
.btn-primary {
background: var(--accent-coral);
color: white;
}
.btn-primary:hover {
background: var(--accent-coral-dark);
transform: translateY(-1px);
}
/* Footer */
.footer {
text-align: center;
padding: 2rem;
margin-top: 3rem;
border-top: 1px solid var(--border-color);
color: var(--text-muted);
font-size: 0.85rem;
}
.footer a {
color: var(--accent-teal);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.footer-note {
margin-top: 0.5rem;
font-size: 0.75rem;
}
/* Loading State */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--text-muted);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--accent-coral);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--text-muted);
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Responsive */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
}
.nav {
width: 100%;
justify-content: center;
}
.main {
padding: 1rem;
}
.hero-title {
font-size: 1.75rem;
}
.filter-row {
flex-direction: column;
}
.schools-grid {
grid-template-columns: 1fr;
}
.modal-content {
margin: 1rem;
max-height: calc(100vh - 2rem);
}
.rankings-controls {
flex-direction: column;
align-items: stretch;
}
}

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
pandas==2.1.4
python-multipart==0.0.6
aiofiles==23.2.1

181
scripts/download_data.py Normal file
View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
Data Download Helper Script
This script provides instructions and utilities for downloading
UK school performance data from the official government source.
Data Source: https://www.compare-school-performance.service.gov.uk/download-data
Note: The actual CSV downloads require manual selection on the website
as they use dynamic form submissions. This script helps prepare and
organize the downloaded data.
"""
import os
import sys
from pathlib import Path
import pandas as pd
DATA_DIR = Path(__file__).parent.parent / "data"
def print_instructions():
"""Print instructions for downloading the data."""
print("""
╔══════════════════════════════════════════════════════════════════════════════╗
║ UK School Performance Data Download Instructions ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 1. Visit: https://www.compare-school-performance.service.gov.uk/download-data║
║ ║
║ 2. For each year (2019-2020 through 2023-2024), select: ║
║ • Year: Select the academic year ║
║ • Data type: "Key Stage 4" (for secondary school GCSE data) ║
║ • File type: "All data" or specific metrics you need ║
║ ║
║ 3. Key metrics available: ║
║ • Progress 8 - measures pupil progress from KS2 to KS4 ║
║ • Attainment 8 - average attainment across 8 qualifications ║
║ • English & Maths Grade 5+ percentage ║
║ • EBacc entry and achievement percentages ║
║ ║
║ 4. Download the CSV files and place them in the 'data' folder ║
║ ║
║ 5. Rename files with the year for clarity, e.g.: ║
║ • ks4_2020.csv ║
║ • ks4_2021.csv ║
║ • ks4_2022.csv ║
║ • ks4_2023.csv ║
║ • ks4_2024.csv ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════╝
""")
def check_data_files():
"""Check what data files are present in the data directory."""
if not DATA_DIR.exists():
print(f"Data directory not found: {DATA_DIR}")
return []
csv_files = list(DATA_DIR.glob("*.csv"))
if not csv_files:
print("No CSV files found in the data directory.")
print(f"Please place your downloaded CSV files in: {DATA_DIR}")
return []
print(f"\nFound {len(csv_files)} CSV file(s):")
for f in csv_files:
size_mb = f.stat().st_size / (1024 * 1024)
print(f"{f.name} ({size_mb:.2f} MB)")
return csv_files
def preview_data(file_path: Path, rows: int = 5):
"""Preview a CSV file."""
try:
df = pd.read_csv(file_path, nrows=rows)
print(f"\n--- Preview of {file_path.name} ---")
print(f"Columns ({len(df.columns)}):")
for col in df.columns[:20]:
print(f"{col}")
if len(df.columns) > 20:
print(f" ... and {len(df.columns) - 20} more columns")
print(f"\nFirst {rows} rows:")
print(df.to_string())
except Exception as e:
print(f"Error reading {file_path}: {e}")
def standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Standardize column names for consistency."""
# Common column mappings from the official data
column_mappings = {
'URN': 'urn',
'SCHNAME': 'school_name',
'TOWN': 'town',
'REGION': 'region',
'RELDENOM': 'school_type',
'P8MEA': 'progress_8',
'ATT8SCR': 'attainment_8',
'PTAC5EM': 'grade_5_eng_maths_pct',
'PTEBACCEG': 'ebacc_entry_pct',
'TPUP': 'pupils',
}
# Normalize column names
df.columns = df.columns.str.strip().str.upper()
# Apply mappings
df = df.rename(columns={k.upper(): v for k, v in column_mappings.items()})
return df
def process_and_combine_data():
"""Process and combine all CSV files into a single dataset."""
csv_files = check_data_files()
if not csv_files:
return None
all_data = []
for csv_file in csv_files:
print(f"\nProcessing: {csv_file.name}")
try:
df = pd.read_csv(csv_file, low_memory=False)
df = standardize_columns(df)
# Try to extract year from filename
import re
year_match = re.search(r'20\d{2}', csv_file.stem)
if year_match:
df['year'] = int(year_match.group())
all_data.append(df)
print(f" Loaded {len(df)} rows")
except Exception as e:
print(f" Error: {e}")
if all_data:
combined = pd.concat(all_data, ignore_index=True)
output_path = DATA_DIR / "combined_data.csv"
combined.to_csv(output_path, index=False)
print(f"\nCombined data saved to: {output_path}")
print(f"Total rows: {len(combined)}")
return combined
return None
def main():
"""Main entry point."""
if len(sys.argv) > 1:
command = sys.argv[1].lower()
if command == "check":
check_data_files()
elif command == "preview" and len(sys.argv) > 2:
file_path = DATA_DIR / sys.argv[2]
if file_path.exists():
preview_data(file_path)
else:
print(f"File not found: {file_path}")
elif command == "combine":
process_and_combine_data()
else:
print_instructions()
else:
print_instructions()
print("\nAvailable commands:")
print(" python download_data.py check - Check for existing data files")
print(" python download_data.py preview <filename> - Preview a CSV file")
print(" python download_data.py combine - Combine all CSV files")
if __name__ == "__main__":
main()

253
scripts/fetch_real_data.py Normal file
View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
Fetch real school performance data from UK Government sources.
This script downloads KS2 (Key Stage 2) primary school data from:
- Compare School Performance service
- Get Information about Schools (GIAS)
Data is filtered to only include schools in Wandsworth and Merton.
"""
import os
import sys
import requests
import pandas as pd
from pathlib import Path
from io import StringIO
# Output directory
DATA_DIR = Path(__file__).parent.parent / "data"
# Local Authority codes for Wandsworth and Merton
LA_CODES = {
"Wandsworth": "212",
"Merton": "315"
}
# Academic years to fetch (last 5 years available)
YEARS = ["2023-2024", "2022-2023", "2021-2022", "2019-2020", "2018-2019"]
# Note: 2020-2021 had no SATs due to COVID
def fetch_gias_data():
"""
Fetch school establishment data from Get Information About Schools.
This gives us the list of schools with URN, name, address, type, etc.
"""
print("Fetching school establishment data from GIAS...")
# GIAS provides downloadable extracts
# Main extract URL (this may need to be updated periodically)
gias_url = "https://ea-edubase-api-prod.azurewebsites.net/edubase/downloads/public/edubasealldata.csv"
try:
response = requests.get(gias_url, timeout=60)
response.raise_for_status()
# Parse CSV
df = pd.read_csv(StringIO(response.text), encoding='utf-8-sig', low_memory=False)
# Filter to primary schools in Wandsworth and Merton
# Phase of education: Primary, Middle deemed primary
# LA codes: 212 (Wandsworth), 315 (Merton)
df = df[
(df['LA (code)'].astype(str).isin(LA_CODES.values())) &
(df['PhaseOfEducation (name)'].str.contains('Primary', na=False))
]
# Select relevant columns
columns_to_keep = [
'URN', 'EstablishmentName', 'LA (name)', 'TypeOfEstablishment (name)',
'Street', 'Locality', 'Town', 'Postcode',
'SchoolCapacity', 'NumberOfPupils', 'OfstedRating (name)'
]
available_cols = [c for c in columns_to_keep if c in df.columns]
df = df[available_cols]
# Rename columns
df = df.rename(columns={
'URN': 'urn',
'EstablishmentName': 'school_name',
'LA (name)': 'local_authority',
'TypeOfEstablishment (name)': 'school_type',
'Street': 'street',
'Town': 'town',
'Postcode': 'postcode',
'NumberOfPupils': 'pupils',
'OfstedRating (name)': 'ofsted_rating'
})
# Create address field
df['address'] = df.apply(
lambda row: f"{row.get('street', '')}, {row.get('postcode', '')}".strip(', '),
axis=1
)
print(f"Found {len(df)} primary schools in Wandsworth and Merton")
return df
except Exception as e:
print(f"Error fetching GIAS data: {e}")
return None
def fetch_ks2_performance_data():
"""
Fetch KS2 performance data from Compare School Performance.
Note: The official download page requires form submission.
We'll try to access the underlying data files directly.
"""
print("\nFetching KS2 performance data...")
# The performance data is available at gov.uk statistics pages
# KS2 data URLs follow a pattern
base_urls = {
"2023-2024": "https://content.explore-education-statistics.service.gov.uk/api/releases/",
"2022-2023": "https://content.explore-education-statistics.service.gov.uk/api/releases/",
}
# Alternative: Direct download links from gov.uk (when available)
# These URLs may need to be updated when new data is released
data_urls = {
# 2024 KS2 results (provisional)
"2024": "https://content.explore-education-statistics.service.gov.uk/api/releases/b4cb82e3-6dca-4c98-a3b0-ba7d1d3ef555/files",
}
print("Note: For the most accurate data, please download manually from:")
print("https://www.compare-school-performance.service.gov.uk/download-data")
print("\nSteps:")
print("1. Select 'Key Stage 2' for Data type")
print("2. Select 'All data' for File type")
print("3. Select desired academic year")
print("4. Download and place CSV files in the 'data' folder")
return None
def download_from_explore_education_statistics():
"""
Try to fetch data from the Explore Education Statistics API.
API docs: https://dfe-analytical-services.github.io/explore-education-statistics-api-docs/
"""
print("\nAttempting to fetch from Explore Education Statistics API...")
api_base = "https://explore-education-statistics.service.gov.uk/api/v1"
# First, list available publications
try:
# Get KS2 publication
publications_url = f"{api_base}/publications"
response = requests.get(publications_url, timeout=30)
if response.status_code == 200:
publications = response.json()
# Find KS2 related publication
ks2_pubs = [p for p in publications.get('results', [])
if 'key stage 2' in p.get('title', '').lower()
or 'ks2' in p.get('title', '').lower()]
if ks2_pubs:
print(f"Found KS2 publications: {[p['title'] for p in ks2_pubs]}")
# Get the latest release
for pub in ks2_pubs:
pub_id = pub.get('id')
if pub_id:
release_url = f"{api_base}/publications/{pub_id}/releases/latest"
release_response = requests.get(release_url, timeout=30)
if release_response.status_code == 200:
release = release_response.json()
print(f"Latest release: {release.get('title')}")
# Get data files
data_sets = release.get('dataSets', [])
for ds in data_sets:
print(f" - Dataset: {ds.get('name')}")
else:
print("No KS2 publications found via API")
else:
print(f"API returned status {response.status_code}")
except Exception as e:
print(f"Error accessing API: {e}")
return None
def create_combined_dataset(schools_df, performance_data=None):
"""
Combine school information with performance data.
If no performance data is available, returns school info only.
"""
if schools_df is None:
return None
# Add year column for compatibility
schools_df['year'] = 2024
# Add placeholder performance columns if no real data
if performance_data is None:
print("\nNo performance data available - school list saved without metrics")
print("Download KS2 data manually and re-run to add performance metrics")
return schools_df
def main():
"""Main entry point."""
print("=" * 60)
print("Fetching Real School Data for Wandsworth & Merton")
print("=" * 60)
# Create data directory
DATA_DIR.mkdir(exist_ok=True)
# Fetch school establishment data
schools_df = fetch_gias_data()
# Try to fetch performance data
fetch_ks2_performance_data()
download_from_explore_education_statistics()
# Save school data
if schools_df is not None:
output_file = DATA_DIR / "schools_wandsworth_merton.csv"
schools_df.to_csv(output_file, index=False)
print(f"\nSchool data saved to: {output_file}")
print(f"Total schools: {len(schools_df)}")
# Show breakdown
print("\nBreakdown by Local Authority:")
print(schools_df['local_authority'].value_counts())
else:
print("\nFailed to fetch school data")
print("\n" + "=" * 60)
print("NEXT STEPS:")
print("=" * 60)
print("""
To get complete performance data:
1. Go to: https://www.compare-school-performance.service.gov.uk/download-data
2. Download KS2 data for each year (2019-2024):
- Select: Key Stage 2
- Select: All data (or specific metrics)
- Select: Academic year
- Click: Download data
3. Place downloaded CSV files in the 'data' folder
4. Restart the application - it will automatically load the real data
The app will merge school info with performance metrics.
""")
if __name__ == "__main__":
main()