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

====================================================…
UK Road Collision Severity Analysis 2024 | Clusy