cori.data.pep

Access and analyze U.S. Census Bureau Population Estimates Program (PEP) data at the county level from 2000 to present, including population totals and components of change (births, deaths, domestic and international migration).

Drew Rosebush 2026-08-31

Overview

The cori.data.pep package provides access to U.S. Census Bureau Population Estimates Program (PEP) data at the county, state, and national level from 2000 to the present. The package covers both population totals and components of change — births, deaths, domestic migration, and international migration — enabling researchers to understand not just whether a community is growing or declining, but why.

Disaggregating population change into its components — rather than reading net headcounts alone — is essential for understanding what is actually happening in a rural community: whether it is losing working-age residents while gaining retirees, or building through in-migration while natural change is negative. PEP is also the population denominator used across the cori.data ecosystem — cori.data.bps and cori.data.bfs both normalize their counts against PEP population estimates. All data are returned in the standard cori.data long format: one row per geoid / year / variable.

Installation & quick start

remotes::install_github("ruralinnovation/cori.data.pep")
library(cori.data.pep)

# All available variables
get_pep_codebook()

# Total population — all counties, all years
get_population(geography = "county", variables = "population")

# Components of change for West Virginia counties
get_population_change(
  variables = c("domestic_migration", "international_migration", "net_migration"),
  geoids    = grep("^54", unique(get_population(geography = "county")$geoid), value = TRUE),
  years     = 2010:2025
)

What this data tells us about rural America

Population change reflects how people are responding to conditions on the ground — economic opportunity, quality of life, housing access, community character. It is often treated as a simple headline indicator, but the headline number obscures as much as it reveals. A community losing population through out-migration while also seeing natural increase is in a different situation than one losing population through both channels simultaneously. A community attracting working-age in-migrants is building differently than one growing through retiree in-migration or international immigration, each of which carries different implications for the labor market, consumer base, and service demands.

The Population Estimates Program data supports exactly this kind of disaggregated reading. Components of change — births, deaths, domestic migration, international migration — allow researchers and community leaders to understand why population is moving, not just whether it is. CORI’s community analysis found that net population numbers frequently masked offsetting flows: communities experiencing apparent stability were in some cases losing working-age residents while gaining older adults — a pattern with significant implications for workforce, entrepreneurship, and long-run fiscal capacity.

The in-migration of older adults is an underrecognized asset for rural communities. Research suggests that individuals migrating to rural areas later in life bring human, financial, and business capital — their experience and resources can stimulate local economies, support entrepreneurship, and deepen community networks. Population dynamics, read through the full components of change rather than the net headline, reveal which assets a community is gaining and which it is at risk of losing.

Introduction

Adapted from the Introduction vignette on GitHub.

cori.data.pep provides county-level population estimates from the U.S. Census Bureau’s Population Estimates Program. Data cover 2000–present at annual frequency.

Variables

get_pep_codebook() |>
  dplyr::select(variable, label, coverage = notes) |>
  knitr::kable()

Reading data

All data are returned in long format: one row per geoid / year / variable.

# Total population, all counties, all years
pop <- get_population(geography = "county", variables = "population")
dplyr::glimpse(pop)

Filter to specific variables, years, or counties:

wv_mig <- get_population_change(
  variables = c("domestic_migration", "international_migration", "net_migration"),
  geoids    = grep("^54", unique(pop$geoid), value = TRUE),
  years     = 2010:2025
)
dplyr::glimpse(wv_mig)

County spotlight: Grafton County, NH

library(ggplot2)
library(cori.charts)

load_fonts()

grafton_pop <- get_population(
  variables = "population",
  geoids    = "33009",
  years     = 2000:2025
)

ggplot(grafton_pop, aes(x = year, y = value)) +
  geom_line(color = cori_colors[["Emerald"]], linewidth = 1.2) +
  geom_point(color = cori_colors[["Emerald"]], size = 2) +
  scale_x_continuous(breaks = seq(2000, 2025, by = 5)) +
  scale_y_continuous(labels = scales::label_comma()) +
  theme_cori() +
  labs(
    title    = "Grafton County, NH population, 2000\u20132025",
    subtitle = "Total resident population estimate",
    x        = NULL,
    y        = NULL,
    caption  = "Source: CORI analysis of U.S. Census Bureau Population Estimates Program (PEP)."
  )
NOTE

Data attribution: Population Estimates Program data are produced by the U.S. Census Bureau. When citing analysis using this package, reference: U.S. Census Bureau, Population Estimates Program, www.census.gov/programs-surveys/popest.