Acessing Census Data in R

Author

Mark Nepf (mnepf@uw.edu)

Published

September 22, 2026

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.

# install
install.packages("tidycensus")  # accessing Census data
install.packages("tidyverse")  # cleaning data
install.packages("janitor")  # cleaning data
install.packages("gt")  # creating tables
# 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:

Figure 1: Census API Key Request 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.

TIP

If running census_api_key returns Error: A CENSUS_API_KEY already exists, it means you already have an API key installed. You can proceed without re-installing, or you can overwrite the existing key by adding overwrite = TRUE to the census_api_key function.

# overwrite Census API key
census_api_key(key = "YOUR_KEY_HERE",
               install = TRUE,
               overwrite = TRUE)

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")
NOTE

The output from Sys.getenv("CENSUS_API_KEY") is not shown in this vignette, as API keys should not be shared.

It is good practice to keep your API key hidden and not include it in files that you plan to publish or distribute. In other words, treat your Census API key like a password. By running census_api_key once, you store your key in your local R environment, allowing tidycensus to use it without exposing the key in your code.

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-year

You 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)
TIP

Once opened, you can search for variables that relate to your topic of interest. Rather than scrolling through thousands of variables, use the search bar in the top right to look for specific terms. For instance, searching for “population” will temporarily filter the dataframe to variables dealing with population estimates.

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 variable
  • concept: broader measurement concept (i.e., table) to which the variable belongs
  • label: description of the specific variable (i.e., the variable nested within concept)
CAUTION

Census variable names and availability can change across years and datasets. A variable that exists in one year may not exist in another. When working with multiple years, it is a good idea to check the available variables for each year before downloading the data.

click here for information on choosing the right Census dataset

Choosing a Census dataset is not simply a technical decision. Before downloading data, you should think critically about what you are trying to measure and what type of data are appropriate for your research question.

Census products vary in their:

  • Time Period: when the data were collected or what years they represent
  • Geographic Coverage: which geographic units are available
  • Geographic Scale: from national and state-level data to counties, tracts, and block groups
  • Population/Topic: what concepts and variables are included
  • Estimation Method: a complete enumeration versus a survey estimate
  • Uncertainty Level: whether estimates include margins of error
  • Data Availability: whether the same variable or geography is available consistently over time

For example, when working with ACS data, the choice between the 1-year and 5-year estimates depends on both the geographic area you need and the type of analysis you are conducting. ACS 1-year estimates provide more recent information for larger geographic areas (e.g., counties), while ACS 5-year estimates are often more precise and available for smaller geographic areas (e.g., Census tracts and block groups).

TIP

The important thing to keep in mind is that there is no single Census dataset that is always the “best” choice. Your research question should determine which Census product, year, geography, and variables you use.

Before making a request with tidycensus, ask yourself:

  • What population or concept am I trying to measure?
  • What geographic area do I need?
  • What time period do I need?
  • Which Census product provides the appropriate data?
  • Are the variables I need available for that product, geography, and year?
  • What limitations or uncertainty should I consider when interpreting the data?

Thinking through these questions before downloading data can prevent common problems such as using the wrong dataset, selecting an inappropriate geographic scale, comparing estimates that are not directly comparable, or assuming that a variable is available for every year.

See Census Information for more.

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
CAUTION

Not every argument is used with every function. For example, survey is used with get_acs, while sumfile is used with get_decennial. Similarly, while the variables and tables argument apply to both get_acs and get_decennial, only one can be used at a time (discussed further below).

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 B19001 contains all Household Income variables, while variable B19001_002 is households making under $10,000
  • Note that the variables belonging to a table will always carry the same prefix (e.g., B19001 is income and B19001_002 is a specific income bracket)
TIP

When working with get_acs or get_decennial, the variables argument is used to specify specific, individual variables, while the table argument is used to specify an entire table (i.e., “container”) of variables related to a single metric.

  • Use variables when you know which specific variable(s) you need
  • Use table when you want to explore or work with all of the variables in a particular table

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
TIP

The variables argument does not limit users to requesting one variable at a time. You can request multiple variables by listing them in a vector (i.e., using the c(...) operator).

multi_acs_vars <- get_acs(geography = "county",
                          variables = c(
                            "B01003_001",  # total population
                            "B19013_001",  # median household income
                            "B17001_002"  # poverty status
                            ),
                          year = 2022,
                          survey = "acs5",
                          state = "WA",
                          county = "King")

This is often the preferred approach when you know which variables you want, as it limits both the number of requests that you need to make and the total amount of data downloaded.

state and county: what is your geographic region of interest?

  • Note that tidycensus functions allow you to identify states and counties by either name or FIPS
  • For example, state = "WA" and state = "53" reference the same state (i.e., Washington), while county = "King" and county = "033" both reference King County.
NOTE

What are FIPS codes?

FIPS stands for Federal Information Processing Series. A FIPS code is a standardized numeric identifier for a geographic area in the United States. States have two-digit FIPS codes, while counties have three-digit FIPS codes. A county’s full geographic identifier (GEOID) combines the state and county FIPS codes. For example, Washington has FIPS code 53, and King County has FIPS code 033. Together, they form the county GEOID 53033.

year: what year of data do you want?

  • The meaning of year depends on the dataset
    • For ACS data, year is the year of release
      • The 2018-2022 5-year estimates are referenced by year = 2022
    • For Decennial data, year is the Census year
      • The 2020 Decennial Census is called with year = 2020

survey vs. sumfile: which data product are you requesting?

  • survey is used with get_acs
  • sumfile is used with get_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.

TIP

When requesting ACS data, two commonly used options are survey = "acs1" for ACS 1-year estimates and survey = "acs5" for ACS 5-year estimates. The appropriate choice depends on factors such as your research question, geographic area, and desired time period.

When working with Decennial data, the sumfile argument has a much wider range of options because the Decennial Census includes multiple data products. Additionally, these options vary by Census year, and not every summary file is available for every year. You can use the summary_files function to see which Decennial Census summary files are available for a particular year.

summary_files(year = 2020)
 [1] "pl"    "dhc"   "dp"    "pes"   "dpas"  "ddhca" "dpmp"  "dpgu"  "dpvi" 
[10] "ddhcb" "sdhc"  "dhcvi" "dhcgu" "dhcvi" "dhcas" "cd118"

The abbreviations output by the summary_files function correspond to different Decennial Census data products. The Decennial Census Technical Documentation provides descriptions for the available data products.

geometry: do you want spatial data to be included in your download?

  • When geometry = TRUE, the geographic boundaries of your selected geography are returned in a geometry column, and the resulting dataframe is an sf object
    • The default is geometry = FALSE
  • Spatial data are particularly useful when you want to map Census estimates or conduct spatial analysis using packages such as sf
CAUTION

geometry = TRUE can substantially increase the size of your dataset because spatial data include the geographic boundaries for each observation. If you only need the Census estimates, you may not need to download the geometry.

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"))
CAUTION

The order of the join matters! Variable metadata (acs_vars) must be joined into the data (acs_data).

The reason is that acs_vars contains every variable that is available for that year and product (i.e., 2018-2022 ACS 5-year estimates), totaling 28,152 observations. By joining these metadata into the estimates data, we preserve our estimates while adding only the matching metadata. Thus, we end up with a dataframe that has the same number of observations as our data request does (acs_data).

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"))
CAUTION

Manually renaming variables is preferable when you have specific names you want to give each variable, rather than using the predefined Census names. However, there is also a higher risk of human error - be sure to double check that the new names you provide correspond correctly with the variables.

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 columns
  • values_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
    )
click here for information on how long and wide format differ

The difference between long and wide format data has to do with how repeated measurements across the same observation are handled.

In the case of Census data, “observations” are relative to the selected geography. For instance, if you requested county data (geography = "county"), tidycensus returns a dataframe with all unique counties for the state specified.

Long Format

In long format data, these unique counties are repeated for every variable you requested. In other words, if you requested five variables, each county will be repeated five times - once for each variable. It’s called “long” format because the duplication of unique observations literally makes the dataframe longer.

Consider the data requested above, which has not been altered yet:

GEOID variable estimate
53033 total_pop 2254371
53033 m_education 263624
53033 f_education 255486
53033 poverty 187794
53033 med_hh_inc 116340

We see that GEOID (i.e., the unique county ID) is repeated five times, once for each of the five requested variables.

Wide Format

In wide format data, each unique observation is listed only once and the estimates for each variable are populated in separate columns. It’s called “wide” format because storing data in this way necessitates new columns, literally making the dataframe wider.

Here is what the above data looks like when transformed to wide format:

GEOID total_pop m_education f_education poverty med_hh_inc
53033 2254371 263624 255486 187794 116340

We now have only one row, reflecting the fact that there is only one King County, Washington. Rather than each variable extending downward, there is now a new column for each variable and it is populated with the estimate.

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.

click here for information on for loops

The point of a for loop is to repeat a chunk of code “for” several items. In the case of Census data, this might be a list of several years or several states. So, the first thing to do when writing a for loop is provide that list of items.

We can do this using the c(...) operator, which stores the items of our choosing inside a list. For example, running the code below prints the years and states provided:

# years
c(2022, 2023, 2024)
[1] 2022 2023 2024
# states
c("WA", "NY", "CA", "TX")
[1] "WA" "NY" "CA" "TX"

Next, we’ll add the for function, which applies each item - one at a time - to a code chunk. The code chunk is placed inside {curly brackets}, as below:

# print years
for (year in c(2022, 2023, 2024)) {print(year)}
[1] 2022
[1] 2023
[1] 2024

which can be read as, for each “year” in the c(2022, 2023, 2024) list, print that “year”

# print states
for (state in c("WA", "NY", "CA", "TX")) {print(state)}
[1] "WA"
[1] "NY"
[1] "CA"
[1] "TX"

which can be read as, for each “state” in the c(“WA”, “NY”, “CA”, “TX”) list, print the “state”

Note that we must also specific a way to reference each item. This is often called the “index variable,” but can more simply be thought of as a placeholder. In the code above, our placeholder is year and then state. These index variables help R apply each item in our list to the subsequent function.

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.

NOTE

When you use Decennial Census data, remember that “the Census” is not necessarily one single dataset. Different Decennial Census data products contain different variables. When using tidycensus, you must specify both the Census year and the particular data product (sumfile) you want.

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.