How to Compare Toxic Releases Between Two Years Facility by Facility

On this page

Problem statement

Did US industry release fewer toxic chemicals in 2024 than ten years earlier? Comparing two years of the Toxics Release Inventory gives a national answer in one line, but facilities come and go between years, and a few very large ones can move the total on their own. This page compares 2014 with 2024: the national total, facilities that reported in both years, closures and newcomers, the facilities that moved most, and changes by sector and by state.

Quick answer

Total each year's releases by facility and compare:

t14 = tri(2014)
a = t14.assign(lb=pounds(t14)).groupby("TRIFD").lb.sum()
b = t24.assign(lb=pounds(t24)).groupby("TRIFD").lb.sum()
print(f"2014: {a.sum() / 1e9:.2f} billion lb from {len(a):,} facilities; 2024: {b.sum() / 1e9:.2f} from {len(b):,} ({b.sum() / a.sum() - 1:+.1%})")
2014: 4.03 billion lb from 22,391 facilities; 2024: 3.29 from 21,482 (-18.3%)

Releases fell from 4.03 billion lb in 2014 to 3.29 billion in 2024, 18.3%, while the number of reporting facilities fell from 22,391 to 21,482.

Waterfall chart from 2014's 4.03 billion lb to 2024's 3.29 billion: minus 341.6 million from facilities that stopped reporting, minus 641.1 million at continuing facilities and plus 247.4 million from new reporters. Waterfall chart from 2014's 4.03 billion lb to 2024's 3.29 billion: minus 341.6 million from facilities that stopped reporting, minus 641.1 million at continuing facilities and plus 247.4 million from new reporters.
Where the fall from 2014 to 2024 came from.

Same facilities, closures and newcomers

1. Who reported in both years

both = a.index.intersection(b.index)
print(f"both years: {len(both):,}; only 2014: {len(a.index.difference(b.index)):,}; only 2024: {len(b.index.difference(a.index)):,}")
print(f"released by facilities in both: {a[both].sum() / 1e9:.2f} -> {b[both].sum() / 1e9:.2f} billion lb ({b[both].sum() / a[both].sum() - 1:+.1%})")
both years: 15,805; only 2014: 6,586; only 2024: 5,677
released by facilities in both: 3.68 -> 3.04 billion lb (-17.4%)

15,805 facilities reported in both years. 6,586 stopped reporting and 5,677 started. The continuing facilities released 3.68 billion lb in 2014 and 3.04 billion in 2024, 17.4% less.

2. Where the change came from

parts = pd.Series({"stopped reporting": -a[a.index.difference(b.index)].sum(), "new reporters": b[b.index.difference(a.index)].sum(),
                   "change at continuing facilities": b[both].sum() - a[both].sum()}) / 1e6
print(parts.round(1).to_string())
stopped reporting                 -341.6
new reporters                      247.4
change at continuing facilities   -641.1

Most of the fall, 641.1 million lb, is change at continuing facilities. Facilities that stopped reporting took 341.6 million lb with them, and new reporters added 247.4 million.

Horizontal bars of the facilities whose releases changed most between 2014 and 2024: Red Dog down 400.5 million lb, two Nevada gold mines down 46.0 and 35.1, Greens Creek up 46.0 and Elemental Environmental Solutions up 39.8. Horizontal bars of the facilities whose releases changed most between 2014 and 2024: Red Dog down 400.5 million lb, two Nevada gold mines down 46.0 and 35.1, Greens Creek up 46.0 and Elemental Environmental Solutions up 39.8.
The facilities that changed most, 2014 to 2024.

3. The biggest movers

d = (b[both] - a[both]).sort_values()
names = t24.drop_duplicates("TRIFD").set_index("TRIFD")[["FACILITY NAME", "ST"]]
print("largest falls:", ", ".join(f"{names.loc[i, 'FACILITY NAME'].strip()[:28]} ({names.loc[i, 'ST']}) {v / 1e6:+.1f}M" for i, v in d.head(3).items()))
print("largest rises:", ", ".join(f"{names.loc[i, 'FACILITY NAME'].strip()[:28]} ({names.loc[i, 'ST']}) {v / 1e6:+.1f}M" for i, v in d.tail(3).items()))
largest falls: RED DOG OPERATIONS (AK) -400.5M, NEVADA GOLD MINES LLC - PHOE (NV) -46.0M, NEVADA GOLD MINES LLC-CORTEZ (NV) -35.1M
largest rises: KENNECOTT UTAH COPPER MINE C (UT) +30.5M, ELEMENTAL ENVIRONMENTAL SOLU (AR) +39.8M, HECLA GREENS CREEK MINING CO (AK) +46.0M

One mine dominates: Red Dog in Alaska released 400.5 million lb less in 2024 than in 2014, more than half of the national fall. Two Nevada gold mines fell by 46.0 and 35.1 million. The largest increases were also at mines and a hazardous-waste site: Greens Creek in Alaska up 46.0 million, Elemental Environmental Solutions in Arkansas 39.8 million and Kennecott in Utah 30.5 million.

Sectors and states

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 industry sector

sec = pd.DataFrame({"2014": t14.assign(lb=pounds(t14)).groupby("INDUSTRY SECTOR").lb.sum(), "2024": t24.assign(lb=pounds(t24)).groupby("INDUSTRY SECTOR").lb.sum()}) / 1e6
sec["change"] = sec["2024"] / sec["2014"] - 1
print(sec.nlargest(6, "2014").round({"2014": 1, "2024": 1, "change": 3}).to_string())
                      2014    2024  change
INDUSTRY SECTOR                           
Metal Mining        1791.8  1345.4  -0.249
Chemicals            534.1   445.8  -0.165
Electric Utilities   528.2   191.1  -0.638
Primary Metals       351.7   344.5  -0.020
Paper                189.9   134.1  -0.294
Food                 133.6   152.2   0.139

Electric utilities cut releases by 63.8%, from 528.2 to 191.1 million lb, the largest relative fall of any large sector. Metal mining fell 24.9% and chemicals 16.5%; food processing rose 13.9%.

Example 2: air releases by state

air = lambda t: (pounds(t, "5.1 - FUGITIVE AIR") + pounds(t, "5.2 - STACK AIR")).groupby(t.ST).sum()
s = pd.DataFrame({"2014": air(t14), "2024": air(t24)}).dropna()
s["change"] = s["2024"] / s["2014"] - 1
big = s[s["2014"] >= 5e6].sort_values("change")
print("fell most:", ", ".join(f"{k} {v:+.0%}" for k, v in big.change.head(4).items()))
print("rose:", ", ".join(f"{k} {v:+.0%}" for k, v in big.change[big.change > 0].items()) or "none")
fell most: KY -68%, WV -65%, UT -64%, PA -62%
rose: LA +3%, ND +14%, OK +34%
Bar chart of the change in toxic air releases by state from 2014 to 2024, the largest falls in Kentucky, West Virginia, Utah and Pennsylvania and rises only in Louisiana, North Dakota and Oklahoma. Bar chart of the change in toxic air releases by state from 2014 to 2024, the largest falls in Kentucky, West Virginia, Utah and Pennsylvania and rises only in Louisiana, North Dakota and Oklahoma.
Change in air releases by state, 2014 to 2024.

Air releases fell most in Kentucky, West Virginia, Utah and Pennsylvania, by 62% to 68%. Among states with large air releases in 2014, only Louisiana, North Dakota and Oklahoma released more in 2024.