Web Applications

Air Quality Watch

Built by Katrina Villamor

A small dashboard that pulls public air quality readings for my city and shows what the official page does not.

  • Builder email verified

    Verification

Project screenshot
Type
Personal
Category
Web Applications
Published
Jul 25, 2026
Updated
Jul 25, 2026
Built withPythonDuckDBStreamlitPlotlyGitHub Actions
TopicsClimateCivic Tech

About this project

What I built & why

My partner has asthma and we were constantly checking a government air quality page that shows the current reading and nothing else. Current is the least useful number — what you want to know is whether it's getting worse, what it did at this hour last week, and whether today is unusual. The data is published, so the gap was purely presentation. I built the first version in a weekend out of irritation and have kept it running for about two years, adding a station whenever one comes online.

The problem

It answers three questions the official page doesn't: is this bad by this station's own standards, what's the trend over the last six hours, and what does the same hour usually look like. That last one matters most — a reading that's alarming in absolute terms might be completely normal for a Tuesday morning near a highway. It's for me and my partner and a handful of people in the same neighbourhood who I sent the link to. It is not a health tool and I say so on the page.

What I owned

All of it, though most of the work is unglamorous. I wrote the scraper that pulls readings on a schedule, the storage, the backfill for gaps when the source is down, and the dashboard. The bit I put actual thought into is the baseline calculation — the same-hour-same-weekday distribution that everything is compared against — because a naive average made every rush hour look like an emergency. I also maintain it, which mostly means fixing the scraper when the source page changes, roughly twice a year.

Technical & product decisions

I decided to store every raw reading rather than only what I display, which has been the single best decision — the source only publishes the current value with no history at all, so my two years of collected data doesn't exist anywhere else that I know of. I chose DuckDB over a hosted database because the whole dataset is small enough to sit in a file, and running a database server for a personal dashboard would have been silly. I show percentile position rather than a colour-coded category, because the official categories are broad enough that the same colour covers a range that matters to someone with asthma.

Hardest challenge

The source is unreliable in an annoying way — it doesn't go down cleanly, it serves stale values with a fresh timestamp. My early data has stretches where the same reading repeats for hours and I recorded it as real. I now detect the repeats and mark them as suspect rather than deleting them, so the gaps are visible instead of being silently interpolated. Working out the threshold took a while, because some genuine readings do hold steady, and I settled on flagging exact repeats beyond a duration that varies by station based on how much that station normally moves.

Result & impact

It's a personal tool and I won't pretend otherwise — maybe thirty people use it. It changed one real behaviour: my partner now checks it before deciding whether to run outside, and we've shifted those runs to a different time of day based on the same-hour baselines. Two neighbours asked me to add their nearest station, which I did. The archive is the part I'd be sad to lose; I've had one request from someone doing a school project and I sent them the whole file.

Who else worked on it

My partner is effectively the product owner — she rejected the first two versions for showing too much and being unreadable on a phone before a run, and the current single-number-plus-trend layout is hers. A colleague who does air quality work professionally looked at my baseline method and warned me that comparing against a station's own history hides a station that has always been bad, which I now say explicitly on the page. Nobody has written code for it but me.