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_injured and number_of_cyclist_killed — cyclist casualty counts, used to find incidents involving cyclists.
  • vehicle_type_code1 through vehicle_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.json

Cyclist 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=10000

Fetches 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=10000

Fetches 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=50000

Fetches 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:

e-bikebikelectric bice-scooterescooterelectric scooterscootermopedmotorizedelectrice bikee-mobilitye-vehiclebatterycharginglithiumpoweredmotore-transportation

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 1Vehicle type 2Classified as
BICYCLEPASSENGER VEHICLETraditional bike
E-BIKETAXIE-bike / e-scooter
BICYCLEELECTRIC SCOOTERE-bike / e-scooter
BICYCLEUNKNOWNTraditional bike
MOTORIZED BICYCLESPORT UTILITY VEHICLEE-bike / e-scooter

Filtering & processing

After fetching and classifying, every view applies the same cleanup steps:

  • Deduplication. Records are keyed by collision_id so 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.