How to Map Toxic Air Releases per Resident by County

On this page

Problem statement

Most of the toxic releases in EPA's inventory never leave the site: mine waste on land, injection wells, landfills. What reaches people most directly is air. A county map of air releases per resident shows where industrial emissions are large relative to the people living nearby, and it looks very different from a map of total releases. This page sums stack and fugitive air releases for 2024, places each facility in a county by its coordinates, divides by the 2025 population and maps the result.

Quick answer

Add stack and fugitive air, total by facility, place the facilities in counties with a spatial join, and divide by residents:

t24["air"] = pounds(t24, "5.1 - FUGITIVE AIR") + pounds(t24, "5.2 - STACK AIR")
fac = t24.groupby("TRIFD").agg(air=("air", "sum"), lat=("LATITUDE", "first"), lon=("LONGITUDE", "first"))
pts = gpd.GeoDataFrame(fac, geometry=gpd.points_from_xy(fac.lon, fac.lat), crs=4269)
j = gpd.sjoin(pts, counties.to_crs(4269)[["GEOID", "geometry"]], predicate="within")
c = pop.set_index("GEOID")[["STNAME", "CTYNAME", "POPESTIMATE2025"]].join(j.groupby("GEOID").air.sum())
c["per_person"] = c.air.fillna(0) / c.POPESTIMATE2025
print(f"air releases 2024: {t24.air.sum() / 1e6:.1f} million lb; joined to counties: {c.air.sum() / 1e6:.1f}; US per person {c.air.sum() / c.POPESTIMATE2025.sum():.2f} lb")
air releases 2024: 530.3 million lb; joined to counties: 528.0; US per person 1.54 lb

Facilities released 530.3 million lb of listed chemicals to air in 2024; 528.0 million of it falls in a county with a population estimate. That is 1.54 lb per US resident.

Horizontal bars of chemicals released to air in 2024: ammonia 131.4 million lb, methanol 83.4, n-hexane 42.7, sulfuric acid aerosols 35.1 and styrene 24.7. Horizontal bars of chemicals released to air in 2024: ammonia 131.4 million lb, methanol 83.4, n-hexane 42.7, sulfuric acid aerosols 35.1 and styrene 24.7.
The chemicals released to air in the largest amounts.

Air, not land, and per person

1. Air is a small part of the total

print(f"stack {pounds(t24, '5.2 - STACK AIR').sum() / 1e6:.1f}M lb, fugitive {pounds(t24, '5.1 - FUGITIVE AIR').sum() / 1e6:.1f}M lb, all releases {pounds(t24).sum() / 1e6:.1f}M lb")
print((t24.groupby("CHEMICAL").air.sum().nlargest(5) / 1e6).round(1).to_string())
stack 383.9M lb, fugitive 146.4M lb, all releases 3290.5M lb
CHEMICAL
Ammonia                                                                                                           131.4
Methanol                                                                                                           83.4
n-Hexane                                                                                                           42.7
Sulfuric acid (acid aerosols including mists, vapors, gas, fog, and other airborne forms of any particle size)     35.1
Styrene                                                                                                            24.7

Air is 530.3 million of the 3,290.5 million lb released, stack emissions 383.9 million and fugitive leaks 146.4 million. Ammonia and methanol lead, then n-hexane, sulfuric acid aerosols and styrene.

2. The highest counties per person

print(c[c.POPESTIMATE2025 >= 10_000].nlargest(8, "per_person")[["CTYNAME", "STNAME", "air", "per_person"]].round(1).to_string())
                   CTYNAME       STNAME        air  per_person
GEOID                                                         
28111         Perry County  Mississippi  2615777.3       225.9
01099        Monroe County      Alabama  4033863.8       212.7
05081  Little River County     Arkansas  2475423.1       210.4
47005        Benton County    Tennessee  2892855.0       178.8
22047     Iberville Parish    Louisiana  5236228.7       177.0
48057       Calhoun County        Texas  3084767.3       157.4
22089   St. Charles Parish    Louisiana  7340321.4       145.1
47161       Stewart County    Tennessee  2033841.1       140.9

Among counties of 10,000 or more people, Perry County, Mississippi, has 225.9 lb per resident, Monroe County, Alabama, 212.7 and Little River County, Arkansas, 210.4, all small rural counties. Iberville and St. Charles parishes in Louisiana, on the Mississippi, also make the list.

Horizontal bars of the counties with the highest air releases per resident: Perry County, Mississippi 225.9 lb, Monroe County, Alabama 212.7 and Little River County, Arkansas 210.4. Horizontal bars of the counties with the highest air releases per resident: Perry County, Mississippi 225.9 lb, Monroe County, Alabama 212.7 and Little River County, Arkansas 210.4.
Highest air releases per resident among counties of 10,000 or more.

3. Most people live where little is released

srt = c.sort_values("per_person", ascending=False)
share = srt.air.fillna(0).cumsum() / srt.air.sum()
print(f"counties holding half of air releases: {(share < 0.5).sum() + 1}, home to {srt.POPESTIMATE2025[share < 0.5].sum() / c.POPESTIMATE2025.sum():.1%} of residents")
print(f"counties with no TRI air release: {(c.air.fillna(0) == 0).sum():,}")
counties holding half of air releases: 168, home to 1.6% of residents
counties with no TRI air release: 902

Half of all air releases come from 168 counties that are home to 1.6% of US residents, and 902 counties have no reported air release at all.

States and the map

The data used on this page

The US EPA's Toxics Release Inventory basic data files for 2014 and 2024, public domain, downloaded on 16 October 2026 and saved on this site, because EPA revises past years when facilities resubmit. With them come the Census Bureau's Vintage 2025 county population estimates and 2024 county boundaries.

from pathlib import Path
import re
import zipfile

import geopandas as gpd
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
# EPA Toxics Release Inventory basic data files, reporting years 2014 and 2024, all states, xz-compressed and
# otherwise unchanged; Census Vintage 2025 county estimates and 2024 cartographic boundary counties.
# Downloaded 16 October 2026 and saved here, because EPA revises past years.
# Live: https://data.epa.gov/efservice/downloads/tri/mv_tri_basic_download/2024_US/csv (and 2014_US)
ARCHIVE = "epa-tri-2014-2024-2026-10-16.zip"
if not Path(f"data/{ARCHIVE}").exists():
    Path(f"data/{ARCHIVE}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{ARCHIVE}", timeout=600).content)
Z = zipfile.ZipFile(f"data/{ARCHIVE}")


def tri(year):
    """One year's TRI file: a row per facility and chemical, columns numbered '1. YEAR' to '122. ...'."""
    d = pd.read_csv(Z.open(f"tri_{year}_us.csv.xz"), compression="xz", dtype=str, low_memory=False)
    d.columns = [re.sub(r"^\d+\.\s*", "", c) for c in d.columns]
    amounts = list(d.columns[d.columns.get_loc("5.1 - FUGITIVE AIR"):d.columns.get_loc("8.8 - ONE-TIME RELEASE") + 1])
    d[amounts + ["LATITUDE", "LONGITUDE"]] = d[amounts + ["LATITUDE", "LONGITUDE"]].apply(pd.to_numeric, errors="coerce")
    return d


def pounds(d, col="TOTAL RELEASES"):
    """A quantity in pounds: dioxin and dioxin-like compounds are reported in grams."""
    return d[col].where(d["UNIT OF MEASURE"] == "Pounds", d[col] / 453.59237)


t24 = tri(2024)
pop = pd.read_csv(Z.open("co-est2025-alldata.csv"), encoding="latin-1", dtype={"STATE": str, "COUNTY": str})
pop = pop[pop.SUMLEV == 50].assign(GEOID=lambda p: p.STATE + p.COUNTY)
Path("data/cb_2024_us_county_20m.zip").write_bytes(Z.read("cb_2024_us_county_20m.zip"))
counties = gpd.read_file("data/cb_2024_us_county_20m.zip")
print(f"TRI 2024: {len(t24):,} facility-chemical rows from {t24.TRIFD.nunique():,} facilities; {pounds(t24).sum() / 1e9:.2f} billion lb released")
TRI 2024: 77,295 facility-chemical rows from 21,482 facilities; 3.29 billion lb released

Example 1: by state

s = c.groupby("STNAME")[["air", "POPESTIMATE2025"]].sum()
s["per_person"] = s.air / s.POPESTIMATE2025
print("highest:", ", ".join(f"{k} {v:.1f}" for k, v in s.per_person.nlargest(5).items()))
print("lowest:", ", ".join(f"{k} {v:.2f}" for k, v in s.per_person.nsmallest(4).items()))
highest: North Dakota 28.2, Louisiana 11.5, Iowa 5.7, Alabama 5.4, Arkansas 4.7
lowest: District of Columbia 0.00, Vermont 0.08, New Hampshire 0.08, Massachusetts 0.09

North Dakota releases 28.2 lb of listed chemicals to air per resident, two and a half times Louisiana's 11.5; Iowa, Alabama and Arkansas follow. Vermont, New Hampshire and Massachusetts release less than a tenth of a pound per person, and Washington, DC, none.

Example 2: the map

low48 = counties[~counties.STUSPS.isin(["AK", "HI", "PR"])].to_crs(5070)
m = low48.merge(c[["per_person"]], left_on="GEOID", right_index=True, how="left")
m["cls"] = pd.cut(m.per_person, [-0.1, 0, 0.1, 1, 5, 20, 1e4])
print(m.cls.value_counts(sort=False).to_string())
ax = m.plot(column="cls", cmap="OrRd", legend=True, figsize=(10, 6))
ax.set_axis_off()
cls
(-0.1, 0.0]        887
(0.0, 0.1]         686
(0.1, 1.0]         631
(1.0, 5.0]         510
(5.0, 20.0]        246
(20.0, 10000.0]    149
Map of lower-48 counties shaded by pounds of toxic chemicals released to air per resident in 2024, darkest in scattered rural counties of the South, the Gulf Coast and the northern Plains, palest where nothing is released. Map of lower-48 counties shaded by pounds of toxic chemicals released to air per resident in 2024, darkest in scattered rural counties of the South, the Gulf Coast and the northern Plains, palest where nothing is released.
Toxic air releases per resident by county, 2024.

149 counties release more than 20 lb per resident and 887 none. The high counties are scattered through the rural South, the Gulf Coast and the northern Plains rather than in the big industrial cities, where large releases are divided by many people.