Analysis

Goals for this notebook

  1. Create an object containing only its most recent case number.
  2. Investigate citation numbers over time
    1. What is the year with the most citations?
    2. What is the month with the most citations?
    3. What is the average number of citations for each month since 2016?
  3. Investigate most common violation types
    1. What are the most common violation types of all time?
    2. What are the most common violation types by year?
    3. What are the most common violation types by month?
  4. Judgement and dispositions
    1. What are the most common judgments?
    2. What are the most common dispositions?
    3. How many guilty cases per year?
  5. Top people
    1. Who has received the most citations? How many people received multiple citations/fines?
      1. Who are the most heavily fined people?
      2. How much of those fines got dismissed?
      3. How much did they pay?
      4. How much do they still have due?
      5. Who has paid the highest amount of dues?
    2. General fine statistics
    1. How many people still owe fines to the city?
    2. How many people have paid their fines?
    3. How many people have had their fines dismissed?
  6. Fines
    1. How many distinct people are there who were fined?
    2. How many people still owe fines to the city? How much is the city owed in total?
    3. How many people have paid some/all of their fines?
    4. How many people have had some/all of their fines dismissed?
  7. Jail time
    1. How many jail judgments have been given?
      1. How many individual people have been jailed?
      2. How many have been jailed more than once?
    2. What do monthly “CREDIT TIME SERVED” judgment percentages look like from January 2016 to November 2025?
  8. Individual people analysis
    1. Kevin P Shakesnider received the highest jail judgments and fines.
    2. Patricia Norris has the highest citation count.
  9. Mapping analysis
    1. Which zone has had the most citations in this data set?
    2. How did zone citation counts and percentages change by year?
    3. How did zone citation counts percentages change by month?

Set up

Expand this to see code
library(tidyverse)
library(janitor)
library(sf)
library(mapview)
library(ggspatial)
library(tigris)
library(mapview)
library(plotly)
library(DT)

Importing

Expand this to see code
unhoused <- read_csv("data-processed/unhoused-clean.csv")
# unhoused |> glimpse()

1. Creating distinct object

Right now, some rows have duplicate case numbers because the second appearance tracks how the case developed. Creating an object that does not have duplicate rows will ensure each row represents an individual citation.

Note

The object will only contain the second appearance because that provides the most updated data.

Expand this to see code
unhoused_distinct <- unhoused |> 
  group_by(case_number) |>  
  arrange(desc(judgment_date)) |> 
  slice(1) |>
  ungroup() |> 
  mutate(off_mo_label = factor(off_mo_label, levels = month.abb, ordered = TRUE), # something changed the mo_label columns from <ord> to <chr>, this fixes that
         jud_mo_label = factor(jud_mo_label, levels = month.abb, ordered = TRUE)
        )

# unhoused_distinct |> glimpse()
unhoused_distinct |> nrow()
[1] 30174

2. Citation numbers over time

Looking at how individual citation numbers have changed over time.

2.1. Year with most citations

What is the year with the most citations?

2.1.1 Making object

Expand this to see code
unhoused_yearly <- unhoused_distinct |> # unhoused_distinct ensures every row counts as an individual citation
  group_by(off_floor_yr, off_yr, whitmire_offense) |> 
  summarize(yearly_citations = n(), .groups = "drop") |> 
  arrange(off_floor_yr) 

unhoused_yearly

2.1.2 Plotting

Plotting yearly citations as an interactive bar chart.

Expand this to see code
yearly_plot <- ggplot(
  unhoused_yearly,
  aes(x = off_floor_yr, 
      y = yearly_citations),
  ) +
  geom_col(fill = "#4E79A7") +
  labs(
    title = "Yearly citations fluctuate from 2016 to 2025",
    subtitle = str_wrap("2019 to 2020 showed a 176% increase in citations per year. 2022 had 2007 citations, which showed a roughly 60% decrease from 2020 to 2022."),
    caption = "By Layla Dajani",
    x = "Year",
    y = "Citations per year"
  ) +
  theme_minimal()

yearly_plot |> 
  ggplotly()

2.1.3 Data takeaways

  • The year with the highest number of citations is 2016 with 5800, followed by 2020 with 5011. The year with the lowest number of citations is 2018, at 1385.
  • 2019 had 1815 citations and 2020 had 5011 – that is roughly a 176% increase in one year. 2022 had 2007 citations, which is a roughly 60% decrease from 2020 to 2022.
  • 2025 had the highest citation count since 2021.

2.2 Monthly citations

2.2.1 Jul. 2023 to Dec. 2025

How did the monthly citation count change between Jul. 2023 to Dec. 2025?

2.2.1.1 Making object
Expand this to see code
monthly_citations <- unhoused_distinct |> 
  filter(offense_date >= "2023-07-01") |>  # looking 6 months before Whitmire's term started to see if there is an uptick in citations after Jan. 2024.
  group_by(off_floor_mo, whitmire_offense) |> # adding Whitmire offense for plotting
  summarise(
    monthly_citations = n(),
    .groups = "drop") |> 
    arrange(off_floor_mo)

monthly_citations
2.2.1.2 Plotting

Plotting a bar chart to show the monthly citation count between Jul. 2023 and Dec. 2025.

About the chart
  • The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.
Expand this to see code
monthly_citations_plot <- ggplot(monthly_citations, 
       aes(x = off_floor_mo, 
           y = monthly_citations, 
           fill = whitmire_offense)) +
  geom_col() +
  geom_vline(xintercept = ymd("2024-01-01"), 
             linetype = "dashed", 
             color = "grey40") +
  geom_vline(xintercept = ymd("2025-07-01"),
             linetype = "dashed", 
             color = "grey40") +
  scale_fill_manual(
    name = "Before/after Whitmire",
    values = c(
      "BEFORE WHITMIRE" = "#4E79A7", 
      "AFTER WHITMIRE"  = "#E15759"
  )) +
  labs(
    title = "May to October 2025 indicate an upward climb of monthly citation numbers",
    subtitle = str_wrap("From July 2023 to December 2025, October 2025 had the highest citation count at 490 citations. Monthly citations increased by about 463.2% between May and October 2025."),
    x = "Date",
    y = "Citations per month"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

monthly_citations_plot |> 
  ggplotly()
2.2.1.3 Data takeaways
  • From July 2023 to December 2025, October 2025 had the highest citation count at 492 citations. This spike is the peak of a monthly increase starting in May 2025. Monthly citations increased by about 463.2% between May and October 2025.

2.2.2 Jan. 2016 to Dec. 2025

2.2.2.1 Making object
Expand this to see code
monthly_citations_all <- unhoused_distinct |> 
  group_by(off_floor_mo, whitmire_offense) |> # adding whitmire_offense for plotting
  summarise(
    yr_mo_off = n(),
    .groups = "drop") |>
  arrange(off_floor_mo)

monthly_citations_all
2.2.2.2 Plotting

Plotting a line graph to show a monthly citation count between January 2016 and December 2025.

About this chart

The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.

Expand this to see code
monthly_citations_all_plot <- ggplot(
  monthly_citations_all, 
  aes(x = off_floor_mo, 
      y = yr_mo_off, 
      color = whitmire_offense)) +
  geom_line(linewidth = 0.9) +
  geom_point(size = 0.8) +
  geom_vline(xintercept = ymd("2024-01-01"),
             linetype = "dashed", 
             color = "grey40") +
  geom_vline(xintercept = ymd("2025-07-01"),
             linetype = "dashed", 
             color = "grey40") +
  scale_color_manual(
    values = c(
      "BEFORE WHITMIRE" = "#4E79A7", 
      "AFTER WHITMIRE"  = "#E15759")
    ) +
  labs(
    title = str_wrap("October 2025 was the month with the highest citation count in over four years"),
    subtitle = str_wrap("The first dotted line indicates when Whitmire's term began, and the second indicates when he introduced new violation codes."),
    x = "Year",
    y = "Citations per month",
    color = ""
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

ggsave("figures/monthly_citations_all_plot.png")

monthly_citations_all_plot |> 
  ggplotly()
2.2.2.3 Data takeaways
  • While October 2025 showed the highest monthly citation count in four years at 492 citations, citation counts peaked even higher in January 2021, at 797 citations, and April 2016, with 715 citations.
  • Monthly citation counts sunk between July 2017 and July 2019.

2.3 Avg monthly citation

What is the average citation count for each calendar month from 2016 to 2025?

2.3.1 Making object

Expand this to see code
mo_avg_citations <- unhoused_distinct |> 
  #filter(off_yr < 2025) |> # i'm including all of 2025, but note that this means Nov. and Dec. are not accurate averages.
  group_by(off_yr,
           off_floor_mo,
           off_mo_label) |> 
  summarise(yr_mo_off = n(),
           .groups = "drop") |> 
  group_by(off_mo_label) |> 
  summarise(avg_mo_off = round(mean(yr_mo_off), 1)) |> 
  arrange(off_mo_label)

mo_avg_citations

2.3.2 Plotting

Expand this to see code
mo_avg_citations_plot <- ggplot(
  mo_avg_citations,
  aes(x = off_mo_label, 
      y = avg_mo_off, 
      group = 1)
  ) +
  geom_col(fill = "#4E79A7") +
  labs(
    title = "Monthly citations averaged from January 2016 to December 2025",
    subtitle = str_wrap(""),
    caption = "Graphic by Layla Dajani",
    x = "Month",
    y = "Offenses per month",
  ) +
  theme_minimal()

mo_avg_citations_plot |> 
  ggplotly()

2.3.3 Data takeaways

  • January is the month that draws the most citations. But note that surges in specific years could be driving the averages higher in certain months. For instance, January 2016 and 2021 had exceptionally high citation counts, seen in figure 2.2.2.1.

2.4 Citations after expansion

Here we want to find the number of citations in the first half of 2026 (before the expansion to 24/7) and then in the later half.

Expand this to see code
unhoused_distinct |> 
  filter(off_yr == 2025) |> 
  mutate(expan_flag = if_else(off_mo < 7, "first half", "second half")) |> 
  count(expan_flag, name = "citations")

There were 1,021 citations from January through June 2025 and 1,955 citations from July through December 2025.

3. Violation types

3.1 Most common

Finding the most common violation types.

Expand this to see code
violation_count <- unhoused_distinct |> 
  group_by(violation_description_refined, violation_code) |> 
  summarise(violations = n(),
            .groups = "drop") |> 
  arrange(desc(violations))

violation_count

3.1.1 Data takeaways

There are 11 types of violations that have been given between January 2016 and December 2025. Here is what they mean and their frequency:

  1. CC753 has 33 citations, a street vendor ordinance
    1. Selling goods in public area: STREET VENDOR (EXPOSE FOR SALE) (SELL) GOODS, TO-WIT:…ON A PUBLIC (SIDEWALK) (STREET) (PROPERTY)
  2. CC921 has 493 citations, a sidewalk obstruction ordinance
    1. Sidewalk obstruction: OBSTRUCT SIDEWALK BY (PLACING/DEPOSITING/PERMIT PERSON UNDER CONTROL) OBJECT (BOX/MAT./VEH./ETC.)
  3. CC940 has 15,294 citations, a civility ordinance
    1. Sidewalk obstruction: SIT/LIE DOWN ON (BLANKET/STOOL) PLACED ON SIDEWALK B/W 7:00 AM AND 11:00 PM IN A PROHIBITED AREA
  4. CC941 has 13,777 citations, a civility ordinance
    1. Sidewalk obstruction: PLACE (BED MAT./PERSONAL POSS.) ON SIDEWALK B/W THE HRS OF 7:00 A.M. & 11:00P.M. IN A PROHIBITED AREA
  5. CC942 has 146 citations, a sidewalk obstruction ordinance
    1. Sidewalk obstruction: IMPAIR OR OBSTRUCT SIDEWALK WITHOUT A PERMIT
  6. CC943 has 16 citations, a sidewalk obstruction ordinance
    1. Sidewalk obstruction: IMPAIR OR OBSTRUCT SIDEWALK BEYOND SCOPE OF PERMIT
  7. CC948 has 373 citations, a sidewalk obstruction ordinance
    1. Sidewalk obstruction: PLACE OR ALLOW OBSTRUCTION ON SIDEWALK
  8. CC956 has 8 citations, a street vendor ordinance
    1. EXPOSE FOR SALE MERCHANDISE, TO-WIT:…ON A PUBLIC (SIDEWALK) (STREET) (ESPLANADE) (PROPERTY)
  9. CC2043 has 18 citations a civility ordinance
    1. (CIVILITY ORDINANCE) AT ANY TIME SIT/LIE DOWN UPON SIDEWALK W/IN CBD, EADO MGMT DIST, EAST END
  10. CC2044 has 15 citations a civility ordinance
    1. CIVILITY ORDINANCE) AT ANY TIME PLACE/DEPOSIT BEDDING OR PERSONAL POSSESSIONS ON SIDEWALK W/IN CBD, EADO
  11. CC20245 has 1 citation a civility ordinance
    1. (CIVILITY ORDINANCE) AT ANY TIME SIT/LIE DOWN UPON SIDEWALK W/IN OTHER DESIGNATED AREA SUBJECT TO RULES AND REGULATIONS FOR ENFORCEMENT
  • The most common citation between January 2016 and December 2025 is CC940 with 15,294 offenses. CC941 is the runner-up at 13,777 offenses. The third most common is CC291 with 943 offenses.

3.2 Violation codes by year

Plotting yearly violation code totals from January 2016 to December 2025.

3.2.1 Making object

Expand this to see code
unhoused_violations <- unhoused_distinct |> 
  group_by(off_floor_yr,
           violation_code)|> 
  summarise(violations = n(),
            .groups = "drop") |> 
  arrange(off_floor_yr)

# unhoused_violations |> glimpse()

3.2.2 Plotting

The first dotted line indicates when Whitmire’s term began.

Expand this to see code
unhoused_violations_plot <- ggplot(
  unhoused_violations, 
  aes(x = off_floor_yr, 
      y = violations, 
      color = violation_code)) +
  geom_line() +
  geom_point(size = 0.6) +
  geom_vline(xintercept = ymd("2024-01-01"),
             linetype = "dashed", 
             color = "grey70") +
  labs(
    title = str_wrap("Number of citations by year broken down by violation code from January 2016 to December 2025"),
    subtitle = "Violation codes CC753, CC942, CC943, CC948 and CC956 first appeared in 2025.",
    caption = "By Layla Dajani",
    x = "Year",
    y = "Citations per year"
  ) +
  theme_minimal()

unhoused_violations_plot |> 
  ggplotly()

3.2.1 Data takeaways

  • All codes except CC921, CC940 and CC941 first appeared in 2025.
  • CC940 and CC941 seem to follow very similar trends until 2025, when they appear to drastically diverge for the first time in this data set.

3.3 Violation codes by month

3.3.1 By count ’16 to ’25

Breaking down monthly citation counts by violation codes from January 2016 to December 2025.

3.3.1.1 Making object
Expand this to see code
types_by_month_16 <- unhoused_distinct |>
  group_by(off_floor_mo, 
           violation_code,
           ordinance_type) |>
  summarise(citation_count = n(), .groups = "drop") |>
  arrange(off_floor_mo)

types_by_month_16
3.3.1.2 Plotting
About this chart

The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.

Expand this to see code
types_by_month_16_plot <- ggplot(
  types_by_month_16,
  aes(x = off_floor_mo, 
      y = citation_count, 
      color = violation_code)) +
  geom_line() +
  geom_point(size = 0.4) +
  geom_vline(xintercept = ymd("2024-01-01"),
             linetype = "dashed", 
             color = "grey70") +
  geom_vline(xintercept = ymd("2025-07-01"),
             linetype = "dashed", 
             color = "grey70") +
  labs(
    title = str_wrap("Number of citations by year broken down by violation code from Jan. 2016 to Nov. 20 2025"),
    caption = "By Layla Dajani",
    x = "Year",
    y = "Citations per year"
      ) +
theme_minimal()

types_by_month_16_plot |> 
  ggplotly()

3.3.2 By count ’22 to ’25

Breaking down monthly citation counts by violation codes from January 2022 to December 2025.

3.3.2.1 Making object
Expand this to see code
types_by_month_22 <- unhoused_distinct |>
  filter(off_floor_mo >= "2022-01-01") |> 
  group_by(off_floor_mo, 
           violation_code,
           ordinance_type) |>
  summarise(citation_count = n(), .groups = "drop") |>
  arrange(off_floor_mo)

types_by_month_22
3.3.1.2 Plotting
About this chart

The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.

Expand this to see code
types_by_month_22_plot <- ggplot(
  types_by_month_22,
  aes(x = off_floor_mo, 
      y = citation_count, 
      color = violation_code)) +
  geom_line() +
  geom_point(size = 0.4) +
  geom_vline(xintercept = ymd("2024-01-01"),
             linetype = "dashed", 
             color = "grey70") +
  geom_vline(xintercept = ymd("2025-07-01"),
             linetype = "dashed", 
             color = "grey70") +
  labs(
    title = str_wrap("Number of citations by year broken down by violation code from January 2022 to December 2025", width = 70),
    subtitle = str_wrap("The first dotted line indicates when Whitmire's term began, and the second indicates when he introduced new violation codes.", width = 90),
    caption = "By Layla Dajani",
    x = "Year",
    y = "Citations per year"
      ) +
  theme_minimal()

ggsave("figures/types-by-month-22-plot.png")

types_by_month_22_plot |> 
  ggplotly()
3.3.1.3 Data takeaways
  • This further suggests CC940 and CC941 have followed similar patterns, indicating they have been given out at the same rate. But starting around July 2025, when Whitmire introduced new ordinances, there became less CC941 citations relative to CC940 numbers, while new violation code counts increased. This is further displayed in the following graph.
  • In December 2025, one of the new violations, CC942, surpassed CC940 by two citations.

3.3.3 By percentage

What is the percentage breakdown of violation codes over time? This tracks the correlation between CC940 and CC941 over time.

3.3.3.1 Making object
Expand this to see code
codes_by_pct <- unhoused_distinct |> 
  group_by(off_floor_mo) |> 
  summarise(
    total_citations = n(),
    total_940 = sum(violation_code == "CC940"),
    total_941 = sum(violation_code == "CC941"),
    total_other = sum(violation_code == "CC753", 
                      violation_code == "CC942" | 
                      violation_code == "CC943" | 
                      violation_code == "CC948" | 
                      violation_code == "CC956" | 
                      violation_code == "CC921"),
    cc940_pct = round((total_940/total_citations)*100, 2),
    cc941_pct = round((total_941/total_citations)*100, 2),
    other_pct = round((total_other/total_citations)*100, 2)
  ) |> 
  ungroup() |>
  arrange(off_floor_mo)

codes_by_pct 
3.3.3.2 Plotting
About the chart
  • The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.
  • “ALL OTHERS” includes all codes that are not CC940 or CC941.
Expand this to see code
codes_by_pct_plot <- ggplot(
  codes_by_pct,
  aes(x = off_floor_mo)) +
  geom_line(aes(y = cc940_pct,
                color = "CC940")) +
  geom_point(aes(y = cc940_pct,
                color = "CC940"),
             size = 0.4) +
  geom_line(aes(y = cc941_pct,
                color = "CC941")) +
  geom_point(aes(y = cc941_pct,
                color = "CC941"),
             size = 0.4) +
  geom_line(aes(y = other_pct,
                color = "ALL OTHERS")) +
  geom_point(aes(y = other_pct,
                color = "ALL OTHERS"),
             size = 0.4) +
  scale_color_manual(
    name = "VIOLATION CODE",
    values = c(
      "CC941" = "#4E79A7",
      "ALL OTHERS" = "#E15759"
    )
  ) +
  geom_vline(xintercept = ymd("2024-01-01"),
             linetype = "dashed", 
             color = "grey70") +
  geom_vline(xintercept = ymd("2025-07-01"),
             linetype = "dashed", 
             color = "grey70") +
  labs(
    title = str_wrap("CC940, CC941 and all 'other' codes percentage breakdown by month."),
    subtitle = str_wrap("'ALL OTHERS' includes all codes that are not CC940 or CC941."),
    caption = "By Layla Dajani",
    x = "Year",
    y = "Percentage of total citations"
      ) +
theme_minimal()

codes_by_pct_plot |> 
  ggplotly()
3.3.3.3 Data takeaways
  • Starting around July 2025, when Whitmire introduced new violation codes, the CC941 percentage decreased, while new violation code percentages increased.
  • August 2025 was the first time the percentage of all “other” codes exceeded CC941 for that month, and October 2025 was they exceeded CC940 for that month.
  • By December 2025, all “other” codes reach its all-time peak at 50.3 percent, surpassing CC940 and CC941 percentages. CC940 also plummetted to about 15.2 percent that month, placing it below CC941, which made up about 23.6 percent.

3.4 Ordinance types over time

3.4.1 Making object

Grouping by ordinance type and plotting over time.

Expand this to see code
types_over_time <- unhoused_distinct |>
  filter(offense_date >= "2023-07-01") |> 
  group_by(off_floor_mo, 
           ordinance_type) |>
  summarise(citation_count = n(), .groups = "drop") |>
  arrange(off_floor_mo)

types_over_time

3.4.2 Plotting

About this chart

The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.

Expand this to see code
types_over_time_plot <- ggplot(
  types_over_time,
  aes(x = off_floor_mo, 
      y = citation_count, 
      fill = ordinance_type
      )) +
  geom_col(position = "stack") +
  scale_fill_manual(
    name = "ORDINANCE TYPE",
    values = c(
      "CIVILITY" = "#E15759",
      "SIDEWALK" = "#4E79A7",
      "OTHER" = "#59A14F"
    )
  ) +
  geom_vline(xintercept = ymd("2024-01-01"),
             linetype = "dashed", 
             color = "grey40") +
  geom_vline(xintercept = ymd("2025-07-01"),
             linetype = "dashed", 
             color = "grey40") +
  labs(
    x = "Date",
    y = "Number of citations per month",
    fill = "Violation type",
    title = "Monthly citations by violation type from July 2023 to December 2025",
    subtitle = str_wrap("The first dotted line indicates when Whitmire's term began, and the second indicates when he introduced new violation codes."),
    caption = "Layla Dajani"
      ) +
  theme_minimal()

ggsave("figures/types-over-time-plot.png")

types_over_time_plot |> 
  ggplotly()

3.4.3 Data takeaways

October 2025 had the highest sidewalk obstruction citations per month in this data set.

4. Judgments and dispositions

4.1 Most common judgment

4.1.1 Unrefined

What is the most common specific judgment?

Expand this to see code
unhoused |> # not using unhoused_distinct here to show the true judgment stats. even if there are repeated cases, those repeats each received their own judgment in court.
  group_by(judgment) |> 
  summarise(judgment_count = n()) |> 
  arrange(desc(judgment_count))

4.1.2 Refined

What is the most common refined judgment?

Note

The column “judgment_refined” was created in the cleaning notebook through the external software OpenRefine. The column standardizes specific judgments into more general categories.

Expand this to see code
unhoused |>
  group_by(judgment_refined) |> 
  summarise(judgment_count = n()) |>
  arrange(desc(judgment_count))

4.1.3 Case dismissed

Break down of case dismissed judgments.

Expand this to see code
unhoused |>
  filter(
    str_detect(judgment, regex("CASE DISMISSED"))
  ) |>
  tabyl(judgment) |>
  rename(judgment_count = n) |>
  mutate(percent = (percent * 100) |> round(1)) |>
  arrange(desc(percent)) |>
  tibble()

4.1.4 Data takeaways

  • “Case dismissed” is the most common judgment, with a count of 11,755.
  • The most common reason a case is dismissed is because the officer was not present. Of the 11,755 cases that were dismissed, about 72% of cases were dismissed because officers were not present. About 20% of case dismissals were because “OUTREACH(Indigency).”

4.2 Most common disposition

What is the most common disposition?

This table omits “undisposed” cases, which are cases that have not received a disposition.

Expand this to see code
unhoused |>
  filter(!c(disposition == "UNDISPOSED")) |> 
  tabyl(disposition) |>
  rename(disposition_count = n) |>
  mutate(percent = (percent * 100) |> round(1)) |>
  arrange(desc(percent)) |>
  tibble()

4.2.1 Data takeaways

  • Of the cases that have received a disposition, about 59% were dismissed at trial.
  • About 35% of cases that have received a disposition were ruled “guilty by trial judge.”

4.3 Yearly guilty cases

Guilty disposition counts by year.

4.3.1 Making object

Expand this to see code
yearly_guilty <- unhoused |> 
  filter(disposition == "GUILTY TRIAL BY JUDGE") |> 
  group_by(off_floor_yr) |> 
  summarize(yearly_guilty = n()) |> 
  arrange(off_floor_yr) 

yearly_guilty

4.3.2 Plotting

Expand this to see code
yearly_guilty_plot <- ggplot(
  yearly_guilty,
  aes(x = off_floor_yr, 
      y = yearly_guilty
      )) +
  geom_col(fill = "#4E79A7") +
  labs(
    x = "Year",
    y = "Number of guilty judgments per month",
    title = "Guilty judgments by year from 2016 to 2025"
      ) +
  theme_minimal()

yearly_guilty_plot |> 
  ggplotly()

4.3.1 Data takeaways

  • 2018 is the year with the lowest number of guilty dispositions at 50 dispositions.
  • 2016 is the year with the highest number of guilty dispositions at 1951 dispositions, followed by 2021 with 931.

5. Top people

5.1 Distinct people

How many distinct people have received a citation?

Expand this to see code
unhoused_distinct |> 
  summarise(people_count = n_distinct(refined_name))

5.1.1 Data takeaway

Important

While thorough efforts were made to consolidate variations of the same names, uncertainties in that process mean this number should be treated as an estimate. See more about this in section 2 of the overview notebook.

  • There are an estimated 4,704 distinct people who have received a citation between Jan. 1, 2016 and Dec. 31, 2025.

5.2 Most citations

Who has received the most citations since January 2016?

Expand this to see code
unhoused_distinct |> 
  group_by(refined_name) |> 
  summarise(citation_count = n(),
            first_citation = min(offense_date),
            last_citation = max(offense_date),
            ) |> 
  arrange(desc(citation_count)) |> 
  filter(citation_count >= 100)

5.2.1 Data takeaways

  • There are 27 people who have received at least 100 total citations.
  • The person with the most citations is Patricia Norris, who has received 794 total citations, all given between October 2017 and May 2025.
  • The person with the second most citations is Forest H Smith, who has received 351 total citations, all given between August 2020 and November 2025.

5.3 Most fines and dues

Below is a searchable table that answers the following questions:

  • Who are the most heavily fined people?
  • How much of those fines got dismissed?
  • How much did they pay?
  • How much do they still have due? Who has paid the highest amount of dues?
Important

Not everyone pays their fines in cash, many pay their fines by serving time. The “fines_paid” column is split into two sub-columns to reflect this.

  • fines_paid_not_credit is how much the person paid when time_served = “NO.”
  • fines_paid_served_credit is how much the person paid when time_served = “YES.”
  • total_fines_paid combines both of the above.
Expand this to see code
individual_fines <- unhoused_distinct |>
  group_by(refined_name) |>
  summarise(
    citations = n(),
    total_fined = sum(amount_fined, na.rm = TRUE) |> round(0),
    total_fines_dismissed = sum(fines_dismissed, na.rm = TRUE) |> round(0),
    total_fines_paid = sum(fines_paid, na.rm = TRUE) |> round(0),
    fines_paid_not_credit = sum(fines_paid[time_served == "NO"], na.rm = TRUE) |> round(0),
    fines_paid_served_credit = sum(fines_paid[time_served == "YES"], na.rm = TRUE) |> round(0),
    total_fines_due = sum(fines_due, na.rm = TRUE) |> round(0)
  ) |>
  arrange(desc(total_fined))

individual_fines |>
  datatable()

5.3.1 Data takeaways

  • Most fined:
    • Patricia Norris has been charged the highest total fines at about $199,123. She hasn’t paid any of her fines, but has had about $149,118 dismissed, which leaves about $50,006 that she still owes.
    • Forest H Smith is the second most fined person, with around $86,761 total fines. He has also never paid any of his fines, but has had roughly $9,584 dismissed, leaving around $77,177 that he still owes.
  • Paid the most in “not credit”:
    • Kevin P Shakesnider is the person who has paid the highest amount of fines when considering “total fines paid.” He is also the person who has paid the most fines in “not credit.” He has paid about $26,108 of his total fined $45,183 with “time served,” he paid $2,576 in “not credit,” and has had roughly $10,916 dismissed. As of December 2025, he still owes about $5,583.
    • 14 people have paid over $1,000 in “not credit.” The top three in descending order are Shakesnider, Melissa Clark and Robin Harrison.
  • Paid the most in time served:
    • Bobby L Anderson Jr. has paid the most fines with “time served.” He has been fined roughly $38,245. Of that sum, he paid $26,489 with time served, none in “not credit” and had about $9,287 dismissed.

6. Fines

  1. How many distinct people are there who were fined?
  2. How many people still owe fines to the city? How much is the city owed in total?
  3. How many people have paid some/all of their fines when time_served = “NO”?
  4. How many people have paid some/all of their fines when time_served = “YES”?
  5. How many people have had some/all of their fines dismissed?

6.1 Fined people

How many distinct people are there who were fined?

Expand this to see code
individual_fines |> 
  filter(total_fined > 0) |>
  summarise(total_fined_people = n(),
            amount_fined_total = sum(total_fined),
            percent_of_total_people = round((total_fined_people / 4704) * 100, 1)
            ) #@ the total number of people must be updated as needed

6.2 Owed fines

How many people still owe fines to the city? How much is the city owed in total?

Expand this to see code
individual_fines |> 
  filter(total_fines_due > 0) |> 
  summarise(people_owing_fines = n(),
            owed_fines_total = sum(total_fines_due),
            percent_of_people = round((people_owing_fines / 4704) * 100, 1), # the total number of people must be updated as needed
            percent_of_fines = round((owed_fines_total / 6735088) * 100, 1)
  )

6.3 Fines paid not served

How many people have paid some/all of their fines when time_served = “NO”?

6.3.1 Some fines paid

How many people have paid at least some of their fines when time_served = “NO”?

Expand this to see code
individual_fines |> 
  filter(fines_paid_not_credit > 0) |> 
  summarise(people_paid_some_fines = n(),
            paid_fines_not_credit_total = sum(fines_paid_not_credit),
            percent_of_people = round((people_paid_some_fines / 4704) * 100, 1), # the total number of people must be updated as needed
            percent_of_fines = round((paid_fines_not_credit_total / 6735088) * 100, 1)
  )

6.3.2 All fines paid - no dismissals

How many people have paid all of their fines when time_served = “NO” with no help from dismissals?

Expand this to see code
individual_fines |> 
  filter(total_fined > 0,
         fines_paid_not_credit == total_fined
  ) |> 
  summarise(people_paid_all_fines = n(),
            paid_fines_not_crdit_total = sum(fines_paid_not_credit),
            percent_of_people = round((people_paid_all_fines / 4704) * 100, 1), # the total number of people must be updated as needed
            percent_of_fines = round((paid_fines_not_crdit_total / 6735088) * 100, 1)
  )

6.4 Fines paid time served

How many people have paid some/all of their fines when time_served = “YES”?

6.4.1 Some fines paid

How many people have paid at least some of their fines when time_served = “YES”?

Expand this to see code
individual_fines |> 
  filter(fines_paid_served_credit > 0) |> 
  summarise(people_paid_some_fines = n(), 
            paid_fines_credit_total = sum(fines_paid_served_credit),
            percent_of_people = round((people_paid_some_fines / 4704) * 100, 1), # the total number of people must be updated as needed
            percent_of_fines = round((paid_fines_credit_total / 6735088) * 100, 1)
  )

6.4.2 All fines paid - no dismissals

How many people have paid all of their fines when time_served = “YES” with no help from dismissals?

Expand this to see code
individual_fines |> 
  filter(total_fined > 0,
         fines_paid_served_credit == total_fined
  ) |> 
  summarise(people_paid_all_fines = n(),
            paid_fines_credit_total = sum(fines_paid_served_credit),
            percent_of_people = round((people_paid_all_fines / 4704) * 100, 1), # the total number of people must be updated as needed
            percent_of_fines = round((paid_fines_credit_total / 6735088) * 100, 1)
  )

6.5 Fines dismissed

How many people have had some/all of their fines dismissed?

6.5.1 Some dismissed

How many people have had at least some of their fines dismissed?

Expand this to see code
individual_fines |> 
  filter(total_fines_dismissed > 0) |> 
  summarise(people_some_dismissed_fines = n(),
            total_dismissed = sum(total_fines_dismissed),
            percent_of_people = round((people_some_dismissed_fines / 4704) * 100, 1), # the total number of people must be updated as needed
            percent_of_fines = round((total_dismissed / 6735088) * 100, 1)
  )

6.5.2 All dismissed

How many people have had all of their fines dismissed?

Expand this to see code
individual_fines |> 
  filter(total_fines_dismissed > 0,
         total_fines_dismissed == total_fined
  ) |> 
  summarise(people_all_dismissed_fines = n(),
            total_dismissed = sum(total_fines_dismissed),
            percent_of_people = round((people_all_dismissed_fines / 4704) * 100, 1), # the total number of people must be updated as needed
            percent_of_fines = round((total_dismissed / 6735088) * 100, 1)
  )

6.6 Data takeaways

  1. Total fined: Of the 4,704 distinct people who have received a citation since 2016, about 4,477 have received a fine with at least one citation. The cumulative total of all fines is $6,795,858.
    1. So about 95.2% of people who have received a citation have also received a fine at some point.
  2. Total owed: Of the 4,704 people, 2,982 still owe fines. The sum of total fines still owed to the city is $3,054,822.
    1. So the city is still owed about 45% of the roughly $6.8 million the city has fined unhoused people since 2016.
  3. Paid with “not time served”: Of the 4,704 people, 235 have paid at least some of their fines when time_served = “NO.” 83 of those people have paid off all their fines without any help from dismissals. The total amount of fines paid when time_served = “NO” since 2016 is about $82,185.
    1. So just over 1% of the roughly $6.8 million the city has fined unhoused people since 2016 has been paid in “not credit.”
  4. Paid with time_served: Of the 4,704 people, 1,070 have paid at least some of their fines when time_served = “YES.” 464 of those people have paid off all their fines without any help from dismissals. The total amount of fines paid when time_served = “YES” since 2016 is about $1,411,989.
    1. So just about 23% of the roughly $6.8 million the city has fined unhoused people since 2016 has been paid in “not credit.”
  5. Total dismissed: Of the 4,704 people, 1,470 have had at least some of their fines dismissed, and 705 of those 1,470 people had all of their fines dismissed. A total of $2,246,750 has been dismissed.
    1. So about 31% of the roughly $6.7 million the city has fined unhoused people since 2016 has been dismissed.

7. Jail time

7.1 Credit time served

7.1.1 Distinct jailed judgments

  • How many individual people have received at least one CREDIT TIME SERVED judgment?
  • Who has received the most CREDIT TIME SERVED judgments?
Expand this to see code
unhoused |> 
  group_by(refined_name) |> 
  summarise(credit_jail_judgments = sum(time_served == "YES")) |> 
  filter(credit_jail_judgments > 1) |> 
  arrange(desc(credit_jail_judgments))

7.1.2 Distinct judgment dates

Who has received the most CREDIT TIME SERVED judgments on distinct judgment dates?

Explanation

Oftentimes, courts rule on several citations for the same person on one day. For example, Kevin Shakesnider didn’t receive all 136 of his CREDIT TIME SERVED judgments on 136 separate days, he was assigned the same judgment date for several of his citations.

Expand this to see code
unhoused |> 
  filter(time_served == "YES") |> 
  distinct(refined_name, judgment_date) |> 
  group_by(refined_name) |> 
  summarise(credit_jail_judgment_days = n()) |> 
  #filter(credit_jail_judgment_days > 1) |> 
  arrange(desc(credit_jail_judgment_days))

7.1.3 Data takeaways

  • Of the 4,704 people who have received a citation since 2016, 1,070 of them received a “CREDIT TIME SERVED” judgment at least once. Of those 4,704, 848 people have received more than one “CREDIT TIME SERVED” judgment.
  • The person with the highest “CREDIT TIME SERVED” judgment count is Kevin P Shakesnider, who has received 136 judgments. Those 136 judgments were given across 22 distinct judgment dates.
  • The person with the second highest “CREDIT TIME SERVED” judgment count is BOBBY L ANDERSON JR, who has received 104 judgments. Those 104 judgments were given across 7 distinct judgment dates.
  • Note that although Patricia Norris has received the highest citation count, she has never received a “CREDIT TIME SERVED” judgment.

8. Individual people analysis

8.1. Kevin P Shakesnider

Shakesnider data takeaways so far:

  • Highest paid “not credit” and “not credit” + “time served” fines: Kevin P Shakesnider is the person who has paid the highest amount of fines when considering “not credit” and time served combined. He is also the person who has paid the most fines in “not credit.” He has been fined a total of $45,183. He paid about $26,108 with “CREDIT TIME SERVED,” he paid $2,576 in “not credit,” and has had roughly $10,916 dismissed. As of December 2025, he still owes about $5,583.
  • Highest “CREDIT TIME SERVED” judgments: He is also the person with the highest “CREDIT TIME SERVED” judgments at 136. Those 136 judgments were given across 22 distinct judgment dates.

8.1.1 “CREDIT TIME SERVED”

When did Kevin P Shakesnider receive the highest jail citations in one judgment? What is the overall date range?

Expand this to see code
shakesnider_jailed <- unhoused |> 
  filter(refined_name == "SHAKESNIDER, KEVIN P",
         time_served == "YES") |>
  group_by(judgment_date) |>
  summarise(jail_count = n()) |> 
  arrange(desc(judgment_date))

shakesnider_jailed

8.1.2 Data takeaway

All 22 of his judgment dates when Shakesnider received “CREDIT TIME SERVED” judgments fall between Jan. 13, 2016 and Nov. 25, 2015. On June 23, he received a “CREDIT TIME SERVED” judgment for 26 citations, his highest in one single day.

8.2 Patricia Norris

Norris takeaway so far:

  • Patricia Norris has been charged the highest total fines at about $199,123. She hasn’t paid any of her fines, but has had about $149,118 dismissed, which leaves about $50,006 that she still owes.

8.2.1 Visualizing

Making Norris object.

Expand this to see code
norris <- unhoused |> 
  filter(refined_name == "NORRIS, PATRICIA")

norris

Checking for what kinds of violation codes Norris has been given.

Expand this to see code
norris |> 
  distinct(violation_code)

Making the object to plot.

Expand this to see code
norris_citations <- norris |> 
  group_by(offense_date) |> 
  summarise(citation_count = n()) |>
  arrange(offense_date)

norris_citations
8.2.2.2 Plotting
Expand this to see code
norris_citations_plot <- ggplot(
  norris_citations, 
       aes(x = offense_date, 
           y = citation_count)) +
  geom_point(alpha = 1 / 3) +
  labs(
    title = "Patricia Norris citation count by offense date",
    x = "Offense date",
    y = "Citation count",
    caption = "By Layla Dajani"
  ) +
  theme_minimal()

ggsave("figures/norris_citations_plot.png")

norris_citations_plot |> ggplotly()
8.2.2.1.1 Data takeaways
  • Each one of these points is a day, so the higher the citation count, the more citations she received in a single day. Almost every time Patricia Norris has been given a citation, she has also been given at least one other citation. She was also hit with citations almost consistently from January 2020 to May 2025.
  • Norris has only ever received violation codes CC940 and CC941.

START FRANKLIN HERE

8.3 Trazawell Franklin

Trazawell Franklin is quoted in the story.

8.3.1 Visualizing

Making Franklin object.

Expand this to see code
franklin <- unhoused |> 
  filter(refined_name == "FRANKLIN, TRAZAWELL")

franklin

Checking for what kinds of violation codes Franklin has been given.

Expand this to see code
franklin |> 
  distinct(violation_code)

Making the object to plot.

Expand this to see code
franklin_citations <- franklin |> 
  group_by(offense_date) |> 
  summarise(citation_count = n()) |>
  arrange(offense_date)

franklin_citations
8.3.1.1 Plotting
Expand this to see code
franklin_citations_plot <- ggplot(
  franklin_citations, 
       aes(x = offense_date, 
           y = citation_count)) +
  geom_point(alpha = 1 / 3) +
  labs(
    title = "Trazawell Franklin citation count by offense date",
    x = "Offense date",
    y = "Citation count",
    caption = "By Layla Dajani"
  ) +
  theme_minimal()

ggsave("figures/franklin_citations_plot.png")

franklin_citations_plot |> ggplotly()
8.3.1.2 Data takeaways
  • Each one of these points is a day, so the higher the citation count, the more citations received in a single day. Franklin received six citations on three occasions.

9. Mapping analysis

Mapping citations by ordinance zones. Skip to section 9.2 for results.

9.1 Preparing to plot map

Setting up to map.

9.1.1 Importing district data

Importing Houston district data as an sf object.

Expand this to see code
hou_zones <- st_read(
  "additional-materials/civility-ordinance-boundaries.geojson"
)
Reading layer `civility-ordinance-boundaries' from data source 
  `/Users/layladajani/Documents/mig/houston-unhoused/additional-materials/civility-ordinance-boundaries.geojson' 
  using driver `GeoJSON'
Simple feature collection with 13 features and 4 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -95.48257 ymin: 29.63981 xmax: -95.27964 ymax: 29.78968
Geodetic CRS:  WGS 84
Expand this to see code
hou_zones

9.1.2 Simple feature

Turning the addresses in a Simple Feature Collection.

Expand this to see code
pts_zones <- st_as_sf(
  unhoused_distinct,
  coords = c("geocodio_longitude", "geocodio_latitude"),
  crs = 4326
)

# pts_zones |> glimpse()

9.1.3 Fixing invalid district lines

Expand this to see code
clean_zones <- st_make_valid(hou_zones)

cleaner_zones <- st_transform(clean_zones, 4326)

9.1.4 Citation to zone

Assigning each citation to a zone.

Expand this to see code
zoning_citations <- st_join(pts_zones, cleaner_zones, join = st_within)

# zoning_citations

Cleaning names after joining:

Expand this to see code
zoning_citations_clean <- zoning_citations |> 
  clean_names()

# zoning_citations_clean |> glimpse()

9.1.5 Citations by zone

Which zone has received the most citations from 2016 to Sept. 2025?

Grouping data by zone, preparing to map.

Expand this to see code
zone_counts <- zoning_citations_clean |>
  st_drop_geometry() |>
  count(name, name = "citation_count") |> 
  arrange(desc(citation_count))

zone_counts
9.1.5.1 With date

Making a version of the zoned object that includes each offense’s floor month to plot externally.

Expand this to see code
zone_counts_monthly <- zoning_citations_clean |>
  st_drop_geometry() |>
  count(off_floor_mo,
        name, 
        name = "citation_count")

zone_counts_monthly

Cleaning cleaner_zone names and joining with the monthly zoned object.

Expand this to see code
cleaner_names_zones <- clean_names(cleaner_zones)
 
zones_to_map_monthly <- cleaner_names_zones |>
  left_join(zone_counts_monthly, by = "name")

# head(zones_to_map_monthly)

Exporting.

Expand this to see code
zones_to_map_monthly |>
  write_csv("data-processed/requested-files/zones-to-map.csv")
9.1.5.2 East End before Nov. 2025

Making a zoned object of all citations given in the East End Area prior to November 2025, which is when the zone was officially added to the ordinance.

Expand this to see code
east_end_citations <- zoning_citations_clean |>
  st_drop_geometry() |>
  filter(name == "East End Area",
         offense_date <= "2025-10-31") |> 
  select(citation_number,
         case_number,
         offense_date,
         refined_name,
         violation_code,
         full_offense_location,
         zone = name) |> 
  arrange(offense_date)

east_end_citations

Exporting.

Expand this to see code
east_end_citations |>
  write_csv("data-processed/requested-files/east_end_citations.csv")

9.2 Plotting

Cleaning cleaner_zone names and joining with the zoned object.

Expand this to see code
cleaner_names_zones <- clean_names(cleaner_zones)
 
zones_to_map <- cleaner_names_zones |>
  left_join(zone_counts, by = "name")

9.2.1 General mapview

Plotting an interactive heat map to show each district’s citations.

Expand this to see code
mapview(
  zones_to_map,
  zcol = "citation_count",
  popup = c("name", "citation_count"),
  layer.name = "Citations"
) 

9.2.2 Zones by year

Visualizing each civility ordinance zone’s citation count by year in two stacked bar charts, one by citation count and the other by citation percentage.

9.2.2.1 By count
9.2.2.1.1 Making object
Expand this to see code
zone_year_counts <- zoning_citations_clean |>
  st_drop_geometry() |>
  filter(!is.na(name)) |> 
  group_by(off_yr, name) |>
  summarise(citation_count = n(),
            .groups = "drop") |> 
  arrange(off_yr)

zone_year_counts
9.2.2.1.2 Plotting
Expand this to see code
zone_year_counts_plot <- ggplot(
  zone_year_counts, 
  aes(x = factor(off_yr), 
      y = citation_count, 
      fill = name)) +
  geom_bar(stat = "identity") +
  scale_fill_manual(
  values = c(
    "#C0392B",
    "#1A5D8F",
    "#F1C40F",
    "#27AE60",
    "#D87BA1",
    "#D35400",
    "#8E4B2D",
    "#7D3C98",
    "#4DA1A0",
    "#E17C5C",
    "#2980B9",
    "#006400",
    "#7F7F7F"
  )) +
  labs(
    x = "Year",
    y = "Citation count",
    fill = "Zone",
    title = "Citation counts per zone by year",
    caption = "By Layla Dajani"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

zone_year_counts_plot |> 
  ggplotly()
9.2.2.2 By percentage
9.2.2.2.1 Making object
Expand this to see code
zone_year_pct <- zone_year_counts |>
  group_by(off_yr) |>
  mutate(citation_pct = round(citation_count / sum(citation_count) * 100, 1)) |> 
  arrange(desc(citation_pct))

zone_year_pct
9.2.2.2.2 Plotting
Expand this to see code
zone_year_pct_plot <- ggplot(
  zone_year_pct, 
  aes(x = factor(off_yr), 
      y = citation_pct, 
      fill = name)) +
  geom_bar(stat = "identity") +
  scale_fill_manual(
  values = c(
    "#C0392B",
    "#1A5D8F",
    "#F1C40F",
    "#27AE60",
    "#D87BA1",
    "#D35400",
    "#8E4B2D",
    "#7D3C98",
    "#4DA1A0",
    "#E17C5C",
    "#2980B9",
    "#006400",
    "#7F7F7F"
  )) +
  labs(
    x = "Year",
    y = "Percentage",
    fill = "Zone",
    title = "Citation percentages per zone by year",
    caption = "By Layla Dajani"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

zone_year_pct_plot |> 
  ggplotly()

9.2.3 Zones by month

9.2.3.1 By count
9.2.3.1.1 Making object
Expand this to see code
zone_month_counts <- zoning_citations_clean |>
  st_drop_geometry() |>
  filter(!is.na(name),
         off_floor_mo >= "2024-01-01") |> 
  group_by(off_floor_mo, name) |>
  summarise(citation_count = n(),
            .groups = "drop") |> 
  arrange(desc(citation_count))

zone_month_counts
9.2.3.1.2 Plotting
Expand this to see code
zone_year_counts_plot <- ggplot(
  zone_month_counts, 
  aes(x = off_floor_mo, 
      y = citation_count, 
      fill = name)) +
  geom_bar(stat = "identity") +
  scale_fill_manual(
  values = c(
    "#C0392B",
    "#1A5D8F",
    "#F1C40F",
    "#27AE60",
    "#D87BA1",
    "#D35400",
    "#8E4B2D",
    "#7D3C98",
    "#4DA1A0",
    "#E17C5C",
    "#2980B9",
    "#006400",
    "#7F7F7F"
  )) +
  scale_x_date(
  breaks = seq(
    from = as.Date("2024-06-01"),
    to   = as.Date("2025-12-01"),
    by   = "6 months"
  ),
  date_labels = "%b %Y"
  ) +
  labs(
    x = "Date",
    y = "Citation count",
    fill = "Zone",
    title = "Citation percentages per zone by month from January 2024 to December 2025",
    caption = "By Layla Dajani"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

zone_year_counts_plot |> 
  ggplotly()
9.2.3.2 By percent
9.2.3.2.1 Making object
Expand this to see code
zone_mo_pct <- zone_month_counts |>
  filter(off_floor_mo >= "2024-01-01") |> 
  group_by(off_floor_mo) |>
  mutate(citation_pct = round(citation_count / sum(citation_count) * 100, 1)) |> 
  arrange(desc(citation_pct))

zone_mo_pct
9.2.2.2.2 Plotting
Expand this to see code
zone_mo_pct_plot <- ggplot(
  zone_mo_pct, 
  aes(x = off_floor_mo, 
      y = citation_pct, 
      fill = name)) +
  geom_bar(stat = "identity") +
  scale_fill_manual(
  values = c(
    "#C0392B",
    "#1A5D8F",
    "#F1C40F",
    "#27AE60",
    "#D87BA1",
    "#D35400",
    "#8E4B2D",
    "#7D3C98",
    "#4DA1A0",
    "#E17C5C",
    "#2980B9",
    "#006400",
    "#7F7F7F"
  )) +
  scale_x_date(
  breaks = seq(
    from = as.Date("2024-06-01"),
    to   = as.Date("2025-12-01"),
    by   = "6 months"
  ),
  date_labels = "%b %Y"
  ) +
  labs(
    x = "Date",
    y = "Percentage",
    fill = "Zone",
    title = "Citation percentages per zone by month from January 2024 to December 2025",
    caption = "By Layla Dajani"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

zone_mo_pct_plot |> 
  ggplotly()

10. last bit

Closing out with some specifics on zones.

Expand this to see code
zoning_citations_clean |> 
  st_drop_geometry() |>
  filter(off_floor_mo >= "2025-11-01") |> 
  filter(str_detect(name, "East")) |> 
  count(name, name = "NovDec_citations") |> 
  adorn_totals() |>
  as_tibble()

Below is all citations in 2025 by area, which shows Central Business District totals.

Expand this to see code
zoning_citations_clean |> 
  st_drop_geometry() |>
  filter(off_yr == 2025) |> 
  count(name, name = "2025 citations")