Code
library(tidyverse)
library(ptaxsim)
library(DBI)
library(httr)
library(jsonlite)
library(glue)
library(DT)

knitr::opts_chunk$set(warning = FALSE, message = FALSE)


file_path <- "C:/Users/aleaw/" 

taxyear <-c(2021)
taxyear = as.data.frame(taxyear)



source("../scripts/helper_tc_muninames.R") 

tc_muninames <- tc_muninames |> select(-tax_code_rate)

#ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), "./ptaxsim.db/ptaxsim-2023.0.0.db")

if (file.exists(file_path)){
   ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), "C:/Users/aleaw/OneDrive/Documents/PhD Fall 2021 - Spring 2022/Merriman RA/ptax/ptaxsim.db/ptaxsim-2023.0.0.db")
} else {
    ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), "./ptaxsim.db/ptaxsim-2023.0.0.db")

}


current_dir = getwd()

if (grepl("Website", current_dir)) {
  dots = "../"
} else {
  dots = "./"
}


path <- paste0(dots, "Necessary_Files/class_dict_singlefamcodes.csv")
class_dict <- read_csv(path) %>% 
  mutate(class_code = as.character(class_code)) # change variable type to character so the join works.

path <- paste0(dots, "inputs/ccao_loa.csv")
ccao_loa <- read_csv(path) %>% 
  mutate(class_code = as.character(class_code))

eq_factor = 3.0027


# get all tax codes and tax rates
tax_codes <- DBI::dbGetQuery(
    ptaxsim_db_conn, 
    glue_sql("
             SELECT DISTINCT tax_code_num, tax_code_rate
             FROM tax_code
             WHERE year = 2021
             "
             , 
             .con = ptaxsim_db_conn)) |> 
  mutate(tax_code_rate = tax_code_rate / 100)

# get percent of eav that goes to a TIF:
tif_distribution <- DBI::dbGetQuery(
    ptaxsim_db_conn, 
    glue_sql("
             SELECT DISTINCT* 
             FROM tif_distribution
             WHERE year = 2021
             "
             , 
             .con = ptaxsim_db_conn)) %>%
  select(-c(tax_code_rate, agency_num)) |>
  mutate(tax_code_distribution_pct = tax_code_distribution_pct / 100)

# All PINs - get AV and exemption amounts -----------------------------
# Include ALL PINs for calculating muni level composite rates:
cook_pins <- DBI::dbGetQuery(
    ptaxsim_db_conn, 
    glue_sql("
             SELECT DISTINCT* 
             FROM pin 
             WHERE year = 2021
             "
             , 
             .con = ptaxsim_db_conn)) 


# create some variables that we need:
muni_rates <- cook_pins |> 
  left_join(tax_codes) |>
  left_join(tc_muninames) |>
  left_join(tif_distribution) |>
  mutate(
    tax_code_distribution_pct = ifelse(is.na(tax_code_distribution_pct), 0, tax_code_distribution_pct),
    in_tif = ifelse(tax_code_num %in% tif_distribution$tax_code_num, 1, 0),
    has_HO_exe = ifelse(exe_homeowner > 0, 1, 0),
    has_other_exe = ifelse(exe_senior + exe_freeze + exe_longtime_homeowner + exe_disabled +
                             exe_vet_returning + exe_vet_dis_lt50 + exe_vet_dis_50_69 + exe_vet_dis_ge70 > 0, 1, 0 ),
    
    final_tax_to_tif = tax_bill_total *  tax_code_distribution_pct,   # TIF revenue
    final_tax_to_dist = tax_bill_total - final_tax_to_tif,      # local government revenue
   all_exemptions = rowSums(across(starts_with("exe_"))),
     tif_increment_eav = final_tax_to_tif / tax_code_rate,
     nontif_eav_preexe = av_clerk*eq_factor - tif_increment_eav,
     nontif_eav_postexe = av_clerk*eq_factor - tif_increment_eav - all_exemptions,
    current_taxbase = final_tax_to_dist / tax_code_rate, # should match nontif_eav_postexe amount

    eav_postexemptions = av_clerk*eq_factor - all_exemptions, # ignores tif distribution
    eav_preexemptions = av_clerk*eq_factor, # ignores tif distribution

  ) %>% 
  mutate(av = av_clerk)  |> # av used for calculations is the final av value from the clerk's office
  group_by(clean_name)|>
  summarize(
    tax_bill_total = sum(tax_bill_total), # TIF and local government revenue
    tif_increment_eav = sum(tif_increment_eav, na.rm=T),  # should be sum of frozen EAV
    muni_taxed_eav = sum(nontif_eav_postexe, na.rm=T),  # non-TIF and non-exempt EAV taxed by government agencies
    muni_exempt_eav_from_GHE = sum(exe_homeowner, na.rm=T),
    muni_levy = sum(final_tax_to_dist, na.rm=T),
    
    muni_min_rate = min(tax_code_rate),
    muni_median_rate = median(tax_code_rate),
    muni_avg_rate = mean(tax_code_rate, na.rm=T),
    muni_max_rate = max(tax_code_rate),
    final_tax_to_dist = sum(final_tax_to_dist, na.rm=T),
    final_tax_to_tif = sum(final_tax_to_tif, na.rm=T)) |>
  
  mutate(muni_hyp_taxbase = muni_taxed_eav + muni_exempt_eav_from_GHE,   
         muni_hyp_taxrate =  muni_levy / muni_hyp_taxbase)
  
## Tax code level "levies" and tax bases ----------------
tax_code_change <- cook_pins |>  
  left_join(tax_codes) |>
  left_join(tc_muninames) |>
  left_join(tif_distribution) |>
  mutate(
    tax_code_distribution_pct = ifelse(is.na(tax_code_distribution_pct), 0, tax_code_distribution_pct),
    in_tif = ifelse(tax_code_num %in% tif_distribution$tax_code_num, 1, 0),
    has_HO_exe = ifelse(exe_homeowner > 0, 1, 0),
    has_other_exe = ifelse(exe_senior + exe_freeze + exe_longtime_homeowner + exe_disabled +
                             exe_vet_returning + exe_vet_dis_lt50 + exe_vet_dis_50_69 + 
                             exe_vet_dis_ge70 > 0, 1, 0 ),
    all_exemptions = rowSums(across(starts_with("exe_"))),
    final_tax_to_tif = tax_bill_total *  tax_code_distribution_pct/100,
    final_tax_to_dist = tax_bill_total - final_tax_to_tif,
    tif_increment_eav = final_tax_to_tif / tax_code_rate,
    
    nontif_eav_preexe = av_clerk*eq_factor - tif_increment_eav,
     nontif_eav_postexe = av_clerk*eq_factor - tif_increment_eav - all_exemptions,
    eav_postexemptions = av_clerk*eq_factor - all_exemptions,
    eav_preexemptions = av_clerk*eq_factor,
    total_bill = final_tax_to_tif + final_tax_to_dist
  ) %>% 
  group_by(clean_name, tax_code_num, tax_code_rate) |>
  summarize(
    tc_pc = n(),
    tc_tax_bill_total = sum(tax_bill_total), # TIF and local government revenue at tax code level
    tc_eav_inTIF = sum(tif_increment_eav, na.rm=T),  # should be sum of frozen EAV
    tc_taxed_eav = sum(nontif_eav_postexe, na.rm=T),  # non-TIF and non-exempt EAV taxed by government agencies
    tc_exempt_eav_from_GHE = sum(exe_homeowner, na.rm=T),
    tc_final_tax_to_dist = sum(final_tax_to_dist, na.rm=T),
    tc_final_tax_to_tif = sum(final_tax_to_tif, na.rm=T)) |>
  mutate(tc_hyp_taxbase = tc_taxed_eav + tc_exempt_eav_from_GHE,   
         tc_hyp_taxrate =  tc_final_tax_to_dist / tc_hyp_taxbase)



# Identify Class 2 Residential PINs ------------- 
c2pins <- DBI::dbGetQuery(
  ptaxsim_db_conn, 
  glue_sql("
             SELECT DISTINCT* 
             FROM pin 
             WHERE class > 199 AND class < 300
             AND year = 2021
             "
           , 
           .con = ptaxsim_db_conn))

c2pins <- c2pins |> 
  mutate(has_HO_exe = ifelse( exe_homeowner > 0, 1, 0),
         has_other_exe = ifelse(exe_senior + exe_freeze + exe_longtime_homeowner + exe_disabled + exe_vet_returning + exe_vet_dis_lt50 + exe_vet_dis_50_69 + exe_vet_dis_ge70 > 0, 1, 0 )) %>% 
  left_join(tax_codes) |>
  left_join(tc_muninames) %>%
  mutate(av = av_clerk) |>
  left_join(tax_code_change) |>
  left_join(tif_distribution) |>
  mutate(
    tax_code_distribution_pct = ifelse(is.na(tax_code_distribution_pct), 0, tax_code_distribution_pct),
    in_tif = ifelse(tax_code_num %in% tif_distribution$tax_code_num, 1, 0),
    has_HO_exe = ifelse(exe_homeowner > 0, 1, 0),
    has_other_exe = ifelse(exe_senior + exe_freeze + exe_longtime_homeowner + exe_disabled +
                             exe_vet_returning + exe_vet_dis_lt50 + exe_vet_dis_50_69 + 
                             exe_vet_dis_ge70 > 0, 1, 0 ),
    all_exemptions = rowSums(across(starts_with("exe_"))),
    final_tax_to_tif = tax_bill_total *  tax_code_distribution_pct/100,
    final_tax_to_dist = tax_bill_total-final_tax_to_tif,
    tif_increment_eav = final_tax_to_tif / tax_code_rate,
    
    nontif_eav_preexe = av_clerk*eq_factor - tif_increment_eav,
    nontif_eav_postexe = av_clerk*eq_factor - tif_increment_eav - all_exemptions,
    
    eav_postexemptions = av_clerk*eq_factor - all_exemptions,
    eav_preexemptions = av_clerk*eq_factor)
Code
q = c(.1, .25, .5, .75, .9)

## ranks properties that are considered C2 properties in order of AV for each Muni
muni_quartiles <- c2pins %>%
  group_by(clean_name) %>%
  arrange(av) %>%
  summarize(count_pins = n(), 
            min = min(av),
            quant10 = round(quantile(av, probs = q[1])), 
            quant25 = round(quantile(av, probs = q[2])), 
            quant50 = round(quantile(av, probs = q[3])),
            quant75 = round(quantile(av, probs = q[4])),
            quant90 = round(quantile(av, probs = q[5])),

            max = max(av)
           ) %>% 
  arrange(desc(quant50))


# Class 2 Descriptive Stats 
C2_munistats <- c2pins %>% 
  filter(class > 199 & class < 300) %>%
  group_by(clean_name)  %>%
  arrange(av) %>%
  summarize(
    median_eav = round(median(eav_postexemptions)), 
    median_av = round(median(av)), 
    avg_av = round(mean(av)),
    avg_eav = round(mean(eav_postexemptions)),
    C2_pins_in_muni = n(),
    C2_current_exemptions = sum(all_exemptions, na.rm = TRUE),
    C2_HO_exemps = sum(exe_homeowner, na.rm = TRUE),
  ) 

# All Class 2 Properties
munis_ranked <- c2pins  %>%
  inner_join(muni_quartiles, by = c("clean_name")) %>% 
  mutate(rank = case_when(
   av > (quant10-500) & (av<quant10+500) ~ "q10",
    av > (quant25-500) & (av<quant25+500) ~ "q25",
    av > (quant50-500) & (av<quant50+500) ~ "q50",
    av > (quant75-500) & (av<quant75+500) ~ "q75",
    av > (quant90-500) & (av<quant90+500) ~ "q90")

    ) %>%
  select(clean_name, rank, av, pin, class, everything())

Which taxpayers benefit the most from homestead exemptions?

Table 1. Comparison of Tax Bills for Median Assessed Value (AV) Class 2 Properties in Selected Municpalities under the Status Quo and Alternative Scenario

Assessed Value is used to identify the quartile breaks within each municipality for single-family properties. These break points will then be used to look at how the tax bill changes for properties with AVs at the 25th percentile, 50th percentile, and 75th percentile.

The assessed value and original equalized assessed values come from the pin data table within PTAXSIM. This table also has every type of exemption that the property received and the amount of EAV that was exempt due to the exemption.

Code
tax_bill_change_HO <- c2pins %>% 
  filter(av > 0) %>% 
  arrange(av) %>%
  mutate(eq_av = av_clerk*eq_factor,
         bill_current = (eav_postexemptions*tax_code_rate),
         bill_noexemps =  tc_hyp_taxrate*(eav_postexemptions+exe_homeowner),
         bill_change = bill_noexemps - bill_current) %>%
  group_by(clean_name, has_HO_exe) %>% 
  summarize(median_AV = median(av),
            median_EAV = median(eav_postexemptions),
            median_bill_cur = round(median(bill_current)),
            median_bill_new = round(median(bill_noexemps)),
            median_change = round(median(bill_change)),
            pincount=n(),
            perceived_savings = median(exe_homeowner*tax_code_rate)
  )

tax_bill_change_HO |> filter(clean_name %in% c("Park Forest", "Phoenix", "Winnetka", "Riverdale"))
Table 2.1: Figure 2 The median property was found for all Class 2 properties and used to calculate the hypothetical tax bill change for those that do and do not claim the GHE. It is important to note that almost all municipalities have a median AV for those that do claim the GHE that is quite different from those that do not claim the GHE.

The table created from the code calculates the median AV for those with exemptions and those without exemptions. Usually these values are significantly different. In the Report, the table was simplified to show the save assessed value for both types of homeowners (See image of table pasted below).

Notes: AV = Assessed Value. “Tax Calculator” reduction refers to the section of the tax bill received by property tax payers (See Figure 1). It does not take into account changes in property tax rates due to the larger taxbase that would come from having a larger tax base. Figures in columns titled Current Bill, Tax Bill without GHE, and “Tax Calculator” Reduction in Tax Liability are estimated using an average of the properties with AV within $1,000 of the municipality median in order to enlarge the number of observations therefore do not precisely represent the median figure.

Figure 1. Examples of Actual Tax Bills

Park Forest vs. Winnetka

Figure 2. Value of Median Residential Property Indifferent to Elimination of GHE

Code
muni_breakeven_points <- c2pins |>
  left_join(muni_rates) |>
  filter(exe_homeowner == 10000) %>% 
  filter(exe_senior == 0 & 
           exe_freeze == 0 & 
           exe_longtime_homeowner == 0 & 
           exe_disabled == 0 & 
           exe_vet_returning == 0 & 
           exe_vet_dis_lt50 == 0 & 
           exe_vet_dis_50_69 == 0 & 
           exe_vet_dis_ge70 == 0 & 
           exe_abate == 0) %>%
  
  group_by(clean_name) %>%
  summarize(
    median_AV = median(av),     # median class 2 AV for properties that did claim the GHE but not other exemptions
    bill_current = mean(eav_postexemptions * tax_code_rate), # money collected by non-TIF agencies
    
    bill_noGHE = mean(tc_hyp_taxrate * (eav_postexemptions+exe_homeowner)), # this uses tax code tax rate
    rate_change = mean(tax_code_rate - tc_hyp_taxrate),   # avg tax code level rate change 
    muni_avg_rate_change = mean(muni_avg_rate - muni_hyp_taxrate),
    nobillchange_propertyEAV = round(mean(exe_homeowner * 
                                            (tax_code_rate / (tax_code_rate-tc_hyp_taxrate)))), #   
    nobillchange_propertyEAV_muni = round(mean(exe_homeowner *
                                                 (muni_avg_rate/(muni_avg_rate-muni_hyp_taxrate) )))
  ) |> 
  mutate(nochange_av = nobillchange_propertyEAV / eq_factor,
         nochange_av_muni = nobillchange_propertyEAV_muni/ eq_factor,
         ) 




muni_breakeven_points %>%       
  filter(clean_name %in% c("Chicago","Park Forest", "Phoenix", "Riverdale", "Winnetka")) |>
  ggplot(aes(y=median_AV, x = clean_name)) +
  geom_col()+
    geom_text(aes(y=median_AV + 3000, label = round(median_AV) ) ) +
  scale_y_continuous(labels = scales::dollar) +
  theme_classic() +
  scale_x_discrete(label = c("Chicago","Park Forest", "Phoenix", "Riverdale", "Winnetka")) + 
  labs(y = "Median Residential AV", x = "", 
       title = "Median Residential Property Assessed Value - All Class 2 property Types")

Code
muni_breakeven_points %>%       
  filter(clean_name %in% c("Chicago","Park Forest", "Phoenix", "Riverdale", "Winnetka")) |>
  ggplot(aes(y=nochange_av, x = clean_name)) +
  geom_col() +
  geom_text(aes(y=nochange_av + 3000, label = round(nochange_av)) ) +
  scale_y_continuous(labels = scales::dollar) +
  theme_classic() +
  scale_x_discrete(label = c("Chicago", "Park Forest", "Phoenix", "Riverdale", "Winnetka")) + 
  labs(y = "Breakeven Point - AV", x = "", 
  title = "Residential Property AV Breakeven Point", 
  caption = "Residential properties above these values would have their bills decrease if the GHE were eliminated
       (if they had claimed the GHE before)")

Code
#|include: false

## Doesn't make bars / doesn't work. idk why ?? I don't need it, I just don't know why it doesn't work. 
muni_breakeven_points %>%       
  filter(clean_name %in% c("Chicago","Park Forest", "Phoenix", "Riverdale", "Winnetka")) |>
  ggplot(aes(y=nochange_av_muni, x = clean_name)) +
  geom_col() +
  geom_text(aes(y=nochange_av_muni + 3000, label = round(nochange_av_muni)) ) +
  scale_y_continuous(labels = scales::dollar) +
  theme_classic() +
  scale_x_discrete(label = c("Chicago", "Park Forest", "Phoenix", "Riverdale", "Winnetka")) + 
  labs(y = "Breakeven Point - AV", x = "", 
  title = "Residential Property AV Breakeven Point", 
  caption = "Residential properties above these values would have their bills decrease if the GHE were eliminated
       (if they had claimed the GHE before)")

Code
muni_breakeven_points %>%
 # filter(nochange_av < 300000) %>%
  ggplot(aes(y=nochange_av, 
             x = median_AV, 
             label=clean_name
             )) +
  geom_abline(intercept = 0, slope = 1, lty = 2, alpha = .4) +
  geom_point(aes(alpha = .5)) +
 # geom_smooth(method = "lm" )+
  geom_text(aes(y = (nochange_av-5000), x = (median_AV)), size = 2)+
  theme_classic() +
  scale_x_continuous(labels = scales::dollar) +
  scale_y_continuous(labels = scales::dollar) +
  theme(legend.position = "none")+
  labs(y = " Breakeven Point", 
       x = "Median AV - Class 2 Properties in Municipality", 
      # title = "Some highly valued homes would have lower tax bills if the GHE were eliminated", 
       #caption = "The breakeven point is the assessed values at which a major class 2 
     #  property would not have their taxbill change if the GHE were eliminated."
     )
Figure 2.1: Includes all municipalities
Code
muni_breakeven_points %>%
  filter(nochange_av < 300000) %>%
  ggplot(aes(y=nochange_av, 
             x = median_AV, 
             label=clean_name
             )) +
  geom_abline(intercept = 0, slope = 1, lty = 2, alpha = .4) +

  geom_point(aes(alpha = .5)) +
  
  geom_point(data = (muni_breakeven_points %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Riverdale","Dolton"))), aes(y = nochange_av, x = median_AV, color = "red"), size = 3) +
  ggrepel::geom_label_repel(data = (muni_breakeven_points %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Riverdale", "Dolton"))), aes(y = (nochange_av), x = (median_AV)), size = 3)+
  theme_classic() +
  theme(panel.background = element_blank()) + 
  scale_x_continuous(labels = scales::dollar, expand = c(0,0), limits = c(0,170000) ) +
  scale_y_continuous(labels = scales::dollar, expand = c(0,0), limits = c(0,170000) ) +
    theme(legend.position = "none") +

  labs(title = "Municipalities' Median AV & Breakeven Point \nClass 2 Properties Only",
    y = "Breakeven Point", 
       x = "Median AV", 
 # caption = "The breakeven point is the assessed values at which a major class 2 property would not have their taxbill change if the 
 # GHE were eliminated. Excludes outliers: University Park, Bedford Park, McCook, Hodgkins, and Rosemont. Class 2 properties that had
 # claimed the GHE would have lower taxbills even if the GHE were removed if the AV is above the breakeven point."
 ) + coord_fixed()

#ggsave("breakevenpoint.svg", bg = "transparent")
Figure 2.2: Breakeven Point …

How progressive are exemptions?

Figure 3. Ratio of Tax Bills to AV for 25th and 75th Percentile AV Class 2 Properties with $10,000 GHE

For the different exemption scenarios, we created new exemption variables for alternate exemption amounts that can be subtracted from a properties equalized AV.

  • ex. For exe_neg10 all exemption amounts are equal to zero.
  • For exe_0, This variable should be the same as the current tax system since we did not add or remove any exempt EAV to the PIN. If the EAV for a PIN is less than 10000 EAV and they did claim the general homestead exemption, then their exempt EAV is equal to their EAV.
  • If the exempt EAV allowable for the GHE was increased to 20,000 EAV, then the variable exe_plus10 is used.
Code
cook_pins <- cook_pins %>% 
  left_join(class_dict, by = c("class" = "class_code")) %>%
    mutate(has_HO_exe = ifelse( exe_homeowner > 0, 1, 0),
         has_other_exe = ifelse(exe_senior + exe_freeze + exe_longtime_homeowner + exe_disabled + exe_vet_returning + exe_vet_dis_lt50 + exe_vet_dis_50_69 + exe_vet_dis_ge70 > 0, 1, 0 )) %>% 
  left_join(tax_codes) |>
  left_join(tc_muninames) %>%
  mutate(av = av_clerk) |>
  left_join(tax_code_change) |>
  left_join(tif_distribution) |>
  mutate(
    tax_code_distribution_pct = ifelse(is.na(tax_code_distribution_pct), 0, tax_code_distribution_pct),
    in_tif = ifelse(tax_code_num %in% tif_distribution$tax_code_num, 1, 0),
    has_HO_exe = ifelse(exe_homeowner > 0, 1, 0),
    has_other_exe = ifelse(exe_senior + exe_freeze + exe_longtime_homeowner + exe_disabled +
                             exe_vet_returning + exe_vet_dis_lt50 + exe_vet_dis_50_69 + 
                             exe_vet_dis_ge70 > 0, 1, 0 ),
    all_exemptions = rowSums(across(starts_with("exe_"))),
    final_tax_to_tif = tax_bill_total *  tax_code_distribution_pct,
    final_tax_to_dist = tax_bill_total-final_tax_to_tif,
    tif_increment_eav = final_tax_to_tif / tax_code_rate,
    
    nontif_eav_preexe = av_clerk*eq_factor - tif_increment_eav,
    nontif_eav_postexe = av_clerk*eq_factor - tif_increment_eav - all_exemptions,
    
    eav_postexemptions = av_clerk*eq_factor - all_exemptions,
    eav_preexemptions = av_clerk*eq_factor) |>
  ## Create variables for alternate exemption amounts that can be subtracted from a properties equalized AV
  mutate(
    eav = eav_preexemptions,
    exe_neg10 = 0,
         
      # exe_0 implies no additional or removed EAV. Current tax system. 
         exe_0 = ifelse(eav < 10000 & exe_homeowner!=0, eav, 
                             ifelse(eav>10000 & exe_homeowner!=0, 10000, 0 )),  # no change in current exemptions
         exe_plus10 = ifelse(eav < 20000 & exe_homeowner!=0, eav, 
                             ifelse(eav>20000 & exe_homeowner!=0, 20000, 0 )),
         exe_plus20 = ifelse(eav < 30000 & exe_homeowner!=0, eav, 
                             ifelse(eav>30000 & exe_homeowner!=0, 30000, 0 ) ),
         exe_plus30 = ifelse(eav < 40000 & exe_homeowner!=0, eav, 
                             ifelse(eav>40000 & exe_homeowner!=0, 40000, 0) ),
         exe_plus40 = ifelse(eav < 50000 & exe_homeowner!=0, eav, 
                             ifelse(eav>50000 & exe_homeowner!=0, 50000, 0) ),
         mil_home = ifelse(major_class_code == 2 & av*10 > 1000000, 1, 0))


# all pins in munis fully within cook county that are some form of single-family, detached home
singfam_pins <- cook_pins %>% 
  filter(Option2 == "Single-Family")
Code
# Calculates tax rates for all exemption scenarios.
scenario_calcs <- cook_pins %>%    
  left_join(nicknames) %>%
  group_by(clean_name) %>%

    summarize(MuniLevy = sum(final_tax_to_dist, na.rm = TRUE), # amount billed by munis with current exemptions in place
            current_nonTIF_EAV_post_exemps = sum(final_tax_to_dist/(tax_code_rate), na.rm = TRUE),
            current_TIF_increment_EAV = sum(final_tax_to_tif/(tax_code_rate), na.rm=TRUE),  
            current_Exempt_EAV = sum(all_exemptions, na.rm=T), 
            current_GHE = sum(exe_homeowner, na.rm=TRUE),
            Total_EAV = sum(all_exemptions+(final_tax_to_dist+final_tax_to_tif)/(tax_code_rate), na.rm = TRUE),
            exe_neg10 = sum(exe_neg10),
            exe_0 = sum(exe_0), # no change, for comparison
            exe_plus10 = sum(exe_plus10),
            exe_plus20 = sum(exe_plus20),
            exe_plus30 = sum(exe_plus30),
            exe_plus40 = sum(exe_plus40),
            mil_home = sum(mil_home)
            ) %>%

  # remove all GHE (up to 10,000 EAV added back to base per PIN), 
  # add exe_homeowner back to taxable base
  mutate(neg10_taxable_eav = Total_EAV - current_TIF_increment_EAV - current_Exempt_EAV + current_GHE,    # adds GHE exempt EAV back to taxable base and decreases tax rates
         plus10_taxable_eav = Total_EAV - current_TIF_increment_EAV - current_Exempt_EAV + current_GHE - exe_plus10, # will increase tax rates
         plus20_taxable_eav = Total_EAV - current_TIF_increment_EAV - current_Exempt_EAV + current_GHE - exe_plus20,
         plus30_taxable_eav = Total_EAV - current_TIF_increment_EAV - current_Exempt_EAV + current_GHE - exe_plus30,
         plus40_taxable_eav = Total_EAV - current_TIF_increment_EAV - current_Exempt_EAV + current_GHE - exe_plus40,
         scenario_noexemptions_taxable_eav = Total_EAV - current_TIF_increment_EAV) %>%
  
  mutate(tr_neg10 = MuniLevy / neg10_taxable_eav,
         tr_nochange = MuniLevy / current_nonTIF_EAV_post_exemps,
         tr_plus10 = MuniLevy / plus10_taxable_eav,
         tr_plus20 = MuniLevy / plus20_taxable_eav,
         tr_plus30 = MuniLevy / plus30_taxable_eav,
         tr_plus40 = MuniLevy / plus40_taxable_eav, 
         tax_rate_current = MuniLevy/current_nonTIF_EAV_post_exemps,
         taxrate_noexemps = MuniLevy /(Total_EAV - current_TIF_increment_EAV  ),
         taxrate_noTIFs = MuniLevy / (Total_EAV - current_Exempt_EAV),
         taxrate_noTIFs_orExemps = MuniLevy / Total_EAV) %>%
  select(clean_name, MuniLevy, tr_neg10:taxrate_noTIFs_orExemps, everything())

scenario_taxrates <- scenario_calcs %>%  
  select(clean_name, MuniLevy, tr_neg10:taxrate_noTIFs_orExemps) 

scenario_taxrates
Code
munis_billchange <- munis_ranked %>% 
    left_join(scenario_taxrates) %>%

  mutate(
    eav = eav_preexemptions,
    exe_neg10 = 0,
         
      # exe_0 implies no additional or removed EAV. Current tax system. 
         exe_0 = ifelse(eav < 10000 & exe_homeowner!=0, eav, 
                             ifelse(eav>10000 & exe_homeowner!=0, 10000, 0 )),  # no change in current exemptions
         exe_plus10 = ifelse(eav < 20000 & exe_homeowner!=0, eav, 
                             ifelse(eav>20000 & exe_homeowner!=0, 20000, 0 )),
         exe_plus20 = ifelse(eav < 30000 & exe_homeowner!=0, eav, 
                             ifelse(eav>30000 & exe_homeowner!=0, 30000, 0 ) ),
         exe_plus30 = ifelse(eav < 40000 & exe_homeowner!=0, eav, 
                             ifelse(eav>40000 & exe_homeowner!=0, 40000, 0) ),
         exe_plus40 = ifelse(eav < 50000 & exe_homeowner!=0, eav, 
                             ifelse(eav>50000 & exe_homeowner!=0, 50000, 0) ),
         mil_home = ifelse(av*10 > 1000000, 1, 0)) |>
    mutate(,
    # current bill = current tax rate * portion of levy billed
         equalized_AV = av_clerk*eq_factor,
         
   # ## Made negative tax bills!! ## #
          bill_neg10 = tr_neg10*(equalized_AV-all_exemptions+ exe_homeowner -exe_neg10),

         bill_current = tax_code_rate*(equalized_AV-all_exemptions),
         bill_plus10 =  tr_plus10*(equalized_AV-all_exemptions+ exe_homeowner -exe_plus10),
         bill_plus20 = tr_plus20*(equalized_AV-all_exemptions+ exe_homeowner -exe_plus20),
         bill_plus30 = tr_plus20*(equalized_AV-all_exemptions+ exe_homeowner -exe_plus30),
         bill_plus40 = tr_plus20*(equalized_AV-all_exemptions+ exe_homeowner -exe_plus40),
    
    # ## make bills $1 if they had been negative.
         bill_neg10 = ifelse(bill_neg10 < 1, 1, bill_neg10),
         bill_current = ifelse(bill_current < 1, 1, bill_current),
         bill_plus10 = ifelse(bill_plus10 < 1, 1, bill_plus10),
         bill_plus20 = ifelse(bill_plus20 < 1, 1, bill_plus20),
         bill_plus30 = ifelse(bill_plus30 < 1, 1, bill_plus30),
         bill_plus40 = ifelse(bill_plus40 < 1, 1, bill_plus40),

         )%>%
  mutate(
         zerodol_bills_ghe0 = ifelse(bill_neg10 < 5, 1, 0),
         zerodol_bills_current = ifelse(bill_current < 5, 1, 0),
         zerodol_bills_ghe20 = ifelse(bill_plus10 < 5, 1, 0),
         zerodol_bills_ghe30 = ifelse(bill_plus20 < 5, 1, 0),
         zerodol_bills_ghe40 = ifelse(bill_plus30 < 5, 1, 0),
         zerodol_bills_ghe50 = ifelse(bill_plus40 < 5, 1, 0),
  ) %>%
  ungroup() |>
  group_by(clean_name, rank, has_HO_exe) %>% 
  summarize(median_AV = round(median(av)),
            median_EAV = round(median(eav)),
            mean_bill_neg10 = round(mean(bill_neg10, na.rm=TRUE)),
            mean_bill_cur = round(mean(bill_current, na.rm=TRUE)),
            mean_bill_plus10 = round(mean(bill_plus10, na.rm=TRUE)),
            mean_bill_plus20 = round(mean(bill_plus20, na.rm=TRUE)),
            mean_bill_plus30 = round(mean(bill_plus30, na.rm=TRUE)),
            mean_bill_plus40 = round(mean(bill_plus40, na.rm=TRUE)),
            
            tr_neg10 = round(mean(tr_neg10*100), digits = 2), 
            cur_comp_TC_rate = round(mean(tax_code_rate*100), digits = 2),
            tr_plus10 = round(mean(tr_plus10*100), digits = 2),
            tr_plus20 = round(mean(tr_plus20*100), digits = 2),
            tr_plus30 = round(mean(tr_plus30*100), digits = 2),
            tr_plus40 = round(mean(tr_plus40*100), digits = 2),
            pincount=n(),
            zerodol_bills_ghe0 = sum(zerodol_bills_ghe0),           
            zerodol_bills_current = sum(zerodol_bills_current),
            zerodol_bills_ghe20 = sum(zerodol_bills_ghe20),
            zerodol_bills_ghe30 = sum(zerodol_bills_ghe30),
            zerodol_bills_ghe40 = sum(zerodol_bills_ghe40),
            zerodol_bills_ghe50 = sum(zerodol_bills_ghe50),


  ) %>%
  mutate(clean_name = ifelse(clean_name == "Hazelcrest", "Hazel Crest", clean_name)) %>%
  arrange(has_HO_exe, rank)





ratios <- munis_billchange %>% 
  filter(has_HO_exe == 1 &  !is.na(rank)) %>% # claimed exemption in 2021
  mutate(currbill_to_AV_25 = ifelse(rank == "q25", mean_bill_cur/median_AV, NA)) %>%
  mutate(currbill_to_AV_75 = ifelse(rank == "q75", mean_bill_cur/median_AV, NA)) %>% 
  group_by(clean_name) %>%
  summarize(currbill_to_AV_25 = max(currbill_to_AV_25, na.rm=TRUE),
            currbill_to_AV_75 = max(currbill_to_AV_75, na.rm=TRUE)) %>%
  mutate(muni_ratio_25to75 = currbill_to_AV_25/currbill_to_AV_75) %>% filter(muni_ratio_25to75 > 0.01)



ggplot(data = ratios, aes(y = currbill_to_AV_25, x = currbill_to_AV_75, label = clean_name)) + 
  geom_abline(intercept = 0, slope = 1, lty = 2, alpha = .4) +
  geom_point(data = ratios, aes(alpha = .5)) + 
  ggrepel::geom_label_repel(data = (ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = currbill_to_AV_25, x = currbill_to_AV_75), size = 2, max.overlaps = Inf, point.padding = 0, # additional padding around each point
    min.segment.length = 0, # draw all line segments
    )+
    geom_point(data = (ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = currbill_to_AV_25, x = currbill_to_AV_75, color = "red"), size = 2)+

  theme_classic() +
  theme(legend.background = element_rect(fill = "transparent"), 
        legend.box.background = element_rect(fill = "transparent"), 
        panel.background = element_rect(fill = "transparent"), 
        panel.grid.major = element_blank(), 
        panel.grid.minor = element_blank(), 
        plot.background = element_rect(fill = "transparent", 
                                       color = NA) ) +
    scale_y_continuous(limits = c(0, .6), expand = c(0,0))+
   scale_x_continuous(limits = c(0, .6), expand = c(0,0))+
  theme(legend.position = "none") +
  labs(title="", y="Tax Burden as a Share of AV \n(25th Percentile)", x="Tax Burden as a Share of AV \n(75th Percentile)", subtitle = "Current Tax System, GHE = 10,000 EAV") + coord_fixed()

Code
cook_pins %>%
  filter(av < 300000) %>% # just to see the histogram better
  ggplot( aes(x=av)) +
  geom_histogram(bins = 50)  +
  theme_classic()+
  labs(title = "Cook County Class 2 Residential PIN Distribution of AV", 
       x = "Assessed Value ($)", y="# of Pins", 
       caption = "Dropped PINs with AVs over $300,000 for better visual of histogram bins.") +
  scale_x_continuous(label = scales::dollar) +
  scale_y_continuous(label = scales::comma)
singfam_pins %>%
 filter(av < 300000) %>% # just to see the histogram better
  
  ggplot( aes(x=av)) +
  geom_histogram(bins = 50)  +
  theme_classic()+
  labs(title = "Cook County Single-Family PIN Distribution of AV", 
       x = "Assessed Value ($)", y="# of Pins", 
       caption = "Dropped PINs with AVs over $300,000 for better visual of histogram bins."
       ) +
  scale_x_continuous(label = scales::dollar)+
  scale_y_continuous(label = scales::comma)
Distribution of Assessed Values: Class 2 Properties vs Single-Family Properties There is a difference in the assessed values depending on if you use all 200 level properties or only a subset of them. All figures and tables were created for both options when writing the report as a robustness check.

Distribution of Assessed Values: Class 2 Properties vs Single-Family Properties There is a difference in the assessed values depending on if you use all 200 level properties or only a subset of them. All figures and tables were created for both options when writing the report as a robustness check.

What would happen if the GHE value were increased?

Figure 4. Effect of Changing the GHE on Progressivity

Code
ratios <- munis_billchange %>% 
  filter(has_HO_exe == 1 &  !is.na(rank)) %>% # claimed exemption in 2021
  mutate(currbill_to_AV_25 = ifelse(rank == "q25", mean_bill_neg10/median_AV, NA)) %>%
  mutate(currbill_to_AV_75 = ifelse(rank == "q75", mean_bill_neg10/median_AV, NA)) %>% 
  group_by(clean_name) %>%
  summarize(GHE_0_bill_to_AV_25 = max(currbill_to_AV_25, na.rm=TRUE),
            GHE_0_bill_to_AV_75 = max(currbill_to_AV_75, na.rm=TRUE)) %>%
  mutate(muni_ratio_25to75 = GHE_0_bill_to_AV_25/GHE_0_bill_to_AV_75) 


ggplot(data = ratios, aes(y = GHE_0_bill_to_AV_25, x = GHE_0_bill_to_AV_75, label = clean_name)) + 
  geom_abline(intercept = 0, slope = 1) +
  geom_point(aes(alpha = .5)) + 
  ggrepel::geom_label_repel(data = (ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = GHE_0_bill_to_AV_25, x = GHE_0_bill_to_AV_75, label = clean_name), size = 3)+
    geom_point(data = (ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = GHE_0_bill_to_AV_25, x = GHE_0_bill_to_AV_75, color = "red"), size = 2)+
  theme_classic() + 
    scale_y_continuous(limits = c(0, .6), expand = c(0,0))+
  scale_x_continuous(limits = c(0, .6), expand = c(0,0))+
  theme(legend.position = "none") +
  labs(title = "GHE = $0 EAV", x = "", y = "")
ratios<- munis_billchange %>% 
  filter(has_HO_exe == 1 &  !is.na(rank)) %>% # claimed exemption in 2021
  mutate(currbill_to_AV_25 = ifelse(rank == "q25", mean_bill_cur/median_AV, NA)) %>%
  mutate(currbill_to_AV_75 = ifelse(rank == "q75", mean_bill_cur/median_AV, NA)) %>% 
  group_by(clean_name) %>%
  summarize(currbill_to_AV_25 = max(currbill_to_AV_25, na.rm=TRUE),
            currbill_to_AV_75 = max(currbill_to_AV_75, na.rm=TRUE)) %>%
  mutate(muni_ratio_25to75 = currbill_to_AV_25/currbill_to_AV_75) %>% filter(muni_ratio_25to75 > 0.01)



ggplot(data = ratios, aes(y = currbill_to_AV_25, x = currbill_to_AV_75, label = clean_name)) + 
  geom_abline(intercept = 0, slope = 1) +
  geom_point(data = ratios, aes(alpha = .5)) + 
  ggrepel::geom_label_repel(data = (ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = currbill_to_AV_25, x = currbill_to_AV_75), size = 3, max.overlaps = Inf, point.padding = 0, # additional padding around each point
    min.segment.length = 0, # draw all line segments
    )+
    geom_point(data = (ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = currbill_to_AV_25, x = currbill_to_AV_75, color = "red"), size = 2)+

  theme_classic() +
    scale_y_continuous(limits = c(0, .6), expand = c(0,0))+
   scale_x_continuous(limits = c(0, .6), expand = c(0,0))+
  theme(legend.position = "none") +
  labs(title = "GHE = $10,000 EAV (Current)", x="", y="")
new_ratios<- munis_billchange %>% 
  filter(has_HO_exe == 1 &  !is.na(rank)) %>% # claimed exemption in 2021
  mutate(newbill_to_AV_25 = ifelse(rank == "q25", mean_bill_plus10/median_AV, NA)) %>%
  mutate(newbill_to_AV_75 = ifelse(rank == "q75", mean_bill_plus10/median_AV, NA)) %>% 
  group_by(clean_name) %>%
  summarize(newbill_to_AV_25 = max(newbill_to_AV_25, na.rm=TRUE),
            newbill_to_AV_75 = max(newbill_to_AV_75, na.rm=TRUE)) %>%
  mutate(muni_ratio_25to75 = newbill_to_AV_25/newbill_to_AV_75) %>% 
  filter(muni_ratio_25to75 > 0.01)


ggplot(data = new_ratios, aes(y = newbill_to_AV_25, x = newbill_to_AV_75, label = clean_name)) + 
  geom_abline(intercept = 0, slope = 1) +
  geom_point(data = new_ratios, aes(alpha = .5)) + 
  ggrepel::geom_label_repel(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75), size = 3, max.overlaps = Inf, point.padding = 0, # additional padding around each point
    min.segment.length = 0, # draw all line segments
    )+
    geom_point(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75, color = "red"), size = 2)+
  theme_classic() + 
  scale_y_continuous(limits = c(0, .6), expand = c(0,0))+
  scale_x_continuous(limits = c(0, .6), expand = c(0,0))+
  theme(legend.position = "none") + 
  labs(title = "GHE = $20,000 EAV", x= "", y="")
new_ratios<- munis_billchange %>% 
  filter(has_HO_exe == 1 &  !is.na(rank)) %>% # claimed exemption in 2021
  mutate(newbill_to_AV_25 = ifelse(rank == "q25", mean_bill_plus20/median_AV, NA)) %>%
  mutate(newbill_to_AV_75 = ifelse(rank == "q75", mean_bill_plus20/median_AV, NA)) %>% 
  group_by(clean_name) %>%
  summarize(newbill_to_AV_25 = max(newbill_to_AV_25, na.rm=TRUE),
            newbill_to_AV_75 = max(newbill_to_AV_75, na.rm=TRUE)) %>%
  mutate(muni_ratio_25to75 = newbill_to_AV_25/newbill_to_AV_75) %>% 
  
  filter(muni_ratio_25to75 > 0.01)


ggplot(data = new_ratios, aes(y = newbill_to_AV_25, x = newbill_to_AV_75, label = clean_name)) + 
  geom_abline(intercept = 0, slope = 1) +
  geom_point(aes(alpha = .5)) + 
  ggrepel::geom_label_repel(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75), size = 3, max.overlaps = Inf, point.padding = 0, # additional padding around each point
    min.segment.length = 0, # draw all line segments
    )+
    geom_point(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75, color = "red"), size = 2)+
  theme_classic() + 
    scale_y_continuous(limits = c(0, .6), expand = c(0,0))+

  scale_x_continuous(limits = c(0, .6), expand = c(0,0))+
  theme(legend.position = "none") + 
  labs(title = "GHE = $30,000 EAV",
                                        x = "", y = "")
new_ratios<- munis_billchange %>% 
  filter(has_HO_exe == 1 &  !is.na(rank)) %>% # claimed exemption in 2021
  mutate(newbill_to_AV_25 = ifelse(rank == "q25", mean_bill_plus30/median_AV, NA)) %>%
  mutate(newbill_to_AV_75 = ifelse(rank == "q75", mean_bill_plus30/median_AV, NA)) %>% 
  group_by(clean_name) %>%
  summarize(newbill_to_AV_25 = max(newbill_to_AV_25, na.rm=TRUE),
            newbill_to_AV_75 = max(newbill_to_AV_75, na.rm=TRUE)) %>%
  mutate(muni_ratio_25to75 = newbill_to_AV_25/newbill_to_AV_75) %>% 
  filter(muni_ratio_25to75 > 0.01)


ggplot(data = new_ratios, aes(y = newbill_to_AV_25, x = newbill_to_AV_75, label = clean_name)) + 
  geom_abline(intercept = 0, slope = 1) +
  geom_point(aes(alpha = .5)) + 
  ggrepel::geom_label_repel(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75), size = 3, max.overlaps = Inf, point.padding = 0, # additional padding around each point
    min.segment.length = 0, # draw all line segments
    )+
    geom_point(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75, color = "red"), size = 2)+
  theme_classic() + 
    scale_y_continuous(limits = c(0, .6), expand = c(0,0))+

  scale_x_continuous(limits = c(0, .6), expand = c(0,0))+
  theme(legend.position = "none") + 
  labs(title = "GHE = $40,000 EAV", 
                                     x= "", y = "")
new_ratios<- munis_billchange %>% 
  filter(has_HO_exe == 1 &  !is.na(rank)) %>% # claimed exemption in 2021
  mutate(newbill_to_AV_25 = ifelse(rank == "q25", mean_bill_plus40/median_AV, NA)) %>%
  mutate(newbill_to_AV_75 = ifelse(rank == "q75", mean_bill_plus40/median_AV, NA)) %>% 
  group_by(clean_name) %>%
  summarize(newbill_to_AV_25 = max(newbill_to_AV_25, na.rm=TRUE),
            newbill_to_AV_75 = max(newbill_to_AV_75, na.rm=TRUE)) %>%
  mutate(muni_ratio_25to75 = newbill_to_AV_25/newbill_to_AV_75) %>% filter(muni_ratio_25to75 > 0.01)


ggplot(data = new_ratios, aes(y = newbill_to_AV_25, x = newbill_to_AV_75, label = clean_name)) + 
  geom_abline(intercept = 0, slope = 1) +
  geom_point(aes(alpha = .5)) + 
  ggrepel::geom_label_repel(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75), size = 3, max.overlaps = Inf, point.padding = 0, # additional padding around each point
    min.segment.length = 0, # draw all line segments
    )+
    geom_point(data = (new_ratios %>% filter(clean_name %in% c("Park Forest", "Chicago", "Winnetka", "Glencoe", "Riverdale", "Dolton", "Markham", "Chicago Heights", "Hazel Crest", "Phoenix"))), aes(y = newbill_to_AV_25, x = newbill_to_AV_75, color = "red"), size = 2)+
  theme_classic() + 
    scale_y_continuous(limits = c(0, .6), expand = c(0,0))+

  scale_x_continuous(limits = c(0, .6), expand = c(0,0))+
  theme(legend.position = "none") + labs(title = "GHE = $50,000", 
                                      x = "", y = "")

Table 3. Revenue Neutral Tax Rates

Revenue Neutral Tax Rates (Percent) for Select Municipalities as a Function of GHE Value

Code
scenario_taxrates %>% 
  filter(clean_name %in% c("Chicago", "Dolton", "Glencoe")) |>
  select(Municipality = clean_name,
         "0 GHE" = tr_neg10,
         "10,000 GHE (Current)" = tr_nochange,
         "20,000 GHE" = tr_plus10,
         "30,000 GHE" = tr_plus20,
         "40,000 GHE" = tr_plus30,
         "50,000 GHE" = tr_plus40)

Figure 5. GHE Value and Revenue Neutral Tax Rates

Relationship of GHE Value and Revenue Neutral Tax Rates in Chicago, Dolton, and Glencoe

Code
scenario_taxrates %>% 
  filter(clean_name %in% c("Chicago", "Dolton", "Glencoe")) |>
  select(Municipality = clean_name,
         "0 GHE" = tr_neg10,
         "10,000 GHE\n(Current)" = tr_nochange,
         "20,000 GHE" = tr_plus10,
         "30,000 GHE" = tr_plus20,
         "40,000 GHE" = tr_plus30,
         "50,000 GHE" = tr_plus40) %>%
  pivot_longer(cols = c(`0 GHE`:`50,000 GHE`), names_to = "Scenarios") %>%

  ggplot() +
  geom_line(aes(x=Scenarios, y = value, group = Municipality, color = Municipality)) + theme_classic() + 
  labs(title = "Tax Rates for GHE Scenarios", x = "GHE Amount", y = "Tax Rate") +
  scale_y_continuous(label = scales::percent)

Paying for the inequitable effects of exemptions

Figure 6. Exemptions by Value in Class 8 Municipalities

Code
cook_pins %>% 
  group_by(clean_name) %>% 
  summarize(
    Homeowner = sum(exe_homeowner),
    Senior = sum(exe_senior),
    `Senior Freeze` = sum(exe_freeze),
   # exe_longtime_homeowner = sum(exe_longtime_homeowner),
    Other = sum(exe_disabled),
    
  ) %>%
  filter(clean_name %in% c("Blue Island", "Markham", "Matteson", "Park Forest")) %>%
  pivot_longer(cols = c(Homeowner:Other), names_to = "Exemption Type") %>%
  ggplot() + 
  geom_col(aes(x=clean_name, y = value, fill = `Exemption Type`), position = "dodge") +
  scale_y_continuous(label = scales::dollar) + theme_classic() + labs(x = element_blank(), y = "Exempt EAV")

Figure 7. Exemption Share in Class 8 Municipalities

Code
cook_pins %>% 
  group_by(clean_name) %>% 
  summarize(
    all_exemptions = sum(all_exemptions),
    eq_av = sum(av_clerk*eq_factor),
    tif_increment_eav = sum(tif_increment_eav)
  ) %>%
  mutate(taxable_eav = eq_av - tif_increment_eav - all_exemptions) |>
  filter(clean_name %in% c("Blue Island", "Markham", "Matteson", "Park Forest")) %>%
  pivot_longer(cols = c(all_exemptions:taxable_eav), names_to = "exe_type") %>%
  filter(exe_type %in% c("taxable_eav", "all_exemptions"))|>
  ggplot() + 
  geom_col(aes(x=clean_name, y = value, fill = exe_type), position = "stack") +
  scale_y_continuous(label = scales::dollar) + 
  theme_classic() + 
  labs(x = element_blank(), y = "Non-TIF Increment EAV in Municipality")