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.

Mean NO2 at North Kensington by fifth of ERA5 mixing-layer height in 2023. NO2 by mixing-layer height, Β΅g/mΒ³lowest fifth Β· median 120 m34second Β· 350 m20third Β· 600 m16fourth Β· 870 m12highest fifth Β· 1,265 m902040Β΅g/mΒ³Same emissions, a volume of air nearly four times less effective spatialworkflow.io
Same emissions, different volumes of air to dilute them in.

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.

The mixing layer at night and in the afternoon, with the same emissions confined to a shallow layer at night. the same emissions, two lidsnight, winterβœ•ground cools, air stratifiesβœ•layer ~100–200 mβœ•emissions accumulateβœ•NO2 peaks in the eveningafternoon, summerβœ“sun heats the groundβœ“layer ~1,300 mβœ“emissions dilutedβœ“NO2 minimum at 16:00Halve the layer depth and, for the same wind, concentrations roughly double spatialworkflow.io
The same emissions in a night-time lid and an afternoon mixing layer.

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.

The 15 worst NO2 days at North Kensington in 2023 compared with all days. what the worst days look like15 worst daysall daysNO255 Β΅g/mΒ³18 Β΅g/mΒ³mixing layer165 m648 mwind at 10 m2.1 m/s4.3 m/smonthsJan 6 Β· Feb 6 Β· Nov 2 Β· Dec 1all yearEmissions were ordinary on those days; the air had nowhere to take them spatialworkflow.io
Stagnation, not extra emissions, makes the worst days.

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.

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.