Created
July 11, 2025 23:59
-
-
Save JEFworks/899a1deed4b7f3d9e3572d3b1351a92c to your computer and use it in GitHub Desktop.
Visualizing spatial temporal trends in ICE detention data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# download data from https://www.ice.gov/detain/detention-management | |
# see blog for more details https://jef.works/blog/2025/07/10/analyzing-ice-detention-data/ | |
# read in data | |
library(readxl) | |
fy21 = readxl::read_xlsx('FY21-detentionstats.xlsx', sheet=4, skip=6) | |
fy22 = readxl::read_xlsx('FY22-detentionstats.xlsx', sheet=7, skip=6) | |
fy23 = readxl::read_xlsx('FY23_detentionStats.xlsx', sheet=8, skip=5) | |
fy24 = readxl::read_xlsx('FY24_detentionStats.xlsx', sheet=8, skip=6) | |
fy25 = readxl::read_xlsx('FY25_detentionStats06202025.xlsx', sheet=7, skip=6) | |
head(fy25) | |
# combine into one list | |
fylist <- list(fy21, fy22, fy23, fy24, fy25) | |
names(fylist) <- 2021:2025 | |
library(zipcodeR) | |
library(dplyr) | |
library(tidyr) | |
# get data we want | |
alldata <- do.call(rbind, lapply(2021:2025, function(year) { | |
fytest <- fylist[[as.character(year)]] | |
# get facility names | |
facs <- na.omit(unique(fytest$Name)) | |
# calculate ratio of criminal to non-criminal | |
facratio <- sapply(facs, function(fac) { | |
(sum(fytest[fytest$Name == fac, "Male Non-Crim"], na.rm=TRUE)+1)/ | |
(sum(fytest[fytest$Name == fac, "Male Crim"], na.rm=TRUE)+1) | |
}) | |
names(facratio) <- facs | |
# take a log2 (fold change) | |
facratio <- log2(facratio) | |
# where are these facilities based on zip code | |
zip <- as.character(fytest$Zip) | |
names(zip) <- fytest$Name | |
# keep track of size as total detainees | |
size <- rowSums(fytest[, c("Male Non-Crim", "Male Crim", "Female Non-Crim", "Female Crim")]) | |
names(size) <- fytest$Name | |
# make into data frame | |
zipdata <- data.frame( | |
name = names(facratio), | |
zip = zip[names(facratio)], | |
size = size[names(facratio)], | |
ratio = facratio | |
) | |
# use zipcodeR to get lat/lon | |
zipcoords <- reverse_zipcode(zipdata$zip) %>% | |
select(zipcode, lat, lng) %>% | |
rename(zip = zipcode) | |
zipdata <- left_join(zipdata, zipcoords, by = "zip") %>% drop_na() | |
zipdata <- zipdata[order(zipdata$ratio),] | |
# keep track of year and return | |
zipdata$year = year | |
return(zipdata) | |
})) | |
library(ggplot2) | |
library(scales) | |
library(maps) | |
library(gganimate) | |
# get map to visualize on US map | |
us_map <- map_data("state") | |
# plot using ggplot and gganimate | |
gg <- ggplot() + | |
geom_polygon(data = us_map, aes(x = long, y = lat, group = group), fill = "gray95", color = "white") + | |
geom_point(data = alldata, aes(x = lng, y = lat, size=size, col=ratio, group=name), alpha = 0.7) + | |
scale_color_gradient2(low = 'blue', mid = 'grey', high='red', limits=c(-2,2), oob=squish) + # squish extreme values | |
scale_size_binned() + | |
coord_quickmap(xlim = c(-125, -66), ylim = c(25, 50)) + ## restrict to continental US | |
theme_void(base_size = 10) + | |
theme(plot.title = element_text(hjust = 0.5, size=16), | |
legend.position="bottom", | |
legend.box = "vertical", # stack legend on top of each other | |
legend.key.width = unit(1, "cm"), | |
plot.margin = margin(1,2,2,1, "cm")) + | |
labs(title = "ICE Detention Facilities Across the USA in {closest_state}", | |
x = "Longitude", y = "Latitude", | |
color = "Log2(Male Non-Crim / Male Crim) Detainees", | |
size = "Number of Total Detainees") + | |
transition_states(year, transition_length = 1, state_length = 3) + | |
enter_fade() + | |
exit_fade() | |
animate(gg, width=600) |
Author
JEFworks
commented
Jul 11, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment