Methodology & data limitations
How this dashboard sources, queries, classifies, and filters NYC cyclist incident data — and the caveats to keep in mind when reading the charts.
Data source
Everything on this site is built from the NYPD Motor Vehicle Collisions dataset, published by the City of New York through its Open Data platform. The dataset covers every police-reported motor vehicle collision in NYC since July 2012 and is refreshed daily, typically one to two days behind the incidents themselves. Nothing is cached on our side — the dashboard queries the live dataset each time you apply a date filter.
Each record represents one collision and includes:
- Date and time of the collision
- Location — borough, street names, and latitude/longitude
- Persons injured or killed, broken out by pedestrians, cyclists, and motorists
- Contributing factors
- Vehicle types involved
Two groups of fields do most of the work for this dashboard:
number_of_cyclist_injuredandnumber_of_cyclist_killed— cyclist casualty counts, used to find incidents involving cyclists.vehicle_type_code1throughvehicle_type_code5— the vehicle types involved, used to tell traditional bicycles apart from e-bikes and e-scooters.
How we query it
NYC Open Data exposes its datasets through the Socrata Open Data API (SODA), which accepts SQL-like SoQL filters directly in the URL. All requests go to a single endpoint:
https://data.cityofnewyork.us/resource/h9gi-nx95.jsonCyclist injuries
https://data.cityofnewyork.us/resource/h9gi-nx95.json?$where=number_of_cyclist_injured > 0 AND crash_date between '2025-01-01' and '2026-01-01'&$limit=10000Fetches every incident in the date range where at least one cyclist was injured.
Cyclist fatalities
https://data.cityofnewyork.us/resource/h9gi-nx95.json?$where=number_of_cyclist_killed > 0 AND crash_date between '2025-01-01' and '2026-01-01'&$limit=10000Fetches every incident in the date range where at least one cyclist was killed.
All incidents (bike-type comparison)
https://data.cityofnewyork.us/resource/h9gi-nx95.json?$where=crash_date between '2025-01-01' and '2026-01-01'&$limit=50000Fetches every incident in the range, which we then classify client-side into traditional-bicycle and e-bike incidents.
Query parameters
$where— SoQL filter string$limit— maximum records returned (default 1,000; max 50,000)$offset— records to skip, for pagination$order— sort field, e.g.crash_date DESC
The API allows 1,000 requests per day without an app token and 10,000 with one. We use anonymous access, which is well within the daily limit for this dashboard.
In the code
The main dashboard fetches injuries and fatalities in parallel, then merges the results — a single crash can involve both an injury and a fatality, so records are deduplicated by collision_id:
const dateFilter = `crash_date between '${from}' and '${to}'`
const [injuriesResponse, fatalitiesResponse] = await Promise.all([
fetch(`${BASE_URL}?$where=number_of_cyclist_injured > 0 AND ${dateFilter}&$limit=10000`),
fetch(`${BASE_URL}?$where=number_of_cyclist_killed > 0 AND ${dateFilter}&$limit=10000`),
])
// Merge and deduplicate by collision_id — an incident can
// appear in both result sets
const uniqueMap = {}
for (const item of [...(await injuriesResponse.json()), ...(await fatalitiesResponse.json())]) {
if (item.collision_id) uniqueMap[item.collision_id] = item
}
const uniqueData = Object.values(uniqueMap)E-bike classification
The dataset never labels a record as an “e-bike incident.” Vehicle types are free-text fields filled in by reporting officers, so the same machine shows up as E-BIKE, ELECTRIC BICYCLE, MOTORIZED SCOOTER, and dozens of other spellings. To classify incidents, we scan every available vehicle-type field (and, on the main dashboard, the contributing-factor fields) for a broad set of terms:
An incident matching any of these terms is classified as an e-bike or e-scooter incident. An incident whose vehicle types mention bicycle or bike without matching an e-bike term is classified as a traditional bicycle:
const isEBikeIncident = (incident) => {
const vehicleTypes = [
incident.vehicle_type_code1,
incident.vehicle_type_code2,
incident.vehicle_type_code3,
incident.vehicle_type_code4,
incident.vehicle_type_code5,
]
.filter(Boolean)
.map((type) => type.toLowerCase())
return vehicleTypes.some((type) => E_BIKE_TERMS.some((term) => type.includes(term)))
}
const isRegularBikeIncident = (incident) =>
!isEBikeIncident(incident) &&
vehicleTypesOf(incident).some((type) => type.includes("bicycle") || type.includes("bike"))A few real-world combinations and how they are classified:
| Vehicle type 1 | Vehicle type 2 | Classified as |
|---|---|---|
| BICYCLE | PASSENGER VEHICLE | Traditional bike |
| E-BIKE | TAXI | E-bike / e-scooter |
| BICYCLE | ELECTRIC SCOOTER | E-bike / e-scooter |
| BICYCLE | UNKNOWN | Traditional bike |
| MOTORIZED BICYCLE | SPORT UTILITY VEHICLE | E-bike / e-scooter |
Filtering & processing
After fetching and classifying, every view applies the same cleanup steps:
- Deduplication. Records are keyed by
collision_idso a crash returned by both the injury and fatality queries is counted once. - Coordinate validation. Incidents with missing coordinates, or coordinates outside the NYC bounding box (latitude 40.4–41.0, longitude −74.3 to −73.6), are excluded from map-based views.
- Date-range filtering. Your selected range is applied in the SoQL query itself, so only relevant records are transferred.
- E-bike exclusion. The main dashboard removes incidents matching the e-bike terms, so its figures describe traditional bicycles only.
Keeping the bike-type comparison fair
- Classification by vehicle type, not casualties. The comparison includes every incident involving a bicycle or e-bike, even when no one was injured or killed.
- Broad term matching.The wide term list compensates for the dataset's inconsistent terminology.
- All vehicle fields checked. Codes 1–5 are all examined, so a bike listed as a secondary vehicle is still counted.
- Identical time periods. Both groups are always compared over the exact same range, so seasons, weather, and traffic affect them equally.
Presentation choices
Comparisons use side-by-side charts with consistent scales, percentages of each group's total to normalize for the different volumes of traditional-bike and e-bike incidents, and visible incident counts on every chart so you always know how much data sits behind a pattern.
Limitations
The numbers on this site are only as good as the underlying police reports. These caveats affect the completeness, accuracy, and comparability of everything shown here.
We keep refining this methodology — better classification of ambiguous vehicle types, supplementary data sources to validate the NYPD records, and deeper temporal and geographic analysis are all on the list.
Frequently asked questions
See the methodology in action on the main dashboard, the bike-type comparison, or the time-period comparison.