An end-to-end classification pipeline that scores road conditions as Low / Medium / High accident risk from weather, traffic, road infrastructure and time-of-day features — then tunes its decision threshold to cut false alarms and trains a separate model per season.
On the data: this project runs on a synthetic 50,000-sample dataset generated inside the script. The features are drawn from realistic distributions (temperature follows a seasonal sinusoid, visibility drops with precipitation and at night, speed falls as volume rises), and the risk label comes from a weighted scoring rule over those features plus Gaussian noise. The pipeline is therefore a demonstration of method, not a validated real-world risk model — the reported scores measure how well the models recover a known generative rule. Swap in real accident records and the same pipeline applies unchanged.
Test set: 10,000 samples held out from 50,000. Best model selected by 5-fold cross-validated F1-macro.
| Model | CV F1-macro |
|---|---|
| Logistic Regression | 0.7396 ± 0.0081 |
| XGBoost | 0.7315 ± 0.0129 |
| Random Forest | 0.7120 ± 0.0112 |
Selected model — Logistic Regression:
| Metric | Score |
|---|---|
| Accuracy | 0.8609 |
| Precision (macro) | 0.7963 |
| Recall (macro) | 0.6991 |
| F1 (macro) | 0.7348 |
| ROC-AUC (one-vs-rest) | 0.9520 |
Per class:
| Class | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| Low | 0.68 | 0.40 | 0.50 | 331 |
| Medium | 0.88 | 0.92 | 0.90 | 6,738 |
| High | 0.83 | 0.78 | 0.80 | 2,931 |
The Low class is the weak point — recall 0.40 on only 331 support. The scoring rule makes genuinely low-risk conditions rare, and the classifier trades them away for accuracy on the two large classes. Class weighting or resampling is the obvious fix and is listed under next steps.
Argmax over three classes is not the right operating point when missing a high-risk road costs more than a false alarm. Sweeping the probability threshold for the High class and maximising its F1 gives:
| Default (argmax) | Tuned (threshold = 0.378) | |
|---|---|---|
| Accuracy | 0.8609 | 0.8602 |
| Recall (macro) | 0.6991 | 0.7112 |
| F1 (macro) | 0.7348 | 0.7375 |
Macro recall rises 1.2 points for 0.07 points of accuracy — the trade you want when the cost of a miss is asymmetric.
Training a dedicated XGBoost per season shows the problem is not uniform across the year:
| Season | F1-macro |
|---|---|
| Winter | 0.6839 |
| Spring | 0.6965 |
| Summer | 0.5939 |
| Autumn | 0.7254 |
Summer is hardest: without precipitation and ice as strong signals, the remaining features separate the classes far less cleanly. Every seasonal model scores below the single global model (0.7348), so per-season splitting hurts here — each model sees a quarter of the data and the loss of sample size outweighs the gain in specialisation. That is a useful negative result, not a feature.
Synthetic generator (50,000 samples)
weather · traffic · road · temporal -> weighted risk score + noise -> Low/Medium/High
|
v
Feature engineering
time_of_day (morning/afternoon/evening/night) · is_weekend · volume-to-speed congestion ratio
|
v
ColumnTransformer: OneHotEncoder (categorical) + StandardScaler (numeric)
|
v
5-fold CV over {Logistic Regression, Random Forest, XGBoost} -> select by F1-macro
|
+--> threshold tuning for the High class
+--> per-season models
+--> high-risk zone flagging + rule-based prevention suggestions
git clone https://github.com/MrDanial-Rafiee/traffic-accident-risk.git
cd traffic-accident-risk
pip install -r requirements.txt
python traffic_risk.pyThe script is deterministic (random_state=42), takes a few minutes on CPU, regenerates every figure into images/, and writes per-sample predictions to predictions.csv.
| Group | Features |
|---|---|
| Weather | temperature, precipitation, visibility |
| Traffic | traffic_volume, average_speed, volume_speed_ratio |
| Road | road_type, lanes, lighting, signage |
| Temporal | hour, day_of_week, month, season, time_of_day, is_weekend |
For each flagged high-risk sample the script emits targeted mitigations from the contributing factors — heavy precipitation triggers a following-distance advisory, poor signage triggers a lane-marking recommendation, winter triggers a gritting recommendation. This is a deliberate rule layer, not a model output: the classifier says how risky, the rules say what to do about it.
- Synthetic data. The headline scores measure rule recovery, not real-world predictive power. Validating against a real accident dataset is the single most important next step.
- Low-class recall is 0.40.
class_weight='balanced'or SMOTE should be tried before anything else. - The GridSearchCV block for XGBoost is written but commented out — it has not been run, so the reported XGBoost score is untuned.
- No spatial features. Real accident risk is strongly geographic; road segment identity, junction density and historical accident counts per location are all missing.
- The threshold is tuned on the test set. With real data this needs its own validation split to avoid an optimistic estimate.
- Seasonal models underperform the global model; a single model with season as a feature (the current global setup) is the better design here.
MIT — see LICENSE.

