Nashville Housing Market Analysis

Nashvilles two-bedroom rents show widespread unaffordability across the metro area.

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. —

Load Packages

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)

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

Download HUD SAFMR Rent Data

download.file(
"https://www.huduser.gov/portal/datasets/fmr/fmr2026/fy2026_safmrs.xlsx",
"rent.xlsx",
mode = "wb"
)

Read & Clean Rent Data

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()

Download ZIP Code Shapefile

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)

Load ZIP Code Map

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

Join Rent Data to Zip Map

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)

Join ACS Data to MapData

MapData <- MapData %>%
  left_join(ACS_Area, by = "ZIP")

Map of Two-Bedroom Fair Market Rent

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

ZIP Code Affordability Table

# 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)

```