OpenAQ quickstart — PM2.5 over a city¶
Fetch a month of ground-station PM2.5 for the Los Angeles basin through the EarthLens facade and plot a per-station time-series.
OpenAQ is the package's first tabular backend: download() returns a long-format pandas.DataFrame (one row per measurement), not a raster.
API key + rate limits. OpenAQ v3 needs a free
X-API-Key(register at https://explore.openaq.org/register). Set it as theOPENAQ_API_KEYenvironment variable. The free tier is rate-limited, so we cap the fan-out withmax_locationsand use the server-sidetemporal_resolution="daily"rollup instead of raw measurements.
Setup¶
Imports for the notebook: os to read the API key from the environment, matplotlib for the time-series plot, and the unified EarthLens entry point.
import os
import matplotlib.pyplot as plt
from earthlens import EarthLens
Request parameters¶
Define the area of interest (the Los Angeles basin) and the January 2024 date window up front, so the request below reads cleanly.
bbox_lat = [34.0, 34.3] # Los Angeles basin
bbox_lon = [-118.5, -118.1]
start, end = "2024-01-01", "2024-01-31"
Build the request¶
Construct the EarthLens facade for the openaq backend, requesting daily pm25 over the basin bounding box. Capping max_locations keeps the fan-out within the free-tier rate limits.
client = 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",
max_locations=10,
path="out/openaq",
)
Download the measurements¶
The live cell is skipped without an OPENAQ_API_KEY, so the notebook stays runnable under pytest --nbval-lax in CI. When a key is present, download() returns a long-format DataFrame, one row per measurement.
# The live cell is skipped without a key, so the notebook stays
# runnable under `pytest --nbval-lax` in CI.
df = None
if os.environ.get("OPENAQ_API_KEY"):
df = client.download(progress_bar=False)
print(df.shape)
else:
print("set OPENAQ_API_KEY to run the live cell")
Inspect the first rows¶
A quick peek at the returned frame confirms the long-format shape: station id, timestamp, and value columns.
if df is not None:
display(df.head())
Plot daily PM2.5 by station¶
Group the measurements by station_id and draw one line per station, so each monitor's daily PM2.5 trace is visible across the month.
if df is not None and not df.empty:
fig, ax = plt.subplots(figsize=(10, 4))
for station_id, group in df.groupby("station_id"):
ax.plot(
group["datetime_utc"], group["value"], marker=".", label=str(station_id)
)
ax.set_ylabel("PM2.5 (µg/m³)")
ax.set_xlabel("date (UTC)")
ax.set_title("Daily PM2.5 by station — Los Angeles")
ax.legend(title="station", fontsize=8)
fig.autofmt_xdate()
plt.show()