Skip to content

Population Estimates

The Census Bureau’s Population Estimates Program (PEP) produces annual estimates of the population for the nation, states, counties, and other geographies in the years between decennial censuses. PyPUMS provides get_estimates() to pull these estimates from the Census API, including breakdowns by age, sex, race, and Hispanic origin.

Function signature

pypums.get_estimates(
    geography,
    *,
    product=None,
    variables=None,
    breakdown=None,
    breakdown_labels=False,
    vintage=2023,
    year=None,
    state=None,
    county=None,
    time_series=False,
    output="tidy",
    geometry=False,
    cache_table=False,
    show_call=False,
    key=None,
)

API key required

All Census API calls require a key. Request one for free at https://api.census.gov/data/key_signup.html, then set the CENSUS_API_KEY environment variable or pass key="YOUR_KEY" to every call.


Products

The product parameter selects which estimates dataset to query. If omitted, it defaults to "population".

Product product= Dataset Description
Population "population" pep/population Total population estimates
Components of Change "components" pep/components Births, deaths, migration
Housing "housing" pep/housing Housing unit estimates
Characteristics "characteristics" pep/charagegroups Population by age, sex, race, Hispanic origin

Getting started

State population estimates

import pypums

state_pop = pypums.get_estimates(
    geography="state",
    product="population",
    vintage=2023,
)
print(state_pop.head())
       GEOID                 NAME     variable      value
0         01              Alabama  POP_2023    5108468
1         02               Alaska  POP_2023     733406
2         04              Arizona  POP_2023    7303398
3         05             Arkansas  POP_2023    3067732
4         06           California  POP_2023   38965193

County population estimates

tx_counties = pypums.get_estimates(
    geography="county",
    product="population",
    state="TX",
    vintage=2023,
)
print(tx_counties.head())
     GEOID                          NAME     variable      value
0  48001   Anderson County, Texas         POP_2023      57245
1  48003    Andrews County, Texas         POP_2023      20536
2  48005   Angelina County, Texas         POP_2023      87032
3  48007    Aransas County, Texas         POP_2023      24425
4  48009     Archer County, Texas         POP_2023       8560

Vintage vs. year

Population estimates use a vintage model. The vintage parameter specifies which release of estimates to query, while the optional year parameter filters to a specific data year within that vintage.

# All data from the 2023 vintage (default)
df = pypums.get_estimates(
    geography="state",
    vintage=2023,
)

# Only the 2021 estimate from the 2023 vintage
df_2021 = pypums.get_estimates(
    geography="state",
    vintage=2023,
    year=2021,
)

What is a vintage?

A vintage is the reference year for an estimates release. The 2023 vintage contains estimates for July 1 of each year from the last decennial census (April 2020) through July 2023. Later vintages may revise earlier estimates based on updated data.


Breakdown dimensions

The breakdown parameter adds demographic dimensions to the query. This is primarily used with product="characteristics".

Available breakdown dimensions:

Dimension breakdown= Values
Age group "AGEGROUP" 0=All ages, 1=0-4, 2=5-13, 3=14-17, 4=18-24, 5=25-44, 6=45-64, 7=65+, …
Sex "SEX" 0=Both, 1=Male, 2=Female
Race "RACE" 0=All, 1=White alone, 2=Black alone, 3=AIAN alone, 4=Asian alone, …
Hispanic origin "HISP" 0=Both, 1=Non-Hispanic, 2=Hispanic

Single breakdown

by_sex = pypums.get_estimates(
    geography="state",
    product="characteristics",
    breakdown="SEX",
    vintage=2023,
)
print(by_sex.head(6))
  GEOID       NAME     variable      value  SEX
0    01    Alabama    POP_2023    5108468    0
1    01    Alabama    POP_2023    2467520    1
2    01    Alabama    POP_2023    2640948    2
3    02     Alaska    POP_2023     733406    0
4    02     Alaska    POP_2023     377621    1
5    02     Alaska    POP_2023     355785    2

Multiple breakdowns

Pass a list to cross-tabulate by multiple dimensions:

by_age_sex = pypums.get_estimates(
    geography="state",
    product="characteristics",
    breakdown=["AGEGROUP", "SEX"],
    vintage=2023,
)
print(f"{len(by_age_sex)} rows (states × age groups × sexes)")
4992 rows (states × age groups × sexes)

Breakdown labels

Numeric breakdown codes are not always intuitive. Set breakdown_labels=True to add human-readable label columns:

labeled = pypums.get_estimates(
    geography="state",
    product="characteristics",
    breakdown=["AGEGROUP", "SEX"],
    breakdown_labels=True,
    vintage=2023,
)
print(labeled[["NAME", "AGEGROUP", "AGEGROUP_label", "SEX", "SEX_label"]].head())
                  NAME  AGEGROUP       AGEGROUP_label  SEX SEX_label
0              Alabama         0            All ages    0  Both sexes
1              Alabama         0            All ages    1       Male
2              Alabama         0            All ages    2     Female
3              Alabama         1    Age 0 to 4 years    0  Both sexes
4              Alabama         1    Age 0 to 4 years    1       Male

Available label mappings

Code Label
0 All ages
1 Age 0 to 4 years
2 Age 5 to 13 years
3 Age 14 to 17 years
4 Age 18 to 24 years
5 Age 25 to 44 years
6 Age 45 to 64 years
7 Age 65 years and over
8 Age 85 years and over
9 Age 0 to 17 years
10 Age 18 to 64 years
11 Age 18 years and over
12 Age 65 years and over
13 Under 18 years
14 5 to 13 years
15 14 to 17 years
16 18 to 64 years
17 16 years and over
18 Under 5 years
29 Age 0 to 14 years
30 Age 15 to 44 years
31 Age 16 years and over
Code Label
0 Both sexes
1 Male
2 Female
Code Label
0 All races
1 White alone
2 Black alone
3 American Indian and Alaska Native alone
4 Asian alone
5 Native Hawaiian and Other Pacific Islander alone
6 Two or more races
7 White alone or in combination
8 Black alone or in combination
9 American Indian and Alaska Native alone or in combination
10 Asian alone or in combination
11 Native Hawaiian and Other Pacific Islander alone or in combination
Code Label
0 Both Hispanic Origins
1 Non-Hispanic
2 Hispanic

Time series

Set time_series=True to retrieve estimates across multiple years within a single vintage. This adds DATE_CODE and DATE_DESC columns to the output.

pop_trend = pypums.get_estimates(
    geography="state",
    product="population",
    vintage=2023,
    time_series=True,
)
print(pop_trend[["NAME", "DATE_CODE", "DATE_DESC"]].head(8))
                  NAME  DATE_CODE              DATE_DESC
0              Alabama          1  4/1/2020 Census population
1              Alabama          2  4/1/2020 estimates base
2              Alabama          3  7/1/2020 population estimate
3              Alabama          4  7/1/2021 population estimate
4              Alabama          5  7/1/2022 population estimate
5              Alabama          6  7/1/2023 population estimate
6               Alaska          1  4/1/2020 Census population
7               Alaska          2  4/1/2020 estimates base

Time series is only available for population estimates

Setting time_series=True with any product other than "population" will raise a ValueError. The Census API only supports the time-series endpoint for the pep/population dataset.

Plotting a time series

import pandas as pd

ts = pypums.get_estimates(
    geography="state",
    product="population",
    vintage=2023,
    time_series=True,
)

# Filter to July 1 estimates (DATE_CODE >= 3)
july_ests = ts[ts["DATE_CODE"] >= 3].copy()

# Extract year from DATE_DESC
july_ests["year"] = july_ests["DATE_DESC"].str.extract(r"(\d{4})")

# Pivot for a specific state
ca = july_ests[july_ests["NAME"] == "California"]
print(ca[["year", "value"]])
    year      value
  2020   39503200
  2021   39142991
  2022   38965193
  2023   39128162

Tidy vs. wide output

df_tidy = pypums.get_estimates(
    geography="county",
    product="population",
    state="CA",
    vintage=2023,
    output="tidy",
)
GEOID NAME variable value
06001 Alameda County, California POP_2023 1638215

Columns: GEOID, NAME, variable, value, plus any breakdown columns.

df_wide = pypums.get_estimates(
    geography="county",
    product="population",
    state="CA",
    vintage=2023,
    output="wide",
)
GEOID NAME POP_2023
06001 Alameda County, California 1638215

Each variable gets its own column.


Components of change

The "components" product provides the building blocks of population change: births, deaths, domestic migration, and international migration.

components = pypums.get_estimates(
    geography="state",
    product="components",
    vintage=2023,
)
print(components.head(10))
  GEOID       NAME       variable      value
0    01    Alabama        BIRTHS      57827
1    01    Alabama        DEATHS      63415
2    01    Alabama    NATURALCHG      -5588
3    01    Alabama   DOMESTICMIG      19826
4    01    Alabama  INTERNATIONALMIG    5634
5    01    Alabama        NETMIG      25460
6    02     Alaska        BIRTHS       9821
7    02     Alaska        DEATHS       5142
8    02     Alaska    NATURALCHG       4679
9    02     Alaska   DOMESTICMIG     -10523

Common component variables include:

Variable Description
BIRTHS Number of births
DEATHS Number of deaths
NATURALCHG Natural change (births minus deaths)
DOMESTICMIG Net domestic migration
INTERNATIONALMIG Net international migration
NETMIG Net migration (domestic + international)

Housing unit estimates

housing = pypums.get_estimates(
    geography="county",
    product="housing",
    state="FL",
    vintage=2023,
)
print(housing.head(3))
     GEOID                          NAME    variable      value
0  12001   Alachua County, Florida        HUEST_2023    125420
1  12003     Baker County, Florida        HUEST_2023     11892
2  12005       Bay County, Florida        HUEST_2023    102318

Geometry support

Set geometry=True to get a GeoDataFrame with cartographic boundary shapes:

pop_geo = pypums.get_estimates(
    geography="county",
    product="population",
    state="WA",
    vintage=2023,
    geometry=True,
)
import altair as alt

alt.Chart(pop_geo).mark_geoshape(stroke="white", strokeWidth=0.5).encode(
    color=alt.Color("value:Q", scale=alt.Scale(scheme="blues"), legend=alt.Legend(title="Population")),
    tooltip=["NAME:N", alt.Tooltip("value:Q", format=",")],
).project("albersUsa").properties(width=500, height=400)

Interactive preview — state-level population estimates

The code above fetches county-level data for Washington state. The chart below uses state-level data to show what geometry=True + Altair looks like at a national scale.

{ "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "width": 500, "height": 350, "title": "2023 Population Estimates by State", "data": { "url": "https://cdn.jsdelivr.net/npm/vega-datasets@v2.7.0/data/us-10m.json", "format": {"type": "topojson", "feature": "states"} }, "transform": [ { "lookup": "id", "from": { "data": { "values": [ {"id": 1, "value": 5108468, "name": "Alabama"}, {"id": 2, "value": 733406, "name": "Alaska"}, {"id": 4, "value": 7303398, "name": "Arizona"}, {"id": 5, "value": 3067732, "name": "Arkansas"}, {"id": 6, "value": 38965193, "name": "California"}, {"id": 8, "value": 5877610, "name": "Colorado"}, {"id": 9, "value": 3617176, "name": "Connecticut"}, {"id": 10, "value": 1017551, "name": "Delaware"}, {"id": 11, "value": 678972, "name": "District of Columbia"}, {"id": 12, "value": 22610726, "name": "Florida"}, {"id": 13, "value": 11029227, "name": "Georgia"}, {"id": 15, "value": 1435138, "name": "Hawaii"}, {"id": 16, "value": 1964726, "name": "Idaho"}, {"id": 17, "value": 12549689, "name": "Illinois"}, {"id": 18, "value": 6862199, "name": "Indiana"}, {"id": 19, "value": 3207004, "name": "Iowa"}, {"id": 20, "value": 2940546, "name": "Kansas"}, {"id": 21, "value": 4526154, "name": "Kentucky"}, {"id": 22, "value": 4573749, "name": "Louisiana"}, {"id": 23, "value": 1395722, "name": "Maine"}, {"id": 24, "value": 6180253, "name": "Maryland"}, {"id": 25, "value": 7001399, "name": "Massachusetts"}, {"id": 26, "value": 10037261, "name": "Michigan"}, {"id": 27, "value": 5737915, "name": "Minnesota"}, {"id": 28, "value": 2939690, "name": "Mississippi"}, {"id": 29, "value": 6196156, "name": "Missouri"}, {"id": 30, "value": 1132812, "name": "Montana"}, {"id": 31, "value": 1978379, "name": "Nebraska"}, {"id": 32, "value": 3194176, "name": "Nevada"}, {"id": 33, "value": 1402054, "name": "New Hampshire"}, {"id": 34, "value": 9290841, "name": "New Jersey"}, {"id": 35, "value": 2114371, "name": "New Mexico"}, {"id": 36, "value": 19571216, "name": "New York"}, {"id": 37, "value": 10835491, "name": "North Carolina"}, {"id": 38, "value": 783926, "name": "North Dakota"}, {"id": 39, "value": 11785935, "name": "Ohio"}, {"id": 40, "value": 4019800, "name": "Oklahoma"}, {"id": 41, "value": 4233358, "name": "Oregon"}, {"id": 42, "value": 12961683, "name": "Pennsylvania"}, {"id": 44, "value": 1095962, "name": "Rhode Island"}, {"id": 45, "value": 5373555, "name": "South Carolina"}, {"id": 46, "value": 919318, "name": "South Dakota"}, {"id": 47, "value": 7126489, "name": "Tennessee"}, {"id": 48, "value": 30503301, "name": "Texas"}, {"id": 49, "value": 3417734, "name": "Utah"}, {"id": 50, "value": 647464, "name": "Vermont"}, {"id": 51, "value": 8683619, "name": "Virginia"}, {"id": 53, "value": 7812880, "name": "Washington"}, {"id": 54, "value": 1770071, "name": "West Virginia"}, {"id": 55, "value": 5910955, "name": "Wisconsin"}, {"id": 56, "value": 584057, "name": "Wyoming"} ] }, "key": "id", "fields": ["value", "name"] } } ], "projection": {"type": "albersUsa"}, "mark": {"type": "geoshape", "stroke": "white", "strokeWidth": 0.5}, "encoding": { "color": { "field": "value", "type": "quantitative", "scale": {"scheme": "blues"}, "legend": {"title": "Population", "format": ","} }, "tooltip": [ {"field": "name", "type": "nominal", "title": "State"}, {"field": "value", "type": "quantitative", "title": "Population", "format": ","} ] } }

Optional dependency

geometry=True requires geopandas. Install with uv add "pypums[spatial]".


Caching

Cache responses locally with cache_table=True:

df = pypums.get_estimates(
    geography="state",
    product="population",
    vintage=2023,
    cache_table=True,
)

Debugging API calls

Set show_call=True to see the exact Census API URL and parameters:

df = pypums.get_estimates(
    geography="state",
    product="population",
    vintage=2023,
    show_call=True,
)
Census API call: https://api.census.gov/data/2023/pep/population
  Parameters: {'get': 'NAME', 'for': 'state:*', 'key': '...'}

Common patterns

Population change between two vintages

pop_2022 = pypums.get_estimates(
    geography="state",
    product="population",
    vintage=2022,
)
pop_2023 = pypums.get_estimates(
    geography="state",
    product="population",
    vintage=2023,
)

merged = pop_2022.merge(
    pop_2023,
    on=["GEOID", "NAME"],
    suffixes=("_2022", "_2023"),
)
merged["change"] = merged["value_2023"] - merged["value_2022"]
merged["pct_change"] = merged["change"] / merged["value_2022"] * 100
print(merged.nlargest(5, "pct_change")[["NAME", "value_2022", "value_2023", "pct_change"]])
              NAME  value_2022  value_2023  pct_change
            Texas   29527941   30503301        3.30
   South Carolina    5282634    5373555        1.72
          Florida   22244823   22610726        1.65
            Idaho    1939033    1964726        1.33
          Montana    1122867    1132812        0.89

Population by age group for a state

age_groups = pypums.get_estimates(
    geography="state",
    product="characteristics",
    breakdown=["AGEGROUP"],
    breakdown_labels=True,
    vintage=2023,
    state="CA",
)
# Filter to non-overlapping age groups
primary_ages = age_groups[age_groups["AGEGROUP"].isin([1, 2, 3, 4, 5, 6, 7])]
print(primary_ages[["NAME", "AGEGROUP_label", "value"]])
          NAME        AGEGROUP_label      value
  California    Age 0 to 4 years    2285340
  California   Age 5 to 13 years    4812530
  California  Age 14 to 17 years    2012450
  California  Age 18 to 24 years    3521870
  California  Age 25 to 44 years   11234560
  California  Age 45 to 64 years    9542130
  California  Age 65 years and over  6556313

County-level race breakdown

race_data = pypums.get_estimates(
    geography="county",
    product="characteristics",
    breakdown=["RACE", "HISP"],
    breakdown_labels=True,
    state="TX",
    vintage=2023,
)
print(f"{len(race_data)} rows")
print(race_data[["NAME", "RACE_label", "HISP_label", "value"]].head(4))
7620 rows
                         NAME          RACE_label           HISP_label      value
0  Anderson County, Texas       All races   Both Hispanic Origins      57245
1  Anderson County, Texas       All races         Non-Hispanic      43210
2  Anderson County, Texas       All races            Hispanic      14035
3  Anderson County, Texas  White alone  Both Hispanic Origins      39812

Components of change for fast-growing states

components = pypums.get_estimates(
    geography="state",
    product="components",
    vintage=2023,
    output="wide",
    cache_table=True,
)
fastest = components.nlargest(10, "NETMIG")
print(fastest[["NAME", "NATURALCHG", "DOMESTICMIG", "INTERNATIONALMIG"]])
                NAME  NATURALCHG  DOMESTICMIG  INTERNATIONALMIG
            Texas       152345       372456        115890
          Florida       -25412       318745         92534
     North Carolina       25310       135420         28560
          Arizona        18230       102345         15670
   South Carolina         3520        85640         12340
         Georgia        41230        78950         35120
       Tennessee        10250        72560         14230
          Nevada         8910        68540         12780
           Idaho          4820        37280          3540
       Colorado        18950        32450         15670

County population map

county_map = pypums.get_estimates(
    geography="county",
    product="population",
    state="CO",
    vintage=2023,
    geometry=True,
)
import altair as alt

alt.Chart(county_map).mark_geoshape(stroke="white", strokeWidth=0.5).encode(
    color=alt.Color("value:Q", scale=alt.Scale(scheme="yellowgreenblue"), legend=alt.Legend(title="Population")),
    tooltip=["NAME:N", alt.Tooltip("value:Q", format=",")],
).project("albersUsa").properties(width=500, height=400)

See Also