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.
Files changed (47) hide show
  1. crashmap-0.1.0/CHANGELOG.md +28 -0
  2. crashmap-0.1.0/CITATION.cff +20 -0
  3. crashmap-0.1.0/LICENSE +21 -0
  4. crashmap-0.1.0/MANIFEST.in +10 -0
  5. crashmap-0.1.0/PKG-INFO +222 -0
  6. crashmap-0.1.0/README.md +174 -0
  7. crashmap-0.1.0/examples/screening_demo.py +54 -0
  8. crashmap-0.1.0/examples/spf_rural_two_lane.toml +11 -0
  9. crashmap-0.1.0/pyproject.toml +132 -0
  10. crashmap-0.1.0/setup.cfg +4 -0
  11. crashmap-0.1.0/src/crashmap/__init__.py +174 -0
  12. crashmap-0.1.0/src/crashmap/_special.py +91 -0
  13. crashmap-0.1.0/src/crashmap/before_after.py +251 -0
  14. crashmap-0.1.0/src/crashmap/cli.py +334 -0
  15. crashmap-0.1.0/src/crashmap/data.py +467 -0
  16. crashmap-0.1.0/src/crashmap/geometry.py +161 -0
  17. crashmap-0.1.0/src/crashmap/nb2.py +281 -0
  18. crashmap-0.1.0/src/crashmap/py.typed +0 -0
  19. crashmap-0.1.0/src/crashmap/ranking.py +91 -0
  20. crashmap-0.1.0/src/crashmap/report.py +247 -0
  21. crashmap-0.1.0/src/crashmap/screening.py +592 -0
  22. crashmap-0.1.0/src/crashmap/segmentation.py +223 -0
  23. crashmap-0.1.0/src/crashmap/severity.py +186 -0
  24. crashmap-0.1.0/src/crashmap/sites.py +248 -0
  25. crashmap-0.1.0/src/crashmap/snapping.py +333 -0
  26. crashmap-0.1.0/src/crashmap/spatial.py +404 -0
  27. crashmap-0.1.0/src/crashmap/spf.py +473 -0
  28. crashmap-0.1.0/src/crashmap/synthetic.py +150 -0
  29. crashmap-0.1.0/src/crashmap/timeparse.py +109 -0
  30. crashmap-0.1.0/src/crashmap.egg-info/PKG-INFO +222 -0
  31. crashmap-0.1.0/src/crashmap.egg-info/SOURCES.txt +45 -0
  32. crashmap-0.1.0/src/crashmap.egg-info/dependency_links.txt +1 -0
  33. crashmap-0.1.0/src/crashmap.egg-info/entry_points.txt +2 -0
  34. crashmap-0.1.0/src/crashmap.egg-info/requires.txt +26 -0
  35. crashmap-0.1.0/src/crashmap.egg-info/top_level.txt +1 -0
  36. crashmap-0.1.0/tests/conftest.py +23 -0
  37. crashmap-0.1.0/tests/data/make_nb2_reference.py +52 -0
  38. crashmap-0.1.0/tests/data/nb2_reference.csv +81 -0
  39. crashmap-0.1.0/tests/test_before_after.py +111 -0
  40. crashmap-0.1.0/tests/test_geometry_data.py +225 -0
  41. crashmap-0.1.0/tests/test_nb2_spf.py +246 -0
  42. crashmap-0.1.0/tests/test_ranking.py +49 -0
  43. crashmap-0.1.0/tests/test_report_cli.py +362 -0
  44. crashmap-0.1.0/tests/test_screening.py +322 -0
  45. crashmap-0.1.0/tests/test_segmentation_snapping.py +197 -0
  46. crashmap-0.1.0/tests/test_severity_time.py +102 -0
  47. 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]
@@ -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.
@@ -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"