raw <- read.csv(
csv_file,
stringsAsFactors = FALSE,
check.names = FALSE
)
# The source contains one aggregate row. It is excluded because this
# analysis is explicitly precinct-level.
precinct_data <- raw %>%
filter(!is.na(Precinct), Precinct != "Total People")
# Convert percentage strings to proportions.
pct_cols <- names(precinct_data)[grepl("%", names(precinct_data), fixed = TRUE)]
precinct_data <- precinct_data %>%
mutate(across(all_of(pct_cols), pct_to_num),
MOV = pct_to_num(MOV))
# Signed percentage-point versions for presentation.
precinct_data <- precinct_data %>%
mutate(
Jones_pp = `Jones %` * 100,
MOV_pp = MOV * 100,
Turnout_pp = `Dem Turnout %` * 100,
Majority_White = `White Dem RV %` > .50,
Majority_Black = `Black Dem RV %` > .50
)
n_precincts <- nrow(precinct_data)
jones_wins <- sum(precinct_data$`Jones Win?` == "Y", na.rm = TRUE)
jones_losses <- sum(precinct_data$`Jones Win?` == "N", na.rm = TRUE)
ties <- sum(precinct_data$`Jones Win?` == "Tie", na.rm = TRUE)
# Strongest demographic associations. MOV is deliberately excluded because
# it is mathematically derived from candidate vote shares.
cor_vars <- c(
"White Dem PV %", "Black Dem PV%", "White Dem RV %", "Black Dem RV %",
"Dem RV %", "Male Dem PV %", "Female Dem PV %", "Dem Turnout %",
"18-24 Dem PV %", "25-34 Dem PV %", "35-49 Dem PV %",
"50-64 Dem PV %", "65+ Dem PV %", "Doorknock Attempts", "Phonebank Attempts"
)
cor_results <- map_dfr(cor_vars, function(v) {
x <- precinct_data[[v]]
y <- precinct_data$`Jones %`
keep <- complete.cases(x, y)
tibble(
Variable = v,
N = sum(keep),
r = cor(x[keep], y[keep], method = "pearson")
)
}) %>%
arrange(desc(abs(r)))
# Group-level summaries directly answer the two race-composition questions.
white_summary <- precinct_data %>%
summarise(
n = sum(Majority_White),
wins = sum(`Jones Win?`[Majority_White] == "Y", na.rm = TRUE),
mean_jones = mean(`Jones %`[Majority_White], na.rm = TRUE),
mean_mov = mean(MOV[Majority_White], na.rm = TRUE)
)
black_summary <- precinct_data %>%
summarise(
n = sum(Majority_Black),
wins = sum(`Jones Win?`[Majority_Black] == "Y", na.rm = TRUE),
mean_jones = mean(`Jones %`[Majority_Black], na.rm = TRUE),
mean_mov = mean(MOV[Majority_Black], na.rm = TRUE)
)
# Highest and lowest Jones precincts.
highest <- precinct_data %>% slice_max(`Jones %`, n = 1, with_ties = FALSE)
lowest <- precinct_data %>% slice_min(`Jones %`, n = 1, with_ties = FALSE)
summary_table <- tibble(
Metric = c(
"Precincts analyzed",
"Jones wins / losses / ties",
"Mean Jones vote share",
"Median Jones vote share",
"Mean precinct MOV",
"Majority-Black precincts: wins / total",
"Majority-White precincts: wins / total",
"Strongest demographic association with Jones %",
"Weakest of the tested turnout/contact relationships",
"Highest Jones precinct",
"Lowest Jones precinct"
),
Result = c(
n_precincts,
paste(jones_wins, "/", jones_losses, "/", ties),
percent(mean(precinct_data$`Jones %`), accuracy = .1),
percent(median(precinct_data$`Jones %`), accuracy = .1),
paste0(round(mean(precinct_data$MOV_pp), 1), " pp"),
paste0(black_summary$wins, " / ", black_summary$n),
paste0(white_summary$wins, " / ", white_summary$n),
cor_results$Variable[1],
cor_results %>%
filter(Variable %in% c("Dem Turnout %", "Doorknock Attempts", "Phonebank Attempts")) %>%
slice_min(abs(r), n = 1) %>%
pull(Variable),
paste0(highest$Precinct, " (", percent(highest$`Jones %`), ")"),
paste0(lowest$Precinct, " (", percent(lowest$`Jones %`), ")")
)
)
kable(summary_table, caption = "What the precinct data says at a glance") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
| Metric | Result |
|---|---|
| Precincts analyzed | 258 |
| Jones wins / losses / ties | 131 / 122 / 5 |
| Mean Jones vote share | 34.7% |
| Median Jones vote share | 31.2% |
| Mean precinct MOV | 2.5 pp |
| Majority-Black precincts: wins / total | 65 / 68 |
| Majority-White precincts: wins / total | 47 / 166 |
| Strongest demographic association with Jones % | White Dem PV % |
| Weakest of the tested turnout/contact relationships | Dem Turnout % |
| Highest Jones precinct | 02-031 (75%) |
| Lowest Jones precinct | 01-022 (0%) |
It can answer: where Jones performed strongly or weakly, how precinct results relate to demographic composition and turnout, which precincts are unusual relative to the overall pattern, and where campaign-contact activity was associated with the result.
It cannot answer by itself: why an individual voter supported a candidate, whether a campaign contact caused a vote, or whether a precinct-level relationship would hold for individual voters. Those questions require voter-level or survey data.
This is the geographic overview. Blue indicates a Jones win, red a Jones loss, and white a near-tie. The value is the percentage-point margin separating Jones from the next-highest candidate.
precinct_shapes <- st_read(geojson_file, quiet = TRUE)
precinct_map <- precinct_shapes %>%
mutate(Precinct = VOTINGDISTRICTS) %>%
left_join(precinct_data, by = "Precinct")
ggplot(precinct_map) +
geom_sf(aes(fill = MOV_pp), color = "white", linewidth = .15) +
scale_fill_gradient2(
low = "#B2182B", mid = "white", high = "#2166AC", midpoint = 0,
name = "Jones MOV (pp)"
) +
labs(
title = "Julian Jones Margin of Victory by Precinct",
subtitle = "Positive = Jones win; negative = Jones loss"
) +
theme_void()
ggplot(precinct_map) +
geom_sf(fill = "grey85", color = "white", linewidth = .15) +
geom_sf(
data = precinct_map %>% filter(Majority_Black),
aes(fill = MOV_pp), color = "white", linewidth = .15
) +
scale_fill_gradient2(
low = "#B2182B", mid = "white", high = "#2166AC", midpoint = 0,
name = "Jones MOV (pp)"
) +
labs(
title = "Jones Margin of Victory in Majority-Black Precincts",
subtitle = "Black Democratic RV share > 50%; all other precincts are gray"
) +
theme_void()
ggplot(precinct_map) +
geom_sf(fill = "grey85", color = "white", linewidth = .15) +
geom_sf(
data = precinct_map %>% filter(Majority_White),
aes(fill = MOV_pp), color = "white", linewidth = .15
) +
scale_fill_gradient2(
low = "#B2182B", mid = "white", high = "#2166AC", midpoint = 0,
name = "Jones MOV (pp)"
) +
labs(
title = "Jones Margin of Victory in Majority-White Precincts",
subtitle = "White Democratic RV share > 50%; all other precincts are gray"
) +
theme_void()
Turnout has a weaker relationship with Jones vote share than racial composition. That distinction matters: a precinct can have high turnout and still produce a very different Jones result depending on who makes up the Democratic electorate.
turnout_model <- lm(`Jones %` ~ `Dem Turnout %`, data = precinct_data)
turnout_plot_data <- precinct_data %>%
mutate(
std_residual = rstandard(turnout_model),
outlier = abs(std_residual) > 2
)
ggplot(turnout_plot_data,
aes(`Dem Turnout %`, `Jones %`, color = `Jones Win?`)) +
geom_point(aes(size = `Dem Primary Voters`), alpha = .7) +
geom_smooth(method = "lm", se = TRUE, color = "black") +
geom_text_repel(
data = turnout_plot_data %>% filter(outlier),
aes(label = Precinct), color = "black", size = 3, max.overlaps = Inf
) +
scale_x_continuous(labels = percent_format(accuracy = 1)) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
scale_color_manual(values = outcome_colors, labels = outcome_labels, name = NULL) +
scale_size_continuous(name = "Democratic\nprimary voters", range = c(1.5, 6)) +
labs(
title = "Jones Vote Share vs. Democratic Primary Turnout",
subtitle = "Labels identify unusually high/low results relative to the fitted relationship",
x = "Democratic primary turnout",
y = "Jones vote share"
)
age_data <- precinct_data %>%
select(
Precinct, `Jones %`,
`18-24 Dem PV %`, `25-34 Dem PV %`, `35-49 Dem PV %`,
`50-64 Dem PV %`, `65+ Dem PV %`
) %>%
pivot_longer(
cols = -c(Precinct, `Jones %`),
names_to = "Age Group",
values_to = "Age Share"
)
ggplot(age_data, aes(`Age Share`, `Jones %`)) +
geom_point(alpha = .55, size = 2) +
geom_smooth(method = "lm", se = TRUE, color = "black") +
facet_wrap(~ `Age Group`, scales = "free_x") +
scale_x_continuous(labels = percent_format(accuracy = 1)) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
labs(
title = "Jones Vote Share vs. Age Composition",
subtitle = "Age-group relationships are substantially weaker than the racial-composition relationships",
x = "Share of Democratic primary voters",
y = "Jones vote share"
)
Campaign-contact variables deserve their own section because they are different from demographics. They can tell us whether where the campaign invested contact was related to the final result, but they cannot establish that the contact caused the result.
contact_results <- tibble(
Variable = c("Doorknock Attempts", "Phonebank Attempts"),
Correlation = c(
cor(precinct_data$`Doorknock Attempts`, precinct_data$`Jones %`, use = "complete.obs"),
cor(precinct_data$`Phonebank Attempts`, precinct_data$`Jones %`, use = "complete.obs")
)
) %>%
mutate(
Correlation = round(Correlation, 3)
)
kable(contact_results, caption = "Association between campaign contact volume and Jones vote share") %>%
kable_styling(full_width = FALSE)
| Variable | Correlation |
|---|---|
| Doorknock Attempts | 0.322 |
| Phonebank Attempts | 0.287 |
contact_data <- precinct_data %>%
select(Precinct, `Jones %`, `Doorknock Attempts`, `Phonebank Attempts`, `Jones Win?`) %>%
pivot_longer(
cols = c(`Doorknock Attempts`, `Phonebank Attempts`),
names_to = "Contact Type",
values_to = "Attempts"
)
ggplot(contact_data, aes(Attempts, `Jones %`, color = `Jones Win?`)) +
geom_point(alpha = .65, size = 2) +
geom_smooth(method = "lm", se = TRUE, color = "black") +
facet_wrap(~ `Contact Type`, scales = "free_x") +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
scale_color_manual(values = outcome_colors, labels = outcome_labels, name = NULL) +
labs(
title = "Campaign Contact Volume vs. Jones Vote Share",
subtitle = "Descriptive relationship; contact was not randomly assigned",
x = "Contact attempts",
y = "Jones vote share"
)
Rather than presenting a long list of “best” and “worst” precincts, this section identifies unusual precincts where the observed Jones result differs substantially from what the turnout relationship alone would predict.
These are places for additional investigation, not rankings.
unusual_precincts <- turnout_plot_data %>%
filter(outlier) %>%
arrange(desc(abs(std_residual))) %>%
select(
Precinct,
`Jones Win?`,
`Jones %`,
`Dem Turnout %`,
`Black Dem RV %`,
`White Dem RV %`,
`Doorknock Attempts`,
`Phonebank Attempts`,
std_residual
) %>%
mutate(
`Jones %` = percent(`Jones %`, accuracy = .1),
`Dem Turnout %` = percent(`Dem Turnout %`, accuracy = .1),
`Black Dem RV %` = percent(`Black Dem RV %`, accuracy = .1),
`White Dem RV %` = percent(`White Dem RV %`, accuracy = .1),
std_residual = round(std_residual, 2)
)
kable(
unusual_precincts,
col.names = c(
"Precinct", "Outcome", "Jones %", "Turnout", "Black Dem RV %",
"White Dem RV %", "Doors", "Phones", "Std. residual"
),
caption = "Precincts with unusually high or low Jones results relative to turnout"
) %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed"))
| Precinct | Outcome | Jones % | Turnout | Black Dem RV % | White Dem RV % | Doors | Phones | Std. residual |
|---|---|---|---|---|---|---|---|---|
| 03-015 | N | 0.0% | 8.3% | 8.3% | 75.0% | 0 | 3 | -2.71 |
| 02-020 | Y | 72.6% | 45.5% | 70.8% | 26.4% | 210 | 1100 | 2.71 |
| 02-018 | Y | 69.4% | 44.2% | 89.7% | 7.7% | 0 | 1214 | 2.48 |
| 02-027 | Y | 70.3% | 38.1% | 86.8% | 9.0% | 3 | 1692 | 2.37 |
| 02-024 | Y | 71.4% | 34.6% | 93.5% | 3.4% | 0 | 1538 | 2.35 |
| 02-010 | Y | 70.6% | 36.2% | 91.7% | 3.4% | 4 | 2646 | 2.34 |
| 02-005 | Y | 70.3% | 32.5% | 93.5% | 2.9% | 139 | 1383 | 2.22 |
| 02-031 | Y | 75.0% | 17.6% | 80.2% | 14.3% | 0 | 50 | 2.13 |
| 02-014 | Y | 66.7% | 37.4% | 89.3% | 7.0% | 0 | 2866 | 2.13 |
| 02-003 | Y | 70.3% | 28.9% | 82.0% | 9.8% | 1 | 223 | 2.13 |
| 02-017 | Y | 69.4% | 30.8% | 92.8% | 4.2% | 3 | 1942 | 2.12 |
| 04-015 | Y | 66.7% | 35.5% | 61.8% | 24.6% | 0 | 86 | 2.08 |
| 02-012 | Y | 67.6% | 32.5% | 89.2% | 7.6% | 211 | 2844 | 2.06 |
| 02-015 | Y | 67.4% | 33.0% | 81.6% | 12.1% | 307 | 3291 | 2.06 |
Total People row.MOV is the signed percentage-point difference between
Jones and the highest vote-getting opposing candidate in each
precinct.MOV is excluded from the explanatory correlation table
because it is mechanically derived from candidate vote shares.map_ids <- precinct_shapes %>% transmute(Precinct = VOTINGDISTRICTS)
quality_table <- tibble(
Check = c(
"Precinct records in CSV after removing aggregate row",
"Precinct polygons in GeoJSON",
"CSV precincts without map geometry",
"Map polygons without CSV data",
"Rows with missing Jones vote share",
"Rows with fewer than 25 Democratic primary voters"
),
Result = c(
nrow(precinct_data),
nrow(map_ids),
nrow(anti_join(precinct_data, map_ids, by = "Precinct")),
nrow(anti_join(map_ids, precinct_data %>% select(Precinct), by = "Precinct")),
sum(is.na(precinct_data$`Jones %`)),
sum(precinct_data$`Dem Primary Voters` < 25, na.rm = TRUE)
)
)
kable(quality_table, caption = "Data quality checks") %>%
kable_styling(full_width = FALSE)
| Check | Result |
|---|---|
| Precinct records in CSV after removing aggregate row | 258 |
| Precinct polygons in GeoJSON | 258 |
| CSV precincts without map geometry | 0 |
| Map polygons without CSV data | 0 |
| Rows with missing Jones vote share | 0 |
| Rows with fewer than 25 Democratic primary voters | 11 |