The analysis presented here appears in this blog post:
Rental Assistance Need in Five of New York’s Mid-Sized Cities
The dataset used here is prepared separately.
# Install required packages
pkgs <- c(
"tidyverse",
"hrbrthemes",
"rmarkdown",
"jsonlite",
"remotes",
"srvyr",
"knitr",
"furrr",
"gt",
"fs",
)
install.packages(pkgs)
remotes::install_github("mikeasilva/blsAPI")
library(tidyverse) # general data manipulation and graphing
library(scales) # number formatting
library(srvyr) # survey functions
library(gt) # making tables
library(furrr) # parallel processing
# Load custom theme function to help with plotting
source("R/plot-helpers.R")
# No scientific notation in outputs
options(scipen = 999)
# To deal with the random assignment of job loss and UI takeup for individuals,
# we need to run generate the results multiple times with different random seeds
# and then average the results. To speed this up we use {furrr} to run this in
# parallel across multiple cores.
plan(multiprocess)
ipums_clean <- read_rds("data/ipums_clean.rds")
# Number of iterations to run when compiling results
ITERATIONS <- as.integer(params$iterations)
# We manage the random number generator seed directly, so we can ignore warnings from the package that handles the parallelization
options(future.rng.onMisuse = "ignore")
In our analysis we randomly assign individuals in the data to job loss status and UI recipiency with probabilities based on the industry-specific job loss rates and the UI benefit recipiency rate. To ensure that our final results are not unduly influenced by random variation in those assignment, we repeat the analysis 100 times and average the results.
# Assumption about % of people who lost jobs that will receive UI benefits
UI_TAKEUP_RATE <- 0.67
Not every person who losses their job will receive unemployment insurance. The reasons for this include:
While we can test for income eligibility in the data, we have to rely on this assumption about overall take up rate to account the other factors in our analysis.
For this analysis we are following the work from The Century Foundation and using a UI recipiency rate of 67% for New York State.
There are two functions that add UI takeup and rental assistance need variables to the clean IPUMS dataset. These are part of the steps that are repeated for many iterations of the simulations.
source("R/add-ui-and-need-vars.R")
This function takes a dataframe as created by the above add_risk_vars()
function that has then been filtered to only one row per household (eg. filter(pernum == 1)
), and summarizes the household level variables (using survey weights) of interest for a single month.
# Preset some arguments for cleaner code below
survey_total_ci90 <- partial(survey_total, vartype = "ci", level = 0.90)
survey_mean_ci90 <- partial(survey_mean, vartype = "ci", level = 0.90)
city_order <- c(
"Albany", "Buffalo", "Rochester", "Syracuse", "Yonkers",
"Total"
)
# When using the {srvyr} package we get separate columns for the lower and
# upper bounds of the confidence interval. This little helper function creates
# a single margin of error column.
add_moe <- function(.data) {
.data %>%
pivot_longer(is.numeric) %>%
mutate(
value_type = case_when(
str_detect(name, "_low") ~ "low",
str_detect(name, "_upp") ~ "upp",
TRUE ~ "est"
),
name = str_remove(name, "(_low|_upp)")
) %>%
pivot_wider(names_from = value_type, values_from = value) %>%
mutate(moe_90 = upp - est) %>%
select(-low, -upp) %>%
pivot_longer(is.numeric, names_to = "value_type") %>%
unite(name_type, name, value_type) %>%
pivot_wider(names_from = name_type, values_from = value)
}
We create a lot of different tables to display our results, and there are a few common tasks we need to do with them all so it’s helpful to separate this code out into small functions to use in each table.
It’s useful to have MOE columns in the tables when drafting the write up, but for displaying the final tables it is overwhelming to have so many numbers so we also want to have versions with the MOEs hidden. We’ve used R Markdown’s “parameters” feature to make it easy to render the document with or without MOE columns.
# Hide margin of error columns in tables?
HIDE_MOE <- as.logical(params$hide_moe)
# Annoyingly, you can't hide the col labels and have spanners, so we just make a
# list of all the column names and set the label to "" and plug this into
# cols_label(.list = hide_cls_list(.data))
hide_cols_list <- function(.data) {
.data %>%
select(is.numeric) %>%
names() %>%
{set_names(rep("", length(.)), .)} %>%
as.list()
}
# {gt} has a useful function for hiding columns, so we just make a little
# helper so we can show/hide based on a boolean defined above.
hide_moe_cols <- function(.data, hide_moe = FALSE) {
if (hide_moe) {
cols_hide(.data, ends_with("_moe_90"))
} else {
.data
}
}
# For each table we include a total row and so it's helpful to style it
# differently for readability.
highlight_total <- function(.data) {
.data %>%
tab_style(
style = cell_fill(color = "#D3D3D3"),
locations = list(
cells_stub(rows = matches("Total")),
cells_body(rows = city == "Total")
)
)
}
# Data prep for Tables 1-2
household_summarise_stats <- function(.data) {
.data %>%
filter(
pernum == 1, # keep only one row per household
hh_any_risk # keep only vulnerable households
) %>%
as_survey_design(weights = hhwt) %>%
group_by(city) %>%
summarise(
renter_households = survey_total_ci90(1),
lost_wages_agg_monthly = survey_total_ci90(hh_risk_wages/12),
ui_benefits_reg_agg_monthly = survey_total_ci90(hh_ui_benefits_month_reg),
ui_benefits_extra300_agg_monthly = survey_total_ci90(hh_ui_benefits_month_extra300),
ui_benefits_all300_agg_monthly = survey_total_ci90(hh_ui_benefits_month_all300),
rent_monthly_tot = survey_total_ci90(gross_rent_nom, na.rm = TRUE),
rent_monthly_avg = survey_mean_ci90(gross_rent_nom, na.rm = TRUE)
) %>%
ungroup()
}
ui_percent_stats <- function(.data) {
.data %>%
filter(
pernum == 1, # keep only one row per household
# Keep all renter households, not just ones with income loss
) %>%
as_survey_design(weights = hhwt) %>%
group_by(city) %>%
summarise(
renter_hh_ui_pct = survey_mean_ci90(hh_any_risk & hh_ui_benefits_month_reg > 0),
renter_hh_noui_pct = survey_mean_ci90(!is.na(hh_any_risk) & hh_any_risk & hh_ui_benefits_month_reg == 0)
)
}
prep_household_data <- function(seed, .data) {
set.seed(seed)
data_w_need <- .data %>%
mutate(risk_group = runif(n()) < job_loss_pct) %>%
add_ui_takeup(UI_TAKEUP_RATE) %>%
add_need_vars()
data_all <- data_w_need %>%
household_summarise_stats() %>%
mutate(type = "all")
data_all_ui_pct <- data_w_need %>%
ui_percent_stats() %>%
mutate(type = "all_ui_pct")
data_sevburden <- data_w_need %>%
# Include only those who were severely rent burdened pre-covid
filter(is_rent_burdened_sev) %>%
household_summarise_stats() %>%
mutate(type = "sevburden")
data_sevburden_pct <- data_w_need %>%
filter(
pernum == 1, # keep only one row per household
hh_any_risk # keep only vulnerable households
) %>%
as_survey_design(weights = hhwt) %>%
group_by(city) %>%
summarise(
sevburden_pct = survey_mean_ci90(is_rent_burdened_sev)
) %>%
mutate(type = "sevburden_pct")
data_all_noui <- data_w_need %>%
# Include only those who don't get UI benefits
filter(hh_ui_benefits_month_reg == 0) %>%
household_summarise_stats() %>%
mutate(type = "all_noui")
data_all_ui <- data_w_need %>%
# Include only those who did get UI benefits
filter(hh_ui_benefits_month_reg > 0) %>%
household_summarise_stats() %>%
mutate(type = "all_ui")
data_lt80ami_ui_pct <- data_w_need %>%
# Include only households with income below 80% AMI
filter(hh_inc_nom > hud_inc_lim80) %>%
ui_percent_stats() %>%
mutate(type = "lt80ami_ui_pct")
data_lt80ami_noui <- data_w_need %>%
# Include only households with income below 80% AMI
filter(hh_inc_nom > hud_inc_lim80) %>%
# Include only those who don't get UI benefits
filter(hh_ui_benefits_month_reg == 0) %>%
household_summarise_stats() %>%
mutate(type = "lt80ami_noui")
data_lt80ami_ui <- data_w_need %>%
# Include only households with income below 80% AMI
filter(hh_inc_nom > hud_inc_lim80) %>%
# Include only those who did get UI benefits
filter(hh_ui_benefits_month_reg > 0) %>%
household_summarise_stats() %>%
mutate(type = "lt80ami_ui")
bind_rows(
data_all,
data_all_ui_pct,
data_sevburden,
data_sevburden_pct,
data_all_noui,
data_all_ui,
data_lt80ami_ui_pct,
data_lt80ami_noui,
data_lt80ami_ui
) %>%
mutate(seed = seed)
}
household_data_multi_cities <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = prep_household_data,
.data = filter(ipums_clean, !is.na(city), is_renter)
)
household_data_multi_all_cities <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = prep_household_data,
.data = ipums_clean %>%
filter(!is.na(city), is_renter) %>%
mutate(city = "Total")
)
household_data <- household_data_multi_cities %>%
bind_rows(household_data_multi_all_cities) %>%
select(-seed) %>%
group_by(city, type) %>%
summarise_all(mean) %>%
ungroup()
household_table_data <- household_data %>%
add_moe() %>%
mutate(city = factor(city, levels = city_order)) %>%
arrange(city, desc(type))
# show % of all renters that did/didn't get ui, join columns in from "all" table
all_ui_pct_cols <- household_table_data %>%
filter(type == "all_ui_pct") %>%
select(city, matches("renter_hh_.*ui_pct")) %>%
pivot_longer(-city) %>%
mutate(
type = str_replace(name, ".*?((?:no)?ui).*", "all_\\1"),
name = str_replace(name, "noui", "ui"),
) %>%
pivot_wider(names_from = name, values_from = value)
table1_data <- household_table_data %>%
filter(type %in% c("all_ui", "all_noui")) %>%
select(-matches("renter_hh_.*ui_pct")) %>%
left_join(all_ui_pct_cols, by = c("city", "type")) %>%
mutate(type = recode(type, "all_ui" = "UI Recipient Households", "all_noui" = "UI Non-Recipient Households")) %>%
select(
city, type, starts_with("renter_households"),
starts_with("renter_hh_ui_pct"), starts_with("lost_wages_agg"),
starts_with("ui_benefits_reg"), starts_with("ui_benefits_all")
)
table_1 <- table1_data %>%
gt(rowname_col = "city", groupname_col = "type") %>%
tab_spanner("Renter Households", starts_with("renter_households")) %>%
tab_spanner("Share of All Renter Households", starts_with("renter_hh_ui_pct")) %>%
tab_spanner("Total Lost Wages (Monthly)", starts_with("lost_wages_agg")) %>%
tab_spanner("Total Standard UI Benefits (Monthly)", starts_with("ui_benefits_reg")) %>%
tab_spanner("Total Standard and $300/week Enhanced UI Benefits (Monthly)", starts_with("ui_benefits_all300")) %>%
cols_label(.list = hide_cols_list(table1_data)) %>%
hide_moe_cols(HIDE_MOE) %>%
highlight_total() %>%
fmt_number(contains("households"), suffixing = TRUE, decimals = 1) %>%
fmt_number(ends_with("_households_moe_90"), suffixing = TRUE, decimals = 1, pattern = "(+/- {x})") %>%
fmt_percent(contains("_pct"), decimals = 1) %>%
fmt_percent(ends_with("_pct_moe_90"), decimals = 1, pattern = "(+/- {x})") %>%
fmt_currency(contains("_monthly"), suffixing = TRUE, decimals = 1) %>%
fmt_currency(ends_with("_monthly_moe_90"), suffixing = TRUE, decimals = 1, pattern = "(+/- {x})") %>%
tab_header(
"Estimated renter households with lost income due to job loss",
"Estimated number of households to have or have not received UI benefits"
)
table_1
Estimated renter households with lost income due to job loss | |||||
---|---|---|---|---|---|
Estimated number of households to have or have not received UI benefits | |||||
Renter Households | Share of All Renter Households | Total Lost Wages (Monthly) | Total Standard UI Benefits (Monthly) | Total Standard and $300/week Enhanced UI Benefits (Monthly) | |
UI Recipient Households | |||||
Albany | 3.6K | 12.4% | $11.8M | $4.8M | $9.4M |
Buffalo | 6.8K | 10.7% | $22.4M | $8.5M | $17.1M |
Rochester | 4.7K | 8.9% | $14.6M | $5.9M | $11.7M |
Syracuse | 3.0K | 9.4% | $9.4M | $3.9M | $7.6M |
Yonkers | 5.2K | 11.8% | $20.5M | $7.2M | $13.8M |
Total | 23.3K | 10.5% | $78.8M | $30.2M | $59.5M |
UI Non-Recipient Households | |||||
Albany | 1.6K | 5.5% | $2.0M | $0.0 | $0.0 |
Buffalo | 3.3K | 5.3% | $4.4M | $0.0 | $0.0 |
Rochester | 2.6K | 4.8% | $2.9M | $0.0 | $0.0 |
Syracuse | 1.4K | 4.3% | $1.7M | $0.0 | $0.0 |
Yonkers | 1.5K | 3.4% | $3.1M | $0.0 | $0.0 |
Total | 10.4K | 4.7% | $14.2M | $0.0 | $0.0 |
# show % of all renters that did/didn't get ui, join columns in from "lt80ami" table
lt80ami_ui_pct_cols <- household_table_data %>%
filter(type == "lt80ami_ui_pct") %>%
select(city, matches("renter_hh_.*ui_pct")) %>%
pivot_longer(-city) %>%
mutate(
type = str_replace(name, ".*?((?:no)?ui).*", "lt80ami_\\1"),
name = str_replace(name, "noui", "ui"),
) %>%
pivot_wider(names_from = name, values_from = value)
table2_data <- household_table_data %>%
filter(type %in% c("lt80ami_ui", "lt80ami_noui")) %>%
select(-matches("renter_hh_.*ui_pct")) %>%
left_join(lt80ami_ui_pct_cols, by = c("city", "type")) %>%
mutate(type = recode(type, "lt80ami_ui" = "UI Recipient Households", "lt80ami_noui" = "UI Non-Recipient Households")) %>%
select(city, type, starts_with("renter_households"), starts_with("lost_wages_agg"), starts_with("ui_benefits_reg"), starts_with("ui_benefits_all"))
table_2 <- table2_data %>%
gt(rowname_col = "city", groupname_col = "type") %>%
tab_spanner("Renter Households below 80% AMI with job loss", starts_with("renter_households")) %>%
tab_spanner("Total Lost Wages (Monthly)", starts_with("lost_wages_agg")) %>%
tab_spanner("Total Standard UI Benefits (Monthly)", starts_with("ui_benefits_reg")) %>%
tab_spanner("Total Standard and $300/week Enhanced UI Benefits (Monthly)", starts_with("ui_benefits_all300")) %>%
cols_label(.list = hide_cols_list(table2_data)) %>%
hide_moe_cols(HIDE_MOE) %>%
highlight_total() %>%
fmt_number(contains("households"), suffixing = TRUE, decimals = 0) %>%
fmt_number(ends_with("_households_moe_90"), suffixing = TRUE, decimals = 0, pattern = "(+/- {x})") %>%
fmt_percent(contains("_pct"), decimals = 1) %>%
fmt_percent(ends_with("_pct_moe_90"), decimals = 1, pattern = "(+/- {x})") %>%
fmt_currency(contains("_monthly"), suffixing = TRUE, decimals = 0) %>%
fmt_currency(ends_with("_monthly_moe_90"), suffixing = TRUE, decimals = 0, pattern = "(+/- {x})") %>%
tab_header(
"Estimated renter households (with pre-COVID incomes below 80% AMI) with lost income due to job loss",
"Estimated number of households to have or have not received UI benefits"
)
table_2
Estimated renter households (with pre-COVID incomes below 80% AMI) with lost income due to job loss | ||||
---|---|---|---|---|
Estimated number of households to have or have not received UI benefits | ||||
Renter Households below 80% AMI with job loss | Total Lost Wages (Monthly) | Total Standard UI Benefits (Monthly) | Total Standard and $300/week Enhanced UI Benefits (Monthly) | |
UI Recipient Households | ||||
Albany | 1K | $7M | $3M | $4M |
Buffalo | 3K | $15M | $5M | $9M |
Rochester | 2K | $9M | $3M | $6M |
Syracuse | 1K | $6M | $2M | $4M |
Yonkers | 2K | $13M | $4M | $6M |
Total | 10K | $50M | $17M | $30M |
UI Non-Recipient Households | ||||
Albany | 293 | $1M | $0 | $0 |
Buffalo | 778 | $3M | $0 | $0 |
Rochester | 554 | $1M | $0 | $0 |
Syracuse | 266 | $875K | $0 | $0 |
Yonkers | 475 | $2M | $0 | $0 |
Total | 2K | $8M | $0 | $0 |
prep_table3_data <- function(.data) {
# total renter population in NY state
nys_renter_pop_num <- .data %>%
as_survey_design(weights = perwt) %>%
summarise(
pop_renter_num = survey_total_ci90(1)
) %>%
pull(pop_renter_num)
table3_pop <- .data %>%
filter(!is.na(city)) %>%
as_survey_design(weights = perwt) %>%
group_by(city) %>%
summarise(
pop_num = survey_total_ci90(1),
pop_renter_num = survey_total_ci90(is_renter)
) %>%
mutate(
pop_renter_nys_pct = pop_renter_num / nys_renter_pop_num
)
table3_renter_pct <- .data %>%
filter(!is.na(city), pernum == 1) %>%
as_survey_design(weights = hhwt) %>%
group_by(city) %>%
summarise(
hh_renter_num = survey_total_ci90(is_renter),
hh_renter_pct = survey_mean_ci90(is_renter),
)
table3_renter_stats <- .data %>%
filter(!is.na(city), pernum == 1, is_renter) %>%
as_survey_design(weights = hhwt) %>%
group_by(city) %>%
summarise(
hh_rent_burden_sev_num = survey_total_ci90(is_rent_burdened_sev, na.rm = TRUE),
hh_rent_burden_sev_pct = survey_mean_ci90(is_rent_burdened_sev, na.rm = TRUE),
hh_renter_lt80ami_num = survey_total_ci90(hh_inc_nom < hud_inc_lim80),
hh_renter_lt80ami_pct = survey_mean_ci90(hh_inc_nom < hud_inc_lim80)
)
table3_data <- list(
table3_pop, table3_renter_pct, table3_renter_stats
) %>%
reduce(left_join, by = "city")
}
table3_data_cities <- prep_table3_data(ipums_clean)
table3_data_all_cities <- ipums_clean %>%
mutate(city = if_else(!is.na(city), "Total", city)) %>%
prep_table3_data()
table3_data <- bind_rows(table3_data_cities, table3_data_all_cities) %>%
add_moe() %>%
mutate(city = factor(city, levels = city_order)) %>%
arrange(city) %>%
# in final tables we leave some variables out, but keep them here commented
# out since they are useful to have for reference in the text
select(-starts_with("pop_renter_num"), -starts_with("pop_renter_nys_pct"))
table_3 <- table3_data %>%
gt(rowname_col = "city") %>%
tab_spanner("Total Population",
starts_with("pop_num")) %>%
tab_spanner("Renter Households",
starts_with("hh_renter_num")) %>%
tab_spanner("Renter Share of Households",
starts_with("hh_renter_pct")) %>%
tab_spanner("Number of Renter Households Severely Rent Burdened (Pre-COVID)",
starts_with("hh_rent_burden_sev_num")) %>%
tab_spanner("Share of Renter Households Severely Rent Burdened (Pre-COVID)",
starts_with("hh_rent_burden_sev_pct")) %>%
tab_spanner("Number of Renter Households with Incomes Below 80% AMI (Pre-COVID)",
starts_with("hh_renter_lt80ami_num")) %>%
tab_spanner("Share of Renter Households with Incomes Below 80% AMI (Pre-COVID)",
starts_with("hh_renter_lt80ami_pct")) %>%
cols_label(.list = hide_cols_list(table3_data)) %>%
hide_moe_cols(HIDE_MOE) %>%
highlight_total() %>%
fmt_number(contains("_num"), suffixing = TRUE, decimals = 1) %>%
fmt_number(ends_with("_num_moe_90"), suffixing = TRUE, decimals = 1, pattern = "(+/- {x})") %>%
fmt_percent(contains("_pct"), decimals = 1) %>%
fmt_percent(ends_with("_pct_moe_90"), decimals = 1, pattern = "(+/- {x})") %>%
tab_header("Estimated number of renter households by pre-COVID rent burden and income")
table_3
Estimated number of renter households by pre-COVID rent burden and income | |||||||
---|---|---|---|---|---|---|---|
Total Population | Renter Households | Renter Share of Households | Number of Renter Households Severely Rent Burdened (Pre-COVID) | Share of Renter Households Severely Rent Burdened (Pre-COVID) | Number of Renter Households with Incomes Below 80% AMI (Pre-COVID) | Share of Renter Households with Incomes Below 80% AMI (Pre-COVID) | |
Albany | 94.7K | 29.3K | 64.4% | 9.5K | 32.3% | 21.5K | 73.3% |
Buffalo | 247.0K | 63.4K | 57.9% | 18.7K | 29.5% | 45.7K | 72.1% |
Rochester | 196.0K | 53.1K | 62.6% | 17.4K | 32.9% | 40.2K | 75.7% |
Syracuse | 127.3K | 32.0K | 59.3% | 9.5K | 29.9% | 23.0K | 71.9% |
Yonkers | 196.4K | 43.9K | 55.4% | 16.4K | 37.4% | 33.1K | 75.4% |
Total | 861.5K | 221.6K | 59.4% | 71.5K | 32.3% | 163.4K | 73.7% |
person_results_multi <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = function(seed, .data) {
set.seed(seed)
data_w_vars <- .data %>%
mutate(risk_group = runif(n()) < job_loss_pct) %>%
add_ui_takeup(UI_TAKEUP_RATE) %>%
add_need_vars() %>%
as_survey_design(weights = perwt)
city_data <- data_w_vars %>%
group_by(city) %>%
summarise(
# Renters only:
pop_num = survey_total_ci90(1),
# total number of wage earners
wage_earners_num = survey_total_ci90(inc_wages > 0, na.rm = TRUE),
# total number of wage earners estiamted to lose jobs
risk_earners_num = survey_total_ci90(risk_group, na.rm = TRUE),
# share of wage earners estimated to lose jobs
risk_earners_pct = survey_mean_ci90(risk_group, na.rm = TRUE)
) %>%
ungroup() %>%
mutate(seed = seed)
all_cities_data <- data_w_vars %>%
summarise(
pop_num = survey_total_ci90(1),
wage_earners_num = survey_total_ci90(inc_wages > 0, na.rm = TRUE),
risk_earners_num = survey_total_ci90(risk_group, na.rm = TRUE),
risk_earners_pct = survey_mean_ci90(risk_group, na.rm = TRUE)
) %>%
mutate(
seed = seed,
city = "Total"
)
bind_rows(city_data, all_cities_data)
},
.data = filter(ipums_clean, !is.na(city), is_renter)
)
person_results <- person_results_multi %>%
select(-seed) %>%
group_by(city) %>%
summarise_all(mean)
person_table_data <- person_results %>%
add_moe() %>%
mutate(city = factor(city, levels = city_order)) %>%
arrange(city)
table_4 <- person_table_data %>%
gt(rowname_col = "city") %>%
tab_spanner("Renter Population", starts_with("pop_num")) %>%
tab_spanner("Number of Wage Earning Renters", starts_with("wage_earners_num")) %>%
tab_spanner("Number of Wage Earning Renters Estimated to Lose Income Due to Job Loss", starts_with("risk_earners_num")) %>%
tab_spanner("Share of Wage Earning Renters Estimated to Lose Income Due to Job Loss", starts_with("risk_earners_pct")) %>%
cols_label(.list = hide_cols_list(person_table_data)) %>%
hide_moe_cols(HIDE_MOE) %>%
highlight_total() %>%
fmt_number(contains("_num"), suffixing = TRUE, decimals = 1) %>%
fmt_number(contains("_num_moe_90"), suffixing = TRUE, decimals = 1, pattern = "(+/- {x})") %>%
fmt_percent(contains("_pct"), decimals = 1) %>%
fmt_percent(ends_with("_pct_moe_90"), decimals = 1, pattern = "(+/- {x})") %>%
tab_header("Estimated Number of Renters by Wage Earning Status")
table_4
Estimated Number of Renters by Wage Earning Status | ||||
---|---|---|---|---|
Renter Population | Number of Wage Earning Renters | Number of Wage Earning Renters Estimated to Lose Income Due to Job Loss | Share of Wage Earning Renters Estimated to Lose Income Due to Job Loss | |
Albany | 59.6K | 33.7K | 5.8K | 17.2% |
Buffalo | 143.0K | 68.0K | 11.0K | 16.2% |
Rochester | 116.9K | 52.8K | 7.9K | 15.0% |
Syracuse | 78.5K | 29.2K | 4.8K | 16.4% |
Yonkers | 102.0K | 48.9K | 7.6K | 15.5% |
Total | 500.0K | 232.7K | 37.1K | 15.9% |
size_summarise_stats <- function(.data) {
size_data <- .data %>%
filter(
pernum == 1, # keep only one row per household
is_renter # keep only renter households
) %>%
as_survey_design(weights = hhwt) %>%
group_by(city, bldg_size, .drop = FALSE) %>%
summarise(households = survey_total(vartype = "ci", level = 0.90))
size_totals <- size_data %>%
mutate(
households_moe = households_upp - households,
households_moe_sqr = households_moe^2
) %>%
group_by(city) %>%
summarize(
total = sum(households),
total_moe = sqrt(sum(households_moe_sqr))
) %>%
ungroup()
size_rates <- size_data %>%
left_join(size_totals, by = "city") %>%
mutate(
households_moe = households_upp - households,
share = households / total,
share_moe = (1 / total) * sqrt(households_moe^2 - (share * total_moe)^2),
share_low = share - share_moe,
share_upp = share + share_moe
) %>%
select(city, bldg_size, share, share_low, share_upp)
size_rates
}
size_data_multi <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = function(seed, .data) {
set.seed(seed)
data_w_vars <- .data %>%
mutate(risk_group = runif(n()) < job_loss_pct) %>%
add_ui_takeup(UI_TAKEUP_RATE) %>%
add_need_vars()
size_loss_stats <- data_w_vars %>%
filter(hh_any_risk) %>% # keep only households with lost job/income
size_summarise_stats() %>%
mutate(type = "Lost income due to job loss")
size_noloss_stats <- data_w_vars %>%
filter(!hh_any_risk) %>% # keep only households without lost job/income
size_summarise_stats() %>%
mutate(type = "Did not lose income due to job loss")
bind_rows(size_loss_stats, size_noloss_stats) %>%
filter(bldg_size != "other") %>%
mutate(seed = seed)
},
.data = filter(ipums_clean, !is.na(city), is_renter)
)
size_data <- size_data_multi %>%
select(-seed) %>%
group_by(city, type, bldg_size) %>%
summarise_all(mean) %>%
ungroup() %>%
mutate(city = factor(city, levels = city_order)) %>%
arrange(city, desc(type), bldg_size)
fig_1 <- size_data %>%
mutate(share_low = if_else(share_low < 0, 0, share_low)) %>%
fc_col_plot_cluster(
x = bldg_size,
y = share,
fill = type,
y_limits = c(0, .8),
ymin = share_low,
ymax = share_upp,
y_format = "percent"
) +
scale_fill_manual(values = c("#2c7fb8", "#98e2c9")) +
lemon::facet_rep_wrap(~city, ncol = 2, repeat.tick.labels = 'bottom') +
labs(
title = str_glue(
"Distribution of renter households across building sizes, by job/income loss"
),
x = "Units in building",
y = "Share of renter households",
fill = NULL,
caption = str_glue(
"Notes: Only renter households with at least one emloyed member estimated to have lost their job are included.
Error bars represent 90% confidence intervals, and value labels reflect point estimates.
Sources: American Community Survey (2018), IPUMS USA, NYU Furman Center"
)
)
fig_1
prep_table5_data <- function(seed, .data) {
set.seed(seed)
data_w_need <- .data %>%
mutate(risk_group = runif(n()) < job_loss_pct) %>%
add_ui_takeup(UI_TAKEUP_RATE) %>%
add_need_vars() %>%
filter(
pernum == 1, # keep only one row per household
hh_any_risk # keep only vulnerable households
) %>%
as_survey_design(weights = hhwt) %>%
group_by(city) %>%
summarise(
renter_households = survey_total_ci90(1),
rent_monthly_tot = survey_total_ci90(gross_rent_nom, na.rm = TRUE),
rent_monthly_avg = survey_mean_ci90(gross_rent_nom, na.rm = TRUE),
lost_wages_agg_monthly = survey_total_ci90(hh_risk_wages / 12),
rent_need_tot_monthly = survey_total_ci90(risk_rent_need),
rent_need_ui_reg_tot_monthly = survey_total_ci90(risk_rent_need_ui_reg),
rent_need_ui_all300_tot_monthly = survey_total_ci90(risk_rent_need_ui_all300),
) %>%
ungroup() %>%
mutate(seed = seed)
}
renter_1_4_stats_cities <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = prep_table5_data,
.data = ipums_clean %>%
filter(!is.na(city), is_renter, bldg_size %in% c("1", "2-4"))
)
renter_1_4_stats_all_cities <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = prep_table5_data,
.data = ipums_clean %>%
filter(!is.na(city), is_renter, bldg_size %in% c("1", "2-4")) %>%
mutate(city = if_else(!is.na(city), "Total", city))
)
table5_data <- renter_1_4_stats_cities %>%
bind_rows(renter_1_4_stats_all_cities) %>%
select(-seed) %>%
group_by(city) %>%
summarise_all(mean) %>%
ungroup() %>%
add_moe() %>%
mutate(city = factor(city, levels = city_order)) %>%
arrange(city)
table_5 <- table5_data %>%
gt(rowname_col = "city") %>%
tab_spanner("Renter Households in 1-4 Unit Properties",
starts_with("renter_households")) %>%
tab_spanner("Total Rent (Monthly)", starts_with("rent_monthly_tot")) %>%
tab_spanner("Average Rent (Monthly)", starts_with("rent_monthly_avg")) %>%
tab_spanner("Total Lost Wages (Monthly)", starts_with("lost_wages_agg")) %>%
tab_spanner("Rental Assistance Need - No UI Benefits (Monthly)",
starts_with("rent_need_tot_monthly")) %>%
tab_spanner("Rental Assistance Need - Standard UI Benefits (Monthly)",
starts_with("rent_need_ui_reg_tot_monthly")) %>%
tab_spanner("Rental Assistance Need - Standard Plus $300/week Enhanced UI Benefits (Monthly)",
starts_with("rent_need_ui_all300_tot_monthly")) %>%
cols_label(.list = hide_cols_list(table5_data)) %>%
hide_moe_cols(TRUE) %>%
highlight_total() %>%
fmt_number(contains("_households"), suffixing = TRUE, decimals = 0) %>%
fmt_number(ends_with("_households_moe_90"), suffixing = TRUE, decimals = 0, pattern = "(+/- {x})") %>%
fmt_currency(contains("_monthly"), suffixing = TRUE, decimals = 1) %>%
fmt_currency(ends_with("_monthly_moe_90"), suffixing = TRUE, decimals = 1, pattern = "(+/- {x})") %>%
fmt_currency(contains("rent_monthly_avg"), decimals = 0) %>%
fmt_currency(ends_with("rent_monthly_avg"),decimals = 0, pattern = "(+/- {x})") %>%
tab_header(
"Estimated rental assistance need for households in 1-4 unit properties with lost income due to job loss"
)
table_5
Estimated rental assistance need for households in 1-4 unit properties with lost income due to job loss | |||||||
---|---|---|---|---|---|---|---|
Renter Households in 1-4 Unit Properties | Total Rent (Monthly) | Average Rent (Monthly) | Total Lost Wages (Monthly) | Rental Assistance Need - No UI Benefits (Monthly) | Rental Assistance Need - Standard UI Benefits (Monthly) | Rental Assistance Need - Standard Plus $300/week Enhanced UI Benefits (Monthly) | |
Albany | 4K | $4.5M | $1,192 | $10.2M | $3.2M | $2.4M | $932.1K |
Buffalo | 9K | $8.1M | $942 | $20.8M | $4.8M | $3.3M | $1.3M |
Rochester | 5K | $5.2M | $989 | $12.2M | $3.1M | $2.6M | $1.0M |
Syracuse | 3K | $2.7M | $977 | $6.5M | $1.8M | $1.4M | $487.8K |
Yonkers | 3K | $4.5M | $1,569 | $8.8M | $3.0M | $2.0M | $859.9K |
Total | 23K | $25.1M | $1,075 | $58.6M | $16.0M | $11.7M | $4.6M |
# tables 6-7
need_summarise_stats <- function(.data) {
.data %>%
filter(
pernum == 1, # keep only one row per household
hh_any_risk # keep only vulnerable households
) %>%
as_survey_design(weights = hhwt) %>%
group_by(city) %>%
summarise(
rent_need_tot_monthly = survey_total_ci90(risk_rent_need),
rent_need_ui_reg_tot_monthly = survey_total_ci90(risk_rent_need_ui_reg),
rent_need_ui_all300_tot_monthly = survey_total_ci90(risk_rent_need_ui_all300),
) %>%
ungroup()
}
prep_need_data <- function(seed, .data) {
set.seed(seed)
data_w_vars <- .data %>%
mutate(risk_group = runif(n()) < job_loss_pct) %>%
add_ui_takeup(UI_TAKEUP_RATE)
data_all_default <- data_w_vars %>%
add_need_vars() %>%
need_summarise_stats()
data_all_30cutoff <- data_w_vars %>%
mutate(target_burden = 0.30) %>%
add_need_vars() %>%
need_summarise_stats()
data_lt80ami_default <- data_w_vars %>%
# Include only households with income below 80% AMI
filter(hh_inc_nom > hud_inc_lim80) %>%
add_need_vars() %>%
need_summarise_stats()
bind_rows(
data_all_default %>% mutate(type = "all_default"),
data_all_30cutoff %>% mutate(type = "all_30cutoff"),
data_lt80ami_default %>% mutate(type = "lt80ami_default"),
) %>%
mutate(seed = seed)
}
need_data_multi_cities <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = prep_need_data,
.data = ipums_clean %>% filter(!is.na(city), is_renter)
)
need_data_multi_all_cities <- seq_len(ITERATIONS) %>%
future_map_dfr(
.f = prep_need_data,
.data = ipums_clean %>%
filter(!is.na(city), is_renter) %>%
mutate(city = "Total")
)
need_data <- need_data_multi_cities %>%
bind_rows(need_data_multi_all_cities) %>%
select(-seed) %>%
group_by(city, type) %>%
summarise_all(mean) %>%
ungroup()
need_table_data <- need_data %>%
add_moe() %>%
mutate(city = factor(city, levels = city_order)) %>%
arrange(city, desc(type))
table6_data <- need_table_data %>%
filter(type == "all_default") %>%
mutate(type = "Higher of pre-COVID rent-to-income ratio or 30% rent-to-income ratio")
table_6 <- table6_data %>%
gt(rowname_col = "city", groupname_col = "type") %>%
tab_spanner("No UI Benefits (Monthly)", starts_with("rent_need_tot_monthly")) %>%
tab_spanner("Standard UI Benefits (Monthly)", starts_with("rent_need_ui_reg_tot_monthly")) %>%
tab_spanner("Standard Plus $300/week Enhanced UI Benefits (Monthly)", starts_with("rent_need_ui_all300_tot_monthly")) %>%
cols_label(.list = hide_cols_list(table6_data)) %>%
hide_moe_cols(HIDE_MOE) %>%
highlight_total() %>%
fmt_currency(contains("_monthly"), suffixing = TRUE, decimals = 1) %>%
fmt_currency(ends_with("_monthly_moe_90"), suffixing = TRUE, decimals = 1, pattern = "(+/- {x})") %>%
tab_header(
"Estimated rental assistance need for households with lost income due to job loss",
"Separated by target rent-to-income ratio"
)
table_6
Estimated rental assistance need for households with lost income due to job loss | |||
---|---|---|---|
Separated by target rent-to-income ratio | |||
No UI Benefits (Monthly) | Standard UI Benefits (Monthly) | Standard Plus $300/week Enhanced UI Benefits (Monthly) | |
Higher of pre-COVID rent-to-income ratio or 30% rent-to-income ratio | |||
Albany | $4.1M | $3.2M | $1.2M |
Buffalo | $5.8M | $4.2M | $1.7M |
Rochester | $4.2M | $3.4M | $1.4M |
Syracuse | $2.8M | $2.2M | $929.6K |
Yonkers | $7.2M | $5.3M | $2.4M |
Total | $24.0M | $18.3M | $7.6M |
table7_data <- need_table_data %>%
filter(type %in% c("all_default", "lt80ami_default")) %>%
mutate(type = recode(type, "all_default" = "All households", "lt80ami_default" = "Households with pre-COVID income below 80% AMI")) %>%
arrange(city, type)
table_7 <- table7_data %>%
gt(rowname_col = "city", groupname_col = "type") %>%
tab_spanner("No UI Benefits (Monthly)", starts_with("rent_need_tot_monthly")) %>%
tab_spanner("Standard UI Benefits (Monthly)", starts_with("rent_need_ui_reg_tot_monthly")) %>%
tab_spanner("Standard Plus $300/week Enhanced UI Benefits (Monthly)", starts_with("rent_need_ui_all300_tot_monthly")) %>%
cols_label(.list = hide_cols_list(table7_data)) %>%
hide_moe_cols(HIDE_MOE) %>%
highlight_total() %>%
fmt_currency(contains("_monthly"), suffixing = TRUE, decimals = 1) %>%
fmt_currency(ends_with("_monthly_moe_90"), suffixing = TRUE, decimals = 1, pattern = "(+/- {x})") %>%
tab_header(
"Estimated rental assistance need for households with lost income due to job loss",
"Separated by pre-COVID income level"
)
table_7
Estimated rental assistance need for households with lost income due to job loss | |||
---|---|---|---|
Separated by pre-COVID income level | |||
No UI Benefits (Monthly) | Standard UI Benefits (Monthly) | Standard Plus $300/week Enhanced UI Benefits (Monthly) | |
All households | |||
Albany | $4.1M | $3.2M | $1.2M |
Buffalo | $5.8M | $4.2M | $1.7M |
Rochester | $4.2M | $3.4M | $1.4M |
Syracuse | $2.8M | $2.2M | $929.6K |
Yonkers | $7.2M | $5.3M | $2.4M |
Total | $24.0M | $18.3M | $7.6M |
Households with pre-COVID income below 80% AMI | |||
Albany | $1.1M | $685.4K | $386.6K |
Buffalo | $2.0M | $1.0M | $568.5K |
Rochester | $1.3M | $885.2K | $474.1K |
Syracuse | $1.1M | $792.7K | $426.1K |
Yonkers | $2.4M | $1.6M | $1.0M |
Total | $7.8M | $4.9M | $2.9M |
There are just a couple simple stats that are helpful to have, and as long as they don’t involve the variables related to job loss (which require the multiple iterations) you can easily add them here, for person or household-level.
ipums_clean %>%
filter(pernum == 1, !is.na(city)) %>%
as_survey_design(weights = hhwt) %>%
summarise(
total_households = survey_total_ci90(1)
)
## # A tibble: 1 x 3
## total_households total_households_low total_households_upp
## <dbl> <dbl> <dbl>
## 1 372849 365587. 380111.
ipums_clean %>%
filter(is_renter) %>%
as_survey_design(weights = perwt) %>%
summarise(
renter_population = survey_total_ci90(1)
)
## # A tibble: 1 x 3
## renter_population renter_population_low renter_population_upp
## <dbl> <dbl> <dbl>
## 1 8162043 8122475. 8201611.