UK Road Collision Severity Analysis 2024
By Eldar · Published July 18, 2026
Exploratory and predictive analysis of 100K+ UK road collisions identifying risk factors for serious/fatal outcomes using machine learning and geospatial visualization.
- road-safety
- classification
- eda
- geospatial
- random-forest
9 cells1 experiment3 views0 forks
Inside this notebook
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings('ignore')
# Set visual defaults
matplotlib.rcParams['figure.dpi'] = 120
matplotlib.rcParams['figure.facecolor'] = 'white'
sns.set_style('whitegrid')
sns.set_palette('viridis')
print("Libraries loaded. Reading dataset...")Libraries loaded. Reading dataset...
# ============================================================
# 1. LOAD AND PROFILE
# ============================================================
df = pd.read_csv('dft-road-casualty-statistics-collision-2024.csv',
low_memory=False)
print(f"Shape: {df.shape}")
print(f"\n=== COLUMN DTYPES ===")
print(df.dtypes.to_string())
print(f"\n=== FIRST 3 ROWS (key columns) ===")
key_cols = ['collision_index','collision_severity','date','time','speed_limit',
'road_type','junction_detail','light_conditions','weather_conditions',
'road_surface_conditions','urban_or_rural_area','latitude','longitude',
'number_of_vehicles','number_of_casualties']
print(df[key_cols].head(3).to_string())
print(f"\n=== UNIQUE VALUES PER COLUMN ===")
…Shape: (100927, 44) === COLUMN DTYPES === collision_index object collision_year int64 collision_ref_no object location_easting_osgr int64 location_northing_osgr int64 longitude float64 latitude float64 police_force…
# ============================================================
# 2. CLEAN AND DECODE THE DATA
# ============================================================
df = pd.read_csv('dft-road-casualty-statistics-collision-2024.csv', low_memory=False)
# --- Severity (target) ---
severity_map = {1: 'Fatal', 2: 'Serious', 3: 'Slight'}
df['severity_label'] = df['collision_severity'].map(severity_map)
# Binary target: 1 = Fatal or Serious, 0 = Slight
df['is_serious_or_fatal'] = df['collision_severity'].isin([1, 2]).astype(int)
# --- Day of week (1=Sunday..7=Saturday per DfT STATS20) ---
dow_map = {1:'Sunday',2:'Monday',3:'Tuesday',4:'Wednesday',
5:'Thursday',6:'Friday',7:'Saturday'}
df['day_name'] = df['day_of_week'].map(dow_map)
# --- Datetime (date format is DD/MM/YYYY) ---
…=== MISSINGNESS SUMMARY ===
missing pct
light_label 6 0.01
local_authority_highway_current 3 0.00
=== SEVERITY DISTRIBUTION ===
severity_label
Slight 75858
Serious 23567
Fatal 1502
Name: count, dtype: int64
Binary target share: 0.248 (25,069 of 100,927 crashes)
=== DATE RANGE ===
From 2024-01-01 00:00:00 to 2024-12-31 00:00:00
Rows with invalid date: 0
=== DECODED KEY COLUMNS (sample) ===
collision_severit…# ============================================================
# 3. EDA — Time, Speed Limits, Weather, Lighting, Road Surface, Junctions
# ============================================================
fig, axes = plt.subplots(2, 3, figsize=(18, 11))
fig.suptitle('Collision Severity by Key Factors — UK 2024', fontsize=16, fontweight='bold', y=1.01)
# --- 1. Hour of day ---
sev_hour = df.groupby(['hour', 'severity_label']).size().unstack(fill_value=0)
sev_hour_pct = sev_hour.div(sev_hour.sum(axis=1), axis=0)
ax = axes[0,0]
for sev, color in zip(['Slight','Serious','Fatal'], ['#2ecc71','#f39c12','#e74c3c']):
if sev in sev_hour_pct:
ax.plot(sev_hour_pct.index, sev_hour_pct[sev], label=sev, color=color, lw=2)
ax.set_xlabel('Hour of day'); ax.set_ylabel('Proportion within hour')
ax.set_title('Severity by Hour of Day', fontsize=12)
ax.legend(fontsize=9); ax.set_xticks(range(0,24,2)); ax.grid(alpha=0.3)
…================================================================================
SERIOUS/FATAL RATE BY FACTOR (top categories)
================================================================================
--- Speed Limit ---
N_crashes serious_fatal_rate N_serious_fatal
speed_limit
30 50257 0.231 11629
20 18881 0.209 3953
60 12709…# ============================================================
# 4. HOTSPOT MAP — Geographic concentration of serious/fatal crashes
# ============================================================
import plotly.express as px
# Filter to crashes with valid lat/lon
map_df = df.dropna(subset=['latitude', 'longitude']).copy()
# Clip to GB mainland roughly
map_df = map_df[(map_df['latitude'] > 49.8) & (map_df['latitude'] < 58.7) &
(map_df['longitude'] > -8.5) & (map_df['longitude'] < 2.5)]
# Separate serious/fatal from slight for layering
sf = map_df[map_df['is_serious_or_fatal'] == 1]
sl = map_df[map_df['is_serious_or_fatal'] == 0]
# Sample for performance (Plotly can struggle with 100K points)
sf_sample = sf.sample(min(8000, len(sf)), random_state=42)
…Saved to outputs (downloadable from the file manager): • collision_hexbin_density.png (364806 bytes) • top_police_forces_sf.png (74435 bytes) Not persisted: • collision_hotspot_map.html — could not read from sandbox
# ============================================================
# 5. PREDICTIVE MODEL — Factors associated with serious/fatal crashes
# ============================================================
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.metrics import (roc_auc_score, classification_report, confusion_matrix,
RocCurveDisplay, PrecisionRecallDisplay,
average_precision_score)
from sklearn.inspection import permutation_importance
import warnings
warnings.filterwarnings('ignore')
# ---- 5a. Prepare feature matrix ----
# Drop invalid speed limits
model_df = df[df['speed_limit'] > 0].copy()
…Feature matrix shape: (100924, 14) Target: 25,068 serious/fatal out of 100,924 (0.248) Features used: ['speed_limit', 'road_type', 'junction_detail', 'junction_control', 'light_conditions', 'weather_conditions', 'road_surface_conditions', 'urban_or_rural_area', 'hour', 'day_of_week', 'month', 'number_of_vehicles', 'special_conditions_at_site', 'carriageway_hazards'] Train: 80,739 | Test: 20,185 Train SF rate: 0.248 | Test SF rate: 0.248 ====================================================…