Skip to main content

Openair-inspired Python tools for air quality analysis, visualisation, and mapping

Project description

airqoair

airqoair is a Python library for air-quality analysis, plotting, reporting, and interactive maps.

It is designed for environmental monitoring workflows where you want:

  • direct input from pandas.DataFrame, .csv, .json, .jsonl, or .ndjson
  • temporal diagnostics such as diurnal, weekday, monthly, and grouped site comparisons
  • directional analysis such as wind roses, pollution roses, and polar plots
  • interactive map outputs with folium
  • no machine learning or forecasting in the package itself

Install

From PyPI:

pip install airqoair

Documentation:

https://airqoair-project.github.io/book

From this monorepo for development:

cd packages/airqoair
pip install -e ".[dev]"

Quick Start

import airqoair as aq

daily = aq.time_average(
    "kampala.csv",
    avg="D",
    pollutant_cols=["pm2_5", "pm10"],
)

variation = aq.time_variation("kampala.csv", pollutant="pm2_5")
monthly = aq.monthly_plot("kampala.csv", pollutant="pm2_5")
rose = aq.wind_rose("kampala.csv")
site_map = aq.station_map("kampala.csv", value="pm2_5")

print(daily.head())

variation.save("outputs/time_variation.png")
monthly.save("outputs/monthly_profile.png")
rose.save("outputs/wind_rose.png")
site_map.save("outputs/stations.html")

Grouped Conditioning

One of the most useful analysis patterns is conditioning the same workflow by site, year, season, or other categories.

airqoair supports this in the temporal workflow through:

  • group_by="site_name" for an existing column in your data
  • type="year", type="monthyear", type="season", type="weekday", or type="weekend" for derived date-based conditioning

Monthly bars by site:

import airqoair as aq

monthly_by_site = aq.monthly_plot(
    "kampala.csv",
    pollutant="pm2_5",
    group_by="site_name",
)

monthly_by_site.save("outputs/monthly_by_site.png")

Diurnal profile by season:

import airqoair as aq

diurnal_by_season = aq.diurnal_plot(
    "kampala.csv",
    pollutant="pm2_5",
    type="season",
)

diurnal_by_season.save("outputs/diurnal_by_season.png")

Monthly profile table by site:

import airqoair as aq

monthly = aq.monthly_profile(
    "kampala.csv",
    pollutant="pm2_5",
    group_by="site_name",
)

print(monthly.head())

Accepted Inputs

Most functions accept either a DataFrame or a file path.

Supported file formats:

  • .csv
  • .json
  • .jsonl
  • .ndjson

Examples:

import airqoair as aq

df = aq.load_data("kampala.jsonl")
daily = aq.time_average(df, avg="D", pollutant_cols=["pm2_5"])
import airqoair as aq

variation = aq.time_variation("kampala.json", pollutant="pm2_5")

Expected Columns

Defaults assume these names:

Purpose Default column(s)
datetime date
pollutant examples pm2_5, pm10, no2, o3
wind speed ws
wind direction wd
latitude latitude
longitude longitude

You can override them in function calls, for example:

import airqoair as aq

aq.wind_rose("kampala.csv", ws="wind_speed", wd="wind_direction")
aq.station_map("kampala.csv", latitude="lat", longitude="lon", value="pm2_5")
aq.time_average("kampala.csv", date_col="timestamp", pollutant_cols=["pm2_5"])

Feature Coverage

Directional Analysis

  • wind_rose()
  • pollution_rose()
  • percentile_rose()
  • polar_frequency()
  • polar_plot()
  • polar_annulus()

Time Series, Profiles, and Reports

  • time_average()
  • time_variation()
  • diurnal_profile()
  • weekday_profile()
  • monthly_profile()
  • diurnal_plot()
  • weekday_plot()
  • monthly_plot()
  • variation_report()

Visualization and Trends

  • time_series_plot()
  • distribution_plot()
  • calendar_plot()
  • time_proportion_plot()
  • trend_level()
  • run_regression()
  • theil_sen_trend()
  • smooth_trend()
  • dilution_plot()
  • pollutant_ratio_plot()
  • trend_heatmap()

Model Evaluation

  • model_evaluation()
  • taylor_diagram()
  • conditional_quantiles()

Interactive Maps

  • station_map()
  • map_primer()
  • network_visualization()
  • directional_map()
  • polar_map()
  • trajectory_analysis()

Common Workflows

Diurnal, Weekday, and Monthly Summaries

import airqoair as aq

diurnal = aq.diurnal_profile("kampala.csv", pollutant="pm2_5")
weekday = aq.weekday_profile("kampala.csv", pollutant="pm2_5")
monthly = aq.monthly_profile("kampala.csv", pollutant="pm2_5")

Grouped Site Comparison

import airqoair as aq

variation = aq.time_variation(
    "kampala.csv",
    pollutant="pm2_5",
    group_by="site_name",
)

report = aq.variation_report(
    "kampala.csv",
    pollutant="pm2_5",
    group_by="site_name",
)

print(report.sections["summary"].head())
variation.save("outputs/site_time_variation.png")

Directional Analysis

import airqoair as aq

aq.wind_rose("kampala.csv").save("outputs/wind_rose.png")
aq.percentile_rose("kampala.csv", pollutant="pm2_5").save("outputs/percentile_rose.png")
aq.polar_plot("kampala.csv", pollutant="pm2_5").save("outputs/polar_plot.png")

Time Trends

import airqoair as aq

aq.calendar_plot("kampala.csv", pollutant="pm2_5").save("outputs/calendar.png")
aq.smooth_trend("kampala.csv", pollutant="pm2_5", deseason=True, n_boot=200).save("outputs/smooth_trend.png")
aq.trend_heatmap("kampala.csv", pollutant="pm2_5").save("outputs/trend_heatmap.png")

Calendar plot with daily value labels and discrete bands:

import airqoair as aq

aq.calendar_plot(
    "kampala.csv",
    pollutant="pm2_5",
    year=2025,
    annotate="value",
    breaks=[0, 15, 35, 55, 150],
    labels=["Good", "Moderate", "Unhealthy", "Very Unhealthy"],
    lim=35,
).save("outputs/calendar_2025.png")

Time proportion plot split by site or wind sector over time:

import airqoair as aq

aq.time_proportion_plot(
    "kampala.csv",
    pollutant="pm2_5",
    proportion="site_name",
    avg_time="7D",
).save("outputs/time_proportion_by_site.png")

aq.time_proportion_plot(
    "kampala.csv",
    pollutant="pm2_5",
    proportion="wd",
    avg_time="1M",
    sector_count=8,
).save("outputs/time_proportion_by_wind_sector.png")

Trend-level heat map across month and hour:

import airqoair as aq

aq.trend_level(
    "kampala.csv",
    pollutant="pm2_5",
    x="month",
    y="hour",
).save("outputs/trend_level_month_hour.png")

Rolling regression diagnostics:

import airqoair as aq

aq.run_regression(
    "kampala.csv",
    x="ws",
    y="pm2_5",
    avg_time="MS",
    window=12,
).save("outputs/run_regression.png")

Theil-Sen trend with deseasonalising and confidence intervals:

import airqoair as aq

aq.theil_sen_trend(
    "kampala.csv",
    pollutant="pm2_5",
    deseason=True,
    n_boot=200,
).save("outputs/theil_sen_trend.png")

Smooth trend with deseasonalising and grouped site panels:

import airqoair as aq

aq.smooth_trend(
    "kampala.csv",
    pollutant="pm2_5",
    group_by="site_name",
    deseason=True,
    n_boot=200,
).save("outputs/smooth_trend_by_site.png")

Maps

import airqoair as aq

aq.station_map("kampala.csv", value="pm2_5").save("outputs/stations.html")
aq.directional_map("kampala.csv").save("outputs/directional_map.html")
aq.polar_map("kampala.csv", site_col="site_name", pollutant="pm2_5", kind="polar_plot").save("outputs/polar_map.html")

Grouped interactive map with clustering, marker scaling, and a heat layer:

import airqoair as aq

aq.station_map(
    "kampala.csv",
    value="pm2_5",
    group_by="site_name",
    radius_col="device_count",
    cluster=True,
    heatmap=True,
    tiles=["CartoDB positron", "OpenStreetMap"],
).save("outputs/stations_grouped.html")

Returned Objects

Plotting functions return AirQoFigure.

  • .data contains the processed table behind the plot
  • .figure is the underlying Matplotlib figure
  • .ax returns the first Matplotlib axis for single-axis convenience
  • .axes returns all Matplotlib axes on the figure
  • .show() displays the plot
  • .save(path) writes the plot to disk

Map functions return AirQoMap.

  • .data contains the mapped rows
  • .map is the underlying folium.Map
  • .save(path) writes the HTML map

variation_report() returns AirQoReport.

  • .sections contains summary and profile tables
  • .save_markdown(path) writes a markdown report
  • .save_figure(path) writes the report figure

Current Scope

airqoair is designed for practical air-quality diagnostics in Python, but it is not yet a full feature-for-feature replacement for every established toolkit in this space.

This package currently focuses on:

  • practical temporal profiles and grouped conditioning
  • directional analysis and mapping
  • trend and evaluation utilities that are straightforward to use from Python data workflows

Development

Run tests from the package root:

cd packages/airqoair
python -m pytest tests

If you are working from the repo without installation, point PYTHONPATH at packages/airqoair, not the repository root.

Documentation site

A book-style documentation site scaffold is included under:

packages/airqoair/docs/
packages/airqoair/mkdocs.yml

To build it locally:

cd packages/airqoair
pip install -e ".[docs]"
python -m mkdocs serve

Images for the docs can be stored in:

packages/airqoair/docs/assets/images/

.gitignore

The package-local .gitignore excludes:

  • Python caches and bytecode
  • virtual environments
  • pytest, coverage, mypy, ruff, tox, and nox caches
  • build artifacts and editable-install metadata
  • generated plots, HTML maps, markdown reports, and temporary outputs

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

airqoair-0.1.1.tar.gz (40.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

airqoair-0.1.1-py3-none-any.whl (37.4 kB view details)

Uploaded Python 3

File details

Details for the file airqoair-0.1.1.tar.gz.

File metadata

  • Download URL: airqoair-0.1.1.tar.gz
  • Upload date:
  • Size: 40.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for airqoair-0.1.1.tar.gz
Algorithm Hash digest
SHA256 6e8d2b4aea0cee4c484f4e24871fb4d0968de0bd9f453bb045d24e3aa77758d1
MD5 b64324f74c64f443b2b25d617fb7d5fc
BLAKE2b-256 57d23fd2b97f66fb3a64593350c0153588d420779b079b61d223c13ee8096674

See more details on using hashes here.

File details

Details for the file airqoair-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: airqoair-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 37.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for airqoair-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 677672e6bd4a424ddea9ed53606a3223d8df18a61506d33615343c6a11ddfd45
MD5 c1b28e7915db560ff8e0ff22889a8f53
BLAKE2b-256 a1cb48165495588a04dbc48b3d4c837ebbb64bcf7dd95799cf33255891378ecd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page