All Projects

Open Source

ML Tool / Data Science Web App

Traffic Accident Severity Predictor

A machine learning web app that classifies vehicle crash severity from mechanical specs using a Random Forest ensemble trained on the UCI Cars93 dataset.

Role: ML Engineer / Sole Developer (Group 45 – Capstone Project, TechCrush Cohort 5)


PythonMachine Learningscikit-learnStreamlitRandom ForestData ScienceNLP
Problem Statement

Emergency responders, insurers, and road-safety researchers need a fast, data-driven way to assess how vehicle physical characteristics correlate with crash severity profiles — without having access to real-time accident data.

Target users: Road safety analysts, data science students, ML learners, vehicle insurers, and anyone exploring how vehicle physics influences crash impact patterns.

Project Walkthrough

Road safety data is notoriously hard to get in a clean, structured form — but the physical properties of a vehicle tell a surprisingly rich story about how it will behave in a collision. That insight drove this capstone project: could we train a machine learning model to classify a vehicle's accident severity profile using nothing but its mechanical specs? The answer, it turns out, is yes — with 88.89% accuracy.

I built the entire system in two clean phases. The first is an offline training pipeline (train.py) that reads the Cars93 dataset, handles missing values with median imputation and forward-fill, encodes categorical features with column-specific LabelEncoders, and scales numerics with StandardScaler. Three models are trained and evaluated head-to-head — Logistic Regression (72.22%), a two-hidden-layer MLP Neural Network (83.33%), and a 300-tree Random Forest (88.89%). The pipeline auto-selects the winner and serialises the model plus all preprocessors to disk as .pkl files.

The second phase is the live web app (app.py), built with Streamlit. This is where I had to think carefully about a problem called training-serving skew — if the scaler or encoders used at prediction time differ even slightly from what was used during training, your predictions will be silently wrong. The solution was to load the exact fitted preprocessor objects that were saved at training time, and call .transform() rather than .fit_transform() on any new user input. It's a small but critical distinction that separates a production-quality ML system from a demo.

The biggest lesson here was how much engineering discipline matters even in a small ML project. The correct serialisation pattern, the stratified split, the median vs. mean choice for imputation — none of these are glamorous decisions, but each one materially affects the correctness and reliability of the final system. This project earned an 88.89% accuracy on an inherently noisy multi-class classification problem with fewer than 90 training samples.


Tech Stack

Languages

Python 3.8+

Frameworks

Streamlit (interactive web app UI from pure Python)

Databases

N/A — flat CSV dataset (Cars93_miss.csv, 93 rows, 27 columns from UCI ML Repository)

Cloud & Infrastructure

Streamlit Community Cloud (free public deployment)

Dev Tools

joblib (model serialisation to .pkl files)scikit-learn (ML models, preprocessing, evaluation)pandas (data loading and manipulation)numpy (numerical operations, array building)matplotlib (bar charts, KDE plots, feature importance charts)seaborn (correlation heatmap)

Authentication

N/A — no authentication required (public prediction tool)

Third-Party APIs & Integrations

N/A — fully self-contained; no external API calls


System Architecture

Architecture Pattern

Two-phase pipeline: offline training script (train.py) → serialised model artefacts → online inference app (app.py). Classic train-serve separation pattern.

Request Data Flow

User adjusts sidebar sliders/dropdowns → [1] Collect 23 feature values (18 numeric + 5 categorical strings) → [2] Encode categoricals with saved LabelEncoders → [3] Build numpy array → [4] Scale numeric columns with saved StandardScaler → [5] model.predict() — 300 RF trees vote → majority integer returned → [6] model.predict_proba() — probability vector across 6 classes → [7] le_target.inverse_transform() — integer back to label → [8] Display prediction card + confidence % + probability bar chart

Key Engineering Decisions

1. Train/serve separation: scaler and encoders are saved at training time and reloaded identically at serving time to prevent training-serving skew. 2. Stratified 80/20 split: with only 89 rows and 6 classes, stratify=y ensures all classes appear proportionally in the test set. 3. Median imputation for numerics (robust to outliers) and forward-fill for categoricals. 4. StandardScaler applied only to numeric columns. 5. Best model auto-selection: train.py compares all 3 models and saves whichever scores highest.

Database Design

No traditional database. The dataset is a flat CSV (Cars93_miss.csv) — 93 rows × 27 columns. Target: Type column renamed to Severity (6 classes: Small, Compact, Midsize, Large, Sporty, Van).

Module Structure

train.pyapp.pymodels/Cars93_miss.csvrequirements.txt

Key Features & Implementation

ML Training Pipeline (train.py)

Runs the full offline pipeline: loads and cleans the Cars93 CSV, encodes categoricals, scales numerics, trains 3 models (Logistic Regression, Random Forest, MLP Neural Network), evaluates all three with classification reports, automatically selects the best-performing model, and saves 5 serialised artefacts to models/.

How it was built

7-step pipeline using pandas/numpy for data processing, scikit-learn for all ML operations (LabelEncoder, StandardScaler, LogisticRegression, RandomForestClassifier with n_estimators=300 and n_jobs=-1, MLPClassifier with hidden_layer_sizes=(100,50)), and joblib for serialisation. Model selection is dynamic (max accuracy key), so retraining on new data automatically promotes the best model.

Real-Time Severity Prediction (Tab 1)

User adjusts 18 numeric sliders and 5 categorical dropdowns to describe a vehicle. Clicking 'Predict Severity Category' instantly returns: the predicted severity class label, a confidence percentage, and a horizontal probability breakdown bar chart across all 6 classes.

How it was built

App loads all 5 pkl artefacts via @st.cache_resource (loaded once, reused). On button click: builds a 23-element feature row, applies saved LabelEncoders to categoricals, applies saved StandardScaler to numeric slice, calls model.predict() and model.predict_proba(), then decodes the integer output with le_target.inverse_transform().

Training-Serving Consistency (Anti-Skew Design)

Ensures user inputs at inference time are processed identically to how training data was processed — preventing prediction errors caused by mismatched preprocessing.

How it was built

The StandardScaler and all LabelEncoders are fitted only once during training (on training data) and saved to .pkl files. The app loads these exact objects via joblib.load() and calls .transform() (not .fit_transform()) at inference time. This is the correct pattern to prevent training-serving skew.


Challenges & Engineering Decisions
1

Small dataset (93 rows, 6 classes): With so few samples, the risk of a poorly distributed train/test split was high. Solved with stratify=y in train_test_split to guarantee proportional class representation in both sets.

2

Training-serving skew: A subtle but critical ML engineering concern — if the scaler or encoders at serving time differ from training, predictions are silently wrong. Mitigated by serialising all fitted preprocessors and loading them identically in the app.

3

Theming Streamlit: Streamlit's default component styling is resistant to overrides. The solution was a comprehensive CSS injection block using Streamlit's internal data-testid attribute selectors.


API Documentation

API Route Namespaces

N/A — Streamlit runs as a single-page app at localhost:8501