Metadata-Version: 2.4
Name: crashmap
Version: 0.1.1
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.
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/crashmap_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/crashmap_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/crashmap_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/crashmap_sdk/blob/main/CHANGELOG.md
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
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.20.0
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
Provides-Extra: scipy
Requires-Dist: scipy>=1.7.0; extra == "scipy"
Provides-Extra: pandas
Requires-Dist: pandas>=1.3.0; extra == "pandas"
Provides-Extra: geo
Requires-Dist: geopandas>=0.12.0; extra == "geo"
Requires-Dist: shapely>=2.0.0; extra == "geo"
Provides-Extra: all
Requires-Dist: scipy>=1.7.0; extra == "all"
Requires-Dist: pandas>=1.3.0; extra == "all"
Requires-Dist: geopandas>=0.12.0; extra == "all"
Requires-Dist: shapely>=2.0.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# crashmap

**Road-safety network screening in Python.** `crashmap` ranks road segments,
sliding windows and intersections by crash risk with the methods of the
*Highway Safety Manual* (AASHTO 2010, Part B, Chapter 4), fits negative binomial
safety performance functions (SPFs), computes Empirical Bayes (EB) estimates,
evaluates treatments with before-after studies and finds crash hotspots.

It is written for road-safety engineers and analysts who identify high-risk
sites (black spots) from crash records. The core needs only NumPy; SciPy,
pandas and GeoPandas are optional.

```bash
pip install crashmap            # NumPy only
pip install "crashmap[all]"     # + SciPy, pandas, GeoPandas/Shapely
```

## Quick start

```python
import crashmap as cm

crashes = cm.read_crashes_csv("crashes.csv")           # x, y, severity, date ...
roads = cm.read_roads_geojson("roads.geojson")         # LineStrings with an "aadt" attribute

segments = cm.fixed_length_segments(roads, 500)        # 500 m sections
snap = cm.snap_to_lines(crashes, segments, max_distance=25)
print(snap.summary())                                  # good / fair / ambiguous / unmatched

sites = cm.build_segment_sites(segments, crashes, snap, years=5)
spf = cm.load_spf("spf.toml")                          # or cm.SPF.segment(a, b, k=...)
result = cm.screen(sites, "eb", spf=spf)

for row in result.top(10):                             # ties at the cut are kept
    print(row["rank"], row["id"], round(row["score"], 2), row["expected"])

cm.write_geojson(result, "ranked.geojson")
cm.write_report(result, "screening.html")
```

Coordinates must be planar (metres in a projected CRS). The CLI detects
longitude/latitude input and projects it to a local metric frame
(`crashmap.LocalProjection`); output GeoJSON is written back in lon/lat.

### Command line

```bash
crashmap screen crashes.csv roads.geojson --method eb --spf spf.toml -o ranked.geojson --report report.html
crashmap screen crashes.csv roads.geojson --method critical-rate --segment-length 500
crashmap screen crashes.csv roads.geojson --site-type window --window 480 --step 160
crashmap screen crashes.csv roads.geojson --site-type intersection --max-distance 76 --method rate
crashmap fit-spf sites.csv --years 5 --length-unit km -o spf.toml --cure cure.csv
crashmap before-after treated.csv --method eb --k 0.4 -o result.json
```

`crashmap <command> --help` lists every option (column names, severity scale,
`--dayfirst` dates, EPDO weights, confidence level, ...).

## Methods

| Method (`screen(method=...)`) | Score (higher = more promising) | Needs | Reference |
|---|---|---|---|
| `frequency` | Average crash frequency, crashes/year | crashes | HSM Ch. 4 |
| `rate` | Crashes per million vehicle-km (or -mi); per million entering vehicles at intersections | AADT, length / entering volume | HSM Ch. 4 |
| `critical-rate` | Observed minus critical rate, `R_c = R_a + P sqrt(R_a/E) + 1/(2E)` | AADT, length / entering volume | HSM Ch. 4 |
| `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 |
| `rsi` | Relative severity index: average crash cost (by severity or crash type) | severity or type, costs | HSM Ch. 4 |
| `excess-predicted` | `(N_obs - N_pred)/years` | SPF | HSM Ch. 4 |
| `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 |
| `proportion` | Probability that the share of a crash type exceeds the threshold proportion (Beta-binomial) | crash type or severity | HSM Ch. 4 |

Other building blocks:

| Area | Functions |
|---|---|
| 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` |
| 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` |
| Snapping | `snap_to_lines`, `snap_to_points` with `max_distance` and quality flags `good` / `fair` / `ambiguous` / `unmatched` |
| 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` |
| Before-after | `naive_before_after`, `comparison_group_before_after`, `eb_before_after` -> CMF, standard error, confidence interval (Hauer 1997) |
| 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 |
| Ranking and output | `rank` (min/max/dense/ordinal/average ties), `top_n`, `to_markdown`, `to_html` (escaped), `ranked_geojson`, `write_geojson` |
| Synthetic data | `synthetic_network`, `synthetic_sites`, `simulate_nb_counts` -- crashes drawn from a known SPF |

### SPF files

```toml
[spf]
name = "Rural two-lane segments (HSM base model)"
form = "segment"          # N = L * exp(a + b ln AADT), crashes per year
a = -8.2276               # ln(365e-6) - 0.312
b = 1.0
k = 0.236
k_form = "per_length"     # k_i = k / L_i
length_unit = "mi"
```

`form = "intersection"` takes `a`, `b`, `c`; `form = "custom"` takes an
`intercept`, an `exposure` variable and `[[spf.terms]]` tables with
`variable`, `coefficient` and `transform` (`ln` or `linear`). `fit_spf(...).spf.save("spf.toml")`
writes the same layout. SPFs must be calibrated to local conditions before use.

## Validation

The test suite (98 % line coverage) checks the implementation against
independent results; all data are synthetic.

* **NB2 maximum likelihood vs statsmodels.** On a synthetic 80-site data set
  (`tests/data/nb2_reference.csv`), `fit_nb2` reproduces statsmodels 0.15.0
  `NegativeBinomial(loglike_method="nb2")` to 1e-12: coefficients
  -5.387829 and 0.673613, alpha 0.282714, standard errors 1.508463, 0.157971
  and 0.085230, log-likelihood -186.414967.
* **Parameter recovery.** Crashes simulated from `N = L exp(-6.2 + 0.75 ln AADT)`
  with k = 0.5 (3,000 sites) are fitted back within four standard errors;
  CURE residuals stay inside the +/-2 sigma band.
* **Hand calculations.** EB weights and expected crashes (including the HSM rural
  two-lane base model with `k = 0.236/L`), critical rates and the HSM P values
  (1.036/1.282/1.645/2.326), crash rates, EPDO and RSI, the threshold-proportion
  Beta prior, and Hauer's naive, comparison-group and EB before-after formulas.
* **Known truth.** On a synthetic grid where two roads carry four times the SPF
  risk, `eb` and `excess-predicted` rank exactly those two roads first; EB
  estimates have lower error than raw counts; a simulated treatment with
  CMF 0.70 applied to high-count sites is recovered by the EB before-after
  study (0.703 +/- 0.008) while the naive study overstates the effect (0.669).
* **Ranking edge cases.** All tie rules, `nan` scores, empty input, and top-N cuts
  through ties.

## Limitations

* Planar geometry only; use a projected CRS (or the local projection, adequate
  for city- and corridor-scale studies).
* Roads must be noded (split at junctions) for intersection extraction and
  network KDE. Crashes are snapped to the geometrically nearest site; junction
  crashes are flagged `ambiguous` but not reassigned.
* SPFs are user supplied or fitted with a constant overdispersion. `k_form="per_length"`
  in `fit_spf` is an approximation (alpha scaled at the mean length), not a
  length-dependent dispersion model. No calibration factor (C) is estimated.
* The EB screening uses totals over the study period without yearly
  calibration factors (HSM `C_y`); `proportion` requires extra-binomial
  variation between sites and raises an error otherwise.
* Before-after standard errors use Hauer's large-sample approximations; the
  comparison-group method assumes a Poisson comparison ratio unless its variance
  is given. Confidence intervals are normal approximations.
* The built-in network KDE is the "simple" kernel of Xie & Yan (2008), which
  inflates densities at junctions with three or more legs. The optional `nkde`
  backend (or any callable) can replace it.
* Getis-Ord p-values are not corrected for multiple testing.
* Screening ranks sites for diagnosis; it does not establish causes or
  countermeasures. Crash records are personal data: keep them out of public
  repositories.

## References

* AASHTO (2010). *Highway Safety Manual*, 1st ed. American Association of State
  Highway and Transportation Officials, Washington, DC. Part B, Chapter 4
  (network screening); Part C, Appendix A (EB method).
* Hauer, E. (1997). *Observational Before-After Studies in Road Safety.* Pergamon, Oxford.
* Hauer, E., Harwood, D. W., Council, F. M., Griffith, M. S. (2002). Estimating safety
  by the empirical Bayes method: a tutorial. *Transportation Research Record* 1784, 126-131.
* Persaud, B., Lyon, C. (2007). Empirical Bayes before-after safety studies: lessons
  learned from two decades of experience and future directions. *Accident Analysis &
  Prevention* 39(3), 546-555.
* Hauer, E., Bamfo, J. (1997). Two tools for finding what function links the dependent
  variable to the explanatory variables. *Proceedings of the ICTCT 1997 Conference*, Lund.
* Cameron, A. C., Trivedi, P. K. (2013). *Regression Analysis of Count Data*, 2nd ed.
  Cambridge University Press.
* Ord, J. K., Getis, A. (1995). Local spatial autocorrelation statistics: distributional
  issues and an application. *Geographical Analysis* 27(4), 286-306.
* Xie, Z., Yan, J. (2008). Kernel density estimation of traffic accidents in a network
  space. *Computers, Environment and Urban Systems* 32(5), 396-406.

## Citation

See `CITATION.cff`. MIT licensed, (c) 2026 Yusuf Eminoğlu.
