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, three
years ago, and the change since three years ago.
Code:
# ==========================================================
# 1. Install and load required packages
# ==========================================================
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("httr2")) install.packages("httr2")
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(httr2)
library(openxlsx)
library(kableExtra)
library(tidycensus)
library(sf)
library(leaflet)
options(tigris_use_cache = TRUE)
# ==========================================================
# 2. Define URLs (manually editable)
# Get latest URLs from:
# https://www.huduser.gov/portal/datasets/fmr/smallarea/index.html
# ==========================================================
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 <- request(url_current) |>
req_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"
) |>
req_perform(path = file_current)
print(resp_status(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 <- request(url_previous) |>
req_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"
) |>
req_perform(path = file_previous)
print(resp_status(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
# ==========================================================
# Current
Current <- filtered_data %>%
select(ZIP, ends_with("_Current")) %>%
mutate(across(-ZIP, scales::dollar)) %>%
kable(caption = "Current Fair Market Rents") %>%
kable_styling(full_width = FALSE)
Current
# Previous
Previous <- filtered_data %>%
select(ZIP, ends_with("_Previous")) %>%
mutate(across(-ZIP, scales::dollar)) %>%
kable(caption = "Previous Fair Market Rents") %>%
kable_styling(full_width = FALSE)
Previous
# Change
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)
Change
# ==========================================================
# 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 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
# ==========================================================
map_data <- st_transform(map_data, 4326)
# ==========================================================
# 15. Color palette
# ==========================================================
pal_current <- colorNumeric(
palette = "viridis",
domain = map_data$TwoBR_Current,
na.color = "transparent"
)
# ==========================================================
# 16. Leaflet map
# ==========================================================
Map <- leaflet(map_data) %>%
addProviderTiles("CartoDB.Voyager") %>%
addPolygons(
fillColor = ~pal_current(TwoBR_Current),
color = "black",
weight = 0.7,
fillOpacity = 0.6,
popup = ~popup
) %>%
addLegend(
pal = pal_current,
values = ~TwoBR_Current,
title = "Current (2BR FMR)",
position = "topright"
)
Map