Skip to content

Instantly share code, notes, and snippets.

@JEFworks
Last active July 20, 2025 20:27
Show Gist options
  • Save JEFworks/0c76f69ea39ce60deabdc3f100f58f5d to your computer and use it in GitHub Desktop.
Save JEFworks/0c76f69ea39ce60deabdc3f100f58f5d to your computer and use it in GitHub Desktop.
Visualizing temporal trends in ICE arrests and encounters across major US cities
#############
## Dataset on ICE enforcement from Sep. 1, 2023 to Jun. 26, 2025
## Data source: https://deportationdata.org/data/ice.html
#############
#############
## Read in data
#############
## Arrest data
## Download from https://ucla.app.box.com/index.php?rm=box_download_shared_file&shared_name=9d8qnnduhus4bd5mwqt7l95kz34fic2v&file_id=f_1922082018423
arrests <- read_excel('2025-ICLI-00019_2024-ICFO-39357_ERO Admin Arrests_raw.xlsx', skip=6, sheet=1)
head(arrests)
## Counters, collate across 2 sheets
## Download from https://ucla.app.box.com/index.php?rm=box_download_shared_file&shared_name=9d8qnnduhus4bd5mwqt7l95kz34fic2v&file_id=f_1922075106849
encounters1 <- read_excel('~/Downloads/2025-ICLI-00019_2024-ICFO-39357_ERO Encounters_raw.xlsx', skip=6, sheet=1)
encounters2 <- read_excel('~/Downloads/2025-ICLI-00019_2024-ICFO-39357_ERO Encounters_raw.xlsx', skip=6, sheet=2)
encounters <- rbind(encounters1, encounters2)
head(encounters)
#############
## Clean data
#############
library(tidyverse)
library(dplyr)
arrests_df <- data.frame(
date = arrests$`Apprehension Date`,
city = str_remove(arrests$`Apprehension AOR`, " Area of Responsibility"))
encounters_df <- data.frame(
date = encounters$`Event Date`,
city = str_remove( encounters$`Responsible AOR`, " Area of Responsibility"))
# Aggregate results by month
arrests_df <- arrests_df %>%
filter(year(date) <= 2026) %>% # <-- Remove dates after 2026 (weird error in data)
mutate(month = floor_date(date, unit = "month"))
# Count methods per month
arrests_df_summary <- arrests_df %>%
group_by(month, city) %>%
summarise(count = n(), .groups = 'drop')
head(arrests_df_summary)
range(arrests_df_summary$count)
encounters_df <- encounters_df %>%
filter(year(date) <= 2026) %>% # <-- Remove dates after 2026 (weird error in data)
mutate(month = floor_date(date, unit = "month"))
# Count methods per month
encounters_df_summary <- encounters_df %>%
group_by(month, city) %>%
summarise(count = n(), .groups = 'drop')
head(encounters_df_summary)
range(encounters_df_summary$count)
# Join the two data sets
joined_df <- encounters_df_summary %>%
rename(count_encounters = count) %>%
full_join(arrests_df_summary %>% rename(count_arrests = count),
by = c("month", "city"))
head(joined_df)
# Cherry pick few cities to highlight
topcities <- joined_df %>% filter(year(joined_df$month) == 2025) %>%
filter(count_arrests > quantile(count_arrests, 0.7)) %>%
summarize(sum(count_arrests), .by=city)
topcities
topcities2 <- c('Miami', 'Houston', 'Los Angeles', 'Chicago', 'Atlanta', 'New York City')
joined_df_sub <- joined_df[joined_df$city %in% topcities2,]
#############
## Plot
#############
library(ggplot2)
library(gganimate)
library(patchwork)
head(joined_df_sub)
# Pivot longer to have one column for value and one for metric type
df_long <- joined_df_sub %>%
pivot_longer(cols = c(count_encounters, count_arrests),
names_to = "metric",
values_to = "count") %>%
arrange(city, metric, month)
# Clean names for nicer labels
df_long$metric <- recode(df_long$metric,
count_arrests = "Arrests",
count_encounters = "Encounters")
df_long$month <- as.Date(df_long$month)
# Order
df_long$city <- factor(df_long$city, levels=topcities2)
# Plot
p <- ggplot(df_long, aes(x = month, y = count, group = city, color = city)) +
geom_line() +
geom_point() +
facet_grid(metric ~ city, scales = "free_y") +
scale_x_date(
date_breaks = "3 months",
date_labels = "%b %Y"
) +
labs(
title = "Trends in ICE Activity",
subtitle = "Number of ICE arrests and encounters from Sep. 1, 2023 to Jun. 26, 2025 per month
highlights recent increased arrests across cities and increased encounters in some cities.",
caption = "'Arrest' = An event where an ICE officer apprehended someone, whether or not that person had a criminal record.
'Encounter' = An event an ICE officer had with someone, whether or not that encounter resulted in an arrest.
Select cities shown. Full data source: https://deportationdata.org/data/ice.html",
x = "",
y = "Number of People"
) +
theme_bw(
base_size = 16
) +
theme(
plot.title = element_text(size = 18, lineheight = 1.9, vjust = 3),
axis.title = element_text(size = 13),
axis.text = element_text(size = 10),
plot.subtitle = element_text(size = 13, lineheight = 0.9),
plot.caption = element_text(size = 10, vjust = -4),
strip.text = element_text(face = "bold", size = 12),
legend.position = "none",
axis.text.x = element_text(angle = 45, hjust = 1),
plot.margin = margin(t = 40, r = 20, b = 40, l = 20) # Top, Right, Bottom, Left
)
p
# Make animated version for fun
panim <- p +
transition_reveal(month) +
enter_fade() +
exit_fade()
animate(panim,
width = 800,
height = 400,
fps = 20,
duration = 8,
end_pause = 100)
@JEFworks
Copy link
Author

filec22c3f93cfb2

@JEFworks
Copy link
Author

Static version

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment