COUNTY DEMOGRAPHICS / PYTHON + CSV

Add county demographics in Python

Join population, household income and poverty estimates to a service-area list, keeping the published margins of error alongside the values.

The example and its inputs

Suppose you are preparing a service-area report and already know the county for each location. The county profile adds context for planning: how many people live in the county, its median household income, and the share of residents below the poverty threshold.

input.csv contains three fictional service-area records, repeated in the sites list in the Python code below. The site labels are invented to illustrate the join; the county identifiers refer to Autauga County, Alabama, Wake County, North Carolina, and San Juan Municipio, Puerto Rico. The input CSV is optional because the code includes these records.

county_profile.csv is a sample of the paid 2020–2024 ACS County Demographic Profile: three unchanged county rows with all 60 columns. It supplies the demographic estimates, margins of error and status fields. The estimates describe the county, not the people who use a particular service.

01 / SAMPLE DATA

Download the product sample

Save this file in your working folder. The five-character county_fips identifier stays text, preserving the leading zero in 01001.

02 / PYTHON

Code example

Run this in one Jupyter cell, an IPython session, or a Python script. It uses Python’s standard library and retains every service-area record. The result includes population as well as the income and poverty figures printed below.

Paths are relative to your working folder; use from pathlib import Path; print(Path.cwd()) to check it, or supply full paths.

Python 3.10+ · Jupyter, IPython or a script
import csv

# Fictional service-area records; these county identifiers are real.
sites = [
    {"site": "Central Alabama", "county_fips": "01001"},
    {"site": "Research Triangle", "county_fips": "37183"},
    {"site": "San Juan", "county_fips": "72127"},
]

with open("county_profile.csv", newline="", encoding="utf-8-sig") as f:
    profile = list(csv.DictReader(f))
by_county = {row["county_fips"]: row for row in profile}
if len(by_county) != len(profile):
    raise ValueError("Expected one profile row per county")

fields = ["county_name", "population", "median_household_income",
          "median_household_income_moe_90", "median_household_income_status",
          "median_household_income_moe_status", "poverty_pct", "poverty_pct_moe_90",
          "poverty_pct_status", "poverty_pct_moe_status"]
results = []
for site in sites:
    county = by_county.get(site["county_fips"])
    result = dict(site)
    result.update({field: county[field] if county else "" for field in fields})
    result["match_status"] = "matched" if county else "county_not_in_reference"
    results.append(result)

# Empty estimates and special MOEs stay explicit; neither becomes zero.
def estimate_with_moe(row, measure, unit):
    value, moe = row[measure], row[measure + "_moe_90"]
    estimate = f"{value} {unit}" if value else row[measure + "_status"]
    uncertainty = f"± {moe}" if moe else row[measure + "_moe_status"]
    return f"{estimate} ({uncertainty}; 90% MOE)"

for row in results:
    print(f"{row['site']} | {row['county_fips']} | {row['county_name'] or row['match_status']}")
    if row["match_status"] == "matched":
        print("  Median household income:", estimate_with_moe(row, "median_household_income", "USD"))
        print("  Poverty:", estimate_with_moe(row, "poverty_pct", "%"))

03 / RESULTS

Analysis of the output

Central Alabama | 01001 | Autauga County, Alabama
  Median household income: 72481 USD (± 5640; 90% MOE)
  Poverty: 11.3 % (± 2.1; 90% MOE)
Research Triangle | 37183 | Wake County, North Carolina
  Median household income: 105768 USD (± 1626; 90% MOE)
  Poverty: 8.1 % (± 0.4; 90% MOE)
San Juan | 72127 | San Juan Municipio, Puerto Rico
  Median household income: 27992 USD (± 1036; 90% MOE)
  Poverty: 38.8 % (± 1.1; 90% MOE)

The join adds the same published fields to all three service areas, including Puerto Rico. Wake County’s median household income is $105,768 in 2024 dollars, with a published 90% margin of error of $1,626. The corresponding interval is $104,142 to $107,394. This is an estimate for households across the county during the 2020–2024 survey period; it is not an estimate of a particular customer’s income.

The poverty estimate is the percentage of people below the poverty threshold, among the population for whom poverty status is determined. Its margin of error is in percentage points. Keep this uncertainty alongside the estimate when presenting the county profile. This example reports published values; it does not perform a statistical test of differences between counties.

A matching county can still have an unavailable measure. The product keeps that estimate blank and records why in its status field. A controlled_moe status means Census does not report a numeric MOE for that controlled estimate; it is not a zero calculated by ACD. A county absent from the reference remains in the result with county_not_in_reference. With the sample, that indicates only the sample’s limited coverage.

04 / YOUR DATA

Using the full dataset

Replace the sample with county_profile.csv from the purchased ZIP, or change the path in open(...). Replace the sites list with your own records, or load a CSV with site and county_fips columns:

with open("my-service-areas.csv", newline="", encoding="utf-8-sig") as f:
    sites = list(csv.DictReader(f))

Insert these lines where the example list appears. The full table covers 3,222 counties and equivalents across the 50 states, DC and Puerto Rico. Its 13 measures cover population, age, households, household income, poverty, unemployment, education, housing, tenure, rent and home value. The accompanying county_measures.csv contains 41,886 detailed records with units, universes, source variable IDs and original annotations.

Use identifiers compatible with 2024 county geography. Connecticut uses nine planning regions, so identifiers from our 2020 ZCTA crosswalk’s former counties will not directly match those regions. This example assumes county identifiers are already known; it does not geocode addresses. Land shares from a ZCTA crosswalk are not population weights, and county medians cannot be averaged into a regional median.

Sources and versions

The Census Bureau’s ACS five-year Data Profiles provide the 2020–2024 estimates and 90% margins of error. We use DP02–DP05, with DP02PR for Puerto Rico household and education measures. Census released this edition on January 29, 2026. ACD produced this package on 2026-10-16; that date identifies our preparation, not a newer survey period.

ACD selects the measures, provides wide and detailed tables, separates source special values from numeric fields, and records the source and processing identities. Percentages and margins of error are taken directly from Census. Review Census comparison guidance before comparing years or overlapping five-year periods.

Example checked October 16, 2026 against both these samples and the complete purchased files. Source and processing records, sample checksums · Expected result CSV. Independent preparation; no agency endorsement.