Air Pollution Meteorology Explained
On this page
Problem statement
A city's emissions change slowly: roughly the same traffic, the same boilers, the same week after week. Its air quality changes by a factor of five from one day to the next. The difference is the weather. Pollution is emitted into a volume of air whose depth and ventilation vary through the day and from day to night, and a bad air day is usually a day on which that volume was small and still.
Anyone comparing air quality between places, years or policies has to separate the weather's contribution from the emissions', or they will credit a windy year to a clean-air zone.
Quick answer
Two meteorological quantities do most of the work:
- Mixing-layer (boundary-layer) height β the depth of air near the ground that is stirred by turbulence. Emissions mix into it; a shallow layer means high concentrations.
- Wind speed β how fast fresh air replaces polluted air. Concentrations from local sources scale roughly with 1/speed.
import io, requests, numpy as np, pandas as pd
def aurn_no2(site):
text = requests.get(f"https://uk-air.defra.gov.uk/datastore/data_files/site_data/"
f"{site}_2023.csv", timeout=120).text
d = pd.read_csv(io.StringIO(text), skiprows=4, low_memory=False)
end = pd.to_datetime(d.Date, dayfirst=True) + pd.to_timedelta(d.time.str[:2].astype(int), "h")
return pd.Series(pd.to_numeric(d["Nitrogen dioxide"], errors="coerce").to_numpy(),
index=end.dt.tz_localize("UTC"), name="no2")
j = requests.get("https://archive-api.open-meteo.com/v1/archive", params={
"latitude": 51.521050, "longitude": -0.213419,
"start_date": "2023-01-01", "end_date": "2023-12-31",
"hourly": "boundary_layer_height,wind_speed_10m,temperature_2m",
"wind_speed_unit": "ms", "timezone": "UTC"}, timeout=120).json()
met = pd.DataFrame(j["hourly"])
met.index = pd.to_datetime(met.pop("time")).dt.tz_localize("UTC")
df = met.join(aurn_no2("KC1"), how="inner").dropna()
df["blh_band"] = pd.qcut(df.boundary_layer_height, 5,
labels=["lowest fifth", "2nd", "3rd", "4th", "highest fifth"])
print("North Kensington NO2 by mixing-layer height, 2023")
print(df.groupby("blh_band", observed=True).agg(
blh_m=("boundary_layer_height", "median"), no2=("no2", "mean")).round(0).to_string())
North Kensington NO2 by mixing-layer height, 2023
blh_m no2
blh_band
lowest fifth 120.0 34.0
2nd 350.0 20.0
3rd 600.0 16.0
4th 870.0 12.0
highest fifth 1265.0 9.0
The same station, the same emissions: the hours with the shallowest fifth of mixing layers average 34 Β΅g/mΒ³ of NO2, the deepest fifth 9 β nearly four times less.
The mixing layer's daily and seasonal cycles, and stagnation
1. Know the daily cycle of the mixing layer
At night the ground cools, the air near it becomes stable, and the mixing layer collapses to a few hundred metres or less. After sunrise it grows, peaking in the early afternoon at one to two kilometres in summer. Rush-hour emissions in the morning go into a shallow layer; afternoon emissions into a deep one.
2. Know the seasonal cycle
Winter days are short and the sun is weak: the mixing layer stays shallow for longer, and inversions can persist all day. The same emissions give higher concentrations in winter.
3. Recognise stagnation episodes
High pressure with light winds and clear nights β anticyclonic conditions β traps pollution day after day. Most short-term exceedances at urban background sites happen in such spells.
4. Account for meteorology before comparing periods
A year-on-year change of 10% can be entirely weather. Comparing like-for-like meteorology β or using a model that normalises for it β is required before attributing a change to a policy.
5. Use reanalysis for the variables stations do not measure
Mixing-layer height is rarely measured routinely. ERA5 provides it hourly everywhere, along with wind and temperature, which is what makes these analyses possible for any station.
Wind, mixing height and the worst days
Example 1 β wind speed and dilution
df["wind_band"] = pd.cut(df.wind_speed_10m, [0, 1, 2, 3, 4, 6, 20],
labels=["<1", "1β2", "2β3", "3β4", "4β6", ">6 m/s"])
w = df.groupby("wind_band", observed=True).agg(hours=("no2", "size"), no2=("no2", "mean"))
print(w.round(1).to_string())
hours no2
wind_band
<1 203 43.4
1β2 993 27.4
2β3 1680 25.4
3β4 1551 18.7
4β6 2460 13.2
>6 m/s 1802 9.5
Concentrations fall steadily as the wind strengthens. Calm hours are rare and disproportionately polluted β which is why an annual mean is dominated by how many still, shallow-layer hours a year happened to have.
Example 2 β the day's cycle, NO2 against the mixing layer
local = df.tz_convert("Europe/London")
winter = local[local.index.month.isin([12, 1, 2])]
summer = local[local.index.month.isin([6, 7, 8])]
for name, part in (("winter", winter), ("summer", summer)):
h = part.groupby(part.index.hour)[["boundary_layer_height", "no2"]].mean()
print(f"{name}: mixing layer {h.boundary_layer_height.min():.0f}β"
f"{h.boundary_layer_height.max():.0f} m, NO2 peak {h.no2.max():.1f} at "
f"{h.no2.idxmax():02d}:00, minimum {h.no2.min():.1f} at {h.no2.idxmin():02d}:00")
winter: mixing layer 550β844 m, NO2 peak 32.3 at 20:00, minimum 17.5 at 05:00
summer: mixing layer 361β1287 m, NO2 peak 15.6 at 00:00, minimum 7.6 at 16:00
The summer afternoon layer averages 1,290 m against 840 m at its winter peak, and summer's NO2 minimum falls at 16:00, when the layer is deepest β the hours the satellite overpasses see. In winter the layer never grows much and NO2 peaks in the evening, when traffic meets a collapsing layer.
Example 3 β what the worst days look like
daily = df.resample("D").agg({"no2": "mean", "boundary_layer_height": "mean",
"wind_speed_10m": "mean"}).dropna()
worst = daily.nlargest(15, "no2")
print(f"15 worst days: NO2 {worst.no2.mean():.0f} Β΅g/mΒ³, mixing layer "
f"{worst.boundary_layer_height.mean():.0f} m, wind {worst.wind_speed_10m.mean():.1f} m/s")
print(f"all days: NO2 {daily.no2.mean():.0f} Β΅g/mΒ³, mixing layer "
f"{daily.boundary_layer_height.mean():.0f} m, wind {daily.wind_speed_10m.mean():.1f} m/s")
print("months of the worst days:", worst.index.month.value_counts().sort_index().to_dict())
15 worst days: NO2 55 Β΅g/mΒ³, mixing layer 165 m, wind 2.1 m/s
all days: NO2 18 Β΅g/mΒ³, mixing layer 648 m, wind 4.3 m/s
months of the worst days: {1: 6, 2: 6, 11: 2, 12: 1}
The worst days are the still ones with a shallow mixing layer, clustered in the cold months. Emissions on those days were not unusual; the air had nowhere to take them.
Why the mixing layer acts like a lid
Why the mixing layer acts like a lid
Above the mixing layer the air is stably stratified: a parcel pushed upward is colder than its surroundings and sinks back. Pollution emitted at the ground spreads upward only as far as turbulence carries it, which is the top of the mixing layer. Halve the layer's depth and, for the same emissions and wind, the concentration roughly doubles.
Why wind and mixing height come together
Both are driven by the same weather systems. Deep low-pressure systems bring wind and deep mixing; anticyclones bring calm and shallow layers. Their effects compound, which is why stagnation episodes are so much worse than either factor alone suggests.
Why weather normalisation matters for policy
Emission changes from a policy are typically a few to tens of percent; weather-driven variation between years is of the same order. Studies of low-emission zones therefore use weather-normalisation methods β regression or machine-learning models of concentration on meteorology β before claiming an effect.
Ozone, secondary particles and street canyons
- Ozone behaves differently: it forms in sunshine, so hot, sunny, stagnant days are its worst.
- Secondary PM2.5 forms over days from regional emissions; its episodes follow air-mass origin more than local mixing.
- Street canyons have their own ventilation, partly decoupled from the city-scale mixing layer.
- Reanalysis mixing-layer height is a model diagnostic; it is smoothed and can miss shallow nocturnal layers.
- Sea breezes and valleys create local circulations that recirculate pollution.
Internal links
- How to analyse an air quality time series β the cycles this guide explains
- How to make a pollution rose β direction as well as speed
- Satellite NO2 explained β why the afternoon overpass sees less
- Wind resource explained β the same boundary layer, for energy
- How to compare CAMS with monitoring stations β a model that includes the weather
- Air pollutants and their units explained β what is being diluted
FAQ
Why is air pollution worse on still days?
Low wind removes the ventilation, and still days often have a shallow mixing layer; emissions accumulate.
What is the boundary layer?
The layer of air near the ground that is mixed by turbulence β from tens of metres at night to one or two kilometres on summer afternoons.
Why is pollution higher in winter?
Shallower mixing, more frequent inversions, weaker sunlight, and extra heating emissions.
Can I get mixing-layer height for my station?
From reanalysis: ERA5 provides boundary_layer_height hourly, available through Open-Meteo.
Does weather explain year-to-year changes?
Often a large part of them. Normalise for meteorology before attributing a change to emissions.
What is a temperature inversion?
A layer where temperature increases with height, suppressing vertical mixing and trapping pollution beneath it.