PM2.5 city time-series vs. the WHO guideline¶
Fetch a year of daily-rollup PM2.5 for one city, plot the series, overlay the WHO 24-hour guideline (15 µg/m³), and annotate the exceedance days. This shows the value of OpenAQ's server-side daily rollup for long windows — a year is ~365 rows per sensor, not tens of thousands of raw readings.
Needs a free
OPENAQ_API_KEY(see the authentication page). The live cell is guarded so the notebook stays--nbval-lax-safe.
Setup¶
os lets us read the API key from the environment, and earthlens provides the
unified EarthLens entry point. The matplotlib import is deferred to the plotting cell
so the notebook imports cleanly even without a display backend.
import os
from earthlens import EarthLens
The request¶
Define the analysis parameters up front: the WHO 24-hour guideline used for the overlay, a bounding box over the Los Angeles basin, and the one-year date window.
WHO_24H_GUIDELINE = 15.0 # µg/m³, WHO 2021 24-hour PM2.5 guideline
bbox_lat = [34.0, 34.3] # Los Angeles basin
bbox_lon = [-118.5, -118.1]
start, end = "2023-01-01", "2023-12-31"
Fetch the daily PM2.5 rollup¶
OpenAQ is a tabular backend, so download() returns a DataFrame of daily rows. We
build the request first, then call download() on its own line. The whole block is
guarded on OPENAQ_API_KEY so the notebook stays runnable (and --nbval-lax-safe)
without credentials.
df = None
if os.environ.get("OPENAQ_API_KEY"):
measurements = EarthLens(
data_source="openaq",
variables=["pm25"],
start=start,
end=end,
aoi=[bbox_lon[0], bbox_lat[0], bbox_lon[1], bbox_lat[1]],
temporal_resolution="daily", # server-side daily rollup
max_locations=5,
path="out/openaq",
)
df = measurements.download(progress_bar=False)
print(df.shape)
else:
print("set OPENAQ_API_KEY to run the live cell")
Collapse to a city-wide daily mean¶
The request can return several stations across the basin. Averaging their values per day yields one city-wide series, and we count how many of those days exceed the WHO guideline.
city = None
if df is not None and not df.empty:
city = (
df.assign(day=df["datetime_utc"].dt.floor("D"))
.groupby("day")["value"]
.mean()
.sort_index()
)
exceedances = city[city > WHO_24H_GUIDELINE]
print(f"{len(exceedances)} of {len(city)} days exceed the WHO guideline")
Plot the series against the guideline¶
Plot the daily mean as a line, draw the WHO guideline as a dashed reference, and mark every exceedance day in crimson so the polluted stretches stand out at a glance.
if city is not None and not city.empty:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(city.index, city.values, color="#1f77b4", lw=1, label="daily mean PM2.5")
ax.axhline(
WHO_24H_GUIDELINE, color="crimson", ls="--", label="WHO 24h guideline (15)"
)
ax.scatter(
exceedances.index,
exceedances.values,
color="crimson",
s=12,
zorder=3,
label="exceedance day",
)
ax.set_ylabel("PM2.5 (µg/m³)")
ax.set_title("Los Angeles daily PM2.5 vs. WHO guideline, 2023")
ax.legend(fontsize=8)
fig.autofmt_xdate()
plt.show()