Project story

Inspiration

When something breaks on your street in Baltimore, you call 311. Then you wait.

We started poking at the city's 311 data and found that the waiting is not the same everywhere. Report a dark streetlight in one neighborhood and there is a 7% chance it is still dark six months later. Report the same thing in Westport and it is 38%. The city gives itself four days for that repair. Nothing about the request changes. Only the address.

Then we noticed the quieter problem. A neighborhood that stops calling looks good on every city dashboard. Few complaints, no red flags, nothing to fix. But people do not stop calling because things got better. They stop because nobody came.

So a city that waits to be called will always give less to the places that gave up on it, and it will never find out it is doing that. We wanted to build the thing that finds out.

What it does

It scores all 55 Baltimore neighborhoods on two separate things.

Need is how bad things are: reports per 1,000 residents, empty buildings per 1,000 lots.

Service is how fast the city answers: how much of a problem is already closed a week after someone reports it.

The score we care about is the gap between them. Not "where is it bad" — every dashboard already shows that. Where is it bad and nobody is coming.

You get a map of Baltimore colored by that gap. Click a neighborhood and it zooms to the streets, where every dot is a real request or a real empty building at its real address. Next to the map, a card ranks the ten problems for that area, widest gap first, so you can see which truck to send. There is a list of the open requests most likely to be abandoned, with addresses, that downloads as a spreadsheet. And a newer panel scores each city agency against its own citywide pace, so you can see not just that your area is slow, but who is slow in it.

There are sliders for what matters to you. We do not decide that part.

How we built it

It is one Python file, built as a marimo notebook. That one file is also the website. marimo run serves it as an app with the code hidden; marimo edit opens the same thing as a notebook you can read and change. There is no second codebase.

Under it: polars for the data, shapely to put every request inside the right neighborhood, and a gradient-boosted survival model for the fix clock. All the dependencies are pinned inside the file itself, so uvx marimo run --sandbox script.py sets everything up and opens it on a machine that has never seen it.

We joined nine Open Baltimore datasets: 235,132 311 requests, 11,481 open vacancy notices, rehabs, demolitions, 234,952 property lots, and the neighborhood boundaries and population behind the denominators. Every request and every building is placed by its own coordinates, and we keep the street address, so the map can go from the whole city down to one block.

The map itself is ours. We wrote it from scratch in plain SVG and JavaScript, with no map library at all, which is also why the whole thing still works with the wifi off. The data ships with the repo as a snapshot, so nothing is fetched while it runs. It cannot fail in front of a judge.

Challenges we ran into

Our first score was wrong in a way that looked right. We counted demolitions as "service." But a neighborhood with more empty houses gets more demolitions just for having more empty houses, so the worst places came out looking well served. Need and service moved together at +0.94. We rewrote service as a share of the need it answers, and the same number went to −0.66: the more vacancy an area has, the smaller the share the city actually gets to.

We could not click the map. marimo turns off click selection on map charts. The whole idea depended on clicking a neighborhood, so we spent a block of the hackathon building our own map widget instead.

Then the map was too big to draw. Full-detail boundaries are 1.7 MB and the cell just showed a size error. Simplifying the shapes to about 10 m got it down to 65 KB.

"It runs" turned out not to mean "it works." The notebook passed every automated check we had. Then we opened it in a real browser and the map was not there. Later the sidebar links passed a small test and then reloaded the entire app once shareable ?area= links existed. Only clicking through the real thing caught either one.

Our first alarm list was nonsense. It flagged Downtown and Harbor East as the most neglected places in Baltimore, because crime per resident explodes where almost nobody lives but everybody visits. We switched that test to vacancy only.

We found a result and had to throw it out. Slow streetlight repairs looked linked to more night crime. So we ran the same test on daytime crime, which no streetlight can possibly affect. Daytime moved more. The finding was really just a map of what kind of neighborhood you are standing in. We report the null.

Accomplishments that we're proud of

The gap is not a subtraction. Subtracting service from need sounds right, but it ranks the city almost exactly like need alone (+0.88), so it just redraws a map everyone already has. Instead we fit the line the whole city sits on and measure how far below it each area falls. Being below that line means other places carry the same load and still get answered faster.

We don't pick the weights, and we can prove it doesn't matter. We tried 2,000 random ways of weighing the ten problems. Three neighborhoods stay in the top ten under every one of them. That is a recommendation nobody can argue away by moving a slider.

The work list. Take the 1,063 open requests that are 7 to 30 days old. They are all the same age, so sorting by age tells you nothing. Rank them by the chance they are still open three weeks from now, flag the worst tenth, and 82% of those flags are right, against 21% if you picked at random. Everything else we built describes Baltimore. This is the part a city could use on Monday.

We showed the cost of waiting. Dirty alleys the city left for a month drew 28% more rat reports nearby than the ones it cleared inside a week. And the extra reports sit right next to the alley and fade as you walk away, which is what a real effect looks like.

What we learned

An average only counts the jobs that got done. Ask how long a road repair takes and the answer is 31 days. That is the median of the ones that closed. 74% of road complaints are still open six months later, so the halfway point never actually arrives. The honest answer is that there is no median at all. Once you count the ones that never closed, the picture changes completely.

The biggest numbers are usually the emptiest. Our strongest correlation in the whole study was between dirty alleys and potholes. A dirty alley cannot make a pothole. That taught us to test every finding against something it could not possibly cause, before believing any of it.

Test in the real product, and then check that your test tested anything. Our first dark mode test was accidentally still in light mode.

The AI was fastest at the boring, checkable parts. It queried the live city APIs and found a pothole request type we had missed with 12,286 records in it, and found that about one in five requests in our topics were duplicates. But the worst flaw in our method was caught by a person reading it and saying "wait, that can't be right." So we started asking it to attack the method instead of just building it.

What's next for Bemore Baltimore

Measure how long vacancy waits, not just whether it got handled. The 311 side already uses a proper clock. Empty buildings still only ask whether something happened since 2023, not how many years the notice has been sitting there.

Score a block, not just a neighborhood. Our whole pitch opens with "one block, three tickets." Right now that is a story. It should be a number.

Find the tipping point instead of assuming one. Model whether a house gets a vacancy notice based on its neighbors last year, and let the data say where a block stops being savable.

Find out who owns the neglect. Property records list owners under dozens of name variants. Untangling them would show which private landlords hold the most abandoned property in the city.

And the one that matters most: check ourselves. Freeze today's ranking, come back in six months, and see whether the neglected neighborhoods actually got more service. A priority list nobody ever checks is just an opinion with a map attached.

Built With

Share this project:

Updates

Submission history