More than a third of Rutherford County renters can’t truly afford their leases, especially those living along the border with Davidson County, the latest Census data suggest.

Financial experts generally recommend spending no more than 30 percent of one’s pre-tax income on housing. But along Rutherford County’s border with Davidson County, around two in five renters shell out 35 percent or more of their pre-tax income to keep a roof over their heads, according to the 2022 American Community Survey conducted by the U.S. Census Bureau.

Rep. Robert Stevens, a Republican who represents the area in the Tennessee House of Representatives, supported a bill this past spring designed to spur low-income housing development through municipal partnerships with private development corporations.

The rest of Rutherford County’s delegation to the state House also supported the bill, with the exception of Republican Rep. Bryan Terry, who represents District 48 in the eastern half of the county, where about a third of renters spend more than 35 percent of their pre-tax income on rent. Terry was present during the bill’s consideration but did not cast a vote.


Proportion of renters overspending on housing, by Tennessee House district

Estimate by district
District Estimate Estimate_MOE From To
State House District 13 (2022), Tennessee 42.9 7.7 35.2 50.6
State House District 37 (2022), Tennessee 40.3 5.5 34.8 45.8
State House District 34 (2022), Tennessee 35.9 4.8 31.1 40.7
State House District 49 (2022), Tennessee 35.7 5.6 30.1 41.3
State House District 48 (2022), Tennessee 34.4 5.9 28.5 40.3

Complete R Script:

Here’s the full script, start to finish, all in one chunk. See below for a step-by-step explanation of how to use the script, and how it works.

# Installing and loading required packages

if (!require("tidyverse"))
  install.packages("tidyverse")
if (!require("tidycensus"))
  install.packages("tidycensus")
if (!require("sf"))
  install.packages("sf")
if (!require("mapview"))
  install.packages("mapview")
if (!require("gtExtras"))
  install.packages("gtExtras")

library(tidyverse)
library(tidycensus)
library(sf)
library(mapview)
library(gtExtras)

# Transmitting API key

census_api_key("PasteYourAPIKeyBetweenTheseQuoteMarks")

# Fetching ACS codebooks

DetailedTables <- load_variables(2022, "acs5", cache = TRUE)
SubjectTables <- load_variables(2022, "acs5/subject", cache = TRUE)
ProfileTables <- load_variables(2022, "acs5/profile", cache = TRUE)
Codebook <- DetailedTables %>% 
  select(name, label, concept)
Codebook <- bind_rows(Codebook,SubjectTables)
Codebook <- bind_rows(Codebook,ProfileTables)
Codebook <- Codebook %>% 
  distinct(label, .keep_all = TRUE)
rm(DetailedTables,
   SubjectTables,
   ProfileTables)

# Filtering the codebook

MyVars <- Codebook %>% 
  filter(grepl("GRAPI", label) &
           grepl("Percent!!", label))

# Making a table of the filtered variables

MyVarsTable <- gt(MyVars) %>%
  tab_header("Variables") %>%
  cols_align(align = "left") %>%
  gt_theme_538

# Displaying the table

MyVarsTable

# Defining the variable to retrieve

VariableList = 
  c(Estimate_ = "DP04_0142P")

# Fetching data

AllData <- get_acs(
  geography = "state legislative district (lower chamber)",
  state = "TN",
  variables = VariableList,
  year = 2022,
  survey = "acs5",
  output = "wide",
  geometry = TRUE
)

# Mutating, selecting and sorting the data

AllData <- AllData %>%
  mutate(
    District = NAME,
    Estimate = Estimate_E,
    Estimate_MOE = Estimate_M,
    From = round(Estimate - Estimate_MOE, 2),
    To = round(Estimate + Estimate_MOE, 2)
  ) %>%
  select(District, Estimate, Estimate_MOE, From, To, geometry) %>%
  arrange(desc(Estimate))

# Filtering for Rutherford County districts

MyData <- AllData %>%
  filter(
    District == "State House District 13 (2022), Tennessee" |
      District == "State House District 37 (2022), Tennessee" |
      District == "State House District 49 (2022), Tennessee" |
      District == "State House District 48 (2022), Tennessee" |
      District == "State House District 34 (2022), Tennessee"
  )

# Producing a map

MapData <- st_as_sf(MyData)

MyMap <- mapview(MapData,
        zcol = "Estimate",
        layer.name = "Estimate",
        popup = TRUE)
#Displaying the map

MyMap

# Producing a table

TableData <- st_drop_geometry(MapData)
MyTable <- gt(TableData) %>%
  tab_header("Estimate by district") %>%
  cols_align(align = "left") %>%
  gt_theme_538

# Displaying the table

MyTable

Step-by-step explanation

Let’s step through the script for a look at how it works and how you can tweak parts of it to get the data you want.

Loading required packages

The script relies on help from five R packages:

  • Tidyverse makes R code easier to write and more intuitive.

  • Tidycensus helps R retrieve data from the U.S. Census Bureau’s application programming interfact, or API.

  • Sf, which stands for “simple features,” lets R read map files, also called geospatial files.

  • Mapview helps R customize and display map files easily.

  • gtExtras helps R make nicely formatted tables.

The first several lines of the script use the if() and install.packages() functions to install each package if it hasn’t been installed previously. Then, it activates each package using the library() function.

if (!require("tidyverse"))
  install.packages("tidyverse")
if (!require("tidycensus"))
  install.packages("tidycensus")
if (!require("sf"))
  install.packages("sf")
if (!require("mapview"))
  install.packages("mapview")
if (!require("gtExtras"))
  install.packages("gtExtras")

library(tidyverse)
library(tidycensus)
library(sf)
library(mapview)
library(gtExtras)

Census API authentication

Next, the script uses the census_api_key() function from the tidyverse package to open and authenticate a connection to the Census Bureau’s API. Before you run the script, you must replace PasteYourAPIKeyBetweenTheseQuoteMarks with the API key that you received by filling out and submitting the Request a U.S. Census Data API Key form.

census_api_key("PasteYourAPIKeyBetweenTheseQuoteMarks")

Retrieving variable descriptions

A typical American Community Survey dataset contains nearly 30,000 variables in close to 50 categories of social, economic, housing, and demographic topics. Finding precisely what you need can be a challenge. This block of code uses the load_variables() function from tidycensus to produces a data frame containing each the name, label/description, and concept description for each variable available in a specified ACS dataset.

In this example, the load_variables() function uses uses the 2022 and acs5 arguments to tell the Census API to send variables for the 2022 five-year American Community Survey dataset.

ACS data come in either single-year datasets that cover only geographic areas with 65,000 or more residents or in five-year datasets that cover all geographic areas, regardless of population. The five-year 2022 dataset requested in the example will cover all geographic areas and reflect variable estimates for the five-year period from 2018 to 2022.

You can change the acs5 arguments to acs1 to retrieve a single-year dataset. You can change the 2022 year arguments, too. For single-year datasets, you can specify 2005 or later. For five-year datasets, you can request 2009 or later. At the time this example was created, the 2018-2022 dataset was the latest-available five-year dataset.

If you change an argument, be sure to change all three occurrences of the argument in the code. Why three? The ACS breaks its variable descriptions into three different files. The code fetches each of the three separately, then combines them using the bind_rows() function from tidyverse. The distinct() function, also from tidyverse, finds and removes any duplicate records. Finally, the rm() function, from base R, removes the three component variable files, leaving only the “Codebook” file.

DetailedTables <- load_variables(2022, "acs5", cache = TRUE)
SubjectTables <- load_variables(2022, "acs5/subject", cache = TRUE)
ProfileTables <- load_variables(2022, "acs5/profile", cache = TRUE)
Codebook <- DetailedTables %>% 
  select(name, label, concept)
Codebook <- bind_rows(Codebook,SubjectTables)
Codebook <- bind_rows(Codebook,ProfileTables)
Codebook <- Codebook %>% 
  distinct(label, .keep_all = TRUE)
rm(DetailedTables,
   SubjectTables,
   ProfileTables)

Searching the variable descriptions for what you want

You certainly don’t want to scroll through nearly 30,000 variable descriptions to find what you’re looking for. This block of code gives you a way to find and display only the descriptions that mention keywords you can specify.

In this example, the code is set to find variable descriptions that contain both “GRAPI” and “Percent!!” “GRAPI” is an acronym for “Gross Rent as a Percentage of Household Income.” “Percent!!” with the two exclamation points is how the Census Bureau denotes variables that show a percentage rather than a count.

You probably wouldn’t know these two shortcuts unless you have some experience working with ACS variables. You’d probably have to start with something like “gross rent,” then, perhaps, repeat the search a few times, with increasingly specific search terms, to narrow the results. Hey, it’s better than scrolling through nearly 30,000 records.

The code uses the %>% pipe operator and filter() function from tidyverse, as well as the grepl() string-search function from base R.

MyVars <- Codebook %>% 
  filter(grepl("GRAPI", label) &
           grepl("Percent!!", label))

Showing your search results in an easy-to-read table

This code uses the gt() function and a few other functions from gtExtras to make a table of the variable descriptions that met the search criteria. Sending R the name of the table, MyVarsTable, in the last line of the block tells R to display the table. The resulting table is shown below the code block.

MyVarsTable <- gt(MyVars) %>%
  tab_header("Variables") %>%
  cols_align(align = "left") %>%
  gt_theme_538

# Displaying the table

MyVarsTable
Variables
name label concept
DP04_0136P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed) Selected Housing Characteristics
DP04_0137P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!Less than 15.0 percent Selected Housing Characteristics
DP04_0138P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!15.0 to 19.9 percent Selected Housing Characteristics
DP04_0139P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!20.0 to 24.9 percent Selected Housing Characteristics
DP04_0140P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!25.0 to 29.9 percent Selected Housing Characteristics
DP04_0141P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!30.0 to 34.9 percent Selected Housing Characteristics
DP04_0142P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!35.0 percent or more Selected Housing Characteristics
DP04_0143P Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!Not computed Selected Housing Characteristics

Specifying which variable you want

Deciphering ACS variable descriptions takes a little practice. But what we’re after here is the variable that shows the percentage of renter-occupied households spending 35 percent or more of their total household income on rent. That would be the next-to-last item in the table, the description of which reads, “Percent!!GROSS RENT AS A PERCENTAGE OF HOUSEHOLD INCOME (GRAPI)!!Occupied units paying rent (excluding units where GRAPI cannot be computed)!!35.0 percent or more.”

To retrieve the data for that variable, you need the variable’s name, DP04_0142P, and you’ll need to paste or carefully type it between the quote marks after Estimate_ = in this snippet of code:

VariableList = 
  c(Estimate_ = "DP04_0142P")

Retrieving data for the variable you specified

This block of code uses the get_acs() function from tidycensus and a whole bunch of arguments to get DP04_0142P for every Tennessee state House district and save it in a dataframe called AllData.

Many of the get_acs() function’s arguments are customizable. Customizing them here, though, would require customizing later parts of the script, particularly those that filter the data for just the districts in Rutherford County, Tennessee.

But just so you know, state legislative district (lower chamber) could be changed to any one of the geographies availabe for five-year ACS data. The tidycensus documentation’s “Geography in tidycensus” section contains a list of available geographies and the types of ACS datasets you can get them from. Additionally, you can change TN to the two-letter abbreviation of any other state, 2022 to any other available year, and acs5 to acs1 to retrieve single year, rather than five-year, data.

AllData <- get_acs(
  geography = "state legislative district (lower chamber)",
  state = "TN",
  variables = VariableList,
  year = 2022,
  survey = "acs5",
  output = "wide",
  geometry = TRUE
)

Some data wrangling

There’s nothing to change here. R is just using various tidyverse functions to rename some variables, compute the error margin’s rounded-off lower and upper limits, and sort the data.

AllData <- AllData %>%
  mutate(
    District = NAME,
    Estimate = Estimate_E,
    Estimate_MOE = Estimate_M,
    From = round(Estimate - Estimate_MOE, 2),
    To = round(Estimate + Estimate_MOE, 2)
  ) %>%
  select(District, Estimate, Estimate_MOE, From, To, geometry) %>%
  arrange(desc(Estimate))

Filtering for Rutherford County districts

Tennessee has 99 state House districts. In this example, we’re after just the five that cover Rutherford County, where MTSU is. This code uses the filter() function from tidyverse to filter the retrieved data for just those five districts. Then, it stores the filtered results in a dataframe called MyData.

MyData <- AllData %>%
  filter(
    District == "State House District 13 (2022), Tennessee" |
      District == "State House District 37 (2022), Tennessee" |
      District == "State House District 49 (2022), Tennessee" |
      District == "State House District 48 (2022), Tennessee" |
      District == "State House District 34 (2022), Tennessee"
  )

Producing and showing the map

We’re in the home stretch now. Here, R uses the st_as_sf() function from sf to turn MyData into a geospatial file that can be mapped, using the geospatial information in the file’s geometry column. Tidycensus added that geometry column when the original data was retrieved. The data wrangling code, above, retained it for exactly this moment.

Once the map has been made and stored as an object called MyMap, sending R the name MyMap tells R to display the map. Here, the map will show up below the code block.

MapData <- st_as_sf(MyData)

MyMap <- mapview(MapData,
        zcol = "Estimate",
        layer.name = "Estimate",
        popup = TRUE)
#Displaying the map

MyMap

Producing and showing the table

Time for the gtExtras package to do its thing again. This code uses the package’s gt() function and some other functions from the package to make a nice table of each Rutherford County district’s DP04_0142P estimate and error margin limits. Along the way, the code uses the sf package’s st_drop_geometry() function to strip the geometry column out of the data. No sense in having that show up in the table.

Finally, the code tells R to show the table by sending R the table’s name, MyTable. Here, the table shows up below the code block.

TableData <- st_drop_geometry(MapData)
MyTable <- gt(TableData) %>%
  tab_header("Estimate by district") %>%
  cols_align(align = "left") %>%
  gt_theme_538

# Displaying the table

MyTable
Estimate by district
District Estimate Estimate_MOE From To
State House District 13 (2022), Tennessee 42.9 7.7 35.2 50.6
State House District 37 (2022), Tennessee 40.3 5.5 34.8 45.8
State House District 34 (2022), Tennessee 35.9 4.8 31.1 40.7
State House District 49 (2022), Tennessee 35.7 5.6 30.1 41.3
State House District 48 (2022), Tennessee 34.4 5.9 28.5 40.3