COUNTY POPULATION & MIGRATION / PYTHON + CSV
Compare county growth and migration in Python
Add population size, growth and migration components to a county market comparison using one consistent Census vintage.
Add population trends to a county market comparison
Suppose you are comparing three North Carolina counties for a service-area expansion. You want to know how large each market is, how quickly it has grown, and how much recent growth came from births and deaths versus migration. The county summary supplies population levels and growth; the annual history supplies the components.
The optional input.csv contains fictional research labels paired with real county FIPS codes for Durham, Robeson and Wake. Those same records appear in the markets list in the Python code below. Only the project labels are invented; the reference values are genuine selections from the product.
Download the product samples
Save both files in your working folder. county_summary.csv contains these three counties with every summary field in the paid download. population_history.csv contains their complete six-year histories, including dates, demographic components and published rates. The example selects 2025 from the history before joining.
The full $29 edition covers 3,144 counties and equivalents across the 50 states and DC, with 18,864 annual records. Inspect all fields and coverage.
Code example
The code reads county identifiers as text and checks that the summary and selected history each have one record per county. It adds July 2025 population, recent and five-year growth, and the 2024–2025 components to each market record. The results list remains available in your notebook.
Paths are relative to your working folder; use from pathlib import Path; print(Path.cwd()) to check it, or supply full paths.
import csv
from pathlib import Path
# Fictional market labels paired with real county FIPS codes.
# The optional example input CSV contains these same records.
markets = [
{"market": "Durham research", "county_fips": "37063"},
{"market": "Robeson research", "county_fips": "37155"},
{"market": "Wake research", "county_fips": "37183"},
]
folder = Path(".") # Folder containing the sample files or extracted full download.
with (folder / "county_summary.csv").open(newline="", encoding="utf-8-sig") as file:
summary_rows = list(csv.DictReader(file))
summary = {row["county_fips"]: row for row in summary_rows}
if len(summary) != len(summary_rows):
raise ValueError("Duplicate county keys in the summary")
with (folder / "population_history.csv").open(newline="", encoding="utf-8-sig") as file:
selected_rows = [row for row in csv.DictReader(file) if row["year"] == "2025"]
history = {row["county_fips"]: row for row in selected_rows}
if len(history) != len(selected_rows):
raise ValueError("Duplicate county/year keys in the selected history")
results = []
for market in markets:
key = market["county_fips"]
county, annual = summary.get(key), history.get(key)
if county is None or annual is None:
raise ValueError(f"County {key} is absent from one of the reference files")
result = {
**market,
"county_name": county["county_name"],
"population_2025": county["population_2025"],
"growth_people": annual["population_change"],
"growth_percent": annual["population_change_pct"],
"growth_since_2020_percent": county["change_pct_2020_2025"],
"natural_change": annual["natural_change"],
"net_domestic_migration": annual["net_domestic_migration"],
"net_international_migration": annual["net_international_migration"],
"residual": annual["residual"],
}
results.append(result)
print(f"{result['market']} | {key} | {result['county_name']}")
print(f" July 2025 population: {int(result['population_2025']):,}; "
f"2024–2025 growth: {int(result['growth_people']):+,} "
f"({float(result['growth_percent']):+.2f}%); "
f"July 2020–2025 growth: {float(result['growth_since_2020_percent']):+.2f}%")
print(f" Natural change: {int(result['natural_change']):+,}; "
f"net domestic migration: {int(result['net_domestic_migration']):+,}; "
f"net international migration: {int(result['net_international_migration']):+,}; "
f"residual: {int(result['residual']):+,}")
Analysis of the output
Durham research | 37063 | Durham County
July 2025 population: 347,240; 2024–2025 growth: +4,926 (+1.44%); July 2020–2025 growth: +7.68%
Natural change: +1,869; net domestic migration: -430; net international migration: +3,493; residual: -6
Robeson research | 37155 | Robeson County
July 2025 population: 119,941; 2024–2025 growth: +541 (+0.45%); July 2020–2025 growth: +3.42%
Natural change: +259; net domestic migration: -171; net international migration: +445; residual: +8
Wake research | 37183 | Wake County
July 2025 population: 1,257,235; 2024–2025 growth: +27,760 (+2.26%); July 2020–2025 growth: +11.17%
Natural change: +6,369; net domestic migration: +12,790; net international migration: +8,602; residual: -1
Wake is both the largest county in this comparison and the fastest growing over the latest year: 27,760 additional people, or 2.26%. Its July population grew 11.17% between 2020 and 2025. Population levels describe market scale; percentages make growth easier to compare across counties of different sizes.
Durham grew by 4,926 people despite negative net domestic migration. Natural change contributed 1,869 people, net international migration contributed 3,493, and net domestic migration contributed −430. The −6 residual completes the reconciliation. Looking only at total growth would miss this difference in the components.
Robeson grew by 541 people, with positive natural change and net international migration offsetting negative net domestic migration. These figures can inform a regional research brief, but they do not measure demand for a particular service or establish why people moved.
Natural change is births minus deaths. Net migration is arrivals minus departures; a negative value is a net loss, not a missing observation. The residual captures changes that are not assigned to the other components. It is retained in the product, so natural_change + net_domestic_migration + net_international_migration + residual equals population_change.
Using the full dataset
Extract the purchased ZIP and replace both samples with the complete files of the same names. Replace markets with your own county list, or load a CSV with market and county_fips columns:
with open("my_counties.csv", newline="", encoding="utf-8-sig") as file:
markets = list(csv.DictReader(file))
For a trend chart, use all six rows per county in population_history.csv, plotting population_date against population. For a different annual comparison, change the history filter and the corresponding population/growth fields and output labels in the example. Do not join all six history rows and then sum a repeated customer measure.
For 2021–2025, components cover July 1 of the preceding year through June 30. The 2020 record covers only April–June, starting at the revised April 1 base. It is explicitly marked initial_quarter; its six source component-rate fields were not published and remain blank. Its counts and growth must not be treated as a full year.
The published component rates are per 1,000 average beginning/end population. The product’s growth percentages use beginning population instead and are rounded to four decimals. They have different units and denominators. The five-year summary compares July 2020 with July 2025, not the April 2020 base.
Our ACS County Demographic Profile adds household and housing context, while the County Income & Poverty Reference adds annual income and poverty estimates. The County Employment & Wage Reference supplies workplace-based employment. Check boundaries, periods and statistical populations before joining; shared FIPS codes do not make these measures interchangeable.
Sources and versions
Source: U.S. Census Bureau, Population Estimates Program, Vintage 2025, released March 26, 2026. The estimates cover July 2020–2025 on January 1, 2025 county boundaries, including Connecticut’s nine planning regions. ACD captured the inputs October 18, 2026 UTC and packaged this preparation 2026-10-18.
This is one consistent source vintage. A later vintage can revise earlier years; combining the 2020 value from one vintage with the 2025 value from another can mix population change with source revisions. This initial ACD edition does not compare vintages. Read the source and preparation record and the official layout for field definitions and periods.
The pipeline checks all population balances, rate formulas and county-to-state sums, and compares delivered values directly with the original CSV. These checks validate the preparation; they do not remove uncertainty in the Census estimates. No forecasts, origin–destination migration flows or unpublished confidence intervals are supplied.
Example checked October 18, 2026 against both these samples and the complete purchased files. Source and processing records, sample checksums · Expected result CSV. Independent preparation; no agency endorsement.