5  Census Data Wrangling

5.1 Overview

So far, we have used spatial data to characterize access to opioid treatment resources and link those patterns with community-level information. Adding demographic, social, and economic context can help us identify whether disparities in access exist across different populations and better understand factors that may contribute to observed spatial patterns. Many of these community characteristics are available from the U.S. Census Bureau.

The American Community Survey (ACS) is an ongoing survey that provides annually updated estimates of population and housing characteristics. ACS estimates are available as 1-year and 5-year products. We generally recommend using the 5-year estimates for small geographic areas because they provide greater statistical reliability, particularly for less populated areas and smaller population subgroups. In this tutorial, we use the 2020–2024 ACS 5-year estimates, referred to as the 2024 ACS 5-year estimates.

In this tutorial, we demonstrate how to use the tidycensus package to explore and download commonly used ACS variables at multiple geographic levels, both with and without spatial geometries. We will also wrangle these data into formats that can be integrated with other spatial datasets for further analysis. This tutorial focuses on ACS data available through the Census Bureau API. Additional tutorials and documentation for tidycensus are available from the package authors.

Our objectives are thus to:

  • Download ACS data through the Census API
  • Download Census geographic boundaries
  • Explore ACS data at multiple geographic levels
  • Wrangle, clean, and merge Census data for further spatial analysis

5.2 Environment Setup

To replicate the code and functions illustrated in this tutorial, you’ll need to have R and RStudio downloaded and installed on your system. This tutorial assumes some familiarity with the R programming language.

5.2.1 Input/Output

Our only external input will be a GeoJSON file of the Chicago city boundary. All ACS data used in this tutorial will be downloaded directly from the U.S. Census Bureau using the Census API.

  • Chicago City Boundary: boundaries_chicago.geojson

We will generate Census datasets at multiple geographic levels throughout this tutorial. Our final outputs will include:

  • A CSV file and spatial file with selected ACS characteristics at the county level for the state of Illinois.
  • A CSV file and spatial file with selected ACS characteristics for zipcodes within the city of Chicago.

These outputs can be used for further spatial analysis or linked with other datasets using their geographic identifiers.

5.2.2 Load Libraries

We will use the following packages in this tutorial:

  • sf: to manipulate spatial data
  • tidycensus: to download Census data using the ACS API
  • tidyverse: to manipulate and clean data
  • tigris: to download Census TIGER/Line geographic boundaries

First, load the required libraries.

library(sf)
library(tidycensus)
library(tidyverse)
library(tigris)

5.3 Enable Census API Key

To use the Census API, we first need to sign up for an API key. The API key allows the Census Bureau API to identify and authorize requests from your R session. A Census API key can be requested here.

Once you receive your key, install it by running the code below.

#census_api_key("YourAPIKeyHere", install = TRUE) # installs the key for future sessions.

Setting install = TRUE saves the key to your .Renviron file so that it is available for future R sessions.

If you are working on a shared computer or do not want to save your API key, you can use the same function with install = FALSE.

To check whether a Census API key is already installed, run:

#Sys.getenv("CENSUS_API_KEY")

The output should be a long string of characters and numbers. If not, save and restart R, as that may be required with installing for the first time. Continue to test the API key to ensure it’s working correctly before continuing.

5.3.1 Test API Key

Using API services can at require patience. Before diving into the rest of the tutorial, let’s test the API first. Run the line of code below to get a view of population data from 2020, at the state level. If you get an error, troubleshoot to resolve!

pop2020 <- get_acs(geography = "state", 
                       variables = "B01001_001", 
                       year = 2020)

head(pop2020)
# A tibble: 6 × 5
  GEOID NAME       variable   estimate   moe
  <chr> <chr>      <chr>         <dbl> <dbl>
1 01    Alabama    B01001_001  4893186    NA
2 02    Alaska     B01001_001   736990    NA
3 04    Arizona    B01001_001  7174064    NA
4 05    Arkansas   B01001_001  3011873    NA
5 06    California B01001_001 39346023    NA
6 08    Colorado   B01001_001  5684926    NA

Troubleshooting tips include:

  • You may need to overwrite an older Census API key.
  • Your API key may not have been updated correctly. Use a different email address to generate a new API key, or wait 48 hours and use the same email address.
  • Search your error in Google, check StackOverflow tips, & restart as needed.
  • Wait a few hours or a day, come back, and try again.

5.4 Load Data Dynamically

We can now start using the tidycensus package to download demographic and socioeconomic data from the U.S. Census Bureau. In this tutorial, we will cover methods to download ACS data at the state, county, census tract, and ZIP Code Tabulation Area (ZCTA) levels. We will also demonstrate how to download the data with and without spatial geometry.

To download a particular variable or table using tidycensus, we need the relevant variable ID. We can identify these IDs by reviewing the variables available through the load_variables() function. For details on exploring available ACS variables and identifying their variable IDs, see the Explore Variables section in the Appendix.

We can download ACS variables using the get_acs() function. Because ACS data are based on survey samples, estimates are provided along with a margin of error (moe). The tidycensus package returns both values for each requested variable in tidy format.

For the examples covered in this tutorial, the four main inputs for the get_acs() function are:

  • geography: the geographic level at which to retrieve the data (state, county, tract, or zcta)
  • variables: a character string or vector of variable IDs to retrieve
  • year: the ACS data year to retrieve
  • geometry: whether to include spatial geometry in the returned data (TRUE / FALSE)

5.4.1 State Level

To begin, we can download selected demographic characteristics for all states.

stateDf <- get_acs(geography = "state",
                   variables = c(totalPop = "B01001_001",
                                 hispanic = "B03003_003",
                                 white = "B02001_002",
                                 black = "B02001_003",
                                 asian = "B02001_005"),
                   year = 2024, geometry = FALSE)

head(stateDf)
# A tibble: 6 × 5
  GEOID NAME    variable estimate   moe
  <chr> <chr>   <chr>       <dbl> <dbl>
1 01    Alabama totalPop  5086768    NA
2 01    Alabama white     3284260  4017
3 01    Alabama black     1311062  3846
4 01    Alabama asian       74435  1388
5 01    Alabama hispanic   284135   217
6 02    Alaska  totalPop   735706    NA

As we can see, the data are available in tidy format. We can use other tools in the tidyverse to clean and manipulate the data.

stateDf <- stateDf %>%
           select(GEOID, NAME, variable, estimate) %>%
           pivot_wider(names_from = variable, values_from = estimate) %>%
           mutate(hispanicPr = hispanic / totalPop,
                  whitePr = white / totalPop,
                  blackPr = black / totalPop,
                  asianPr = asian / totalPop) %>%
           select(GEOID, totalPop, hispanicPr, whitePr, blackPr, asianPr)

head(stateDf)
# A tibble: 6 × 6
  GEOID totalPop hispanicPr whitePr blackPr asianPr
  <chr>    <dbl>      <dbl>   <dbl>   <dbl>   <dbl>
1 01     5086768     0.0559   0.646  0.258   0.0146
2 02      735706     0.0731   0.596  0.0300  0.0645
3 04     7378838     0.314    0.590  0.0460  0.0358
4 05     3049391     0.0904   0.691  0.147   0.0163
5 06    39287377     0.402    0.397  0.0543  0.155 
6 08     5862189     0.225    0.705  0.0405  0.0329

5.4.2 County Level

Similarly, for county-level data:

  • use geography = "county" to download all counties in the U.S.
  • use geography = "county", state = sampleStateName to download all counties within a state
  • use geography = "county", state = sampleStateName, county = sampleCountyName to download a specific county

For example, the code below downloads selected demographic characteristics for all counties in Illinois.

countyDf <- get_acs(geography = "county",
                    variables = c(totalPop = "B01001_001",
                                  hispanic = "B03003_003",
                                  white = "B02001_002",
                                  black = "B02001_003",
                                  asian = "B02001_005"),
                    year = 2024, state = "IL", geometry = FALSE) %>%
            select(GEOID, NAME, variable, estimate) %>%
            pivot_wider(names_from = variable, values_from = estimate) %>%
            mutate(hispanicPr = hispanic / totalPop,
                                whitePr = white / totalPop,
                                blackPr = black / totalPop,
                                asianPr = asian / totalPop) %>%
            select(GEOID, totalPop, hispanicPr, whitePr, blackPr, asianPr)

head(countyDf)
# A tibble: 6 × 6
  GEOID totalPop hispanicPr whitePr blackPr  asianPr
  <chr>    <dbl>      <dbl>   <dbl>   <dbl>    <dbl>
1 17001    64754    0.0208    0.906 0.0284  0.00672 
2 17003     4875    0.00677   0.630 0.331   0.00431 
3 17005    16716    0.0201    0.867 0.0612  0.00658 
4 17007    53230    0.256     0.718 0.0305  0.00960 
5 17009     6322    0.0671    0.745 0.179   0.000475
6 17011    32866    0.102     0.875 0.00587 0.00873 

5.4.3 Census tract Level

For census tract-level data, at minimum a state must be provided.

  • use geography = "tract", state = sampleStateName to download all census tracts within a state
  • use geography = "tract", state = sampleStateName, county = sampleCountyName to download all census tracts within a specific county

The code below downloads selected demographic characteristics for all census tracts in Illinois.

tractDf <- get_acs(geography = "tract",
                   variables = c(totalPop = "B01001_001",
                                 hispanic = "B03003_003",
                                 white = "B02001_002",
                                 black = "B02001_003",
                                 asian = "B02001_005"),
                   year = 2024, state = "IL", geometry = FALSE) %>%
           select(GEOID, NAME, variable, estimate) %>%
           pivot_wider(names_from = variable, values_from = estimate) %>%
           mutate(hispanicPr = hispanic / totalPop,
                  whitePr = white / totalPop,
                  blackPr = black / totalPop,
                  asianPr = asian / totalPop) %>%
           select(GEOID, totalPop, hispanicPr, whitePr, blackPr, asianPr)

head(tractDf)
# A tibble: 6 × 6
  GEOID       totalPop hispanicPr whitePr blackPr asianPr
  <chr>          <dbl>      <dbl>   <dbl>   <dbl>   <dbl>
1 17001000100     4219    0         0.882  0.0356 0.0166 
2 17001000201     1889    0.00159   0.931  0.0291 0      
3 17001000202     3033    0.0310    0.778  0.0633 0.0501 
4 17001000400     3451    0.0151    0.770  0.143  0      
5 17001000500     1808    0.137     0.877  0.0271 0.0144 
6 17001000600     3931    0.0231    0.907  0.0237 0.00254

5.4.4 Zipcode Level

For zipcode-level data, use geography = "zcta". ZIP Code Tabulation Areas (ZCTAs) are Census Bureau approximations of U.S. Postal Service ZIP Codes. Because ZCTAs can cross county and state boundaries, tidycensus retrieves ZCTA data for the entire U.S.

zctaDf <- get_acs(geography = "zcta",
                  variables = c(totalPop = "B01001_001",
                                hispanic = "B03003_003",
                                white = "B02001_002",
                                black = "B02001_003",
                                asian = "B02001_005"),
                  year = 2024, geometry = FALSE) %>%
          select(GEOID, NAME, variable, estimate) %>%
          pivot_wider(names_from = variable, values_from = estimate) %>%
          mutate(hispanicPr = hispanic / totalPop,
                 whitePr = white / totalPop,
                 blackPr = black / totalPop,
                 asianPr = asian / totalPop) %>%
          select(GEOID, totalPop, hispanicPr, whitePr, blackPr, asianPr)

Inspect the data.

head(zctaDf)
# A tibble: 6 × 6
  GEOID totalPop hispanicPr whitePr blackPr  asianPr
  <chr>    <dbl>      <dbl>   <dbl>   <dbl>    <dbl>
1 00601    16669      0.993   0.798 0.0145  0.00108 
2 00602    37233      0.972   0.272 0.0180  0.00118 
3 00603    48448      0.984   0.729 0.0333  0.000186
4 00606     5163      0.997   0.666 0.00136 0.00232 
5 00610    25357      0.964   0.187 0.0189  0       
6 00611     1383      0.974   0.612 0.0253  0       
dim(zctaDf)
[1] 33772     6

Because ZCTA data are retrieved for the entire nation, we can filter the results to a particular region after downloading them. For example, Chicago ZIP Codes commonly begin with 606, so we can use str_detect() as a simple non-spatial filter. If geometry information is available, we can instead overlay the ZCTA geometries with a city boundary to identify the required region. We will illustrate this approach in the Get Geometry section.

In addition to demographic characteristics, ACS data can provide community context that may help us better understand geographic patterns in healthcare access. Here, we use poverty and household vehicle availability as two examples of access-relevant community characteristics.

zipChicagoDf <- get_acs(geography = "zcta",
                        variables = c(povertyTotal = "B17001_001",
                                      povertyBelow = "B17001_002",
                                      households = "B08201_001",
                                      noVehicle = "B08201_002"),
                        year = 2024, geometry = FALSE) %>%
                select(GEOID, NAME, variable, estimate) %>%
                filter(str_detect(GEOID, "^606")) %>%
                pivot_wider(names_from = variable, values_from = estimate) %>%
                mutate(povertyPr = povertyBelow / povertyTotal,
                       noVehiclePr = noVehicle / households) %>%
                select(GEOID, povertyPr, noVehiclePr)

Inspect the Chicago ZCTA data.

head(zipChicagoDf)
# A tibble: 6 × 3
  GEOID povertyPr noVehiclePr
  <chr>     <dbl>       <dbl>
1 60601    0.0564       0.454
2 60602    0.113        0.552
3 60603    0.141        0.367
4 60604    0.125        0.206
5 60605    0.0979       0.391
6 60606    0.0453       0.549

5.4.5 Save Data

We can now save the county and Chicago zipcode datasets as CSV files using the code below.

write.csv(countyDf, "data/ilcounty_24_demographic.csv")
write.csv(zipChicagoDf, "data/chizips_24_context.csv")

5.5 Get Geometry

Geometry and geographic boundaries are key components of American Community Survey data because they provide the spatial framework used to map and analyze Census estimates. Geographic boundaries may be updated over time, and Census data for a given year generally correspond to geographic definitions available for that data vintage. In this tutorial, we use 2024 Census geographic boundaries to correspond with the 2024 ACS 5-year estimates.

The datasets downloaded so far do not include spatial geometry. To conduct spatial analysis, we need to join these data frames to spatially enabled sf objects using a common geographic identifier such as GEOID.

We can download Census geographic boundaries using two methods: - using tigris - using tidycensus

5.5.1 Using tigris

To download TIGER/Line geographic boundaries provided by the U.S. Census Bureau, we can use the tigris package. Setting cb = TRUE downloads generalized cartographic boundary files, which have less spatial detail and smaller file sizes than full-resolution TIGER/Line files.

For states, counties, and census tracts, we use 2024 boundary files. The 2024 cartographic boundary file for ZCTAs is not currently available, so we use the 2020 ZCTA boundaries.

yearToFetch <- 2024

stateShp <- states(year = yearToFetch, cb = TRUE)
countyShp <- counties(year = yearToFetch, state = "IL", cb = TRUE)
tractShp <- tracts(state = "IL", year = yearToFetch, cb = TRUE)
zctaShp <- zctas(year = 2020, cb = TRUE)

After downloading the geometry files, we can merge these shapes with the demographic data downloaded in the previous section.

For states:

# check object types & identifier variable type
# str(stateShp)
# str(stateDf)

stateShp <- merge(stateShp, stateDf, by.x = "STATEFP", by.y = "GEOID", all.x = TRUE)

head(stateShp)
Simple feature collection with 6 features and 14 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -179.1467 ymin: 30.22333 xmax: 179.7785 ymax: 71.38782
Geodetic CRS:  NAD83
  STATEFP  STATENS     GEOIDFQ GEOID STUSPS       NAME LSAD        ALAND
1      01 01779775 0400000US01    01     AL    Alabama   00 1.311856e+11
2      02 01785533 0400000US02    02     AK     Alaska   00 1.479509e+12
3      04 01779777 0400000US04    04     AZ    Arizona   00 2.943661e+11
4      05 00068085 0400000US05    05     AR   Arkansas   00 1.346585e+11
5      06 01779778 0400000US06    06     CA California   00 4.036734e+11
6      08 01779779 0400000US08    08     CO   Colorado   00 2.684190e+11
        AWATER totalPop hispanicPr   whitePr    blackPr    asianPr
1   4581813708  5086768 0.05585767 0.6456477 0.25773969 0.01463306
2 244710526650   735706 0.07313111 0.5955885 0.02999976 0.06445917
3    853991999  7378838 0.31357322 0.5900494 0.04601849 0.03578057
4   3122715710  3049391 0.09036919 0.6914810 0.14713528 0.01629375
5  20291632828 39287377 0.40162933 0.3971951 0.05433084 0.15528041
6   1185541418  5862189 0.22533170 0.7051564 0.04049835 0.03287117
                        geometry
1 MULTIPOLYGON (((-88.05338 3...
2 MULTIPOLYGON (((-132.4825 5...
3 MULTIPOLYGON (((-114.8163 3...
4 MULTIPOLYGON (((-94.61792 3...
5 MULTIPOLYGON (((-118.6044 3...
6 MULTIPOLYGON (((-109.0603 3...

Similarly for counties, zctas & census tracts we can use the code below.

countyShp <- merge(countyShp, countyDf, by.x = "GEOID", by.y = "GEOID", all.x = TRUE)%>%
             select(GEOID, totalPop, hispanicPr, whitePr, blackPr, asianPr)

tractShp <- merge(tractShp, tractDf, by.x = "GEOID", by.y = "GEOID", all.x = TRUE)%>%
            select(GEOID, totalPop, hispanicPr, whitePr, blackPr, asianPr)

The 2024 cartographic boundary file for ZCTAs is not currently available, so we use the 2020 ZCTA boundaries, which correspond to the current ZCTA geography used by the 2024 ACS.

zctaShp <- merge(zctaShp, zctaDf, by.x = "GEOID20", by.y = "GEOID", all.x = TRUE) %>%
              select(GEOID20, totalPop, hispanicPr, whitePr, blackPr, asianPr)

5.5.2 Using tidycensus

The previous method adds an additional step of using the tigris package to download geographic boundary files. The tidycensus package can also invoke tigris within the get_acs() function, allowing us to download ACS data with geometry by setting geometry = TRUE.

Adding geometry to each variable can increase the size of intermediate datasets and slow down processing, particularly when downloading many variables. For larger requests, we recommend downloading the ACS variables without geometry and then downloading a single variable with geometry = TRUE. The resulting spatial object can then be merged with the larger ACS dataset. We illustrate this approach below.

First, download the census tract demographic data without geometry.

tractDf <- get_acs(geography = "tract",
                   variables = c(totalPop = "B01001_001",
                                 hispanic = "B03003_003",
                                 white = "B02001_002",
                                 black = "B02001_003",
                                 asian = "B02001_005"),
                   year = 2024, state = "IL", geometry = FALSE) %>%
           select(GEOID, NAME, variable, estimate) %>%
           pivot_wider(names_from = variable, values_from = estimate) %>%
           mutate(hispanicPr = hispanic / totalPop,
                  whitePr = white / totalPop,
                  blackPr = black / totalPop,
                  asianPr = asian / totalPop) %>%
           select(GEOID, totalPop, hispanicPr, whitePr, blackPr, asianPr)

We can then download a single variable with geometry.

tractShp <- get_acs(geography = "tract",
                    variables = c(totalPop = "B01001_001"),
                    year = 2024, state = "IL", geometry = TRUE) %>%
            select(GEOID, NAME, variable, estimate) %>%
            pivot_wider(names_from = variable, values_from = estimate)

Merge the geometry with the demographic data using GEOID.

tractShp <- merge(tractShp, tractDf, by.x = "GEOID", by.y = "GEOID", all.x = TRUE)
head(tractShp)
Simple feature collection with 6 features and 8 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -91.42005 ymin: 39.92603 xmax: -91.32937 ymax: 39.97247
Geodetic CRS:  NAD83
        GEOID                                      NAME totalPop.x totalPop.y
1 17001000100    Census Tract 1; Adams County; Illinois       4219       4219
2 17001000201 Census Tract 2.01; Adams County; Illinois       1889       1889
3 17001000202 Census Tract 2.02; Adams County; Illinois       3033       3033
4 17001000400    Census Tract 4; Adams County; Illinois       3451       3451
5 17001000500    Census Tract 5; Adams County; Illinois       1808       1808
6 17001000600    Census Tract 6; Adams County; Illinois       3931       3931
   hispanicPr   whitePr    blackPr     asianPr                       geometry
1 0.000000000 0.8824366 0.03555345 0.016591609 MULTIPOLYGON (((-91.37766 3...
2 0.001588142 0.9311805 0.02911593 0.000000000 MULTIPOLYGON (((-91.39646 3...
3 0.030992417 0.7777778 0.06330366 0.050115397 MULTIPOLYGON (((-91.3937 39...
4 0.015068096 0.7696320 0.14256737 0.000000000 MULTIPOLYGON (((-91.42005 3...
5 0.137168142 0.8772124 0.02710177 0.014380531 MULTIPOLYGON (((-91.4034 39...
6 0.023149326 0.9071483 0.02365810 0.002543882 MULTIPOLYGON (((-91.39655 3...

To get ZCTA-level data for a specific region, we first download the required dataset for the entire country and then filter the relevant ZCTAs by overlaying the downloaded dataset with the geometry of the region of interest.

Here, we download the poverty and household vehicle availability measures used in the previous section without geometry.

zctaContextDf <- get_acs(geography = "zcta",
                         variables = c(povertyTotal = "B17001_001",
                                       povertyBelow = "B17001_002",
                                       households = "B08201_001",
                                       noVehicle = "B08201_002"),
                         year = 2024,geometry = FALSE) %>%
               select(GEOID, NAME, variable, estimate) %>%
               pivot_wider(names_from = variable, values_from = estimate) %>%
               mutate(povertyPr = povertyBelow / povertyTotal,
                      noVehiclePr = noVehicle / households) %>%
               select(GEOID, povertyPr, noVehiclePr)

Download the 2020 ZCTA boundaries and merge them with the 2024 ACS data.

zctaShp <- zctas(year = 2020, cb = TRUE)

zctaShp <- merge(zctaShp, zctaContextDf, by.x = "GEOID20", by.y = "GEOID", all.x = TRUE) %>%
           select(GEOID20, povertyPr, noVehiclePr)

Inspect the data.

head(zctaShp)
Simple feature collection with 6 features and 3 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -67.23915 ymin: 18.11201 xmax: -66.66106 ymax: 18.51335
Geodetic CRS:  NAD83
  GEOID20 povertyPr noVehiclePr                       geometry
1   00601 0.5990141  0.12014563 MULTIPOLYGON (((-66.83637 1...
2   00602 0.4695903  0.12575266 MULTIPOLYGON (((-67.23915 1...
3   00603 0.4442137  0.15985296 MULTIPOLYGON (((-67.16914 1...
4   00606 0.5448383  0.08978495 MULTIPOLYGON (((-67.05134 1...
5   00610 0.4226457  0.08923365 MULTIPOLYGON (((-67.22534 1...
6   00611 0.3984093  0.12812960 MULTIPOLYGON (((-66.82743 1...

Read in the Chicago city boundary file, which can be download here.

chiCityBoundary <- st_read("data/boundaries_chicago.geojson")
Reading layer `boundaries_chicago' from data source 
  `/Users/maryniakolak/Code/opioid-environment-toolkit/data/boundaries_chicago.geojson' 
  using driver `GeoJSON'
Simple feature collection with 1 feature and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -87.94011 ymin: 41.64454 xmax: -87.52414 ymax: 42.02304
Geodetic CRS:  WGS 84

Set the same coordinate reference system for both spatial datasets.

chiCityBoundary <- st_transform(chiCityBoundary, 4326)
zctaShp <- st_transform(zctaShp, 4326)

Only keep ZCTAs that intersect the Chicago city boundary.

zipChicagoShp <- st_intersection(zctaShp, chiCityBoundary)

Inspect data.

head(zipChicagoShp)
Simple feature collection with 6 features and 7 fields
Geometry type: GEOMETRY
Dimension:     XY
Bounding box:  xmin: -87.94011 ymin: 41.68284 xmax: -87.52454 ymax: 42.01914
Geodetic CRS:  WGS 84
      GEOID20  povertyPr noVehiclePr    name objectid    shape_area
15446   46394 0.15652542  0.07039934 CHICAGO        1 6450276623.31
20826   60007 0.07980461  0.05677877 CHICAGO        1 6450276623.31
20834   60018 0.06665457  0.05746167 CHICAGO        1 6450276623.31
20864   60068 0.02912086  0.04585859 CHICAGO        1 6450276623.31
20872   60076 0.06489307  0.05527768 CHICAGO        1 6450276623.31
20873   60077 0.14556758  0.09126948 CHICAGO        1 6450276623.31
          shape_len                       geometry
15446 845282.931362 MULTIPOLYGON (((-87.52454 4...
20826 845282.931362 MULTIPOLYGON (((-87.93993 4...
20834 845282.931362 MULTIPOLYGON (((-87.93523 4...
20864 845282.931362 MULTIPOLYGON (((-87.85584 4...
20872 845282.931362 POLYGON ((-87.70919 42.0118...
20873 845282.931362 POLYGON ((-87.76167 42.0082...

5.5.3 Save Data

We previously saved our datasets without geometry information as CSV files. Now we can save the Illinois county demographic data and Chicago ZCTA community context data with geometries as shapefiles using write_sf().

write_sf(countyShp, "data/ilcounty_24_demographic.shp")
write_sf(zipChicagoShp, "data/chizips_24_context.shp")

5.6 Appendix

5.6.1 Explore Variables

Using tidycensus, we can download ACS variables from several types of tables. Some commonly used products include:

  • Data Profiles: Broad collections of social, economic, housing, and demographic characteristics, such as Social Characteristics (DP02), Economic Characteristics (DP03), Housing Characteristics (DP04), and Demographic and Housing Estimates (DP05).
  • Subject Tables: Topic-specific tables that provide estimates and percentages for subjects such as age and sex (S0101), employment, education, income, and disability.
  • Detailed Tables: Detailed ACS tables identified primarily by B and C table IDs. These tables provide the variables used throughout this tutorial.

We can explore the variables available for our year of interest using load_variables(). Variable IDs and table structures may change over time, so it is important to check the variables for the ACS year being used.

sVarNames <- load_variables(2024, "acs5/subject", cache = TRUE)
pVarNames <- load_variables(2024, "acs5/profile", cache = TRUE)
otherVarNames <- load_variables(2024, "acs5", cache = TRUE)

head(pVarNames)
# A tibble: 6 × 3
  name         label                                                     concept
  <chr>        <chr>                                                     <chr>  
1 DP02PR_0001  Estimate!!HOUSEHOLDS BY TYPE!!Total households            Select…
2 DP02PR_0001P Percent!!HOUSEHOLDS BY TYPE!!Total households             Select…
3 DP02PR_0002  Estimate!!HOUSEHOLDS BY TYPE!!Total households!!Married-… Select…
4 DP02PR_0002P Percent!!HOUSEHOLDS BY TYPE!!Total households!!Married-c… Select…
5 DP02PR_0003  Estimate!!HOUSEHOLDS BY TYPE!!Total households!!Married-… Select…
6 DP02PR_0003P Percent!!HOUSEHOLDS BY TYPE!!Total households!!Married-c… Select…

A tibble containing table and variable information includes three main columns: name, label, and concept.

name identifies the ACS variable. concept generally identifies the table or topic associated with the variable, while label provides a description of the variable.

We can explore these tibbles to identify the appropriate variable ID for use with get_acs(). For example, we can search the Subject Tables for variables related to children under 5 years of age.

sVarNames %>%
  filter(str_detect(concept, "AGE AND SEX")) %>%
  filter(str_detect(label, "Under 5 years")) %>%
  mutate(label = sub("^Estimate!!", "", label)) %>%
  select(variableId = name, label)
# A tibble: 0 × 2
# ℹ 2 variables: variableId <chr>, label <chr>

We can also search directly within a specific Subject Table, such as S0101.

sVarNames %>%
  filter(str_sub(name, 1, 5) == "S0101") %>%
  filter(str_detect(label, "Under 5 years")) %>%
  mutate(label = sub("^Estimate!!", "", label)) %>%
  select(variableId = name, label)
# A tibble: 6 × 2
  variableId    label                                               
  <chr>         <chr>                                               
1 S0101_C01_002 Total!!Total population!!AGE!!Under 5 years         
2 S0101_C02_002 Percent!!Total population!!AGE!!Under 5 years       
3 S0101_C03_002 Male!!Total population!!AGE!!Under 5 years          
4 S0101_C04_002 Percent Male!!Total population!!AGE!!Under 5 years  
5 S0101_C05_002 Female!!Total population!!AGE!!Under 5 years        
6 S0101_C06_002 Percent Female!!Total population!!AGE!!Under 5 years

Similarly, we can explore Data Profile variables. For example, the following code searches for per capita income variables.

pVarNames %>%
  filter(str_detect(label, "Per capita")) %>%
  mutate(label = sub("^Estimate!!", "", label)) %>%
  select(variable = name, label)
# A tibble: 2 × 2
  variable   label                                                              
  <chr>      <chr>                                                              
1 DP03_0088  INCOME AND BENEFITS (IN 2024 INFLATION-ADJUSTED DOLLARS)!!Per capi…
2 DP03_0088P Percent!!INCOME AND BENEFITS (IN 2024 INFLATION-ADJUSTED DOLLARS)!…

We can also search for age-related variables in the Demographic and Housing Estimates profile.

pVarNames %>%
  filter(str_detect(label, "Under 5 years")) %>%
  mutate(label = sub("^Estimate!!", "", label)) %>%
  select(variable = name, label)
# A tibble: 2 × 2
  variable   label                                                
  <chr>      <chr>                                                
1 DP05_0005  SEX AND AGE!!Total population!!Under 5 years         
2 DP05_0005P Percent!!SEX AND AGE!!Total population!!Under 5 years

Because variable IDs and table structures can change across ACS releases, users should verify the appropriate variable IDs for each year. For analyses across multiple years, Detailed Tables can often provide a more consistent basis for identifying comparable variables. The examples in this tutorial primarily use Detailed Table variables.

For example, we can search the Detailed Tables for per capita income.

otherVarNames %>%
  filter(str_detect(label, "Per capita income")) %>%
  mutate(label = sub("^Estimate!!", "", label)) %>%
  select(variable = name, label)
# A tibble: 10 × 2
   variable    label                                                            
   <chr>       <chr>                                                            
 1 B19301A_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 2 B19301B_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 3 B19301C_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 4 B19301D_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 5 B19301E_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 6 B19301F_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 7 B19301G_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 8 B19301H_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
 9 B19301I_001 Per capita income in the past 12 months (in 2024 inflation-adjus…
10 B19301_001  Per capita income in the past 12 months (in 2024 inflation-adjus…