Census Geographies Explained: Blocks, Tracts, Output Areas and Why They Nest

Problem statement

A census table does not describe people. It describes areas, and every number in it only means something alongside three facts about its area: how large the area was designed to be, which larger area contains it, and whether the boundary file you are about to join lists the same set of areas.

Most census work that goes wrong goes wrong at one of those three. Measured on the 2023 American Community Survey (ACS) 5-year release:

  • Not every census area nests. States, counties, tracts, block groups and blocks fit inside one another exactly. Places do not: 1,331 of them cross a county boundary. ZIP Code Tabulation Areas are published with no state or county code at all.
  • "Tract" is a design target, not a size. 95.1% of tracts fall inside the 1,200โ€“8,000 population range they were drawn to, and 849 have nobody living in them.
  • The table and the boundary file are different lists. 336 tract rows in the table have no polygon in the cartographic boundary file, and 141 polygons have no row in the table.

The design itself โ€” a small building block grouped into successively larger reporting areas โ€” is common to most national statistics. England and Wales build Output Areas into LSOAs and MSOAs; the EU layers NUTS regions over local administrative units. The concepts transfer; the names and thresholds change.

Quick answer

The US hierarchy is state โ†’ county โ†’ tract โ†’ block group โ†’ block. Each level's identifier is its parent's identifier plus more digits, so the nesting is visible in the codes, and counting rows by summary level shows the shape of it:

import pandas as pd

acs = pd.read_csv("acsdt5y2023-b01003.dat", sep="|", dtype=str)

levels = {"state": "0400000US", "county": "0500000US",
          "tract": "1400000US", "block group": "1500000US"}
for name, prefix in levels.items():
    rows = acs[acs.GEO_ID.str.startswith(prefix)]
    print(f"{name:12} {len(rows):>8,}")
state              52
county          3,222
tract          85,381
block group   242,296

That is the ACS total-population table B01003, read from the Census Bureau's bulk table-based summary file. The 52 "states" are the 50 states, the District of Columbia and Puerto Rico. Blocks come only from the decennial census; Delaware alone has 20,198.

The working rule: use a level of the nesting hierarchy whenever results must add up to an official total, and treat everything else โ€” places, ZCTAs, districts โ€” as an overlay that cuts across it.

Stack of the five US census levels from state to block, with the 2023 row count and identifier length of each.
Each level adds digits to its parent's code, which is why a prefix test is enough to check the nesting.

Step-by-step solution

1. Learn the five levels and what each is for

Level Identifier Designed population Published in
State 2 digits โ€” everything
County state + 3 = 5 digits โ€” everything
Census tract county + 6 = 11 digits 1,200โ€“8,000, optimum 4,000 ACS and decennial
Block group tract + 1 = 12 digits 600โ€“3,000 ACS (some tables) and decennial
Block tract + 4 = 15 digits none decennial only

The population ranges are the Census Bureau's own, from its geography glossary. A block's four-digit number begins with the digit of its block group, which is why a block group needs only one extra character.

Tracts are the workhorse: small enough to show variation inside a city, large enough that almost every ACS table is published for them. Block groups are finer but thinner โ€” the poverty-by-age table B17001 has 85,381 tract rows and no block-group rows at all.

2. Check that the nesting holds in your files

The design guarantees nesting; a particular release may still surprise you. Test it by prefix:

tracts = acs[acs.GEO_ID.str.startswith("1400000US")].GEO_ID.str[9:]
groups = acs[acs.GEO_ID.str.startswith("1500000US")].GEO_ID.str[9:]
orphans = groups[~groups.str[:11].isin(set(tracts))]
print(len(orphans), orphans.tolist())
2 ['361031460013', '361119544012']

All 85,381 tracts begin with a county code that exists. Two of 242,296 block groups do not begin with a tract that exists, both in New York โ€” Suffolk and Ulster counties โ€” where 15 tracts in the 2023 boundary file have no row in the tract table. The same two are the only counties, of 3,222, whose tract estimates fail to sum to the county estimate. A hierarchy that nests by design still deserves a check by code.

3. Know which geographies sit outside the hierarchy

Several areas people ask for by name are not built from tracts:

geos = pd.read_csv("Geos20235YR.txt", sep="|", dtype=str, encoding="utf-8-sig",
                   usecols=["SUMLEVEL", "COMPONENT", "STATE", "COUNTY", "PLACE"])
pieces = geos[(geos.SUMLEVEL == "155") & (geos.COMPONENT == "00")]
counties_per_place = pieces.groupby(["STATE", "PLACE"]).COUNTY.nunique()
print(f"{(counties_per_place > 1).sum():,} places cross a county boundary")
1,331 places cross a county boundary

Summary level 155, "place within county", exists precisely because places do not nest; New York city alone is split across five counties. ZIP Code Tabulation Areas go further: all 33,774 ZCTA rows in the 2023 geography file have an empty state column, because a ZCTA is assembled from blocks by postal delivery area rather than inside any state or county. Congressional districts, school districts and urban areas are overlays in the same way.

4. Treat blocks as building material, not as statistics

import geopandas as gpd

blocks = gpd.read_file("tl_2020_10_tabblock20.zip",
                       columns=["GEOID20", "POP20", "ALAND20"], ignore_geometry=True)
empty = blocks.POP20 == 0
print(f"{len(blocks):,} blocks, {empty.mean():.1%} with no residents, "
      f"covering {blocks.ALAND20[empty].sum() / blocks.ALAND20.sum():.1%} of the land")
20,198 blocks, 24.2% with no residents, covering 7.7% of the land

A quarter of Delaware's 2020 blocks are empty: medians, parks, industrial sites, shorelines. The occupied ones hold a median of 36 people. Blocks carry decennial counts (POP20, HOUSING20) and are what dasymetric mapping and year-to-year crosswalks are built from, but they are far too small for survey estimates and the ACS does not publish them.

5. Match the level to the question

  • Variation inside a city: tracts; block groups only if the table exists and its margins of error survive.
  • Anything that must reconcile with an official total: a nesting level, aggregated upwards.
  • Catchments, service areas, custom regions: assemble them from tracts or blocks, rather than borrowing places or ZCTAs because their names are familiar.
  • Comparisons across years: confirm the boundaries did not change between the two releases.

6. Translate the idea outside the US

System Smallest unit Grouped into Grouped into Above that
US Census Bureau block block group, 600โ€“3,000 tract, 1,200โ€“8,000 county, state
ONS, England and Wales Output Area, 100โ€“625 LSOA, 1,000โ€“3,000 MSOA, 5,000โ€“15,000 local authority
Eurostat LAU (municipality) NUTS 3, 150,000โ€“800,000 NUTS 2, 800,000โ€“3 million NUTS 1, 3โ€“7 million

The ONS thresholds come from its statistical geographies page and the NUTS ones from the NUTS regulation. England's 2021 geography has 178,605 Output Areas grouped into 33,755 LSOAs and 6,856 MSOAs โ€” about 5.3 Output Areas per LSOA and 4.9 LSOAs per MSOA โ€” and the ONS lookup files name exactly one parent at each level for every Output Area.

Grid comparing the building block and reporting levels of the US census, ONS statistical geographies and the EU NUTS system.
The thresholds differ by an order of magnitude, but every system nests small units into larger ones.

Code examples

Example 1 โ€” split a summary file into hierarchy levels

import pandas as pd

LEVELS = {
    "state": ("0400000US", 2),
    "county": ("0500000US", 5),
    "tract": ("1400000US", 11),
    "block group": ("1500000US", 12),
}


def geographies_in_table(path, value_column, levels=LEVELS):
    """Split a table-based summary file into one Series per hierarchy level."""
    table = pd.read_csv(path, sep="|", dtype=str, usecols=["GEO_ID", value_column])
    out = {}
    for name, (prefix, width) in levels.items():
        rows = table[table.GEO_ID.str.startswith(prefix)]
        ids = rows.GEO_ID.str[len(prefix):]
        wrong_width = int((ids.str.len() != width).sum())
        out[name] = pd.Series(pd.to_numeric(rows[value_column]).values, index=ids.values)
        print(f"{name:12} {len(ids):>8,} rows, {wrong_width} ids not {width} characters")
    return out
state              52 rows, 0 ids not 2 characters
county          3,222 rows, 0 ids not 5 characters
tract          85,381 rows, 0 ids not 11 characters
block group   242,296 rows, 0 ids not 12 characters

Matching the full prefix matters. Filtering on the summary-level digits 040 alone returns 603 "state" rows instead of 52, because the file also carries geographic components โ€” "Alabama -- In metropolitan statistical area", "Alabama -- In micropolitan statistical area" and so on โ€” whose GEO_ID differs only in characters six and seven.

Example 2 โ€” test nesting and additivity together

def check_nesting(children, parents, prefix_len, label):
    """Every child id should start with an existing parent id."""
    child_ids = pd.Series(children.index, dtype=str)
    parent_of = child_ids.str[:prefix_len]
    orphans = child_ids[~parent_of.isin(set(parents.index))]
    per_parent = parent_of.value_counts()
    print(f"{label}: {len(orphans)} orphans; children per parent "
          f"median {per_parent.median():.0f}, max {per_parent.max():,}")
    sums = children.groupby(parent_of.values).sum()
    mismatched = (sums.reindex(parents.index) != parents)
    print(f"  parents whose children do not sum to them: {int(mismatched.sum())}")
    return orphans.tolist(), parents.index[mismatched].tolist()
block group -> tract: 2 orphans; children per parent median 3, max 10
  parents whose children do not sum to them: 0
tract -> county: 0 orphans; children per parent median 8, max 2,498
  parents whose children do not sum to them: 2
county -> state: 0 orphans; children per parent median 63, max 254
  parents whose children do not sum to them: 0

The 2,498-tract county is Los Angeles County; the 254-county state is Texas. The additivity check is the stronger of the two tests, because it catches rows that are missing rather than rows that are mislabelled.

Example 3 โ€” compare areas with the size they were designed for

def size_against_design(population, low, high, label):
    """How closely a set of areas meets the population range it was drawn to."""
    inside = population.between(low, high).mean()
    below = (population < low).mean()
    above = (population > high).mean()
    empty = int((population == 0).sum())
    q = population.quantile([0.05, 0.5, 0.95]).round().astype(int).tolist()
    print(f"{label}: {inside:.1%} inside {low:,}-{high:,}, {below:.1%} below, "
          f"{above:.1%} above, {empty:,} empty; p5/median/p95 = {q}")
tracts: 95.1% inside 1,200-8,000, 2.8% below, 2.0% above, 849 empty; p5/median/p95 = [1495, 3751, 6962]
block groups: 87.5% inside 600-3,000, 9.4% below, 3.1% above, 2,147 empty; p5/median/p95 = [477, 1247, 2724]

Run it before choosing a level for a rate map. The empty and near-empty areas are the ones that will produce rates of 0% and 100%.

Explanation

Why the hierarchy is built from the bottom

Blocks are drawn first, from visible features โ€” streets, rivers, railways โ€” and every higher statistical level is a union of blocks. That is what makes the nesting exact rather than approximate: a tract boundary is a chain of block edges, and in the Bureau's words, "State and county boundaries always are census tract boundaries".

It also explains the empty blocks. A block is defined by the features around it, not by who lives in it, so a motorway interchange is as much a block as a street of houses โ€” hence 24.2% of Delaware's blocks with no residents.

Why tracts are sized by population, not by area

A tract is meant to hold a similar number of people everywhere, so that an estimate for it has similar precision everywhere. The price is that tract areas vary enormously. Measured across 84,532 populated tracts, population density runs from 1.4 people per kmยฒ at the 1st percentile to 23,887 at the 99th.

The population design holds up well. The median tract has 3,751 people, close to the 4,000 optimum, and 95.1% sit inside the range. The empty tail is partly deliberate: at least 335 of the 849 empty tracts are water-only tracts, coded in the 99xxxx range.

Why block groups are less dependable than their size suggests

A block group holds a median of 1,247 people, a third of a tract, and the ACS samples only a fraction of households. Smaller samples mean wider margins of error: the median coefficient of variation of the total-population estimate is 18.6% for block groups against 8.4% for tracts. Some tables are not published at block-group level at all.

Block groups also meet their design range less often โ€” 87.5% against 95.1% for tracts โ€” because they are carved out of tracts that were themselves sized with some slack.

Why the table and the boundary file disagree

The table lists every tabulated area; the cartographic boundary file is generalised and clipped for mapping, and it covers territory the survey does not.

Measured against cb_2023_us_tract_500k, the 336 table rows without a polygon all have zero population, and 335 of them carry 99xxxx water-tract codes. The 141 polygons without a row are 126 tracts in Guam, the US Virgin Islands, the Northern Mariana Islands and American Samoa, which the ACS does not survey, plus the 15 New York tracts from step 2. A left join from boundaries to table therefore always leaves some empty polygons, and an inner join always drops some table rows; neither is a bug in your code.

Two panels contrasting geographies that nest inside counties with overlays such as places and ZCTAs that cut across them.
An overlay can still be tabulated โ€” it just cannot be summed into, or out of, the hierarchy.

Edge cases or notes

  • Puerto Rico is in the ACS; the island areas are not. That is why there are 52 "states" in the table and 126 unmatched polygons in the boundary file.
  • Tract numbers repeat across counties. Tract code 950100 is used in 312 counties and 020100 in 78; only the full 11-digit GEOID is unique.
  • Filter summary levels on the full prefix. 0400000US returns 52 states; the first three characters alone return 603 rows, including the metropolitan and micropolitan parts of each state.
  • Not every table exists at every level. B17001 has tract rows and no block-group rows.
  • Blocks are decennial only. For ACS work the smallest level is the block group, and only for some tables.
  • Water-only tracts are real rows. They have codes from 990000 upwards, zero population, and often no polygon in a generalised boundary file.
  • Nesting holds within one vintage. Tracts are redrawn after each decennial census, so a 2019 tract and a 2023 tract with the same code may not be the same area.
  • ONS codes carry a letter prefix. E00 for Output Areas, E01 for LSOAs, E02 for MSOAs โ€” which also spares them the leading-zero trap US codes fall into.

FAQ

What is the difference between a census tract and a block group?

A block group is a subdivision of a tract, designed for 600โ€“3,000 people against a tract's 1,200โ€“8,000. The median tract in the 2023 ACS has 3,751 people and three block groups.

Do census tracts cross county lines?

No. Tracts nest within counties and states by definition, and all 85,381 tracts in the 2023 table begin with an existing county code.

Are ZIP codes census geographies?

Not directly. The Census Bureau publishes ZIP Code Tabulation Areas, built from blocks to approximate postal areas, and they sit outside the stateโ€“countyโ€“tract hierarchy; the 2023 geography file lists all 33,774 with no state code.

What is the UK equivalent of a census tract?

The closest in size is the MSOA, designed for 5,000โ€“15,000 people; the LSOA, at 1,000โ€“3,000, is closer to a block group. Both are built from Output Areas, as US levels are built from blocks.

Why does my boundary file have more tracts than my data table?

Because the files cover different territory. The 2023 cartographic tract file includes 126 tracts in island areas the ACS does not survey, and the table includes 336 water-only or empty tracts the boundary file omits.

How many blocks are there in a tract?

It varies with the landscape. In Delaware's 2020 geography the median tract contains 66.5 blocks, and a quarter of all blocks have no residents.