Housing costs for two-bedroom units in the greater Nashville area have risen sharply, with nearly 50 ZIP codes considered unaffordable for typical single-parent households, and only a small fraction remaining within an affordable range.
The analysis combines HUD’s 2026 SAFMR rent estimates with ACS 2022 data to estimate the number of rental households and total households by ZIP code. The map below shows two-bedroom rent levels, shaded from light to dark pink, and detail windows provide rental and total household estimates for each ZIP code. This evaluation highlights which neighborhoods may offer more accessible housing and which are likely to place higher cost burdens on families, helping planners, policymakers, and residents make informed decisions. —
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("gt")) install.packages("gt")
if (!require("leaflet")) install.packages("leaflet")
if (!require("leafpop")) install.packages("leafpop")
if (!require("sf")) install.packages("sf")
if (!require("RColorBrewer")) install.packages("RColorBrewer")
if (!require("classInt")) install.packages("classInt")
if (!require("scales")) install.packages("scales")
if (!require("htmlwidgets")) install.packages("htmlwidgets")
if (!require("tidycensus")) install.packages("tidycensus")
library(tidyverse)
library(gt)
library(sf)
library(leaflet)
library(leafpop)
library(RColorBrewer)
library(classInt)
library(scales)
library(htmlwidgets)
library(tidycensus)
ZIPList <- c(
"37135","37215","37064","37060","37014","37122","37027","37046","37221","37153",
"37210","37202","37024","37218","37062","37179","37025","37206","37065","37214",
"37067","37246","37068","37167","37069","37189","37070","37204","37072","37208",
"37076","37212","37080","37216","37085","37020","37086","38476","37089","37160",
"37090","37174","37115","37180","37116","37201","37118","37203","37015","37205",
"37127","37207","37128","37209","37129","37211","37130","37213","37220","37037",
"37222","37217","37228","37219","37232","37013","37131","37224","37132","37229",
"37133","37236","37238","37240","37243","37138","38401","37143","37011","37149"
)
download.file(
"https://www.huduser.gov/portal/datasets/fmr/fmr2026/fy2026_safmrs.xlsx",
"rent.xlsx",
mode = "wb"
)
FMR_Area <- readxl::read_xlsx(
path = "rent.xlsx",
.name_repair = "universal"
)
FMR_Area <- FMR_Area %>%
transmute(
ZIP = ZIP.Code,
Studio = SAFMR.0BR,
BR1 = SAFMR.1BR,
BR2 = SAFMR.2BR,
BR3 = SAFMR.3BR,
BR4 = SAFMR.4BR
) %>%
filter(ZIP %in% ZIPList) %>%
distinct()
if(!file.exists("zipmap.zip")){
download.file(
"https://www2.census.gov/geo/tiger/GENZ2020/shp/cb_2020_us_zcta520_500k.zip",
"zipmap.zip",
mode = "wb"
)
}
unzip("zipmap.zip", overwrite = TRUE)
ZIPMap <- st_read("cb_2020_us_zcta520_500k.shp") %>%
transmute(ZIP = ZCTA5CE20) %>%
filter(ZIP %in% ZIPList)
## Reading layer `cb_2020_us_zcta520_500k' from data source
## `C:\Users\miaru\OneDrive\Documents\cb_2020_us_zcta520_500k.shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 33791 features and 7 fields
## Geometry type: MULTIPOLYGON
## Dimension: XY
## Bounding box: xmin: -176.6967 ymin: -14.37374 xmax: 145.8304 ymax: 71.34122
## Geodetic CRS: NAD83
MapData <- ZIPMap %>%
left_join(FMR_Area, by = "ZIP")
ACS_Area <- get_acs(
geography = "zcta",
variables = c(
Rentals = "B25032_013",
TotalHH = "B11001_001"
),
year = 2022
) %>%
select(GEOID, variable, estimate, moe) %>%
pivot_wider(
names_from = variable,
values_from = c(estimate, moe)
) %>%
transmute(
ZIP = GEOID,
Rentals_E = estimate_Rentals,
Rentals_M = moe_Rentals,
TotalHH_E = estimate_TotalHH,
TotalHH_M = moe_TotalHH
) %>%
filter(ZIP %in% ZIPList)
MapData <- MapData %>%
left_join(ACS_Area, by = "ZIP")
pal <- colorNumeric(
palette = "RdPu",
domain = MapData$BR2
)
leaflet(MapData) %>%
addProviderTiles("CartoDB.Positron") %>%
addPolygons(
fillColor = ~pal(BR2),
weight = 1,
color = "white",
fillOpacity = 0.7,
popup = ~paste0(
"<b>ZIP:</b> ", ZIP,
"<br><b>Studio:</b> $", Studio,
"<br><b>1BR:</b> $", BR1,
"<br><b>2BR:</b> $", BR2,
"<br><b>3BR:</b> $", BR3,
"<br><b>4BR:</b> $", BR4,
"<br><br><b>Rental Homes:</b> ", Rentals_E,
"<br><b>Rental MOE:</b> ", Rentals_M,
"<br><br><b>Total Households:</b> ", TotalHH_E,
"<br><b>Total HH MOE:</b> ", TotalHH_M
)
) %>%
addLegend(
pal = pal,
values = ~BR2,
title = "Two Bedroom Fair Market Rent",
position = "bottomright"
)
# Define affordability threshold (example: 2BR rent <= $1400 is affordable)
MapData <- MapData %>%
mutate(Affordability = ifelse(BR2 <= 1400, "Affordable", "Unaffordable"))
# Summarize
Summary <- MapData %>%
st_set_geometry(NULL) %>% # drop geometry for summarise
group_by(Affordability) %>%
summarise(
Count = n(),
Minimum = min(BR2, na.rm = TRUE),
Average = round(mean(BR2, na.rm = TRUE)),
Maximum = max(BR2, na.rm = TRUE)
)
# Display with gt
Summary_Table <- gt(Summary) %>%
tab_header(title = "ZIP Code Affordability") %>%
cols_align(align = "left")
Summary_Table
| ZIP Code Affordability | ||||
| Affordability | Count | Minimum | Average | Maximum |
|---|---|---|---|---|
| Affordable | 9 | 1320 | 1332 | 1390 |
| Unaffordable | 54 | 1450 | 1906 | 2600 |
# Subset MapData for the table
Data_From_Map <- MapData %>%
st_set_geometry(NULL) %>% # remove geometry
select(ZIP, BR2, Rentals_E, TotalHH_E, Affordability)
# Subset MapData for the table
Data_From_Map <- MapData %>%
st_set_geometry(NULL) %>% # remove geometry
select(ZIP, BR2, Rentals_E, TotalHH_E, Affordability)
# Create gt table
Data_From_Map_Table <- gt(Data_From_Map) %>%
tab_header(title = "Data") %>%
cols_align(align = "left")
Data_From_Map_Table
| Data | ||||
| ZIP | BR2 | Rentals_E | TotalHH_E | Affordability |
|---|---|---|---|---|
| 37064 | 2150 | 5035 | 23199 | Unaffordable |
| 37232 | 1770 | 0 | 0 | Unaffordable |
| 37072 | 1630 | 4138 | 13159 | Unaffordable |
| 37015 | 1460 | 1471 | 7347 | Unaffordable |
| 37090 | 1640 | 2081 | 6590 | Unaffordable |
| 37167 | 1640 | 8454 | 21855 | Unaffordable |
| 37179 | 2440 | 813 | 5570 | Unaffordable |
| 37153 | 1920 | 276 | 1883 | Unaffordable |
| 37013 | 1870 | 15919 | 36255 | Unaffordable |
| 37201 | 2360 | 452 | 885 | Unaffordable |
| 37025 | 1320 | 130 | 2172 | Affordable |
| 37206 | 1840 | 6453 | 12862 | Unaffordable |
| 37037 | 2180 | 290 | 2864 | Unaffordable |
| 37046 | 1880 | 161 | 2458 | Unaffordable |
| 37213 | 1830 | 13 | 13 | Unaffordable |
| 37067 | 2250 | 6236 | 13057 | Unaffordable |
| 37228 | 1730 | 1480 | 1492 | Unaffordable |
| 37160 | 1320 | 4817 | 13127 | Affordable |
| 37180 | 1320 | 259 | 1452 | Affordable |
| 37122 | 2200 | 4111 | 23409 | Unaffordable |
| 37128 | 1800 | 8900 | 26411 | Unaffordable |
| 37130 | 1470 | 12252 | 23323 | Unaffordable |
| 37238 | 1770 | 0 | 0 | Unaffordable |
| 37174 | 2050 | 4317 | 17221 | Unaffordable |
| 38401 | 1320 | 7003 | 25448 | Affordable |
| 37020 | 1360 | 140 | 1890 | Affordable |
| 37027 | 2440 | 3922 | 22708 | Unaffordable |
| 37210 | 1580 | 5337 | 7344 | Unaffordable |
| 37129 | 1800 | 6797 | 21677 | Unaffordable |
| 37115 | 1620 | 10268 | 17954 | Unaffordable |
| 37209 | 1950 | 10128 | 18570 | Unaffordable |
| 37086 | 1990 | 3637 | 12449 | Unaffordable |
| 37076 | 1780 | 7512 | 17136 | Unaffordable |
| 37219 | 2440 | 882 | 1158 | Unaffordable |
| 37080 | 1320 | 527 | 3028 | Affordable |
| 38476 | 1390 | 24 | 300 | Affordable |
| 37014 | 2250 | 0 | 1307 | Unaffordable |
| 37203 | 1990 | 9564 | 11834 | Unaffordable |
| 37204 | 2280 | 3077 | 6935 | Unaffordable |
| 37189 | 1480 | 258 | 1567 | Unaffordable |
| 37135 | 2600 | 421 | 7240 | Unaffordable |
| 37207 | 1450 | 6960 | 16034 | Unaffordable |
| 37205 | 2230 | 3727 | 11955 | Unaffordable |
| 37212 | 1820 | 4007 | 6938 | Unaffordable |
| 37060 | 1600 | 95 | 975 | Unaffordable |
| 37062 | 1690 | 870 | 4951 | Unaffordable |
| 37211 | 1780 | 15376 | 30135 | Unaffordable |
| 37214 | 2000 | 5623 | 14475 | Unaffordable |
| 37218 | 1580 | 1808 | 5945 | Unaffordable |
| 37220 | 2470 | 225 | 2410 | Unaffordable |
| 37085 | 1520 | 55 | 1878 | Unaffordable |
| 37215 | 2200 | 2588 | 10260 | Unaffordable |
| 37216 | 1810 | 2466 | 8597 | Unaffordable |
| 37217 | 1780 | 6660 | 12468 | Unaffordable |
| 37069 | 2600 | 787 | 6811 | Unaffordable |
| 37127 | 1560 | 1688 | 6988 | Unaffordable |
| 37132 | 1470 | 0 | 0 | Unaffordable |
| 37143 | 1490 | 222 | 1517 | Unaffordable |
| 37221 | 2200 | 5282 | 18787 | Unaffordable |
| 37149 | 1320 | 76 | 911 | Affordable |
| 37138 | 1730 | 1991 | 9485 | Unaffordable |
| 37208 | 1880 | 5888 | 8670 | Unaffordable |
| 37118 | 1320 | 0 | 494 | Affordable |
## Code
# Load Packages
library(tidyverse)
library(gt)
library(sf)
library(leaflet)
library(leafpop)
library(RColorBrewer)
library(classInt)
library(scales)
library(htmlwidgets)
library(tidycensus)
library(readxl)
# Define Nashville ZIP codes
ZIPList <- c(
"37135","37215","37064","37060","37014","37122","37027","37046","37221","37153",
"37210","37202","37024","37218","37062","37179","37025","37206","37065","37214",
"37067","37246","37068","37167","37069","37189","37070","37204","37072","37208",
"37076","37212","37080","37216","37085","37020","37086","38476","37089","37160",
"37090","37174","37115","37180","37116","37201","37118","37203","37015","37205",
"37127","37207","37128","37209","37129","37211","37130","37213","37220","37037",
"37222","37217","37228","37219","37232","37013","37131","37224","37132","37229",
"37133","37236","37238","37240","37243","37138","38401","37143","37011","37149"
)
# Read rent data
FMR_Area <- readxl::read_xlsx("rent.xlsx", .name_repair="universal") %>%
transmute(
ZIP = ZIP.Code,
Studio = SAFMR.0BR,
BR1 = SAFMR.1BR,
BR2 = SAFMR.2BR,
BR3 = SAFMR.3BR,
BR4 = SAFMR.4BR
) %>%
filter(ZIP %in% ZIPList) %>%
distinct()
# Load ZIP shapefile
ZIPMap <- st_read("cb_2020_us_zcta520_500k.shp") %>%
transmute(ZIP = ZCTA5CE20) %>%
filter(ZIP %in% ZIPList)
MapData <- ZIPMap %>% left_join(FMR_Area, by="ZIP")
# Get ACS rental data
ACS_Area <- get_acs(
geography="zcta",
variables=c(Rentals="B25032_013", TotalHH="B11001_001"),
year=2022
) %>%
select(GEOID, variable, estimate, moe) %>%
pivot_wider(names_from=variable, values_from=c(estimate, moe)) %>%
transmute(
ZIP = GEOID,
Rentals_E = estimate_Rentals,
Rentals_M = moe_Rentals,
TotalHH_E = estimate_TotalHH,
TotalHH_M = moe_TotalHH
) %>%
filter(ZIP %in% ZIPList)
MapData <- MapData %>% left_join(ACS_Area, by="ZIP")
# Map
pal <- colorNumeric(palette="RdPu", domain=MapData$BR2)
leaflet(MapData) %>%
addProviderTiles("CartoDB.Positron") %>%
addPolygons(
fillColor = ~pal(BR2),
weight = 1,
color = "white",
fillOpacity = 0.7,
popup = ~paste0(
"<b>ZIP:</b> ", ZIP,
"<br><b>Studio:</b> $", Studio,
"<br><b>1BR:</b> $", BR1,
"<br><b>2BR:</b> $", BR2,
"<br><b>3BR:</b> $", BR3,
"<br><b>4BR:</b> $", BR4,
"<br><br><b>Rental Homes:</b> ", Rentals_E,
"<br><b>Total Households:</b> ", TotalHH_E
)
)
# Affordability summary
MapData <- MapData %>%
mutate(Affordability = ifelse(BR2 <= 1400, "Affordable", "Unaffordable"))
Summary <- MapData %>%
st_set_geometry(NULL) %>%
group_by(Affordability) %>%
summarise(
Count = n(),
Minimum = min(BR2, na.rm=TRUE),
Average = round(mean(BR2, na.rm=TRUE)),
Maximum = max(BR2, na.rm=TRUE)
)
gt(Summary)
# Data table
Data_From_Map <- MapData %>%
st_set_geometry(NULL) %>%
select(ZIP, BR2, Rentals_E, TotalHH_E, Affordability)
gt(Data_From_Map)
```