library(tidyverse)
counties = left_join(
read_csv("https://princeton-ddss.github.io/r-bootcamp/files/county_data.csv"),
read_csv("https://princeton-ddss.github.io/r-bootcamp/files/county_population.csv"),
join_by(GEOID == fips)
)Data visualization exercise: solutions
This document includes only one possible solution. Yours may vary.
Setup (given)
Part 1: the county data
Rent and income distributions by group
library(scales)
## 12 NA values. Fine.
counties |>
filter(!is.na(region)) |>
pivot_longer(c(income, rent)) |>
mutate(name = str_to_sentence(name)) |>
ggplot(aes(x = value, color = region)) +
geom_density(linewidth = 1) +
facet_wrap(~ name, scales = "free") +
labs(x = NULL, y = "Density", color = "Region") +
theme_bw(base_size = 20) +
theme(
legend.position = 'bottom',
axis.text.y = element_blank(),
strip.background = element_rect(
fill = 'white',
color = "black",
linewidth = 0
),
) +
scale_x_continuous(labels = label_dollar(scale_cut = cut_short_scale())) +
expand_limits(x = 0)Publication-ready
ggsave("rent_income_densities.pdf", width = 8, height = 4)Part 2: mapping America’s partisan shift
Fetching data
pres = read_csv("data/countypres_2000-2024.csv")
glimpse(pres)
count(pres, year)Rows: 94,151
Columns: 12
$ year <dbl> 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2…
$ state <chr> "ALABAMA", "ALABAMA", "ALABAMA", "ALABAMA", "ALABAMA", …
$ state_po <chr> "AL", "AL", "AL", "AL", "AL", "AL", "AL", "AL", "AL", "…
$ county_name <chr> "AUTAUGA", "AUTAUGA", "AUTAUGA", "AUTAUGA", "BALDWIN", …
$ county_fips <dbl> 1001, 1001, 1001, 1001, 1003, 1003, 1003, 1003, 1005, 1…
$ office <chr> "US PRESIDENT", "US PRESIDENT", "US PRESIDENT", "US PRE…
$ candidate <chr> "OTHER", "CHASE OLIVER", "KAMALA D HARRIS", "DONALD J T…
$ party <chr> "OTHER", "LIBERTARIAN", "DEMOCRAT", "REPUBLICAN", "OTHE…
$ candidatevotes <dbl> 293, 65, 7439, 20484, 1276, 95798, 24934, 241, 5606, 41…
$ totalvotes <dbl> 28281, 28281, 28281, 28281, 122249, 122249, 122249, 122…
$ version <dbl> 20260225, 20260225, 20260225, 20260225, 20260225, 20260…
$ mode <chr> "TOTAL", "TOTAL", "TOTAL", "TOTAL", "TOTAL", "TOTAL", "…
# A tibble: 7 × 2
year n
<dbl> <int>
1 2000 12628
2 2004 9474
3 2008 9474
4 2012 9474
5 2016 9474
6 2020 22093
7 2024 21534
Fixing problems
pres = pres |>
filter(!is.na(party), !is.na(county_fips)) |>
mutate(county_fips = str_pad(county_fips, width = 5, pad = "0"))
glimpse(pres)Rows: 93,598
Columns: 12
$ year <dbl> 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2…
$ state <chr> "ALABAMA", "ALABAMA", "ALABAMA", "ALABAMA", "ALABAMA", …
$ state_po <chr> "AL", "AL", "AL", "AL", "AL", "AL", "AL", "AL", "AL", "…
$ county_name <chr> "AUTAUGA", "AUTAUGA", "AUTAUGA", "AUTAUGA", "BALDWIN", …
$ county_fips <chr> "01001", "01001", "01001", "01001", "01003", "01003", "…
$ office <chr> "US PRESIDENT", "US PRESIDENT", "US PRESIDENT", "US PRE…
$ candidate <chr> "OTHER", "CHASE OLIVER", "KAMALA D HARRIS", "DONALD J T…
$ party <chr> "OTHER", "LIBERTARIAN", "DEMOCRAT", "REPUBLICAN", "OTHE…
$ candidatevotes <dbl> 293, 65, 7439, 20484, 1276, 95798, 24934, 241, 5606, 41…
$ totalvotes <dbl> 28281, 28281, 28281, 28281, 122249, 122249, 122249, 122…
$ version <dbl> 20260225, 20260225, 20260225, 20260225, 20260225, 20260…
$ mode <chr> "TOTAL", "TOTAL", "TOTAL", "TOTAL", "TOTAL", "TOTAL", "…
First, read_csv() guessed county_fips as a number (<dbl>). Second, the 2024 bookkeeping rows (TOTAL VOTES CAST, OVERVOTES, …) have an empty party, which read_csv() reads in as NA. County votes are overcalculated with these rows in place. We also drop the handful of state-level rows with no county_fips at all.
Visualizing county-level shifts
ggplot(rep_wide, aes(x = r2012, y = r2024)) +
geom_point(size = 0.5) +
geom_abline() +
labs(x = "Republican share, 2012", y = "Republican share, 2024",
title = "County vote shares, 2024 vs. 2012") +
theme_bw(base_size = 20)Mapping the shifts
library(sf)
shapes = st_read("https://princeton-ddss.github.io/r-bootcamp/files/county_shapes.geojson")shift_map = shapes |>
left_join(rep_wide, join_by(GEOID == county_fips))
ggplot(shift_map) +
geom_sf(aes(fill = shift), linewidth = 0.02) +
scale_fill_gradient2(
low = "blue", mid = "white", high = "red",
guide = guide_colorbar(
ticks.colour = "black", # Makes ticks stand out against light colors
ticks.linewidth = 1 # Makes the tick marks thicker
)
) +
labs(fill = "Shift in R share, 2012 to 2024") +
theme_void(base_size = 15) +
theme(legend.position = 'top')Part 3: Putting it together
Republican shifts and county-level income
shift_map = shift_map |>
left_join(
counties |> select(GEOID, income),
join_by(GEOID)
)
ggplot(shift_map, aes(x = income, y = shift)) +
geom_point(size = 0.5) +
geom_hline(yintercept = 0, linetype = "dotted") +
labs(
x = "Median household income ($)",
y = "Shift in Republican share,\n2012 - 2024",
title = "Median HH Income vs.\nShift in tpvs, 2012 - 2024"
) +
theme_minimal(base_size = 15) +
scale_x_continuous(labels = label_dollar(scale_cut = cut_short_scale()))