q26·intermediate

Is the air in my city getting more polluted — and how many people are breathing the worst of it?

atmospherepublic-healthurbanair-quality Datasets: 5 20–45 min
Find the data for your area

Draw a rectangle to pick your area of interest, then see what NASA data covers it (live, here in your browser) or download a ready-to-run notebook with your AOI pre-filled. The notebook runs in any Python environment — it needs a free Earthdata Login to fetch the data.

Current AOI: 30.9, 29.7 → 31.6, 30.4 (Greater Cairo, Egypt)
On this page

Nitrogen dioxide (NO₂) is the fingerprint of combustion — traffic, power plants, industry. The TROPOMI instrument on Sentinel-5P measures it from orbit, and NASA serves a tidy **monthly gridded** version. Pair it with free population data and you can answer two questions at once: *is my city's air getting worse over the years*, and *how many people live where it's worst*. This is a **multi-agency** question: the NO₂ is NASA/ESA, but the "who breathes it" answer comes from free non-NASA population and boundary layers joined on top.

Is the air in my city getting more polluted — and how many people are breathing the worst of it?

Nitrogen dioxide (NO₂) is the fingerprint of combustion — traffic, power plants, industry. The TROPOMI instrument on Sentinel-5P measures it from orbit, and NASA serves a tidy monthly gridded version. Pair it with free population data and you can answer two questions at once: is my city’s air getting worse over the years, and how many people live where it’s worst.

This is a multi-agency question: the NO₂ is NASA/ESA, but the “who breathes it” answer comes from free non-NASA population and boundary layers joined on top.

What you can answer

  • Is NO₂ trending up or down over my city — stack the monthly L3 grids across 2018→now and fit a trend (verified for Greater Cairo: +26% from Jan 2019 to Jan 2024)
  • Which parts of the metro are worst — map the NO₂ grid to find the high-NO₂ corridor (downtown, ring roads, industrial edge)
  • How many people live in the dirty-air zone — overlay free WorldPop population and count the people inside the high-NO₂ pixels (verified: ~22.9M in the default Cairo box, worst around district Qasr Al-Nile)
  • Did a lockdown / new highway / new plant change things — the monthly cadence resolves the 2020 COVID dip and step-changes after big infrastructure
  • How my city compares to its region — the global grid lets you rank neighboring cities on the same scale
  • The pollutant that actually harms health (PM2.5) — Copernicus CAMS adds modeled surface PM2.5 and aerosol, the fine particles NO₂ can’t see (free Copernicus ADS account)

What you can NOT answer with these datasets alone

  • Street-by-street “is my block polluted” — the L3 grid is ~10 km; it captures the city and its major corridors, not individual streets. For finer detail use the L2 S5P_L2__NO2____HiR swaths (~5.5 km) or ground monitors (OpenAQ)
  • The pollution you actually inhale — TROPOMI measures the column of NO₂ above the ground, not the concentration at breathing height; surface levels depend on mixing and weather
  • Breathing-height ground truth — the CAMS step below adds modeled surface PM2.5, but confirming what people actually inhale on a given street still needs ground monitors (OpenAQ); satellite and model fields are regional, not your block
  • Exactly who is exposed (census-grade) — WorldPop is modeled population; it gives sound exposure totals, not household income, age, or health status for equity targeting
  • Blame for the trend — a rising column doesn’t say whether traffic, industry, or a power plant drove it; pair with GHSL built-up + emissions inventories to attribute

Code template (Python, cloud-direct)

Verified locally. The HAQ TROPOMI product is monthly gridded netCDF (variable Tropospheric_NO2, dims Latitude/Longitude) on NASA GES DISC — open with xarray. The population and boundary layers are free and need no login. The optional CAMS PM2.5 step (step 3) needs a free Copernicus ADS account — it’s documented here, not run in this verification.

import earthaccess
import numpy as np
import xarray as xr

earthaccess.login(strategy="netrc")

# Greater Cairo, Egypt — a high-NO₂ megacity
aoi = (30.9, 29.7, 31.6, 30.4)              # (W, S, E, N)

def city_no2(year_month):
    """Mean tropospheric NO₂ over the AOI for one month (molec/cm²)."""
    r = earthaccess.search_data(short_name="HAQ_TROPOMI_NO2_GLOBAL_M_L3",
                                temporal=year_month, count=1)
    ds = xr.open_dataset(earthaccess.open(r[:1])[0])
    da, la, lo = ds["Tropospheric_NO2"], ds["Latitude"], ds["Longitude"]
    sub = (da.where((la >= aoi[1]) & (la <= aoi[3]), drop=True)
             .where((lo >= aoi[0]) & (lo <= aoi[2]), drop=True))
    return float(np.nanmean(sub.values))

# 1. Build a multi-year trend (one point per January)
trend = {y: city_no2((f"{y}-01-01", f"{y}-02-01")) for y in ["2019", "2021", "2024"]}
for y, v in trend.items():
    print(f"Cairo Jan {y}: {v:.2e} molec/cm^2")
change = (trend["2024"] - trend["2019"]) / trend["2019"] * 100
print(f"Change 2019->2024: {change:+.0f}%")     # verified ~ +26%

# 2. Who breathes it — free WorldPop population + geoBoundaries place names (no NASA login)
import requests, rasterio
from rasterio.windows import from_bounds
import geopandas as gpd
from shapely.geometry import Point

meta = requests.get("https://www.worldpop.org/rest/data/pop/wpic1km?iso3=EGY").json()
pop_url = next(f for f in meta["data"][-1]["files"] if f.endswith(".tif"))
open("egy_pop_1km.tif", "wb").write(requests.get(pop_url).content)

with rasterio.open("egy_pop_1km.tif") as src:
    pop = src.read(1, window=from_bounds(*aoi, transform=src.transform)).astype("float64")
    pop[pop == src.nodata] = np.nan
print(f"People in AOI: {np.nansum(pop):,.0f}")   # verified ~22.9M for this Cairo box

adm = gpd.read_file(requests.get(
    "https://www.geoboundaries.org/api/current/gbOpen/EGY/ADM2/").json()["gjDownloadURL"])
print("Central district:", adm[adm.contains(Point(31.24, 30.05))].iloc[0]["shapeName"])  # Qasr Al-Nile

# To count people in the dirty-air zone: resample the NO₂ grid onto the population grid
#   (or vice-versa), threshold the top NO₂ quantile, and sum `pop` inside it.

# 3. (free Copernicus ADS account) Add PM2.5 — the fine particles NO₂ can't see.
#    Register at ads.atmosphere.copernicus.eu, save your key to ~/.cdsapirc, then:
import cdsapi
c = cdsapi.Client()                                  # reads ~/.cdsapirc (ADS url + key)
c.retrieve("cams-global-reanalysis-eac4", {
    "variable": "particulate_matter_2.5um",
    "date": "2024-01-01/2024-01-31", "time": "12:00",
    "area": [aoi[3], aoi[0], aoi[1], aoi[2]],         # N, W, S, E
    "format": "netcdf",
}, "cairo_pm25.nc")
pm = xr.open_dataset("cairo_pm25.nc")["pm2p5"] * 1e9  # kg/m³ -> µg/m³
print(f"CAMS PM2.5 over AOI: {float(pm.mean()):.0f} µg/m³ (WHO 24h guideline: 15)")

Expected output

  • NO₂ trend line: monthly (or per-January) tropospheric NO₂ over the city, 2018→now, showing whether the air is getting dirtier — for Cairo, a clear rising trend (~+26% 2019→2024)
  • NO₂ map: the gridded column over the metro, highlighting the high-pollution corridor
  • Exposure estimate: people living in the high-NO₂ pixels (WorldPop), with the worst district named (geoBoundaries)
  • Comparison: the same metric for neighboring cities, ranked on one scale
  • Event markers (optional): the 2020 COVID dip or a post-infrastructure step-change

Caveats

  • Column, not surface — TROPOMI sees the NO₂ column from space; ground concentration (what you breathe) depends on boundary-layer mixing and weather. Treat the map as relative and trend information, not an absolute dose.
  • ~10 km grid — the monthly L3 resolves the city and major corridors, not streets; for finer work use L2 HiR swaths or OpenAQ ground monitors.
  • Cloud and season — TROPOMI needs clear sky; winter monthly means are more complete than monsoon months. Compare like months across years, not adjacent months.
  • NO₂ ≠ overall air quality — it tracks combustion; PM2.5 (the biggest health burden) comes from the CAMS step, and even that is a model — confirm with ground monitors (OpenAQ) where lives are at stake.
  • Modeled population — WorldPop is an estimate; exposure totals are order-of-magnitude sound but not a census.

Cross-agency composition

NASA GES DISC serves the TROPOMI NO₂ L3 (Sentinel-5P is an ESA mission; NASA hosts this gridded product). WorldPop (Univ. Southampton), GHSL (EU JRC), and geoBoundaries (William & Mary) are all free, non-NASA layers joined client-side — no extra login.

Sources

How a scientist answers this
Parameters
Tropospheric NO2 column density from TROPOMI (HAQ_TROPOMI_NO2_GLOBAL_M_L3, monthly gridded global L3, 2018→now) in molecules/cm², area-averaged over the city and deseasonalized; report the trend slope with confidence interval and significance. Exposure is WorldPop (1 km) population inside high-NO2 pixels; GHSL GHS-BUILT separates traffic/industry zones, geoBoundaries ADM2 names the worst districts, and Copernicus CAMS EAC4 PM2.5 covers the health pollutant NO2 misses.
Method
Stack the monthly L3 grids over the city box, remove the seasonal cycle, and fit an area-weighted (cos-lat) Theil–Sen slope with a Mann–Kendall significance test; map the climatological NO2 field to find the worst corridors and overlay WorldPop to count people in high-NO2 pixels. The monthly cadence resolves step changes (lockdowns, new infrastructure).
Validation
Cross-check the satellite trend and hotspot pattern against ground monitors (OpenAQ/local network) and the CAMS PM2.5 field; note that NO2 columns are sensitive to clouds and viewing geometry, are a combustion proxy not a direct surface-concentration or health metric, and that the ~3.5×5.5 km native footprint is coarse for fine intra-city detail.
In plain EnglishMeasure the city's NO2 from orbit every month for years, strip out the seasonal pattern, test whether the rest trends up or down, and count how many people live where it's worst.

Make it yours → Set the city bounding box, the year range, and the high-NO2 pixel threshold used to define the dirty-air zone, and add CAMS PM2.5 for the health-relevant pollutant.

Run the core method · no login

The detection / counting above a threshold at the heart of this question — runnable on synthetic data, right here. The full earthaccess code template further down does it on real NASA data (needs an Earthdata login).

editable · runs in your browser