Metrics & Analysis¶
Aeolus includes analysis functions for regulatory statistics, trend detection, and air quality index calculation.
See it in action
- Notebook 01 uses
time_average()andaq_stats()for roadside vs background analysis - Notebook 02 uses
aqi_summary(),aqi_check_who(), andaq_stats()for a compliance report - Notebook 08 uses
trend()andplot_trend()for multi-year Theil-Sen analysis with deseasonalisation
Time Averaging¶
Use time_average() to resample data to a coarser time resolution with data capture thresholds. Periods with insufficient valid observations are set to NaN:
from aeolus import metrics
# Daily averages (at least 75% data capture required)
daily = metrics.time_average(data, freq="D", data_thresh=0.75)
# Monthly averages
monthly = metrics.time_average(data, freq="ME")
# Daily maximum
daily_max = metrics.time_average(data, freq="D", statistic="max")
# 95th percentile, no threshold
p95 = metrics.time_average(data, freq="D", statistic="percentile", percentile=95, data_thresh=0)
# Only specific pollutants
no2_daily = metrics.time_average(data, freq="D", pollutants=["NO2"])
The output includes a data_capture column showing the fraction of valid observations in each period.
Annual Statistics¶
Use aq_stats() to calculate regulatory statistics suitable for LAQM Annual Status Reports:
# All years and pollutants
stats = metrics.aq_stats(data)
# Specific year
stats = metrics.aq_stats(data, year=2024)
# Specific pollutant
stats = metrics.aq_stats(data, pollutant="NO2")
Output columns:
| Column | Description |
|---|---|
annual_mean |
Annual mean concentration |
max_hourly |
Maximum hourly value |
max_daily_mean |
Maximum daily mean |
max_8h_rolling_mean |
Maximum 8-hour rolling mean |
p95, p99 |
95th and 99th percentiles |
data_capture |
Fraction of valid hours |
exceedance_hours_200 |
Hours > 200 µg/m³ (NO2) |
exceedance_days_50 |
Days with daily mean > 50 µg/m³ (PM10) |
exceedance_days_120 |
Days where max 8h rolling mean > 120 µg/m³ (O3) |
Trend Analysis¶
Use trend() for non-parametric trend detection using Theil-Sen slope and Mann-Kendall significance test:
# Monthly trend with deseasonalisation
result = metrics.trend(data, pollutant="NO2")
print(f"Slope: {result.slope:.2f} µg/m³/year")
print(f"Change: {result.slope_pct:.1f}%/year")
print(f"p-value: {result.p_value:.4f}")
print(f"95% CI: [{result.ci_lower:.2f}, {result.ci_upper:.2f}]")
# Seasonal aggregation
result = metrics.trend(data, pollutant="PM2.5", avg_time="season")
# Annual aggregation (no deseasonalisation)
result = metrics.trend(data, pollutant="O3", avg_time="year")
# Multi-site data returns a list
results = metrics.trend(multi_site_data, pollutant="NO2")
for r in results:
print(f"{r.site_code}: {r.slope:.2f} ({r.p_value:.3f})")
Visualise the result with plot_trend():
Supported AQI Systems¶
| Index | Country | Scale | Pollutants |
|---|---|---|---|
| UK_DAQI | UK | 1-10 | O3, NO2, SO2, PM2.5, PM10 |
| US_EPA | USA | 0-500 | O3, PM2.5, PM10, CO, SO2, NO2 |
| CHINA | China | 0-500 | PM2.5, PM10, SO2, NO2, O3, CO |
| EU_CAQI_ROADSIDE | EU | 1-6 | NO2, PM2.5, PM10 |
| EU_CAQI_BACKGROUND | EU | 1-6 | NO2, O3, PM2.5, PM10, SO2 |
| INDIA_NAQI | India | 0-500 | PM2.5, PM10, SO2, NO2, O3, CO, NH3, Pb |
| WHO | Global | - | Guidelines compliance checker |
AQI Summary¶
Calculate AQI statistics over a time period:
import aeolus
from aeolus import metrics
from datetime import datetime
# Download data
data = aeolus.download(
sources="AURN",
sites=["MY1"],
start_date=datetime(2024, 1, 1),
end_date=datetime(2024, 12, 31)
)
# Overall summary for the entire period
summary = metrics.aqi_summary(data, index="UK_DAQI")
# Monthly breakdown
monthly = metrics.aqi_summary(data, index="UK_DAQI", freq="M")
# Just the overall AQI (not per-pollutant)
simple = metrics.aqi_summary(data, index="UK_DAQI", overall_only=True)
Output Formats¶
Long format (default) - one row per site/period/pollutant:
site_code | period | pollutant | mean | aqi_value | aqi_category
MY1 | 2024-01 | NO2 | 45.2 | 3 | Low
MY1 | 2024-01 | PM2.5 | 12.1 | 2 | Low
Wide format - one row per site/period:
AQI Time Series¶
Get AQI values for each timestamp with appropriate rolling averages:
# Calculate hourly AQI with rolling averages
ts = metrics.aqi_timeseries(data, index="UK_DAQI")
# The function applies the correct rolling window for each pollutant
# (e.g., 8-hour for O3, 24-hour for PM2.5)
Plot the results:
import matplotlib.pyplot as plt
pivot = ts.pivot(index="date_time", columns="pollutant", values="aqi_value")
pivot.plot(figsize=(12, 4))
plt.ylabel("AQI Value")
plt.title("UK DAQI by Pollutant")
plt.show()
WHO Guidelines¶
Check compliance with WHO Air Quality Guidelines:
# Check against the strictest guideline (AQG)
compliance = metrics.aqi_check_who(data)
print(compliance[["pollutant", "mean_concentration", "guideline_value", "meets_guideline"]])
WHO provides interim targets for regions working towards the guidelines:
# Check against Interim Target 1 (least strict)
compliance_it1 = metrics.aqi_check_who(data, target="IT-1")
# Available targets: "AQG", "IT-1", "IT-2", "IT-3", "IT-4"
Listing Available Indices¶
# See all available indices
indices = metrics.list_indices()
print(indices)
# Get details about an index
info = metrics.get_index_info("UK_DAQI")
print(f"Name: {info['name']}")
print(f"Scale: {info['scale_min']}-{info['scale_max']}")
print(f"Pollutants: {info['pollutants']}")
Example: Annual AQI Report¶
import aeolus
from aeolus import metrics
from datetime import datetime
# Download a full year
data = aeolus.download(
sources="AURN",
sites=["MY1", "KC1"],
start_date=datetime(2024, 1, 1),
end_date=datetime(2024, 12, 31)
)
# Monthly AQI summary
monthly = metrics.aqi_summary(
data,
index="UK_DAQI",
freq="M",
overall_only=True
)
print(monthly)
# WHO compliance check
who_check = metrics.aqi_check_who(data)
print("\nWHO Guideline Compliance:")
print(who_check[["site_code", "pollutant", "meets_guideline", "exceedance_ratio"]])