311 Data Explained: What a Service Request Does and Does Not Measure

On this page

Problem statement

311 data is often used as if it were a sensor: noise complaints as a noise map, heat complaints as a map of cold apartments, illegal-parking requests as a map of illegal parking. It is something else β€” a log of requests made by people, routed to agencies and closed by them β€” and the difference decides what conclusions it can carry. This page goes through the fields of a New York 311 record using every request from July 2025: complaint types and descriptors, agencies, channels, statuses and dates, and the gap between a request and an incident.

Quick answer

Each row is one request: who it went to (agency), what it was about (complaint_type, refined by descriptor), where (incident_address, coordinates), how it arrived (open_data_channel_type), and what happened (status, closed_date, resolution_description). Several rows can describe one problem, and many problems never produce a row.

print(f"July 2025: {len(july):,} requests; distinct complaint types {july.complaint_type.nunique()}, descriptors {july.descriptor.nunique()}")
print("status:", july.status.value_counts(normalize=True).round(3).to_dict())
print(f"closed date filled: {july.closed_date.notna().mean():.1%}; resolution update date filled: {july.resolution_action_updated_date.notna().mean():.1%}")
July 2025: 315,883 requests; distinct complaint types 171, descriptors 805
status: {'Closed': 0.986, 'In Progress': 0.01, 'Pending': 0.001, 'Assigned': 0.001, 'Open': 0.001, 'Started': 0.0, 'Unspecified': 0.0}
closed date filled: 98.7%; resolution update date filled: 99.4%

July 2025's 315,883 requests use 171 complaint types and 805 descriptors. 98.6% were closed when the data was saved, fifteen months later; the rest are in progress, pending or open. 98.7% have a closed date and 99.4% a resolution update date.

Two bar charts for the ten busiest agencies: July 2025 requests and the number of complaint types each handles Two bar charts for the ten busiest agencies: July 2025 requests and the number of complaint types each handles
The police handled 0.474 of July's requests; the health department the most complaint types (34).

The fields of a request

1. Complaint type and descriptor

t = july[july.complaint_type == "Noise - Residential"].descriptor.value_counts()
print("Noise - Residential descriptors:", t.to_dict())
print("complaint types written in capitals (older HPD categories):", sorted({c for c in july.complaint_type.unique() if c.isupper()})[:6])
Noise - Residential descriptors: {'Loud Music/Party': 15990, 'Banging/Pounding': 7952, 'Loud Talking': 2437, 'Loud Television': 280}
complaint types written in capitals (older HPD categories): ['APPLIANCE', 'DOOR/WINDOW', 'ELECTRIC', 'ELEVATOR', 'FLOORING/STAIRS', 'GENERAL']

The complaint type is the category the caller or app chose; the descriptor refines it. Residential noise splits into loud music or parties (15,990 in July), banging (7,952), loud talking and loud television. Some complaint types are in capitals β€” APPLIANCE, ELEVATOR, PLUMBING β€” a naming convention carried over from the housing department's older categories.

2. Agencies and routing

x = july.groupby("agency").complaint_type.nunique().sort_values(ascending=False)
print("complaint types per agency:", x.head(6).to_dict())
print("NYPD's share of July requests:", round((july.agency == "NYPD").mean(), 3))
complaint types per agency: {'DOHMH': 34, 'DOT': 25, 'DSNY': 24, 'NYPD': 23, 'DOB': 17, 'DEP': 13}
NYPD's share of July requests: 0.474

Each agency handles a family of types: the health department (DOHMH) 34 in July, transportation (DOT) 25, sanitation (DSNY) 24, police 23. The police alone handled 0.474 of July's requests, because parking and noise go to them.

3. Channels: who reports

ch = pd.crosstab(july.complaint_type, july.open_data_channel_type, normalize="index")
top = july.complaint_type.value_counts().head(6).index
print(ch.loc[top].round(2).to_string())
open_data_channel_type   MOBILE  ONLINE  OTHER  PHONE  UNKNOWN
complaint_type                                                
Illegal Parking            0.45    0.38    0.0   0.16      0.0
Noise - Residential        0.21    0.65    0.0   0.14      0.0
Noise - Street/Sidewalk    0.42    0.48    0.0   0.10      0.0
Water System               0.23    0.32    0.0   0.45      0.0
Blocked Driveway           0.26    0.29    0.0   0.45      0.0
UNSANITARY CONDITION       0.00    0.37    0.0   0.63      0.0

The channel depends on the complaint. Illegal parking is reported mostly from phones in the app (45%), residential noise mostly online (65%), and unsanitary conditions mostly by phone call (63%). Each channel reaches different people, so a change in channel use β€” a new app feature, a phone outage β€” changes the counts.

Stacked bars of mobile, online and phone shares for six common complaint types in July 2025 Stacked bars of mobile, online and phone shares for six common complaint types in July 2025
Illegal parking comes mostly from the app, residential noise online, unsanitary conditions by phone.

4. A request is not an incident

same = july.groupby(["complaint_type", "incident_address", july.created_date.dt.date]).size()
print(f"type x address x day groups: {len(same):,} for {len(july):,} requests; groups with more than one request: {(same > 1).mean():.1%}, "
      f"holding {same[same > 1].sum() / same.sum():.1%} of requests")
type x address x day groups: 248,744 for 315,883 requests; groups with more than one request: 12.8%, holding 28.7% of requests

Grouping by type, address and day leaves 248,744 groups for 315,883 requests: 12.8% of the groups have more than one request, and they hold 28.7% of all requests. Several neighbours call about one party; one person reports the same blocked hydrant three times. A count of requests overstates the count of incidents, by different amounts for different types (311 counts inflated by duplicates).

Locations and channels over time

The data used on this page

NYC 311 service requests from NYC Open Data: every request created in July 2025 and the 2025 daily counts, as queried on 15 October 2026 and saved on this site.

from pathlib import Path

import geopandas as gpd
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
# NYC 311 Service Requests (NYC Open Data, dataset erm2-nwe9) as queried on 15 October 2026 and saved on this site,
# because requests that are still open keep changing: every request created in July 2025, and the 2025 daily counts by
# agency, complaint type, borough and channel. The queries are in data/README.txt; the live API is
# https://data.cityofnewyork.us/resource/erm2-nwe9.csv
FILES = ["nyc-311-2025-daily-counts-2026-10-15.zip", "nyc-311-2025-07-requests-2026-10-15.zip", "nyc-census-2020-by-geography-2026-10-15.xlsx"]
for name in FILES:
    if not Path(f"data/{name}").exists():
        Path(f"data/{name}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{name}", timeout=600).content)
daily = pd.read_csv("data/nyc-311-2025-daily-counts-2026-10-15.zip", parse_dates=["day"])
july = pd.read_csv("data/nyc-311-2025-07-requests-2026-10-15.zip", low_memory=False,
                   dtype={"unique_key": str, "incident_zip": str, "bbl": str, "community_board": str},
                   parse_dates=["created_date", "closed_date", "resolution_action_updated_date"])
cen = pd.read_excel("data/nyc-census-2020-by-geography-2026-10-15.xlsx", sheet_name="2010, 2020, and Change", skiprows=3)
boro_pop = cen[cen.GeoType == "Boro"].assign(b=lambda d: d.Name.str.upper()).set_index("b").Pop_20
print(f"NYC 311: {daily.n.sum():,} requests in the 2025 daily counts; {len(july):,} requests created in July 2025")
NYC 311: 3,655,042 requests in the 2025 daily counts; 315,883 requests created in July 2025

Example 1: location types

print("noise locations:", july[july.complaint_type.str.startswith("Noise")].location_type.value_counts().head(5).to_dict())
noise locations: {'Street/Sidewalk': 27006, 'Residential Building/House': 26643, 'Store/Commercial': 2398, 'Club/Bar/Restaurant': 1578, 'Above Address': 1324}

For noise, the location type says where the noise was: street or sidewalk (27,006) and residential buildings (26,643) far ahead of stores, bars and clubs.

Lines of online, phone and mobile app shares of 311 requests by month in 2025 Lines of online, phone and mobile app shares of 311 requests by month in 2025
The online share moved between 39.2% in January and 46.0% in December.

Example 2: how much the channel mix moved during 2025

c = daily.pivot_table(index=daily.day.dt.month, columns="open_data_channel_type", values="n", aggfunc="sum")
print("online share by month:", (c.ONLINE / c.sum(axis=1)).round(3).to_dict())
online share by month: {1: 0.392, 2: 0.429, 3: 0.437, 4: 0.426, 5: 0.433, 6: 0.423, 7: 0.42, 8: 0.449, 9: 0.441, 10: 0.416, 11: 0.415, 12: 0.46}

The online share of requests moved between 39.2% in January and 46.0% in December 2025 β€” enough to matter when comparing months.

Requests, not incidents

Every 311 analysis rests on two filters between the world and the data. First, a problem has to be noticed and reported: that depends on who lives there, whether they know about 311, whether they expect it to help, and whether they would rather not involve the police. Second, the report has to be routed and recorded: complaint types are chosen from menus that change, and agencies close requests in different ways, some immediately on referral, some after an inspection.

So 311 counts compare well within one complaint type, one area and a short period, and badly across types, places with different populations, or years with different systems. Normalising by population helps with the second; deduplicating helps with repeat reports; nothing in the data itself corrects for who chooses to call.

Fields that mislead

  • closed_date is administrative. A request may be closed when referred, when an officer arrives, or when the problem is fixed; see 311 response times.
  • resolution_action_updated_date often falls after the close date; it is the last update, not the resolution time.
  • status changes until a request is closed; save the date of your download.
  • Coordinates are geocoded from the address or intersection and can be missing for street segments.

FAQ

Do 311 complaints measure how often problems happen?

No β€” they measure reports. In July 2025, 28.7% of requests shared a type, address and day with another request, and problems nobody reports never appear.

What is the difference between complaint type and descriptor?

The complaint type is the category (Noise - Residential); the descriptor refines it (Loud Music/Party).