Restaurants, Rats, and 311 Complaints: Mapping NYC’s Rodent Hotspots

Author

WingChan

Introduction

New York City is a global dining hub, with restaurants seemingly on every street corner. At the same time, the city is infamous for rodent sightings, often reported through 311 complaints. The COVID-19 pandemic reshaped the city’s dining landscape: indoor dining was temporarily halted, and the Open Restaurants Program was launched, enabling restaurants to apply for outdoor seating.

Given this unique intersection of dense restaurant activity, expanded outdoor dining, and persistent rodent issues, it is important to understand how these factors may be related. This analysis aims to uncover patterns and insights connecting restaurant density, outdoor seating, and rodent complaints across the city focusing on the following key areas:

  • Neighborhoods with elevated density of rodent-related restaurant inspections

  • Temporal and spatial trends in rodent complaints

  • Potential influence of outdoor dining on rodent complaints

  • Correlation between restaurant-related rodent activity and 311 complaints

Datasets used:

  • DOHMH Restaurant Inspections (2024–2025, including rodent-related violations)

  • 311 Rodent Complaints (2019–2025)

  • Open Restaurant Applications (June 2020–Aug 2023)

  • NTA 2020 boundaries for spatial analysis

Data acquisition

  1. DOHMH New York City Restaurant Inspection

This dataset provides the data tracks all violations, scores, and grades from inspections of NYC restaurants.

The following process loaded the raw data and then filtered it to capture only the most recent, relevant inspection for each restaurant. This was achieved by selecting specific inspection types (like cycle or re-inspection) and records with a passing score or an assigned grade, ensuring the focus is solely on the current, final status of each establishment.

Code
library(readr)
library(tidyverse)
# Download DOHMH NYC Restaurant Inspection Results data set and save as CSV file: NYC_Insp_Results.csv
Open_Data_Sample <- read_csv("data/project/DOHMH_New_York_City_Restaurant_Inspection_Results_20251105.csv",
                             col_types= cols(ZIPCODE = col_character())
)
#Filter on inspection type, score, grade
Inspections <- Open_Data_Sample %>%
  filter((`INSPECTION TYPE` %in% 
            c('Cycle Inspection / Re-inspection'
              ,'Pre-permit (Operational) / Re-inspection')
          |(`INSPECTION TYPE` %in%
              c('Cycle Inspection / Initial Inspection'
                ,'Pre-permit (Operational) / Initial Inspection')) 
          & SCORE <= 13)
         | (`INSPECTION TYPE` %in%  
              c('Pre-permit (Operational) / Reopening Inspection'
                ,'Cycle Inspection / Reopening Inspection'))
         & GRADE %in% c('A', 'B', 'C', 'P', 'Z')) %>%
  
  select(CAMIS,`INSPECTION DATE`)

#Select distinct inspections
Inspections_Distinct <- distinct(Inspections)

#Select most recent inspection date
MostRecentInsp <- Inspections_Distinct %>%
  group_by(CAMIS) %>%
  slice(which.max(as.Date(`INSPECTION DATE`,'%m/%d/%Y')))

#Join most recent inspection with original dataset
inner_join(Open_Data_Sample,MostRecentInsp, by = "CAMIS","INSPECTION DATE")


#Select restaurant inspection data based on most recent inspection date
Final <- Open_Data_Sample %>% inner_join(MostRecentInsp) %>%
  filter((`INSPECTION TYPE` %in% 
            c('Cycle Inspection / Re-inspection'
              ,'Pre-permit (Operational) / Re-inspection'
              , 'Pre-permit (Operational) / Reopening Inspection' 
              ,'Cycle Inspection / Reopening Inspection')
          |(`INSPECTION TYPE` %in%
              c('Cycle Inspection / Initial Inspection'
                ,'Pre-permit (Operational) / Initial Inspection')) 
          & SCORE <= 13)) %>%
  select(CAMIS,DBA,`INSPECTION DATE`,GRADE,`INSPECTION TYPE`,`VIOLATION CODE`, `VIOLATION DESCRIPTION`,`CRITICAL FLAG` ,BORO, BUILDING,STREET,ZIPCODE, NTA,Latitude,Longitude) #select necessary columns


#  filter(`VIOLATION CODE` %in% c("08A", "04K", "04L"))%>%  #Filter the violation with mice, rat, rodent related inspection, assuming those are rest with rodent issue
  
###Select distinct restaurant inspection data
Final <- distinct(Final)
glimpse(Final) 

Final <- Final %>%
  mutate(`INSPECTION_DATE` = mdy(`INSPECTION DATE`))
Final %>%
  arrange(`INSPECTION_DATE`)
Final|>
  arrange(desc(`INSPECTION_DATE`))

Final <- Final %>%
  filter(year(INSPECTION_DATE) %in% c(2024, 2025))

glimpse(Final) 
  1. 311 Rodent Complaints

The data tracks all rodent-related problems reported by NYC residents to the 311 service line, covering complaints about rats, mice, or sanitation issues. It records the exact date, time, and location of the problem, along with the subsequent action taken by the Department of Health.

The following acquisition process pull the data directly from the city’s open data source via API. The function is written to download the data responsibly. It page through the entire data set with repeated requests to the server, downloading the entire history of complaints in small batches, one after the other. All these individual batches were then combined into one final dataset for analysis. It is also designed to adopt a strategy of caching results.

Code
library(httr2) 
library(jsonlite) 
library(dplyr) 
library(sf) 
library(purrr) 
library(tidyr)

# This makes the workflow reproducible and avoids re-downloading each time. 
# Since we are not creating any docs or files right now, this is for later reference. 
if (!dir.exists("data/final")) { 
  dir.create("data/final", showWarnings = FALSE, recursive = TRUE) 
} 

# Function to download 311 rat complaints 
##################################################### 
# Define file path
rat311_file <- "data/final/rat311_full.rds"

get_rat311_full <- function() {
  base_url <- "https://data.cityofnewyork.us/resource/cvf2-zn8s.geojson"
  limit <- 50000   # max per request
  offset <- 0
  all_data <- list()
  
  repeat {
    resp <- request(base_url) %>%
      req_url_query(
        "$limit" = limit,
        "$offset" = offset
      ) %>%
      req_perform()
    
    resp_check_status(resp)
    
    raw_json <- resp_body_string(resp)
    gj <- fromJSON(raw_json, flatten = TRUE)
    
    # Convert features to tibble
    dat <- gj$features %>% as_tibble()
    
    if (nrow(dat) == 0) break   # stop if no more rows
    
    all_data[[length(all_data) + 1]] <- dat
    offset <- offset + nrow(dat)
    
    cat("Downloaded rows:", offset, "\n")
  }
  
  # Combine all pages into a single tibble
  bind_rows(all_data)
}

# Check if file exists, otherwise download
if (file.exists(rat311_file)) {
  rat311_full <- readRDS(rat311_file)
  cat("Loaded rat 311 data from local file.\n")
} else {
  rat311_full <- get_rat311_full()
  saveRDS(rat311_full, rat311_file)
  cat("Downloaded rat 311 data and saved locally.\n")
}

# Quick look at the data
glimpse(rat311_full)
  1. Open_Restaurant_Applications_(Historic) data

This dataset tracks all historical applications submitted by New York City restaurants to participate in the Open Restaurant Program, which permits temporary expansion of seating onto sidewalks and roadways during COVID period. The temporary emergency program was ended in Aug 2023, replaced by the permanent “Dining Out NYC” initiative.

The following acquisition process involved reading a file containing the complete application history, and the accompanying code then filtered this history to keep only the most recent application for each unique restaurant. This step ensures that the final data used for analysis accurately reflects once for every establishment that applied to the program.

Code
open_rest <- read_csv("data/project/Open_Restaurant_Applications_(Historic)_20251107.csv")
glimpse(open_rest)
summary(open_rest)

open_rest <- open_rest %>%
  mutate(`SubmissionDateTime` = mdy_hms(`Time of Submission`))
open_rest %>%
  arrange(`SubmissionDateTime`) |>
  select(SubmissionDateTime)


#get distinct resturant application
distinct_open_rest <-
  open_rest |>
  group_by(`Food Service Establishment Permit #`) %>%
  arrange(desc(`Time of Submission`), .by_group = TRUE) %>%
  slice_head(n = 1) %>%
  ungroup()
  1. NTA data

The data provides the official geographic boundaries (shapes) for all Neighborhood Tabulation Areas (NTAs) across New York City, which are standardized, mid-sized statistical units used by the Department of City Planning. It is essential for geographically linking the data (inspections, complaints, and open resturants) to a neighborhood level for analysis.

The acquisition process involved using a computer script to retrieve the required GeoJSON file (a type of geographic data format) from the city’s server. To maintain efficiency, the script was designed to download and save the file only once locally, after which it reads the data and prepares the map by setting all coordinates to the standard Latitude/Longitude system, making it immediately ready for geographic analysis with other datasets.

Code
# Downloads and prepares NTA polygons 
###################################### 

# This URL serves the 2020 NTA boundaries as a (projected) GeoJSON from ArcGIS. 
nta_url  <- "https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/NYC_Neighborhood_Tabulation_Areas_2020/FeatureServer/0/query?where=1=1&outFields=*&outSR=4326&f=pgeojson" 
nta_path <- "data/final/nta_2020.geojson" 

# To keep the workflow efficient and reproducible, this: 
#  - Downloads the file once and saves it locally 
#  - Reuses the local file in future runs instead of hitting the server again 
if (!file.exists(nta_path)) { 
  resp <- request(nta_url) |> 
    req_perform() 
  
  resp_check_status(resp) 
  
  resp_body_raw(resp) |> 
    writeBin(con = nta_path) 
} 

# Reads the saved NTA GeoJSON as an sf object (polygons). 
# st_transform(4326) ensures everything is in WGS84 lat/lon (EPSG:4326),  
# which matches the coordinates used in the rat 311 data. 
nta <- st_read(nta_path, quiet = TRUE) |> 
  st_transform(4326) 

glimpse(nta)

Data Integration & Exploration

Potential influence of outdoor dining on rodent complaints

This analysis utilizes the NYC Open Restaurants Program dataset, introduced in June 2020 to facilitate outdoor dining when indoor seating was restricted due to COVID-19. As this data provides a longer timeline than the one-year DOHMHInspection data, it serves as a supplement for exploring the temporal and spatial connections between the proliferation of outdoor seating (and potential sanitary risks) and the rise in rodent complaints.

The analysis first analysis the total number of Open Restaurant applications submitted between June 2020 and August 2023 and visualizes the application trend over time. We then calculate the density of Open Restaurant applications per unit of neighborhood area to pinpoint the top ten NTAs where outdoor seating proliferation was most concentrated. Finally, the analysis filters 311 Rodent Complaints to the same time period and calculates the density of these complaints in each NTA, allowing for a direct comparison of the geospatial relationship between outdoor dining density and reported rodent issues across the city.

Integrate 311 Rodent Complaints data with NTA

The Open Restaurant application data is integrated with the Neighborhood Tabulation Areas (NTAs) as a preparation for spatial analysis. Although the original dataset provided the NTA name, it lacked the standardized NTA code and area information necessary for density calculations and alignment with other analyses.

The below code first prepares the raw application records by extracting the latitude and longitude and converting the data into mappable points. A spatial join is then executed, which uses these coordinates to accurately assign the corresponding standardized NTA code, name, and area (Shape__Area) to each application. It enables us to group the applications and reliably calculate the density of outdoor seating at the required neighborhood level.

Code
#4a. OpenResturant + NTA
# Converts OpenResturant coordinates into sf points 
########################################## 
open_rest <- open_rest |> 
  mutate( 
    lon = as.numeric(Longitude), 
    lat = as.numeric(Latitude) 
  ) |> 
  drop_na(lon, lat)


open_rest_sf <- open_rest %>%
  st_as_sf(
    coords = c("lon", "lat"),  
    crs = 4326,
    remove = FALSE
  )

open_rest_NTA <- st_join( 
  open_rest_sf, 
  nta[, c("NTA2020", "NTAName", "BoroName","Shape__Area")] # <- adjust to actual column names 
) 

glimpse(open_rest_NTA) 

Open Restaurant Applications (Historic) data Exploration

This step is to examine the overall Open Restaurant Applications data to understand the application trend and neighborhood with highest outdoor seating.

Code
#drop geo code for analysis
open_rest_NTA <- open_rest_NTA %>%
  st_drop_geometry() 
  

#Check data period
period_open_rest_NTA <- open_rest_NTA %>%
  summarise(
    min_date = min(as.Date(SubmissionDateTime), na.rm = TRUE),
    max_date = max(as.Date(SubmissionDateTime), na.rm = TRUE)
  )

#total number of applications = no. resturants had outdoor seating
tot_openseat = nrow(open_rest_NTA)
tot_openseat

library(dplyr)
library(ggplot2)
library(lubridate)

# Summarize number of applications per month
monthly_apps <- open_rest_NTA %>%
  mutate(submit_date = as.Date(SubmissionDateTime),        # convert to date
         month = floor_date(submit_date, "month")) %>%    # get first day of month
  group_by(month) %>%
  summarise(n_applications = n(), .groups = "drop")

#find top NTA
open_rest_NTA_top10 <- open_rest_NTA %>%
group_by(NTA2020, NTAName) %>%
  summarise(n_applications = n(), .groups = "drop") %>%
  arrange(desc(n_applications)) %>%
  slice_head(n = 10)

open_rest_NTA_top10

# normalize the # of outdoor seating resturant by divided by the area of NTA - to be fair
density_outdoor_by_nta <- open_rest_NTA %>%
  # count applications per NTA
  group_by(NTA2020, NTAName) %>%
  summarise(
    n_applications = n(),
    # area is repeated per row, so take first non-NA value
    nta_area = first(Shape__Area),
    .groups = "drop"
  ) %>%
  mutate(
    application_density = n_applications / nta_area
  ) %>%
  arrange(desc(application_density))

density_outdoor_by_nta_top10 <-density_outdoor_by_nta %>%
  select(NTA2020,NTAName,application_density) %>%
  slice_head(n=10)

Key Findings

  • In the period of 2020-06-19 to 2023-08-02, there are 13121 number of applications.

  • The application volume peaked dramatically during the initial months, specifically from June to August 2020. This surge reflects the urgent need for restaurants to adapt and survive the pandemic-induced shutdown of indoor dining, with the application rate naturally slowing down significantly thereafter.

Code
# Plot line chart
ggplot(monthly_apps, aes(x = month, y = n_applications)) +
  geom_line(color = "steelblue", size = 1) +
  geom_point(color = "steelblue", size = 2) +
  labs(
    title = "Number of Restaurant Applications per Month",
    x = "Month",
    y = "Number of Applications"
  ) +
  theme_minimal() +
  scale_x_date(date_labels = "%Y-%m", date_breaks = "2 months") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

  • the highest number of outdoor seating applications were overwhelmingly concentrated in Manhattan’s commercial and dense residential centers. Specifically, the top neighborhoods for application volume are located in Downtown and Mid-Manhattan (e.g., East Village, Midtown, West Village). Outside of Manhattan, the only two neighborhoods that made the top application list were Williamsburg and Park Slope in Brooklyn.
Code
library(DT)
library(stringr)
library(dplyr)
open_rest_NTA_top10 |>
  datatable(options=list(searching=FALSE, info=FALSE)) 
  • To ensure a fair comparison across neighborhoods of varying sizes, the density of Open Restaurant applications (applications per unit of area) was calculated. This analysis revealed that the highest concentration of outdoor seating was overwhelmingly located in Manhattan’s most commercially dense districts, specifically Downtown and Mid-Manhattan (e.g., East Village, Greenwich Village, Midtown South). This pattern highlights the intense utilization of outdoor space in the city’s central business and entertainment areas during the program period.
Code
library(DT)
library(stringr)
library(dplyr)
density_outdoor_by_nta_top10 |>
  datatable(options=list(searching=FALSE, info=FALSE)) |>
  formatRound('application_density',  digits = 2)

Conclusion: Rodent Activity, Public Perception, and the Open Restaurants Program

This analysis integrated three disparate datasets (Open Restaurant Applications, 311 Rodent Complaints, and DOHMH Inspections) using Neighborhood (NTA) to determine the spatial and statistical relationship between expanded outdoor dining and rodent issues in New York City. The study design focused on leveraging normalized density metrics to ensure the analysis was robust and fair across NTAs of varying sizes, thereby overcoming a major challenge in city-wide spatial analysis where raw counts can simply reflect population or size.

Integrated Findings and Statistical Interpretation

Spatially, the analysis confirmed a concentration of both the supply of outdoor seating and publicly reported rodent activity in dense, mixed-use areas of Manhattan (e.g., Harlem, Upper West Side, and East Village). The core statistical objective was to quantify the environmental impact of the Open Restaurants Program:The correlation between Rodent Complaint Density and Official Inspection Violation Density was weak 0.3085201. This indicates that public perception (311 calls) is only weakly tied to verifiable internal restaurant sanitation failures.The correlation between Rodent Complaint Density and Outdoor Seating Density was marginally higher, yet still weak 0.3456858.

Policy Implications and External Factors

This marginal difference in correlation is highly instructive for policy development. It implies that the public’s decision to file a 311 complaint was slightly more aligned with the spatial concentration of visible external dining structures than with the official regulatory findings of internal restaurant hygiene.

This finding suggests that the expanded use of public space for dining contributed to a heightened perception of rodent activity, likely driven by external factors such as improper waste storage or structure-related harborage in dense outdoor dining zones. For the permanent “Dining Out NYC” initiative, this highlights that rodent mitigation efforts cannot solely focus on traditional health inspections but must prioritize the rigorous and consistent management of the visible, street-level infrastructure and waste disposal associated with outdoor dining setups.