Fixing TRI County Names That Do Not Join to County Boundaries
On this page
Problem statement
The Toxics Release Inventory gives every facility's county as a name in capitals, not as a FIPS code. Join those names to a county boundary file and about one facility in twenty finds no match: Saint is written ST, parishes carry the word PARISH, independent cities are marked (CITY), and Connecticut's counties were replaced by planning regions in the boundaries. Even where a name matches, it is sometimes not the county the facility's coordinates fall in. This page measures both problems in the 2024 file and uses the coordinates instead.
Quick answer
Make points from the latitude and longitude and place each facility with a spatial join:
fac = t24.drop_duplicates("TRIFD")[["TRIFD", "FACILITY NAME", "COUNTY", "ST", "LATITUDE", "LONGITUDE"]]
pts = gpd.GeoDataFrame(fac, geometry=gpd.points_from_xy(fac.LONGITUDE, fac.LATITUDE), crs=4269)
j = gpd.sjoin(pts, counties.to_crs(4269)[["GEOID", "NAME", "STUSPS", "geometry"]], predicate="within", how="left")
print(f"facilities: {len(fac):,}; placed in a county by location: {j.GEOID.notna().sum():,}")
facilities: 21,482; placed in a county by location: 21,451
The spatial join places 21,451 of the 21,482 facilities in a 2024 county.
Why the names fail
1. Join on the name
cn = counties.assign(key=counties.NAME.str.upper() + "|" + counties.STUSPS)
f = fac.assign(key=fac.COUNTY.str.upper().str.strip() + "|" + fac.ST)
hit = f.key.isin(cn.key)
print(f"matched by name and state: {hit.sum():,} of {len(f):,} ({hit.mean():.1%})")
print(f[~hit].COUNTY.value_counts().head(8).to_string())
matched by name and state: 20,333 of 21,482 (94.7%)
COUNTY
NEW HAVEN 77
HARTFORD 73
ST LOUIS 56
ST LOUIS (CITY) 45
ST JOSEPH 44
FAIRFIELD 40
CALCASIEU PARISH 31
EAST BATON ROUGE PARISH 27
Joining on name and state instead matches 20,333 facilities, 94.7%. The unmatched names include New Haven, Hartford and Fairfield in Connecticut, ST LOUIS and ST LOUIS (CITY), and parishes written in full.
2. What the unmatched names are
miss = f[~hit]
print("with '(CITY)':", miss.COUNTY.str.contains(r"\(CITY\)").sum(), "| with 'ST.' or 'SAINT':", miss.COUNTY.str.contains(r"^ST\.?\s|SAINT").sum(),
"| Connecticut:", (miss.ST == "CT").sum(), "| Alaska:", (miss.ST == "AK").sum(), "| outside the states:", miss.ST.isin(["PR", "GU", "VI", "MP", "AS"]).sum())
with '(CITY)': 181 | with 'ST.' or 'SAINT': 262 | Connecticut: 252 | Alaska: 48 | outside the states: 123
181 unmatched names carry (CITY), 262 start with ST or contain SAINT, 252 are in Connecticut and 48 in Alaska, and 123 facilities are outside the states. Each could be fixed by a rule, but every rule leaves exceptions.
3. Location and name disagree
both = j[j.GEOID.notna()].assign(key=lambda d: d.COUNTY.str.upper().str.strip() + "|" + d.ST)
agree = both.key == both.NAME.str.upper() + "|" + both.STUSPS
print(f"name matches the county the point falls in: {agree.mean():.1%}")
print(both[~agree & both.key.isin(cn.key)][["FACILITY NAME", "COUNTY", "NAME", "STUSPS"]].head(4).to_string(index=False))
name matches the county the point falls in: 92.9%
FACILITY NAME COUNTY NAME STUSPS
CHEVRON PHILLIPS CHEMICAL CO LP HARRIS Chambers TX
US ARMY NATIONAL GUARD CAMP ATTERBURY RANGES BARTHOLOMEW Johnson IN
SYNTHOMER ADHESIVE TEHCNOLOGIES EASTOFLEX & EAST HARRISON Gregg TX
BUCKEYE FIRE EQUIPMENT GASTON Cleveland NC
Where both work, the name agrees with the county the point falls in for 92.9% of facilities. Some disagreements are a plant near a county line reported under the neighbouring county, such as a Chevron Phillips plant listed in Harris County whose coordinates are in Chambers County, Texas.
Points near borders and a rule
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: the rule
j["county"] = j.GEOID
print(f"facilities with a county by location: {j.county.notna().mean():.2%}; left over: {j[j.county.isna()].ST.value_counts().head(4).to_dict()}")
facilities with a county by location: 99.86%; left over: {'GU': 9, 'VI': 5, 'MP': 4, 'HI': 2}
The spatial join places 99.86% of facilities. What is left are 9 in Guam, 5 in the Virgin Islands, 4 in the Northern Marianas, which the boundary file does not include, and 2 Hawaii facilities on the coast just outside the generalised county outlines.
Example 2: facilities per county
per = j.groupby(["GEOID", "NAME", "STUSPS"]).size().sort_values(ascending=False)
print(per.head(5).to_string())
GEOID NAME STUSPS
48201 Harris TX 358
06037 Los Angeles CA 307
17031 Cook IL 290
04013 Maricopa AZ 194
39035 Cuyahoga OH 126
Harris County, Texas has the most facilities, 358, then Los Angeles, Cook, Maricopa and Cuyahoga counties.
Internal links
- Air releases per resident β a county map built on this join
- Read the TRI files β the location columns
- Why Connecticut is missing from a county map β the planning regions
- Facilities by amount released β the points themselves