Utah PM2.5 forecasting models
Hourly PM2.5 (µg/m³) forecasts 24, 48, and 72 hours ahead for Utah's regulatory air quality monitors. These are the current champions of a weekly retraining job with a promotion gate. They're used in batch every few hours, and the forecasts are published to itstheryguy/aq-utah-forecasts and shown in the Utah PM2.5 Forecast Space. Code and design: github.com/itstheryguy/aq_utah_project.
| Folder | Model |
|---|---|
lightgbm/ |
LightGBM, one gradient-boosted model per horizon (Tweedie objective) |
gru/ |
GRU trained from scratch in PyTorch: the last 72 h as a sequence, plus forecast weather, three outputs |
linear/ |
Ridge regression per horizon on five inputs: current and 24 h mean PM2.5, forecast inversion, wind, precipitation |
Each folder is an MLflow pyfunc model (MLmodel, model files under artifacts/, model code under code/):
import mlflow
model = mlflow.pyfunc.load_model("lightgbm") # a local copy of the folder
forecasts = model.predict(gold_rows) # station_id, hour_utc, pred_t24, pred_t48, pred_t72
The input is rows of the project's gold_features table, one per station-hour: recent PM2.5 and NO₂, rolling
statistics, a regional mean, calendar features, and weather forecasts as issued 1–3 days ahead (Open-Meteo,
gfs_seamless), including a valley-minus-ridge temperature inversion proxy.
Evaluation
Promotion gate for the current versions: mean absolute error (µg/m³) on the 28 most recent days with labels, which neither the new version nor the previous champion was trained on.
| Model | Version | Trained through | 24 h | 48 h | 72 h | Persistence 24 h | 48 h | 72 h |
|---|---|---|---|---|---|---|---|---|
| lightgbm | v1 | 2026-10-02 | 1.83 | 1.80 | 1.90 | 2.44 | 2.93 | 3.19 |
| gru | v2 | 2026-10-06 | 1.85 | 1.95 | 2.14 | 2.52 | 2.96 | 3.23 |
| linear | v2 | 2026-10-06 | 2.08 | 2.28 | 2.37 | 2.52 | 2.96 | 3.23 |
Backtest (trained through Oct 2025, scored on Nov 2025 → Oct 2026), MAE in µg/m³, all stations:
| Model | 24 h | 48 h | 72 h | Winter 72 h | Summer 72 h |
|---|---|---|---|---|---|
| Persistence | 3.30 | 3.80 | 4.07 | 5.20 | 4.89 |
| LightGBM | 2.50 | 2.66 | 2.80 | 3.19 | 3.74 |
| GRU | 2.57 | 2.67 | 2.78 | 3.05 | 3.70 |
| Ridge, 5 inputs | 2.78 | 3.02 | 3.11 | 4.00 | 3.84 |
Training data
2024-01-22 onward: AirNow PM2.5 and NO₂ history via OpenAQ, with hours removed by EPA quality review (EPA AirData) excluded from the labels; Utah DAQ's live feed after that; Open-Meteo forecasts as issued (Previous Runs API). 26 stations, ~635k station-hours.
Limitations
- Spikes aren't predicted. On hours ≥ 35.5 µg/m³, every model (and persistence) is off by ~30–35, almost all underprediction. Fireworks and wildfire smoke from outside the region aren't visible in the features.
- Labels from the last few months are provisional: EPA's quality review lags by about half a year.
- Trained on Utah's monitors only. Not an official forecast.
Published 2026-11-09 13:53 UTC from Unity Catalog @champion aliases.