Expand this to see code
library(tidyverse)
library(janitor)
library(sf)
library(mapview)
library(ggspatial)
library(tigris)
library(mapview)
library(plotly)
library(DT)library(tidyverse)
library(janitor)
library(sf)
library(mapview)
library(ggspatial)
library(tigris)
library(mapview)
library(plotly)
library(DT)unhoused <- read_csv("data-processed/unhoused-clean.csv")
# unhoused |> glimpse()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.
The object will only contain the second appearance because that provides the most updated data.
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
Looking at how individual citation numbers have changed over time.
What is the year with the most citations?
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_yearlyPlotting yearly citations as an interactive bar chart.
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()How did the monthly citation count change between Jul. 2023 to Dec. 2025?
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_citationsPlotting a bar chart to show the monthly citation count between Jul. 2023 and Dec. 2025.
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()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_allPlotting a line graph to show a monthly citation count between January 2016 and December 2025.
The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.
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()What is the average citation count for each calendar month from 2016 to 2025?
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_citationsmo_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()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.
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.
Finding the most common violation types.
violation_count <- unhoused_distinct |>
group_by(violation_description_refined, violation_code) |>
summarise(violations = n(),
.groups = "drop") |>
arrange(desc(violations))
violation_countThere are 11 types of violations that have been given between January 2016 and December 2025. Here is what they mean and their frequency:
Plotting yearly violation code totals from January 2016 to December 2025.
unhoused_violations <- unhoused_distinct |>
group_by(off_floor_yr,
violation_code)|>
summarise(violations = n(),
.groups = "drop") |>
arrange(off_floor_yr)
# unhoused_violations |> glimpse()The first dotted line indicates when Whitmire’s term began.
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()Breaking down monthly citation counts by violation codes from January 2016 to December 2025.
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_16The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.
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()Breaking down monthly citation counts by violation codes from January 2022 to December 2025.
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_22The first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.
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()What is the percentage breakdown of violation codes over time? This tracks the correlation between CC940 and CC941 over time.
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 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()Grouping by ordinance type and plotting over time.
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_timeThe first dotted line indicates when Whitmire’s term began, and the second indicates when he introduced new violation codes.
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()October 2025 had the highest sidewalk obstruction citations per month in this data set.
What is the most common specific judgment?
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))What is the most common refined judgment?
The column “judgment_refined” was created in the cleaning notebook through the external software OpenRefine. The column standardizes specific judgments into more general categories.
unhoused |>
group_by(judgment_refined) |>
summarise(judgment_count = n()) |>
arrange(desc(judgment_count))Break down of case dismissed judgments.
unhoused |>
filter(
str_detect(judgment, regex("CASE DISMISSED"))
) |>
tabyl(judgment) |>
rename(judgment_count = n) |>
mutate(percent = (percent * 100) |> round(1)) |>
arrange(desc(percent)) |>
tibble()What is the most common disposition?
This table omits “undisposed” cases, which are cases that have not received a disposition.
unhoused |>
filter(!c(disposition == "UNDISPOSED")) |>
tabyl(disposition) |>
rename(disposition_count = n) |>
mutate(percent = (percent * 100) |> round(1)) |>
arrange(desc(percent)) |>
tibble()Guilty disposition counts by year.
yearly_guilty <- unhoused |>
filter(disposition == "GUILTY TRIAL BY JUDGE") |>
group_by(off_floor_yr) |>
summarize(yearly_guilty = n()) |>
arrange(off_floor_yr)
yearly_guiltyyearly_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()How many distinct people have received a citation?
unhoused_distinct |>
summarise(people_count = n_distinct(refined_name))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.
Who has received the most citations since January 2016?
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)Below is a searchable table that answers the following questions:
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.
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()How many distinct people are there who were fined?
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 neededHow many people still owe fines to the city? How much is the city owed in total?
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)
)How many people have paid some/all of their fines when time_served = “NO”?
How many people have paid at least some of their fines when time_served = “NO”?
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)
)How many people have paid all of their fines when time_served = “NO” with no help from dismissals?
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)
)How many people have paid some/all of their fines when time_served = “YES”?
How many people have paid at least some of their fines when time_served = “YES”?
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)
)How many people have paid all of their fines when time_served = “YES” with no help from dismissals?
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)
)How many people have had some/all of their fines dismissed?
How many people have had at least some of their fines dismissed?
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)
)How many people have had all of their fines dismissed?
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)
)unhoused |>
group_by(refined_name) |>
summarise(credit_jail_judgments = sum(time_served == "YES")) |>
filter(credit_jail_judgments > 1) |>
arrange(desc(credit_jail_judgments))Who has received the most CREDIT TIME SERVED judgments on distinct judgment dates?
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.
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))Shakesnider data takeaways so far:
When did Kevin P Shakesnider receive the highest jail citations in one judgment? What is the overall date range?
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_jailedAll 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.
Norris takeaway so far:
Making Norris object.
norris <- unhoused |>
filter(refined_name == "NORRIS, PATRICIA")
norrisChecking for what kinds of violation codes Norris has been given.
norris |>
distinct(violation_code)Making the object to plot.
norris_citations <- norris |>
group_by(offense_date) |>
summarise(citation_count = n()) |>
arrange(offense_date)
norris_citationsnorris_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()START FRANKLIN HERE
Trazawell Franklin is quoted in the story.
Making Franklin object.
franklin <- unhoused |>
filter(refined_name == "FRANKLIN, TRAZAWELL")
franklinChecking for what kinds of violation codes Franklin has been given.
franklin |>
distinct(violation_code)Making the object to plot.
franklin_citations <- franklin |>
group_by(offense_date) |>
summarise(citation_count = n()) |>
arrange(offense_date)
franklin_citationsfranklin_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()Mapping citations by ordinance zones. Skip to section 9.2 for results.
Setting up to map.
Importing Houston district data as an sf object.
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
hou_zonesTurning the addresses in a Simple Feature Collection.
pts_zones <- st_as_sf(
unhoused_distinct,
coords = c("geocodio_longitude", "geocodio_latitude"),
crs = 4326
)
# pts_zones |> glimpse()clean_zones <- st_make_valid(hou_zones)
cleaner_zones <- st_transform(clean_zones, 4326)Assigning each citation to a zone.
zoning_citations <- st_join(pts_zones, cleaner_zones, join = st_within)
# zoning_citationsCleaning names after joining:
zoning_citations_clean <- zoning_citations |>
clean_names()
# zoning_citations_clean |> glimpse()Which zone has received the most citations from 2016 to Sept. 2025?
Grouping data by zone, preparing to map.
zone_counts <- zoning_citations_clean |>
st_drop_geometry() |>
count(name, name = "citation_count") |>
arrange(desc(citation_count))
zone_countsMaking a version of the zoned object that includes each offense’s floor month to plot externally.
zone_counts_monthly <- zoning_citations_clean |>
st_drop_geometry() |>
count(off_floor_mo,
name,
name = "citation_count")
zone_counts_monthlyCleaning cleaner_zone names and joining with the monthly zoned object.
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.
zones_to_map_monthly |>
write_csv("data-processed/requested-files/zones-to-map.csv")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.
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_citationsExporting.
east_end_citations |>
write_csv("data-processed/requested-files/east_end_citations.csv")Cleaning cleaner_zone names and joining with the zoned object.
cleaner_names_zones <- clean_names(cleaner_zones)
zones_to_map <- cleaner_names_zones |>
left_join(zone_counts, by = "name")Plotting an interactive heat map to show each district’s citations.
mapview(
zones_to_map,
zcol = "citation_count",
popup = c("name", "citation_count"),
layer.name = "Citations"
) 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.
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_countszone_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()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_pctzone_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()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_countszone_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()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_pctzone_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()Closing out with some specifics on zones.
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.
zoning_citations_clean |>
st_drop_geometry() |>
filter(off_yr == 2025) |>
count(name, name = "2025 citations")