Here's the write-up, in your template headings. Anything in [brackets] is a number or finding only your actual run can give you, so fill those in last. I wrote it assuming the pipeline works as planned, so trim anything you don't end up building.
Inspiration
NYC's rodent problem shows up in two places that rarely get compared. Residents report rats on streets through 311, and health inspectors find rodent evidence in restaurant kitchens. We wanted to put those two views side by side. When they agree, you have a real hotspot. When they disagree, that is more interesting: either residents are seeing a street problem that restaurants aren't causing, or inspectors are finding a problem nobody is reporting. We wanted a tool that ranks neighborhoods and also explains why.
What it does
Rodent Radar ranks NYC ZIP codes by rodent problem by combining 311 rodent complaints with restaurant rodent violations. It sorts every ZIP into one of four groups:
- True hotspot: high on both complaints and violations.
- Street problem: many complaints, few restaurant violations.
- Hidden problem: many violations, few complaints, which suggests under-reporting.
- Quiet: low on both.
For each top ZIP, a Mistral reasoning model writes a short explanation: restaurant density versus complaint volume, what kind of evidence dominates, and where the two sources disagree. Every claim cites a document ID, and our code checks that each cited ID actually exists in Elasticsearch. The output is a notebook, with no front end.
How we built it
Data. The DOHMH restaurant inspection results and the 311 Service Requests dataset, both from NYC Open Data, filtered to rodent-related records and cut to the period they both cover.
Mistral.
- Structured outputs turn free text (violation descriptions, 311 descriptors and resolution notes) into clean fields: rodent type, evidence type, location context, severity (1 to 5), and resolution outcome.
- We extract once per unique normalized string and cache the result, which turns tens of thousands of calls into a few hundred.
- A reasoning model writes the per-ZIP explanations as strict JSON, using only an evidence packet we build for it.
Elastic.
- Explicit mappings and bulk ingest into separate indices for all restaurant inspection rows (used for denominators), rodent violations, and rodent complaints.
- ES|QL rollups per ZIP: complaint counts, violation rates per inspection, restaurant density, and average severity.
- A transparent composite score built from percentile ranks, with the quadrant computed in code.
- A hotspots index that stores the metrics, the explanation, and the citation check result, queryable with ES|QL for the demo.
Challenges we ran into
- Row meaning. The inspection data has one row per violation line, not per inspection, so we defined an inspection as restaurant ID plus inspection date to get honest rates.
- A mismatched starter notebook. The 311 starter notebook was set up for a different dataset (public feedback), so we switched to the full Service Requests data.
- Dirty dates and mismatched windows. Placeholder 1900 dates, non-ISO date formats, and two sources covering different periods meant we cut both to their overlap.
- Repetitive text. Resolution notes contain dates, which made nearly every string look unique. Normalizing before caching fixed that.
- Keeping the model honest. We used an evidence-only prompt, JSON output, and a citation check that flags anything the data doesn't support.
- Reporting bias. Engaged neighborhoods file more 311 complaints, so the explanations state this caveat instead of treating complaints as ground truth.
Accomplishments that we're proud of
- A pipeline from raw messy text to ranked, explained neighborhoods, finished in one hack night.
- [N] complaints and [N] violation records across [N] ZIP codes, with [X]% of records extracted successfully.
- Explanations you can trace: [N of N] passed the citation check, so every cited document exists in the index.
- The quadrant view surfaced [ZIP / neighborhood] as a hidden problem, where inspectors find rodents but residents aren't reporting.
- Both halves are doing real work: Mistral for understanding text and reasoning, and Elastic for aggregation and ranking.
What we learned
- Structured outputs make messy city text usable, and caching on normalized text makes it cheap enough for a hackathon.
- ES|QL is a fast way to roll up several indices by a shared key.
- Disagreement between two data sources can be more useful than agreement.
- Starter code and data need checking before they can be trusted.
- Reasoning models give better explanations when they can only use evidence you hand them and you verify what they cite.
What's next for rodent-radar
- Semantic search over the evidence text with Mistral embeddings and
semantic_text(for example, "droppings near the dishwashing area"). - An Elastic Agent Builder agent so anyone can ask "why is this ZIP a hotspot?" in plain language.
- Finer resolution than ZIP using a geohex grid.
- Time trends and seasonality, plus normalizing by population.
- More sources, such as housing and building violations and sanitation data.
- Voice and multilingual answers with Voxtral, so the tool works for all of NYC's communities.
Log in or sign up for Devpost to join the conversation.