# install
install.packages("tidycensus") # accessing Census data
install.packages("tidyverse") # cleaning data
install.packages("janitor") # cleaning data
install.packages("gt") # creating tablesAcessing Census Data in R
Introduction
The following vignette provides a guide for accessing United States Census Bureau data directly through R. The primary R package for doing so is tidycensus, developed by Walker & Herman (2024).
tidycensus allows users to quickly download and work with Census data entirely within R. Data are automatically formatted into dataframes, making them compatible with popular data science packages like tidyverse. Additionally, users can request spatial data (e.g., Census tract boundaries) all through the same code, making these data compatible with packages like sf that allow for spatial mapping and analysis.
R Packages
You will need the following R packages. If you have already installed them, skip the install.packages lines.
# load
library(tidycensus)
library(tidyverse)
library(janitor)
library(gt)Census Application Programming Interface
tidycensus accesses Census data through the Census Bureau’s Application Programming Interface (API). An API is a way for one computer program to request data from another program. In this case, R sends a request to the Census Bureau and receives the requested data directly. To make these requests, however, you will need a Census API Key. API keys are free and help the Census Bureau monitor and manage API usage.
You can request a free API key from the Census Bureau here. This link will bring you to the following page:
You will be asked to provide an Organization Name and Email Address. The former can be your place of work or academic institution, or simply your name or “Individual” or “Personal.” Once entered, click REQUEST KEY and the Census Bureau will email you your API key (~5 minutes). The key will be a long alphanumeric string (~40 characters). Don’t worry, you only need to install it once!
To install you API key, use the census_api_key function (part of the tidycensus package).
# install Census API key
census_api_key(key = "YOUR_KEY_HERE", install = TRUE)Replace "YOUR_KEY_HERE" with the key provided in the email. By setting install = TRUE, you are telling R to save your key to your local R environment. This prevents you from needing to enter the key each time you use tidycensus.
After installing the key, restart R (or restart your R session). Once R is reloaded, you can check whether your API key was installed by running Sys.getenv("CENSUS_API_KEY"), which will print your API key in the R Console.
Sys.getenv("CENSUS_API_KEY")Once your API key is installed, you can run tidycensus functions normally, without specifying an API key each time. tidycensus will find your API key within your local R environment when functions like load_variables, get_acs, and get_decennial are used.
Finding Variables
The first part of working with Census data is identifying the variable(s) you want. The Census Bureau organizes data into tables based on broad concepts. Each table then contains several variables that represent different subsets of that concept.
For example, a table may contain the concept of Total Population, while nested variables within that table provide estimates for the total population at different age brackets (e.g., under 5, 5 to 9, and so forth).
To explore what data are available, run the load_variables function. This function works equivalently for both Decennial Census data and American Community Survey (ACS) data, returning a searchable dataframe with the variables available for that particular Census dataset and year.
# Decennial Census
decennial_pl_vars <- load_variables(year = 2020, dataset = "pl") # 2020 Redistricting Data
decennial_dhc_vars <- load_variables(year = 2020, dataset = "dhc") # 2020 Demographic & Housing Characteristics
decennial_dp_vars <- load_variables(year = 2020, dataset = "dp") # 2020 Demographic Profile
# American Community Survey
acs1_2022_vars <- load_variables(year = 2022, dataset = "acs1") # 2022 ACS 1-year
acs5_2022_vars <- load_variables(year = 2022, dataset = "acs5") # 2022 ACS 5-yearYou can then open and inspect these dataframes by running View (as below), or by double-clicking on the object in the Data Environment.
View(decennial_pl_vars)
View(acs1_2022_vars)Included in these dataframes are the name, concept, and label associated with each variable.
name: internal ID used by the Census Bureau to uniquely identify each variableconcept: broader measurement concept (i.e., table) to which the variable belongslabel: description of the specific variable (i.e., the variable nested withinconcept)
Downloading
Once you know which variables you want, tidycensus provides several functions for downloading data. Two of the most commonly used are get_decennial (Decennial Census) and get_acs (American Community Survey). Both functions return dataframes that can be immediately used with tidyverse (e.g., dplyr, ggplot2).
Function Format
It is helpful to understand the basic structure of a tidycensus function. Although some functions may include different arguments, the same basic structure applies to each.
A typical request looks like this:
acs_data <- get_acs(geography = "county",
variables = "B01003_001",
year = 2022,
survey = "acs5",
state = "WA",
county = "King")Where the arguments collectively tell R what data to retrieve, where to retrieve it from, and how to return it.
| Argument | What it specifies | Example |
|---|---|---|
geography |
Geographic scale for the data | "state", "county", "tract" |
variables |
One or more Census variables | "B01003_001" |
table |
An entire Census table | "B01001" |
year |
Year of data | 2022 |
state |
State where geography is located |
"WA" or "53" |
county |
County where geography is located |
"King" or "033" |
survey |
American Community Survey product | "acs1" or "acs5" |
sumfile |
Decennial Census product | "pl" or "dhc" |
geometry |
Whether to return spatial data for geography |
TRUE or FALSE |
geography: what geographic scale do you want data at?
- For example, retrieve Census tract data using
geography = "tract" - The available geographic levels depend on the Census dataset you are using
variables vs. table: what data are you requesting?
- Census data are organized into tables with individual variables nested inside
- A table acts as the parent container; variables are the specific, nested rows inside that container
- For instance, table
B19001contains all Household Income variables, while variableB19001_002is households making under $10,000
- For instance, table
- A table acts as the parent container; variables are the specific, nested rows inside that container
- Note that the variables belonging to a table will always carry the same prefix (e.g.,
B19001is income andB19001_002is a specific income bracket)
The following example demonstrates the difference between these two approaches using the Sex-by-Age Census data.
The first request selects the B01001_003 variable, which records the estimated number of males under age five:
# using `variables` argument
acs_var <- get_acs(geography = "county",
variables = "B01001_003", # sex-by-age (males under 5)
year = 2022,
survey = "acs5",
state = "WA",
county = "King")The second request retrieves the entire B01001 table, which includes the B01001_003 variable in addition to all other variables within the table:
# using `table` argument
acs_tab <- get_acs(geography = "county",
table = "B01001", # sex-by-age (all variables)
year = 2022,
survey = "acs5",
state = "WA",
county = "King")If we compare the output from these two requests, the difference becomes apparent: acs_var contains only the single variable we requested, whereas acs_tab contains all of the variables included in the B01001 table. Notice that in the acs_tab output, we see the B01001_003 variable and estimate in the third row, which matches that of the acs_var output.
acs_var |>
select(variable, estimate) |>
gt()| variable | estimate |
|---|---|
| B01001_003 | 62532 |
acs_tab |>
select(variable, estimate) |>
head(10) |> # print first 10 rows
gt()| variable | estimate |
|---|---|
| B01001_001 | 2254371 |
| B01001_002 | 1143593 |
| B01001_003 | 62532 |
| B01001_004 | 64195 |
| B01001_005 | 63784 |
| B01001_006 | 37244 |
| B01001_007 | 25041 |
| B01001_008 | 12383 |
| B01001_009 | 11862 |
| B01001_010 | 45530 |
state and county: what is your geographic region of interest?
- Note that
tidycensusfunctions allow you to identify states and counties by either name or FIPS - For example,
state = "WA"andstate = "53"reference the same state (i.e., Washington), whilecounty = "King"andcounty = "033"both reference King County.
year: what year of data do you want?
- The meaning of
yeardepends on the dataset- For ACS data,
yearis the year of release- The 2018-2022 5-year estimates are referenced by
year = 2022
- The 2018-2022 5-year estimates are referenced by
- For Decennial data,
yearis the Census year- The 2020 Decennial Census is called with
year = 2020
- The 2020 Decennial Census is called with
- For ACS data,
survey vs. sumfile: which data product are you requesting?
surveyis used withget_acssumfileis used withget_decennial
Note that these arguments are not interchangeable. When working with get_acs, the survey argument specifies which ACS product you want to use, such as the ACS 1-year or 5-year estimates. When working with get_decennial, the sumfile argument specifies which Decennial Census data product you want to use, such as the 2020 Redistricting Data or Demographic & Housing Characteristics File.
geometry: do you want spatial data to be included in your download?
- When
geometry = TRUE, the geographic boundaries of your selectedgeographyare returned in ageometrycolumn, and the resulting dataframe is ansfobject- The default is
geometry = FALSE
- The default is
- Spatial data are particularly useful when you want to map Census estimates or conduct spatial analysis using packages such as
sf
Putting it Together
Once you understand the individual arguments, a tidycensus request becomes easier to read.
For example:
acs_data <- get_acs(geography = "county",
variables = "B01003_001", # total population
year = 2022,
survey = "acs5", # `survey` because using `get_acs`
state = "WA",
county = "King",
geometry = FALSE)can be read as, return the 2022 ACS 5-year estimate for the total population of King County, Washington, without geographic boundaries.
Similarly:
dec_data <- get_decennial(geography = "county",
variables = "P1_001N", # total population
year = 2020,
sumfile = "pl", # `sumfile` because using `get_decennial`
state = "WA",
county = "King",
geometry = FALSE)tells R to retrieve the 2020 Decennial Census estimate for the total population of King County, Washington, from the Redistricting Data, without geographic boundaries.
Cleaning
A useful step to take immediately after downloading Census data is to clean it. Though tidycensus returns Census data in a neat dataframe, the variable names are often a cryptic string of letters and numbers and the dataframe is in long format.
Renaming
If we call the variable column from the above two requests, we see that each variable’s name is still stored as the ID that tidycensus used to identify and request it.
print(acs_data$variable)[1] "B01003_001"
print(dec_data$variable)[1] "P1_001N"
While internally consistent, these IDs are not intuitive to most users, making it difficult to immediately understand what each variable represents without referring back to the Census variable documentation.
Fortunately, adjusting these variable names is straightforward. Recall that the load_variables function provides metadata for each Census variable, including a description (label) for what it actually measures. These descriptions can be joined with our requested Census data so that our cleaned dataframe has both the variable ID (e.g., B01003_001) and label (e.g., Total Population).
Begin by running the get_acs (or get_decennial) and load_variables functions.
# ACS data request
acs_data <- get_acs(geography = "county",
table = "B01001A", # total population table (sex-by-age)
year = 2022,
survey = "acs5",
state = "WA",
county = "King",
geometry = FALSE)
# ACS variable metadata
acs_vars <- load_variables(year = 2022, dataset = "acs5")Be sure that year and survey (or sumfile) correspond with year and dataset, respectively.
Notice that the metadata (acs_vars) contain several columns, not all of which we need.
# ACS metadata columns
print(colnames(acs_vars))[1] "name" "label" "concept" "geography"
We are interested in label because it provides a description of each variable. However, we also need a way to pair these descriptions with our estimates data (acs_data). For that, we will use the name column, which reports the unique alphanumeric ID for each variable.
The geography column is simply the finest spatial scale at which each variable is available, and the concept column notes the broader measurement concept (i.e., the table) to which each variable belongs. Neither of these are necessary at this stage and so we remove them for simplicity:
# select the desired columns
acs_vars <- acs_vars |>
select(name, label)Next, to join acs_vars and acs_data, we will use the left_join function. The unique identifier available in each dataframe are the variables’ alphanumeric ID. Hence, this is what the join will be based on.
Because this alphanumeric ID is stored in the estimate data as variable and in the metadata as name, we set by = c("variable" = "name")
# join metadata into estimates data
acs_data <- acs_data |>
left_join(acs_vars, by = c("variable" = "name"))After joining, we see that acs_data now contains a label column, which we can use to identify what each variable measures.
acs_data |>
select(estimate, label) |>
gt()| estimate | label |
|---|---|
| 1325673 | Estimate!!Total: |
| 675388 | Estimate!!Total:!!Male: |
| 29018 | Estimate!!Total:!!Male:!!Under 5 years |
| 30760 | Estimate!!Total:!!Male:!!5 to 9 years |
| 31971 | Estimate!!Total:!!Male:!!10 to 14 years |
| 19024 | Estimate!!Total:!!Male:!!15 to 17 years |
| 12628 | Estimate!!Total:!!Male:!!18 and 19 years |
| 35206 | Estimate!!Total:!!Male:!!20 to 24 years |
| 54710 | Estimate!!Total:!!Male:!!25 to 29 years |
| 61696 | Estimate!!Total:!!Male:!!30 to 34 years |
| 106723 | Estimate!!Total:!!Male:!!35 to 44 years |
| 97004 | Estimate!!Total:!!Male:!!45 to 54 years |
| 93677 | Estimate!!Total:!!Male:!!55 to 64 years |
| 64650 | Estimate!!Total:!!Male:!!65 to 74 years |
| 26546 | Estimate!!Total:!!Male:!!75 to 84 years |
| 11775 | Estimate!!Total:!!Male:!!85 years and over |
| 650285 | Estimate!!Total:!!Female: |
| 27307 | Estimate!!Total:!!Female:!!Under 5 years |
| 28236 | Estimate!!Total:!!Female:!!5 to 9 years |
| 30653 | Estimate!!Total:!!Female:!!10 to 14 years |
| 17957 | Estimate!!Total:!!Female:!!15 to 17 years |
| 12935 | Estimate!!Total:!!Female:!!18 and 19 years |
| 34327 | Estimate!!Total:!!Female:!!20 to 24 years |
| 49923 | Estimate!!Total:!!Female:!!25 to 29 years |
| 54042 | Estimate!!Total:!!Female:!!30 to 34 years |
| 94048 | Estimate!!Total:!!Female:!!35 to 44 years |
| 87132 | Estimate!!Total:!!Female:!!45 to 54 years |
| 89377 | Estimate!!Total:!!Female:!!55 to 64 years |
| 69304 | Estimate!!Total:!!Female:!!65 to 74 years |
| 35064 | Estimate!!Total:!!Female:!!75 to 84 years |
| 19980 | Estimate!!Total:!!Female:!!85 years and over |
Optionally, we can do a few more things to clean these data.
acs_data <- acs_data |>
# convert column names to snake_case
clean_names() |>
mutate(
# create a `sex` column that indicates whether the estimate is for Men or Women
sex = ifelse(grepl("Female", label, fixed = TRUE), "female", "male"),
# remove "!!"
label = gsub("!!", " ", label),
# with a `sex` column, labels no longer need "Male" and "Female"
label = gsub("Total: Male: ", "", label),
label = gsub("Total: Female: ", "", label),
# remove remaining colons (":")
label = gsub(":", "", label)
) |>
# separates the `name` variable into `county` and `state`
separate(name, into = c("county", "state"), sep = ", ")
acs_data |>
select(county, state, estimate, label, sex) |>
gt()| county | state | estimate | label | sex |
|---|---|---|---|---|
| King County | Washington | 1325673 | Estimate Total | male |
| King County | Washington | 675388 | Estimate Total Male | male |
| King County | Washington | 29018 | Estimate Under 5 years | male |
| King County | Washington | 30760 | Estimate 5 to 9 years | male |
| King County | Washington | 31971 | Estimate 10 to 14 years | male |
| King County | Washington | 19024 | Estimate 15 to 17 years | male |
| King County | Washington | 12628 | Estimate 18 and 19 years | male |
| King County | Washington | 35206 | Estimate 20 to 24 years | male |
| King County | Washington | 54710 | Estimate 25 to 29 years | male |
| King County | Washington | 61696 | Estimate 30 to 34 years | male |
| King County | Washington | 106723 | Estimate 35 to 44 years | male |
| King County | Washington | 97004 | Estimate 45 to 54 years | male |
| King County | Washington | 93677 | Estimate 55 to 64 years | male |
| King County | Washington | 64650 | Estimate 65 to 74 years | male |
| King County | Washington | 26546 | Estimate 75 to 84 years | male |
| King County | Washington | 11775 | Estimate 85 years and over | male |
| King County | Washington | 650285 | Estimate Total Female | female |
| King County | Washington | 27307 | Estimate Under 5 years | female |
| King County | Washington | 28236 | Estimate 5 to 9 years | female |
| King County | Washington | 30653 | Estimate 10 to 14 years | female |
| King County | Washington | 17957 | Estimate 15 to 17 years | female |
| King County | Washington | 12935 | Estimate 18 and 19 years | female |
| King County | Washington | 34327 | Estimate 20 to 24 years | female |
| King County | Washington | 49923 | Estimate 25 to 29 years | female |
| King County | Washington | 54042 | Estimate 30 to 34 years | female |
| King County | Washington | 94048 | Estimate 35 to 44 years | female |
| King County | Washington | 87132 | Estimate 45 to 54 years | female |
| King County | Washington | 89377 | Estimate 55 to 64 years | female |
| King County | Washington | 69304 | Estimate 65 to 74 years | female |
| King County | Washington | 35064 | Estimate 75 to 84 years | female |
| King County | Washington | 19980 | Estimate 85 years and over | female |
Alternatively, we can accomplish the same thing manually. Doing so avoids the need for the load_variables and gsub functions.
Let’s start with a new data request:
demo_data <- get_acs(geography = "county",
variables = c(
"B01003_001", # total population
"B19013_001", # median household income
"B15002_015", # bachelor's degree (male)
"B15002_032", # bachelor's degree (female)
"B17001_002" # below poverty level
),
year = 2022,
survey = "acs5",
state = "WA",
county = "King")We can then use the mutate and recode functions to replace the Census variable IDs with names that are easier to understand:
demo_data <- demo_data |>
mutate(variable = recode(variable,
"B01003_001" = "total_pop",
"B19013_001" = "med_hh_inc",
"B15002_015" = "m_education",
"B15002_032" = "f_education",
"B17001_002" = "poverty"))Reshaping
Another useful operation to implement when cleaning Census data is to pivot it from long to wide format. The choice to do so depends on what you will use the data for, but in general, wide format data is preferable for viewing and summarizing data.
To transform your data from long to wide format, use the pivot_wider function
names_from: takes the column name that contains the names that will become the new columnsvalues_from: argument takes the column name that contains the values that will populate those new columns.
wide_data <- demo_data |>
select(GEOID, variable, estimate) |>
pivot_wider(
names_from = variable, # `variable` is the `demo_data` column with names that will become column names
values_from = estimate # `estimate` is the `demo_data` column with values that will populate those columns
)Requesting Multiple Years or States
It is often the case that we want to grab Census data from multiple years or multiple states. Rather then running the above functions separately for each year or state, we can write a for loop that will download all our desired Census data in one chunk of code.
To do so, we first need to create an empty list (empty_list) using the list function:
# create empty list
empty_list <- list()This empty list is where we will store the Census data from one loop to the next. In other words, our code will access data for one year (or state) at a time, same as before, and then store that data in empty_list as a dataframe. This process repeats for each year or state requested.
Let’s use the same demo_data download from before, but request those data for the years 2022, 2023, and 2024. We begin with a for loop, telling R to repeat everything inside the {curly brackets} “for” each year listed after the for function.
The only change in the Census function is our year = argument, which now takes our index variable (“yr”). Additionally, we add mutate(year = yr) so that each estimate has a record of what Census year it came from.
# multiple years
for (yr in c(2022, 2023, 2024)) {
# download Census data (one year at a time)
demo_data <- get_acs(
geography = "county",
variables = c(
"B01003_001", # total population
"B19013_001", # median household income
"B15002_015", # bachelor's degree (male)
"B15002_032", # bachelor's degree (female)
"B17001_002" # below poverty level
),
year = yr, # THIS IS WHERE THE INDEX VARIABLE GOES
survey = "acs5",
state = "WA",
county = "King"
) |>
# add a variable to indicate which Census year the estimate is from
mutate(year = yr)
# store that data inside `empty_list`
empty_list[[as.character(yr)]] <- demo_data
# once stored, the loop can repeat and overwrite `demo_data` with the following year
}Now we have Census years 2022, 2023, and 2024 all stored as separated dataframes inside (the no longer empty) empty_list. To combine these into a single dataframe, use the bind_rows function:
# bind into single dataframe
all_years <- bind_rows(empty_list)
all_years |>
select(NAME, variable, estimate, year) |>
gt()| NAME | variable | estimate | year |
|---|---|---|---|
| King County, Washington | B01003_001 | 2254371 | 2022 |
| King County, Washington | B15002_015 | 263624 | 2022 |
| King County, Washington | B15002_032 | 255486 | 2022 |
| King County, Washington | B17001_002 | 187794 | 2022 |
| King County, Washington | B19013_001 | 116340 | 2022 |
| King County, Washington | B01003_001 | 2262713 | 2023 |
| King County, Washington | B15002_015 | 267649 | 2023 |
| King County, Washington | B15002_032 | 262193 | 2023 |
| King County, Washington | B17001_002 | 186954 | 2023 |
| King County, Washington | B19013_001 | 122148 | 2023 |
| King County, Washington | B01003_001 | 2287171 | 2024 |
| King County, Washington | B15002_015 | 271885 | 2024 |
| King County, Washington | B15002_032 | 268203 | 2024 |
| King County, Washington | B17001_002 | 193848 | 2024 |
| King County, Washington | B19013_001 | 124746 | 2024 |
We now have a dataframe filled with the requested data - for all three years - that is ready to be cleaned, same as above (see Cleaning).
We can similarly apply the same for loop to access multiple states at once. Note that now, state = is the argument we apply the index variable to:
# create empty list
empty_list <- list()
# multiple states
for (st in c("WA", "NY", "CA", "TX")) {
# download Census data (one state at a time)
demo_data <- get_acs(
geography = "state", # we now specify state as the `geography`
variables = c(
"B01003_001", # total population
"B19013_001", # median household income
"B15002_015", # bachelor's degree (male)
"B15002_032", # bachelor's degree (female)
"B17001_002" # below poverty level
),
year = 2024,
survey = "acs5",
state = st # THIS IS WHERE THE INDEX VARIABLE GOES
) |>
# add a variable to indicate which state the estimate is for
mutate(state = st)
# store that data inside `empty_list`
empty_list[[as.character(st)]] <- demo_data
# once stored, the loop can repeat and overwrite `demo_data` with the following state
}
# bind into single dataframe
all_states <- bind_rows(empty_list)
all_states |>
select(variable, estimate, state) |>
gt()| variable | estimate | state |
|---|---|---|
| B01003_001 | 7816116 | WA |
| B15002_015 | 644140 | WA |
| B15002_032 | 673065 | WA |
| B17001_002 | 760577 | WA |
| B19013_001 | 98141 | WA |
| B01003_001 | 19852366 | NY |
| B15002_015 | 1508969 | NY |
| B15002_032 | 1624863 | NY |
| B17001_002 | 2711742 | NY |
| B19013_001 | 85974 | NY |
| B01003_001 | 39287377 | CA |
| B15002_015 | 2940190 | CA |
| B15002_032 | 3193271 | CA |
| B17001_002 | 4632248 | CA |
| B19013_001 | 99122 | CA |
| B01003_001 | 30188424 | TX |
| B15002_015 | 2013264 | TX |
| B15002_032 | 2222883 | TX |
| B17001_002 | 4074940 | TX |
| B19013_001 | 78476 | TX |
Finally, you can also stack for loops, if you want multiple years and multiple states. Here, both the year = and state = arguments will require an index variable:
# create empty list
empty_list <- list()
for (yr in c(2022, 2023, 2024)) {
for (st in c("WA", "NY", "CA", "TX")) {
# download Census data (one year-state pair at a time)
demo_data <- get_acs(
geography = "state",
variables = c(
"B01003_001", # total population
"B19013_001", # median household income
"B15002_015", # bachelor's degree (male)
"B15002_032", # bachelor's degree (female)
"B17001_002" # below poverty level
),
year = yr, # YEAR INDEX VARIABLE HERE
survey = "acs5",
state = st # STATE INDEX VARIABLE HERE
) |>
# add a variable to indicate the year and state
mutate(year = yr,
state = st)
# store that data inside `empty_list`
empty_list[[paste(yr, st, sep = "_")]] <- demo_data
# once stored, the loop can repeat and overwrite `demo_data` with the following year-state pair
}
}
# bind into single dataframe
all_data <- bind_rows(empty_list)Census Information
Decennial Census
The Decennial Census is conducted every ten years and is designed to provide a complete count of the population and housing units in the United States. The Census is conducted in years ending in zero (e.g., 2000, 2010, and 2020).
The primary purpose of the Decennial Census is to determine how many people live in the United States and where they live. The resulting population counts are used for a wide range of purposes, including determining the number of seats each state receives in the U.S. House of Representatives, redrawing political districts, and distributing federal funding.
The Decennial Census collects information about both people and housing units. Depending on the data product, variables can describe characteristics such as:
- Total population
- Age and sex
- Race and Hispanic or Latino origin
- Household and family characteristics
- Housing units and occupancy
- Group quarters populations
One important feature of the Decennial Census is its geographic detail. Census data are available for a wide range of geographic areas, including states, counties, cities, tracts, block groups, and blocks. This makes the Decennial Census particularly useful when studying population distributions at relatively small geographic scales.
It is important to note, however, that the Decennial Census is not designed to collect detailed information about every aspect of people’s lives. For example, the Decennial Census does not provide the same level of information about income, employment, educational attainment, commuting, or detailed housing characteristics as the American Community Survey.
The Decennial Census also contains multiple data products, which provide different sets of variables. For example, the 2020 Census included the Redistricting Data (pl) and the Demographic & Housing Characteristics File (dhc). These products are based on the same Decennial Census but contain different variables and are intended for different analytical purposes.
The Decennial Census is therefore particularly appropriate when your research question involves population counts, basic demographic characteristics, or geographic distributions of the population, especially when you need data for small geographic areas.
American Community Survey
The American Community Survey (ACS) is an ongoing survey that provides detailed demographic, social, economic, and housing estimates for communities throughout the United States.
Unlike the Decennial Census, which occurs once every ten years, the ACS is conducted continuously throughout the year. Rather than attempting to count every person, the ACS surveys a sample of the population and uses those responses to produce estimates about the broader population.
The ACS provides information about topics that are generally not covered in the Decennial Census, including:
- Educational attainment
- Employment and labor force characteristics
- Income and earnings
- Poverty
- Health insurance coverage
- Disability
- Commuting and transportation
- Language spoken at home
- Migration
- Detailed housing characteristics
- Household and family characteristics
For example, if you wanted to know the median household income, the percentage of adults with a bachelor’s degree, or how many people commute to work by public transportation, the ACS would generally be a more appropriate source than the Decennial Census.
Because the ACS is based on a sample survey, its estimates contain uncertainty. This is an important difference from the population counts produced by the Decennial Census. ACS estimates are therefore typically accompanied by a margin of error (MOE). When analyzing ACS data, it is important to consider both the estimate and its margin of error.
ACS 1-year estimates are based on data collected over a single year. They provide relatively current estimates but are generally available only for larger geographic areas and populations that meet Census Bureau population thresholds.
ACS 5-year estimates combine five years of survey data. Because they use a larger sample collected over a longer period, they are available for many smaller geographic areas, including Census tracts and block groups. However, the resulting estimates represent a longer period of time rather than a single year’s observations.
The choice between the ACS 1-year and 5-year products depends largely on your geographic scale, research question, and need for current versus more geographically detailed estimates. For example, an analyst studying income across large counties might use ACS 1-year estimates, while an analyst studying income at the Census tract level would generally need to use ACS 5-year estimates.
The ACS is therefore particularly useful when your research question involves social, economic, or housing characteristics that require more detail than the Decennial Census provides.
Decennial vs. ACS
| Decennial Census | American Community Survey | |
|---|---|---|
| Frequency | Every 10 years | Continuously (released annually) |
| Primary purpose | Population and housing unit counts | Demographic, social, economic, and housing characteristic estimates |
| Data type | Population enumeration | Sample survey estimates |
| Examples of variables | Population, age, sex, race, Hispanic/Latino origin, housing units | Income, poverty, education, employment, commuting, housing, language |
| Geographic detail | Census block | Census block group (5-year estimates) |
| Uncertainty | Population counts are not survey estimates | Estimates include margins of error |
| Common products | Redistricting Data, Demographic & Housing Characteristics | ACS 1-year and ACS 5-year |
| Best suited for | Population counts and basic demographics | Detailed community characteristics |
The distinction is important when deciding which dataset to use. The Decennial Census is generally the better choice when you need an official population count or basic demographic information, while the ACS is generally the better choice when you need detailed information about the social and economic characteristics of a population.
The two datasets can also be used together. For example, a researcher might use Decennial Census data to obtain population counts and ACS data to examine income, education, or poverty in the same geographic areas. When doing so, however, it is important to pay attention to the year, geographic boundaries, variable definitions, and estimation methods of each dataset rather than assuming that data from the two sources are automatically comparable.