python-aqi - AQI conversion
A library to convert between AQI value and pollutant concentration (µg/m³ or ppm) using the following algorithms:
United States Environmental Protection Agency (EPA)
China Ministry of Environmental Protection (MEP)
The versions of each standard are kept, see the documentation for how an AQI is computed, the standards and versions, and the API reference.
Install
With pip:
$ pip install python-aqi
With uv, to add it to a project:
$ uv add python-aqi
or to install the aqi command line tool:
$ uv tool install python-aqi
Usage
Library
Convert a pollutant to its IAQI (Intermediate Air Quality Index):
import aqi myaqi = aqi.to_iaqi(aqi.POLLUTANT_PM25, '12', algo=aqi.ALGO_EPA)
Get an AQI out of several pollutant concentrations, default algorithm is EPA:
import aqi
myaqi = aqi.to_aqi([
(aqi.POLLUTANT_PM25, '12'),
(aqi.POLLUTANT_PM10, '24'),
(aqi.POLLUTANT_O3_8H, '0.087')
])
The algorithm is a standard and its version: 'epa:2012', or 'epa' for the latest version of the family (aqi.ALGO_EPA, aqi.ALGO_EPA_2012 and the matching MEP constants are available). Old versions stay selectable, the default is always the latest. aqi.available_algos() lists them all:
aqi.available_algos() # ['epa:2012', 'epa:2015', 'epa:2024', 'mep:2012', 'mep:2026']
The EPA versions are named after the revision that changed the table:
epa:2012: what python-aqi computed until 0.6, PM2.5 Good up to 12.0 µg/m³ and the 1997 ozone 8 hour breakpoints
epa:2015: new ozone 8 hour breakpoints (0.054, 0.070, 0.085, 0.105, 0.200 ppm), 1 hour ozone only above 0.125 ppm
epa:2024: new PM2.5 breakpoints (Good up to 9.0 µg/m³) and a single Hazardous category, 301 to 500, for every pollutant. This is the default.
The MEP (China) versions follow the standards. The name MEP stands for the Ministry of Environmental Protection, which published HJ 633-2012. The ministry was renamed Ministry of Ecology and Environment (MEE) in 2018 and publishes HJ 633-2026 under that name, the library keeps MEP for all versions so that the name used by existing code does not change:
mep:2012: HJ 633-2012, IAQI interpolated between the limits of the table and IAQI and AQI rounded up, concentrations used as given
mep:2026: HJ 633-2026, effective 1 March 2026. The limits of IAQI 100 change for PM10 (150 to 120 µg/m³) and PM2.5 (75 to 60 µg/m³), and the calculation follows the standard: IAQI interpolated between the limits of the table, IAQI and AQI rounded up, concentrations rounded to integers (CO to one decimal). This is the default for MEP.
mep:2012 used to be laid out like the EPA tables and now follows HJ 633-2012 (aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP_2012) is 82, it was 81). The default changed with the 2024 and 2026 versions: the same PM2.5 concentration can now give a different AQI, aqi.to_iaqi(aqi.POLLUTANT_PM25, '12.1') is 57, it was 51, and aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP) is 100, it was 81. Use algo=aqi.ALGO_EPA_2012 to keep the old EPA results.
1 hour ozone has no value under 0.125 ppm in the EPA table. to_aqi ignores it when other pollutants are given (the standard says to disregard it), for to_iaqi and to_cc it raises aqi.NotApplicableError, an OutOfRangeError.
Convert an IAQI to its pollutant concentration:
import aqi mycc = aqi.to_cc(aqi.POLLUTANT_PM25, '22', algo=aqi.ALGO_EPA)
NowCast
For the current air quality from hourly PM2.5 and PM10 readings (strings, oldest first, None for a missing hour), the EPA NowCast gives more weight to the recent hours when the air quality changes quickly. It works with any algorithm, hours= changes the window (12 in the standard) and min_weight= the lower limit of the weight factor ('0.5' in the standard):
pm25 = ['9.0', '12.1', '14.3', '20.5', '35.0', '41.2', None, '55.0']
aqi.to_nowcast(aqi.POLLUTANT_PM25, pm25) # the concentration
aqi.to_nowcast_aqi([(aqi.POLLUTANT_PM25, pm25)]) # the AQI
aqi.to_nowcast_aqi([(aqi.POLLUTANT_PM25, pm25)], hours=3, min_weight='0.1',
algo=aqi.ALGO_MEP)
It is defined for PM2.5 and PM10 only, and 2 of the last 3 hours must be valid (aqi.InsufficientDataError). See the documentation for the details.
Out of range values
A concentration (or an AQI) that is not covered by the algorithm raises aqi.OutOfRangeError, a subclass of ValueError that carries elem, value, low and high. Values are never capped: a capped value would hide how far above the scale a measure is, so what to do with it is up to the calling code:
try:
myaqi = aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')])
except aqi.OutOfRangeError as err:
print(err.elem, err.value, err.high)
The EPA calls AQI values above 500 “Beyond the AQI” and explains how to compute them: use the regular formula with the Hazardous breakpoints. This is available on request with extrapolate=True:
aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')], extrapolate=True) # 1445
This only applies to the EPA algorithm and to pollutants whose table covers the whole scale. 8 hour ozone, for example, stops at 300 because the EPA uses 1 hour ozone above that. The MEP standard has no such method, so extrapolate=True still raises with it. HJ 633-2026 does fix the IAQI of two pollutants above their table: SO2 1 hour above 800 µg/m³ is 200 and O3 8 hour above 800 µg/m³ is 300 (HJ 633-2012 said they are not computed: mep:2012 raises aqi.NotApplicableError and to_aqi leaves them out).
Command line
List supported algorithms and pollutants:
$ aqi -l epa:2012: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb) epa:2015: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb) epa:2024 (latest): o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb) mep:2012: so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³) mep:2026 (latest): so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
Pick the version of a standard with family:version, epa alone means the latest version, the module names (aqi.algos.epa) still work and also mean the latest:
$ aqi epa:2012 pm25:12 50
Convert PM2.5 to IAQI using EPA algorithm:
$ aqi aqi.algos.epa pm25:12 56
Convert PM2.5 to IAQI using EPA algorithm (full length):
$ aqi -c aqi aqi.algos.epa pm25:12 56
Convert pollutants concentrations to AQI using EPA algorithm:
$ aqi aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4 122
Convert pollutants concentrations to AQI using EPA algorithm, display IAQIs:
$ aqi -v aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4 pm25:114 o3_8h:122 co_8h:90 122
Convert PM2.5 IAQI to concentration using EPA algorithm:
$ aqi -c cc aqi.algos.epa pm25:39 7.0
Out of range values are an error, the message goes to stderr and the exit code is 3:
$ aqi aqi.algos.epa pm25:800 Out of range: pm25: 800.0 is not covered by the algorithm (valid range is 0.0 to 325.4)
-e computes EPA values beyond the scale (“Beyond the AQI”):
$ aqi -e aqi.algos.epa pm25:800 1445
Development
Set up the development environment (ruff, pre-commit, hatch, coverage):
$ uv sync $ uv run pre-commit install
Code is linted and formatted with ruff through pre-commit, the checks run on each commit. To run them on all files:
$ uv run pre-commit run --all-files
Test the package:
$ uv run python -m unittest discover
The same checks and the tests on Python 3.9 to 3.14 run on GitHub Actions (.github/workflows/ci.yml) for each push and pull request.
Release
A release is made from master with a clean tree, after adding a X.Y.Z (unreleased) section to docs/changelog.rst:
$ make release SEGMENT=major # or minor, patch (the default)
It runs, in this order:
- make bump
runs the tests, bumps the version with hatch version, refreshes uv.lock, dates the changelog section, commits release: bump to X.Y.Z and tags X.Y.Z (no v).
- make publish
builds the tagged commit and uploads it to PyPI with uv publish. The token is a PyPI API token in the UV_PUBLISH_TOKEN environment variable, it is not stored in the repository.
- make push
pushes the commit and the tag to origin (REMOTE=... to change it) once the upload worked.
Project
Resources
EPA AQI: Technical Assistance Document for the Reporting of Daily Air Quality – the Air Quality Index (AQI) December 2013) found at http://www.epa.gov/airnow/aqi-technical-assistance-document-dec2013.pdf
National Ambient Air Quality Standards for Particulate Matter found at http://www.gpo.gov/fdsys/pkg/FR-2013-01-15/pdf/2012-30946.pdf
MEP AQI:
GB3095—2012 (2012/02/29) found at http://www.mep.gov.cn/gkml/hbb/bwj/201203/t20120302_224147.htm
HJ633-2012 (2012/02/29) found at https://www.mee.gov.cn/ywgz/fgbz/bz/bzwb/jcffbz/201203/W020120410332725219541.pdf
License
python-aqi is published under a BSD 3-clause license, see the LICENSE file distributed with the project.
Metadata
Release files for python-aqi 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| python_aqi-1.1.0.tar.gz | 123.8 kB | Without provenance | Details for python_aqi-1.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| python_aqi-1.1.0-py3-none-any.whl | Python 3 | none | any | Details for python_aqi-1.1.0-py3-none-any.whl |
Total release size: 144.3 kB
Release files / python_aqi-1.1.0.tar.gz
| Download URL | python_aqi-1.1.0.tar.gz |
|---|---|
| Size | 123.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8ed3552cbd614aff02a5993ca73ce8d64b02f2c9285e6c22ef77533090485362
|
|
BLAKE2b-256 checksum How to use checksums |
49e14ef13c1da171173b1dd43d43217c24485b0b3af5ad10c8fb8f318d55588d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / python_aqi-1.1.0-py3-none-any.whl
| Download URL | python_aqi-1.1.0-py3-none-any.whl |
|---|---|
| Size | 20.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b4b327263e1590e363978c3b8ce23e2a0d14b4821a16e1a132fce0a76a41c9d7
|
|
BLAKE2b-256 checksum How to use checksums |
652c5cf3939027908eb021df19f6c15d857974a1e997ffea9b14f0c02be921b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for python-aqi 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.