Why Bullet Voting is probably a bad idea

The answer: Combinatorics.

Different kind of post today. Time for some game theory (or just math? I dunno.)

Commissioner Schmidt tweeted this on Tuesday.

It references Bullet Voting, a topic his Office has covered in great detail before. Bullet voting is the act of voting for a single candidate even when you’re allowed to vote for more. A lot of voters do it, either because they think it gives their favorite candidate a better chance to win, or as a cathartic experience. According to another infographic, 19% of Democrats voted for a single At Large candidate on May 21st.

Today, I argue that Bullet Voting is almost always sub-optimal. To make Bullet Voting the right decision, you don’t just need to have a candidate that you love, but a candidate that you love many times more than your second favorite candidates, because you’re hurting your foregone candidates in many more scenarios than you’re helping your favorite one.

Let’s do some math.

Three-Candidate Example

First, consider a simple race.

Suppose there are three candidates for two positions: A , B , and C . A is your favorite, B your second favorite, and C your least.

Write the value you get from two candidates winning as V(A,B) (if e.g. A and B won). Let’s make a simplifying assumption that

V(A,B)=V(A)+V(B) ,

that is, the value of winners is independent of each other, is additive, and the order of their win doesn’t matter. This doesn’t seem too restrictive, but it does mean you don’t have either (a) teams of candidates who are better together than separate, or (b) diminishing returns to candidates, so having two A s really would be twice as good as having one.

The question: Should you vote for both A and B, or bullet vote for A? Assume that in the case of a tie, a coin is flipped for the winner. (I’ll ignore three-way ties).

It helps to frame it as the following: suppose everyone else in the city has voted. You don’t know what the results are, but it’s done. Your choice of bullet voting for A instead of voting for A and B could have the following effects, and only the following effects:

  • It could win the election for A if A is losing to B by 1 vote or tied.
  • It could lose the election for B if B is losing to C by 1 vote or tied.

That’s it. Bullet voting helps your favorite candidate beat your second tier candidate, but your foregone vote harms your second tier candidate against the third. Importantly, notice that it has no effect on whether A beats C, head-to-head.

This means the expected benefit from bullet voting is

0.5 (V(A)−V(B)) p(C, B>A) − 0.5 (V(B)−V(C)) p(A,C>B)

where the notation p(C, B>A) is the probability that among everyone else’s votes, C is in first, and A is either tied with or one vote behind B . (The 0.5 comes from the coin flip. If there is a 4% chance that A ties B, A would still win half of those, so your vote only increases A‘s win probability by 2%). To interpret the equation above, bullet voting for A increases your value by the value-add of A over B if A is losing to B by one vote, but costs you the value of B over C if B is losing to C by one vote. The choice has no effect in any other ordering. Let’s drop the 0.5, since that multiplies everything, and call the marginal value of the bullet vote:

Marginal Value of Bullet Vote = (V(A)−V(B)) p(C,B>A) − (V(B)−V(C)) p(A,C>B).

You should bullet vote whenever this value is positive.

Consider some cases. (1) If all candidates have equal chances of winning, so that p(C,B>A)=p(A,C>B), then you should bullet vote when V(A)−V(B) > V(B)−V(C), i.e. when the gap in value between A and B is larger than the gap in value between B and C. If you have a strong preference for A, but B and C are interchangeable, then bullet vote for A. But if what really matters is keeping C out of office, then vote for A and B.

(2) If C is a runaway winner, so that p(C,B>A) is much larger than p(A,C>B), then you should bullet vote for A; it’s basically a two-person race.

With more candidates, bullet voting becomes less appealing.

Now, suppose candidate D joins the race. D is somehow even less preferable than C. We will still only have two winners.

Bullet voting for A still only makes a positive difference in the case where A is losing to B by one vote or tied, but foregoing voting for B now hurts you when B is losing to C *or* D by one vote or tied.

In this case, the marginal value of bullet voting is

E[V|Bullet vote for A]−E[V|Vote for both A and B]=
(V(A) − V(B)) (p(C, B>A, D) + p(D, B>A, C))
−(V(B) − V(C)) (p(A, C>B, D) + p(D, C>B, A))
−(V(B) − V(D)) (p(A, D>B, C) + p(C, D>B, A))

This is messy, but it’s basically the difference in value between A and B times the probability that your vote would prove decisive in A beating B, minus the differences in value between B and C, and B and D, times the probabilities your vote would be decisive there. Notice that we don’t need to include cases where A loses to C or D by one vote; your decision to bullet vote or not doesn’t change those head-to-head matchups.

Notice that the value math has completely changed. Now, if all the orderings of candidates are equally probable, the value-add of A over B would need to be twice as large as the value-add of B over C/D because your additional vote for B is decisive in twice as many scenarios as your bullet vote for A . This is the key: the power of the combinatorics means that bullet voting harms your second-favorite candidates in many times more scenarios than it helps your favorite one. And this is why bullet voting is usually a bad idea; it only makes sense if the value-add of your preferred candidate versus your second favorites is many times larger than the value-add of your second favorites versus the rest.

Consider some numbers. Suppose you think candidate A gives you a value of 10, B a value of 4, and C and D are worthless at 0. (The value can represent anything: good to the world, improvement to your life. Whatever you optimize for.) And suppose you think all candidates have equal chances of winning. Should you bullet vote for the candidate you love, A? No. That only helps in the case where A is losing to B by one vote among everyone else, in which case it gives you +6 value. But your foregone vote for B costs you -4 if they lose to C or D, and that is twice as likely to happen: when B loses by a vote to C, and when B loses by a vote to D.

5 slots, 10 candidates

Now let’s consider a situation close to the City Council race on May 21st. Suppose there is a single candidate, A, you love. There are four candidates, B, C, D, and E, who are good. There are five candidates, F, G, H, I, and J, that you dislike. The race actually had more candidates, but suppose only these ten had plausible chances. Let’s further assume that among these candidates, all possible election orderings are equally possible.

Bullet voting for A helps you when B, C, D, or E are beating A by one vote. But it hurts you when any of B, C, D, or E are losing to any of F, G, H, I, or J. Thats 4 scenarios where it helps, but 20 scenarios where it hurts you. Bullet Voting only makes sense if the average gap in your preferences for A vs B/C/D/E is five times larger than the average gap in your preferences for B/C/D/E vs F/G/H/I/J.

You can complicate this by giving each ordering of candidates different probabilities of occuring, but the math of the combinatorics will probably swamp whatever you come up with: the chances that one of your second tier candidates is losing to a third tier candidate will almost always be many times greater than the probability your favorite candidate is losing to a second tier candidate. And Bullet Voting only helps in that second scenario. It only makes sense to bullet vote if the gaps in your preferences are equally disproportionate in the opposite direction.

Note: While I think it’s unlikely that the gap between candidates A and B is five times larger than the gap between B and F, it certainly isn’t impossible. I definitely knew voters in the Primary for whom the value of their personal favorite candidate was five times larger than the gap between their second favorite and sixth; for them, bullet voting was optimal. But it comes at a steep cost.

General Solution

More generally, suppose you are able to vote for K out of C candidates. You have a single favorite candidate, K−1 second favorites, and CK left over candidates. Let them be ordered, and indexed by i, so i=1 is your favorite and i=C your least favorite. If you bullet vote, you decrease the likelihood that your favorite loses to your second favorites, at the cost of increasing the chances that your second favorites lose to the leftovers.

The value of the Bullet Vote, versus voting for all K, is

where $p_{ji}$ is the probability that among all voters but yourself, candidate j is in Kth place and is tied with i or beating them by a single vote (it’s all of the combinations of P(A>B>…) from above.) I’ve again divided everything by 0.5 for simplicity. Notice that the first summation has K−1 terms, while the second has (K−1)(CK) terms. If the probabilities are all equal, the average difference in values in the first sum must be CK times larger than the average difference in values in the second sum for bullet voting to be optimal.

The exact solution for a given race will depend on (a) the gap between your preferred candidate and the others you would vote for, (b) the gap between those others and the candidates you don’t want to win, and (c) the relative probabilities of everyone winning. But the combinatorics will be salient in every scenario.

Example: Third Party Voting

In November’s City Council election, there will be five Democrats, five Republicans, and a number of third party candidates on the ballot for seven At Large spots. Voters can vote for up to five candidates, and the Philadelphia Charter stipulates that no more than five winners can come from the same party. This usually means the five Democrats win in a landslide, and two of the Republicans win. Suppose you wanted to minimize the chance that a Republican would win a seat. What would be the optimal strategy?

Finding one: Bullet Voting doesn’t help. Voters might think Bullet Voting for two third party candidates helps them more than voting for three Democrats and two third party candidates. This isn’t right. Remember that bullet voting only help the candidates you do vote for against the candidates you would have voted for. Here, bullet voting would help the third party candidates if they came within one vote of the Democrat you would vote for. They have the same chance of beating the Republican if you spend a vote on the Democrats or not.

Finding two: Voting for Democrats is also wrong. If you think there is a 100% chance that all the Democrats will win (which there is), then notice that all of their pp s in the sums above are zero: there is no chance that they happen to lose to someone else by a single vote. Thus, it doesn’t help to vote for them.

So it doesn’t help to bullet vote, but it also isn’t optimal to vote for Democrats. What should you do? The best way to beat all the Republicans is to spend all of your votes on third party candidates; this eliminates the chance that you happen to bank on the wrong one, with no effect on the Democrats who are guaranteed to win. If there aren’t enough third party candidates to spend all five of your votes on, you can spend one on your favorite Democrat; it doesn’t make a difference either way (see above).

(Of course, this assumes that you do prefer each of the third party candidates to the Republicans. If you prefer a Republican to a third party candidate, you would have to work out all the summations above to decide if you should vote for that Republican or just withhold a vote.)

So, should you Bullet Vote?

Probably not.

How did they win? A look at 2019’s Voting Blocs.

On election night and the next day, I made some quick maps of the results. There were obvious patterns: the Democratic endorsees won largely on the back of traditional Party strongholds, Helen Gym did well everywhere, Justin looked like Gym from four years ago.

Today, I’m going to take those broad patterns and distill them down. Where did each candidate do well? What cohorts were decisive? (I’m not going to reproduce the maps, click through to the post for them.)

View code
library(tidyverse)
library(sf)
select <- dplyr::select

setwd("C:/Users/Jonathan Tannen/Dropbox/sixty_six/posts/quick_election_night_maps/")
source("../../admin_scripts/util.R")

filename <- max(list.files("../election_night_needle/raw_data/"))

df <- read_delim(
  paste0("../election_night_needle/raw_data/", filename),
  delim = "@"
) %>%
  rename(
    OFFICE = `Office_Prop Name`,
    candidate = Tape_Text,
    warddiv = Precinct_Name
  )

df <- df[-(nrow(df) - 0:1),]

df <- df %>% group_by(warddiv) %>%
  filter(sum(Vote_Count) > 0) %>%
  group_by()

turnout_19 <- df %>%
  filter(OFFICE == "MAYOR-DEM") %>%
  group_by(warddiv) %>%
  summarise(turnout = sum(Vote_Count))

df <- df %>% 
  filter(!grepl("Write", candidate, ignore.case=TRUE))

cand_rank <- df %>% 
  group_by(candidate, OFFICE) %>%
  summarise(votes=sum(Vote_Count)) %>%
  arrange(desc(votes)) %>%
  group_by(OFFICE) %>%
  mutate(office_rank = rank(desc(votes)))
cand_order <- cand_rank$candidate

df <- df %>%
  group_by(warddiv, OFFICE) %>%
  mutate(pvote = Vote_Count / sum(Vote_Count)) %>%
  group_by() %>%
  left_join(cand_rank)

divs <- st_read("../../data/gis/2019/Political_Divisions.shp")

divs <- st_transform(divs, 2272) %>%
  mutate(
    warddiv = paste0(
      substr(DIVISION_N, 1, 2), 
      "-", 
      substr(DIVISION_N,3,4)
    )
  )

saved_covars <- safe_load("../election_night_needle/saved_covars_logTRUE_2012after.Rda")
divs_to_council <- safe_load("../election_night_needle/divs_to_council.Rda")

turnout_cov <- saved_covars$turnout_cov_dem
pvote_cov <- saved_covars$pvote_cov
svd <- saved_covars$svd

First, let’s divide the city up into regions. I’ll use the Singular Value Decomposition. The SVD is a method that assigns a single score to each precinct, and chooses the best score that minimizes the variance left over (I’ll actually use the three best dimensions). When one division turns out especially strongly, the others with similar scores will too. If that division loves a candidate, the others will too. It’s the method at the core of the Turnout Tracker and The Needle.

The method identifies the most important “dimensions” of the vote in order of importance. Here are the results for Philadelphia.

View code
divs <- arrange(divs, DIVISION_N)
divs_svd <- cbind(divs, svd$u[,-1])

ggplot(
  divs_svd %>% gather("dimension", "score", X1:X4) %>% 
    mutate(dimension = paste0("Dimension ", substr(dimension,2,2)))
  ) +
  geom_sf(aes(fill=score), color=NA) +
  scale_fill_gradient2(
    "Dimension\n Score",
    low=strong_blue, high=strong_red, mid="white"
  ) +
  facet_wrap(~dimension) +
  theme_map_sixtysix() %+replace%
  theme(legend.position = "right") +
  ggtitle("Dimensions of Philadelphia's Votes")

The dimension scores don’t know anything about the city’s demographics–SVD just identifies divisions that vote similarly–but to a Philadelphian’s eyes the story is clear. The vote is best explained by race and class. The first dimension that the SVD discovered divides Philadelphia’s Black neighborhoods from its White ones, the second divides wealthy neighborhoods from non-wealthy ones, and the third divides the Hispanic section of North Philly from everywhere else. The fourth looks like noise to my eyes, so we’ll just use the three. I’ll use these scores to divide up the city into four sections, named after the obvious demographics they represent.

(A note about naming: since writing this post, I’ve changed the way I name the dimensions. I’ve left the old names in this post, but see the discussion.)

Here are the city’s sections. Remember, I’m not actually using Census data to identify them. I’m finding divisions that vote similarly, and then attaching the names post-hoc.

I’m using only data from 2012 and later for this, to capture recent changes. This does make the regions noisy, but bear with me.

View code
library(ggmap)

divs_svd$cat <- NA

divs_svd$cat[divs_svd$X3 < -0.03] <- "Hispanic North Philly"
divs_svd$cat[divs_svd$X2 > 0.025] <- "Wealthy"
divs_svd$cat[divs_svd$X1 < 0.00 & is.na(divs_svd$cat)] <- "White non-wealthy"
divs_svd$cat[is.na(divs_svd$cat)] <- "Black non-wealthy"             

divs_svd$cat <- factor(
  divs_svd$cat,
  levels = c(
    "Black non-wealthy",
    "Wealthy",
    "White non-wealthy",
    "Hispanic North Philly"
  )
)

cat_colors <- c(light_blue, light_red, light_orange, light_green)
names(cat_colors) <- levels(divs_svd$cat)             

phila_bbox <- st_bbox(st_transform(divs, 4326))
names(phila_bbox) <- c("left", "bottom", "right", "top") 
phila_map <- ggmap::get_map(
  location = phila_bbox,
  maptype="toner-lite"  
)

ggmap(phila_map) +
  geom_sf(
    data=divs_svd %>% st_transform(4326),
    aes(fill=cat), color=NA,
    alpha=0.7,
    inherit.aes = F
  ) +
  theme_map_sixtysix() +
  scale_fill_manual("", values=cat_colors) +
  ggtitle(
    "Philadelphia's Geographic Voting Blocs"
  )

The Black non-wealthy divisions made up 49.7% of the Democratic Primary votes on Tuesday.

View code
div_cats <- divs_svd %>%
  as.data.frame() %>% 
  select(DIVISION_N, cat) %>%
  rename(warddiv = DIVISION_N) %>%
  mutate(warddiv = paste0(substr(warddiv,1,2),"-",substr(warddiv,3,4)))

turnout_by_cat_19 <- turnout_19 %>% left_join(div_cats) %>%
  group_by(cat) %>%
  summarise(turnout = sum(turnout))

ggplot(
  turnout_by_cat_19,
  aes(x=cat, y=turnout)
) +
  geom_bar(aes(fill = cat), color=NA, stat="identity") +
  theme_sixtysix() +
  scale_fill_manual("", values=cat_colors, guide=FALSE) +
  scale_y_continuous("Votes cast for Mayor (D)", labels=scales::comma) +
  xlab("") +
  ggtitle("Turnout in the 2019 Democratic Primary")

That represents a reversion of turnout share to 2014-2015-2016 levels, and not a repeat of the 2017-2018 levels. Black neighborhoods made up 50% of the vote, versus only 46% in 2017/2018; wealthy neighborhoods made up 26%, down from their 2017/2018 highs of 29/30%, but not quite down to the pre-2017 21%.

View code
format_name <- function(x){
  x <- gsub("(^\\s+)|(\\s+$)", "", x)
  x <- gsub("\\s+", " ", x)
  x <- gsub("\\b([A-Za-z])([A-Za-z]+)\\b", "\\U\\1\\L\\2", x, perl = TRUE)
  x <- gsub("(.*),.*", "\\1", x)
  x[x == "Almiron"] <- "Almirón"
  x[x == "Diberardinis"] <- "DiBerardinis"
  return(x)
}

df_with_cat <- df %>% 
  filter(OFFICE == "COUNCIL AT LARGE-DEM") %>%
  left_join(
    divs_svd %>% as.data.frame() %>% select(warddiv, cat) 
  ) %>%
  mutate(candidate = format_name(candidate))

df_past <- safe_load("../../data/processed_data/df_major_2019_05_14.Rda")
df_past<- df_past %>%
  filter(is_primary_office & election == "primary" & grepl("DEM", PARTY)) %>%
  mutate(warddiv = paste0(WARD19, "-", DIV19)) %>%
  group_by(warddiv, year) %>%
  summarise(turnout = sum(VOTES))

df_past <- bind_rows(df_past, turnout_19 %>% mutate(year = "2019"))

df_past <- df_past %>%
  left_join(df_with_cat %>% as.data.frame() %>% select(warddiv, cat))


turnout_by_cat <- df_past %>%
    group_by(year, cat) %>%
    summarise(turnout=sum(turnout)) %>%
    group_by(year) %>%
    mutate(prop = turnout/sum(turnout))

ggplot(
  turnout_by_cat,
  aes(x=year, y=prop*100, color = cat)
) +
  geom_line(aes(group=cat), size=2) +
  geom_point(data = turnout_by_cat %>% filter(year == 2019), size = 4) +
  annotate(
    geom="text",
    x = "2007",
    y = c(53, 16, 32, 8),
    label = names(cat_colors),
    color=cat_colors,
    fontface="bold"
  ) +
  scale_color_manual("", values=cat_colors, guide=FALSE) +
  scale_y_continuous("Percentage of total turnout") +
  scale_x_discrete("") +
  # facet_wrap(~year) +
  theme_sixtysix() %+replace%
  theme(axis.text.x = element_text(angle = 90)) +
  ggtitle("Turnout in Democratic Primaries by Neighborhood Group")

One important thing to remember is that Tuesday’s reversion in vote share happened amidst stunningly high turnout for an election with an incumbent Mayor; the story isn’t that turnout in the wealthy neighborhoods lagged, but that excitement in Black wards caught up.

Now for the telling part: how did the candidates fare in each of these blocks?

View code
df_with_cat$candidate <- factor(
  df_with_cat$candidate, 
  levels=unique(df_with_cat$candidate[order(df_with_cat$office_rank)])
)

ggplot(
  df_with_cat %>% 
    filter(office_rank <= 12) %>%
    group_by(candidate, cat) %>%
    summarise(votes = sum(Vote_Count)) %>%
    group_by(cat) %>%
    mutate(pvote = 100 * votes / sum(votes)),
  aes(x=cat, y=pvote)
) + 
  geom_bar(stat="identity", aes(fill=cat)) +
  scale_fill_manual("", values=c(light_blue, light_red, light_orange, light_green)) +
  facet_wrap(~candidate) +
  ylab("Percent of Vote") +
  xlab("") +
  theme_sixtysix() %+replace%
  theme(axis.text.x = element_blank()) +
  ggtitle("At Large Candidates' performance by voting bloc")

The two non-incumbents to win–Isaiah Thomas and Katherine Gilmore Richardson–did so with strong support from the Black neighborhoods. Their weakest results came from the wealthy ones. Meanwhile, three of the first four runners up have the exact same profile: DiBerardinis, Santamoor, and Almirón all did best in the wealthy wards and worst in the Black, non-wealthy ones. (Gym has a similar profile, but on steroids). Rivera Reyes and Alvarez dominated the Hispanic wards, but didn’t get enough traction elsewhere.

The Commissioners’ results divided the city even more clearly.

View code
commish_with_cat <- df %>% 
  filter(OFFICE == "CITY COMMISSIONERS-DEM") %>%
  left_join(
    divs_svd %>% as.data.frame() %>% select(warddiv, cat) 
  ) %>%
  mutate(candidate = format_name(candidate))

commish_with_cat$candidate <- factor(
  commish_with_cat$candidate, 
  levels=unique(commish_with_cat$candidate[order(commish_with_cat$office_rank)])
)

ggplot(
  commish_with_cat %>% 
    filter(office_rank <= 6) %>%
    group_by(candidate, cat) %>%
    summarise(votes = sum(Vote_Count)) %>%
    group_by(cat) %>%
    mutate(pvote = 100 * votes / sum(votes)),
  aes(x=cat, y=pvote)
) + 
  geom_bar(stat="identity", aes(fill=cat)) +
  scale_fill_manual("", values=c(light_blue, light_red, light_orange, light_green)) +
  facet_wrap(~candidate) +
  ylab("Percent of Vote") +
  xlab("") +
  theme_sixtysix() %+replace%
  theme(axis.text.x = element_blank()) +
  ggtitle("Commissioners' performance by voting bloc")

Sabir dominated Black and Hispanic neighborhoods, Deeley the White ones, and Williams and Devor the wealthy ones. Remember that Black non-wealthy divisions had 50% of the vote, White non-wealthy ones had 26%, wealthy 20%, and Hispanic 4%. With those shares, Sabir (25%) and Deeley (21%) won handily over Williams (17%) and Devor (8%).

The District 3 Surprise

The Wealthy voting bloc wasn’t entirely shut out on Tuesday. Gauthier won a surprise victory in West Philly’s 3rd District, with strong support from the wealthier divisions. (See the last post for the full maps).

In my pre-election analysis, I pointed out that recent increases in turnout made the University City vote much stronger (40% of the total district’s votes), and said Jamie would need to win 80% of the vote there, while winning 40% in the rest of the district. How did that fare?

Here are the categories for the district. Notice that these categories don’t perfectly match the West Philly/UCity divide from my pre-election post, because I created them in two different ways.

View code
d3 <- df %>%
  filter(OFFICE == "DISTRICT COUNCIL-3RD DISTRICT-DEM") %>%
  left_join(div_cats) %>%
  mutate(candidate = format_name(candidate))

d3_cat <- d3 %>%
  group_by(cat, candidate) %>%
  summarise(votes = sum(Vote_Count)) %>%
  group_by(cat) %>%
  mutate(
    turnout = sum(votes),
    pvote = votes / turnout
  )

d3_box <- divs %>% 
  st_transform(4326) %>%
    inner_join(
      df %>%
        filter(OFFICE == "DISTRICT COUNCIL-3RD DISTRICT-DEM") %>%
        select(warddiv) %>% unique
  ) %>% st_bbox() 
names(d3_box) <- c("left", "bottom", "right", "top")

wphilly_map <- ggmap::get_map(
  location = d3_box,
  maptype="toner-lite"  
)

ggmap::ggmap(
  wphilly_map,
) + 
  geom_sf(
    data = divs %>% st_transform(4326) %>%
      inner_join(
        df %>%
          filter(OFFICE == "DISTRICT COUNCIL-3RD DISTRICT-DEM") %>%
          select(warddiv) %>% unique
    ) %>%
      left_join(div_cats),
    inherit.aes=F,
    aes(fill=cat), color=NA,
    alpha = 0.7
  ) +
  theme_map_sixtysix() +
  scale_fill_manual("", values=cat_colors) +
  ggtitle(
    "District 3's Geographic Voting Blocs"
  ) 

Turnout in the District strongly reverted back to 2015 levels. Black wards turned out strongly in the district, and the wealthy divisions only represented 27% of the vote. (White non-wealthy and Hispanic divisions didn’t make up enough votes to merit plotting.)

View code
ggplot(
  d3_cat %>% select(cat, turnout) %>% unique %>% filter(turnout > 200),
  aes(x=cat, y=turnout)
) +
  geom_bar(aes(fill = cat), color=NA, stat="identity") +
  theme_sixtysix() +
  scale_fill_manual("", values=cat_colors, guide=FALSE) +
  scale_y_continuous("Votes cast for Council District 3", labels=scales::comma) +
  xlab("") +
  ggtitle("Turnout in District 3")

With that turnout like that, Jamie needed to do better than 40% in the Black districts, and she did.

View code
ggplot(
  d3_cat %>% 
    filter(as.numeric(cat) %in% 1:2),
  aes(x=cat, y=100 * pvote)
) + 
  geom_bar(stat="identity", aes(fill=cat)) +
  scale_fill_manual("", values=c(light_blue, light_red, light_orange, light_green)) +
  facet_wrap(~candidate) +
  ylab("Percent of Vote") +
  xlab("") +
  theme_sixtysix() %+replace%
  theme(axis.text.x = element_blank()) +
  ggtitle("At Large Candidates' performance by voting bloc")

Gauthier got 79% of the vote in the wealthier divisions of University City, but managed a whopping 47% of the vote in the District’s Black divisions. That was enough for a decisive 56-44 win.

What are we to make of this?

City-wide, Black neighborhoods turned out stronger than they had the last two primaries, reasserting their electoral power and pushing their preferred candidates over the finish line. DiBerardinis came closest to recreating Gym’s 2015 path to victory, but finished just short. Even in West Philly’s surprise upset, Jamie Gauthier’s win was largely enabled by surprising strength in the high-turnout West, Southwest, and Mantua divisions.

Coming Up

I’ll be looking at more aspects of the race in the weeks to come, including a post tentatively titled: “The Election Needle: Was it lucky? Or psychic?”. Stay tuned!