Forecasting Air Quality with Machine Learning

Suman SharmaSuman Sharma

Telling people the air is bad right now is useful. Telling them it'll be bad tomorrow morning lets them act: shift a run, keep kids indoors, wear a mask, or for governments, restrict traffic.

After building Vayu, forecasting was the obvious next step. Here's how I'd approach it today.

Frame the problem

Target: PM2.5 average for each hour, 1–24 hours ahead, at a given station.

Baseline to beat: persistence, meaning "tomorrow at 8am = today at 8am." It's surprisingly hard to beat on calm days. If your model doesn't beat it, you don't have a model.

Features that matter

Feature groupExamples
Lagged pollutionPM2.5 at t-1h, t-24h, rolling 6h mean
WeatherWind speed and direction, temperature, humidity, rainfall, boundary layer height
Weather forecastsThe same variables for the target hour, from a forecast model
TimeHour of day, day of week, holidays
Seasonal sourcesCrop burning season, heating season, festivals

Boundary layer height is the one people forget. When it's low (cold, calm nights), pollution is trapped near the ground.

Weather data sources include ERA5 reanalysis for training history and operational forecasts for live predictions. See Open Climate Datasets.

A baseline in Python

import pandas as pd from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.metrics import mean_absolute_error df = pd.read_parquet("station_hourly.parquet").sort_values("time") for lag in [1, 2, 3, 6, 12, 24]: df[f"pm25_lag_{lag}"] = df["pm25"].shift(lag) df["pm25_roll_6"] = df["pm25"].shift(1).rolling(6).mean() df["hour"] = df["time"].dt.hour df["dow"] = df["time"].dt.dayofweek # Target: PM2.5 24 hours ahead df["target"] = df["pm25"].shift(-24) df = df.dropna() features = [c for c in df.columns if c.startswith("pm25_")] + [ "hour", "dow", "wind_speed", "rh", "temp", "blh", ] split = df["time"] < "2026-01-01" model = HistGradientBoostingRegressor(max_iter=400, learning_rate=0.05) model.fit(df.loc[split, features], df.loc[split, "target"]) pred = model.predict(df.loc[~split, features]) print("MAE:", mean_absolute_error(df.loc[~split, "target"], pred)) persistence = df.loc[~split, "pm25"] # value now as forecast for +24h print("Persistence MAE:", mean_absolute_error(df.loc[~split, "target"], persistence))

Note: for a real deployment, the weather features for the target hour should come from a forecast, not observed values, or you're leaking the future.

Evaluate honestly

  • Split by time, never randomly.
  • Compare to persistence and climatology baselines.
  • Report error on high-pollution days separately. Those are the days that matter.
  • Check category accuracy (did you predict "Unhealthy" correctly?), not just MAE.

Deploying

  • Retrain on a schedule. Sources and patterns change.
  • Monitor drift: if errors grow, investigate.
  • Show uncertainty. Quantile regression gives you a range instead of one number.
  • Use calibrated sensor data if you rely on low-cost sensors.

Frequently asked questions

What features predict air quality best?

Recent pollution levels (lagged values), weather such as wind speed, boundary layer height, temperature, and humidity, time of day and day of week, and, where relevant, seasonal sources like crop burning or heating.

Should I use LSTMs or gradient boosting?

Start with gradient boosting on engineered features. It's fast, strong, and interpretable. Try deep learning only if you have lots of data and the baseline plateaus.

How far ahead can you forecast?

Short-term forecasts from hours to a day or two ahead are usually most reliable, because they rely on recent measurements and weather forecasts. Accuracy drops with longer horizons.

Contact

Get in Touch

Want to call me? book a call and I'll respond in the available time slot. I will ignore all soliciting.