Five state crash files to an on-device risk score.

A clustering algorithm tuned against a real percolation failure. A label leak caught and fixed with spatial cross-validation. A fusion rule that guarantees bad weather never makes the app less cautious.

Five-stage pipeline: data, clustering, training, export, on-device fusion 1 5 State DOT Crash Datasets 955,848 rows sampled 318,616 real collisions 2 Percolation-Bounded Clustering Density-adaptive DBSCAN 2,989 real hotspots 3 Leakage-Fixed Training Spatial block CV out-of-fold features 4 CoreML Export sklearn → .mlpackage versioned model card 5 On-Device Fusion Engine Rules × ML blend weather applied last

What the scripts and Swift source in this repo actually do.

Clustering tuned against its own failure mode

The textbook DBSCAN eps (the "k-distance knee") was implemented, measured, and rejected. On Iowa's 85,366-point dataset it picks eps ≈ 2,593 m. At that radius DBSCAN percolates: crash points chain along the road network into one county-spanning mega-cluster, not usable corridors.

The percolation sweep — Iowa

v3 sweeps a density-adaptive eps grid and takes the largest value that doesn't percolate: no cluster over 5% of the state's points.

The result

Textbook knee—
Percolation-bounded—

At the knee's ~2,593 m, this curve sits between 34% and 65% of Iowa's points in a single cluster, a county rather than a corridor. 899 m keeps every cluster under the 5% ceiling.

Clusters shape further into road-segment polylines when points are genuinely road-shaped. Otherwise a circle ships instead, with the rejection reason recorded rather than faked precision.

The leak, and the fix

Two features (distance and severity of the nearest corridor) come from these clusters. The earlier pipeline built clusters from every positive row, including the ones later used to test the model: a crash helped create the corridor whose distance then predicted that same crash. Reported ROC AUC: 0.867.

Spatial block cross-validation

Geographic blocks held out as whole units, not random rows

~22 km geographic blocks held out as whole units. A held-out block's corridor features rebuild from training data only.

The result

The exported model fits on out-of-fold corridor features, so the leak stays out of the model's learned weights, not just the metric.

Leaky, reported0.867
Leaky, reproduced0.853–0.860
Honest — spatial CV0.8205

A lower honest number is the correct outcome of fixing a leak. Per-state breakdown on the Data page.

Export: the model card travels with the model

The trained classifier converts directly to CoreML with the same 15-feature schema the Swift side expects. No field mapping to keep in sync by hand. Honest metrics, the leaky comparison, and known limitations are written into the .mlpackage itself.

Staged, not live

Exports to WildlifeRiskModel_v3.mlpackage, never overwriting the shipping model. Swapping the live model is a separate, human-performed step.

Self-documenting

Version 3.0-real-multistate-leakage-fixed-ia-il-va-ma-tn. Honest metrics and states used are embedded in the model file.

Fully on-device

No network call at inference time: WildlifeRiskModel.prediction(input:) runs locally.

Weather, applied last

The model predicts lower collision probability for fog, rain, and snow than for clear weather. Not because bad weather is safer, but because most driving happens in clear conditions, so training data holds far more clear-weather crashes than bad-weather ones. Feeding it live weather made the score go down in fog.

Rule engine and ML model blend, then weather multiplies the result last RULE-BASED ENGINE • Corridor severity (Moderate/High/Severe) • Distance decay (in-radius → 3× falloff) • Time of day — dawn/dusk Gaussian peaks • Season — fall rut ×1.5 peak • Traffic pace vs. route's expected speed • Community sightings (recency-weighted) Weather excluded here applied once, later, to the whole blend MODEL · ON-DEVICE • hour_of_day, month • distance_to_nearest_corridor_m • corridor_base_severity • species (Deer / Elk / Moose) • road_type weather_condition fixed to "clear" the model conflates exposure frequency with danger for bad weather, so it's queried at its best-calibrated, modal condition instead of the driver's real weather BLEND: 60% rule-based + 40% model both factors stay visible to the driver Live weather fog / rain / snow / clear × WEATHER MULTIPLIER — applied LAST fog ×1.4 · rain/snow ×1.25 · always ≥ 1.0 vs. clear FINAL RISK SCORE (0–100%) worse weather never lowers the score

One weather multiplier, applied last to the combined blend, not fed into the model directly.

Watch it run against real hotspots.

The simulation runs the same real hotspot data this pipeline produced.

Open the simulation →