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
Table 4.1: Change in Tax Bill for Properties with an Assessed Value of $15,000. AV stands for “assessed value” “Tax calculator reduction” refers to the exemption values shown on the tax bill received by taxpayers. Values are estimated using an average of the properties with an AV within $1,000 of it’s municipality’s median AV to enlarge the number of observations. Thus, our calculations do not precisely represent the median.
Municipality
Claims GHE
median_AV
median_EAV
Current Bill
Hyp. Bill
Bill Change
pincount
Perceived Savings
Park Forest
0
$5,703.50
$17,125.90
$7,511
$5,316
-$1,534
1,840
$0.00
Park Forest
1
$6,968.00
$7,611.83
$3,311
$5,462
$2,315
3,436
$4,394.30
Phoenix
0
$2,000.50
$6,006.90
$1,799
$1,267
-$532
522
$0.00
Phoenix
1
$3,668.00
$884.71
$265
$2,110
$1,700
474
$2,995.40
Riverdale
0
$5,534
$16,616.94
$5,201
$4,141
-$1,057
1,544
$0.00
Riverdale
1
$6,056
$6,634.96
$2,082
$4,130
$1,979
2,391
$3,137.50
Winnetka
0
$71,071.00
$213,405
$18,652
$18,033
-$422
1,316
$0.00
Winnetka
1
$85,822.50
$245,686
$21,252
$21,601
$357
3,162
$864.30
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")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)")
Median property values vary widely across Cook County.
Break even points also vary across municipalities, sometimes by orders of magnitude. Breakeven points are always higher than the median property within a municipality.
Median Residential Property and Breakeven Point
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." )
Figure 4.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 4.2: 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.
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
pin_data <- pin_data %>%left_join(class_dict, by =c("class"="class_code")) %>%mutate(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 AVmutate(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 exemptionsexe_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 homesingfam_pins <- pin_data %>%filter(Option2 =="Single-Family")
Code
# Calculates tax rates for all exemption scenarios.scenario_calcs <- pin_data %>%group_by(clean_name) %>%summarize(MuniLevy =round(sum(final_tax_to_dist, na.rm =TRUE)), # amount billed by munis with current exemptions in placecurrent_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 comparisonexe_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 basemutate(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 ratesplus10_taxable_eav = Total_EAV - current_TIF_increment_EAV - current_Exempt_EAV + current_GHE - exe_plus10, # will increase tax ratesplus20_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 |>select(-c(tax_rate_current:taxrate_noTIFs_orExemps)) |>#mutate(across(.cols=(tr_neg10:tr_plus40), s, accuracy = 0.01)) |>rename(Municipality = clean_name,`Composite Levy`= MuniLevy,`Rate if \nGHE=0K`= tr_neg10, `Current\nTax Rate`= tr_nochange, `Rate if\nGHE=20K`= tr_plus10, `Rate if\nGHE=30K`= tr_plus20, `Rate if\nGHE=40K`= tr_plus30, `Rate if\nGHE=50K`= tr_plus40) |> DT::datatable(rownames =FALSE) |>formatCurrency(columns =c(2), digits =0) |>formatPercentage(3:8, digits =1)
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)
Major Class 2 Properties
Single-family subgroup of C2 Properties
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
Figure 7. Exemption Share in Class 8 Municipalities
Code
pin_data %>%group_by(clean_name) %>%summarize(all_exemptions =sum(exe_total_adj),eq_av =sum(av_clerk*eq_factor),tif_increment_eav =sum(final_tax_to_tif/tax_code_rate) ) %>%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") %>%# all exemptions includes all exemptions, but adjusted for missing disabled veteran exemptions filter(exe_type %in%c("taxable_eav", "all_exemptions"))|>mutate(EAV =factor(exe_type, levels =c("taxable_eav", "all_exemptions"), labels =c("Taxed EAV", "Exempt EAV")) ) |>ggplot() +geom_col(aes(x=clean_name, y = value, fill = EAV), position ="stack") +scale_y_continuous(label = scales::dollar, limits =c(0,600000000)) +theme_classic() +geom_text(aes(x=clean_name, y = value, label = scales::comma(value)), position ="stack", vjust=1.1)+labs(x =element_blank(), y ="Non-TIF Increment EAV in Municipality")
With adjusted exemption values and calculating allvalues from the pin datatable instead of using ptaxsim::tax_bill() values. Would be more correct than original ways values were calculated.
Million dollar homes
Addendum mentions scenario where homes valued at $1 million were ineligible for the GHE.