crashmap 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crashmap-0.1.0/CHANGELOG.md +28 -0
- crashmap-0.1.0/CITATION.cff +20 -0
- crashmap-0.1.0/LICENSE +21 -0
- crashmap-0.1.0/MANIFEST.in +10 -0
- crashmap-0.1.0/PKG-INFO +222 -0
- crashmap-0.1.0/README.md +174 -0
- crashmap-0.1.0/examples/screening_demo.py +54 -0
- crashmap-0.1.0/examples/spf_rural_two_lane.toml +11 -0
- crashmap-0.1.0/pyproject.toml +132 -0
- crashmap-0.1.0/setup.cfg +4 -0
- crashmap-0.1.0/src/crashmap/__init__.py +174 -0
- crashmap-0.1.0/src/crashmap/_special.py +91 -0
- crashmap-0.1.0/src/crashmap/before_after.py +251 -0
- crashmap-0.1.0/src/crashmap/cli.py +334 -0
- crashmap-0.1.0/src/crashmap/data.py +467 -0
- crashmap-0.1.0/src/crashmap/geometry.py +161 -0
- crashmap-0.1.0/src/crashmap/nb2.py +281 -0
- crashmap-0.1.0/src/crashmap/py.typed +0 -0
- crashmap-0.1.0/src/crashmap/ranking.py +91 -0
- crashmap-0.1.0/src/crashmap/report.py +247 -0
- crashmap-0.1.0/src/crashmap/screening.py +592 -0
- crashmap-0.1.0/src/crashmap/segmentation.py +223 -0
- crashmap-0.1.0/src/crashmap/severity.py +186 -0
- crashmap-0.1.0/src/crashmap/sites.py +248 -0
- crashmap-0.1.0/src/crashmap/snapping.py +333 -0
- crashmap-0.1.0/src/crashmap/spatial.py +404 -0
- crashmap-0.1.0/src/crashmap/spf.py +473 -0
- crashmap-0.1.0/src/crashmap/synthetic.py +150 -0
- crashmap-0.1.0/src/crashmap/timeparse.py +109 -0
- crashmap-0.1.0/src/crashmap.egg-info/PKG-INFO +222 -0
- crashmap-0.1.0/src/crashmap.egg-info/SOURCES.txt +45 -0
- crashmap-0.1.0/src/crashmap.egg-info/dependency_links.txt +1 -0
- crashmap-0.1.0/src/crashmap.egg-info/entry_points.txt +2 -0
- crashmap-0.1.0/src/crashmap.egg-info/requires.txt +26 -0
- crashmap-0.1.0/src/crashmap.egg-info/top_level.txt +1 -0
- crashmap-0.1.0/tests/conftest.py +23 -0
- crashmap-0.1.0/tests/data/make_nb2_reference.py +52 -0
- crashmap-0.1.0/tests/data/nb2_reference.csv +81 -0
- crashmap-0.1.0/tests/test_before_after.py +111 -0
- crashmap-0.1.0/tests/test_geometry_data.py +225 -0
- crashmap-0.1.0/tests/test_nb2_spf.py +246 -0
- crashmap-0.1.0/tests/test_ranking.py +49 -0
- crashmap-0.1.0/tests/test_report_cli.py +362 -0
- crashmap-0.1.0/tests/test_screening.py +322 -0
- crashmap-0.1.0/tests/test_segmentation_snapping.py +197 -0
- crashmap-0.1.0/tests/test_severity_time.py +102 -0
- crashmap-0.1.0/tests/test_spatial.py +169 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
### Added
|
|
9
|
+
- Crash data handling: `CrashSet`, CSV / GeoJSON / (Geo)DataFrame readers and writers,
|
|
10
|
+
KABCO and fatal/serious/slight severity scales with mapping, date and time parsing.
|
|
11
|
+
- Road handling: fixed-length and homogeneous segmentation, sliding windows,
|
|
12
|
+
intersection extraction with entering-volume estimates.
|
|
13
|
+
- Snapping of crashes to segments or intersections with a maximum distance and
|
|
14
|
+
`good` / `fair` / `ambiguous` / `unmatched` quality flags.
|
|
15
|
+
- HSM Part B network screening (`screen`): crash frequency, crash rate, critical rate,
|
|
16
|
+
EPDO, relative severity index, excess predicted frequency, Empirical Bayes excess
|
|
17
|
+
expected frequency and probability of exceeding a threshold proportion; ranking with
|
|
18
|
+
explicit tie rules and top-N lists.
|
|
19
|
+
- Safety performance functions: segment, intersection and custom forms, constant or
|
|
20
|
+
length-dependent overdispersion, TOML/JSON files, pure-NumPy NB2 maximum likelihood
|
|
21
|
+
(`fit_nb2`, `fit_spf`) and CURE plot data.
|
|
22
|
+
- Before-after evaluation: naive, comparison-group and Empirical Bayes (Hauer) with CMF,
|
|
23
|
+
standard error and confidence interval.
|
|
24
|
+
- Spatial hotspots: planar KDE, network KDE (built-in or optional `nkde` backend) and
|
|
25
|
+
Getis-Ord Gi*.
|
|
26
|
+
- Markdown / HTML reports and GeoJSON export of ranked sites.
|
|
27
|
+
- Command line: `crashmap screen`, `crashmap fit-spf`, `crashmap before-after`.
|
|
28
|
+
- Synthetic data generators with known truth for testing and teaching.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "crashmap"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/crashmap_sdk"
|
|
13
|
+
url: "https://pypi.org/project/crashmap/"
|
|
14
|
+
abstract: "Road-safety network screening in Python: HSM crash frequency, rate, critical rate, EPDO, Empirical Bayes, negative binomial safety performance functions, before-after studies and crash hotspots."
|
|
15
|
+
keywords:
|
|
16
|
+
- road safety
|
|
17
|
+
- network screening
|
|
18
|
+
- Highway Safety Manual
|
|
19
|
+
- Empirical Bayes
|
|
20
|
+
- safety performance function
|
crashmap-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CITATION.cff
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include src/crashmap/py.typed
|
|
7
|
+
recursive-include src/crashmap *.py
|
|
8
|
+
recursive-include tests *.py *.csv
|
|
9
|
+
recursive-include examples *.py *.toml
|
|
10
|
+
global-exclude __pycache__ *.py[cod]
|
crashmap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crashmap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Road-safety network screening in Python: HSM crash frequency, rate, critical rate, EPDO, Empirical Bayes, negative binomial SPFs, before-after studies and crash hotspots.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/crashmap_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/crashmap_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/crashmap_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/crashmap_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: road-safety,traffic-safety,crash-analysis,network-screening,black-spot,hotspot,highway-safety-manual,empirical-bayes,safety-performance-function,negative-binomial,before-after,crash-modification-factor,epdo,kabco,gis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
22
|
+
Classifier: Intended Audience :: Science/Research
|
|
23
|
+
Classifier: Intended Audience :: Developers
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: numpy>=1.20.0
|
|
29
|
+
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
|
|
30
|
+
Provides-Extra: scipy
|
|
31
|
+
Requires-Dist: scipy>=1.7.0; extra == "scipy"
|
|
32
|
+
Provides-Extra: pandas
|
|
33
|
+
Requires-Dist: pandas>=1.3.0; extra == "pandas"
|
|
34
|
+
Provides-Extra: geo
|
|
35
|
+
Requires-Dist: geopandas>=0.12.0; extra == "geo"
|
|
36
|
+
Requires-Dist: shapely>=2.0.0; extra == "geo"
|
|
37
|
+
Provides-Extra: all
|
|
38
|
+
Requires-Dist: scipy>=1.7.0; extra == "all"
|
|
39
|
+
Requires-Dist: pandas>=1.3.0; extra == "all"
|
|
40
|
+
Requires-Dist: geopandas>=0.12.0; extra == "all"
|
|
41
|
+
Requires-Dist: shapely>=2.0.0; extra == "all"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
46
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# crashmap
|
|
50
|
+
|
|
51
|
+
**Road-safety network screening in Python.** `crashmap` ranks road segments,
|
|
52
|
+
sliding windows and intersections by crash risk with the methods of the
|
|
53
|
+
*Highway Safety Manual* (AASHTO 2010, Part B, Chapter 4), fits negative binomial
|
|
54
|
+
safety performance functions (SPFs), computes Empirical Bayes (EB) estimates,
|
|
55
|
+
evaluates treatments with before-after studies and finds crash hotspots.
|
|
56
|
+
|
|
57
|
+
It is written for road-safety engineers and analysts who identify high-risk
|
|
58
|
+
sites (black spots) from crash records. The core needs only NumPy; SciPy,
|
|
59
|
+
pandas and GeoPandas are optional.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install crashmap # NumPy only
|
|
63
|
+
pip install "crashmap[all]" # + SciPy, pandas, GeoPandas/Shapely
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import crashmap as cm
|
|
70
|
+
|
|
71
|
+
crashes = cm.read_crashes_csv("crashes.csv") # x, y, severity, date ...
|
|
72
|
+
roads = cm.read_roads_geojson("roads.geojson") # LineStrings with an "aadt" attribute
|
|
73
|
+
|
|
74
|
+
segments = cm.fixed_length_segments(roads, 500) # 500 m sections
|
|
75
|
+
snap = cm.snap_to_lines(crashes, segments, max_distance=25)
|
|
76
|
+
print(snap.summary()) # good / fair / ambiguous / unmatched
|
|
77
|
+
|
|
78
|
+
sites = cm.build_segment_sites(segments, crashes, snap, years=5)
|
|
79
|
+
spf = cm.load_spf("spf.toml") # or cm.SPF.segment(a, b, k=...)
|
|
80
|
+
result = cm.screen(sites, "eb", spf=spf)
|
|
81
|
+
|
|
82
|
+
for row in result.top(10): # ties at the cut are kept
|
|
83
|
+
print(row["rank"], row["id"], round(row["score"], 2), row["expected"])
|
|
84
|
+
|
|
85
|
+
cm.write_geojson(result, "ranked.geojson")
|
|
86
|
+
cm.write_report(result, "screening.html")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Coordinates must be planar (metres in a projected CRS). The CLI detects
|
|
90
|
+
longitude/latitude input and projects it to a local metric frame
|
|
91
|
+
(`crashmap.LocalProjection`); output GeoJSON is written back in lon/lat.
|
|
92
|
+
|
|
93
|
+
### Command line
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
crashmap screen crashes.csv roads.geojson --method eb --spf spf.toml -o ranked.geojson --report report.html
|
|
97
|
+
crashmap screen crashes.csv roads.geojson --method critical-rate --segment-length 500
|
|
98
|
+
crashmap screen crashes.csv roads.geojson --site-type window --window 480 --step 160
|
|
99
|
+
crashmap screen crashes.csv roads.geojson --site-type intersection --max-distance 76 --method rate
|
|
100
|
+
crashmap fit-spf sites.csv --years 5 --length-unit km -o spf.toml --cure cure.csv
|
|
101
|
+
crashmap before-after treated.csv --method eb --k 0.4 -o result.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`crashmap <command> --help` lists every option (column names, severity scale,
|
|
105
|
+
`--dayfirst` dates, EPDO weights, confidence level, ...).
|
|
106
|
+
|
|
107
|
+
## Methods
|
|
108
|
+
|
|
109
|
+
| Method (`screen(method=...)`) | Score (higher = more promising) | Needs | Reference |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| `frequency` | Average crash frequency, crashes/year | crashes | HSM Ch. 4 |
|
|
112
|
+
| `rate` | Crashes per million vehicle-km (or -mi); per million entering vehicles at intersections | AADT, length / entering volume | HSM Ch. 4 |
|
|
113
|
+
| `critical-rate` | Observed minus critical rate, `R_c = R_a + P sqrt(R_a/E) + 1/(2E)` | AADT, length / entering volume | HSM Ch. 4 |
|
|
114
|
+
| `epdo` | EPDO average crash frequency, `sum(w_s N_s)/years`; default weights K 542, A/B/C 11, O 1 | severity | HSM Ch. 4 |
|
|
115
|
+
| `rsi` | Relative severity index: average crash cost (by severity or crash type) | severity or type, costs | HSM Ch. 4 |
|
|
116
|
+
| `excess-predicted` | `(N_obs - N_pred)/years` | SPF | HSM Ch. 4 |
|
|
117
|
+
| `eb` | Excess expected frequency `(N_exp - N_pred)/years`, `N_exp = w N_pred + (1-w) N_obs`, `w = 1/(1 + k sum N_pred)` | SPF with *k* | HSM Ch. 4; Part C App. A |
|
|
118
|
+
| `proportion` | Probability that the share of a crash type exceeds the threshold proportion (Beta-binomial) | crash type or severity | HSM Ch. 4 |
|
|
119
|
+
|
|
120
|
+
Other building blocks:
|
|
121
|
+
|
|
122
|
+
| Area | Functions |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Crash data | `read_crashes_csv`, `read_crashes_geojson`, `crashes_from_dataframe`, `CrashSet` (KABCO severities, dates, crash types), `normalize_severity` (KABCO, fatal/serious/slight, labels, 1..5 codes), `parse_datetime` |
|
|
125
|
+
| Roads and sites | `read_roads_geojson`, `roads_from_geodataframe`, `fixed_length_segments`, `homogeneous_segments`, `sliding_windows`, `extract_intersections`, `build_segment_sites`, `build_window_sites`, `build_intersection_sites` |
|
|
126
|
+
| Snapping | `snap_to_lines`, `snap_to_points` with `max_distance` and quality flags `good` / `fair` / `ambiguous` / `unmatched` |
|
|
127
|
+
| SPFs | `SPF.segment` (`N = L exp(a + b ln AADT)`), `SPF.intersection` (`N = exp(a + b ln AADT_maj + c ln AADT_min)`), custom forms, `k` constant or `k/L`, TOML/JSON I/O, `fit_spf`, `fit_nb2` (pure-NumPy NB2 maximum likelihood), `cure_plot` |
|
|
128
|
+
| Before-after | `naive_before_after`, `comparison_group_before_after`, `eb_before_after` -> CMF, standard error, confidence interval (Hauer 1997) |
|
|
129
|
+
| Spatial | `kde_grid` / `kde_at` (planar KDE), `network_kde` (built-in simple NKDE or an optional `nkde` backend), `getis_ord_gi_star` with distance-band or touching weights |
|
|
130
|
+
| Ranking and output | `rank` (min/max/dense/ordinal/average ties), `top_n`, `to_markdown`, `to_html` (escaped), `ranked_geojson`, `write_geojson` |
|
|
131
|
+
| Synthetic data | `synthetic_network`, `synthetic_sites`, `simulate_nb_counts` -- crashes drawn from a known SPF |
|
|
132
|
+
|
|
133
|
+
### SPF files
|
|
134
|
+
|
|
135
|
+
```toml
|
|
136
|
+
[spf]
|
|
137
|
+
name = "Rural two-lane segments (HSM base model)"
|
|
138
|
+
form = "segment" # N = L * exp(a + b ln AADT), crashes per year
|
|
139
|
+
a = -8.2276 # ln(365e-6) - 0.312
|
|
140
|
+
b = 1.0
|
|
141
|
+
k = 0.236
|
|
142
|
+
k_form = "per_length" # k_i = k / L_i
|
|
143
|
+
length_unit = "mi"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`form = "intersection"` takes `a`, `b`, `c`; `form = "custom"` takes an
|
|
147
|
+
`intercept`, an `exposure` variable and `[[spf.terms]]` tables with
|
|
148
|
+
`variable`, `coefficient` and `transform` (`ln` or `linear`). `fit_spf(...).spf.save("spf.toml")`
|
|
149
|
+
writes the same layout. SPFs must be calibrated to local conditions before use.
|
|
150
|
+
|
|
151
|
+
## Validation
|
|
152
|
+
|
|
153
|
+
The test suite (98 % line coverage) checks the implementation against
|
|
154
|
+
independent results; all data are synthetic.
|
|
155
|
+
|
|
156
|
+
* **NB2 maximum likelihood vs statsmodels.** On a synthetic 80-site data set
|
|
157
|
+
(`tests/data/nb2_reference.csv`), `fit_nb2` reproduces statsmodels 0.15.0
|
|
158
|
+
`NegativeBinomial(loglike_method="nb2")` to 1e-12: coefficients
|
|
159
|
+
-5.387829 and 0.673613, alpha 0.282714, standard errors 1.508463, 0.157971
|
|
160
|
+
and 0.085230, log-likelihood -186.414967.
|
|
161
|
+
* **Parameter recovery.** Crashes simulated from `N = L exp(-6.2 + 0.75 ln AADT)`
|
|
162
|
+
with k = 0.5 (3,000 sites) are fitted back within four standard errors;
|
|
163
|
+
CURE residuals stay inside the +/-2 sigma band.
|
|
164
|
+
* **Hand calculations.** EB weights and expected crashes (including the HSM rural
|
|
165
|
+
two-lane base model with `k = 0.236/L`), critical rates and the HSM P values
|
|
166
|
+
(1.036/1.282/1.645/2.326), crash rates, EPDO and RSI, the threshold-proportion
|
|
167
|
+
Beta prior, and Hauer's naive, comparison-group and EB before-after formulas.
|
|
168
|
+
* **Known truth.** On a synthetic grid where two roads carry four times the SPF
|
|
169
|
+
risk, `eb` and `excess-predicted` rank exactly those two roads first; EB
|
|
170
|
+
estimates have lower error than raw counts; a simulated treatment with
|
|
171
|
+
CMF 0.70 applied to high-count sites is recovered by the EB before-after
|
|
172
|
+
study (0.703 +/- 0.008) while the naive study overstates the effect (0.669).
|
|
173
|
+
* **Ranking edge cases.** All tie rules, `nan` scores, empty input, and top-N cuts
|
|
174
|
+
through ties.
|
|
175
|
+
|
|
176
|
+
## Limitations
|
|
177
|
+
|
|
178
|
+
* Planar geometry only; use a projected CRS (or the local projection, adequate
|
|
179
|
+
for city- and corridor-scale studies).
|
|
180
|
+
* Roads must be noded (split at junctions) for intersection extraction and
|
|
181
|
+
network KDE. Crashes are snapped to the geometrically nearest site; junction
|
|
182
|
+
crashes are flagged `ambiguous` but not reassigned.
|
|
183
|
+
* SPFs are user supplied or fitted with a constant overdispersion. `k_form="per_length"`
|
|
184
|
+
in `fit_spf` is an approximation (alpha scaled at the mean length), not a
|
|
185
|
+
length-dependent dispersion model. No calibration factor (C) is estimated.
|
|
186
|
+
* The EB screening uses totals over the study period without yearly
|
|
187
|
+
calibration factors (HSM `C_y`); `proportion` requires extra-binomial
|
|
188
|
+
variation between sites and raises an error otherwise.
|
|
189
|
+
* Before-after standard errors use Hauer's large-sample approximations; the
|
|
190
|
+
comparison-group method assumes a Poisson comparison ratio unless its variance
|
|
191
|
+
is given. Confidence intervals are normal approximations.
|
|
192
|
+
* The built-in network KDE is the "simple" kernel of Xie & Yan (2008), which
|
|
193
|
+
inflates densities at junctions with three or more legs. The optional `nkde`
|
|
194
|
+
backend (or any callable) can replace it.
|
|
195
|
+
* Getis-Ord p-values are not corrected for multiple testing.
|
|
196
|
+
* Screening ranks sites for diagnosis; it does not establish causes or
|
|
197
|
+
countermeasures. Crash records are personal data: keep them out of public
|
|
198
|
+
repositories.
|
|
199
|
+
|
|
200
|
+
## References
|
|
201
|
+
|
|
202
|
+
* AASHTO (2010). *Highway Safety Manual*, 1st ed. American Association of State
|
|
203
|
+
Highway and Transportation Officials, Washington, DC. Part B, Chapter 4
|
|
204
|
+
(network screening); Part C, Appendix A (EB method).
|
|
205
|
+
* Hauer, E. (1997). *Observational Before-After Studies in Road Safety.* Pergamon, Oxford.
|
|
206
|
+
* Hauer, E., Harwood, D. W., Council, F. M., Griffith, M. S. (2002). Estimating safety
|
|
207
|
+
by the empirical Bayes method: a tutorial. *Transportation Research Record* 1784, 126-131.
|
|
208
|
+
* Persaud, B., Lyon, C. (2007). Empirical Bayes before-after safety studies: lessons
|
|
209
|
+
learned from two decades of experience and future directions. *Accident Analysis &
|
|
210
|
+
Prevention* 39(3), 546-555.
|
|
211
|
+
* Hauer, E., Bamfo, J. (1997). Two tools for finding what function links the dependent
|
|
212
|
+
variable to the explanatory variables. *Proceedings of the ICTCT 1997 Conference*, Lund.
|
|
213
|
+
* Cameron, A. C., Trivedi, P. K. (2013). *Regression Analysis of Count Data*, 2nd ed.
|
|
214
|
+
Cambridge University Press.
|
|
215
|
+
* Ord, J. K., Getis, A. (1995). Local spatial autocorrelation statistics: distributional
|
|
216
|
+
issues and an application. *Geographical Analysis* 27(4), 286-306.
|
|
217
|
+
* Xie, Z., Yan, J. (2008). Kernel density estimation of traffic accidents in a network
|
|
218
|
+
space. *Computers, Environment and Urban Systems* 32(5), 396-406.
|
|
219
|
+
|
|
220
|
+
## Citation
|
|
221
|
+
|
|
222
|
+
See `CITATION.cff`. MIT licensed, (c) 2026 Yusuf Eminoğlu.
|
crashmap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# crashmap
|
|
2
|
+
|
|
3
|
+
**Road-safety network screening in Python.** `crashmap` ranks road segments,
|
|
4
|
+
sliding windows and intersections by crash risk with the methods of the
|
|
5
|
+
*Highway Safety Manual* (AASHTO 2010, Part B, Chapter 4), fits negative binomial
|
|
6
|
+
safety performance functions (SPFs), computes Empirical Bayes (EB) estimates,
|
|
7
|
+
evaluates treatments with before-after studies and finds crash hotspots.
|
|
8
|
+
|
|
9
|
+
It is written for road-safety engineers and analysts who identify high-risk
|
|
10
|
+
sites (black spots) from crash records. The core needs only NumPy; SciPy,
|
|
11
|
+
pandas and GeoPandas are optional.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install crashmap # NumPy only
|
|
15
|
+
pip install "crashmap[all]" # + SciPy, pandas, GeoPandas/Shapely
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import crashmap as cm
|
|
22
|
+
|
|
23
|
+
crashes = cm.read_crashes_csv("crashes.csv") # x, y, severity, date ...
|
|
24
|
+
roads = cm.read_roads_geojson("roads.geojson") # LineStrings with an "aadt" attribute
|
|
25
|
+
|
|
26
|
+
segments = cm.fixed_length_segments(roads, 500) # 500 m sections
|
|
27
|
+
snap = cm.snap_to_lines(crashes, segments, max_distance=25)
|
|
28
|
+
print(snap.summary()) # good / fair / ambiguous / unmatched
|
|
29
|
+
|
|
30
|
+
sites = cm.build_segment_sites(segments, crashes, snap, years=5)
|
|
31
|
+
spf = cm.load_spf("spf.toml") # or cm.SPF.segment(a, b, k=...)
|
|
32
|
+
result = cm.screen(sites, "eb", spf=spf)
|
|
33
|
+
|
|
34
|
+
for row in result.top(10): # ties at the cut are kept
|
|
35
|
+
print(row["rank"], row["id"], round(row["score"], 2), row["expected"])
|
|
36
|
+
|
|
37
|
+
cm.write_geojson(result, "ranked.geojson")
|
|
38
|
+
cm.write_report(result, "screening.html")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Coordinates must be planar (metres in a projected CRS). The CLI detects
|
|
42
|
+
longitude/latitude input and projects it to a local metric frame
|
|
43
|
+
(`crashmap.LocalProjection`); output GeoJSON is written back in lon/lat.
|
|
44
|
+
|
|
45
|
+
### Command line
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
crashmap screen crashes.csv roads.geojson --method eb --spf spf.toml -o ranked.geojson --report report.html
|
|
49
|
+
crashmap screen crashes.csv roads.geojson --method critical-rate --segment-length 500
|
|
50
|
+
crashmap screen crashes.csv roads.geojson --site-type window --window 480 --step 160
|
|
51
|
+
crashmap screen crashes.csv roads.geojson --site-type intersection --max-distance 76 --method rate
|
|
52
|
+
crashmap fit-spf sites.csv --years 5 --length-unit km -o spf.toml --cure cure.csv
|
|
53
|
+
crashmap before-after treated.csv --method eb --k 0.4 -o result.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`crashmap <command> --help` lists every option (column names, severity scale,
|
|
57
|
+
`--dayfirst` dates, EPDO weights, confidence level, ...).
|
|
58
|
+
|
|
59
|
+
## Methods
|
|
60
|
+
|
|
61
|
+
| Method (`screen(method=...)`) | Score (higher = more promising) | Needs | Reference |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `frequency` | Average crash frequency, crashes/year | crashes | HSM Ch. 4 |
|
|
64
|
+
| `rate` | Crashes per million vehicle-km (or -mi); per million entering vehicles at intersections | AADT, length / entering volume | HSM Ch. 4 |
|
|
65
|
+
| `critical-rate` | Observed minus critical rate, `R_c = R_a + P sqrt(R_a/E) + 1/(2E)` | AADT, length / entering volume | HSM Ch. 4 |
|
|
66
|
+
| `epdo` | EPDO average crash frequency, `sum(w_s N_s)/years`; default weights K 542, A/B/C 11, O 1 | severity | HSM Ch. 4 |
|
|
67
|
+
| `rsi` | Relative severity index: average crash cost (by severity or crash type) | severity or type, costs | HSM Ch. 4 |
|
|
68
|
+
| `excess-predicted` | `(N_obs - N_pred)/years` | SPF | HSM Ch. 4 |
|
|
69
|
+
| `eb` | Excess expected frequency `(N_exp - N_pred)/years`, `N_exp = w N_pred + (1-w) N_obs`, `w = 1/(1 + k sum N_pred)` | SPF with *k* | HSM Ch. 4; Part C App. A |
|
|
70
|
+
| `proportion` | Probability that the share of a crash type exceeds the threshold proportion (Beta-binomial) | crash type or severity | HSM Ch. 4 |
|
|
71
|
+
|
|
72
|
+
Other building blocks:
|
|
73
|
+
|
|
74
|
+
| Area | Functions |
|
|
75
|
+
|---|---|
|
|
76
|
+
| Crash data | `read_crashes_csv`, `read_crashes_geojson`, `crashes_from_dataframe`, `CrashSet` (KABCO severities, dates, crash types), `normalize_severity` (KABCO, fatal/serious/slight, labels, 1..5 codes), `parse_datetime` |
|
|
77
|
+
| Roads and sites | `read_roads_geojson`, `roads_from_geodataframe`, `fixed_length_segments`, `homogeneous_segments`, `sliding_windows`, `extract_intersections`, `build_segment_sites`, `build_window_sites`, `build_intersection_sites` |
|
|
78
|
+
| Snapping | `snap_to_lines`, `snap_to_points` with `max_distance` and quality flags `good` / `fair` / `ambiguous` / `unmatched` |
|
|
79
|
+
| SPFs | `SPF.segment` (`N = L exp(a + b ln AADT)`), `SPF.intersection` (`N = exp(a + b ln AADT_maj + c ln AADT_min)`), custom forms, `k` constant or `k/L`, TOML/JSON I/O, `fit_spf`, `fit_nb2` (pure-NumPy NB2 maximum likelihood), `cure_plot` |
|
|
80
|
+
| Before-after | `naive_before_after`, `comparison_group_before_after`, `eb_before_after` -> CMF, standard error, confidence interval (Hauer 1997) |
|
|
81
|
+
| Spatial | `kde_grid` / `kde_at` (planar KDE), `network_kde` (built-in simple NKDE or an optional `nkde` backend), `getis_ord_gi_star` with distance-band or touching weights |
|
|
82
|
+
| Ranking and output | `rank` (min/max/dense/ordinal/average ties), `top_n`, `to_markdown`, `to_html` (escaped), `ranked_geojson`, `write_geojson` |
|
|
83
|
+
| Synthetic data | `synthetic_network`, `synthetic_sites`, `simulate_nb_counts` -- crashes drawn from a known SPF |
|
|
84
|
+
|
|
85
|
+
### SPF files
|
|
86
|
+
|
|
87
|
+
```toml
|
|
88
|
+
[spf]
|
|
89
|
+
name = "Rural two-lane segments (HSM base model)"
|
|
90
|
+
form = "segment" # N = L * exp(a + b ln AADT), crashes per year
|
|
91
|
+
a = -8.2276 # ln(365e-6) - 0.312
|
|
92
|
+
b = 1.0
|
|
93
|
+
k = 0.236
|
|
94
|
+
k_form = "per_length" # k_i = k / L_i
|
|
95
|
+
length_unit = "mi"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`form = "intersection"` takes `a`, `b`, `c`; `form = "custom"` takes an
|
|
99
|
+
`intercept`, an `exposure` variable and `[[spf.terms]]` tables with
|
|
100
|
+
`variable`, `coefficient` and `transform` (`ln` or `linear`). `fit_spf(...).spf.save("spf.toml")`
|
|
101
|
+
writes the same layout. SPFs must be calibrated to local conditions before use.
|
|
102
|
+
|
|
103
|
+
## Validation
|
|
104
|
+
|
|
105
|
+
The test suite (98 % line coverage) checks the implementation against
|
|
106
|
+
independent results; all data are synthetic.
|
|
107
|
+
|
|
108
|
+
* **NB2 maximum likelihood vs statsmodels.** On a synthetic 80-site data set
|
|
109
|
+
(`tests/data/nb2_reference.csv`), `fit_nb2` reproduces statsmodels 0.15.0
|
|
110
|
+
`NegativeBinomial(loglike_method="nb2")` to 1e-12: coefficients
|
|
111
|
+
-5.387829 and 0.673613, alpha 0.282714, standard errors 1.508463, 0.157971
|
|
112
|
+
and 0.085230, log-likelihood -186.414967.
|
|
113
|
+
* **Parameter recovery.** Crashes simulated from `N = L exp(-6.2 + 0.75 ln AADT)`
|
|
114
|
+
with k = 0.5 (3,000 sites) are fitted back within four standard errors;
|
|
115
|
+
CURE residuals stay inside the +/-2 sigma band.
|
|
116
|
+
* **Hand calculations.** EB weights and expected crashes (including the HSM rural
|
|
117
|
+
two-lane base model with `k = 0.236/L`), critical rates and the HSM P values
|
|
118
|
+
(1.036/1.282/1.645/2.326), crash rates, EPDO and RSI, the threshold-proportion
|
|
119
|
+
Beta prior, and Hauer's naive, comparison-group and EB before-after formulas.
|
|
120
|
+
* **Known truth.** On a synthetic grid where two roads carry four times the SPF
|
|
121
|
+
risk, `eb` and `excess-predicted` rank exactly those two roads first; EB
|
|
122
|
+
estimates have lower error than raw counts; a simulated treatment with
|
|
123
|
+
CMF 0.70 applied to high-count sites is recovered by the EB before-after
|
|
124
|
+
study (0.703 +/- 0.008) while the naive study overstates the effect (0.669).
|
|
125
|
+
* **Ranking edge cases.** All tie rules, `nan` scores, empty input, and top-N cuts
|
|
126
|
+
through ties.
|
|
127
|
+
|
|
128
|
+
## Limitations
|
|
129
|
+
|
|
130
|
+
* Planar geometry only; use a projected CRS (or the local projection, adequate
|
|
131
|
+
for city- and corridor-scale studies).
|
|
132
|
+
* Roads must be noded (split at junctions) for intersection extraction and
|
|
133
|
+
network KDE. Crashes are snapped to the geometrically nearest site; junction
|
|
134
|
+
crashes are flagged `ambiguous` but not reassigned.
|
|
135
|
+
* SPFs are user supplied or fitted with a constant overdispersion. `k_form="per_length"`
|
|
136
|
+
in `fit_spf` is an approximation (alpha scaled at the mean length), not a
|
|
137
|
+
length-dependent dispersion model. No calibration factor (C) is estimated.
|
|
138
|
+
* The EB screening uses totals over the study period without yearly
|
|
139
|
+
calibration factors (HSM `C_y`); `proportion` requires extra-binomial
|
|
140
|
+
variation between sites and raises an error otherwise.
|
|
141
|
+
* Before-after standard errors use Hauer's large-sample approximations; the
|
|
142
|
+
comparison-group method assumes a Poisson comparison ratio unless its variance
|
|
143
|
+
is given. Confidence intervals are normal approximations.
|
|
144
|
+
* The built-in network KDE is the "simple" kernel of Xie & Yan (2008), which
|
|
145
|
+
inflates densities at junctions with three or more legs. The optional `nkde`
|
|
146
|
+
backend (or any callable) can replace it.
|
|
147
|
+
* Getis-Ord p-values are not corrected for multiple testing.
|
|
148
|
+
* Screening ranks sites for diagnosis; it does not establish causes or
|
|
149
|
+
countermeasures. Crash records are personal data: keep them out of public
|
|
150
|
+
repositories.
|
|
151
|
+
|
|
152
|
+
## References
|
|
153
|
+
|
|
154
|
+
* AASHTO (2010). *Highway Safety Manual*, 1st ed. American Association of State
|
|
155
|
+
Highway and Transportation Officials, Washington, DC. Part B, Chapter 4
|
|
156
|
+
(network screening); Part C, Appendix A (EB method).
|
|
157
|
+
* Hauer, E. (1997). *Observational Before-After Studies in Road Safety.* Pergamon, Oxford.
|
|
158
|
+
* Hauer, E., Harwood, D. W., Council, F. M., Griffith, M. S. (2002). Estimating safety
|
|
159
|
+
by the empirical Bayes method: a tutorial. *Transportation Research Record* 1784, 126-131.
|
|
160
|
+
* Persaud, B., Lyon, C. (2007). Empirical Bayes before-after safety studies: lessons
|
|
161
|
+
learned from two decades of experience and future directions. *Accident Analysis &
|
|
162
|
+
Prevention* 39(3), 546-555.
|
|
163
|
+
* Hauer, E., Bamfo, J. (1997). Two tools for finding what function links the dependent
|
|
164
|
+
variable to the explanatory variables. *Proceedings of the ICTCT 1997 Conference*, Lund.
|
|
165
|
+
* Cameron, A. C., Trivedi, P. K. (2013). *Regression Analysis of Count Data*, 2nd ed.
|
|
166
|
+
Cambridge University Press.
|
|
167
|
+
* Ord, J. K., Getis, A. (1995). Local spatial autocorrelation statistics: distributional
|
|
168
|
+
issues and an application. *Geographical Analysis* 27(4), 286-306.
|
|
169
|
+
* Xie, Z., Yan, J. (2008). Kernel density estimation of traffic accidents in a network
|
|
170
|
+
space. *Computers, Environment and Urban Systems* 32(5), 396-406.
|
|
171
|
+
|
|
172
|
+
## Citation
|
|
173
|
+
|
|
174
|
+
See `CITATION.cff`. MIT licensed, (c) 2026 Yusuf Eminoğlu.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""End-to-end network screening on synthetic data (no real crash records).
|
|
2
|
+
|
|
3
|
+
Run: python examples/screening_demo.py [output_dir]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import crashmap as cm
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(out_dir: str = "crashmap_demo") -> None:
|
|
15
|
+
out = Path(out_dir)
|
|
16
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
# 1. Synthetic network: roads r5 and r20 are four times riskier than the SPF says.
|
|
19
|
+
roads, crashes = cm.synthetic_network(hot_roads={"r5": 4.0, "r20": 4.0}, seed=1)
|
|
20
|
+
cm.write_roads_geojson(roads, out / "roads.geojson")
|
|
21
|
+
cm.write_crashes_csv(crashes, out / "crashes.csv")
|
|
22
|
+
|
|
23
|
+
# 2. Segment, snap and count.
|
|
24
|
+
segments = cm.roads_as_segments(roads)
|
|
25
|
+
snap = cm.snap_to_lines(crashes, segments, max_distance=25)
|
|
26
|
+
print("snapping:", snap.summary())
|
|
27
|
+
sites = cm.build_segment_sites(segments, crashes, snap)
|
|
28
|
+
|
|
29
|
+
# 3. Fit an SPF to the network itself (in practice: a calibrated, published SPF).
|
|
30
|
+
fitted = cm.fit_spf(
|
|
31
|
+
[s.total for s in sites],
|
|
32
|
+
{"aadt": [s.aadt for s in sites], "length": [s.length / 1000 for s in sites]},
|
|
33
|
+
years=sites[0].years,
|
|
34
|
+
)
|
|
35
|
+
print(fitted.result.summary())
|
|
36
|
+
fitted.spf.save(out / "spf.toml")
|
|
37
|
+
|
|
38
|
+
# 4. Screen with Empirical Bayes and other measures.
|
|
39
|
+
for method in ("frequency", "critical-rate", "epdo", "eb"):
|
|
40
|
+
res = cm.screen(sites, method, spf=fitted.spf)
|
|
41
|
+
print(method, [r["id"] for r in res.top(3)])
|
|
42
|
+
res = cm.screen(sites, "eb", spf=fitted.spf)
|
|
43
|
+
cm.write_geojson(res, out / "ranked.geojson")
|
|
44
|
+
cm.write_report(res, out / "report.html")
|
|
45
|
+
|
|
46
|
+
# 5. Hotspots: Getis-Ord Gi* on segment crash frequencies.
|
|
47
|
+
gi = cm.getis_ord_gi_star([s.total for s in sites], cm.touching_weights(segments))
|
|
48
|
+
hot = [s.id for s, lbl in zip(sites, gi.label) if lbl.startswith("hot")]
|
|
49
|
+
print("Gi* hot segments:", hot)
|
|
50
|
+
print(f"outputs written to {out.resolve()}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main(*sys.argv[1:2])
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# HSM (2010) Part C base SPF for rural two-lane two-way road segments:
|
|
2
|
+
# N = AADT * L * 365e-6 * exp(-0.312), k = 0.236 / L (L in miles).
|
|
3
|
+
# Written in crashmap's log-linear form: N = L * exp(a + b ln AADT).
|
|
4
|
+
[spf]
|
|
5
|
+
name = "Rural two-lane segments (HSM base model)"
|
|
6
|
+
form = "segment"
|
|
7
|
+
a = -8.227613204381782
|
|
8
|
+
b = 1.0
|
|
9
|
+
k = 0.236
|
|
10
|
+
k_form = "per_length"
|
|
11
|
+
length_unit = "mi"
|