Is the air in my city getting more polluted — and how many people are breathing the worst of it?
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.
30.9, 29.7 → 31.6, 30.4 (Greater Cairo, Egypt)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
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____HiRswaths (~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
- Other pollutants that hurt health — NO₂ is one marker; PM2.5 (the deadliest) needs separate data (ground monitors, or aerosol optical depth as a rough proxy)
- 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, dimsLatitude/Longitude) on NASA GES DISC — open withxarray. The population and boundary layers are free and need no login.
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.
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, but PM2.5 (the biggest health burden) needs separate measurement.
- 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
- HAQ TROPOMI NO₂ L3 (GES DISC): https://disc.gsfc.nasa.gov/datasets/HAQ_TROPOMI_NO2_GLOBAL_M_L3_2/summary
- Sentinel-5P TROPOMI mission: https://sentinels.copernicus.eu/web/sentinel/missions/sentinel-5p
- NASA Health & Air Quality (HAQ): https://www.earthdata.nasa.gov/topics/human-dimensions/air-quality
- WorldPop population (free, no login): https://www.worldpop.org/
- geoBoundaries (free CC-BY admin boundaries): https://www.geoboundaries.org/
- OpenAQ ground monitors (for surface validation): https://openaq.org/
📚 Problem Finder KB
Not yet tracked in the KB.