library(tidyverse)library(ptaxsim)library(DBI)library(httr)library(jsonlite)library(glue)library(DT)knitr::opts_chunk$set(warning =FALSE, message =FALSE)# # Create an empty data frame with a column named "year"# params <- data.frame(year = numeric(0))# # # Add the value 2021 to the "year" column# params <- rbind(params, data.frame(year = 2021))i = params$yearclass_dict <-read_csv("../Necessary_Files/class_dict_singlefamcodes.csv") %>%mutate(class_code =as.character(class_code)) # change variable type to character so the join works.eq_factor <-read_csv("../Necessary_Files/eq_factor.csv") %>%filter(year == i) |>select(eq_factor = eq_factor_final) |>as.numeric()muni_ratechange <-read_csv(paste0("../Output/muni_ratechange_", i, "_test.csv"))muni_rates <- muni_ratechange |># keep just a couple variables to avoid join variable name conflicts laterselect(clean_name, current_rate_avg, rate_noGHE, rate_current)pin_data <-read_csv(paste0("../Output/Dont_Upload/0_joined_PIN_data_", i, "_test.csv")) %>%mutate(class =as.character(class))c2pins <- pin_data |>filter(class >199& class <300) |>mutate(eav_postexemptions = taxed_eav)
Addendum to Exemption Report - Tax Year 2021
Code
q =c(.1, .25, .5, .75, .9)## ranks properties that are considered C2 properties in order of AV for each Munimuni_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(taxed_eav)), median_av =round(median(av)), avg_av =round(mean(av)),avg_eav =round(mean(taxed_eav)),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 Propertiesmunis_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())
Many of the figures and tables in the Exemption Addendum (2024) refer to the “Alternative Scenario” (AS) and the “Status Quo” (SQ). The status quo is simply Cook County’s current property tax regime. The alternative scenario, by way of contrast, sets the general homestead exemption (and only general homestead exemption) to zero.
We believe this decision is a feature and not bug of the analysis in the Addendum, which has an emphasis on equity. The effects of changing the value of the most prevalent homestead exemption illuminate the effect of changing other exemptions.
Which taxpayers benefit the most and least 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
Code
tax_bill_change_HO <- c2pins %>%left_join(muni_rates) |>filter(av >0) %>%arrange(av) %>%mutate(#eq_av = av_clerk*eq_factor,bill_current = (taxed_eav*tax_code_rate),bill_noexemps = rate_noGHE*(taxed_eav+exe_homeowner),bill_change = bill_noexemps - bill_current) %>%group_by(clean_name, has_HO_exemp) %>%summarize(median_AV =median(av),median_EAV =median(taxed_eav),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")) |> DT::datatable(rownames =FALSE, colnames =c('Claims GHE'='has_HO_exemp','Current Bill'='median_bill_cur','Hyp. Bill'='median_bill_new', 'Bill Change'='median_change', 'Perceived Savings'='perceived_savings'),caption ="Change in Tax Bill for Properties with an Assessed Value of $15,000.") |>formatCurrency(columns =c(2, 4:9), digits =0)
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& exe_missing_disvet ==0) %>%group_by(clean_name) %>%summarize(median_AV =median(av), # median class 2 AV for properties that did claim the GHE but not other exemptionsbill_current =mean(eav_postexemptions * tax_code_rate), # money collected by non-TIF agenciesbill_noGHE =mean(rate_noGHE * (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(tax_code_rate - rate_noGHE),# nobillchange_propertyEAV = round(mean(exe_homeowner * # (tax_code_rate / (tax_code_rate-tc_hyp_taxrate)))), # nobillchange_propertyEAV_muni =round(mean(exe_homeowner * (rate_current/(rate_current-rate_noGHE) ))) ) |>mutate(nochange_av = nobillchange_propertyEAV_muni / 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)")
Of homeowners who take the GHE, those that gain the largest monetary benefit from the exemption in proportion to their home values are those who own below-average valued properties that are in a municipality with relatively high tax rates. Property owners who benefit the least from taking the exemption (again, relative to the value of their homes) are those with above-average valued homes in jurisdictions with low tax rates. Note that these are not the same homeowners who benefit the most in absolute dollar terms—that outcome depends on both the absolute and relative value of the property and the composition of properties within the jurisdiction.
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." )
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")
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.
pin_data %>%filter(av <300000) %>%# just to see the histogram betterggplot( 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 betterggplot( 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)
What would happen if the GHE value were increased?
Figure 4. Effect of Changing the GHE on Progressivity