Data Science in Context | GeoAI for Safer Cities

From Raw Data to Safer Roads | Staten Island, New York

Sarah Abdelrahman | Fall 2026

Project objectives

01
Research question

Which Staten Island collision concentrations should be investigated first based on 2024 reported injuries?

02
Analytical methods

GIS mapping, 500-meter hexagonal collision counts, DBSCAN spatial clustering, and transparent ranking.

03
Decision-support outcome

Identify locations for road-safety investigation—not forecasts or causal conclusions.

WHY IT MATTERS A borough-wide collision total cannot show where targeted safety reviews may be useful.

Seven-step GeoAI workflow

Seven-stage GeoAI workflow: acquisition, cleaning, GIS, clustering, ranking, visualization, decision support

01 · Data acquisition

Source: NYC Open Data · NYPD Motor Vehicle Collisions · 2024 · Staten Island.

First six API records (live output)
collision_id crash_date borough latitude longitude number_of_persons_injured number_of_persons_killed
2044fd58e252f73aa85cb4ed7a6e8238 2024-01-01 STATEN ISLAND 40.64 -74.09 0 0
2c754de7a241f6ba4873748db90648f8 2024-01-01 STATEN ISLAND 40.56 -74.10 1 0
49dcf0a23e87f0e2d56f317aa1ce4bfa 2024-01-01 STATEN ISLAND 40.58 -74.09 0 0
af43f84a7471c60174396dd1316ca296 2024-01-01 STATEN ISLAND 40.64 -74.08 0 0
bc63e1006cd96ff2410242012043ac89 2024-01-01 STATEN ISLAND 40.58 -74.12 1 0
f6bc8934a9e2b5b05a9329d561866174 2024-01-01 STATEN ISLAND 40.56 -74.13 1 0

02 · Cleaning and validation

Cleaning audit and analysis totals
Metric Value
Raw records 2698
Valid unique geocoded collisions 2646
Records excluded or duplicated 52
DBSCAN clusters (excluding noise) 67
DBSCAN noise points 1047
Reported injuries in analyzed records 1285
  • Remove records without plausible Staten Island coordinates and duplicate collision IDs.
  • Convert injury counts to numeric and document missing-value treatment.
  • Coordinates outside the approximate borough bounding box are excluded; this is a screening rule, not a polygon-based borough test.

03 · GIS processing

  • Transform WGS84 longitude/latitude to EPSG:32618 (meters).
  • Build 500-meter hexagonal cells; count geocoded collisions per cell.
  • Run DBSCAN on projected point coordinates so eps = 180 means 180 meters.
Eight Highest Collision Counts — 500m Grid
Rank collisions
1 51
2 47
3 34
4 30
5 28
6 28
7 26
8 25

Map 1 · Collision locations

2024 · GEOCODED COLLISION RECORDS
KEY INSIGHT Blue points indicate collisions without reported injuries; red points indicate collisions with reported injuries.

Map 2 · Collision density

SPATIAL AGGREGATION · 500-METER HEXAGONS
KEY INSIGHT Highest observed hexagon count: 51 collisions. Counts are not adjusted for traffic exposure.

04 · Machine learning: DBSCAN

Unsupervised spatial clustering

  • eps = 180 meters: neighborhood search radius.
  • minPts = 8: minimum neighboring observations for a core point (including itself).
  • Cluster 0 is noise, not automatically a safe location.
  • Sensitivity to parameter selection must be assessed before policy use.
Eight highest-ranked clusters by reported injuries
cluster collisions injured killed
6 187 94 2
1 192 90 0
7 146 62 1
9 95 52 0
16 29 25 0
20 49 22 0
35 23 20 0
41 29 17 0

Map 3 · DBSCAN clusters

UNSUPERVISED LEARNING · 180-METER RADIUS
KEY INSIGHT 67 clusters detected; unassigned points are noise, not necessarily low-risk locations.

05 · Results and ranking

Geocoded collisions2,646Validated 2024 records
DBSCAN clusters67Excluding noise points
Reported injuries1,285In analyzed records

Ranking rule: total reported persons injured, then collision count.

Top three injury-ranked DBSCAN clusters
cluster collisions injured killed
6 187 94 2
1 192 90 0
7 146 62 1

Map 4 · Three priority clusters

DECISION SUPPORT · INJURY-FIRST RANKING

Decision rule: Rank by total reported persons injured, breaking ties with crash counts. Priority 1 has 94 reported injuries across 187 crashes. These are historical investigation priorities, not exposure-adjusted risk estimates.

KEY INSIGHT Priority 1: 94 injured persons across 187 collisions; historical counts, not predicted risk.

06 · GIS visualization

  • Four static maps are exported to generated_maps/ as PNGs.
  • Three additional close-ups are exported for review.
  • Esri World Street Map tiles are downloaded using maptiles and displayed using native terra::plotRGB() with sf overlays.
  • If the Esri basemap service is unavailable, rendering stops with an explicit error rather than silently omitting the street basemap.

07 · Decision support

  • Review the top-ranked clusters with NYC DOT and local stakeholders.
  • Inspect intersections, crossing distances, signal timing, speed patterns, and street design before choosing interventions.
  • Compare with pedestrian/cyclist exposure and traffic volumes to avoid treating raw counts as risk rates.
  • Evaluate changes with a before/after design and appropriate comparison locations.

Conclusions and recommendations

01 · Evidence GIS and DBSCAN reveal spatial concentrations of reported 2024 collisions.

02 · Prioritization Rank clusters by reported injuries, with collision count as the tie-breaker.

03 · Action Review street conditions and exposure before selecting interventions.

WHAT THIS DOES NOT PROVE

Crash causation, per-vehicle risk, future collision forecasts, or intervention effectiveness.

NEXT STEP

Validate sites, compare exposure, and test sensitivity to DBSCAN settings.

Technical appendix

Appendix A · Data acquisition (NYC Open Data)

Purpose: Retrieve 2024 Staten Island collision records using the public Socrata API.

endpoint <- "https://data.cityofnewyork.us/resource/h9gi-nx95.csv"
params <- list(
  `$select` = paste(
    "collision_id,crash_date,borough,latitude,longitude,",
    "number_of_persons_injured,number_of_persons_killed", sep = ""
  ),
  `$where` = paste0(
    "borough = 'STATEN ISLAND' AND ",
    "crash_date >= '2024-01-01T00:00:00' AND ",
    "crash_date < '2025-01-01T00:00:00'"
  ),
  `$order` = "crash_date ASC,collision_id ASC",
  `$limit` = "50000"
)
response <- httr::GET(endpoint, query = params, httr::timeout(90))
httr::stop_for_status(response)
raw <- readr::read_csv(
  httr::content(response, as = "text", encoding = "UTF-8"),
  show_col_types = FALSE
)

Appendix B · Data cleaning and validation

Purpose: Convert data types, screen coordinates, and remove duplicate collision IDs.

clean <- raw |>
  dplyr::mutate(
    latitude  = suppressWarnings(as.numeric(latitude)),
    longitude = suppressWarnings(as.numeric(longitude)),
    injured = dplyr::coalesce(
      suppressWarnings(as.numeric(number_of_persons_injured)), 0
    ),
    killed = dplyr::coalesce(
      suppressWarnings(as.numeric(number_of_persons_killed)), 0
    ),
    valid_coord = !is.na(latitude) & !is.na(longitude) &
      latitude >= 40.47 & latitude <= 40.66 &
      longitude >= -74.28 & longitude <= -74.04
  ) |>
  dplyr::filter(valid_coord) |>
  dplyr::distinct(collision_id, .keep_all = TRUE)

Coordinates are screened using approximate bounds, not an exact borough polygon. Missing injury and fatality counts are treated as zero in this analysis; this assumption should be checked against the source documentation.

Appendix C · Cleaning audit

Purpose: Make the number of retained and excluded records transparent.

audit <- data.frame(
  Metric = c("Raw API records",
             "Valid unique geocoded records",
             "Excluded or duplicate records"),
  Count = c(nrow(raw), nrow(clean), nrow(raw) - nrow(clean))
)
knitr::kable(audit, caption = "Data-cleaning audit")

The live audit is also reported in the main Cleaning and validation slide.

Appendix D · GIS coordinate transformation

Purpose: Work in meters before measuring distances and creating spatial grids.

pts_ll <- sf::st_as_sf(
  clean, coords = c("longitude", "latitude"),
  crs = 4326, remove = FALSE
)
pts_m <- sf::st_transform(pts_ll, 32618)  # UTM Zone 18N
xy <- sf::st_coordinates(pts_m)

Why it matters: DBSCAN’s 180-meter radius and the 500-meter grid require projected coordinates.

Appendix E · Hexagonal density analysis

Purpose: Count observed collisions inside 500-meter hexagonal cells.

hex <- sf::st_sf(
  geometry = sf::st_make_grid(
    pts_m, cellsize = 500, square = FALSE
  )
)
hex$collisions <- lengths(sf::st_intersects(hex, pts_m))
hex <- hex[hex$collisions > 0, , drop = FALSE]

These are raw collision counts, not traffic-volume-adjusted risk rates.

Appendix F · DBSCAN spatial clustering

Purpose: Identify spatial concentrations without specifying the number of clusters in advance.

cluster <- dbscan::dbscan(
  xy, eps = 180, minPts = 8
)
pts_m$cluster <- as.integer(cluster$cluster)
pts_ll$cluster <- pts_m$cluster

Parameters: eps = 180 meters; minPts = 8. Cluster 0 denotes noise, not a safe area.

Appendix G · Injury-based hotspot ranking

Purpose: Turn detected clusters into a transparent list for site investigation.

cluster_table <- pts_m |>
  sf::st_drop_geometry() |>
  dplyr::filter(cluster > 0) |>
  dplyr::group_by(cluster) |>
  dplyr::summarise(
    collisions = dplyr::n(),
    injured = sum(injured),
    killed = sum(killed),
    .groups = "drop"
  ) |>
  dplyr::arrange(
    dplyr::desc(injured),
    dplyr::desc(collisions),
    cluster
  )
priority <- head(cluster_table, 3)

Ranking rule: Most reported injuries first; collision count breaks ties. This does not predict future crashes.

Appendix H · GIS visualization and exports

Purpose: Produce reproducible map images and share the analytical tables.

# The full save_map() function is defined in the setup chunk.
# It plots Esri tiles using terra::plotRGB(), then overlays sf layers.
save_map("01_collision_locations", "locations",
         heading = "Collision locations")
knitr::include_graphics(
  "generated_maps/01_collision_locations.png"
)

write.csv(cluster_table, "generated_data/03_cluster_summary.csv",
          row.names = FALSE)
write.csv(priority, "generated_data/04_priority_clusters.csv",
          row.names = FALSE)

Map generation requires internet access for Esri basemap tiles on first render. The QMD contains the full implementation; this slide shows a concise example.

Appendix I · Priority close-up 1

SITE REVIEW · PRIORITY 1
KEY INSIGHT Reported injuries: 94 · Collisions: 187.

Appendix J · Priority close-up 2

SITE REVIEW · PRIORITY 2
KEY INSIGHT Reported injuries: 90 · Collisions: 192.

Appendix K · Priority close-up 3

SITE REVIEW · PRIORITY 3
KEY INSIGHT Reported injuries: 62 · Collisions: 146.

Appendix L · Data and reproducibility

Outputs created automatically

  • generated_data/01_raw_collisions.csv
  • generated_data/02_clean_collisions.csv
  • generated_data/03_cluster_summary.csv
  • generated_data/04_priority_clusters.csv
  • generated_data/05_data_quality_summary.csv
  • generated_maps/ — four main maps and three priority close-ups

Parameters: Staten Island · 2024 · EPSG:32618 · 500 m hexagons · DBSCAN 180 m / 8 points.

References