diff --git a/vignettes/agencies.Rmd b/vignettes/agencies.Rmd new file mode 100644 index 0000000..22b63c4 --- /dev/null +++ b/vignettes/agencies.Rmd @@ -0,0 +1,465 @@ +--- +title: "Tracking taxing agency revenue over time" +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. + +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 + +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 variable name `ptaxsim_db_conn`. + +```{r} +library(data.table) +library(dplyr) +library(DT) +library(here) +library(ggplot2) +library(ptaxsim) +library(tidyr) + +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") +) +``` + +## 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 the 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 + " +) %>% + select(agency_num, agency_name, agency_num_24, agency_name_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. + +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 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. + +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( + ptaxsim_db_conn, + " + SELECT agency_num, agency_name + FROM agency_info + WHERE agency_name LIKE '%CHICAGO%' + " +) + +datatable(chi_agency_nums) +``` + +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( + ptaxsim_db_conn, + " + SELECT DISTINCT * + FROM agency + 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`.[^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 %>% + # 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 + ) + ) %>% + group_by(year, agency_num) %>% + summarize( + total_final_levy = sum(total_final_levy), + total_ext = sum(total_ext) + ) +``` + +## Tracking Chicago, CPS, and CPKD levies + +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[^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. + +[^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. + +```{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 + ) %>% + ungroup() %>% + select(agency_num, year, levy_pct_inc) %>% + pivot_longer( + cols = levy_pct_inc, + names_to = "series", + values_to = "pct_inc" + ) + +# Query CPI data from PTAXSIM db, calculate percent change indexed to 2006 +cpi <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT * + FROM cpi + " +) %>% + mutate( + pct_inc = (cpi / cpi[year == 2006] - 1) * 100, + agency_num = "100", + series = "cpi" + ) %>% + filter(year >= 2006) %>% + 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( + 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")) + +chi_levy_plot_1 <- + ggplot() + + geom_line( + data = plot_1_df, + 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), + 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 = "Year", + y = "Percent change from 2006", + ) + + theme_minimal(base_size = 12) +``` + +
+ +
+ +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. + +
+ +Click here to show plot code + +```{r} +plot_2_df <- chi_agencies %>% + 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 + ) + + 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) +``` + +
+ +
+ +```{r, echo=FALSE, out.width="100%"} +chi_levy_plot_2 +``` + +## 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/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". + +```{r} +chi_agency_fund <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT * + FROM agency_fund + WHERE agency_num = '030210000' + OR agency_num = '030210001' + " +) + +chi_agency_fund_info <- DBI::dbGetQuery( + ptaxsim_db_conn, + " + SELECT * + FROM agency_fund_info + WHERE agency_num = '030210000' + OR agency_num = '030210001' + " +) + +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 = + 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) + ) +``` +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() + + 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( + labels = scales::label_dollar(scale = 1e-9, suffix = "B"), + limits = c(0, NA) + ) + + labs( + x = NULL, + y = "Final Levy (Billions)", + color = NULL + ) + + 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, 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, echo = FALSE} +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 + ) +``` +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) + )) %>% + 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 = "%" + ) +``` +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!