Here’s a quick map of the U.S. Department of Housing and Urban
Development’s current two-bedroom Fair Market Rent estimates for the
four Rutherford County ZIP codes closest to MTSU’s campus. Below the
map, tables show estimates for all rental unit sizes at present, five
years ago, and the change since five years ago.
Code:
# ==========================================================
# 1. Install and load required packages
# ==========================================================
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("httr")) install.packages("httr")
if (!require("openxlsx")) install.packages("openxlsx")
if (!require("kableExtra")) install.packages("kableExtra")
if (!require("tidycensus")) install.packages("tidycensus")
if (!require("sf")) install.packages("sf")
if (!require("leaflet")) install.packages("leaflet")
library(tidyverse)
library(httr)
library(openxlsx)
library(kableExtra)
library(tidycensus)
library(sf)
library(leaflet)
options(tigris_use_cache = TRUE)
# ==========================================================
# 2. Define URLs (manually editable)
# ==========================================================
url_current <- "https://www.huduser.gov/portal/datasets/fmr/fmr2026/fy2026_safmrs_revised.xlsx"
url_previous <- "https://www.huduser.gov/portal/datasets/fmr/fmr2023/fy2023_safmrs_revised.xlsx"
file_current <- "Current_data.xlsx"
file_previous <- "Previous_data.xlsx"
# ==========================================================
# 3. Download CURRENT dataset
# ==========================================================
res_current <- GET(
url_current,
write_disk(file_current, overwrite = TRUE),
add_headers(
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept" = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Referer" = "https://www.huduser.gov/",
"Connection" = "keep-alive"
)
)
print(status_code(res_current))
if (!file.exists(file_current) || file.info(file_current)$size == 0) {
stop("Current file download failed or is empty")
}
# ==========================================================
# 4. Download PREVIOUS dataset
# ==========================================================
res_previous <- GET(
url_previous,
write_disk(file_previous, overwrite = TRUE),
add_headers(
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept" = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Referer" = "https://www.huduser.gov/",
"Connection" = "keep-alive"
)
)
print(status_code(res_previous))
if (!file.exists(file_previous) || file.info(file_previous)$size == 0) {
stop("Previous file download failed or is empty")
}
# ==========================================================
# 5. Read Excel files
# ==========================================================
current_data <- read.xlsx(file_current, sheet = 1)
previous_data <- read.xlsx(file_previous, sheet = 1)
# ==========================================================
# 6. Clean and standardize variables
# ==========================================================
current_clean <- current_data %>%
rename_with(~ str_replace_all(.x, " ", "_")) %>%
mutate(ZIP.Code = as.character(ZIP.Code)) %>%
select(
ZIP = ZIP.Code,
Studio = SAFMR.0BR,
OneBR = SAFMR.1BR,
TwoBR = SAFMR.2BR,
ThreeBR = SAFMR.3BR,
FourBR = SAFMR.4BR
) %>%
distinct(ZIP, .keep_all = TRUE) %>%
rename_with(~ paste0(.x, "_Current"), -ZIP)
previous_clean <- previous_data %>%
rename_with(~ str_replace_all(.x, " ", "_")) %>%
mutate(ZIP.Code = as.character(ZIP.Code)) %>%
select(
ZIP = ZIP.Code,
Studio = SAFMR.0BR,
OneBR = SAFMR.1BR,
TwoBR = SAFMR.2BR,
ThreeBR = SAFMR.3BR,
FourBR = SAFMR.4BR
) %>%
distinct(ZIP, .keep_all = TRUE) %>%
rename_with(~ paste0(.x, "_Previous"), -ZIP)
# ==========================================================
# 7. Merge datasets
# ==========================================================
merged_data <- current_clean %>%
left_join(previous_clean, by = "ZIP")
# ==========================================================
# 8. Calculate changes
# ==========================================================
merged_data <- merged_data %>%
mutate(
Studio_Change = Studio_Current - Studio_Previous,
OneBR_Change = OneBR_Current - OneBR_Previous,
TwoBR_Change = TwoBR_Current - TwoBR_Previous,
ThreeBR_Change = ThreeBR_Current - ThreeBR_Previous,
FourBR_Change = FourBR_Current - FourBR_Previous
)
# ==========================================================
# 9. Filter data
# ==========================================================
ZIPList <- c("37127","37128","37129","37130","37132")
filtered_data <- merged_data %>%
filter(ZIP %in% ZIPList)
# ==========================================================
# 10. Tables (kableExtra)
# ==========================================================
# Current
filtered_data %>%
select(ZIP, ends_with("_Current")) %>%
mutate(across(-ZIP, scales::dollar)) %>%
kable(caption = "Current Fair Market Rents") %>%
kable_styling(full_width = FALSE)
# Previous
filtered_data %>%
select(ZIP, ends_with("_Previous")) %>%
mutate(across(-ZIP, scales::dollar)) %>%
kable(caption = "Previous Fair Market Rents") %>%
kable_styling(full_width = FALSE)
# Change
filtered_data %>%
select(ZIP, ends_with("_Change")) %>%
mutate(across(-ZIP, scales::dollar)) %>%
kable(caption = "Change in Fair Market Rents") %>%
kable_styling(full_width = FALSE)
# ==========================================================
# 11. Get ZIP geometry
# ==========================================================
zcta_map <- get_acs(
geography = "zcta",
variables = "B01001_001",
year = 2020,
geometry = TRUE
)
# ==========================================================
# 12. Join data to map
# ==========================================================
map_data <- zcta_map %>%
rename(ZIP = GEOID) %>%
left_join(merged_data, by = "ZIP") %>%
filter(ZIP %in% ZIPList)
# ==========================================================
# 13. Create improved popup labels
# ==========================================================
map_data <- map_data %>%
mutate(
popup = paste0(
"<strong>ZIP: ", ZIP, "</strong><br><br>",
"<strong>Current (2BR):</strong> ", scales::dollar(TwoBR_Current), "<br>",
"<strong>Previous (2BR):</strong> ", scales::dollar(TwoBR_Previous), "<br>",
"<strong>Change:</strong> ", scales::dollar(TwoBR_Change)
)
)
# ==========================================================
# 14. Fix CRS (important)
# ==========================================================
map_data <- st_transform(map_data, 4326)
# ==========================================================
# 15. Improved color palette
# ==========================================================
pal_current <- colorNumeric(
palette = "viridis",
domain = map_data$TwoBR_Current,
na.color = "transparent"
)
# ==========================================================
# 16. Leaflet map (Current only, improved styling)
# ==========================================================
leaflet(map_data) %>%
addProviderTiles("CartoDB.Voyager") %>%
addPolygons(
fillColor = ~pal_current(TwoBR_Current),
color = "black", # ✅ thin black borders
weight = 0.7,
fillOpacity = 0.6, # ✅ slightly stronger fill
popup = ~popup
) %>%
addLegend(
pal = pal_current,
values = ~TwoBR_Current,
title = "Current (2BR FMR)",
position = "topright"
)