Inspiration
A previous manager during my co-op at the Town of Caledon once told me, "People love dashboards". So I wanted to make something that combines that with my newfound interest of machine learning. Toronto City Council makes thousands of decisions a year, but the records of those decisions — a staff report request log and a council voting record — are built for internal tracking, not for residents. The challenge prompt asked how to help people understand a proposed decision, how it could affect them, and when they can have a say. I realized one thing: most people don't disengage from local government because they don't care — they disengage because figuring out where a decision stands and whether it's even worth showing up for takes more effort than anyone has time for. I wanted to build something that did that triage for them.
What it does
Council Tracker joins two of the City of Toronto's open datasets — the Report Request Log and the Council Voting Record — into one dashboard. It tracks every report request through its lifecycle (requested → delivered → passed/failed), and for votes that were actually contested, a model trained on historical voting patterns predicts how likely the next one is to pass. That probability becomes an "engagement priority" score: close votes rank highest, landslides in either direction rank lowest, because a close vote is exactly where a resident's voice is most likely to matter.
How we built it
A Python pipeline (pandas, scikit-learn) pulls both datasets live from Toronto's CKAN open data API, cleans their real-world schemas, and joins them on a shared agenda-item numbering scheme. A logistic regression model, trained on leakage-safe expanding-average features (no vote ever sees its own outcome or a future one), produces the probability behind the priority score. A Streamlit dashboard sits on top, deployed on Streamlit Community Cloud, with GitHub Actions running the test suite on every push.
Challenges we ran into
The "Status" field wasn't a clean Open/Closed pair — it was NCF and three different casings of "closed." The "Closed by" field held either a date or a follow-up item number, with nothing telling you which. We (pretty much just AI) validated our Result-parsing regex against all 252 distinct values actually in the file rather than guessing at a pattern. We (again, pretty much just AI) found and fixed an ordering bug: 65% of vote events (83% of City Council-level ones) had no timestamp at all, which was silently corrupting both the lifecycle outcomes and the model's time-based training split — fixed by switching to the raw file's row order. Deploying surfaced another issue: Streamlit Cloud's default Python version (3.14) had no prebuilt wheel for the pandas version we'd tested against, so a routine deploy turned into a 20-minute silent hang compiling pandas from source before failing.
Accomplishments that we're proud of
Nothing in this project is mocked — every number, every schema quirk, every bug fix in the documentation is backed by actually running the pipeline against the live data and verifying the result, including a real worked example traced end-to-end through every layer. The model doesn't just predict an outcome — it's evaluated on how well it separates genuine toss-ups from foregone conclusions (AUC ≈ 0.81), which is the thing that actually matters for the product. And the dashboard went from a developer tool full of raw column names and jargon to something a first-time visitor/resident can actually use, without losing any of the underlying rigour.
What we learned
Open data isn't static — a portal's schema can shift under you mid-project, so defensive, verify-before-you-trust engineering matters a lot. Messy real-world fields (inconsistent casing, overloaded columns, missing timestamps) aren't just cosmetic annoyances; they silently change what your analysis actually concludes if you don't catch them. And a technically correct dashboard is worthless to the people it's for if they can't understand what it's telling them — the plain-language rewrite mattered as much as the data science underneath it.
What's next for Council Tracker
Plain-language summaries of each report request; deputation deadlines and meeting dates pulled from agenda data, so residents know exactly when to act, not just that a decision is pending; a geography mapping layer so people can filter to what affects their specific part of the city; and training the model on the full five council terms of voting history available (currently uses just the current term) for a stronger signal.
Built With
- ai
- python