There's been a lot of political noise about European air quality: EU targets, diesel bans, clean air zones, industrial permits. But what does a decade of satellite and model data actually show?
We pulled 10 years of Copernicus CAMS reanalysis data for four major European cities — Paris, London, Berlin, and Warsaw — and let the numbers speak.
The dataset
All data comes from the CAMS European Reanalysis (CAMS-REG-REAN), a Copernicus service produced by ECMWF. It combines satellite observations, ground-station measurements, and atmospheric modelling into a gridded product at 0.1° resolution (~11 km) with hourly timesteps, spanning 2013 to present.
We queried annual mean NO₂ and PM2.5 for each city using the Jiskta API.
Instead of supplying manual bounding boxes, we pass area="paris" —
the API resolves the exact OSM administrative boundary automatically.
For the OLS trend (slope in µg/m³/year), a single aggregate="trend"
call returns the regression coefficients directly:
import requests
HEADERS = {"X-API-Key": "your_key"}
BASE = "https://api.jiskta.com/api/v1/climate/query"
# ── Annual means (for time-series charts) ──────────────────────────────────
# One call per city; area= uses the OSM boundary
for city in ["paris", "london", "berlin", "warsaw"]:
r = requests.get(BASE, headers=HEADERS, params={
"area": city,
"time_start": "2013-01", "time_end": "2023-12",
"variables": "no2",
"format": "csv", "aggregate": "area_monthly",
})
df = pd.read_csv(StringIO(r.json()["output"]))
print(city, df.groupby(df.year_month.str[:4])["no2_mean"].mean())
# ── OLS trend — single call, returns slope + R² directly ──────────────────
r = requests.get(BASE, headers=HEADERS, params={
"area": "paris",
"time_start": "2013-01", "time_end": "2023-12",
"variables": "no2",
"aggregate": "trend",
})
result = r.json()
# slope (µg/m³/year), r2 — area-averaged
slope = result["slope"]
r2 = result["r2"]
print(f"Paris NO₂ trend: {slope:+.3f} µg/m³/yr (R²={r2:.2f})") NO₂: the big picture
Nitrogen dioxide comes primarily from road transport and industrial combustion. It's the headline pollutant for urban air quality — and the one where the EU has consistently missed its own targets.
Annual mean NO₂ (µg/m³) at city centres, 2013–2023:
| City | 2013 | 2017 | 2019 | 2020 | 2021 | 2023 | Change |
|---|---|---|---|---|---|---|---|
| Paris | 31.6 | 31.2 | 27.3 | 22.7 | 26.4 | 22.8 | −28% |
| London | 27.4 | 27.8 | 26.9 | 20.7 | 21.6 | 22.9 | −17% |
| Berlin | 15.5 | 17.3 | 15.6 | 14.3 | 15.3 | 13.4 | −14% |
| Warsaw | 13.9 | 19.8 | 17.1 | 17.7 | 18.7 | 17.6 | +26% |
Western European cities show a clear downward trend. Paris fell 28% thanks to its ZFE (zone à faibles émissions) and the shift away from diesel-heavy vehicle fleets. Berlin improved 14% and London 17%, driven by the Ultra Low Emission Zone (launched 2019). Warsaw tells a different story: NO₂ actually increased 26% between 2013 and 2023, reflecting rapid traffic growth and slower vehicle fleet electrification in Poland compared to its Western European neighbours.
PM2.5: a different story
Fine particulate matter (PM2.5) tells a more complex story than NO₂. While NO₂ is predominantly traffic-sourced and responds quickly to vehicle fleet changes, PM2.5 has many sources: industry, agriculture (ammonia → secondary particles), residential heating, and long-range transport from Eastern Europe and North Africa.
| City | 2013 | 2019 | 2020 | 2023 | WHO limit | Status |
|---|---|---|---|---|---|---|
| Paris | ~15 | ~12 | ~10 | ~10 | 5 µg/m³ | 2× over limit |
| London | ~13 | ~11 | ~9 | ~9 | 5 µg/m³ | 1.7× over limit |
| Berlin | ~15 | ~13 | ~11 | ~10 | 5 µg/m³ | 2× over limit |
| Warsaw | ~22 | ~18 | ~16 | ~16 | 5 µg/m³ | 3.2× over limit |
PM2.5 values are approximate estimates based on EEA monitoring station data and CAMS model outputs. Direct PM2.5 queries via the Jiskta API are currently in beta.
Even with meaningful reductions over 10 years, every city remains well above the WHO's 2021 revised guideline of 5 µg/m³. Warsaw is the most challenging — coal-fired residential heating creates severe winter peaks that dominate the annual average.
Seasonal patterns: when is air quality worst?
Aggregating to monthly means reveals clear seasonal signals. To get these with one call:
r = requests.get(
"https://api.jiskta.com/api/v1/climate/query",
params={
"lat": 52.23, "lon": 21.01, # Warsaw
"time_start": "2013-01", "time_end": "2023-12",
"variables": "no2",
"format": "csv", "aggregate": "seasonal",
},
headers={"X-API-Key": "your_key"},
)
# lat,lon,season,no2_mean
# 52.2500,21.0500,DJF,23.2 ← winter inversion season
# 52.2500,21.0500,MAM,18.5
# 52.2500,21.0500,JJA,14.7
# 52.2500,21.0500,SON,19.7 Warsaw's winter NO₂ is 1.6× higher than summer — driven by atmospheric inversions that trap pollutants close to the ground during cold months, compounding the effect of coal-fired residential heating. In Paris and London, the seasonal NO₂ pattern is similar but less pronounced, with winter peaks driven primarily by lower atmospheric mixing height rather than heating emissions.
What this means for analysts
This kind of analysis used to require downloading hundreds of gigabytes of NetCDF files, installing specialised libraries, and spending days on data wrangling. The queries above took under a second and cost well under €1 in API credits.
If you're doing location risk assessment, ESG scoring, environmental impact modelling, or academic research — the same queries work for any location globally, any time range from 2013 onwards.
Run this analysis yourself
500 free credits included — enough to replicate everything in this post and more.
Get started free →