This card documents the published, open nearmiss dataset: the GeoJSON aggregated to
public street segments and the accompanying rate tables emitted by publish.py. It follows the spirit of
Datasheets for Datasets (Gebru et al.) and HuggingFace dataset cards: what the data is,
how it was made, what it can and cannot support, and where it will mislead you if you
misuse it.
It is written to hold up when a skeptical traffic engineer pushes back. Where the data is weak, this card says so plainly. Read the "Out-of-scope and discouraged uses" and the "Known reporting biases" sections before you cite a number.
- Dataset name: nearmiss published open dataset
- Version: tracks the release tag of the repository (semver); the exact dataset version
(
dataset_version, currently0.1.0), schema version (schema_version, currently1.0.0), and content hash are recorded both in the published GeoJSON's embeddedmetadatamember and in the per-city metadata sidecar (<city-slug>.metadata.json, e.g.davis.metadata.json) shipped next to the GeoJSON. - Maintainer: Chelsea Kelly-Reif (GitHub @ChelseaKR). Contact via the GitHub repository's issues. This is an independent personal open-source project, unaffiliated with any employer or client.
- Repository:
github.com/ChelseaKR/nearmiss(public, pre-1.0 beta). - License: Apache-2.0 (see License and citation).
- Schema: report intake schema at
schema/report.schema.json; published GeoJSON schema atschema/dataset.schema.md. - Published files (per city, named by city slug): the open, block-level GeoJSON is
<city-slug>.geojson(e.g.davis.geojson) and its metadata sidecar is<city-slug>.metadata.json(e.g.davis.metadata.json). A second, coarser GeoJSON,<city-slug>.corridors.geojson(e.g.davis.corridors.geojson), merges contiguous significant blocks of the same street into named corridors for advocacy asks — published alongside, never instead of, the block-level file (seeschema/dataset.schema.md§9). There is nonearmiss.geojson. This data card lives atdocs/DATA-CARD.md; there is no sidecarDATA-CARD.md.
nearmiss publishes an exposure-normalized analysis of road-hazard and near-miss reports made by people on bikes and on foot — close passes, door-zone conflicts, blind-corner / sightline problems, surface hazards (potholes), debris, and signal/crossing problems. The core idea is that raw report counts confound danger with traffic: the busiest bike route collects the most reports whether or not it is the most dangerous. The published dataset is therefore not a pile of pins. It is:
- a segment-aggregated GeoJSON of near-miss reports, where the published geometry is the real public street centerline (public infrastructure) and no per-report coordinate is published, plus
- per-segment rate estimates that divide report counts by an exposure denominator
(a bike/pedestrian volume or demand estimate), reported in a stated exposure unit
(
exposure_unit, e.g. "bike trips") and each carrying a confidence interval and an n, plus - hotspot outputs — a kernel-density surface labeled by what it actually shows, and Getis-Ord Gi* clusters flagged as statistically significant given exposure and spatial structure — plus
- a self-describing top-level
metadatamember embedded in the GeoJSON itself, plus a machine-readable metadata sidecar (<city-slug>.metadata.json) recording sources, dates, methods, thresholds, totals, and the content hash for reproducibility, plus - an optional null-calibration artifact (
<city-slug>.calibration.json, written bynearmiss analyze --calibrate) — "we attacked our own dataset": the hotspot method run against many seeded label-shuffles of this city's own report counts, exposure and geometry held fixed, publishing the method's empirical false-positive rate on this city's actual network (docs/METHODOLOGY.md§9.4), not just on an invented test fixture.
Each city also ships a threshold-sensitivity & statistical-power note
(<city-slug>-sensitivity.md, e.g. davis-sensitivity.md)
showing how stable the ranking is across snapping/dedupe thresholds and how many reports a segment
needs before it is rankable (R29 / R34; see METHODOLOGY.md §5.6).
The dataset is the product, not an app. The accessible web map and table are just two views of these published artifacts.
A human-readable brief is rendered from the published dataset by nearmiss brief --config <cfg> (or run), in English or Spanish (--lang es, via src/nearmiss/i18n.py; unknown
languages fall back to English). The brief leads with a plain-language glossary (what a
rate, a confidence interval, and a Getis-Ord Gi* hotspot mean) and a one-sentence bottom
line, reports every rate in the dataset's exposure unit (exposure_unit, e.g. "bike
trips"), and closes with a bias counterweight — the reminder that an exposure-normalized
rate with a stated interval and a flagged bias is a far better basis for action than a raw heat
map, so the named biases are a call to act on the strongest signals, not an excuse to conclude
nothing.
This card is bound by the project's five hard rules (enforced in CI and by policy):
- No rate without a denominator. Every risk number here is a rate per unit of exposure, with the exposure source and its date stated. Raw-count layers exist but are labeled "report volume," never "danger."
- No estimate without an interval. Every published rate, ranking, and comparison carries
a confidence interval and an
n. Small-sample segments are marked uncertain, not ranked. - Reporting bias is named, not hidden. See Known reporting biases.
- Contributor privacy is protected. Reports are pseudonymous; the open dataset is aggregated to public street segments, low-count segments are withheld entirely, and raw precise reports stay private. See Privacy: aggregation and minimum occupancy.
- Open and reproducible end to end.
make reproduceregenerates every figure and table from raw inputs. See Maintenance and updates.
Vulnerable road users absorb most of the risk on streets and produce almost none of the official data. A near miss leaves no police report, and even collisions involving people on bikes and on foot are routinely undercounted. Advocates are then told "show us the data" by the same agencies whose data does not capture the problem. nearmiss exists to collect that missing evidence carefully and to analyze it without cheating — specifically, to refuse the tempting lie of a raw heat map that confidently points at the busiest, best-lit bike route and calls it the most dangerous one.
The published dataset turns the reports cyclists and pedestrians already make into a rigorous, open, reusable evidence base that advocates, researchers, journalists, and cooperative city staff can check, fork, and build on.
Built and maintained by a single person (Chelsea Kelly-Reif) as an independent personal open-source project. There is no funding, sponsor, employer, or client behind it, and it contains no proprietary or client material. There are no contributors beyond the maintainer at the time of writing; community contributions, when they arrive, will be acknowledged in the repository.
It is a community-owned evidence base — not a city 311 queue and not a public-works complaint inbox. Filing a report here does not dispatch a crew or fix a pothole. The value is the open dataset and the honest analysis under it, available to anyone, independent of any city's data portal or goodwill.
The published dataset has two coupled layers, both keyed to street segments (not to addresses or to exact points):
- Report-volume layer — segment-aggregated geometry representing where near-miss reports cluster. A feature here answers "how many reports of what type fell on this segment," after aggregation to the public street segment. It is explicitly labeled report volume, not danger.
- Rate layer — per-segment rate estimates: report count over an exposure denominator,
with an interval and an
n. This is the layer that makes a risk claim.
A published record is therefore a GeoJSON Feature whose geometry is the real public
street centerline LineString for a segment, and whose properties carry the
classification, counts, exposure, rate, interval, and quality/significance flags. No
published record is an individual person's report, and no per-report coordinate is
published. Individual raw reports exist only in the private store.
The authoritative, versioned field list and types live in schema/dataset.schema.md. The
table below is the human summary; if the two disagree, the schema file wins. Each published
feature is a GeoJSON LineString (the public street centerline) whose properties carry:
Field (properties.*) |
Type / values | Meaning |
|---|---|---|
segment_id |
string (stable, opaque) | Internal street-segment identifier the report was snapped to. Opaque; not a street address. |
name |
string | Human-readable street-block name for the segment (real Davis block names such as 5th St (C–D), not a seg-NN placeholder). |
report_count |
integer ≥ 0 | Number of reports aggregated into this feature after dedupe. This is report volume, not danger. |
n |
integer ≥ 0 | Sample size behind the estimate (the contributing report count the interval is computed from). |
exposure_estimate |
number | null |
Exposure denominator (estimated bike/ped volume) for the segment, in the unit named by exposure_source / the dataset's exposure_unit. null means exposure unknown, including a raw estimate at or below the configured exposure floor. |
exposure_source |
string | null |
Provenance of the denominator (count program, demand model, or named exposure layer). null only when exposure_estimate is null. |
exposure_date |
date (ISO 8601) | null |
The date/vintage of the exposure source. null only when exposure_estimate is null. |
exposure_tier |
observed | modeled | proxy | unknown |
How much to trust exposure_estimate: a direct count station (observed) beats a calibrated demand model (modeled) beats a third-party activity proxy (proxy); unknown is the honest default for exposure rows written before this field existed. A rate on an observed denominator and a rate on a proxy denominator are different measurements. |
exposure_disagreement |
number in [0, 1] | null |
Published only when a second exposure source corroborates the segment: 1 - min/max across all readings. 0 is perfect agreement; values near 1 flag a real cross-source disagreement, surfaced rather than averaged away. null when the segment has only one exposure reading. |
rate |
number | null |
report_count normalized by exposure_estimate, per the dataset's rate_per and exposure_unit. null when exposure is unknown. |
rate_ci_low |
number | null |
Lower bound of the rate confidence interval. |
rate_ci_high |
number | null |
Upper bound of the rate confidence interval. |
getis_ord_z |
number | null |
Getis-Ord Gi* z-score on the exposure-normalized rate, where computed. |
getis_ord_significant |
boolean | null |
Whether the segment is a statistically significant cluster after Benjamini-Hochberg FDR correction. null when getis_ord_z is null. |
rate_sensitivity_delta |
number | null |
Signed difference (all-records rate minus the published primary rate), reported only when excluding low-confidence records (low_accuracy/far_snap) would move the rate outside its confidence interval; null when the exclusion is immaterial. |
confidence_label |
certain | uncertain | exposure_unknown |
Plain-language reliability label surfaced in the map and table. |
hazard_breakdown |
object (closed hazard vocabulary → integer) | Counts of reports at this feature by hazard type; suppressed (emitted as {}) for segments below small_n. |
quality_flags |
array of strings | Pipeline quality flags from the published vocabulary low_sample, geocode_low_confidence, exposure_unknown, exposure_stale, singleton_neighborhood (see Quality flags). |
The intake (private) report schema — what a contributor actually submits — is separately
documented in schema/report.schema.json and includes the optional free-text note, the
pseudonymous contributor token, the optional BCP-47 language tag, and the precise location
(location lat/lon or a free-text address — see How a report enters),
none of which appear in the published dataset at full fidelity (see privacy section).
The published GeoJSON FeatureCollection carries a top-level metadata member (a foreign
member permitted by RFC 7946) so a consumer reading only the file still gets the version, the
provenance, and the privacy parameters that govern interpretation. It includes:
dataset_version (0.1.0), schema_version (1.1.0), license (Apache-2.0), city,
exposure_unit, segments_published, segments_withheld_low_count, dataset_note (a
synthetic-demo / provenance label), a plain-language privacy note, and a significance note
(the Getis-Ord Gi* / FDR method).
The metadata sidecar (<city-slug>.metadata.json, e.g. davis.metadata.json) carries the
content hash (geojson_sha256) plus the full methods block (confidence_z, fdr_alpha,
getis_ord_band_m, getis_ord_neighbors, getis_ord_node_snap_m, kde_bandwidth_m,
min_publish_n, rate_per, small_n, exposure_floor,
exposure_stale_days, and the significance string) and a summary block (reports_in, duplicates_removed, snapped,
unsnapped, exposure_coverage, excluded_low_confidence_fraction, segments_total,
segments_published, segments_withheld_low_count), the report_intensity_peak_segment (a segment id only, never a
coordinate), and a pointer to this data card (docs/DATA-CARD.md).
A published feature's quality_flags array draws from a small closed vocabulary; the briefs
and the table surface these so a consumer can filter. The flags that reach the published
dataset are:
| Flag | Meaning |
|---|---|
low_sample |
The contributing report count (n) is below the small-sample threshold (small_n); the rate and any ranking are uncertain — read it with the wide interval, and note the hazard_breakdown is suppressed ({}). |
geocode_low_confidence |
This segment aggregates one or more low-positional-accuracy or geocoded-from-address locations; placement is less certain. |
exposure_unknown |
No exposure denominator was available (including a denominator at or below the configured exposure floor); exposure_estimate, rate, and the CI bounds are null, and the feature is labeled exposure-unknown rather than rated. |
exposure_stale |
The exposure vintage (exposure_date) is more than the configured exposure_stale_days from the reports the rate is built from — a temporal-alignment caveat: exposure measured in a different period than the reports is a different measurement, not the same one revisited. |
singleton_neighborhood |
This segment's Gi* neighborhood held no other rated segment, so its getis_ord_z is a global z-score (this segment against the whole city's rate distribution), not the local cluster statistic Gi* is normally read as. The z is published and labeled rather than hidden, and getis_ord_significant is always false for such a segment, so no ★ in a brief ever rests on one. Raised by a disconnected segment, a segment longer than twice gi_band_m (which cannot reach a neighbor under the half-length edge weight), or a segment whose neighbors all lack an exposure denominator. See ADR-0015. |
These names replace any earlier flag spellings (e.g. low_geocode_confidence,
outside_study_area, small_n). The authoritative, versioned flag vocabulary — including
additional internal flags and the config thresholds behind each — lives in
schema/dataset.schema.md.
- Instances: aggregated segment/point features, not individual reports. Count and
geographic extent depend on the deployment (city) and the reporting window; the metadata
sidecar's
summaryblock (and the embeddedmetadatamember) records the exact totals for each release. - "Labels": the
hazard_breakdownkeys,getis_ord_significant, andconfidence_labelfields are the closest thing to labels. They are classifier and statistical outputs, not adjudicated ground truth. - Deliberately absent: per-report coordinates (whether submitted as a
locationor as a geocodedaddress), any per-report timestamp, contributor identity, the free-text note, the reportlanguage, mode, severity, heading, route/home-end information, and any field that would let a reader reconstruct an individual's routine. These are withheld by design, not lost.
The published GeoJSON is aligned to a documented near-miss/collision schema so it can sit
beside official collision data and exposure layers. It is a sibling to
davis-bike-hazard-map (there the product is the map; here it is the data and the statistics
under it). Exposure denominators are imported from external sources (count programs,
demand models, or exposure layers); those sources keep their own licenses and vintages and
are cited per feature.
Reports are made by people on bikes and on foot through a short form, a documented JSON submission, or an import path, and capture: location, time, mode, hazard type, a self-reported severity, and an optional free-text note. Reporting is self-selected and voluntary — this is central to the bias discussion below.
A report carries its location in one of two ways: a precise WGS84 location (lat/lon),
or a free-text address/intersection (e.g. "B St & 3rd St, Davis CA"). The intake schema
requires one or the other (an anyOf constraint), so an address-only report is a first-class
submission rather than an error. An address-only report is resolved to coordinates at the
geocode stage by a pluggable geocoder (see Pipeline transforms).
The default geocoder is an offline GazetteerGeocoder backed by a committed
address→coordinate table, configured by the gazetteer config key (a JSON file of the form
{"addresses":[{"address","lat","lon"}]}); matching is case-insensitive and
whitespace-normalized. A networked geocoder (e.g. Nominatim) would implement the same protocol
but is not the default, so the analysis runs anywhere with no external service. Either way,
the resolved precise coordinate is treated as private location data and is never published.
A report may also carry an optional BCP-47 language tag (e.g. en, es) recording the
language it was submitted in; it defaults to en when absent. The tag is used to characterize
language-based under-representation (see Known reporting biases)
and to drive the bilingual brief; it is not published per report.
Every incoming report is validated against schema/report.schema.json before it is accepted.
Malformed or out-of-range submissions are rejected at the door rather than silently corrupting
the dataset; intake is rate-limited to resist spam and poisoning. Accepted reports land in the
private raw store (data/raw/, gitignored) and never leave it at full precision.
External source refreshes can use a separate, operator-selected private POSIX root. Raw bytes and
normalized bytes are stored under immutable SHA-256 names; normalized/current.json is a validated
success receipt whose atomic replacement commits the active version. Historical success/failure
receipts record the source attempt without copying callback messages, URLs, or payload fragments.
This foundation does not change the composition or schema of the published dataset by itself. The
legacy nearmiss ingest-fars command validates a private crash-level FARS artifact; the additive
nearmiss ingest-fars-year command validates an exact registered annual archive and privately joins
its accident/person tables into a mode-aware artifact. Neither makes a FARS outcome, BikeMaps
refresh, exposure layer, or other imported artifact public until a reviewed aggregation/privacy
publication path explicitly consumes it. The private root must remain outside every served directory
and is never part of the allowlisted Pages artifact. See INGESTION.md for the
operator contract and limits.
The coverage command may read that private FARS root only when the operator supplies --fars-root.
It verifies the active receipt/history/raw/artifact chain and deterministic normalization replay, then
emits aggregate lineage metadata only. A matching registry declaration is still required; neither
declaration nor verification alone changes the dataset, evidence tier, report counts, exposure, or
public artifact. Even the matched state is crash context, not outcome triangulation.
The pipeline is a sequence of pure transforms, each emitting plain inspectable data so a published number traces back to its inputs:
- Dedupe (
pipeline/dedupe.py) — collapse duplicate and near-duplicate submissions (same event reported twice, or by two people) using spatial/temporal/type proximity, so one event is not double-counted. - Geocode (
pipeline/geocode.py) — for an address-only report, resolve the free-textaddressto coordinates via the configured pluggable geocoder (the default offlineGazetteerGeocoder); reports that already carry a preciselocationpass through. A geocode confidence is attached, and low-confidence results are flagged (geocode_low_confidence), not dropped. - Snap-to-segment (
pipeline/snap.py) — snap each report to a street segment so analysis is per-segment rather than per-pin; the precise offset is used internally and discarded before publication. - Classify (
pipeline/classify.py) — assign thehazard_typefrom the report's structured fields (and, where present, the note) into the documented categories. - Quality-flag (
pipeline/quality.py) — attachquality_flags(low_sample,geocode_low_confidence,exposure_unknown) that follow the record into publication so consumers can filter.
A single bad geocode or bad report flags and continues; it never aborts the rebuild.
Exposure join. Exposure rows join to streets by exact segment_id. A total mismatch
— no exposure id matching any street id, which almost always means the two layers use different
id schemes — raises a clear error rather than silently producing 0% coverage that would read as
"no denominators." A partial mismatch warns and lists the unmatched ids, so a miswired
exposure layer is visible rather than silently dropped.
- Timeframe: each release covers a stated reporting window recorded in the metadata sidecar.
- Language: reports may carry an optional BCP-47
languagetag (e.g.en,es), and the brief renders in English or Spanish (nearmiss brief --lang es, viasrc/nearmiss/i18n.py). Intake supports the report form in the languages of the community it is deployed for; coverage is uneven and is itself a known bias (below). - Consent: contributors submit voluntarily and pseudonymously, with the understanding (stated at intake) that a segment-aggregated, de-identified form of their report will be published openly under Apache-2.0, and that the precise report will not be.
Beyond the pipeline stages above, the following cleaning is applied between raw intake and publication. Every step is deterministic and seeded so a rebuild reproduces the same output.
- Deduplication removes double-reported events; the surviving
report_countreflects distinct events, not raw submissions. - Geocoding resolves an address-only report to coordinates via the configured pluggable
geocoder (default offline gazetteer) and attaches a confidence score; results below the
configured confidence threshold are flagged
geocode_low_confidenceand are eligible for exclusion from rate estimates while remaining visible in the report-volume layer. - Snapping moves analysis to the segment level; this is also a privacy step, because it removes the exact point a reporter chose.
- Classification maps reports to the documented hazard categories; anything that does not
fit is
other, never force-fit. - Quality flags mark records that are geographically out of the study area, exposure-less,
or too sparse to support a rate. A segment with no exposure data is published with
confidence_label = exposure_unknownand anullrate — shown, not silently dropped and not falsely rated. - Aggregation to public street segments and withholding of low-count segments (the final,
irreversible cleaning step) are applied by
publish.py— see the privacy section.
The raw, precise data is never part of any published or committed artifact.
This is the section a traffic engineer will read first, so it is explicit.
Every rate in this dataset is report_count / exposure_estimate. The exposure estimate is
an estimate of how much biking/walking happened on that segment in that window — the
denominator that turns a count into a rate, reported in the dataset's exposure_unit (e.g.
"bike trips"). Without it, more reports on a busy street would masquerade as more danger.
Exposure rows join to streets by exact segment_id; a total id mismatch raises a clear error,
and a partial mismatch warns, so a miswired denominator is never silently read as 0% coverage.
- What exposure can be. Depending on the deployment,
exposure_estimatecomes from one of: observed bike/pedestrian counts where a count program exists; a demand model; or an imported exposure layer (e.g. a Strava/StreetLight-style volume surface). The specific source, its vintage, and its trust tier are recorded per feature inexposure_source,exposure_date, andexposure_tier(observed>modeled>proxy, most to least trusted;unknownwhen no tier was recorded), and summarized in the metadata sidecar. Sources are interchangeable behind one interface, so a given city's denominators may come from a different source — and a different trust tier — than another's, and even from segment to segment within one city. - Corroboration, not false consensus. When a segment's exposure is backed by more than one
source (e.g. a count station and a demand model both covering the same block), the primary
reading is still what
rateis computed against, butexposure_disagreementpublishes how well the sources agree (0= perfect agreement, near1= a large disagreement). A large disagreement between a count and a proxy layer is treated as a finding in its own right, not averaged away — it means the denominator itself is contested, and the rate should be read with that in mind alongside the confidence interval. - Assumptions baked into the denominator. The rate is only as good as the exposure
estimate. Known assumptions and their failure modes:
- Spatial coverage gaps. Count programs cover a fraction of segments; the rest rely on
modeled or imported exposure, which is smoother and less locally accurate. Segments with no
usable exposure — including an estimate at or below the configured exposure floor, which
exists because rates blow up as exposure approaches zero — get
exposure_unknownand no rate. - Temporal mismatch. The exposure source's vintage rarely matches the reporting window
exactly;
exposure_dateexposes this gap, and theexposure_staleflag marks a feature whose exposure vintage is more than the configured threshold from its reports. - Mode and time-of-day aggregation. Exposure is typically a coarse volume, not matched to the specific mode, hour, or direction of each report; rates are averages over that coarseness.
- Modeled-demand circularity. If demand is modeled partly from infrastructure that also influences where people report, the denominator and numerator are not fully independent. This is disclosed where it applies.
- Spatial coverage gaps. Count programs cover a fraction of segments; the rest rely on
modeled or imported exposure, which is smoother and less locally accurate. Segments with no
usable exposure — including an estimate at or below the configured exposure floor, which
exists because rates blow up as exposure approaches zero — get
- What this means for the numbers. Rates are estimates with intervals, computed with a
count model appropriate to the data and a small-count interval method for sparse segments;
every rate ships its
rate_ci_low/rate_ci_highbounds andn. A segment is never ranked above another on a difference the interval does not support. The exposure sensitivity of the headline findings is checked in a reproducible notebook, and material sensitivity is stated in the briefs.
If you change the exposure source, you change the ranking. That is expected and is why every rate ships its denominator's identity and date.
The dataset is built from voluntary, self-selected reports, and that biases it in ways
that are named here rather than hidden. bias.py characterizes these by comparing the
reporter pool and the geographic spread of reports against ridership and demographic
baselines; the briefs restate the relevant ones in plain language.
- Route-choice / "busy-route" bias. Reports concentrate where people actually ride and walk. Without exposure normalization, the busiest corridors look the most dangerous. This is the central bias the whole rate pipeline exists to counter — but normalization only corrects it to the quality of the exposure estimate.
- Reporter-pool bias (who reports). People who file near-miss reports skew toward confident, regular, English-comfortable, smartphone-carrying riders who know the project exists. Occasional riders, children, people walking, people without app access, and people who already avoid dangerous streets entirely are under-represented.
- App-access and digital-divide bias. A report requires a device, connectivity, and the awareness to submit. Lower-income and less-connected areas are likely under-reported even where risk is high.
- Language bias. Where the form is not available in a community's language, that community is under-represented. Form-language coverage is uneven across deployments.
- Demographic and geographic skew. Reports may over-represent areas and groups with more
advocacy presence and under-represent others;
bias.pyreports the direction where a baseline exists. - Survivorship / avoidance bias. The most dangerous segments may show few reports because few people will ride or walk them at all. Low report volume is not safety.
- Salience and severity bias. Dramatic events (a close pass at speed) are more likely to be reported than mundane chronic hazards; report mix is not an unbiased sample of all hazards.
- Temporal and campaign bias. A local campaign, a news story, or a crash can cause a reporting spike that is about attention, not a change in danger.
- Per-source bias when reports are imported from a real-data source. Each source adapter
(
src/nearmiss/adapters/, seedocs/REAL-DATA.md) carries its ownbias_labelandbias_notesin its crosswalk manifest — e.g. BikeMaps.org skews toward self-selected app-user cyclists in cities where it has been promoted, while SimRa skews toward app-recruited riders in a handful of research-partner regions and can only ever report near-misses, never collision severity. A published dataset assembled from more than one source should name each source's bias separately rather than blend them into one undifferentiated reporter-pool description.
Net effect: treat the dataset as a biased sample of reported near misses, partially corrected for traffic by exposure, with residual bias that cannot be fully removed. Findings that could be artifacts of who reports are labeled as such in the outputs.
Contributor privacy is a hard rule, and the published dataset is engineered around it.
- Pseudonymous reports. Reports carry an opaque contributor token, not an identity. The token is not published.
- Aggregation to public street segments. Publication aggregates reports to public street segments. The published geometry is the real public street centerline (public infrastructure) — not a perturbed point and not a report location. No per-report coordinate is published.
- No per-report timestamp. No per-report timestamp is published; only the aggregation window recorded in the metadata sidecar appears.
- k-anonymity / minimum occupancy. Any segment with a non-zero report count below
min_publish_n(default 3) is withheld entirely from the published GeoJSON, its embedded and sidecar metadata, and the briefs. No published place can mean "one or two people reported an incident here." This is enforced inpublish.pybyassert_published_clean(which raises) andassert_metadata_clean, and is covered by the test suite. - Small-sample suppression. Hazard-type breakdowns (
hazard_breakdown) for segments with a count belowsmall_nare suppressed (emitted as{}), and the segment is flaggedlow_sample. - What is deliberately withheld. Per-report coordinates; any per-report timestamp; the
free-text note; contributor token/identity; mode, severity, and heading; route and home-end
detail; and any combination that could re-identify an individual. Publication is enforced by
an allowlist in
publish._featureand a denylist invariant inassert_published_clean(), withassert_metadata_clean()covering the sidecar metadata. The KDE report-intensity peak is published only as a segment id, never a coordinate. - Private source stores stay outside publication. Precise contributor reports live in
data/raw/, which is gitignored and never committed, deployed, or served. The dev server (server.py/nearmiss serve) is read-only (GET/HEAD) and refuses any request underdata/raw/or any dotfile path with HTTP 403, even when launched on the repo root. External-source ingestion uses a separate owner-only root that operators must keep outside all served directories; arbitrary ingestion paths are not covered by the dev server'sdata/raw/guard. GitHub Pages publishes neither tree because its build artifact is allowlisted. - Residual risk. Aggregation and withholding reduce but do not erase re-identification risk: a repeat contributor reporting across multiple segments could still be linked across those segments. This residual risk remains and is not claimed away.
- Re-identification model. The detailed model for the rare-
hazard_type-on-a-low-traffic-segment attack — the adversary, the join, themin_publish_n/small_nparameter rationale, and why the pipeline withholds rather than jitters — is documented indocs/RE-IDENTIFICATION.md.
These are publishing-rule and threat-model protections, not license restrictions — the data
itself is meant to be free, so the privacy lives in what is published, not in legal terms.
See docs/THREAT-MODEL.md for the full analysis.
This dataset is built for, and holds up under, uses like:
- Safe-streets advocacy — pointing at exposure-normalized, interval-bearing risk rates and statistically significant Getis-Ord Gi* hotspots when asking a city to fix specific corridors, with the bias caveats stated.
- Corroborating official collision data — overlaying reported near misses on collision and exposure layers to surface near-miss-heavy, collision-light segments (the "lucky so far" problem).
- Prioritization with uncertainty — ranking candidate locations for intervention while
respecting the confidence intervals and the
confidence_label, not over-reading sparse segments. - Research and journalism — reusing the open GeoJSON, the schema, and the reproducible notebooks, with the documented biases and exposure assumptions cited.
- Method reuse — adopting the schema and the exposure-normalization/hotspot code for other point-hazard datasets.
Always cite the exposure source and date, the interval, and the n alongside any rate, and
carry the reporting-bias statement into your own work.
Do not use this dataset for the following. These are not edge cases; they are the standard ways this kind of data is misused.
- Do not treat it as a census or an unbiased sample. It is a voluntary, self-selected, biased sample of reported near misses, not a complete record of hazards or of risk.
- Do not rank or target individual addresses, homes, or people. The data is aggregated to public street segments on purpose, and no per-report coordinate is published; using it to infer a person's routine, identity, residence, or movements is a misuse and is defeated by the published precision.
- Do not read raw report volume as danger. The report-volume layer is labeled report volume. Mapping raw counts as "where it's dangerous" reproduces exactly the lie this project exists to refuse. Use the rate layer.
- Do not rank sparse segments as if they were certain. Respect the intervals and the
confidence_label. A segment markeduncertainorexposure_unknowncannot be placed confidently above or below another. - Do not treat it as a 311 queue or expect a response. Submitting or citing a report does not dispatch repairs, enforcement, or emergency response. For an active hazard or emergency, contact the responsible authority directly.
- Do not use it for enforcement, surveillance, insurance, or punitive action against individuals. The dataset is not designed or licensed-in-spirit for adverse action against people; its de-identification makes such use both wrong and unreliable.
- Do not compare across cities or sources without accounting for different exposure sources and reporting cultures. Denominators and reporter pools differ; cross-deployment comparison requires explicit care.
- Do not strip the caveats. Publishing a rate, ranking, or map from this data without its
interval,
n, exposure source/date, and bias statement misrepresents it.
- Maintainer. Chelsea Kelly-Reif (@ChelseaKR); contact via the repository's GitHub issues. Single maintainer; no other contributors at this time.
- Update cadence. The published dataset is regenerated on scheduled rebuilds as new reports accumulate and on each release. There is no service-level guarantee on freshness; the live window and the build date are always recorded in the metadata sidecar. As an independent, zero-cost, volunteer project, update frequency is best-effort, not contractual.
- Reproducibility. The dataset is fully reproducible from raw inputs:
make reproduceregenerates every published figure, table, and GeoJSON deterministically (seeded pipelines and analyses).make verifyreproduces the full CI gate (lint, types, tests including planted-hotspot fixtures, accessibility, security). - Accessibility of the views. The two views over this dataset (map, table) are checked by a
structural accessibility gate (
tools/a11y_check.py) and a real automated axe-core run in jsdom (make axe, viaweb/package.json+web/axe_check.mjs). The segment-name table column is sticky (usable at 200% zoom) and column sorts announce through an aria-live region. Manual NVDA / VoiceOver screen-reader review is still pending and is named here as an open item, not claimed as done. - Versioning and stability. Releases follow semver. The published GeoJSON schema is versioned with a deprecation policy and migrations; schema changes are recorded in the CHANGELOG and ADRs. Published artifacts are content-hashed so tampering or drift is detectable.
- Ingestion-schema review impact (2026-07-12). Adding the
1.0.0ingestion receipt contract and private transaction tree was reviewed against this data card andTHREAT-MODEL.md. It changes operational lineage and private retention, not the current published GeoJSON composition or privacy contract. A future source integration must review this card again before publishing new aggregates. - FARS-artifact schema review impact (2026-07-12). The
1.0.0official-outcome artifact and local CLIs add private national fatal-crash lineage; the CLIs themselves publish no feature or rate. Exact outcome coordinates remain in the owner-only ingestion tree and stdout is aggregate-only. A separately reviewed projection already publishes 2024 state × involved-mode context with small-cell suppression. The annual path joins accident/person mode context privately, but no segment/time comparison or near-miss/outcome triangulation is published without another methodology and privacy review. - Errata and corrections. Data and threshold fixes are made as recorded, re-run edits; corrections ship in a new versioned release rather than mutating a published one.
- 2024 FARS provenance correction (2026-07-12). NHTSA identifies 2024 as its Annual Report File. The corrected revision changes only release-stage provenance; the original artifact/index remain available at their exact hashes, and a machine-readable ledger pins both generations. Counts, suppression, geography, raw archive identity, and mappings are unchanged.
- Deprecation. Superseded dataset versions remain identifiable by version and hash; consumers should pin the version they cite.
Starting with the first tagged release, every vX.Y.Z tag runs the release pipeline in
.github/workflows/release.yml: it builds the Python
package, generates a CycloneDX SBOM, keyless-signs the sdist, wheel, SBOM, and every
published <city-slug>.geojson with Sigstore cosign (no
long-lived private key — the signer's identity is this repository's own GitHub Actions OIDC
token, publicly logged to the Rekor
transparency log), and attaches SLSA build provenance. Signing a data
artifact (rather than only code) is unusual — it exists so a consumer can answer, offline and
without trusting this README, "did this exact davis.geojson come from a tagged, CI-built
release of github.com/ChelseaKR/nearmiss, unmodified since?"
Every GitHub Release attaches a <artifact>.sigstore.json bundle (signature + certificate +
Rekor inclusion proof) next to each signed file. To verify a downloaded davis.geojson against
its bundle from the same release:
cosign verify-blob \
--bundle davis.geojson.sigstore.json \
--certificate-identity-regexp '^https://github\.com/ChelseaKR/nearmiss/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
davis.geojsonA Verified OK result means: this file was signed by this repository's own release
workflow, the signature is logged in Rekor (so a maintainer cannot quietly re-sign a tampered
file later without leaving a public trace), and the file has not been altered since. Separately,
geojson_sha256 in the metadata sidecar (davis.metadata.json) lets you confirm content
identity even without cosign installed. To additionally verify the SLSA provenance attestation
(that the artifact was built by the expected GitHub Actions workflow from the expected source
repository, not hand-crafted and uploaded):
gh attestation verify davis.geojson --owner ChelseaKRThe nearmiss code, schema, pipeline, notebooks, and published dataset are released under the Apache License 2.0. Apache-2.0 is chosen so the dataset and the methods spread as widely as possible: its patent grant and permissive terms let other advocates, researchers, and cooperative city staff adopt the schema, reuse the statistics code, and build on the open data without friction. Privacy is enforced by the publishing rules and threat model, not by a restrictive license — the data is meant to be free.
Third-party exposure sources (count programs, demand models, exposure layers) used as
denominators retain their own licenses and terms; consult exposure_source /
exposure_date and the metadata sidecar before redistributing derived exposure values.
Third-party incident sources (the numerator) are the harder case, and they are not interchangeable. Apache-2.0 covers this project's code, schema, and methods. It cannot relicense somebody else's data, and an adapter does not launder a source's terms.
| Source | License | Redistribution of derived data |
|---|---|---|
| BikeMaps.org | CC BY 4.0 | Permitted with attribution |
| SimRa | CC BY-NC 4.0 | NonCommercial condition survives aggregation |
The NonCommercial condition on SimRa is load-bearing: a published dataset containing SimRa reports is not distributable under Apache-2.0 alone, and aggregation does not dissolve the clause. No SimRa-derived data is currently published from this repository. Before publishing any dataset that merges incident sources, check each source's license individually and record the most restrictive terms that apply to the merged result; see the licensing checklist in CONTRIBUTING.md.
Cite the dataset with its version, the access date, and the repository. Suggested form:
Kelly-Reif, C. nearmiss: an open dataset and statistically honest analysis of road hazards
and near misses for safe-streets advocacy [data set]. Version <vX.Y.Z>. Apache-2.0.
github.com/ChelseaKR/nearmiss (accessed <YYYY-MM-DD>).
When you cite a specific rate, ranking, or hotspot, also report the exposure source and date,
the confidence interval, the n, and the reporting-bias caveat — that is the honest unit of
this dataset, and a number from here without them is not this dataset's claim.
schema/report.schema.json— intake (private) report schema.schema/dataset.schema.md— published GeoJSON schema (authoritative field reference).docs/METHODOLOGY.md— full statistical methodology (rates, intervals, KDE, Getis-Ord Gi*).docs/THREAT-MODEL.md— privacy threat model and the basis for the withheld-precision rules.docs/RE-IDENTIFICATION.md— the rare-hazard-type re-identification model and parameter rationale.data/published/<city-slug>.metadata.json(e.g.davis.metadata.json) — per-release machine-readable metadata sidecar (versions, content hash, methods, sources, windows, totals).data/published/<city-slug>.geojson(e.g.davis.geojson) — the published open GeoJSON, with its self-describing top-levelmetadatamember.data/published/<city-slug>.corridors.geojson(e.g.davis.corridors.geojson) — the corridor-level view (EXP-03), published alongside the block-level file above, never instead of it; seeschema/dataset.schema.md§9.