New York City, particularly Manhattan, has experienced sustained rent increases over the last decades, fueling debates around affordability, displacement, and the role of rent regulation. In this project, I combine Zillow’s Observed Rent Index (ZORI) at the ZIP-code level with HUD Fair Market Rent (FMR) estimates for New York County to (1) describe recent rent trends and (2) model how a rent-freeze or rent-stabilization policy might alter the trajectory of Manhattan rents compared with a simple “free-market” trend.
The central question is:
How might a rent-freeze or rent-stabilization policy alter the trajectory of Manhattan rents compared with a continuation of recent free-market trends?
The project follows a data science workflow: data acquisition from at least two different sources, data cleaning and transformation, exploratory analysis, statistical modeling (linear regression), visualization, and scenario simulation, with all steps implemented in a R Markdown document and GitHub repository.
library(tidyverse)
library(lubridate)
library(janitor)
library(broom)
library(scales)
library(sf)
library(tigris)
options(tigris_use_cache = TRUE, tigris_quiet = TRUE)
File: Zip_zori_uc_sfrcondomfr_sm_month.csv (Zillow Observed Rent Index, by ZIP)
This file has monthly rent index values for all US ZIP codes, including Manhattan.
Use: aggregate Manhattan ZIP codes to obtain a Manhattan ZORI series.
File (cleaned from FMR_All_1983_2026.csv): hud_manhattan_fmr_1983_2026.csv
https://www.huduser.gov/portal/datasets/fmr.html#history
The file has annual 1-bedroom fair market rent estimates for New York County (Manhattan), 1983–2026.
Use: level-of-rent measure, suitable for modeling and policy interpretation.
In the GitHub repo, I included a short README documenting the original HUD file (FMR_All_1983_2026.csv) and how hud_manhattan_fmr_1983_2026.csv was derived.
ZORI: Manhattan ZIP-level aggregation
zori_raw <- read_csv("C:\\Users\\jpfer\\OneDrive\\Ambiente de Trabalho\\DATA\\DATA607_FINAL\\Zip_zori_uc_sfrcondomfr_sm_month.csv") %>%
clean_names()
glimpse(zori_raw)
## Rows: 7,822
## Columns: 139
## $ region_id <dbl> 91982, 61148, 91940, 62080, 91733, 93144, 62093, 92593, 62…
## $ size_rank <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…
## $ region_name <chr> "77494", "08701", "77449", "11368", "77084", "79936", "113…
## $ region_type <chr> "zip", "zip", "zip", "zip", "zip", "zip", "zip", "zip", "z…
## $ state_name <chr> "TX", "NJ", "TX", "NY", "TX", "TX", "NY", "TX", "NY", "CA"…
## $ state <chr> "TX", "NJ", "TX", "NY", "TX", "TX", "NY", "TX", "NY", "CA"…
## $ city <chr> "Katy", "Lakewood", "Katy", "New York", "Houston", "El Pas…
## $ metro <chr> "Houston-The Woodlands-Sugar Land, TX", "New York-Newark-J…
## $ county_name <chr> "Fort Bend County", "Ocean County", "Harris County", "Quee…
## $ x2015_01_31 <dbl> 1446.8002, NA, 1244.2791, NA, 1102.4754, NA, NA, 1191.1935…
## $ x2015_02_28 <dbl> 1449.9754, NA, 1239.8238, NA, 1101.0316, NA, NA, 1201.0065…
## $ x2015_03_31 <dbl> 1457.3223, NA, 1248.4911, NA, 1112.3693, NA, NA, 1189.1674…
## $ x2015_04_30 <dbl> 1463.5759, NA, 1261.4089, NA, 1124.8826, NA, NA, 1184.9496…
## $ x2015_05_31 <dbl> 1468.9021, NA, 1273.0660, NA, 1133.1871, NA, NA, 1195.9622…
## $ x2015_06_30 <dbl> 1471.0872, NA, 1280.2615, NA, 1137.9441, NA, NA, 1213.5454…
## $ x2015_07_31 <dbl> 1477.5318, NA, 1282.7293, NA, 1136.9382, NA, NA, 1226.3482…
## $ x2015_08_31 <dbl> 1474.704, NA, 1286.085, NA, 1140.568, NA, NA, 1230.189, NA…
## $ x2015_09_30 <dbl> 1469.909, NA, 1288.994, NA, 1143.718, NA, NA, 1227.268, NA…
## $ x2015_10_31 <dbl> 1461.8015, NA, 1289.9917, NA, 1140.2382, NA, NA, 1230.6343…
## $ x2015_11_30 <dbl> 1454.6089, NA, 1287.4252, NA, 1138.3489, NA, NA, 1231.0954…
## $ x2015_12_31 <dbl> 1435.9565, NA, 1284.6940, NA, 1122.7109, NA, NA, 1241.2530…
## $ x2016_01_31 <dbl> 1427.5525, NA, 1278.5443, NA, 1134.5566, NA, NA, 1248.2262…
## $ x2016_02_29 <dbl> 1415.6528, NA, 1279.6384, NA, 1130.7584, NA, NA, 1256.5079…
## $ x2016_03_31 <dbl> 1425.403, NA, 1287.758, NA, 1146.180, NA, NA, 1245.995, NA…
## $ x2016_04_30 <dbl> 1414.933, NA, 1297.081, NA, 1140.767, NA, NA, 1250.514, NA…
## $ x2016_05_31 <dbl> 1414.824, NA, 1301.661, NA, 1139.512, NA, 2284.553, 1245.4…
## $ x2016_06_30 <dbl> 1410.715, NA, 1294.295, NA, 1133.591, NA, 2308.115, 1269.0…
## $ x2016_07_31 <dbl> 1412.025, NA, 1289.616, NA, 1136.219, NA, 2337.909, 1266.5…
## $ x2016_08_31 <dbl> 1410.351, NA, 1288.997, NA, 1142.756, NA, 2361.412, 1275.8…
## $ x2016_09_30 <dbl> 1399.746, NA, 1288.225, NA, 1142.871, NA, 2357.086, 1270.5…
## $ x2016_10_31 <dbl> 1390.610, NA, 1290.279, NA, 1139.816, NA, 2364.387, 1285.3…
## $ x2016_11_30 <dbl> 1376.575, NA, 1280.864, NA, 1136.282, NA, 2338.299, 1284.6…
## $ x2016_12_31 <dbl> 1365.572, NA, 1274.790, NA, 1139.061, NA, 2333.129, 1278.1…
## $ x2017_01_31 <dbl> 1365.808, NA, 1268.084, NA, 1135.526, NA, 2318.531, 1268.4…
## $ x2017_02_28 <dbl> 1380.041, NA, 1274.928, NA, 1137.556, NA, 2325.558, 1272.8…
## $ x2017_03_31 <dbl> 1394.723, NA, 1283.053, NA, 1140.176, NA, 2335.981, 1291.5…
## $ x2017_04_30 <dbl> 1400.289, NA, 1290.670, NA, 1145.013, NA, 2328.524, 1300.4…
## $ x2017_05_31 <dbl> 1407.118, NA, 1294.291, NA, 1153.430, NA, 2350.962, 1299.8…
## $ x2017_06_30 <dbl> 1413.781, NA, 1299.233, NA, 1159.472, NA, 2341.931, 1295.0…
## $ x2017_07_31 <dbl> 1414.229, NA, 1305.663, NA, 1165.973, NA, 2369.777, 1296.4…
## $ x2017_08_31 <dbl> 1402.786, NA, 1307.512, NA, 1167.629, NA, 2373.884, 1301.9…
## $ x2017_09_30 <dbl> 1410.137, NA, 1311.191, NA, 1181.080, NA, 2415.484, 1311.9…
## $ x2017_10_31 <dbl> 1424.420, NA, 1311.993, NA, 1191.276, NA, 2381.105, 1308.7…
## $ x2017_11_30 <dbl> 1440.132, NA, 1318.096, NA, 1198.336, NA, 2374.852, 1308.2…
## $ x2017_12_31 <dbl> 1442.707, NA, 1321.271, NA, 1186.859, NA, 2343.978, 1304.6…
## $ x2018_01_31 <dbl> 1450.804, NA, 1332.028, NA, 1183.833, NA, 2341.108, 1306.6…
## $ x2018_02_28 <dbl> 1456.757, NA, 1335.999, NA, 1183.248, NA, 2326.196, 1317.5…
## $ x2018_03_31 <dbl> 1459.011, NA, 1338.352, NA, 1184.742, NA, 2329.436, 1325.0…
## $ x2018_04_30 <dbl> 1453.379, NA, 1341.847, NA, 1194.548, NA, 2346.135, 1337.5…
## $ x2018_05_31 <dbl> 1451.708, NA, 1347.464, NA, 1197.123, NA, 2362.407, 1336.9…
## $ x2018_06_30 <dbl> 1449.920, NA, 1354.598, NA, 1205.002, NA, 2378.892, 1340.1…
## $ x2018_07_31 <dbl> 1445.519, NA, 1354.421, NA, 1197.809, NA, 2387.779, 1340.2…
## $ x2018_08_31 <dbl> 1434.107, NA, 1356.465, NA, 1202.455, NA, 2395.117, 1345.1…
## $ x2018_09_30 <dbl> 1422.230, NA, 1351.830, NA, 1204.256, NA, 2398.030, 1345.3…
## $ x2018_10_31 <dbl> 1416.551, NA, 1348.769, NA, 1201.701, NA, 2415.601, 1344.5…
## $ x2018_11_30 <dbl> 1421.571, NA, 1351.335, NA, 1207.788, NA, 2403.728, 1347.9…
## $ x2018_12_31 <dbl> 1426.426, NA, 1349.136, NA, 1210.319, NA, 2408.551, 1351.9…
## $ x2019_01_31 <dbl> 1435.472, NA, 1348.123, NA, 1213.338, NA, 2388.051, 1351.4…
## $ x2019_02_28 <dbl> 1442.884, NA, 1346.534, NA, 1203.974, NA, 2416.820, 1354.9…
## $ x2019_03_31 <dbl> 1452.168, NA, 1354.193, NA, 1211.203, NA, 2400.920, 1357.9…
## $ x2019_04_30 <dbl> 1456.869, NA, 1358.175, NA, 1227.825, NA, 2422.674, 1370.4…
## $ x2019_05_31 <dbl> 1454.616, NA, 1367.373, NA, 1246.125, NA, 2403.171, 1372.3…
## $ x2019_06_30 <dbl> 1452.716, NA, 1371.754, NA, 1245.627, NA, 2464.130, 1372.7…
## $ x2019_07_31 <dbl> 1453.803, NA, 1378.452, NA, 1249.361, NA, 2492.731, 1376.9…
## $ x2019_08_31 <dbl> 1462.451, NA, 1372.463, NA, 1246.523, NA, 2518.579, 1378.1…
## $ x2019_09_30 <dbl> 1459.105, NA, 1372.359, NA, 1242.761, NA, 2498.805, 1378.8…
## $ x2019_10_31 <dbl> 1459.350, NA, 1368.019, NA, 1232.242, NA, 2475.134, 1370.2…
## $ x2019_11_30 <dbl> 1453.237, NA, 1367.710, NA, 1233.489, NA, 2502.388, 1374.1…
## $ x2019_12_31 <dbl> 1454.479, NA, 1370.378, NA, 1237.726, NA, 2517.854, 1375.3…
## $ x2020_01_31 <dbl> 1456.325, NA, 1378.370, NA, 1242.070, NA, 2541.607, 1382.5…
## $ x2020_02_29 <dbl> 1460.142, NA, 1386.334, NA, 1244.135, NA, 2538.894, 1389.0…
## $ x2020_03_31 <dbl> 1467.837, NA, 1396.295, NA, 1255.211, NA, 2513.429, 1402.8…
## $ x2020_04_30 <dbl> 1478.190, NA, 1397.715, NA, 1254.770, NA, 2491.226, 1409.3…
## $ x2020_05_31 <dbl> 1474.473, NA, 1392.266, NA, 1262.553, NA, 2497.093, 1407.3…
## $ x2020_06_30 <dbl> 1473.976, NA, 1391.953, NA, 1268.211, NA, 2496.551, 1399.8…
## $ x2020_07_31 <dbl> 1464.596, NA, 1401.503, NA, 1278.458, NA, 2495.054, 1411.4…
## $ x2020_08_31 <dbl> 1475.605, NA, 1416.730, NA, 1278.908, NA, 2441.667, 1411.6…
## $ x2020_09_30 <dbl> 1485.950, NA, 1426.217, NA, 1281.713, NA, 2415.618, 1415.1…
## $ x2020_10_31 <dbl> 1484.506, NA, 1429.091, NA, 1290.475, NA, 2386.132, 1407.7…
## $ x2020_11_30 <dbl> 1492.269, NA, 1436.227, NA, 1286.518, NA, 2351.549, 1403.0…
## $ x2020_12_31 <dbl> 1494.897, NA, 1449.687, NA, 1288.835, NA, 2317.752, 1406.0…
## $ x2021_01_31 <dbl> 1512.128, NA, 1457.041, NA, 1289.257, NA, 2296.447, 1420.2…
## $ x2021_02_28 <dbl> 1516.130, NA, 1462.780, NA, 1306.489, NA, 2292.093, 1439.6…
## $ x2021_03_31 <dbl> 1524.2300, NA, 1468.0635, NA, 1312.8552, NA, 2296.1242, 14…
## $ x2021_04_30 <dbl> 1540.408, NA, 1489.935, NA, 1326.116, NA, 2298.080, 1466.5…
## $ x2021_05_31 <dbl> 1568.4153, NA, 1524.0198, NA, 1347.3700, 1097.4105, 2330.7…
## $ x2021_06_30 <dbl> 1607.2526, NA, 1555.1001, NA, 1374.1984, 1099.3924, 2364.6…
## $ x2021_07_31 <dbl> 1645.1823, NA, 1595.8896, NA, 1422.5280, 1102.2117, 2419.3…
## $ x2021_08_31 <dbl> 1672.2333, NA, 1613.3790, NA, 1470.6691, 1109.6131, 2451.9…
## $ x2021_09_30 <dbl> 1685.947, NA, 1630.558, NA, 1509.692, 1125.083, 2500.281, …
## $ x2021_10_31 <dbl> 1695.503, NA, 1635.459, NA, 1497.741, 1140.405, 2535.905, …
## $ x2021_11_30 <dbl> 1710.171, NA, 1633.480, NA, 1470.685, 1152.101, 2562.069, …
## $ x2021_12_31 <dbl> 1718.437, NA, 1647.613, NA, 1469.201, 1145.456, 2580.434, …
## $ x2022_01_31 <dbl> 1722.402, NA, 1646.891, NA, 1481.813, 1147.210, 2593.613, …
## $ x2022_02_28 <dbl> 1726.550, NA, 1680.627, NA, 1494.386, 1151.600, 2610.630, …
## $ x2022_03_31 <dbl> 1734.239, NA, 1695.678, NA, 1493.579, 1189.061, 2627.994, …
## $ x2022_04_30 <dbl> 1756.982, NA, 1709.675, NA, 1503.126, 1196.425, 2674.330, …
## $ x2022_05_31 <dbl> 1780.543, NA, 1692.275, NA, 1527.481, 1205.682, 2740.078, …
## $ x2022_06_30 <dbl> 1806.270, NA, 1683.470, NA, 1549.629, 1223.331, 2848.817, …
## $ x2022_07_31 <dbl> 1820.159, NA, 1724.023, NA, 1564.761, 1261.369, 2910.319, …
## $ x2022_08_31 <dbl> 1822.025, NA, 1773.261, NA, 1556.269, 1284.484, 2972.619, …
## $ x2022_09_30 <dbl> 1824.311, NA, 1781.303, NA, 1552.759, 1296.962, 2974.355, …
## $ x2022_10_31 <dbl> 1817.565, NA, 1758.799, NA, 1538.325, 1281.977, 2989.222, …
## $ x2022_11_30 <dbl> 1799.883, NA, 1731.840, NA, 1536.698, 1287.654, 2964.024, …
## $ x2022_12_31 <dbl> 1791.422, NA, 1737.455, NA, 1527.176, 1288.302, 2942.171, …
## $ x2023_01_31 <dbl> 1802.658, NA, 1730.481, NA, 1532.546, 1321.182, 2896.901, …
## $ x2023_02_28 <dbl> 1809.891, NA, 1736.672, NA, 1531.679, 1335.557, 2886.893, …
## $ x2023_03_31 <dbl> 1821.685, NA, 1749.184, NA, 1541.298, 1342.261, 2894.087, …
## $ x2023_04_30 <dbl> 1810.794, NA, 1766.300, NA, 1546.197, 1328.999, 2932.117, …
## $ x2023_05_31 <dbl> 1823.360, NA, 1774.519, NA, 1558.093, 1334.020, 2968.485, …
## $ x2023_06_30 <dbl> 1829.445, NA, 1779.943, NA, 1563.637, 1341.997, 2994.945, …
## $ x2023_07_31 <dbl> 1835.481, NA, 1779.375, NA, 1564.885, 1364.479, 3008.702, …
## $ x2023_08_31 <dbl> 1841.528, NA, 1787.802, NA, 1567.733, 1367.936, 3025.773, …
## $ x2023_09_30 <dbl> 1841.055, NA, 1782.795, NA, 1565.781, 1364.828, 3046.992, …
## $ x2023_10_31 <dbl> 1837.556, NA, 1772.783, NA, 1586.204, 1364.943, 3056.806, …
## $ x2023_11_30 <dbl> 1819.829, NA, 1764.091, NA, 1580.264, 1367.063, 3046.013, …
## $ x2023_12_31 <dbl> 1806.918, NA, 1766.641, NA, 1578.072, 1374.941, 3002.993, …
## $ x2024_01_31 <dbl> 1800.949, NA, 1777.752, NA, 1570.595, 1375.062, 2997.165, …
## $ x2024_02_29 <dbl> 1813.652, NA, 1793.031, NA, 1577.023, 1370.036, 2992.146, …
## $ x2024_03_31 <dbl> 1820.445, NA, 1805.813, NA, 1579.879, 1368.061, 3046.819, …
## $ x2024_04_30 <dbl> 1834.667, NA, 1817.664, NA, 1587.172, 1371.301, 3056.582, …
## $ x2024_05_31 <dbl> 1836.971, NA, 1810.956, NA, 1598.067, 1389.360, 3118.848, …
## $ x2024_06_30 <dbl> 1849.299, NA, 1816.906, NA, 1602.706, 1400.761, 3153.278, …
## $ x2024_07_31 <dbl> 1854.336, NA, 1822.882, NA, 1594.248, 1408.468, 3212.852, …
## $ x2024_08_31 <dbl> 1857.926, NA, 1829.623, NA, 1582.846, 1401.370, 3219.831, …
## $ x2024_09_30 <dbl> 1845.631, NA, 1815.186, NA, 1586.176, 1397.937, 3181.727, …
## $ x2024_10_31 <dbl> 1834.394, NA, 1803.616, NA, 1598.516, 1387.582, 3133.624, …
## $ x2024_11_30 <dbl> 1829.682, NA, 1801.632, NA, 1602.917, 1394.823, 3073.222, …
## $ x2024_12_31 <dbl> 1826.529, NA, 1808.801, NA, 1599.218, 1407.910, 3052.154, …
## $ x2025_01_31 <dbl> 1826.611, NA, 1816.554, NA, 1588.774, 1434.633, 3095.916, …
## $ x2025_02_28 <dbl> 1826.097, NA, 1834.912, NA, 1605.035, 1447.890, 3134.039, …
## $ x2025_03_31 <dbl> 1833.236, NA, 1849.753, NA, 1606.771, 1441.400, 3187.523, …
## $ x2025_04_30 <dbl> 1835.823, NA, 1852.464, NA, 1609.123, 1413.003, 3187.406, …
## $ x2025_05_31 <dbl> 1839.497, NA, 1843.621, NA, 1586.395, 1411.020, 3252.595, …
## $ x2025_06_30 <dbl> 1836.635, NA, 1836.464, NA, 1587.864, 1421.998, 3302.803, …
## $ x2025_07_31 <dbl> 1842.943, NA, 1832.727, NA, 1584.815, 1444.418, 3370.108, …
## $ x2025_08_31 <dbl> 1847.824, NA, 1824.652, NA, 1594.554, 1443.283, 3347.434, …
## $ x2025_09_30 <dbl> 1843.482, NA, 1815.695, NA, 1592.930, 1442.032, 3326.396, …
## $ x2025_10_31 <dbl> 1834.158, 2448.250, 1815.290, 2675.000, 1587.546, 1439.824…
The ZORI file includes columns such as: region_id,
region_name (ZIP code),
city,county_name, state, and
Monthly columns like 2015-01-31, 2015-02-28,
…
I will keep only New York County (Manhattan) ZIPs, pivot to long format, and aggregate by year:
zori_manhattan <- zori_raw %>%
filter(county_name == "New York County", state == "NY") %>%
pivot_longer(
cols = -c(
region_id,
size_rank,
region_name,
region_type,
state_name,
state,
city,
metro,
county_name
),
names_to = "date",
values_to = "zori"
) %>%
mutate(
# clean the column names that became "date"
date = gsub("^x", "", date),
date = gsub("_", "-", date),
date = as.Date(date, format = "%Y-%m-%d"),
year = lubridate::year(date)
) %>%
group_by(year) %>%
summarize(
zori_mean = mean(zori, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(year)
zori_manhattan
HUD FMR: Manhattan 1-bedroom time series
hud_nyc <- read_csv("C:\\Users\\jpfer\\OneDrive\\Ambiente de Trabalho\\DATA\\DATA607_FINAL\\hud_manhattan_fmr_1983_2026.csv") %>%
clean_names() %>%
mutate(
year = as.integer(year),
fmr_1br = as.numeric(fmr_1br)
) %>%
arrange(year)
hud_nyc
For this analysis, I will focus on the modern period where both datasets overlap reasonably well, e.g., 2015–2024.
rent_combined <- hud_nyc %>%
inner_join(zori_manhattan, by = "year") %>%
filter(year >= 2015, year <= 2024) %>%
arrange(year)
rent_combined
This table is the primary modeling dataset, with: - year
- fmr_1br (HUD Fair Market Rent, NYC County) -
zori_mean (average Manhattan ZORI for that year)
ggplot(hud_nyc %>% filter(year >= 2000), aes(x = year, y = fmr_1br)) +
geom_line() +
geom_point() +
scale_y_continuous(labels = dollar_format()) +
labs(
title = "HUD Fair Market Rent (1BR) – New York County (2000–2026)",
x = "Year",
y = "FMR (1-bedroom, USD)",
caption = "Source: HUD FMR All 1983–2026 (cleaned for New York County)"
)
ggplot(rent_combined, aes(x = year)) +
geom_line(aes(y = fmr_1br, color = "HUD FMR 1BR")) +
geom_line(aes(y = zori_mean, color = "ZORI (Manhattan mean)")) +
scale_y_continuous(labels = dollar_format()) +
scale_color_manual(values = c("HUD FMR 1BR" = "black", "ZORI (Manhattan mean)" = "blue")) +
labs(
title = "HUD FMR vs ZORI – Manhattan (2015–2024)",
x = "Year",
y = "Rent Level / Index (USD-equivalent)",
color = "Series",
caption = "Source: HUD FMR (New York County) & Zillow ZORI (Manhattan ZIPs)"
)
They seem to have a similar pattern with exception for the COVID period despite different values.
I use a linear regression of FMR on year for the period 2015–2024. This serves as a stylized representation of a “free-market trend.”
rent_model_data <- rent_combined %>%
mutate(year_centered = year - mean(year))
lm_free_market <- lm(fmr_1br ~ year_centered, data = rent_model_data)
summary(lm_free_market)
##
## Call:
## lm(formula = fmr_1br ~ year_centered, data = rent_model_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -122.818 -58.788 1.564 43.286 153.945
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1737.200 27.428 63.34 4.29e-12 ***
## year_centered 124.412 9.549 13.03 1.14e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 86.74 on 8 degrees of freedom
## Multiple R-squared: 0.955, Adjusted R-squared: 0.9494
## F-statistic: 169.7 on 1 and 8 DF, p-value: 1.143e-06
tidy(lm_free_market)
glance(lm_free_market)
The linear regression model indicates a strong and statistically significant upward trend in Manhattan’s one-bedroom Fair Market Rent between 2015 and 2024. The estimated slope of 124.4 suggests that rents increased by roughly $124 per year during this period (p < 0.001). The model explains approximately 95% of the variation in annual FMR levels, indicating an exceptionally good fit for a simple time-trend model. These results support the idea that Manhattan rents have risen steadily under free-market conditions, providing a useful baseline for comparing alternative policy scenarios such as a rent freeze or rent stabilization cap. Actual annual FMR values are, on average, about $87 away from the fitted trend line.
rent_model_aug <- augment(lm_free_market, data = rent_model_data)
ggplot(rent_model_aug, aes(x = year, y = fmr_1br)) +
geom_point(color = "black") +
geom_line(aes(y = .fitted), color = "red") +
scale_y_continuous(labels = dollar_format()) +
labs(
title = "Free-Market Linear Trend vs Actual FMR – Manhattan (2015–2024)",
x = "Year",
y = "HUD FMR (1BR, USD)",
caption = "Black = actual FMR; red = fitted trend line."
)
We now project the model forward 2025–2030 and compare:
future_years <- tibble(
year = 2025:2030
) %>%
mutate(
year_centered = year - mean(rent_model_data$year)
)
free_market_pred <- augment(
lm_free_market,
newdata = future_years
) %>%
transmute(
year,
fmr_1br = .fitted,
scenario = "Free market"
)
free_market_pred
fmr_2024 <- rent_model_data %>%
filter(year == 2024) %>%
pull(fmr_1br)
freeze_scenario <- future_years %>%
transmute(
year,
fmr_1br = fmr_2024,
scenario = "Rent freeze"
)
stabilized_scenario <- future_years %>%
transmute(
year,
fmr_1br = fmr_2024 * (1.01 ^ (year - 2024)),
scenario = "Rent stabilization (1% cap)"
)
historical <- hud_nyc %>%
filter(year >= 2015, year <= 2024) %>%
transmute(
year,
fmr_1br,
scenario = "Historical"
)
scenario_all <- bind_rows(
historical,
free_market_pred,
freeze_scenario,
stabilized_scenario
)
scenario_all
ggplot(
scenario_all,
aes(x = year, y = fmr_1br, color = scenario)
) +
geom_line(size = 1.1) +
geom_point(data = subset(scenario_all, scenario == "Historical")) +
scale_y_continuous(labels = dollar_format()) +
labs(
title = "Manhattan 1BR Fair Market Rent: Historical vs Policy Scenarios",
x = "Year",
y = "FMR (1BR, USD)",
color = "Scenario",
caption = "Historical FMR from HUD; projections via linear trend and policy assumptions."
)
scenario_2030 <- scenario_all %>%
filter(year == 2030) %>%
select(year, scenario, fmr_1br) %>%
arrange(desc(fmr_1br))
scenario_2030
baseline_2030 <- scenario_2030 %>%
filter(scenario == "Free market") %>%
pull(fmr_1br)
scenario_2030 %>%
mutate(
diff_vs_free_market = fmr_1br - baseline_2030,
diff_vs_free_market_pct = diff_vs_free_market / baseline_2030
)
The scenario analysis for 2030 compares projected Manhattan Fair Market Rents (FMR) under three different conditions: a continuation of free-market trends, a rent-stabilization policy with a 1% annual cap, and a full rent freeze beginning in 2024. The free-market forecast—based on the statistically significant linear trend estimated from 2015–2024—projects a 1-bedroom FMR of approximately $3,043 by 2030. Under a rent stabilization policy limiting annual increases to 1%, the projected 2030 FMR falls to about $2,602, which is roughly $442 lower than the free-market projection. This represents a 14.6% reduction relative to the free-market level. The rent freeze scenario generates the largest divergence: by holding rents constant at the 2024 level, the model estimates a 2030 FMR of $2,451, which is $593 below the free-market projection—a 19.5% reduction in rent levels.
Overall, these results suggest that policy interventions have a meaningful potential impact on long-term housing affordability. Even modest stabilization policies significantly slow the pace of rent increases, while a full rent freeze produces substantial divergence from expected free-market trends over a six-year period. Although the model is not causal and does not account for broader economic or market feedback effects, the projections provide a clear quantitative illustration of how rent-regulation strategies could reshape Manhattan’s rental landscape by 2030.
Below you can find a simple geospatial map using Census shapefiles to highlight Manhattan and annotate it with the latest FMR level.
# Get NY state counties shapefile
ny_counties <- counties(state = "NY", cb = TRUE, year = 2023) %>%
st_transform(4326)
# Filter for Manhattan (New York County)
manhattan_sf <- ny_counties %>%
filter(NAME == "New York")
# Latest FMR value
latest_fmr <- hud_nyc %>%
filter(year == max(year)) %>%
pull(fmr_1br)
manhattan_sf <- manhattan_sf %>%
mutate(fmr1_latest = latest_fmr)
ggplot(manhattan_sf) +
geom_sf(aes(fill = fmr1_latest), color = "white", size = 0.4) +
scale_fill_gradient(
name = "FMR (1BR, USD)",
labels = scales::dollar_format()
) +
labs(
title = "Manhattan (New York County)",
subtitle = paste0("Latest HUD Fair Market Rent for 1BR Units: ",
scales::dollar(latest_fmr)),
caption = "Spatial Feature created using Census shapefiles via tigris/sf."
) +
theme_minimal(base_size = 14) +
theme(
axis.title = element_blank(),
axis.text = element_blank(),
axis.ticks = element_blank(),
panel.grid = element_blank(),
plot.title = element_text(face = "bold", size = 18),
plot.subtitle = element_text(size = 13)
)
The regression results show that Manhattan’s 1-bedroom Fair Market Rent (FMR) increased by about $124 per year from 2015–2024, with an R² of 0.955, indicating a very consistent upward trend. Using this trend to project rents forward, the free-market model estimates a 2030 FMR of $3,043.
The policy scenarios reveal meaningful differences. A 1% rent-stabilization cap produces a projected 2030 rent of $2,602, which is $442 (14.6%) lower than the free-market estimate. A rent freeze results in an even larger reduction, with a 2030 rent level of $2,451, or $593 (19.5%) below the free-market projection. These results suggest that even modest regulation substantially slows rent growth, and a full freeze would produce the largest affordability gains.
While this analysis does not model causal effects or market feedback responses, it clearly illustrates how different policy approaches could reshape Manhattan’s rent trajectory. Even simple regulatory interventions lead to noticeably lower projected rents compared to an unregulated free-market path.
This project combines two independent sources of rental information—Zillow ZORI and HUD Fair Market Rents—to describe Manhattan’s rent trajectory and explore the implications of hypothetical rent-freeze and rent-stabilization policies. A simple linear model suggests continued rent growth under a free-market scenario, while simulated policy interventions yield substantially lower projected rent levels by 2030. While stylized, this analysis illustrates how publicly available data can be used to inform debates about housing affordability and rent regulation in New York City.