From 5a4ec3c5feab1629a67e81a0b1780f01f663df96 Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Thu, 2 Apr 2026 15:03:12 +0000 Subject: [PATCH 01/18] Add new agencies vignette --- vignettes/agencies.Rmd | 629 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 629 insertions(+) create mode 100644 vignettes/agencies.Rmd diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd new file mode 100644 index 0000000..d86ba74 --- /dev/null +++ b/vignettes/agencies.Rmd @@ -0,0 +1,629 @@ +--- +title: "Keeping tabs on taxing agencies" +output: html_document +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + message = FALSE +) +``` + +# Introduction + +One of the unique features of PTAXSIM is the database that accompanies the package - this DB contains the only publicly accessible, machine-readable data collected from the various Cook County property tax agencies going back to 2006. While the primary use of PTAXSIM is its functionality to calculate tax bills, its database holds a plethora of data that can be used to investigate Cook County's property tax system, including the behavior of the over 900 taxing agencies and 400 TIFs that collect revenue through property taxes. + +In this vignette, we'll demonstrate how to query data from the PTAXSIM SQLite database that can help us analyze taxing agencies' property tax revenue over time, as well as revenue collected by TIFs. + +Additionally, we'll show how to account for the 2024 changes to the Cook County Clerk's agency fund report data structure which will be necessary for users to pay attention to when conducting a time series analysis of certain taxing agencies. + +# Chicago's levy over time + +Using data from the PTAXSIM database, let's look at the levy history for the City of Chicago from 2006 to 2024. + +First, load some useful libraries and instantiate a PTAXSIM DBI connection with the default name (`ptaxsim_db_conn`) expected by PTAXSIM functions. + +```{r} +library(data.table) +library(dplyr) +library(here) +library(ggplot2) +#library(ptaxsim) +devtools::load_all() + +ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.db")) +``` + +```{r, echo=FALSE} +# This is needed to build the vignette using GitHub Actions +#ptaxsim_db_conn <- DBI::dbConnect( + # RSQLite::SQLite(), + #Sys.getenv("PTAXSIM_DB_PATH") +#) +``` + +First, we'll query the database table `agency_info` to determine the City of Chicago's unique `agency_num`. + +```{r} +chi_agency_nums <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT agency_num, agency_name + FROM agency_info + WHERE agency_name LIKE '%CITY OF CHICAGO%' + " +) + +chi_agency_nums +``` + +Many taxing agencies pop up, most of which are Special Service Areas, or SSAs. SSAs typically overlay a commercial corridor where property owners will pay an additional tax to fund the SSA's additional serivces which can include maintenance, beautification and other additional services. + +For purpose of our analysis, we'll just focus on `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. + +First, we'll query all fields from the `agencies` table. + +```{r} +chi_agencies <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT DISTINCT * + FROM agency + WHERE agency_num = '030210000' + OR agency_num = '030210001' + " +) +``` + +When examining `chi_agencies`, we see that in 2024 the `CITY OF CHICAGO LIBRARY FUND` has a \$0 levy and therefore a tax rate of 0. This is because, starting in 2024, the Cook County Clerk began reporting the requested levy for the City of Chicago libraries as a fund under the `CITY OF CHICAGO` taxing agency. This is in fact aligned with how the City of Chicago reports its property tax levy in its own budget [documentation](https://chicityclerk.s3.us-west-2.amazonaws.com/s3fs-public/O2023-0005291_Tax_Levy.pdf). + +This reporting change occurred for many municipal taxing agencies, where the Clerk had previously reported municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agencies data over time. + +To make this process easier, we have added new fields to the `agency_info` table in the PTAXSIM database which identify the agencies that have been folded into their parent agencies. `agency_num_24` and `agency_name_24` contain the agency info that the old "sub-agency" has been merged into. Note that the user can still see details about these former agencies, now funds, by querying the `agency_fund` table. + +```{r} +# Query agency_info table for all agencies with the 2024 update +agency_cw_24 <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT * + FROM agency_info + WHERE agency_change_24 = 1 + " +) %>% + select(agency_num, agency_name, agency_num_24, agency_name_24) + +agency_cw_24 +``` +We can now use `agency_cw_24` as a crosswalk to convert these sub-agencies to their parent agency numbers. + +Knowing this caveat about 2024 agency data, we'll query the both `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND` for all years, and then convert them to have the same `agency_num` using `agency_cw_24` so we can the levy data into one summed amount per year. This will ensure consistent reporting across the time series. + +```{r} +chi_agencies <- chi_agencies %>% + # Join the agency crosswalk to get the parent agency number for pre-2024 years + left_join(agency_cw_24, "agency_num") %>% + # for the agencies that did have an agency number change in 2024, replace the + # old agency_num with the new one + mutate( + agency_num = + ifelse(!is.na(agency_num_24), + agency_num_24, + agency_num) + ) +``` + +Now that we have the correct agency numbers for all years, we can select and aggregate the fields of interest for `CITY OF CHICAGO`. + +Many of the fields in the `agencies` table, which come from the Clerk's agency rate report, relate to PTELL calculations. Because the Chicago is a home rule municipality, it is not subject to PTELL limits as imposed by the State of Illinois, so these fields will not contain relevant info. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror those of PTELL which we'll revisit later. + +```{r} +chi_levy <- + chi_agencies %>% + group_by(year, agency_num) %>% + summarize( + total_final_levy = sum(total_final_levy), + total_final_rate = sum(total_final_rate), + total_ext = sum(total_ext), + cty_cook_eav = first(cty_cook_eav), + prior_eav = first(prior_eav), + curr_new_prop = first(curr_new_prop) + ) + + +chi_levy %>% + pivot(cols = c(total_final_levy, total_final_rate, total_ext)) + +``` + + +```{r, echo=FALSE} +chi_levy_plot_1 <- chi_levy %>% + ggplot() + + geom_line(aes(x = year, y = total_final_levy), linewidth = .8) + + scale_x_continuous(n.breaks = 10) + + scale_y_continuous(labels = scales::label_currency(), limits = c(0, 2000000000)) + + theme_minimal() + + theme( + axis.title = element_text(size = 13), + axis.title.x = element_text(margin = margin(t = 6)), + axis.title.y = element_text(margin = margin(r = 6)), + axis.text = element_text(size = 11), + strip.text = element_text(size = 16), + strip.background = element_rect(fill = "#c9c9c9"), + legend.title = element_text(size = 14), + legend.key.size = unit(24, "points"), + legend.text = element_text(size = 12), + legend.position = "bottom" + ) + +plotly::plot_ly(chi_levy, x = ~year, + y = ~total_final_levy, + type = 'scatter') + + + +``` + + + +
+ +```{r, echo=FALSE, out.width="100%"} +chi_levy_plot_1 +``` + +This PIN's total tax bill has increased slightly since 2006, with some dips in the last few years. Let's see how much it would've increased *without* its Homeowner Exemption. + +## Removing exemptions + +We can remove exemptions by modifying the inputs to the `tax_bill()` function and then recalculating each bill. + +First, we retrieve the `pin_dt` input to `tax_bill()` by using the `lookup_pin()` function. This input contains all of the exemptions, AV, and EAV information for each PIN. By default, it has the actual historical values, but we can modify it to produce counterfactual bills. In this case, we can remove all exemptions by setting the amount in each exemption column (prefixed with `exe_`) to zero. + +```{r} +exe_dt <- lookup_pin(2006:2020, "25321140050000") %>% + mutate(across(starts_with("exe_"), ~0)) %>% + setDT(key = c("year", "pin")) +``` + +Then, we recalculate each bill using the new, zeroed-out `pin_dt`. + +```{r} +bills_no_exe <- tax_bill(2006:2020, + "25321140050000", + pin_dt = exe_dt, + simplify = FALSE +) +``` + +Next, we do the same aggregation that we did for bills *with* exemptions, collapsing each bill into a total by year. + +```{r} +bills_no_exe_summ <- bills_no_exe %>% + group_by(year) %>% + summarize( + exe = sum(tax_amt_exe), + bill_total = sum(final_tax_to_tif) + sum(final_tax_to_dist), + Type = "No exemptions" + ) %>% + select(Year = year, Type, "Exemption Amt." = exe, "Bill Amt." = bill_total) +``` + +Finally, we can compare the real bills (with exemptions) to the counterfactual bills we just created (without exemptions). + +
+ +Click here to show plot code + +```{r, echo=FALSE} +bills_plot_2 <- rbind(bills_w_exe_summ, bills_no_exe_summ) %>% + ggplot() + + geom_line(aes(x = Year, y = `Bill Amt.`, linetype = Type), linewidth = 1.1) + + scale_x_continuous(n.breaks = 9) + + scale_y_continuous(labels = scales::label_dollar(), limits = c(0, 6500)) + + scale_linetype_manual( + name = "", + values = c("With exemptions" = "solid", "No exemptions" = "dashed") + ) + + theme_minimal() + + theme( + axis.title = element_text(size = 13), + axis.title.x = element_text(margin = margin(t = 6)), + axis.title.y = element_text(margin = margin(r = 6)), + axis.text = element_text(size = 11), + strip.text = element_text(size = 16), + strip.background = element_rect(fill = "#c9c9c9"), + legend.title = element_text(size = 14), + legend.key.size = unit(24, "points"), + legend.text = element_text(size = 12), + legend.position = "bottom" + ) +``` + +
+ +
+ +```{r, echo=FALSE, out.width="100%"} +bills_plot_2 +``` + +The exemption amount for this PIN has increased in tandem with increases in the local tax rate. There were also a statutory increases in the amount of the Homeowner Exemption during the same time period. + +## Changing exemptions + +We can also use PTAXSIM to answer hypotheticals. For example, how would this PIN's bill history change if the Homeowner Exemption increased from \$10,000 to \$15,000 in 2018? + +To find out, we again create a modified PIN input to pass to `tax_bill()`. This time, we increase the Homeowner Exemption to \$15,000 for all years after 2018. + +```{r} +exe_dt_2 <- lookup_pin(2006:2020, "25321140050000") %>% + mutate(exe_homeowner = ifelse(year >= 2018, 15000, exe_homeowner)) %>% + setDT(key = c("year", "pin")) +``` + +Then, we recalculate all the bills with the new PIN input and do the same aggregation as before. + +```{r} +bills_new_exe <- tax_bill( + 2006:2020, + "25321140050000", + pin_dt = exe_dt_2, + simplify = FALSE +) + +bills_new_exe_summ <- bills_new_exe %>% + group_by(year) %>% + summarize( + exe = sum(tax_amt_exe), + bill_total = sum(final_tax_to_tif) + sum(final_tax_to_dist), + Type = "Changed exemption" + ) %>% + select(Year = year, Type, "Exemption Amt." = exe, "Bill Amt." = bill_total) +``` + +Finally, we add a third line to our plot showing the total tax bill by year after the hypothetical exemption increase in 2018. + +
+ +Click here to show plot code + +```{r} +bills_plot_3 <- rbind( + bills_w_exe_summ, + bills_no_exe_summ, + bills_new_exe_summ +) %>% + ggplot() + + geom_line(aes(x = Year, y = `Bill Amt.`, linetype = Type), linewidth = 1.1) + + scale_x_continuous(n.breaks = 9) + + scale_y_continuous(labels = scales::label_dollar(), limits = c(0, 6500)) + + scale_linetype_manual( + name = "", + values = c( + "With exemptions" = "solid", + "No exemptions" = "dashed", + "Changed exemption" = "dotted" + ) + ) + + theme_minimal() + + theme( + axis.title = element_text(size = 13), + axis.title.x = element_text(margin = margin(t = 6)), + axis.title.y = element_text(margin = margin(r = 6)), + axis.text = element_text(size = 11), + strip.text = element_text(size = 16), + strip.background = element_rect(fill = "#c9c9c9"), + legend.title = element_text(size = 14), + legend.key.size = unit(24, "points"), + legend.text = element_text(size = 12), + legend.position = "bottom" + ) +``` + +
+ +
+ +```{r, echo=FALSE, out.width="100%"} +bills_plot_3 +``` + +Increasing the Homeowner Exemption to \$15,000 would save this property owner around \$1,000 per year in taxes. However, this hypothetical does not account for changes in the tax base that would occur if overall exemption amounts changed, so it is (slightly) inaccurate. + +# Many PINs + +PTAXSIM can also perform more complex analysis, such as measuring the impact of exemptions in a given area. To perform this analysis, we can again use the `tax_bill()` function to calculate tax bills before and after exemptions are removed, this time for many PINs. + +## Removing exemptions + +Let's look at the overall effect of exemptions in the Cook County township of Calumet, shown in [red]{style="color:#e41a1c"} below. + +![](../man/figures/exemptions-map.png) + +First, we can use the PTAXSIM database to get a list of all the unique PINs in Calumet township. We can also create a vector of years we're interested in. + +```{r} +t_pins <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT DISTINCT pin + FROM pin + WHERE substr(tax_code_num, 1, 2) = '14' + " +) +t_pins <- t_pins$pin +t_years <- 2006:2020 +``` + +Next, we can generate bills for all PINs in Calumet for the past 15 years. These bills will *include* any exemptions they actually received. + +We're using `data.table` syntax here because it's much faster than `dplyr` when working with large data. Note that PTAXSIM functions always output a `data.table` with keys. + +```{r} +t_bills_w_exe <- tax_bill(t_years, t_pins)[, stage := "With exemptions"] +``` + +Unlike a single PIN, removing exemptions from many PINs means that the base (the amount of total taxable value available) will change substantially. In order to accurately model the effect of removing exemptions, we need to fully recalculate the base of each district by adding the sum of taxable value recovered from each PIN. + +To start, we use the `lookup_pin()` function to recover the total EAV of exemptions for each PIN. + +```{r} +t_pin_dt_no_exe <- lookup_pin(t_years, t_pins) +t_pin_dt_no_exe[, tax_code := lookup_tax_code(year, pin)] + +exe_cols <- names(t_pin_dt_no_exe)[startsWith(names(t_pin_dt_no_exe), "exe_")] +t_tc_sum_no_exe <- t_pin_dt_no_exe[, + .(exe_total = sum(rowSums(.SD))), + .SDcols = exe_cols, + by = .(year, tax_code) +] +``` + +Next, we recalculate the base of all taxing districts in Calumet by adding the EAV returned from exemptions to each district's total EAV. + +```{r} +t_agency_dt_no_exe <- lookup_agency(t_years, t_pin_dt_no_exe$tax_code) +t_agency_dt_no_exe[ + t_tc_sum_no_exe, + on = .(year, tax_code), + agency_total_eav := agency_total_eav + exe_total +] +``` + +Then, we again alter the `pin_dt` input by setting all exemption columns equal to zero. + +```{r} +t_pin_dt_no_exe[, (exe_cols) := 0][, c("tax_code") := NULL] +``` + +We recalculate all Calumet tax bills *without* exemptions and with an updated tax base for each district (passed via `agency_dt`). + +```{r} +t_bills_no_exe <- tax_bill( + year_vec = t_years, + pin_vec = t_pins, + agency_dt = t_agency_dt_no_exe, + pin_dt = t_pin_dt_no_exe +)[ + , stage := "No exemptions" +] +``` + +To see the results, we can calculate the average tax bill by year by property type (residential or commercial), with and without exemptions. We can also index the result to the earliest year available (in this case 2006) to make the different property types comparable on the same scale. + +```{r} +# Little function to get the statistical mode +Mode <- function(x) { + ux <- unique(x) + ux[which.max(tabulate(match(x, ux)))] +} + +t_no_exe_summ <- rbind(t_bills_w_exe, t_bills_no_exe)[ + , class := Mode(substr(class, 1, 1)), + by = pin +][ + class %in% c("2", "3", "5"), +][ + , class := ifelse(class == "2", "Residential", "Commercial") +][ + , .(total_bill = sum(final_tax)), + by = .(year, pin, class, stage) +][ + , .(avg_bill = mean(total_bill)), + by = .(year, class, stage) +][ + , idx_bill := (avg_bill / avg_bill[year == 2006]) * 100, + by = .(class, stage) +] +``` + +Finally, we can plot the average bill with and without exemptions by property type. + +
+ +Click here to show plot code + +```{r} +t_annot <- tibble( + class = c("Residential", "Commercial"), + x = c(2008, 2006.4), + y = c(105, 115) +) + +# Plot the change in indexed values over time +t_no_exe_summ_plot <- ggplot(data = t_no_exe_summ) + + geom_line( + aes(x = year, y = idx_bill, color = class, linetype = stage), + linewidth = 1.1 + ) + + geom_text( + data = t_annot, + aes(x = x, y = y, color = class, label = class), + hjust = 0 + ) + + scale_y_continuous(name = "Average Tax Bill, Indexed to 2006") + + scale_x_continuous(name = "Year", n.breaks = 10, limits = c(2006, 2020.4)) + + scale_linetype_manual( + name = "", + values = c("With exemptions" = "solid", "No exemptions" = "dashed") + ) + + scale_color_brewer(name = "", palette = "Set1", direction = -1) + + guides(color = "none") + + facet_wrap(vars(class)) + + theme_minimal() + + theme( + axis.title = element_text(size = 13), + axis.title.x = element_text(margin = margin(t = 6)), + axis.title.y = element_text(margin = margin(r = 6)), + axis.text.y = element_text(size = 11), + strip.text = element_text(size = 16), + strip.background = element_rect(fill = "#c9c9c9"), + legend.title = element_text(size = 14), + legend.key.size = unit(24, "points"), + legend.text = element_text(size = 12), + legend.position = "bottom" + ) +``` + +
+ +
+ +```{r, echo=FALSE, out.width="100%"} +t_no_exe_summ_plot +``` + +Exemptions in Calumet have significantly increased in both volume and amount (via increased tax rates) in recent years. In 2019, the average residential homeowner saved around \$1,100 via exemptions. + +Conversely, Calumet's commercial property owners have picked up an increasingly large share of the overall tax burden since 2006. In 2019, the average commercial property paid about \$1,100 more than they would have if exemptions did not exist. + +## Changing exemptions + +PTAXSIM can also answer hypotheticals about large areas. For example, how would the average residential tax bill in Calumet change if the Senior Exemption increased by \$5,000 and the Senior Freeze Exemption was removed? + +To find out, we again create a PIN input with modified exemption amounts, then recalculate the base by taking the difference between the real and hypothetical exemptions. + +```{r} +t_pin_dt_new_exe <- lookup_pin(t_years, t_pins) +t_pin_dt_new_exe[, tax_code := lookup_tax_code(year, pin)] + +t_tc_sum_new_exe <- t_pin_dt_new_exe[ + , .(exe_total = sum(exe_freeze - (5000 * (exe_senior != 0)))), + by = .(year, tax_code) +] +``` + +Next, we recalculate the base of each district. This time, the base may *lose* some EAV, since the Senior Exemption is increasing substantially. + +```{r} +t_agency_dt_new_exe <- lookup_agency(t_years, t_pin_dt_new_exe$tax_code) +t_agency_dt_new_exe[ + t_tc_sum_new_exe, + on = .(year, tax_code), + agency_total_eav := agency_total_eav + exe_total +] +``` + +Then, we again alter the `pin_dt` input by setting the Senior Freeze Exemption to zero and adding \$5,000 to any Senior Exemption. + +```{r} +t_pin_dt_new_exe <- t_pin_dt_new_exe[ + , exe_freeze := 0 +][ + exe_senior != 0, exe_senior := exe_senior + 5000 +][ + , c("tax_code") := NULL +] +``` + +We again recalculate all Calumet tax bills with our updated exemptions and with an updated tax base for each district. + +```{r} +t_bills_new_exe <- tax_bill( + year_vec = t_years, + pin_vec = t_pins, + agency_dt = t_agency_dt_new_exe, + pin_dt = t_pin_dt_new_exe +)[ + , stage := "Changed exemptions" +] +``` + +Then, do the same aggregation and indexing we did previously, this time using the updated bills. + +```{r} +t_new_exe_summ <- rbind(t_bills_w_exe, t_bills_new_exe)[ + , class := Mode(substr(class, 1, 1)), + by = pin +][ + class %in% c("2", "3", "5"), +][ + , class := ifelse(class == "2", "Residential", "Commercial") +][ + , .(total_bill = sum(final_tax)), + by = .(year, pin, class, stage) +][ + , .(avg_bill = mean(total_bill)), + by = .(year, class, stage) +][ + , idx_bill := (avg_bill / avg_bill[year == 2006]) * 100, + by = .(class, stage) +] +``` + +Finally, we can plot the original bills against the updated ones. + +
+ +Click here to show plot code + +```{r} +t_new_exe_summ_plot <- ggplot(data = t_new_exe_summ) + + geom_line( + aes(x = year, y = idx_bill, color = class, linetype = stage), + linewidth = 1.1 + ) + + geom_text( + data = t_annot, + aes(x = x, y = y, color = class, label = class), + hjust = 0 + ) + + scale_y_continuous(name = "Average Tax Bill, Indexed to 2006") + + scale_x_continuous(name = "Year", n.breaks = 10, limits = c(2006, 2020.4)) + + scale_linetype_manual( + name = "", + values = c("With exemptions" = "solid", "Changed exemptions" = "dotted") + ) + + scale_color_brewer(name = "", palette = "Set1", direction = -1) + + guides(color = "none") + + facet_wrap(vars(class)) + + theme_minimal() + + theme( + axis.title = element_text(size = 13), + axis.title.x = element_text(margin = margin(t = 6)), + axis.title.y = element_text(margin = margin(r = 6)), + axis.text.y = element_text(size = 11), + strip.text = element_text(size = 16), + strip.background = element_rect(fill = "#c9c9c9"), + legend.title = element_text(size = 14), + legend.key.size = unit(24, "points"), + legend.text = element_text(size = 12), + legend.position = "bottom" + ) +``` + +
+ +
+ +```{r, out.width="100%", echo=FALSE} +t_new_exe_summ_plot +``` + +The net effect of increasing the Senior Exemption while removing the Senior Freeze Exemption is a slight decrease in the *average* bill. However, this conclusion is ambiguous, complicated by the fact that the Senior Freeze is means-tested, while the Senior Exemption is not. The "real-world effect" of our hypothetical policy change would most likely be an increase in the property tax bills of poorer seniors, even though the average bill decreased. + +Ultimately, with some careful coding and assumptions, PTAXSIM (and its included data) can be used to test almost any hypothetical change in exemptions. From 9a7efb87850d77c9671f3483e6345c9440606b3a Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Tue, 7 Apr 2026 20:56:15 +0000 Subject: [PATCH 02/18] New plots, remove exemption vignette text, add narrative --- vignettes/agencies.Rmd | 583 ++++++++++++++--------------------------- 1 file changed, 203 insertions(+), 380 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index d86ba74..cb55cd4 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -28,12 +28,14 @@ First, load some useful libraries and instantiate a PTAXSIM DBI connection with ```{r} library(data.table) library(dplyr) +library(DT) library(here) library(ggplot2) #library(ptaxsim) +library(tidyr) devtools::load_all() -ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.db")) +ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2.db")) ``` ```{r, echo=FALSE} @@ -56,12 +58,9 @@ chi_agency_nums <- DBI::dbGetQuery( " ) -chi_agency_nums +head(chi_agency_nums) ``` - -Many taxing agencies pop up, most of which are Special Service Areas, or SSAs. SSAs typically overlay a commercial corridor where property owners will pay an additional tax to fund the SSA's additional serivces which can include maintenance, beautification and other additional services. - -For purpose of our analysis, we'll just focus on `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. +For purpose of our analysis, we'll just focus on `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. While the `CITY OF CHICAGO LIBRARY FUND` has been treated as its own taxing agency in the Clerk's data, the City itself reports the amount levied for the purpose of funding the Chicago Public Library as a portion of its entire property tax levy. First, we'll query all fields from the `agencies` table. @@ -77,12 +76,16 @@ chi_agencies <- DBI::dbGetQuery( ) ``` -When examining `chi_agencies`, we see that in 2024 the `CITY OF CHICAGO LIBRARY FUND` has a \$0 levy and therefore a tax rate of 0. This is because, starting in 2024, the Cook County Clerk began reporting the requested levy for the City of Chicago libraries as a fund under the `CITY OF CHICAGO` taxing agency. This is in fact aligned with how the City of Chicago reports its property tax levy in its own budget [documentation](https://chicityclerk.s3.us-west-2.amazonaws.com/s3fs-public/O2023-0005291_Tax_Levy.pdf). +## Accounting for 2024 changes to agency fund reporting + +When examining `chi_agencies`, we see that in 2024 the `CITY OF CHICAGO LIBRARY FUND` has a \$0 levy and therefore a tax rate of 0. This is because, starting in 2024, the Cook County Clerk began reporting the requested levy for the City of Chicago libraries as a fund under the `CITY OF CHICAGO` taxing agency. As mentioned above, this is in fact aligned with how the City of Chicago reports its property tax levy in its own budget [documentation](https://chicityclerk.s3.us-west-2.amazonaws.com/s3fs-public/O2023-0005291_Tax_Levy.pdf). This reporting change occurred for many municipal taxing agencies, where the Clerk had previously reported municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agencies data over time. To make this process easier, we have added new fields to the `agency_info` table in the PTAXSIM database which identify the agencies that have been folded into their parent agencies. `agency_num_24` and `agency_name_24` contain the agency info that the old "sub-agency" has been merged into. Note that the user can still see details about these former agencies, now funds, by querying the `agency_fund` table. +The table below shows all of the taxing agencies that have been been folded into their parent agencies beginning with tax year 2024. In all, we have 78 former taxing agencies that are no longer reported as individual agencies beginning in 2024. + ```{r} # Query agency_info table for all agencies with the 2024 update agency_cw_24 <- DBI::dbGetQuery( @@ -95,11 +98,12 @@ agency_cw_24 <- DBI::dbGetQuery( ) %>% select(agency_num, agency_name, agency_num_24, agency_name_24) -agency_cw_24 +datatable(agency_cw_24) + ``` We can now use `agency_cw_24` as a crosswalk to convert these sub-agencies to their parent agency numbers. -Knowing this caveat about 2024 agency data, we'll query the both `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND` for all years, and then convert them to have the same `agency_num` using `agency_cw_24` so we can the levy data into one summed amount per year. This will ensure consistent reporting across the time series. +Knowing this caveat about 2024 agency data, we'll query the both `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND` for all years, and then update the `agency_num` field for `CITY OF CHICAGO LIBRARY FUND` using `agency_cw_24`. This will ensure consistent reporting across all years. ```{r} chi_agencies <- chi_agencies %>% @@ -115,9 +119,11 @@ chi_agencies <- chi_agencies %>% ) ``` +## Tracking City of Chicago's levy growth with inflation + Now that we have the correct agency numbers for all years, we can select and aggregate the fields of interest for `CITY OF CHICAGO`. -Many of the fields in the `agencies` table, which come from the Clerk's agency rate report, relate to PTELL calculations. Because the Chicago is a home rule municipality, it is not subject to PTELL limits as imposed by the State of Illinois, so these fields will not contain relevant info. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror those of PTELL which we'll revisit later. +Many of the fields in the `agencies` table, which come from the Clerk's agency rate report, relate to PTELL calculations. Because the Chicago is a home rule municipality, it is not subject to PTELL (Property Tax Extension Law Limit) as imposed by the State of Illinois, so these fields will not contain relevant info. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's limits which prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. ```{r} chi_levy <- @@ -131,102 +137,68 @@ chi_levy <- prior_eav = first(prior_eav), curr_new_prop = first(curr_new_prop) ) - - -chi_levy %>% - pivot(cols = c(total_final_levy, total_final_rate, total_ext)) - -``` - - -```{r, echo=FALSE} -chi_levy_plot_1 <- chi_levy %>% - ggplot() + - geom_line(aes(x = year, y = total_final_levy), linewidth = .8) + - scale_x_continuous(n.breaks = 10) + - scale_y_continuous(labels = scales::label_currency(), limits = c(0, 2000000000)) + - theme_minimal() + - theme( - axis.title = element_text(size = 13), - axis.title.x = element_text(margin = margin(t = 6)), - axis.title.y = element_text(margin = margin(r = 6)), - axis.text = element_text(size = 11), - strip.text = element_text(size = 16), - strip.background = element_rect(fill = "#c9c9c9"), - legend.title = element_text(size = 14), - legend.key.size = unit(24, "points"), - legend.text = element_text(size = 12), - legend.position = "bottom" - ) - -plotly::plot_ly(chi_levy, x = ~year, - y = ~total_final_levy, - type = 'scatter') - - - ``` +PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to the inflation rate since 2006. - - -
- -```{r, echo=FALSE, out.width="100%"} -chi_levy_plot_1 -``` - -This PIN's total tax bill has increased slightly since 2006, with some dips in the last few years. Let's see how much it would've increased *without* its Homeowner Exemption. - -## Removing exemptions - -We can remove exemptions by modifying the inputs to the `tax_bill()` function and then recalculating each bill. - -First, we retrieve the `pin_dt` input to `tax_bill()` by using the `lookup_pin()` function. This input contains all of the exemptions, AV, and EAV information for each PIN. By default, it has the actual historical values, but we can modify it to produce counterfactual bills. In this case, we can remove all exemptions by setting the amount in each exemption column (prefixed with `exe_`) to zero. +To do this we can pull CPI data from the PTAXSIM database - we include the CPI tables published annually by the Illinois Department of Revenue (IDOR) for the purpose of setting the PTELL limit. ```{r} -exe_dt <- lookup_pin(2006:2020, "25321140050000") %>% - mutate(across(starts_with("exe_"), ~0)) %>% - setDT(key = c("year", "pin")) +cpi <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT + levy_year as year, + cpi, + ptell_cook + FROM cpi + " +) %>% + filter(year >= 2006) ``` -Then, we recalculate each bill using the new, zeroed-out `pin_dt`. +First, let's convert each year's levy to be in terms of 2006 dollars using the consumer price index (CPI). This will help us understand the degree to which the levy has been adjusted on account of inflation vs. increasing revenue. ```{r} -bills_no_exe <- tax_bill(2006:2020, - "25321140050000", - pin_dt = exe_dt, - simplify = FALSE -) -``` +cpi <- cpi %>% + mutate(multiplier = cpi/cpi[year == 2006]) -Next, we do the same aggregation that we did for bills *with* exemptions, collapsing each bill into a total by year. +chi_levy_adj <- chi_levy %>% + left_join(cpi, "year") %>% + mutate( + total_final_levy_adj = total_final_levy / multiplier + ) -```{r} -bills_no_exe_summ <- bills_no_exe %>% - group_by(year) %>% - summarize( - exe = sum(tax_amt_exe), - bill_total = sum(final_tax_to_tif) + sum(final_tax_to_dist), - Type = "No exemptions" - ) %>% - select(Year = year, Type, "Exemption Amt." = exe, "Bill Amt." = bill_total) ``` -Finally, we can compare the real bills (with exemptions) to the counterfactual bills we just created (without exemptions). +Plotting Chicago's total property tax levy from 2006 to 2024 in both 2006 dollars and nominal dollars, we can see that the levy remained steady from 2006 to 2014. The levy actually decreased slightly in real dollars given the levy was not increased to account for inflation during this time. Then in 2015, City begins increasing its levy significantly due to State legislation that dictates [funding requirements for Chicago's Fire and Police pension funds](https://www.civicfed.org/civic-federation/blog/chicago-police-and-fire-pension-funding-changes-become-law). + +Rising inflation beginning in the wake of the Covid-19 pandemic is evident with the levy in "real" dollars decreasing slightly from 2020 to 2024 even as the levy in nominal dollars increased in that time period.
Click here to show plot code - -```{r, echo=FALSE} -bills_plot_2 <- rbind(bills_w_exe_summ, bills_no_exe_summ) %>% +```{r} +chi_levy_plot_1 <- chi_levy_adj %>% ggplot() + - geom_line(aes(x = Year, y = `Bill Amt.`, linetype = Type), linewidth = 1.1) + - scale_x_continuous(n.breaks = 9) + - scale_y_continuous(labels = scales::label_dollar(), limits = c(0, 6500)) + - scale_linetype_manual( - name = "", - values = c("With exemptions" = "solid", "No exemptions" = "dashed") + geom_line(aes(x = year, y = total_final_levy, color = "Levy in nominal dollars"), + linewidth = .8) + + geom_point(aes(x = year, y = total_final_levy, color = "Levy in nominal dollars")) + + geom_line(aes(x = year, y = total_final_levy_adj, color = "Levy in 2006 dollars"), + linewidth = .8, linetype = "dashed") + + geom_point(aes(x = year, y = total_final_levy_adj, color = "Levy in 2006 dollars")) + + scale_color_manual( + name = NULL, + values = c("Levy in nominal dollars" = "black", + "Levy in 2006 dollars" = "blue") + ) + + scale_x_continuous(n.breaks = 10) + + scale_y_continuous( + labels = scales::label_dollar(scale = 1e-9, suffix = "B"), + limits = c(0, 2000000000) + ) + + labs( + x = NULL, + y = "Total Final Levy (Billions)" ) + theme_minimal() + theme( @@ -241,6 +213,7 @@ bills_plot_2 <- rbind(bills_w_exe_summ, bills_no_exe_summ) %>% legend.text = element_text(size = 12), legend.position = "bottom" ) + ```
@@ -248,71 +221,48 @@ bills_plot_2 <- rbind(bills_w_exe_summ, bills_no_exe_summ) %>%
```{r, echo=FALSE, out.width="100%"} -bills_plot_2 -``` - -The exemption amount for this PIN has increased in tandem with increases in the local tax rate. There were also a statutory increases in the amount of the Homeowner Exemption during the same time period. - -## Changing exemptions - -We can also use PTAXSIM to answer hypotheticals. For example, how would this PIN's bill history change if the Homeowner Exemption increased from \$10,000 to \$15,000 in 2018? - -To find out, we again create a modified PIN input to pass to `tax_bill()`. This time, we increase the Homeowner Exemption to \$15,000 for all years after 2018. - -```{r} -exe_dt_2 <- lookup_pin(2006:2020, "25321140050000") %>% - mutate(exe_homeowner = ifelse(year >= 2018, 15000, exe_homeowner)) %>% - setDT(key = c("year", "pin")) +chi_levy_plot_1 ``` - -Then, we recalculate all the bills with the new PIN input and do the same aggregation as before. +Next, we'll take a look at how the percent change in the levy compares to the percent change in CPI - or the rate of inflation. We add some data manipulation to calculate the percent change in both the levy and CPI by each year since 2006. ```{r} -bills_new_exe <- tax_bill( - 2006:2020, - "25321140050000", - pin_dt = exe_dt_2, - simplify = FALSE -) - -bills_new_exe_summ <- bills_new_exe %>% - group_by(year) %>% - summarize( - exe = sum(tax_amt_exe), - bill_total = sum(final_tax_to_tif) + sum(final_tax_to_dist), - Type = "Changed exemption" - ) %>% - select(Year = year, Type, "Exemption Amt." = exe, "Bill Amt." = bill_total) +chi_levy_adj <- chi_levy_adj %>% + ungroup() %>% + mutate( + pct_change_final_levy = (total_ext - total_ext[year == 2006]) / total_ext[year == 2006] * 100, + pct_change_ptell = (1 * c(1, cumprod(1 + tail(ptell_cook, -1))) - 1) * 100, + ) ``` -Finally, we add a third line to our plot showing the total tax bill by year after the hypothetical exemption increase in 2018. +Plotting the percent change of the City's levy since 2006, we see it has outgrown the cumulative effects of the City's property tax limitation
Click here to show plot code - -```{r} -bills_plot_3 <- rbind( - bills_w_exe_summ, - bills_no_exe_summ, - bills_new_exe_summ -) %>% +```{r, echo=FALSE, out.width="100%"} +chi_levy_plot_2 <- chi_levy_adj %>% + pivot_longer( + cols = c(pct_change_final_levy, pct_change_ptell), + names_to = "series", + values_to = "pct_change" + ) %>% + mutate(series = recode(series, + "pct_change_final_levy" = "Levy % change (nominal $)", + "pct_change_ptell" = "PTELL % change" + )) %>% ggplot() + - geom_line(aes(x = Year, y = `Bill Amt.`, linetype = Type), linewidth = 1.1) + - scale_x_continuous(n.breaks = 9) + - scale_y_continuous(labels = scales::label_dollar(), limits = c(0, 6500)) + - scale_linetype_manual( - name = "", - values = c( - "With exemptions" = "solid", - "No exemptions" = "dashed", - "Changed exemption" = "dotted" - ) + geom_line(aes(x = year, y = pct_change, color = series), linewidth = .8) + + geom_point(aes(x = year, y = pct_change, color = series)) + + scale_x_continuous(n.breaks = 10) + + scale_y_continuous(labels = scales::label_percent(scale = 1, suffix = "%")) + + labs( + x = NULL, + y = "Percent Change Since Base Year", + color = NULL ) + theme_minimal() + theme( axis.title = element_text(size = 13), - axis.title.x = element_text(margin = margin(t = 6)), axis.title.y = element_text(margin = margin(r = 6)), axis.text = element_text(size = 11), strip.text = element_text(size = 16), @@ -323,163 +273,81 @@ bills_plot_3 <- rbind( legend.position = "bottom" ) ``` -

```{r, echo=FALSE, out.width="100%"} -bills_plot_3 +chi_levy_plot_2 ``` -Increasing the Homeowner Exemption to \$15,000 would save this property owner around \$1,000 per year in taxes. However, this hypothetical does not account for changes in the tax base that would occur if overall exemption amounts changed, so it is (slightly) inaccurate. - -# Many PINs - -PTAXSIM can also perform more complex analysis, such as measuring the impact of exemptions in a given area. To perform this analysis, we can again use the `tax_bill()` function to calculate tax bills before and after exemptions are removed, this time for many PINs. - -## Removing exemptions - -Let's look at the overall effect of exemptions in the Cook County township of Calumet, shown in [red]{style="color:#e41a1c"} below. - -![](../man/figures/exemptions-map.png) - -First, we can use the PTAXSIM database to get a list of all the unique PINs in Calumet township. We can also create a vector of years we're interested in. ```{r} -t_pins <- DBI::dbGetQuery( +chi_agency_fund <- DBI::dbGetQuery( ptaxsim_db_conn, " - SELECT DISTINCT pin - FROM pin - WHERE substr(tax_code_num, 1, 2) = '14' + SELECT * + FROM agency_fund + WHERE agency_num = '030210000' + OR agency_num = '030210001' " ) -t_pins <- t_pins$pin -t_years <- 2006:2020 -``` - -Next, we can generate bills for all PINs in Calumet for the past 15 years. These bills will *include* any exemptions they actually received. - -We're using `data.table` syntax here because it's much faster than `dplyr` when working with large data. Note that PTAXSIM functions always output a `data.table` with keys. - -```{r} -t_bills_w_exe <- tax_bill(t_years, t_pins)[, stage := "With exemptions"] -``` - -Unlike a single PIN, removing exemptions from many PINs means that the base (the amount of total taxable value available) will change substantially. In order to accurately model the effect of removing exemptions, we need to fully recalculate the base of each district by adding the sum of taxable value recovered from each PIN. -To start, we use the `lookup_pin()` function to recover the total EAV of exemptions for each PIN. - -```{r} -t_pin_dt_no_exe <- lookup_pin(t_years, t_pins) -t_pin_dt_no_exe[, tax_code := lookup_tax_code(year, pin)] - -exe_cols <- names(t_pin_dt_no_exe)[startsWith(names(t_pin_dt_no_exe), "exe_")] -t_tc_sum_no_exe <- t_pin_dt_no_exe[, - .(exe_total = sum(rowSums(.SD))), - .SDcols = exe_cols, - by = .(year, tax_code) -] -``` - -Next, we recalculate the base of all taxing districts in Calumet by adding the EAV returned from exemptions to each district's total EAV. - -```{r} -t_agency_dt_no_exe <- lookup_agency(t_years, t_pin_dt_no_exe$tax_code) -t_agency_dt_no_exe[ - t_tc_sum_no_exe, - on = .(year, tax_code), - agency_total_eav := agency_total_eav + exe_total -] -``` - -Then, we again alter the `pin_dt` input by setting all exemption columns equal to zero. - -```{r} -t_pin_dt_no_exe[, (exe_cols) := 0][, c("tax_code") := NULL] -``` - -We recalculate all Calumet tax bills *without* exemptions and with an updated tax base for each district (passed via `agency_dt`). - -```{r} -t_bills_no_exe <- tax_bill( - year_vec = t_years, - pin_vec = t_pins, - agency_dt = t_agency_dt_no_exe, - pin_dt = t_pin_dt_no_exe -)[ - , stage := "No exemptions" -] -``` +chi_agency_fund_info <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT * + FROM agency_fund_info + WHERE agency_num = '030210000' + OR agency_num = '030210001' + " +) -To see the results, we can calculate the average tax bill by year by property type (residential or commercial), with and without exemptions. We can also index the result to the earliest year available (in this case 2006) to make the different property types comparable on the same scale. +chi_agency_fund <- chi_agency_fund %>% + left_join(chi_agency_fund_info) %>% + filter(final_levy > 0, + fund_type_num != "319") %>% + mutate( + fund_catg = + case_when(grepl("A & B", fund_name) ~ "Pension", + grepl("NOTE REDEMPTION & INTEREST FUND", fund_name) ~ + "Bond Payments", + grepl("BONDS & INTEREST", fund_name) ~ "Bond Payments", + grepl("LIBRARY NOTE REDEMPTION", fund_name) ~ "Library Fund", + grepl("BONDS & INTEREST", fund_type_name) ~ "Bond Payments", + grepl("LIBRARY NOTE REDEMPTION", fund_type_name) ~ "Library Fund", + TRUE ~ fund_type_name) + ) %>% + group_by(year, fund_catg) %>% + summarise(final_levy = sum(final_levy), + final_rate = sum(final_rate)) -```{r} -# Little function to get the statistical mode -Mode <- function(x) { - ux <- unique(x) - ux[which.max(tabulate(match(x, ux)))] -} - -t_no_exe_summ <- rbind(t_bills_w_exe, t_bills_no_exe)[ - , class := Mode(substr(class, 1, 1)), - by = pin -][ - class %in% c("2", "3", "5"), -][ - , class := ifelse(class == "2", "Residential", "Commercial") -][ - , .(total_bill = sum(final_tax)), - by = .(year, pin, class, stage) -][ - , .(avg_bill = mean(total_bill)), - by = .(year, class, stage) -][ - , idx_bill := (avg_bill / avg_bill[year == 2006]) * 100, - by = .(class, stage) -] ``` -Finally, we can plot the average bill with and without exemptions by property type. -
Click here to show plot code - ```{r} -t_annot <- tibble( - class = c("Residential", "Commercial"), - x = c(2008, 2006.4), - y = c(105, 115) -) - -# Plot the change in indexed values over time -t_no_exe_summ_plot <- ggplot(data = t_no_exe_summ) + - geom_line( - aes(x = year, y = idx_bill, color = class, linetype = stage), - linewidth = 1.1 - ) + - geom_text( - data = t_annot, - aes(x = x, y = y, color = class, label = class), - hjust = 0 +chi_levy_plot_3 <- chi_agency_fund %>% + ggplot() + + geom_line(aes(x = year, y = final_levy, color = fund_catg), + linewidth = .8) + + geom_point(aes(x = year, y = final_levy, color = fund_catg)) + + scale_x_continuous(n.breaks = 10) + + scale_y_continuous( + labels = scales::label_dollar(scale = 1e-9, suffix = "B"), + limits = c(0, NA) ) + - scale_y_continuous(name = "Average Tax Bill, Indexed to 2006") + - scale_x_continuous(name = "Year", n.breaks = 10, limits = c(2006, 2020.4)) + - scale_linetype_manual( - name = "", - values = c("With exemptions" = "solid", "No exemptions" = "dashed") + labs( + x = NULL, + y = "Final Levy (Billions)", + color = NULL ) + - scale_color_brewer(name = "", palette = "Set1", direction = -1) + - guides(color = "none") + - facet_wrap(vars(class)) + theme_minimal() + theme( axis.title = element_text(size = 13), - axis.title.x = element_text(margin = margin(t = 6)), axis.title.y = element_text(margin = margin(r = 6)), - axis.text.y = element_text(size = 11), + axis.text = element_text(size = 11), strip.text = element_text(size = 16), strip.background = element_rect(fill = "#c9c9c9"), legend.title = element_text(size = 14), @@ -488,125 +356,87 @@ t_no_exe_summ_plot <- ggplot(data = t_no_exe_summ) + legend.position = "bottom" ) ``` -

```{r, echo=FALSE, out.width="100%"} -t_no_exe_summ_plot -``` - -Exemptions in Calumet have significantly increased in both volume and amount (via increased tax rates) in recent years. In 2019, the average residential homeowner saved around \$1,100 via exemptions. - -Conversely, Calumet's commercial property owners have picked up an increasingly large share of the overall tax burden since 2006. In 2019, the average commercial property paid about \$1,100 more than they would have if exemptions did not exist. - -## Changing exemptions - -PTAXSIM can also answer hypotheticals about large areas. For example, how would the average residential tax bill in Calumet change if the Senior Exemption increased by \$5,000 and the Senior Freeze Exemption was removed? - -To find out, we again create a PIN input with modified exemption amounts, then recalculate the base by taking the difference between the real and hypothetical exemptions. - -```{r} -t_pin_dt_new_exe <- lookup_pin(t_years, t_pins) -t_pin_dt_new_exe[, tax_code := lookup_tax_code(year, pin)] - -t_tc_sum_new_exe <- t_pin_dt_new_exe[ - , .(exe_total = sum(exe_freeze - (5000 * (exe_senior != 0)))), - by = .(year, tax_code) -] +chi_levy_plot_3 ``` -Next, we recalculate the base of each district. This time, the base may *lose* some EAV, since the Senior Exemption is increasing substantially. - ```{r} -t_agency_dt_new_exe <- lookup_agency(t_years, t_pin_dt_new_exe$tax_code) -t_agency_dt_new_exe[ - t_tc_sum_new_exe, - on = .(year, tax_code), - agency_total_eav := agency_total_eav + exe_total -] -``` - -Then, we again alter the `pin_dt` input by setting the Senior Freeze Exemption to zero and adding \$5,000 to any Senior Exemption. - -```{r} -t_pin_dt_new_exe <- t_pin_dt_new_exe[ - , exe_freeze := 0 -][ - exe_senior != 0, exe_senior := exe_senior + 5000 -][ - , c("tax_code") := NULL -] -``` - -We again recalculate all Calumet tax bills with our updated exemptions and with an updated tax base for each district. - -```{r} -t_bills_new_exe <- tax_bill( - year_vec = t_years, - pin_vec = t_pins, - agency_dt = t_agency_dt_new_exe, - pin_dt = t_pin_dt_new_exe -)[ - , stage := "Changed exemptions" -] +fund_type_table <- chi_agency_fund %>% + select(-final_rate) %>% + filter(year > 2019) %>% + pivot_wider(names_from = year, values_from = final_levy) + +fund_type_table %>% + arrange(desc(`2024`)) %>% + bind_rows( + summarise(., fund_catg = "Total Levy", across(c("2020", "2021", "2022", "2023", "2024"), sum)) + ) %>% + rename(`Fund Type` = fund_catg) %>% + datatable( + caption = "Chicago Property Tax Levies by Fund Type", + rownames = FALSE, + options = list( + pageLength = nrow(fund_type_table) + 1, + dom = "t", + columnDefs = list( + list(className = "dt-left", targets = 0), + list(className = "dt-right", targets = 1:5) + ) + ) + ) %>% + formatCurrency( + columns = c("2020", "2021", "2022", "2023", "2024"), + currency = "$", + digits = 0 + ) ``` - -Then, do the same aggregation and indexing we did previously, this time using the updated bills. - ```{r} -t_new_exe_summ <- rbind(t_bills_w_exe, t_bills_new_exe)[ - , class := Mode(substr(class, 1, 1)), - by = pin -][ - class %in% c("2", "3", "5"), -][ - , class := ifelse(class == "2", "Residential", "Commercial") -][ - , .(total_bill = sum(final_tax)), - by = .(year, pin, class, stage) -][ - , .(avg_bill = mean(total_bill)), - by = .(year, class, stage) -][ - , idx_bill := (avg_bill / avg_bill[year == 2006]) * 100, - by = .(class, stage) -] +fund_type_table %>% + mutate(across(c("2020", "2021", "2022", "2023", "2024"), + ~ round(. / sum(.) * 100))) %>% + arrange(desc(`2024`)) %>% + datatable( + caption = "Chicago Property Tax Levies by Fund Type — Share by Year", + rownames = FALSE, + options = list( + pageLength = nrow(fund_type_table), + dom = "t", + columnDefs = list( + list(className = "dt-left", targets = 0), + list(className = "dt-right", targets = 1:5) + ) + ) + ) %>% + formatRound( + columns = c("2020", "2021", "2022", "2023", "2024"), + digits = 1 + ) %>% + formatString( + columns = c("2020", "2021", "2022", "2023", "2024"), + suffix = "%" + ) ``` - -Finally, we can plot the original bills against the updated ones. - -
- -Click here to show plot code - ```{r} -t_new_exe_summ_plot <- ggplot(data = t_new_exe_summ) + - geom_line( - aes(x = year, y = idx_bill, color = class, linetype = stage), - linewidth = 1.1 - ) + - geom_text( - data = t_annot, - aes(x = x, y = y, color = class, label = class), - hjust = 0 - ) + - scale_y_continuous(name = "Average Tax Bill, Indexed to 2006") + - scale_x_continuous(name = "Year", n.breaks = 10, limits = c(2006, 2020.4)) + - scale_linetype_manual( - name = "", - values = c("With exemptions" = "solid", "Changed exemptions" = "dotted") +chi_levy_plot_4 <- chi_levy %>% + ggplot() + + geom_line(aes(x = year, y = total_final_rate), linewidth = .8) + + geom_point(aes(x = year, y = total_final_rate)) + + scale_x_continuous(n.breaks = 10) + + scale_y_continuous(labels = scales::label_percent(scale = 1, suffix = "%"), + limits = c(0, 2)) + + labs( + x = NULL, + y = "Tax Rate" ) + - scale_color_brewer(name = "", palette = "Set1", direction = -1) + - guides(color = "none") + - facet_wrap(vars(class)) + theme_minimal() + theme( axis.title = element_text(size = 13), - axis.title.x = element_text(margin = margin(t = 6)), axis.title.y = element_text(margin = margin(r = 6)), - axis.text.y = element_text(size = 11), + axis.text = element_text(size = 11), strip.text = element_text(size = 16), strip.background = element_rect(fill = "#c9c9c9"), legend.title = element_text(size = 14), @@ -616,14 +446,7 @@ t_new_exe_summ_plot <- ggplot(data = t_new_exe_summ) + ) ``` -
- -
- -```{r, out.width="100%", echo=FALSE} -t_new_exe_summ_plot +```{r} +chi_levy_plot_4 ``` -The net effect of increasing the Senior Exemption while removing the Senior Freeze Exemption is a slight decrease in the *average* bill. However, this conclusion is ambiguous, complicated by the fact that the Senior Freeze is means-tested, while the Senior Exemption is not. The "real-world effect" of our hypothetical policy change would most likely be an increase in the property tax bills of poorer seniors, even though the average bill decreased. - -Ultimately, with some careful coding and assumptions, PTAXSIM (and its included data) can be used to test almost any hypothetical change in exemptions. From baea7ddce40605cf533a1fdf8ed931586e3ec800 Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Wed, 15 Apr 2026 20:02:17 +0000 Subject: [PATCH 03/18] Update inflation portion --- vignettes/agencies.Rmd | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index cb55cd4..49cb2c4 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -35,7 +35,7 @@ library(ggplot2) library(tidyr) devtools::load_all() -ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2.db")) +ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-alpha.2.db")) ``` ```{r, echo=FALSE} @@ -125,6 +125,7 @@ Now that we have the correct agency numbers for all years, we can select and agg Many of the fields in the `agencies` table, which come from the Clerk's agency rate report, relate to PTELL calculations. Because the Chicago is a home rule municipality, it is not subject to PTELL (Property Tax Extension Law Limit) as imposed by the State of Illinois, so these fields will not contain relevant info. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's limits which prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. +Let's create an object called `chi_levy` that contains final total levy, taxable aggregate EAV (or the tax base), and the amount of EAV designated as "New Property". New Property is defined as EAV from expiring TIFs, new constriction, or expiring incentives. ```{r} chi_levy <- chi_agencies %>% @@ -138,7 +139,7 @@ chi_levy <- curr_new_prop = first(curr_new_prop) ) ``` -PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to the inflation rate since 2006. +PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to inflation since 2006. To do this we can pull CPI data from the PTAXSIM database - we include the CPI tables published annually by the Illinois Department of Revenue (IDOR) for the purpose of setting the PTELL limit. @@ -158,16 +159,23 @@ cpi <- DBI::dbGetQuery( First, let's convert each year's levy to be in terms of 2006 dollars using the consumer price index (CPI). This will help us understand the degree to which the levy has been adjusted on account of inflation vs. increasing revenue. -```{r} -cpi <- cpi %>% - mutate(multiplier = cpi/cpi[year == 2006]) +The code below queries CPI data, then calculates its percent change since 2006. We will need to add the fields `agency_num` and `agency_name` in preparation of combining it with the `chi_levy` data. -chi_levy_adj <- chi_levy %>% - left_join(cpi, "year") %>% +```{r} +cpi <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT * + FROM cpi + " +) %>% mutate( - total_final_levy_adj = total_final_levy / multiplier - ) - + pct_inc = (cpi / cpi[year == 2006] - 1) * 100, + agency_num = "100", + agency_name = "CPI-U" + ) %>% + filter(year >= 2006) %>% + select(year, agency_num, agency_name, pct_inc) ``` Plotting Chicago's total property tax levy from 2006 to 2024 in both 2006 dollars and nominal dollars, we can see that the levy remained steady from 2006 to 2014. The levy actually decreased slightly in real dollars given the levy was not increased to account for inflation during this time. Then in 2015, City begins increasing its levy significantly due to State legislation that dictates [funding requirements for Chicago's Fire and Police pension funds](https://www.civicfed.org/civic-federation/blog/chicago-police-and-fire-pension-funding-changes-become-law). From dee9488e1f61091438504a7d89b9e270c3d2bcb8 Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Thu, 16 Apr 2026 18:00:34 +0000 Subject: [PATCH 04/18] Add some more plots and text --- vignettes/agencies.Rmd | 268 +++++++++++++++++++---------------------- 1 file changed, 125 insertions(+), 143 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 49cb2c4..0cdf135 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -19,9 +19,9 @@ In this vignette, we'll demonstrate how to query data from the PTAXSIM SQLite da Additionally, we'll show how to account for the 2024 changes to the Cook County Clerk's agency fund report data structure which will be necessary for users to pay attention to when conducting a time series analysis of certain taxing agencies. -# Chicago's levy over time +# Chicago taxing agencies -Using data from the PTAXSIM database, let's look at the levy history for the City of Chicago from 2006 to 2024. +Using data from the PTAXSIM database, let's look at the levy history for the primary taxing agencies that appear on a Chicago property's tax bill: the `CITY OF CHICAGO`, `BOARD OF EDUCATION` (a.k.a. Chicago Public Schools), and `CHICAGO PARK DISTRICT`. First, load some useful libraries and instantiate a PTAXSIM DBI connection with the default name (`ptaxsim_db_conn`) expected by PTAXSIM functions. @@ -46,7 +46,7 @@ ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-al #) ``` -First, we'll query the database table `agency_info` to determine the City of Chicago's unique `agency_num`. +We can query the database table `agency_info` to search for `agency_num` key assigned the the taxing agencies of interest. This is what shows up when we search of agencies with the `CHICAGO` in the `agenc_name`. ```{r} chi_agency_nums <- DBI::dbGetQuery( @@ -54,15 +54,16 @@ chi_agency_nums <- DBI::dbGetQuery( " SELECT agency_num, agency_name FROM agency_info - WHERE agency_name LIKE '%CITY OF CHICAGO%' + WHERE agency_name LIKE '%CHICAGO%' " ) head(chi_agency_nums) ``` -For purpose of our analysis, we'll just focus on `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. While the `CITY OF CHICAGO LIBRARY FUND` has been treated as its own taxing agency in the Clerk's data, the City itself reports the amount levied for the purpose of funding the Chicago Public Library as a portion of its entire property tax levy. -First, we'll query all fields from the `agencies` table. +Note that there are several agencies that pop up when we search of agencies that contain `CHICAGO` in the name. Let's take a moment to examine the agencies `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. While the `CITY OF CHICAGO LIBRARY FUND` has been treated as its own taxing agency in the Clerk's data, the City itself reports the amount levied for the purpose of funding the Chicago Public Library as a portion of its entire property tax levy. + +We're also interested in the `BOARD OF EDUCATION` and `CHICAGO PARK DISTRICT` agencies, which have agency numbers `050200000` and `044060000` respectively. We'll query all fields from the `agency` table and filter by their `agency_num`. ```{r} chi_agencies <- DBI::dbGetQuery( @@ -72,6 +73,8 @@ chi_agencies <- DBI::dbGetQuery( FROM agency WHERE agency_num = '030210000' OR agency_num = '030210001' + OR agency_num = '050200000' + OR agency_num = '044060000' " ) ``` @@ -99,7 +102,6 @@ agency_cw_24 <- DBI::dbGetQuery( select(agency_num, agency_name, agency_num_24, agency_name_24) datatable(agency_cw_24) - ``` We can now use `agency_cw_24` as a crosswalk to convert these sub-agencies to their parent agency numbers. @@ -119,13 +121,12 @@ chi_agencies <- chi_agencies %>% ) ``` -## Tracking City of Chicago's levy growth with inflation +## Tracking Chicago levies -Now that we have the correct agency numbers for all years, we can select and aggregate the fields of interest for `CITY OF CHICAGO`. - -Many of the fields in the `agencies` table, which come from the Clerk's agency rate report, relate to PTELL calculations. Because the Chicago is a home rule municipality, it is not subject to PTELL (Property Tax Extension Law Limit) as imposed by the State of Illinois, so these fields will not contain relevant info. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's limits which prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. +Now that we have the correct agency numbers for all years, we can select and aggregate the fields of interest for the Chicago taxing agencies we want to look at. Let's create an object called `chi_levy` that contains final total levy, taxable aggregate EAV (or the tax base), and the amount of EAV designated as "New Property". New Property is defined as EAV from expiring TIFs, new constriction, or expiring incentives. + ```{r} chi_levy <- chi_agencies %>% @@ -138,28 +139,25 @@ chi_levy <- prior_eav = first(prior_eav), curr_new_prop = first(curr_new_prop) ) -``` -PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to inflation since 2006. - -To do this we can pull CPI data from the PTAXSIM database - we include the CPI tables published annually by the Illinois Department of Revenue (IDOR) for the purpose of setting the PTELL limit. -```{r} -cpi <- DBI::dbGetQuery( - ptaxsim_db_conn, - " - SELECT - levy_year as year, - cpi, - ptell_cook - FROM cpi - " -) %>% - filter(year >= 2006) +chi_levy_indx <- chi_levy %>% + group_by(agency_num) %>% + mutate(levy_2006 = total_final_levy[year == 2006][1], + levy_pct_inc = (total_final_levy - levy_2006) / levy_2006 * 100) %>% + ungroup() %>% + select(agency_num, year, levy_pct_inc) %>% + pivot_longer( + cols = levy_pct_inc, + names_to = "series", + values_to = "pct_inc" + ) ``` -First, let's convert each year's levy to be in terms of 2006 dollars using the consumer price index (CPI). This will help us understand the degree to which the levy has been adjusted on account of inflation vs. increasing revenue. +Many of the fields in the `agencies` table, which come from the Clerk's agency rate report, relate to the Clerk's PTELL (Property Tax Extension Law Limit) calculations. PTELL ensures certain taxing agencies do not increase their levies beyond the rate of inflation (with some exceptions). CPS and Chicago Park District are subject to PTELL rules, but the City of Chicago is not by virtue of being a non-home rule agency. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's limits which prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. -The code below queries CPI data, then calculates its percent change since 2006. We will need to add the fields `agency_num` and `agency_name` in preparation of combining it with the `chi_levy` data. +PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to inflation since 2006. + +To do this we can pull CPI data from the PTAXSIM database - we include the CPI tables published annually by the Illinois Department of Revenue (IDOR) for the purpose of setting the PTELL limit. We will calculate the CPI percent change from 2006, and then combine that data with the levy percent change data from `chi_levy_indx`. ```{r} cpi <- DBI::dbGetQuery( @@ -172,114 +170,118 @@ cpi <- DBI::dbGetQuery( mutate( pct_inc = (cpi / cpi[year == 2006] - 1) * 100, agency_num = "100", - agency_name = "CPI-U" + series = "cpi" ) %>% filter(year >= 2006) %>% - select(year, agency_num, agency_name, pct_inc) + select(year, agency_num, series, pct_inc) ``` -Plotting Chicago's total property tax levy from 2006 to 2024 in both 2006 dollars and nominal dollars, we can see that the levy remained steady from 2006 to 2014. The levy actually decreased slightly in real dollars given the levy was not increased to account for inflation during this time. Then in 2015, City begins increasing its levy significantly due to State legislation that dictates [funding requirements for Chicago's Fire and Police pension funds](https://www.civicfed.org/civic-federation/blog/chicago-police-and-fire-pension-funding-changes-become-law). - -Rising inflation beginning in the wake of the Covid-19 pandemic is evident with the levy in "real" dollars decreasing slightly from 2020 to 2024 even as the levy in nominal dollars increased in that time period.
Click here to show plot code ```{r} -chi_levy_plot_1 <- chi_levy_adj %>% +highlight_key <- tibble::tibble( + agency_num = c("100", "050200000", "030210000", "044060000"), + label = c("CPI-U", "CPKD", "City", "CPS") +) + +plot_1_df <- rbind(cpi, chi_levy_indx) %>% + left_join(highlight_key, by = "agency_num") + +df_lab <- plot_1_df %>% + group_by(agency_num, label) %>% + filter(year == max(year, na.rm = TRUE)) %>% + slice_tail(n = 1) %>% + ungroup() %>% + mutate(label_col = ifelse(label == "CPI-U", "#d62728", "black")) # red for CPI-U + +chi_levy_plot_1 <- ggplot() + - geom_line(aes(x = year, y = total_final_levy, color = "Levy in nominal dollars"), - linewidth = .8) + - geom_point(aes(x = year, y = total_final_levy, color = "Levy in nominal dollars")) + - geom_line(aes(x = year, y = total_final_levy_adj, color = "Levy in 2006 dollars"), - linewidth = .8, linetype = "dashed") + - geom_point(aes(x = year, y = total_final_levy_adj, color = "Levy in 2006 dollars")) + - scale_color_manual( - name = NULL, - values = c("Levy in nominal dollars" = "black", - "Levy in 2006 dollars" = "blue") + geom_line( + data = plot_1_df, + aes(x = year, y = pct_inc/100, group = agency_num, color = label), + linewidth = .5 ) + - scale_x_continuous(n.breaks = 10) + - scale_y_continuous( - labels = scales::label_dollar(scale = 1e-9, suffix = "B"), - limits = c(0, 2000000000) + # right-side labels in black (no color mapping) + geom_text( + data = df_lab, + aes(x = year, y = pct_inc/100, label = label), + color = df_lab$label_col, + hjust = -0.1, + size = 3.5 + ) + + scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + + scale_x_continuous( + breaks = seq(min(plot_1_df$year, na.rm = TRUE), max(plot_1_df$year, na.rm = TRUE), by = 2), + expand = expansion(mult = c(0.01, 0.08)) # add right padding so labels fit + ) + + scale_color_manual( + values = c( + "CPKD" = "black", + "City" = "black", + "CPS" = "black", + "CPI-U" = "#d62728" + ), + breaks = c("CPKD", "City", "CPS"), + guide = "none" # hide legend since lines are labeled ) + labs( - x = NULL, - y = "Total Final Levy (Billions)" + x = "Year", + y = "Percent change from 2006", ) + - theme_minimal() + - theme( - axis.title = element_text(size = 13), - axis.title.x = element_text(margin = margin(t = 6)), - axis.title.y = element_text(margin = margin(r = 6)), - axis.text = element_text(size = 11), - strip.text = element_text(size = 16), - strip.background = element_rect(fill = "#c9c9c9"), - legend.title = element_text(size = 14), - legend.key.size = unit(24, "points"), - legend.text = element_text(size = 12), - legend.position = "bottom" - ) - + theme_minimal(base_size = 12) ``` -

+Next, we will plot the percent change in levy from 2006 for the City of Chicago, Chicago Public Schools, and Chicago Park District, and compare that to the percent change in CPI-U over the same period. + +We can see that the City of Chicago's levy has increased the most, having more than doubled from 2006 to 2024. Both Chicago and CPS's levies have increased far beyond the rate of inflation, while the Chicago Park District's levy has kept pace with it. + ```{r, echo=FALSE, out.width="100%"} chi_levy_plot_1 ``` -Next, we'll take a look at how the percent change in the levy compares to the percent change in CPI - or the rate of inflation. We add some data manipulation to calculate the percent change in both the levy and CPI by each year since 2006. - -```{r} -chi_levy_adj <- chi_levy_adj %>% - ungroup() %>% - mutate( - pct_change_final_levy = (total_ext - total_ext[year == 2006]) / total_ext[year == 2006] * 100, - pct_change_ptell = (1 * c(1, cumprod(1 + tail(ptell_cook, -1))) - 1) * 100, - ) -``` - -Plotting the percent change of the City's levy since 2006, we see it has outgrown the cumulative effects of the City's property tax limitation -
+Even though the City's levy has increased the most in percentage terms since 2006, it does not have the largest levy. The taxing agencies that rely most on property taxes are school districts and this is true for Chicago as well. The plot below shows the final tax extensions (meaning the final amount to be received by taxing agencies after the levies are validated by PTELL), for each of the three agencies. CPS is slightly more than double the City's extension in 2024 at about $4 Billion. Chicago Park District's extension is substantially less at less than 1 Billion. Click here to show plot code -```{r, echo=FALSE, out.width="100%"} -chi_levy_plot_2 <- chi_levy_adj %>% - pivot_longer( - cols = c(pct_change_final_levy, pct_change_ptell), - names_to = "series", - values_to = "pct_change" - ) %>% - mutate(series = recode(series, - "pct_change_final_levy" = "Levy % change (nominal $)", - "pct_change_ptell" = "PTELL % change" - )) %>% - ggplot() + - geom_line(aes(x = year, y = pct_change, color = series), linewidth = .8) + - geom_point(aes(x = year, y = pct_change, color = series)) + - scale_x_continuous(n.breaks = 10) + - scale_y_continuous(labels = scales::label_percent(scale = 1, suffix = "%")) + - labs( - x = NULL, - y = "Percent Change Since Base Year", - color = NULL +```{r} +plot_2_df <- chi_levy %>% + left_join(highlight_key) + +df_lab <- plot_2_df %>% + group_by(agency_num, label) %>% + slice_max(year, n = 1, with_ties = FALSE) %>% + ungroup() + +df_2024 <- plot_2_df %>% + filter(year == 2024) %>% + group_by(agency_num, label) %>% + slice_tail(n = 1) %>% # in case there are duplicates in 2024 + ungroup() %>% + mutate(label_2024 = paste0( + label, ": ", scales::label_dollar(scale = 1e-9, suffix = "B", accuracy = 0.1)(total_ext) + )) + +chi_levy_plot_2 <- ggplot(plot_2_df, aes(year, total_ext, group = agency_num)) + + geom_line(linewidth = 0.5, color = "black") + + # 2024 value labels like "CPS: $1.1B" + geom_text( + data = df_2024, + aes(label = label_2024), + color = "black", + hjust = .6, + vjust = -.5 ) + - theme_minimal() + - theme( - axis.title = element_text(size = 13), - axis.title.y = element_text(margin = margin(r = 6)), - axis.text = element_text(size = 11), - strip.text = element_text(size = 16), - strip.background = element_rect(fill = "#c9c9c9"), - legend.title = element_text(size = 14), - legend.key.size = unit(24, "points"), - legend.text = element_text(size = 12), - legend.position = "bottom" - ) + scale_y_continuous(labels = scales::label_dollar(scale = 1e-9, suffix = "B")) + + scale_x_continuous( + breaks = seq(min(plot_2_df$year, na.rm = TRUE), max(plot_2_df$year, na.rm = TRUE), by = 2), + expand = expansion(mult = c(0.01, 0.08)) + ) + + labs(x = "Year", y = "Final Agency Extension") + + theme_minimal(base_size = 12) ```
@@ -288,7 +290,11 @@ chi_levy_plot_2 <- chi_levy_adj %>% ```{r, echo=FALSE, out.width="100%"} chi_levy_plot_2 ``` +The PTAXSIM database also contains information related to taxing agency's property tax funds so we can understand exactly what they intend to spend their property tax revenue on. This information can be found in the `agency_fund` and `agency_fund_info` tables. This data is not utilized by any of PTAXSIM's functions, but it is available to be queried an analyzed. +We will query the fund information for the `CITY OF CHICAGO` and the `CITY OF CHICAGO LIBRARY FUND`. + +To simplify the `agency_fund` data, we will add broader categories to define funds. For example, there is not a consistent identifier for which funds are contributions to the City's various pension funds. In this case, we'll tag any fund with "A & B" (Annuities and Benefits) in the name as a pension fund. We'll label funds related to bond and interest payments, as well as note redemption, as "Bond Payments". ```{r} chi_agency_fund <- DBI::dbGetQuery( @@ -332,6 +338,8 @@ chi_agency_fund <- chi_agency_fund %>% ``` +The plot below +
Click here to show plot code @@ -339,7 +347,7 @@ chi_agency_fund <- chi_agency_fund %>% chi_levy_plot_3 <- chi_agency_fund %>% ggplot() + geom_line(aes(x = year, y = final_levy, color = fund_catg), - linewidth = .8) + + linewidth = .5) + geom_point(aes(x = year, y = final_levy, color = fund_catg)) + scale_x_continuous(n.breaks = 10) + scale_y_continuous( @@ -368,11 +376,14 @@ chi_levy_plot_3 <- chi_agency_fund %>%
+The plot below illustrates how the City's levy increase in 2016 was by soley driven by a massive increase in the City's pension contributions. Pension contributions grew again in 2021, but in tandem with a decrease in bond financing. + ```{r, echo=FALSE, out.width="100%"} chi_levy_plot_3 ``` +With so much of the City's property tax revenue going to funding pensions, it's difficult to see if or how there was any change to the amount going to the Library Fund. The below tables show the final levy for each fund type in 2020-2024, as well as the share of the total levy for each fund type by year. -```{r} +```{r, echo = FALSE} fund_type_table <- chi_agency_fund %>% select(-final_rate) %>% filter(year > 2019) %>% @@ -402,7 +413,7 @@ fund_type_table %>% digits = 0 ) ``` -```{r} +```{r, echo = FALSE} fund_type_table %>% mutate(across(c("2020", "2021", "2022", "2023", "2024"), ~ round(. / sum(.) * 100))) %>% @@ -428,33 +439,4 @@ fund_type_table %>% suffix = "%" ) ``` -```{r} -chi_levy_plot_4 <- chi_levy %>% - ggplot() + - geom_line(aes(x = year, y = total_final_rate), linewidth = .8) + - geom_point(aes(x = year, y = total_final_rate)) + - scale_x_continuous(n.breaks = 10) + - scale_y_continuous(labels = scales::label_percent(scale = 1, suffix = "%"), - limits = c(0, 2)) + - labs( - x = NULL, - y = "Tax Rate" - ) + - theme_minimal() + - theme( - axis.title = element_text(size = 13), - axis.title.y = element_text(margin = margin(r = 6)), - axis.text = element_text(size = 11), - strip.text = element_text(size = 16), - strip.background = element_rect(fill = "#c9c9c9"), - legend.title = element_text(size = 14), - legend.key.size = unit(24, "points"), - legend.text = element_text(size = 12), - legend.position = "bottom" - ) -``` - -```{r} -chi_levy_plot_4 -``` From 4814a5cceaef5ba7a19c95c412a6031d19c60fe9 Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Thu, 16 Apr 2026 18:07:04 +0000 Subject: [PATCH 05/18] Add github actions path back in --- vignettes/agencies.Rmd | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 0cdf135..19e14d8 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -31,19 +31,18 @@ library(dplyr) library(DT) library(here) library(ggplot2) -#library(ptaxsim) +library(ptaxsim) library(tidyr) -devtools::load_all() -ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-alpha.2.db")) +#ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-alpha.2.db")) ``` ```{r, echo=FALSE} # This is needed to build the vignette using GitHub Actions -#ptaxsim_db_conn <- DBI::dbConnect( - # RSQLite::SQLite(), - #Sys.getenv("PTAXSIM_DB_PATH") -#) +ptaxsim_db_conn <- DBI::dbConnect( + RSQLite::SQLite(), + Sys.getenv("PTAXSIM_DB_PATH") +) ``` We can query the database table `agency_info` to search for `agency_num` key assigned the the taxing agencies of interest. This is what shows up when we search of agencies with the `CHICAGO` in the `agenc_name`. From e3c1bb18fcc08596c998e49e7ec0233e70287168 Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Mon, 20 Apr 2026 17:22:17 +0000 Subject: [PATCH 06/18] Structure change --- vignettes/agencies.Rmd | 72 +++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 19e14d8..6e06dd3 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -15,15 +15,15 @@ knitr::opts_chunk$set( One of the unique features of PTAXSIM is the database that accompanies the package - this DB contains the only publicly accessible, machine-readable data collected from the various Cook County property tax agencies going back to 2006. While the primary use of PTAXSIM is its functionality to calculate tax bills, its database holds a plethora of data that can be used to investigate Cook County's property tax system, including the behavior of the over 900 taxing agencies and 400 TIFs that collect revenue through property taxes. -In this vignette, we'll demonstrate how to query data from the PTAXSIM SQLite database that can help us analyze taxing agencies' property tax revenue over time, as well as revenue collected by TIFs. +In this vignette, we'll demonstrate how to query data from the PTAXSIM SQLite database that can help us analyze taxing agencies' property tax revenue over time. -Additionally, we'll show how to account for the 2024 changes to the Cook County Clerk's agency fund report data structure which will be necessary for users to pay attention to when conducting a time series analysis of certain taxing agencies. +Additionally, we'll show how to account for the 2024 changes to the Cook County Clerk's agency data structure which will be necessary for users to account for when conducting a time series analysis of certain taxing agencies. # Chicago taxing agencies -Using data from the PTAXSIM database, let's look at the levy history for the primary taxing agencies that appear on a Chicago property's tax bill: the `CITY OF CHICAGO`, `BOARD OF EDUCATION` (a.k.a. Chicago Public Schools), and `CHICAGO PARK DISTRICT`. +Using data from the PTAXSIM database, let's look at the levy history for three of the primary taxing agencies that appear on a Chicago property's tax bill: the `CITY OF CHICAGO`, `BOARD OF EDUCATION` (a.k.a. Chicago Public Schools), and `CHICAGO PARK DISTRICT`. -First, load some useful libraries and instantiate a PTAXSIM DBI connection with the default name (`ptaxsim_db_conn`) expected by PTAXSIM functions. +First, load some useful libraries and instantiate a PTAXSIM DBI connection with the default name (`ptaxsim_db_conn`). ```{r} library(data.table) @@ -34,18 +34,45 @@ library(ggplot2) library(ptaxsim) library(tidyr) -#ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-alpha.2.db")) +ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-alpha.2.db")) ``` ```{r, echo=FALSE} # This is needed to build the vignette using GitHub Actions -ptaxsim_db_conn <- DBI::dbConnect( - RSQLite::SQLite(), - Sys.getenv("PTAXSIM_DB_PATH") -) +#ptaxsim_db_conn <- DBI::dbConnect( +# RSQLite::SQLite(), +# Sys.getenv("PTAXSIM_DB_PATH") +#) ``` -We can query the database table `agency_info` to search for `agency_num` key assigned the the taxing agencies of interest. This is what shows up when we search of agencies with the `CHICAGO` in the `agenc_name`. +## Accounting for 2024 changes to agency fund reporting + +Before we query the relevant agency data, we first will need to check if any of the agencies of interest were impacted by the Clerk's 2024 updates to the data structure. We added new fields to the `agency_info` table to document these changes - we'll query this table and select only they agencies where the field `agency_change_24 = TRUE`. + +```{r} +# Query agency_info table for all agencies with the 2024 update +agency_cw_24 <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT * + FROM agency_info + WHERE agency_change_24 = TRUE + " +) %>% + select(agency_num, agency_name, agency_num_24, agency_name_24) + +datatable(agency_cw_24) +``` + +From this table it appears that the updated agencies are specific funds for various municipalities - the Clerk had previously reported certain municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agencies data over time. + +To make this process easier, we have added new fields to the `agency_info` table in the PTAXSIM database which identify the agencies that have been folded into their parent agencies. `agency_num_24` and `agency_name_24` contain the agency info that the old "sub-agency" has been merged into. With this table we can create a crosswalk, as we did above, and call it `agency_cw_24`. Note that the user can still see details about these former agencies, now funds, by querying the `agency_fund` table. + +A quick search of the crosswalk shows that the `CITY OF CHICAGO LIBRARY FUND` was previously defined as its own agency and has now been folded into the `CITY OF CHICAGO`. This is in fact aligned with how the City of Chicago reports its property tax levy in its own budget [documentation](https://chicityclerk.s3.us-west-2.amazonaws.com/s3fs-public/O2023-0005291_Tax_Levy.pdf). + + + +We can query the database table `agency_info` to search for `agency_num` key assigned the the taxing agencies of interest. The table below displays all taxing agencies and TIFs with `CHICAGO` presenet in the `agency_name`. ```{r} chi_agency_nums <- DBI::dbGetQuery( @@ -57,10 +84,10 @@ chi_agency_nums <- DBI::dbGetQuery( " ) -head(chi_agency_nums) +datatable(chi_agency_nums) ``` -Note that there are several agencies that pop up when we search of agencies that contain `CHICAGO` in the name. Let's take a moment to examine the agencies `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. While the `CITY OF CHICAGO LIBRARY FUND` has been treated as its own taxing agency in the Clerk's data, the City itself reports the amount levied for the purpose of funding the Chicago Public Library as a portion of its entire property tax levy. +Let's take a moment to examine the agencies `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. While the `CITY OF CHICAGO LIBRARY FUND` has been treated as its own taxing agency in the Clerk's data, the City itself reports the amount levied for the purpose of funding the Chicago Public Library as a portion of its entire property tax levy. We're also interested in the `BOARD OF EDUCATION` and `CHICAGO PARK DISTRICT` agencies, which have agency numbers `050200000` and `044060000` respectively. We'll query all fields from the `agency` table and filter by their `agency_num`. @@ -78,30 +105,9 @@ chi_agencies <- DBI::dbGetQuery( ) ``` -## Accounting for 2024 changes to agency fund reporting - -When examining `chi_agencies`, we see that in 2024 the `CITY OF CHICAGO LIBRARY FUND` has a \$0 levy and therefore a tax rate of 0. This is because, starting in 2024, the Cook County Clerk began reporting the requested levy for the City of Chicago libraries as a fund under the `CITY OF CHICAGO` taxing agency. As mentioned above, this is in fact aligned with how the City of Chicago reports its property tax levy in its own budget [documentation](https://chicityclerk.s3.us-west-2.amazonaws.com/s3fs-public/O2023-0005291_Tax_Levy.pdf). -This reporting change occurred for many municipal taxing agencies, where the Clerk had previously reported municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agencies data over time. -To make this process easier, we have added new fields to the `agency_info` table in the PTAXSIM database which identify the agencies that have been folded into their parent agencies. `agency_num_24` and `agency_name_24` contain the agency info that the old "sub-agency" has been merged into. Note that the user can still see details about these former agencies, now funds, by querying the `agency_fund` table. -The table below shows all of the taxing agencies that have been been folded into their parent agencies beginning with tax year 2024. In all, we have 78 former taxing agencies that are no longer reported as individual agencies beginning in 2024. - -```{r} -# Query agency_info table for all agencies with the 2024 update -agency_cw_24 <- DBI::dbGetQuery( - ptaxsim_db_conn, - " - SELECT * - FROM agency_info - WHERE agency_change_24 = 1 - " -) %>% - select(agency_num, agency_name, agency_num_24, agency_name_24) - -datatable(agency_cw_24) -``` We can now use `agency_cw_24` as a crosswalk to convert these sub-agencies to their parent agency numbers. Knowing this caveat about 2024 agency data, we'll query the both `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND` for all years, and then update the `agency_num` field for `CITY OF CHICAGO LIBRARY FUND` using `agency_cw_24`. This will ensure consistent reporting across all years. From 0f983e45c6620c30796c5de8fff6f96e4e7c8dd9 Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Tue, 21 Apr 2026 21:15:50 +0000 Subject: [PATCH 07/18] Final narrative structure I think --- vignettes/agencies.Rmd | 106 +++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 6e06dd3..477e10e 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -47,7 +47,7 @@ ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-al ## Accounting for 2024 changes to agency fund reporting -Before we query the relevant agency data, we first will need to check if any of the agencies of interest were impacted by the Clerk's 2024 updates to the data structure. We added new fields to the `agency_info` table to document these changes - we'll query this table and select only they agencies where the field `agency_change_24 = TRUE`. +Before we query the relevant agency data, we first will need to check if any of the agencies of interest were impacted by the Clerk's 2024 updates to the data structure. We added new fields to the `agency_info` table to document these changes - we'll query this table and select only the agencies where the field `agency_change_24 = TRUE`. ```{r} # Query agency_info table for all agencies with the 2024 update @@ -64,15 +64,13 @@ agency_cw_24 <- DBI::dbGetQuery( datatable(agency_cw_24) ``` -From this table it appears that the updated agencies are specific funds for various municipalities - the Clerk had previously reported certain municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agencies data over time. +From this table it appears that the updated agencies are specific funds for various municipalities - the Clerk had previously reported certain municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agency data over time. -To make this process easier, we have added new fields to the `agency_info` table in the PTAXSIM database which identify the agencies that have been folded into their parent agencies. `agency_num_24` and `agency_name_24` contain the agency info that the old "sub-agency" has been merged into. With this table we can create a crosswalk, as we did above, and call it `agency_cw_24`. Note that the user can still see details about these former agencies, now funds, by querying the `agency_fund` table. +To make this process possible, we added new fields to the `agency_info` table in the PTAXSIM database which identify the agencies that have been folded into their parent agencies. `agency_num_24` and `agency_name_24` contain the agency info that the "sub-agency" has been merged into. With this table we can create a crosswalk, as we did above, calling it `agency_cw_24`. Note that the user can still see details about these former agencies, now funds, by querying the `agency_fund` table. -A quick search of the crosswalk shows that the `CITY OF CHICAGO LIBRARY FUND` was previously defined as its own agency and has now been folded into the `CITY OF CHICAGO`. This is in fact aligned with how the City of Chicago reports its property tax levy in its own budget [documentation](https://chicityclerk.s3.us-west-2.amazonaws.com/s3fs-public/O2023-0005291_Tax_Levy.pdf). +A quick search of the crosswalk shows that the `CITY OF CHICAGO LIBRARY FUND` was previously defined as an independent agency and has now been folded into the `CITY OF CHICAGO`. This is in fact aligned with how the City of Chicago reports its property tax levy in its own budget [documentation](https://chicityclerk.s3.us-west-2.amazonaws.com/s3fs-public/O2023-0005291_Tax_Levy.pdf). When we eventually query the agency data for `CITY OF CHICAGO`, we'll want to include the `CITY OF CHICAGO LIBRARY FUND` as well. - - -We can query the database table `agency_info` to search for `agency_num` key assigned the the taxing agencies of interest. The table below displays all taxing agencies and TIFs with `CHICAGO` presenet in the `agency_name`. +Next, we'll search the database table `agency_info` to find the right `agency_num` key assigned to the taxing agencies we want to learn more about. The table below displays all taxing agencies and TIFs with `CHICAGO` present in the `agency_name`. ```{r} chi_agency_nums <- DBI::dbGetQuery( @@ -87,9 +85,7 @@ chi_agency_nums <- DBI::dbGetQuery( datatable(chi_agency_nums) ``` -Let's take a moment to examine the agencies `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND`, with agency fund numbers `030210000` and `030210002` respectively. While the `CITY OF CHICAGO LIBRARY FUND` has been treated as its own taxing agency in the Clerk's data, the City itself reports the amount levied for the purpose of funding the Chicago Public Library as a portion of its entire property tax levy. - -We're also interested in the `BOARD OF EDUCATION` and `CHICAGO PARK DISTRICT` agencies, which have agency numbers `050200000` and `044060000` respectively. We'll query all fields from the `agency` table and filter by their `agency_num`. +Using the table above, we learn that the `agency_num` values for the agencies `CITY OF CHICAGO`, `CITY OF CHICAGO LIBRARY FUND`, `BOARD OF EDUCATION`, and `CHICAGO PARK DISTRICT` are `030210000`, `030210001`, `050200000`, and `044060000` respectively. We'll query all fields from the `agency` table and filter by their `agency_num`. ```{r} chi_agencies <- DBI::dbGetQuery( @@ -105,12 +101,7 @@ chi_agencies <- DBI::dbGetQuery( ) ``` - - - -We can now use `agency_cw_24` as a crosswalk to convert these sub-agencies to their parent agency numbers. - -Knowing this caveat about 2024 agency data, we'll query the both `CITY OF CHICAGO` and `CITY OF CHICAGO LIBRARY FUND` for all years, and then update the `agency_num` field for `CITY OF CHICAGO LIBRARY FUND` using `agency_cw_24`. This will ensure consistent reporting across all years. +Before we do anything, we need to fold the `CITY OF CHICAGO LIBRARY FUND` into the `CITY OF CHICAGO` levy total for all years prior to 2024. We can do this by joining `agency_cw_24` by `agency_num` to `chi_agencies` and replacing the old `agency_num` with `agency_num_24`, then grouping by agency and year and then summing the fields `total_final_levy` and `total_ext`. *Terminology note*: The **levy** is the amount of money a local government budgets for and requests to receive from property taxes. Many taxing agencies are subject to limits imposed by State law. The **extension** (called `final_ext` in the database) is the total, final amount a taxing body is allowed to receive which is calculated and confirmed by the Cook County Clerk. ```{r} chi_agencies <- chi_agencies %>% @@ -122,30 +113,27 @@ chi_agencies <- chi_agencies %>% agency_num = ifelse(!is.na(agency_num_24), agency_num_24, - agency_num) - ) + agency_num)) %>% + group_by(year, agency_num) %>% + summarize( + total_final_levy = sum(total_final_levy), + total_ext = sum(total_ext) +) ``` -## Tracking Chicago levies +## Tracking Chicago, CPS, and CPKD levies -Now that we have the correct agency numbers for all years, we can select and aggregate the fields of interest for the Chicago taxing agencies we want to look at. +Now that we have the correct total levies for the City of Chicago, Chicago Public Schools and Chicago Park District across all years, we can look at how those levies have changed from 2006 to 2024. -Let's create an object called `chi_levy` that contains final total levy, taxable aggregate EAV (or the tax base), and the amount of EAV designated as "New Property". New Property is defined as EAV from expiring TIFs, new constriction, or expiring incentives. +CPS and CPKD are both subject to PTELL (Property Tax Extension Law Limit), which ensures certain taxing agencies do not increase their levies beyond the rate of inflation (with some exceptions\*). The City of Chicago is not by virtue of being a non-home rule agency. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's and prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. -```{r} -chi_levy <- - chi_agencies %>% - group_by(year, agency_num) %>% - summarize( - total_final_levy = sum(total_final_levy), - total_final_rate = sum(total_final_rate), - total_ext = sum(total_ext), - cty_cook_eav = first(cty_cook_eav), - prior_eav = first(prior_eav), - curr_new_prop = first(curr_new_prop) - ) +\*PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to inflation since 2006. + +To do so, we'll calculate the rate at which the levies have grown compared to the [CPI-U](https://www.bls.gov/cpi/). Fortunately CPI data, as reported by the Illinois Department of Revenue (IDOR) for purpose of PTELL calculatios, is available in the PTAXSIM data base in the `cpi` table. -chi_levy_indx <- chi_levy %>% +```{r} +# Calculate the levy percent change for each agency indexed to 2006 +chi_levy_indx <- chi_agencies %>% group_by(agency_num) %>% mutate(levy_2006 = total_final_levy[year == 2006][1], levy_pct_inc = (total_final_levy - levy_2006) / levy_2006 * 100) %>% @@ -156,15 +144,8 @@ chi_levy_indx <- chi_levy %>% names_to = "series", values_to = "pct_inc" ) -``` -Many of the fields in the `agencies` table, which come from the Clerk's agency rate report, relate to the Clerk's PTELL (Property Tax Extension Law Limit) calculations. PTELL ensures certain taxing agencies do not increase their levies beyond the rate of inflation (with some exceptions). CPS and Chicago Park District are subject to PTELL rules, but the City of Chicago is not by virtue of being a non-home rule agency. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's limits which prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. - -PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to inflation since 2006. - -To do this we can pull CPI data from the PTAXSIM database - we include the CPI tables published annually by the Illinois Department of Revenue (IDOR) for the purpose of setting the PTELL limit. We will calculate the CPI percent change from 2006, and then combine that data with the levy percent change data from `chi_levy_indx`. - -```{r} +# Query CPI data from PTAXSIM db, calculate percent change indexed to 2006 cpi <- DBI::dbGetQuery( ptaxsim_db_conn, " @@ -181,10 +162,12 @@ cpi <- DBI::dbGetQuery( select(year, agency_num, series, pct_inc) ``` +We'll then plot the rate of change for each levy compared to inflation.
Click here to show plot code + ```{r} highlight_key <- tibble::tibble( agency_num = c("100", "050200000", "030210000", "044060000"), @@ -237,23 +220,25 @@ chi_levy_plot_1 <- ) + theme_minimal(base_size = 12) ``` +

-Next, we will plot the percent change in levy from 2006 for the City of Chicago, Chicago Public Schools, and Chicago Park District, and compare that to the percent change in CPI-U over the same period. - -We can see that the City of Chicago's levy has increased the most, having more than doubled from 2006 to 2024. Both Chicago and CPS's levies have increased far beyond the rate of inflation, while the Chicago Park District's levy has kept pace with it. +We can see that the City of Chicago's levy has increased the most, having more than doubled from 2006 to 2024. Both Chicago and CPS's levies have increased far beyond the rate of inflation. ```{r, echo=FALSE, out.width="100%"} chi_levy_plot_1 ``` -Even though the City's levy has increased the most in percentage terms since 2006, it does not have the largest levy. The taxing agencies that rely most on property taxes are school districts and this is true for Chicago as well. The plot below shows the final tax extensions (meaning the final amount to be received by taxing agencies after the levies are validated by PTELL), for each of the three agencies. CPS is slightly more than double the City's extension in 2024 at about $4 Billion. Chicago Park District's extension is substantially less at less than 1 Billion. +Even though the City's levy has increased the most in percentage terms since 2006, it does not have the largest levy. The taxing agencies that rely most on property taxes are school districts and this is true for Chicago as well. The plot below shows the final tax extensions (meaning the final amount to be received by taxing agencies after the levies are validated by PTELL), for each of the three agencies. CPS is slightly more than double the City's extension in 2024 at about \$4 Billion. Chicago Park District's extension is substantially less at less than 1 Billion. + +
Click here to show plot code + ```{r} -plot_2_df <- chi_levy %>% +plot_2_df <- chi_agencies %>% left_join(highlight_key) df_lab <- plot_2_df %>% @@ -288,6 +273,7 @@ chi_levy_plot_2 <- ggplot(plot_2_df, aes(year, total_ext, group = agency_num)) + labs(x = "Year", y = "Final Agency Extension") + theme_minimal(base_size = 12) ``` +

@@ -295,11 +281,16 @@ chi_levy_plot_2 <- ggplot(plot_2_df, aes(year, total_ext, group = agency_num)) + ```{r, echo=FALSE, out.width="100%"} chi_levy_plot_2 ``` -The PTAXSIM database also contains information related to taxing agency's property tax funds so we can understand exactly what they intend to spend their property tax revenue on. This information can be found in the `agency_fund` and `agency_fund_info` tables. This data is not utilized by any of PTAXSIM's functions, but it is available to be queried an analyzed. -We will query the fund information for the `CITY OF CHICAGO` and the `CITY OF CHICAGO LIBRARY FUND`. +## Agency fund data updates and query demo + +The PTAXSIM database also contains information related to taxing agency's property tax funds so we can understand in greater detail what they intend to spend their property tax revenue on. This information can be found in the `agency_fund` and `agency_fund_info` tables. This data is not utilized by any of PTAXSIM's functions, but it is available to be queried an analyzed. + +In 2024, the Cook County Clerk's agency fund identifier keys were updated, now showing a greater level of detail than in prior years. You can learn more about this change and how we account for it in the PTAXSIM database by reading out 2024 update [changelog](https://ccao-data.github.io/ptaxsim/changelog). -To simplify the `agency_fund` data, we will add broader categories to define funds. For example, there is not a consistent identifier for which funds are contributions to the City's various pension funds. In this case, we'll tag any fund with "A & B" (Annuities and Benefits) in the name as a pension fund. We'll label funds related to bond and interest payments, as well as note redemption, as "Bond Payments". +To demonstrate working with the agency fund data, we will query the fund information for the `CITY OF CHICAGO` and the `CITY OF CHICAGO LIBRARY FUND`. + +The level of detail provided in`fund_name` varies across years and agencies which can make analysis or plotting the data tricky. To simplify `agency_fund` data further, we opted below to add broader categories to define funds. In this case, we'll tag any fund with "A & B" (Annuities and Benefits) in the name as a pension fund. We'll label funds related to bond and interest payments, as well as note redemption, as "Bond Payments". ```{r} chi_agency_fund <- DBI::dbGetQuery( @@ -324,7 +315,9 @@ chi_agency_fund_info <- DBI::dbGetQuery( chi_agency_fund <- chi_agency_fund %>% left_join(chi_agency_fund_info) %>% + # Remove funds that levy equal to 0 filter(final_levy > 0, + # Remove fund that only existed in 2006 fund_type_num != "319") %>% mutate( fund_catg = @@ -342,12 +335,12 @@ chi_agency_fund <- chi_agency_fund %>% final_rate = sum(final_rate)) ``` - -The plot below +The plot below illustrates how the City's levy increase in 2016 was driven by a massive increase in the City's pension contributions. Pension contributions grew again in 2021, but in tandem with a decrease in bond financing.
Click here to show plot code + ```{r} chi_levy_plot_3 <- chi_agency_fund %>% ggplot() + @@ -377,16 +370,16 @@ chi_levy_plot_3 <- chi_agency_fund %>% legend.position = "bottom" ) ``` +

-The plot below illustrates how the City's levy increase in 2016 was by soley driven by a massive increase in the City's pension contributions. Pension contributions grew again in 2021, but in tandem with a decrease in bond financing. - ```{r, echo=FALSE, out.width="100%"} chi_levy_plot_3 ``` -With so much of the City's property tax revenue going to funding pensions, it's difficult to see if or how there was any change to the amount going to the Library Fund. The below tables show the final levy for each fund type in 2020-2024, as well as the share of the total levy for each fund type by year. + +With so much of the City's property tax revenue going to funding pensions, it's difficult to see if or how there was any change to the amount going to the Library Fund. The below tables show the final levy for each fund type in 2020-2024, as well as the share of the total levy for each fund type by year. ```{r, echo = FALSE} fund_type_table <- chi_agency_fund %>% @@ -418,6 +411,7 @@ fund_type_table %>% digits = 0 ) ``` +We can also break down the total levy by share allocated to each fund. While the City's property tax levy has grown each year, the portion allocated to the Library Fund has remained steady. ```{r, echo = FALSE} fund_type_table %>% mutate(across(c("2020", "2021", "2022", "2023", "2024"), @@ -444,4 +438,4 @@ fund_type_table %>% suffix = "%" ) ``` - +The code provided in this vignette are very simple tutorials on how to query taxing agency data from the PTAXSIM database. We hope access to a free and open data source that aggregates data from multiple disparate sources for the first time enables rigorous analysis of the Cook County property tax system! From 4a2b3a108d9a49efab566866f7ab1bb3bd10d838 Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Tue, 21 Apr 2026 21:16:30 +0000 Subject: [PATCH 08/18] Remove ptaxsim db --- vignettes/agencies.Rmd | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 477e10e..859199f 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -33,16 +33,14 @@ library(here) library(ggplot2) library(ptaxsim) library(tidyr) - -ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.2024.0.0-alpha.2.db")) ``` ```{r, echo=FALSE} # This is needed to build the vignette using GitHub Actions -#ptaxsim_db_conn <- DBI::dbConnect( -# RSQLite::SQLite(), -# Sys.getenv("PTAXSIM_DB_PATH") -#) +ptaxsim_db_conn <- DBI::dbConnect( + RSQLite::SQLite(), + Sys.getenv("PTAXSIM_DB_PATH") +) ``` ## Accounting for 2024 changes to agency fund reporting From ca32b0ae23dd4f8ee47f64447cdf601dd02e4907 Mon Sep 17 00:00:00 2001 From: Kyra Sturgill <49281603+kyrasturgill@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:13:08 -0500 Subject: [PATCH 09/18] Update vignettes/agencies.Rmd Co-authored-by: Jean Cochrane --- vignettes/agencies.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 859199f..cc3e732 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -1,5 +1,5 @@ --- -title: "Keeping tabs on taxing agencies" +title: "Tracking taxing agency revenue over time" output: html_document --- From 3a6b945eff0ecaee332fa74fda07b19422853d0f Mon Sep 17 00:00:00 2001 From: Kyra Sturgill <49281603+kyrasturgill@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:13:33 -0500 Subject: [PATCH 10/18] Update vignettes/agencies.Rmd Co-authored-by: Jean Cochrane --- vignettes/agencies.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index cc3e732..5198324 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -17,7 +17,7 @@ One of the unique features of PTAXSIM is the database that accompanies the packa In this vignette, we'll demonstrate how to query data from the PTAXSIM SQLite database that can help us analyze taxing agencies' property tax revenue over time. -Additionally, we'll show how to account for the 2024 changes to the Cook County Clerk's agency data structure which will be necessary for users to account for when conducting a time series analysis of certain taxing agencies. +Additionally, we'll show how to account for the 2024 changes to the Cook County Clerk's agency data structure, which is necessary when conducting a time series analysis of taxing agencies before and after 2024. # Chicago taxing agencies From 37526dd4ca2c77e2f9929edd71052d637fd7529a Mon Sep 17 00:00:00 2001 From: Kyra Sturgill <49281603+kyrasturgill@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:14:26 -0500 Subject: [PATCH 11/18] Update vignettes/agencies.Rmd Co-authored-by: Jean Cochrane --- vignettes/agencies.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 5198324..d783551 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -23,7 +23,7 @@ Additionally, we'll show how to account for the 2024 changes to the Cook County Using data from the PTAXSIM database, let's look at the levy history for three of the primary taxing agencies that appear on a Chicago property's tax bill: the `CITY OF CHICAGO`, `BOARD OF EDUCATION` (a.k.a. Chicago Public Schools), and `CHICAGO PARK DISTRICT`. -First, load some useful libraries and instantiate a PTAXSIM DBI connection with the default name (`ptaxsim_db_conn`). +First, load some useful libraries and instantiate a PTAXSIM DBI connection with the variable name `ptaxsim_db_conn`. ```{r} library(data.table) From b7bfdc58f68ca2a1211b9c3ee7d4c90415a3c6ff Mon Sep 17 00:00:00 2001 From: Kyra Sturgill <49281603+kyrasturgill@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:43:31 -0500 Subject: [PATCH 12/18] Apply suggestions from code review Co-authored-by: Jean Cochrane --- vignettes/agencies.Rmd | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index d783551..69db3c6 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -91,15 +91,14 @@ chi_agencies <- DBI::dbGetQuery( " SELECT DISTINCT * FROM agency - WHERE agency_num = '030210000' - OR agency_num = '030210001' - OR agency_num = '050200000' - OR agency_num = '044060000' + WHERE agency_num IN ('030210000', '030210001', '050200000', '044060000') " ) ``` -Before we do anything, we need to fold the `CITY OF CHICAGO LIBRARY FUND` into the `CITY OF CHICAGO` levy total for all years prior to 2024. We can do this by joining `agency_cw_24` by `agency_num` to `chi_agencies` and replacing the old `agency_num` with `agency_num_24`, then grouping by agency and year and then summing the fields `total_final_levy` and `total_ext`. *Terminology note*: The **levy** is the amount of money a local government budgets for and requests to receive from property taxes. Many taxing agencies are subject to limits imposed by State law. The **extension** (called `final_ext` in the database) is the total, final amount a taxing body is allowed to receive which is calculated and confirmed by the Cook County Clerk. +Before we do anything, we need to fold the `CITY OF CHICAGO LIBRARY FUND` into the `CITY OF CHICAGO` levy total for all years prior to 2024. We can do this by joining `agency_cw_24` by `agency_num` to `chi_agencies` and replacing the old `agency_num` with `agency_num_24`, then grouping by agency and year and then summing the fields `total_final_levy` and `total_ext`.[^1] + +[^1]: *Terminology note*: The **levy** is the amount of money a local government budgets for and requests to receive from property taxes. Many taxing agencies are subject to limits imposed by State law. The **extension** (called `final_ext` in the database) is the total, final amount a taxing body is allowed to receive which is calculated and confirmed by the Cook County Clerk. ```{r} chi_agencies <- chi_agencies %>% @@ -123,9 +122,9 @@ chi_agencies <- chi_agencies %>% Now that we have the correct total levies for the City of Chicago, Chicago Public Schools and Chicago Park District across all years, we can look at how those levies have changed from 2006 to 2024. -CPS and CPKD are both subject to PTELL (Property Tax Extension Law Limit), which ensures certain taxing agencies do not increase their levies beyond the rate of inflation (with some exceptions\*). The City of Chicago is not by virtue of being a non-home rule agency. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's and prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. +CPS and CPKD are both subject to PTELL (Property Tax Extension Law Limit), which ensures certain taxing agencies do not increase their levies beyond the rate of inflation (with some exceptions[^2]). The City of Chicago is not by virtue of being a non-home rule agency. However, the City of Chicago imposes [its own limits](https://codelibrary.amlegal.com/codes/chicago/latest/chicago_il/0-0-0-2608573) which mirror PTELL's and prohibit a taxing agency from increasing its levy more than the rate of inflation or 5%, whichever is less. -\*PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to inflation since 2006. +[^2]: PTELL, as well as Chicago's property tax limitation ordinance, contain loopholes that allow taxing agencies to [increase their levies beyond the rate of inflation](https://civicfed.org/press/new-report-finds-illinois-property-tax-cap-law-not-working-intended). Let's see how Chicago's levy has fared compared to inflation since 2006. To do so, we'll calculate the rate at which the levies have grown compared to the [CPI-U](https://www.bls.gov/cpi/). Fortunately CPI data, as reported by the Illinois Department of Revenue (IDOR) for purpose of PTELL calculatios, is available in the PTAXSIM data base in the `cpi` table. @@ -284,11 +283,11 @@ chi_levy_plot_2 The PTAXSIM database also contains information related to taxing agency's property tax funds so we can understand in greater detail what they intend to spend their property tax revenue on. This information can be found in the `agency_fund` and `agency_fund_info` tables. This data is not utilized by any of PTAXSIM's functions, but it is available to be queried an analyzed. -In 2024, the Cook County Clerk's agency fund identifier keys were updated, now showing a greater level of detail than in prior years. You can learn more about this change and how we account for it in the PTAXSIM database by reading out 2024 update [changelog](https://ccao-data.github.io/ptaxsim/changelog). +In 2024, the Cook County Clerk's agency fund identifier keys were updated, now showing a greater level of detail than in prior years. You can learn more about this change and how we account for it in the PTAXSIM database by reading out 2024 update [changelog](https://ccao-data.github.io/ptaxsim/news/). To demonstrate working with the agency fund data, we will query the fund information for the `CITY OF CHICAGO` and the `CITY OF CHICAGO LIBRARY FUND`. -The level of detail provided in`fund_name` varies across years and agencies which can make analysis or plotting the data tricky. To simplify `agency_fund` data further, we opted below to add broader categories to define funds. In this case, we'll tag any fund with "A & B" (Annuities and Benefits) in the name as a pension fund. We'll label funds related to bond and interest payments, as well as note redemption, as "Bond Payments". +The level of detail provided in `fund_name` varies across years and agencies which can make analysis or plotting the data tricky. To simplify `agency_fund` data further, we opted below to add broader categories to define funds. In this case, we'll tag any fund with "A & B" (Annuities and Benefits) in the name as a pension fund. We'll label funds related to bond and interest payments, as well as note redemption, as "Bond Payments". ```{r} chi_agency_fund <- DBI::dbGetQuery( From da50b07aa0fc79856def2233f4a7c394e2c45d2e Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Wed, 22 Apr 2026 16:56:22 -0500 Subject: [PATCH 13/18] Add ptaxsim_db_conn back in --- vignettes/agencies.Rmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 69db3c6..97b72db 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -33,6 +33,8 @@ library(here) library(ggplot2) library(ptaxsim) library(tidyr) + +ptaxsim_db_conn <- DBI::dbConnect(RSQLite::SQLite(), here("./ptaxsim.db")) ``` ```{r, echo=FALSE} From 6371aef80d0f36020af40615e13e2b0ab843a42a Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Wed, 22 Apr 2026 17:05:15 -0500 Subject: [PATCH 14/18] Style and linting fixes --- vignettes/agencies.Rmd | 98 ++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 97b72db..bf60574 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -111,13 +111,15 @@ chi_agencies <- chi_agencies %>% mutate( agency_num = ifelse(!is.na(agency_num_24), - agency_num_24, - agency_num)) %>% + agency_num_24, + agency_num + ) + ) %>% group_by(year, agency_num) %>% summarize( total_final_levy = sum(total_final_levy), total_ext = sum(total_ext) -) + ) ``` ## Tracking Chicago, CPS, and CPKD levies @@ -134,8 +136,10 @@ To do so, we'll calculate the rate at which the levies have grown compared to th # Calculate the levy percent change for each agency indexed to 2006 chi_levy_indx <- chi_agencies %>% group_by(agency_num) %>% - mutate(levy_2006 = total_final_levy[year == 2006][1], - levy_pct_inc = (total_final_levy - levy_2006) / levy_2006 * 100) %>% + mutate( + levy_2006 = total_final_levy[year == 2006][1], + levy_pct_inc = (total_final_levy - levy_2006) / levy_2006 * 100 + ) %>% ungroup() %>% select(agency_num, year, levy_pct_inc) %>% pivot_longer( @@ -170,38 +174,41 @@ We'll then plot the rate of change for each levy compared to inflation. ```{r} highlight_key <- tibble::tibble( agency_num = c("100", "050200000", "030210000", "044060000"), - label = c("CPI-U", "CPKD", "City", "CPS") + label = c("CPI-U", "CPKD", "City", "CPS") ) plot_1_df <- rbind(cpi, chi_levy_indx) %>% left_join(highlight_key, by = "agency_num") - + df_lab <- plot_1_df %>% group_by(agency_num, label) %>% filter(year == max(year, na.rm = TRUE)) %>% - slice_tail(n = 1) %>% + slice_tail(n = 1) %>% ungroup() %>% - mutate(label_col = ifelse(label == "CPI-U", "#d62728", "black")) # red for CPI-U + mutate(label_col = ifelse(label == "CPI-U", "#d62728", "black")) -chi_levy_plot_1 <- +chi_levy_plot_1 <- ggplot() + geom_line( data = plot_1_df, - aes(x = year, y = pct_inc/100, group = agency_num, color = label), + aes(x = year, y = pct_inc / 100, group = agency_num, color = label), linewidth = .5 ) + # right-side labels in black (no color mapping) geom_text( data = df_lab, - aes(x = year, y = pct_inc/100, label = label), + aes(x = year, y = pct_inc / 100, label = label), color = df_lab$label_col, hjust = -0.1, size = 3.5 ) + scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + scale_x_continuous( - breaks = seq(min(plot_1_df$year, na.rm = TRUE), max(plot_1_df$year, na.rm = TRUE), by = 2), - expand = expansion(mult = c(0.01, 0.08)) # add right padding so labels fit + breaks = seq(min(plot_1_df$year, na.rm = TRUE), + max(plot_1_df$year, na.rm = TRUE), + by = 2 + ), + expand = expansion(mult = c(0.01, 0.08)) # add right padding so labels fit ) + scale_color_manual( values = c( @@ -211,7 +218,7 @@ chi_levy_plot_1 <- "CPI-U" = "#d62728" ), breaks = c("CPKD", "City", "CPS"), - guide = "none" # hide legend since lines are labeled + guide = "none" # hide legend since lines are labeled ) + labs( x = "Year", @@ -248,10 +255,11 @@ df_lab <- plot_2_df %>% df_2024 <- plot_2_df %>% filter(year == 2024) %>% group_by(agency_num, label) %>% - slice_tail(n = 1) %>% # in case there are duplicates in 2024 + slice_tail(n = 1) %>% # in case there are duplicates in 2024 ungroup() %>% mutate(label_2024 = paste0( - label, ": ", scales::label_dollar(scale = 1e-9, suffix = "B", accuracy = 0.1)(total_ext) + label, ": ", + scales::label_dollar(scale = 1e-9, suffix = "B", accuracy = 0.1)(total_ext) )) chi_levy_plot_2 <- ggplot(plot_2_df, aes(year, total_ext, group = agency_num)) + @@ -266,7 +274,10 @@ chi_levy_plot_2 <- ggplot(plot_2_df, aes(year, total_ext, group = agency_num)) + ) + scale_y_continuous(labels = scales::label_dollar(scale = 1e-9, suffix = "B")) + scale_x_continuous( - breaks = seq(min(plot_2_df$year, na.rm = TRUE), max(plot_2_df$year, na.rm = TRUE), by = 2), + breaks = seq(min(plot_2_df$year, na.rm = TRUE), + max(plot_2_df$year, na.rm = TRUE), + by = 2 + ), expand = expansion(mult = c(0.01, 0.08)) ) + labs(x = "Year", y = "Final Agency Extension") + @@ -315,24 +326,29 @@ chi_agency_fund_info <- DBI::dbGetQuery( chi_agency_fund <- chi_agency_fund %>% left_join(chi_agency_fund_info) %>% # Remove funds that levy equal to 0 - filter(final_levy > 0, - # Remove fund that only existed in 2006 - fund_type_num != "319") %>% + filter( + final_levy > 0, + # Remove fund that only existed in 2006 + fund_type_num != "319" + ) %>% mutate( fund_catg = - case_when(grepl("A & B", fund_name) ~ "Pension", - grepl("NOTE REDEMPTION & INTEREST FUND", fund_name) ~ - "Bond Payments", - grepl("BONDS & INTEREST", fund_name) ~ "Bond Payments", - grepl("LIBRARY NOTE REDEMPTION", fund_name) ~ "Library Fund", - grepl("BONDS & INTEREST", fund_type_name) ~ "Bond Payments", - grepl("LIBRARY NOTE REDEMPTION", fund_type_name) ~ "Library Fund", - TRUE ~ fund_type_name) + case_when( + grepl("A & B", fund_name) ~ "Pension", + grepl("NOTE REDEMPTION & INTEREST FUND", fund_name) ~ + "Bond Payments", + grepl("BONDS & INTEREST", fund_name) ~ "Bond Payments", + grepl("LIBRARY NOTE REDEMPTION", fund_name) ~ "Library Fund", + grepl("BONDS & INTEREST", fund_type_name) ~ "Bond Payments", + grepl("LIBRARY NOTE REDEMPTION", fund_type_name) ~ "Library Fund", + TRUE ~ fund_type_name + ) ) %>% group_by(year, fund_catg) %>% - summarise(final_levy = sum(final_levy), - final_rate = sum(final_rate)) - + summarise( + final_levy = sum(final_levy), + final_rate = sum(final_rate) + ) ``` The plot below illustrates how the City's levy increase in 2016 was driven by a massive increase in the City's pension contributions. Pension contributions grew again in 2021, but in tandem with a decrease in bond financing. @@ -343,8 +359,9 @@ The plot below illustrates how the City's levy increase in 2016 was driven by a ```{r} chi_levy_plot_3 <- chi_agency_fund %>% ggplot() + - geom_line(aes(x = year, y = final_levy, color = fund_catg), - linewidth = .5) + + geom_line(aes(x = year, y = final_levy, color = fund_catg), + linewidth = .5 + ) + geom_point(aes(x = year, y = final_levy, color = fund_catg)) + scale_x_continuous(n.breaks = 10) + scale_y_continuous( @@ -389,9 +406,12 @@ fund_type_table <- chi_agency_fund %>% fund_type_table %>% arrange(desc(`2024`)) %>% bind_rows( - summarise(., fund_catg = "Total Levy", across(c("2020", "2021", "2022", "2023", "2024"), sum)) + summarise(., + fund_catg = "Total Levy", + across(c("2020", "2021", "2022", "2023", "2024"), sum) + ) ) %>% - rename(`Fund Type` = fund_catg) %>% + rename(`Fund Type` = fund_catg) %>% datatable( caption = "Chicago Property Tax Levies by Fund Type", rownames = FALSE, @@ -413,8 +433,10 @@ fund_type_table %>% We can also break down the total levy by share allocated to each fund. While the City's property tax levy has grown each year, the portion allocated to the Library Fund has remained steady. ```{r, echo = FALSE} fund_type_table %>% - mutate(across(c("2020", "2021", "2022", "2023", "2024"), - ~ round(. / sum(.) * 100))) %>% + mutate(across( + c("2020", "2021", "2022", "2023", "2024"), + ~ round(. / sum(.) * 100) + )) %>% arrange(desc(`2024`)) %>% datatable( caption = "Chicago Property Tax Levies by Fund Type — Share by Year", From 75c652ce5a6a506dcf6e7bd742815a8f52e64aad Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Wed, 22 Apr 2026 17:46:20 -0500 Subject: [PATCH 15/18] More style clean up --- vignettes/agencies.Rmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index bf60574..025ee9f 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -272,7 +272,8 @@ chi_levy_plot_2 <- ggplot(plot_2_df, aes(year, total_ext, group = agency_num)) + hjust = .6, vjust = -.5 ) + - scale_y_continuous(labels = scales::label_dollar(scale = 1e-9, suffix = "B")) + + scale_y_continuous(labels = scales::label_dollar(scale = 1e-9, suffix = "B") + ) + scale_x_continuous( breaks = seq(min(plot_2_df$year, na.rm = TRUE), max(plot_2_df$year, na.rm = TRUE), From 89c545cb73b28999f7e0c9148244f162051b68de Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Wed, 22 Apr 2026 22:51:22 +0000 Subject: [PATCH 16/18] Remove agency_change_24 filter --- vignettes/agencies.Rmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index 025ee9f..e64d189 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -56,12 +56,12 @@ agency_cw_24 <- DBI::dbGetQuery( " SELECT * FROM agency_info - WHERE agency_change_24 = TRUE " ) %>% select(agency_num, agency_name, agency_num_24, agency_name_24) -datatable(agency_cw_24) +datatable(agency_cw_24 %>% + filter(!is.na(agency_num_24))) ``` From this table it appears that the updated agencies are specific funds for various municipalities - the Clerk had previously reported certain municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agency data over time. From 4abacb37119574ca191d1ed91109a436b74b88fe Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Wed, 22 Apr 2026 22:58:42 +0000 Subject: [PATCH 17/18] Final pre-commit fixes --- vignettes/agencies.Rmd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index e64d189..db700d5 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -61,7 +61,7 @@ agency_cw_24 <- DBI::dbGetQuery( select(agency_num, agency_name, agency_num_24, agency_name_24) datatable(agency_cw_24 %>% - filter(!is.na(agency_num_24))) + filter(!is.na(agency_num_24))) ``` From this table it appears that the updated agencies are specific funds for various municipalities - the Clerk had previously reported certain municipal fund levies as separate taxing agencies. This updated data structure begins in 2024 while years prior to 2024 remain the same, meaning users will need to account for this discrepancy if ever analyzing these taxing agency data over time. @@ -272,8 +272,10 @@ chi_levy_plot_2 <- ggplot(plot_2_df, aes(year, total_ext, group = agency_num)) + hjust = .6, vjust = -.5 ) + - scale_y_continuous(labels = scales::label_dollar(scale = 1e-9, suffix = "B") - ) + + scale_y_continuous(labels = scales::label_dollar( + scale = 1e-9, + suffix = "B" + )) + scale_x_continuous( breaks = seq(min(plot_2_df$year, na.rm = TRUE), max(plot_2_df$year, na.rm = TRUE), From 10e4686208ad972efb137b41cfa98310bbd23e9f Mon Sep 17 00:00:00 2001 From: kyrasturgill Date: Tue, 28 Apr 2026 19:52:06 +0000 Subject: [PATCH 18/18] Remove reference to package that is not listed in dependencies --- vignettes/agencies.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd index db700d5..22b63c4 100644 --- a/vignettes/agencies.Rmd +++ b/vignettes/agencies.Rmd @@ -172,7 +172,7 @@ We'll then plot the rate of change for each levy compared to inflation. Click here to show plot code ```{r} -highlight_key <- tibble::tibble( +highlight_key <- tibble( agency_num = c("100", "050200000", "030210000", "044060000"), label = c("CPI-U", "CPKD", "City", "CPS") )