This vignette explains how urbnindicators translates raw
ACS data into analysis-ready output. It covers:
- How ACS tables are structured at the Census Bureau
- How the package selects and renames variables
- How percentages are calculated
- What the output looks like and how to interpret it
How ACS Tables Are Structured
The Census Bureau organizes ACS data into thousands of
tables, each identified by a code like
B22003 or B01001. Each table contains a set of
numbered variables—B22003_001, B22003_002,
etc.—that report counts for specific subgroups.
Tables have a hierarchical label structure. For example, table B22003 (Receipt of Food Stamps/SNAP) looks like:
| Variable | Label | Role |
|---|---|---|
| B22003_001 | Total | Universe (all households) |
| B22003_002 | Received Food Stamps/SNAP | Subgroup count |
| B22003_003 | … with 1 or more persons with a disability | Sub-subgroup |
| B22003_004 | … with no persons with a disability | Sub-subgroup |
| B22003_005 | Did not receive Food Stamps/SNAP | Subgroup count |
The first variable (_001) is always the table
total, or universe—the count of all observations to
which the table applies. Subsequent variables break down the total into
subcategories, sometimes nested several levels deep.
Table prefixes
The Census Bureau uses prefixes to distinguish table types:
- B (Detailed Tables): The most granular tabulations, with the full set of categories.
- C (Collapsed Tables): Simplified versions of B tables with fewer categories. A given C table typically mirrors a B table but groups some subcategories together.
-
S (Subject Tables): Pre-computed summaries, often
including percentages and medians. These are not used by
urbnindicatorsdirectly; the package computes its own percentages from B and C tables.
urbnindicators works with B and C tables. You can browse
the full Census Bureau variable catalog with
get_acs_codebook():
get_acs_codebook() %>%
filter(table == "B22003") %>%
head(6)
#> # A tibble: 6 × 3
#> table variable_raw variable_clean
#> <chr> <chr> <chr>
#> 1 B22003 B22003_001 receipt_food_stamps/snap_in_past_12_months_by_poverty_sta…
#> 2 B22003 B22003_002 receipt_food_stamps/snap_in_past_12_months_by_poverty_sta…
#> 3 B22003 B22003_003 receipt_food_stamps/snap_in_past_12_months_by_poverty_sta…
#> 4 B22003 B22003_004 receipt_food_stamps/snap_in_past_12_months_by_poverty_sta…
#> 5 B22003 B22003_005 receipt_food_stamps/snap_in_past_12_months_by_poverty_sta…
#> 6 B22003 B22003_006 receipt_food_stamps/snap_in_past_12_months_by_poverty_sta…Two Paths: Registered Tables and Auto-Processed Tables
compile_acs_data() supports two kinds of table inputs,
each processed differently.
Registered tables
These are tables that the package has pre-built definitions for. They
have curated variable names, carefully chosen denominators, and tested
percentage calculations. Use list_tables() to see what’s
available:
list_tables()
#> [1] "age" "computing_devices"
#> [3] "cost_burden" "disability"
#> [5] "educational_attainment" "employment"
#> [7] "gini" "health_insurance"
#> [9] "household_size" "income_quintiles"
#> [11] "internet" "language"
#> [13] "median_household_income" "median_housing_cost"
#> [15] "median_income_by_tenure" "mortgage_status"
#> [17] "nativity" "occupants_per_room"
#> [19] "owner_cost_burden" "population_density"
#> [21] "poverty" "public_assistance"
#> [23] "race" "school_enrollment"
#> [25] "sex" "snap"
#> [27] "tenure" "tenure_by_housing_costs"
#> [29] "tenure_by_units_in_structure" "total_population"
#> [31] "transportation_to_work" "travel_time_to_work"
#> [33] "units_in_structure" "vehicles_available"
#> [35] "year_structure_built"When you request a registered table by name (e.g.,
tables = "snap"), the package knows exactly which ACS
variables to fetch and how to calculate derived measures from them.
Auto-processed tables
Any valid ACS table code can be passed directly (e.g.,
tables = "B25070"). The table is
auto-processed: all of its variables are fetched, names
are generated algorithmically from the Census Bureau’s labels, and
percentages are computed based on the table’s label hierarchy. This
holds even when a registered table covers the same code —
tables = "B22003" returns the auto-processed table, while
tables = "snap" returns the registered version. (Requesting
both in the same call returns the registered version and drops the
auto-processed one with a warning, since the two would otherwise fetch
duplicate variables.)
Auto-processed tables are a flexible route for the thousands of ACS
tables that urbnindicators doesn’t have pre-built
definitions for. The tradeoff is that variable names tend to be longer
and less polished (they’re derived mechanically from Census labels).
How Variables Are Selected and Renamed
Registered tables
For registered tables, the package maintainers have manually identified constructs that are widely used in analyses, vetted how to calculate these constructs, and simplified the resulting variable names to follow a concise and consistent structure:
| ACS code |
urbnindicators name |
|---|---|
| B22003_001 | snap_universe |
| B22003_002 | snap_received |
These names follow the pattern:
[concept]_[subconcept]_[characteristic]_[metric].
Some registered tables use pattern-based selection
instead of listing variables one by one. For example, the
sex_by_age table selects all variables matching the pattern
B01001_, which pulls every variable in table B01001 and
renames them by parsing the Census Bureau’s hierarchical labels into
snake_case.
Auto-processed tables
For auto-processed tables, variable names are generated by converting
the Census Bureau’s concept and label fields
to snake_case. For example, a variable from table B25070 with concept
“GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME IN THE PAST 12 MONTHS”
and label “Less than 10.0 percent” becomes:
gross_rent_as_a_percentage_household_income_in_past_12_months_less_than_10_0_pct
(Common connector words like “of” and “the” are collapsed, and a
trailing “percent” becomes pct so that computed percentage
variables–which end in _percent–remain distinguishable from
raw ACS variables that happen to measure percentages.)
These names are functional but verbose. To see what names a table
will produce before pulling data, you can use
get_acs_codebook():
get_acs_codebook(table = "B25070")
#> # A tibble: 11 × 3
#> table variable_raw variable_clean
#> <chr> <chr> <chr>
#> 1 B25070 B25070_001 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 2 B25070 B25070_002 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 3 B25070 B25070_003 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 4 B25070 B25070_004 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 5 B25070 B25070_005 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 6 B25070 B25070_006 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 7 B25070 B25070_007 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 8 B25070 B25070_008 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 9 B25070 B25070_009 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 10 B25070 B25070_010 gross_rent_as_a_percentage_household_income_in_past_12_m…
#> 11 B25070 B25070_011 gross_rent_as_a_percentage_household_income_in_past_12_m…How Percentages Are Calculated
Registered tables
Registered tables define their percentages explicitly. Each table’s
definitions specify which variable(s) form the numerator, which form the
denominator, and what the output variable should be called. For example,
snap_received_percent is documented in the codebook as:
Numerator = snap_received (B22003_002). Denominator = snap_universe (B22003_001).
Some percentage calculations are more complex. For example,
disability_percent sums numerous sex-by-age-by-disability
variables into a single numerator, then divides by the table
universe.
All percentage variables end with _percent.
Auto-processed tables
For auto-processed tables, the package builds a label hierarchy tree from the Census Bureau’s ACS codebook. Each variable’s “parent” is determined by its position in the hierarchy:
| Variable | Label | Depth | Parent |
|---|---|---|---|
| B25070_001 | Total | 1 | (none—this is the universe) |
| B25070_002 | Less than 10.0 percent | 2 | B25070_001 |
| B25070_003 | 10.0 to 14.9 percent | 2 | B25070_001 |
The denominator parameter on
compile_acs_data() controls which variable is used as the
denominator for auto-computed percentages:
-
"parent"(default): Each variable is divided by its nearest parent subtotal in the label hierarchy. For flat tables (like B25070, where all variables are direct children of the total), this is equivalent to dividing by the table total. For nested tables with subtotals, this produces within-group percentages. -
"total": Every variable is divided by the table total (_001), producing shares of the overall population regardless of hierarchy. -
A specific ACS variable code (e.g.,
"B25070_001"): Every variable is divided by that variable.
To illustrate: consider a table with structure Total > Male > Male 18-24, Male 25-34, … and Total > Female > Female 18-24, Female 25-34, …
With denominator = "parent", “Male 18-24” would be
divided by “Male” (its parent subtotal), yielding the share of
males in that age group. With denominator = "total",
“Male 18-24” would be divided by “Total”, yielding the share of all
adults.
The package skips percentage computation for tables that contain medians, aggregates, averages, or other non-count measures.
Understanding the Output
compile_acs_data() returns a wide-format dataframe with
one row per geography per year. Here’s what the columns look like:
df = compile_acs_data(
tables = "snap",
years = 2024,
geography = "county",
states = "DC")
names(df)
#> [1] "data_source_year" "GEOID"
#> [3] "NAME" "total_population_universe"
#> [5] "snap_universe" "snap_received"
#> [7] "snap_received_percent" "total_population_universe_M"
#> [9] "snap_universe_M" "snap_received_M"
#> [11] "snap_received_percent_M"Column types
The output contains several types of columns, identifiable by their naming conventions:
| Pattern | Type | Example | Description |
|---|---|---|---|
GEOID |
Identifier | "11001" |
FIPS code for the geography |
NAME |
Identifier | "District of Columbia" |
Human-readable name |
data_source_year |
Identifier | 2024 |
The ACS release year (end year of the 5-year window) |
*_universe |
Count | snap_universe |
Table total / denominator population |
| (no suffix) | Count | snap_received |
Raw ACS estimate |
*_percent |
Derived | snap_received_percent |
Percentage (0–1 scale) |
*_M |
MOE | snap_received_M |
Margin of error (90% confidence) |
Every raw count variable has a corresponding _M column
with its margin of error, sourced directly from the Census Bureau. Every
derived percentage variable also has a _M column, with the
margin of error calculated by urbnindicators using Census
Bureau-recommended formulas for pooling errors.
The codebook
Every dataframe returned by compile_acs_data() carries a
codebook as an attribute. The codebook documents how each variable was
calculated:
attr(df, "codebook")
#> # A tibble: 11 × 10
#> calculated_variable variable_type definition numerator_vars
#> <chr> <chr> <chr> <list>
#> 1 total_population_universe Count This is a raw ACS … <chr [0]>
#> 2 snap_universe Count This is a raw ACS … <chr [0]>
#> 3 snap_received Count This is a raw ACS … <chr [0]>
#> 4 snap_received_percent Percent Numerator = snap_r… <chr [1]>
#> 5 data_source_year Metadata End year of five-y… <chr [0]>
#> 6 GEOID Metadata A federally-issued… <chr [0]>
#> 7 NAME Metadata The name of the ge… <chr [0]>
#> 8 area_land_sq_kilometer Metadata Land area of the g… <chr [0]>
#> 9 area_water_sq_kilometer Metadata Water area of the … <chr [0]>
#> 10 area_land_water_sq_kilometer Metadata Combined land and … <chr [0]>
#> 11 geometry Metadata The spatial geomet… <chr [0]>
#> # ℹ 6 more variables: numerator_subtract_vars <list>, denominator_vars <list>,
#> # denominator_subtract_vars <list>, se_calculation_type <chr>,
#> # aggregation_strategy <chr>, universe <chr>The definition column is especially important: raw
ACS variables read "This is a raw ACS estimate.", while
derived variables spell out their inputs alongside the original ACS
codes (e.g.,
"Numerator = snap_received (B22003_002). Denominator = snap_universe (B22003_001).").
The codebook’s component list-columns (numerator_vars,
denominator_vars, and their _subtract_
counterparts) drive the pooled margin-of-error calculations, so they
always reflect the actual calculation.
GEOIDs
The GEOID column contains FIPS codes whose structure
depends on the geography level:
| Geography | GEOID format | Example | Components |
|---|---|---|---|
| State | 2 digits | "11" |
State |
| County | 5 digits | "11001" |
State + County |
| Tract | 11 digits | "11001001002" |
State + County + Tract |
| Place | 7 digits | "1150000" |
State + Place |
You can parse these to join with other datasets. For example,
substr(GEOID, 1, 2) extracts the state code from any
sub-state geography.
Discovering Available Data
urbnindicators provides several functions for exploring
what data is available:
| Function | Purpose | Requires API key? |
|---|---|---|
list_tables() |
Names of all registered tables | No* |
list_variables() |
Every variable mapped to its table | Yes |
get_acs_codebook() |
Browse all ACS variables with clean names | Yes |
*list_tables(geography = "block group") also requires a
key, because block-group availability is read from the ACS variable
dictionary. The Census Bureau’s variables metadata endpoint requires an
API key, so any function that loads the dictionary needs
CENSUS_API_KEY set (see
tidycensus::census_api_key("YOUR_KEY", install = TRUE)).
A typical discovery workflow:
-
Start with
list_tables()to see the registered tables. -
Use
list_variables()to preview what a specific table will produce:list_variables() %>% filter(table == "snap"). -
If your topic isn’t covered, search the full ACS
catalog:
get_acs_codebook() %>% filter(str_detect(variable_clean, "poverty")). -
Pass the table code directly to
compile_acs_data():compile_acs_data(tables = "B17001", ...).
Registered Table Reference
The table below lists all pre-built tables available via
list_tables().