Background¶

This all stems from an observation that even in the snowiest city in the country, there is not an online way to make a service request for snow plawing on your street.

Cityline tells us how we can make service requests for lots of things (potholes, etc), but not snow plowing. For that, you have to make a phone call.

Figuring that people will still report online, I wanted to pull in data from the last couple of months to Cityline to see how many were snow plow related.

Pull data from Seel Click Fix API¶

What we’re pulling (and why)¶

This notebook uses the public SeeClickFix API to pull issues for Syracuse (Cityline). Technically, the API returns a list of issues with fields like:

  • summary: the official request type the resident selected (the dropdown choice)
  • description: the free‑text narrative the resident wrote (what they actually want)
  • created_at, lat/lng, address, status, etc.

Interpretation: the summary column reflects how the system wants issues to be grouped; the description reflects the resident’s intent. The rest of the notebook compares those two things.

In [1]:
import time
import requests
import pandas as pd

BASE = "https://seeclickfix.com/api/v2/issues"

def fetch_syracuse_issues(
    after_iso="2025-11-15T00:00:00Z",
    #status="open,acknowledged,closed",
    per_page=100,
    details=True,
    max_pages=200
):
    """
    Pull SeeClickFix issues for Syracuse using place_url filter.
    - after_iso: ISO8601 (Z) timestamp filter on created_at
    - status: comma-delimited statuses: open,acknowledged,closed,archived
    - details=True returns richer objects but per_page is effectively limited (docs warn details requests are limited)
    """
    session = requests.Session()
    headers = {
        # API requires a User-Agent; identify your app/project (docs)
        "User-Agent": "sam-seeclickfix-syracuse/0.1 (contact: you@example.com)",
        "Accept": "application/json",
    }


    page = 1
    all_issues = []

    while page <= max_pages:
        params = {
            "min_lat": 42.98,
            "min_lng": -76.25,
            "max_lat": 43.12,
            "max_lng": -76.05,       # key Syracuse filter
            "after": after_iso,
            #"status": status,
            "per_page": min(per_page, 100),
            "page": page,
        }
        if details:
            params["details"] = "true"

        r = session.get(BASE, headers=headers, params=params, timeout=60)
        r.raise_for_status()
        data = r.json()

        issues = data.get("issues", []) or []
        all_issues.extend(issues)

        # pagination info is in metadata.pagination (docs)
        meta = (data.get("metadata") or {}).get("pagination") or {}
        next_page = meta.get("next_page")

        if not next_page or len(issues) == 0:
            break

        page = next_page

        # be kind to their rate limits (docs mention limits; 429 includes Retry-After)
        time.sleep(0.25)

    return pd.DataFrame(all_issues)

df = fetch_syracuse_issues(after_iso="2025-11-15T00:00:00Z")
print(df.shape)
print(df.columns.tolist())
print(df.head(3)[["id","status","summary","created_at","lat","lng","address"]])
(381, 35)
['id', 'status', 'summary', 'description', 'rating', 'lat', 'lng', 'address', 'created_at', 'acknowledged_at', 'closed_at', 'reopened_at', 'updated_at', 'comments_count', 'actions', 'url', 'point', 'private_visibility', 'html_url', 'show_blocked_issue_text', 'request_type', 'comment_url', 'flag_url', 'transitions', 'reporter', 'media', 'crm_issue_url', 'organization_issue_url', 'integrations', 'assignee', 'questions', 'comments', 'votes', 'vote_count', 'current_user_relationship']
         id status                                       summary  \
0  20702931   Open            Report an illegally parked vehicle   
1  20702900   Open  Other Housing & Property Maintenance Concern   
2  20702740   Open                              Report a Pothole   

                  created_at        lat        lng  \
0  2025-12-31T12:34:28-05:00  43.025378 -76.161049   
1  2025-12-31T12:30:40-05:00  43.037988 -76.092673   
2  2025-12-31T12:07:46-05:00  43.076549 -76.148103   

                                       address  
0  1-133 Parkside Ave Syracuse, NY, 13207, USA  
1     102 Lockwood Rd Syracuse, NY, 13214, USA  
2       301 Hood Ave Syracuse, New York, 13208  

Scope and caveats¶

  • We’re pulling issues since mid‑November (see after_iso in the fetch function).
  • SeeClickFix records are user-entered; people will choose whatever category allows submission.
  • For the story we’re telling, we treat the official request type (summary) as the bucket and the description as the signal.

If you rerun this later, the counts can change as new issues come in or old issues are updated/closed.

How many requests overall?¶

Quick check: how many issues are we looking at?¶

This cell just reports the row count (number of issues) so we know the analysis sample size.

In [2]:
len(df)
Out[2]:
381

First pass: snow-related mentions (broad filter)¶

Technically, this step does a simple keyword search over description for snow-adjacent terms (e.g., snow, ice, plow, clear).

Interpretation: this is not the final classification. It’s a quick way to sanity-check that we’re seeing winter-related content and to pull a subset for inspection.

In [3]:
terms = [" plow", " clear", " snow", " ice"]

mask = df["description"].str.contains(
    "|".join(terms),
    case=False,
    na=False
)

snow_issues = df[mask]
In [4]:
snow_issues
Out[4]:
id status summary description rating lat lng address created_at acknowledged_at ... media crm_issue_url organization_issue_url integrations assignee questions comments votes vote_count current_user_relationship
0 20702931 Open Report an illegally parked vehicle Cars parked on wrong side of the road preventi... 1 43.025378 -76.161049 1-133 Parkside Ave Syracuse, NY, 13207, USA 2025-12-31T12:34:28-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 2431494, 'name': 'Syracuse Police Ordin... None [{'comment': 'SYRCityline assigned this issue ... [] 0 {'following': False, 'voted': False, 'reporter...
1 20702900 Open Other Housing & Property Maintenance Concern Yesterday, Dec 30, a plow took out our mailbox... 1 43.037988 -76.092673 102 Lockwood Rd Syracuse, NY, 13214, USA 2025-12-31T12:30:40-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 3286476, 'name': 'Cityline Coordinator'... None [{'comment': 'SYRCityline assigned this issue ... [] 0 {'following': False, 'voted': False, 'reporter...
2 20702740 Open Report a Pothole Filing this under Report a Pothole since there... 2 43.076549 -76.148103 301 Hood Ave Syracuse, New York, 13208 2025-12-31T12:07:46-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 3277038, 'name': 'Superintendent of Str... None [{'comment': 'SYRCityline assigned this issue ... [{'created_at': '2025-12-31T12:35:36-05:00'}] 1 {'following': False, 'voted': False, 'reporter...
4 20702654 Open Report an illegally parked vehicle car has been parked illegally since christmas.... 1 43.043100 -76.162600 433 Gifford St Syracuse, New York, 13204 2025-12-31T11:57:46-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 2431494, 'name': 'Syracuse Police Ordin... None [{'comment': 'SYRCityline assigned this issue ... [] 0 {'following': False, 'voted': False, 'reporter...
10 20702372 Open Pavement Markings Over 24 hours and street has not been plowed 2 43.081117 -76.145778 313 Malverne Dr Syracuse, New York, 13208 2025-12-31T11:24:40-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 3397675, 'name': 'Transportation - Crew... [{'question': 'Which of the following best des... [{'comment': 'SYRCityline assigned this issue ... [{'created_at': '2025-12-31T11:34:02-05:00'}] 1 {'following': False, 'voted': False, 'reporter...
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
341 20516219 Acknowledged Other Housing & Property Maintenance Concern Neighbor plowed snow onto our sidewalk and lef... 3 43.078960 -76.142946 151 Malverne Dr Syracuse, NY, 13208, USA 2025-12-01T18:20:18-05:00 2025-12-03T16:47:39-05:00 ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 3286476, 'name': 'Cityline Coordinator'... None [{'comment': 'SYRCityline assigned this issue ... [{'created_at': '2025-12-02T08:04:14-05:00'}] 1 {'following': False, 'voted': False, 'reporter...
350 20510266 Closed Yard Waste I called 2 weeks ago and was told our street w... 2 43.032982 -76.121176 123 Buckingham Ave Syracuse NY 13210, United S... 2025-12-01T10:46:14-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 3339684, 'name': 'Superintendent of Str... None [{'comment': 'SYRCityline assigned this issue ... [] 0 {'following': False, 'voted': False, 'reporter...
355 20504601 Open Home & Building Maintenance owner has rented out building to drug addicts ... 1 43.033158 -76.170224 1000 Bellevue Ave Syracuse, New York, 13204 2025-11-30T10:42:15-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 2431494, 'name': 'Syracuse Police Ordin... [{'question': 'For additional assistance, plea... [{'comment': 'SYRCityline assigned this issue ... [] 0 {'following': False, 'voted': False, 'reporter...
356 20504388 Acknowledged Report an Unlisted Issue in a Park The owners of 107 Sedgwick Drive plow and dump... 4 43.062735 -76.129709 107 Sedgwick Dr Syracuse, New York, 13203 2025-11-30T09:32:26-05:00 2025-11-30T09:32:28-05:00 ... {'video_url': None, 'image_full': None, 'image... None None [{'remote_id': '2397', 'status': 'accepted', '... {'id': 3281754, 'name': 'Department of Public ... None [{'comment': 'SYRCityline assigned this issue ... [] 0 {'following': False, 'voted': False, 'reporter...
357 20502444 Closed Other Sewer-related Concerns Plows have come through twice and keep not doi... 2 43.066281 -76.146687 810 E Division St Syracuse, NY, 13208, USA 2025-11-29T14:31:59-05:00 None ... {'video_url': None, 'image_full': None, 'image... None None [] {'id': 3339684, 'name': 'Superintendent of Str... [{'question': 'Which of the following best des... [{'comment': 'SYRCityline assigned this issue ... [] 0 {'following': False, 'voted': False, 'reporter...

89 rows × 35 columns

How many snow/plow specific types of requests are there?¶

In [5]:
len(snow_issues)
Out[5]:
89

Intent classification (description-only) + request-type matrix¶

Technical approach

  • We do not use the request type label (summary) to infer intent.
  • We classify intent using the description only to avoid the category name “poisoning” the signal (e.g., “Property Maintenance” contains the word property even when the issue is clearly street not plowed).

We generate three flags:

  1. plow_related — street plowing / roadway clearing intent
  2. snow_not_plow — snow mentioned, but the request is not about street plowing (stairs/steps/sidewalk/tenant/property context, etc.)
  3. not_snow — everything else

Then we group by summary to form the matrix: what people selected vs what they wrote.

In [6]:
import pandas as pd
import re

# --- Columns ---
REQ_COL = "summary"        # request type (grouping)
DESC_COL = "description"   # ONLY source for intent classification

df = df.copy()
df[REQ_COL] = df[REQ_COL].fillna("").astype(str)
df[DESC_COL] = df[DESC_COL].fillna("").astype(str)

desc = df[DESC_COL].str.lower()

# --- Patterns ---
SNOW_ANY_RE = re.compile(r"\b(snow|ice|slush|winter|salt|sanding)\b", re.IGNORECASE)

# Street plowing / roadway clearing intent
PLOW_RE = re.compile(
    r"\b(plow|plowed|plowing|unplowed|not\s+plow|hasn.?t\s+been\s+plow|needs?\s+plow)\b",
    re.IGNORECASE,
)

# Private-property / non-road contexts that often mention snow but aren't street plowing
NON_PLOW_CONTEXT_RE = re.compile(
    r"\b(sidewalk|steps?|stairs?|porch|driveway|walkway|yard|tenant|landlord|building|house|property|code\s+enforcement)\b",
    re.IGNORECASE,
)

# --- Flags (BASED ONLY ON DESCRIPTION) ---
df["mentions_snow"] = desc.str.contains(SNOW_ANY_RE, na=False)
df["mentions_plow"] = desc.str.contains(PLOW_RE, na=False)
df["nonplow_context"] = desc.str.contains(NON_PLOW_CONTEXT_RE, na=False)

# Plow related = mentions plow AND not obviously private-property context
df["is_plow_related"] = df["mentions_plow"] & ~df["nonplow_context"]

# Snow but not plow = mentions snow (or ice etc) AND not plow-related
df["is_snow_not_plow"] = df["mentions_snow"] & ~df["is_plow_related"]

# Not snow at all (in description)
df["is_not_snow"] = ~df["mentions_snow"]

# --- Matrix by request type (summary) ---
matrix = (
    df.groupby(REQ_COL)
      .agg(
          total_requests=(REQ_COL, "size"),
          plow_related=("is_plow_related", "sum"),
          snow_not_plow=("is_snow_not_plow", "sum"),
          not_snow=("is_not_snow", "sum"),
      )
      .reset_index()
)

matrix["pct_plow_related"] = (matrix["plow_related"] / matrix["total_requests"]).round(3)
matrix["pct_snow_not_plow"] = (matrix["snow_not_plow"] / matrix["total_requests"]).round(3)

matrix = matrix.sort_values("total_requests", ascending=False)

matrix
C:\Users\samie\AppData\Local\Temp\ipykernel_53164\1746045822.py:30: UserWarning: This pattern is interpreted as a regular expression, and has match groups. To actually get the groups, use str.extract.
  df["mentions_snow"] = desc.str.contains(SNOW_ANY_RE, na=False)
C:\Users\samie\AppData\Local\Temp\ipykernel_53164\1746045822.py:31: UserWarning: This pattern is interpreted as a regular expression, and has match groups. To actually get the groups, use str.extract.
  df["mentions_plow"] = desc.str.contains(PLOW_RE, na=False)
C:\Users\samie\AppData\Local\Temp\ipykernel_53164\1746045822.py:32: UserWarning: This pattern is interpreted as a regular expression, and has match groups. To actually get the groups, use str.extract.
  df["nonplow_context"] = desc.str.contains(NON_PLOW_CONTEXT_RE, na=False)
Out[6]:
summary total_requests plow_related snow_not_plow not_snow pct_plow_related pct_snow_not_plow
38 Yard Waste 49 0 2 47 0.000 0.041
7 Other Housing & Property Maintenance Concern 42 8 15 22 0.190 0.357
9 Pavement Markings 37 30 2 35 0.811 0.054
19 Report a Pothole 34 1 1 33 0.029 0.029
28 Report an illegally parked vehicle 23 2 3 19 0.087 0.130
15 Report Litter on Public Land 17 0 0 17 0.000 0.000
22 Report a Skipped Bulk Pick Up (Internal) 16 0 0 16 0.000 0.000
23 Report a Skipped Recycling Pick Up (Internal) 15 0 0 15 0.000 0.000
1 Cart Broken 13 0 0 13 0.000 0.000
13 Report Improperly Set Out Trash or Recycling (... 13 0 1 12 0.000 0.077
8 Other Sewer-related Concerns 12 0 2 10 0.000 0.167
31 Street Lights 11 0 0 11 0.000 0.000
30 Sanitation Exempt Skipped 9 0 0 9 0.000 0.000
32 Tires 8 0 0 8 0.000 0.000
20 Report a Problem with a Catch Basin/Storm Drain 8 0 1 7 0.000 0.125
5 Home & Building Maintenance 8 3 3 5 0.375 0.375
16 Report Trash/Debris Outside a Home/Building 7 1 0 6 0.143 0.000
35 Trash Can on Public Land 7 0 1 6 0.000 0.143
17 Report a Dog Complaint 6 0 0 6 0.000 0.000
33 Traffic & Parking Signs 6 0 0 6 0.000 0.000
34 Traffic Signals 6 0 0 6 0.000 0.000
12 Removal Of Tree On Public Land 4 0 0 4 0.000 0.000
24 Report an Abandoned Vehicle 4 0 0 4 0.000 0.000
18 Report a Maintenance Issue in a Park 3 0 0 3 0.000 0.000
3 Deer Activity Reporting 2 0 0 2 0.000 0.000
4 Deer Sighting 2 0 0 2 0.000 0.000
36 Unlawful Dumping on Vacant Land 2 0 0 2 0.000 0.000
21 Report a Safety Issue in a Park (Non-Emergency... 2 1 1 0 0.500 0.500
6 Inspection Of Tree On Private Land 2 0 0 2 0.000 0.000
11 Pruning Of Tree On Public Land 2 0 0 2 0.000 0.000
39 big @#$% pothole 1 0 0 1 0.000 0.000
37 Vacant Buildings 1 0 0 1 0.000 0.000
0 Big pothole needs to be fixed one highway depa... 1 0 0 1 0.000 0.000
29 Request a free street tree planting (City of S... 1 0 0 1 0.000 0.000
27 Report an Unlisted Issue in a Park 1 0 1 0 0.000 1.000
26 Report an Issue with a Fire Hydrant 1 0 0 1 0.000 0.000
25 Report an Animal Complaint 1 0 0 1 0.000 0.000
14 Report Litter on Private Land 1 0 1 0 0.000 1.000
10 Post to Neighbors 1 0 0 1 0.000 0.000
2 Cart Stolen 1 0 0 1 0.000 0.000
40 huge pothole needs fix 1 0 0 1 0.000 0.000
In [7]:
totals = {
    "total": len(df),
    "plow_related": int(df["is_plow_related"].sum()),
    "snow_not_plow": int(df["is_snow_not_plow"].sum()),
    "mentions_snow_any": int(df["mentions_snow"].sum()),
}
totals
Out[7]:
{'total': 381,
 'plow_related': 46,
 'snow_not_plow': 34,
 'mentions_snow_any': 42}

What to look for in the output¶

The matrix is most informative when you focus on:

  • Proxy categories: request types that end up dominated by plow_related (suggesting residents found a workaround).
  • Catch-all categories: request types where a large share is snow_not_plow (legitimate winter safety/maintenance issues that are not street plowing).
  • Mixing zones: categories that contain meaningful counts of both plow_related and snow_not_plow.

Those patterns support the narrative:

  1. people complain about snow regardless of whether the system provides a “correct” place, and
  2. snow-related content ends up mixed into unrelated workflows, potentially burying other issues.

Pull examples for the write-up¶

Below we extract a few quotable examples from the core request types.
This is useful for turning the matrix into a story: you can show a couple of “obvious plowing” examples filed under unrelated categories, and a couple of “snow but not plowing” examples filed under broad maintenance buckets.

In [8]:
import pandas as pd
import re

CORE_COLS = ["id", "created_at", "summary", "address", "description"]

def pull_examples(df, request_type, cls, n=8, seed=7):
    """Return a small sample of examples from a request type.

    cls:
      - "plow"         -> df['is_plow_related'] == True
      - "snow_not_plow"-> df['is_snow_not_plow'] == True
      - "not_snow"     -> df['mentions_snow'] == False (or fallback)
    """
    d = df[df["summary"].eq(request_type)].copy()

    if cls == "plow":
        d = d[d["is_plow_related"]]
    elif cls == "snow_not_plow":
        d = d[d["is_snow_not_plow"]]
    elif cls == "not_snow":
        if "mentions_snow" in d.columns:
            d = d[~d["mentions_snow"]]
        else:
            d = d[~d["is_snow_not_plow"] & ~d["is_plow_related"]]
    else:
        raise ValueError("cls must be one of: plow, snow_not_plow, not_snow")

    d["desc_len"] = d["description"].fillna("").astype(str).str.len()
    d = d.sort_values("desc_len", ascending=False).head(50)  # richest descriptions
    if len(d) > 0:
        d = d.sample(min(n, len(d)), random_state=seed)

    return d[[c for c in CORE_COLS if c in d.columns]]

# ---- Examples (edit these categories as you like) ----
examples_pavement_plow = pull_examples(df, "Pavement Markings", cls="plow", n=6)
examples_property_plow = pull_examples(df, "Other Housing & Property Maintenance Concern", cls="plow", n=6)
examples_property_snow_not_plow = pull_examples(df, "Other Housing & Property Maintenance Concern", cls="snow_not_plow", n=6)

examples_pavement_plow, examples_property_plow, examples_property_snow_not_plow
Out[8]:
(           id                 created_at            summary  \
 137  20677764  2025-12-27T15:25:42-05:00  Pavement Markings   
 121  20677859  2025-12-27T15:43:37-05:00  Pavement Markings   
 125  20677836  2025-12-27T15:39:12-05:00  Pavement Markings   
 130  20677804  2025-12-27T15:33:40-05:00  Pavement Markings   
 128  20677816  2025-12-27T15:35:25-05:00  Pavement Markings   
 134  20677785  2025-12-27T15:30:10-05:00  Pavement Markings   
 
                                       address  \
 137  131 Huntley St Syracuse, New York, 13208   
 121  147 Huntley St Syracuse, New York, 13208   
 125  135 Huntley St Syracuse, New York, 13208   
 130  126 Huntley St Syracuse, New York, 13208   
 128  123 Huntley St Syracuse, New York, 13208   
 134  132 Huntley St Syracuse, New York, 13208   
 
                                            description  
 137  Needs to be plowed all streets around us are p...  
 121  Needs to be plowed all streets around us are p...  
 125  Needs to be plowed all streets around us are p...  
 130  Needs to be plowed all streets around us are p...  
 128  Needs to be plowed all streets around us are p...  
 134  Needs to be plowed all streets around us are p...  ,
            id                 created_at  \
 315  20553162  2025-12-06T11:58:51-05:00   
 22   20701442  2025-12-31T09:25:53-05:00   
 1    20702900  2025-12-31T12:30:40-05:00   
 110  20678790  2025-12-27T19:38:29-05:00   
 98   20683960  2025-12-29T08:04:03-05:00   
 42   20697797  2025-12-30T15:53:55-05:00   
 
                                           summary  \
 315  Other Housing & Property Maintenance Concern   
 22   Other Housing & Property Maintenance Concern   
 1    Other Housing & Property Maintenance Concern   
 110  Other Housing & Property Maintenance Concern   
 98   Other Housing & Property Maintenance Concern   
 42   Other Housing & Property Maintenance Concern   
 
                                              address  \
 315  Mile 3.2 State Rte 92 Syracuse, New York, 13214   
 22      1300-1316 Court St Syracuse, New York, 13208   
 1           102 Lockwood Rd Syracuse, NY, 13214, USA   
 110          311 Carlton Rd Syracuse, NY, 13207, USA   
 98              121 Avon Rd Syracuse, NY, 13206, USA   
 42      355 Buckingham Ave Syracuse, New York, 13210   
 
                                            description  
 315  snow plow damaged front lawn. Eventhough there...  
 22   Multiple cars are getting stuck on Hood Ave. H...  
 1    Yesterday, Dec 30, a plow took out our mailbox...  
 110  Snow is not plowed since last night. It’s haza...  
 98   Can this half of our street get plowed today b...  
 42   Plow this road. This road consistently is not ...  ,
            id                 created_at  \
 103  20681504  2025-12-28T14:47:38-05:00   
 341  20516219  2025-12-01T18:20:18-05:00   
 54   20690623  2025-12-29T17:25:37-05:00   
 297  20593214  2025-12-12T11:40:26-05:00   
 285  20620071  2025-12-16T16:03:48-05:00   
 211  20660228  2025-12-23T11:35:03-05:00   
 
                                           summary  \
 103  Other Housing & Property Maintenance Concern   
 341  Other Housing & Property Maintenance Concern   
 54   Other Housing & Property Maintenance Concern   
 297  Other Housing & Property Maintenance Concern   
 285  Other Housing & Property Maintenance Concern   
 211  Other Housing & Property Maintenance Concern   
 
                                            address  \
 103        708 Avery Ave Syracuse, New York, 13204   
 341       151 Malverne Dr Syracuse, NY, 13208, USA   
 54        2813 Burnet Ave Syracuse, NY, 13206, USA   
 297  1001 E Brighton Ave Syracuse, New York, 13205   
 285        429 Grant Blvd Syracuse, NY, 13206, USA   
 211         176 Wiman Ave Syracuse, NY, 13205, USA   
 
                                            description  
 103  The sidewalk snow plow hit our sewer vent and ...  
 341  Neighbor plowed snow onto our sidewalk and lef...  
 54                          Snow plowed over sidewalk   
 297  Kinney Drugs,  Nob Hill Apartments,  SPLASH CA...  
 285                         Snow plowed over sidewalk   
 211  I’m an English tutor at the West Side Learning...  )
In [ ]: