1  The Use and Effects of Homestead Exemptions in Cook County

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

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

muni_shp <- read_sf("https://gis.cookcountyil.gov/traditional/rest/services/politicalBoundary/MapServer/2/query?outFields=*&where=1%3D1&f=geojson")


nicknames <- readxl::read_excel("../Necessary_Files/muni_shortnames.xlsx")

1.1 Cook County’s Use of Homestead Exemptions

1.1.1 Figure 1 or 2?. Exemption Use

Code
muni_sums <- read_csv("../Output/ptaxsim_muni_level_2006-2022.csv") |> 
  left_join(nicknames)

muni_cl_sums <- read_csv("../Output/ptaxsim_muni_class_summaries_2006-2022.csv") |> 
  left_join(nicknames)
Code
year_examples <- c(2020, 2021, 2022, 2023)

for(i in year_examples){
  tbl <- muni_cl_sums |> 
  filter(year == i) |>
  group_by(year) |> 
  summarize(AV = sum(muni_c_av), 
            EAV = sum(muni_c_eav), 
            Eq_AV = sum(muni_c_equalized_av),
            'Taxed EAV' = sum(muni_c_current_taxable_eav),
            'All Exemptions' = sum(muni_c_all_exemptions), 
            'GHE' = sum(muni_c_exe_homeowner), 
            'Senior Exemp.' = sum(muni_c_exe_senior), 
            'Freeze Exemp.' = sum(muni_c_exe_freeze), 
            'PINs in Muni' = sum(muni_c_pins_in_muni),
            'PINs with Exemptions' = sum(muni_c_has_HO_exemp))  |> 
  pivot_longer(cols = c(AV:'PINs with Exemptions'), 
               names_to = "Totals", values_to = "Values") 
print(tbl)
}
# A tibble: 10 × 3
    year Totals                      Values
   <dbl> <chr>                        <dbl>
 1  2020 AV                    64631584955 
 2  2020 EAV                  208327851913 
 3  2020 Eq_AV                208333450231 
 4  2020 Taxed EAV            173429797449.
 5  2020 All Exemptions        15618565749 
 6  2020 GHE                   10142504653 
 7  2020 Senior Exemp.          2495592777 
 8  2020 Freeze Exemp.          2903305677 
 9  2020 PINs in Muni              1864035 
10  2020 PINs with Exemptions      1025686 
# A tibble: 10 × 3
    year Totals                      Values
   <dbl> <chr>                        <dbl>
 1  2021 AV                    70733758749 
 2  2021 EAV                  212387387384 
 3  2021 Eq_AV                212392258554 
 4  2021 Taxed EAV            175164229005.
 5  2021 All Exemptions        16110627371 
 6  2021 GHE                   10161823944 
 7  2021 Senior Exemp.          2627007663 
 8  2021 Freeze Exemp.          3229599122 
 9  2021 PINs in Muni              1864594 
10  2021 PINs with Exemptions      1028964 
# A tibble: 10 × 3
    year Totals                      Values
   <dbl> <chr>                        <dbl>
 1  2022 AV                    75674322433 
 2  2022 EAV                  221244018107 
 3  2022 Eq_AV                221249023646 
 4  2022 Taxed EAV            184148539238.
 5  2022 All Exemptions        15202462011 
 6  2022 GHE                    9971093071 
 7  2022 Senior Exemp.          2687007250 
 8  2022 Freeze Exemp.          2450908713 
 9  2022 PINs in Muni              1865016 
10  2022 PINs with Exemptions      1010623 
# A tibble: 0 × 3
# ℹ 3 variables: year <dbl>, Totals <chr>, Values <dbl>

1.1.2 Figure 3. Percent Exempt

Total value of exemptions as a share of residential EAV, 2021

Code
year_examples <- c(2020, 2021, 2022)

for(i in year_examples){

median_exempt <- muni_sums |>
  filter(year==i)|>
  mutate(pct_fmv_exempt = muni_fmv_exempt / muni_fmv ) |> 
  select(pct_fmv_exempt)

print(muni_sums |>
  filter(year==i)|>
  mutate(pct_fmv_exempt = muni_fmv_exempt / muni_fmv ) |>
    mutate(agency_name = ifelse(agency_name == "TOWN CICERO", "CITY OF CICERO", agency_name) ) |>
 full_join(muni_shp, by = c("agency_name" = "AGENCY_DESC")) |>
  ggplot(aes(fill = pct_fmv_exempt)) + 
  geom_sf(aes(geometry = geometry), color = "black") + 
  theme_void()+ 
  labs(title = paste0(i), 
       subtitle =  "Exempt FMV / Municipality FMV") +
    theme_void() + 
  theme(axis.ticks = element_blank(), 
        axis.text = element_blank())+
   scale_fill_steps2(high = "darkblue", low = "black",  mid = "beige",
                       n.breaks = 7, show.limits=TRUE,
                       na.value = NA,
                    nice.breaks = FALSE,
                   midpoint = median(median_exempt$pct_fmv_exempt),
                        name = "% FMV \nthat is exempt", label = scales::percent))
}

1.1.3 Figure 4. Exempt Tax Base in Cook County

Exempt Tax Base in Cook County by exemption type and geography, for tax year 2021

1.1.4 Figure 5. Value of residential exemptions by type in Cook County, 2006 - 2023

Report went up to 2021, this image includes values for 2021 & 2023.

1.2 Effect on Composite Tax Rates

Code
muni_sums <- read_csv("../Output/ptaxsim_muni_level_2006-2022.csv") |> 
  left_join(nicknames)

muni_comp_rates <- muni_sums |>
  select(year, clean_name, muni_current_rate_avg)

muni_cl_sums <- left_join(muni_cl_sums, muni_comp_rates, by = c("year", "clean_name"))

year_examples <- c(2020, 2021, 2022, 2023)

for(i in year_examples){
  
tbl <- muni_cl_sums |>  
      filter(year == i) |> 
      group_by(clean_name) |> 
      summarize(muni_current_rate_avg = first(muni_current_rate_avg),
            muni_levy = sum(muni_c_final_tax_to_dist),
            muni_current_taxable_eav = sum(muni_c_current_taxable_eav),
            muni_exe_homeowner = sum(muni_c_exe_homeowner)) |>
      mutate(        
            new_comp_muni_rate = muni_levy/(muni_current_taxable_eav + muni_exe_homeowner),
        new_comp_muni_rate = new_comp_muni_rate, 
        cur_comp_muni_rate = muni_current_rate_avg / 100,
        rate_change = cur_comp_muni_rate - new_comp_muni_rate) |>
          
      select(clean_name, rate_change, cur_comp_muni_rate, new_comp_muni_rate, muni_levy) |> 
    arrange(desc(rate_change)) |>
     datatable(rownames = FALSE,
              colnames = c('Municipality' = 'clean_name', 
                           'Composite Tax Rate Change'='rate_change', 
                           'Current Comp. Rate' = 'cur_comp_muni_rate', 
                           'Hypothetical Rate' = 'new_comp_muni_rate', 
                           'Composite Levy' = 'muni_levy')
              ) |>
    formatPercentage(c('Current Comp. Rate', 'Hypothetical Rate', 
                       'Composite Tax Rate Change'
    ), 
    digits = 2) |>  
  formatCurrency('Composite Levy', digits = 0) 
   print(htmltools::tagList(tbl)) 
}
Code
# 
# 
# MuniLevel_CompRates <- muni_sums |> 
#   filter(year == 2021) |>
#   select(clean_name, cur_comp_muni_rate, new_comp_muni_rate, tif_share, zero_bills, final_tax_to_dist) |>
#   mutate(cur_comp_muni_rate = cur_comp_muni_rate/100,
# new_comp_muni_rate = new_comp_muni_rate / 100 ,
# rate_change = cur_comp_muni_rate - new_comp_muni_rate) |>
#   select(clean_name, rate_change, cur_comp_muni_rate, new_comp_muni_rate, final_tax_to_dist) |> arrange(desc(rate_change))
# 
# datatable(MuniLevel_CompRates, rownames = FALSE,
#           colnames = c('Municipality' = 'clean_name', 
#                        'Composite Tax Rate Change'='rate_change', 
#                        'Current Comp. Rate' = 'cur_comp_muni_rate', 
#                        'Hypothetical Rate' = 'new_comp_muni_rate', 
#                        #'%Rev to TIF' = 'tif_share', 'Count of $0 Tax Bills' = 'zero_bills', 
#                        'Composite Levy' = 'final_tax_to_dist'),
#           caption = "Table 1: 2021 Current and Hypothetical Composite Tax Rates if GHE $0") |>
#   formatPercentage(c('Current Comp. Rate', 'Hypothetical Rate', 
#                     # '%Rev to TIF'
#                     'Composite Tax Rate Change'
#                      ), 
#                    digits = 2) |>  
#   formatCurrency('Composite Levy', digits = 0)
Table 1.1: Table 1: Actual and Hypothetical Composite Tax Rates if GHE $0 in select Years

1.2.1 Figure 6. Composite property tax rates with and without homestead exemptions, tax year 2021

1.2.2 Table 1. Change in composite property tax rates due to exemptions, tax year 2021.

Code
muni_sums <- read_csv("../Output/ptaxsim_muni_level_2006-2021.csv") |> 
  filter(year == 2021) |> 
  left_join(nicknames)
Code
MuniLevel_CompRates <- muni_sums |> 
  select(clean_name, cur_comp_muni_rate, new_comp_muni_rate, tif_share, zero_bills, final_tax_to_dist) |>
  mutate(current_rate_avg = cur_comp_muni_rate/100,
new_comp_muni_rate = new_comp_muni_rate / 100 ,
rate_change = current_rate_avg - new_comp_muni_rate) |>
  select(clean_name, rate_change, current_rate_avg, new_comp_muni_rate, final_tax_to_dist) |> arrange(desc(rate_change))

datatable(MuniLevel_CompRates, rownames = FALSE,
          colnames = c('Municipality' = 'clean_name', 'Composite Tax Rate Change'='rate_change', 'Current Comp. Rate' = 'current_rate_avg', 'Hypothetical Rate' = 'new_comp_muni_rate', 
                       #'%Rev to TIF' = 'tif_share', 'Count of $0 Tax Bills' = 'zero_bills', 
                       'Composite Levy' = 'final_tax_to_dist'),
          caption = "Table 1: Current and Hypothetical Composite Tax Rates if GHE $0") |>
  formatPercentage(c('Current Comp. Rate', 'Hypothetical Rate', 
                    # '%Rev to TIF'
                    'Composite Tax Rate Change'
                     ), 
                   digits = 2) |>  
  formatCurrency('Composite Levy', digits = 0)

1.2.3 Figure 7. Map of Spatial Patterns in Composite Tax Rate Change

Change in composite tax rate if exempt EAV was added back to the the taxable tax base (i.e. if there were no exemptions).

1.2.4 Figure 7 Alternate. Composite Tax Rates for Municipalities

1.3 Effect on Tax Burdens

1.3.1 Figure 8. Dolton example of Share of levy paid by property type

CMAP used a donut chart, we will use a bar chart that represents 100% on the x a axis.

Code
MC_sums <- read_csv("../Output/ptaxsim_muni_MC_2006-2021.csv") |> 
 # rename_all(~str_replace_all(., "muni_mc_","")) |>
  filter(year == 2021)  |> 
  left_join(nicknames)

MC_burden <- MC_sums |> 
  mutate(hyp_taxable_eav = current_taxable_eav + exe_homeowner,
         class_taxes_current = current_taxable_eav * (cur_comp_muni_rate/100)
     ) |>
  group_by(clean_name) |>
  mutate(muni_eav = sum(eav),
         muni_levy = sum(final_tax_to_dist),
       hyp_muni_taxableEAV = sum(new_taxable_eav)
  ) |>
  ungroup() |>
  mutate(new_comp_rate = muni_levy / hyp_muni_taxableEAV,
         class_taxes_hyp = hyp_taxable_eav * (new_comp_rate),
         pct_eav = eav / muni_eav,
         # pct_taxburden_current = total_bill_current / muni_levy,
         # pct_taxburden_ghe0 = new_taxable_eav / muni_levy,
         pct_taxburden_current = class_taxes_current / muni_levy,
         pct_taxburden_ghe0 = class_taxes_hyp / muni_levy,
         burden_shift = (pct_taxburden_current - pct_taxburden_ghe0)*100)


current_burden_c2 <- MC_burden |> 
  filter(major_class_code == 2) |> 
  select(clean_name, pct_eav, burden_shift,  pct_taxburden_current, pct_taxburden_ghe0) |> arrange(pct_eav)

MC_burden |> filter(clean_name == "Dolton") |> select(major_class_code, pct_taxburden_current, pct_taxburden_ghe0) |> arrange(desc(pct_taxburden_current))

1.3.2 Table 3.

Difference in taxbills for those that do and do not claim homeowner exemption

1.3.3 Table 4.

Municipalities with the largest and smallest reductions in tax base (as a share of residential EAV due to exemptions) and median property values

1.3.4 Figure 10. Tax Burden Shift from Current GHE

Share of municipal property tax levy paid by Class 2 properties with and without homestead exemptions, tax year 2021

1.3.5 Table 2. Change in share of property tax burden

Table 2: Change in the share of property tax burden due to exemptions for single-family, multi-family, and commercial and industrial properties, tax year 2021

Code
datatable(current_burden_c2, rownames = FALSE,
          colnames = c('Municipality' = 'clean_name', 'Burden Shift, Pct Pt Change' = 'burden_shift', "C2 EAV/Muni EAV" = 'pct_eav', 'Current Tax Burden\nC2 Tax Collected / Muni Levy' = 'pct_taxburden_current', "Hypothetical Tax Burden\nHyp. C2 Tax Collected / Muni Levy" = 'pct_taxburden_ghe0'),
          caption = "Table 2: Current Share of Taxable EAV and Share of Levy Paid by Class 2 Properties"
           ) |>
  formatPercentage(c(2,4,5), digits = 2) |>
  formatRound(c(3), digits = 2)

1.3.5.1 Figure 10 Follow up. Current Tax Burden Map

Code
MC_burden |>
     # mutate(burden_current = ifelse(burden_current>1, 1, burden_current)) |>

     filter(major_class_code == 2) |>
  mutate(agency_name = ifelse(agency_name == "TOWN CICERO", "CITY OF CICERO", agency_name) ) |>
  full_join(muni_shp, by = c("agency_name" = "AGENCY_DESC")) |>

  ggplot(aes(fill = pct_taxburden_current)) + 
  geom_sf(aes(geometry = geometry), color = "black") + 
  theme_void() + 
  theme(axis.ticks = element_blank(), axis.text = element_blank())+
  # scale_fill_gradientn(
   scale_fill_stepsn(colors = c("#ffffcc","#a1dab4" ,"#41b6c4","#2c7fb8", "#253494"),
                        show.limits=TRUE, 
                     limits = c(0,1),
                     na.value = "gray70",
                     n.breaks = 6,
                        name = "Burden with \nExemptions", 
                     labels = scales::percent
                     
                    )+
  labs(title = "Current share of property tax burden", 
       subtitle = "for Class = 2 Property Types")

1.3.6 Figure 11. Zero Dollar Bills

MVH: I don’t think we need this one anymore?