initial commit
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
venv
|
||||
+31
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 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
@@ -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)
|
||||
Vendored
BIN
Binary file not shown.
@@ -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.
@@ -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
|
||||
|
@@ -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]
|
||||
|
@@ -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
|
||||
|
@@ -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
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user