Visualizing the 2020 Georgia Races with R

Matt Thoburn
10 min readNov 29, 2020

--

Introduction

If you’ve turned on a TV or been on a news site in the last month you’ll no doubt be aware that Georgia has been, as the southerners would say, the belle of the ball this election cycle. Between Biden’s historic win (in that Georgia has only gone for two Democrats since 1964, their native son Jimmy Carter in 1976 and 1980 and fellow southerner Bill Clinton in 1992) and the almost theatrical way in which control of the senate has come down to two runoffs in a possibly new swing state (although it remains to be seen how swingy Georgia will continue to be in the future), you might say that everybody (or at least the political junkies) has Georgia on their minds.

This presents me with an opportunity to pull out a new R package and learn how to do things with maps. In this article we’ll take voting data from the presidential and senate elections and use it to compare candidates to each other and to their performances across different counties. Hopefully by doing so we can better understand how Biden pulled off his win in Georgia, where the senate races stand as of the November election, and what that might mean for the runoffs in January.

Data was taken from Politico’s website and combined into one spreadsheet. With all the recounts going on right now there might be some minor fluctuations between the numbers here and the final reporting, but these differences should be marginal if any. That being the case, if you see anything egregiously incorrect, let me know so I can correct it.

As usual, the data and code can be found on my Github.

Read the Data

library(usmap)
library(ggplot2)
ga <- read.table(‘Georgia.csv’,header = TRUE,sep=”,”)ga$BIDEN.VOTES <- as.numeric(gsub(“,”, “”,ga$BIDEN.VOTES))
ga$TRUMP.VOTES <- as.numeric(gsub(“,”, “”,ga$TRUMP.VOTES))
ga$OSSOFF.VOTES <- as.numeric(gsub(“,”, “”,ga$OSSOFF.VOTES))
ga$PERDUE.VOTES <- as.numeric(gsub(“,”, “”,ga$PERDUE.VOTES))
ga$WARNOCK.VOTES <- as.numeric(gsub(“,”, “”,ga$WARNOCK.VOTES))
ga$LOEFFLER.VOTES <- as.numeric(gsub(“,”, “”,ga$LOEFFLER.VOTES))
head(ga)
##            COUNTY  fips BIDEN.VOTES BIDEN.PCT TRUMP.VOTES TRUMP.PCT
## 1 Appling County 13001 1779 21.3 6526 78.2
## 2 Atkinson County 13003 825 26.1 2300 72.9
## 3 Bacon County 13005 625 13.4 4018 86.1
## 4 Baker County 13007 652 41.9 897 57.7
## 5 Baldwin County 13009 9140 50.1 8903 48.8
## 6 Banks County 13011 932 10.6 7795 88.6
## PERDUE.VOTES PERDUE.PCT OSSOFF.VOTES OSSOFF.PCT WARNOCK.VOTES WARNOCK.PCT
## 1 6306 77.0 1753 21.4 1051 12.9
## 2 2272 73.4 773 25.0 406 13.6
## 3 3929 85.6 591 12.9 315 7.0
## 4 873 56.9 648 42.2 457 30.2
## 5 8873 49.1 8783 48.6 6167 34.4
## 6 7636 87.5 899 10.3 604 7.0
## LOEFFLER.VOTES LOEFFLER.PCT
## 1 1807 22.1
## 2 946 31.6
## 3 1547 34.3
## 4 413 27.3
## 5 4639 25.9
## 6 2605 30.0

Discussion

Reading in the data, we see the number of votes by county, percent vote share for each candidate, as well as the FIPS code for each county which will be used by R for plotting purposes shortly. Its worth noting that two rows are missing for Rev. Warnock in counties where he was out of the top-two choices in the special election.

Lets begin by comparing the total number of votes for each candidate to get an overall sense of where the races stand before diving into county-by-county breakdowns.

Comparing Candidates Overall

# Count sum of votes by candidate
biden.sum <- sum(ga$BIDEN.VOTES)
trump.sum <- sum(ga$TRUMP.VOTES)
ossoff.sum <- sum(ga$OSSOFF.VOTES)
perdue.sum <- sum(ga$PERDUE.VOTES)
warnock.sum <- sum(ga$WARNOCK.VOTES,na.rm=T)
loeffler.sum <- sum(ga$LOEFFLER.VOTES,na.rm=T)
# Make a Dataframe
candidate <- c(‘biden’,’trump’,’ossoff’,’perdue’,’warnock’,’loeffler’)
sum <- c(biden.sum, trump.sum, ossoff.sum, perdue.sum, warnock.sum, loeffler.sum)
party <- c(‘D’,’R’,’D’,’R’,’D’,’R’)
sum.df <- data.frame(candidate,sum,party)
sum.df
sum.df$candidate <- factor(sum.df$candidate, levels = c(sum.df[order(sum.df$sum,decreasing=T),]$candidate))
# Graph
ggplot(data=sum.df, aes(x=candidate,y=sum,fill=party)) + geom_bar(stat=”identity”) + scale_fill_manual(values = c(‘#00BFC4’,’#F8766D’))
Total sum of votes for each candidate
# Lets get a closer look at the top four
ggplot(data=sum.df[c(1,2,3,4),], aes(x=candidate,y=sum,fill=party)) + geom_bar(stat=”identity”) + scale_fill_manual(values = c(‘#00BFC4’,’#F8766D’)) + coord_cartesian(ylim = c(2000000,2500000))
Total sum of votes for top four candidates

Discussion

After doing some data manipulation, we can plot the total number of votes for each candidate and get an appreciation for how close these races were, particularly at the presidential level. Biden came in first among candidates overall with Perdue ever so slightly ahead of Trump (about a thousand votes, give or take) and Ossoff coming in slightly further behind in fourth place. Warnock and Loeffler are a distant 5th and 6th, although this is unsurprising given the nature of their special election and the number of other candidates. I imagine in the runoff their votes will consolidate to be much closer to what we see in the other races.

Examining the Presidential Race

To better understands Biden’s win in Georgia we can visualize the results by county using the plot_usmap function, which extends ggplot.

The first plot shows the percent difference in vote share for each county, and the second plot shows the difference in raw votes, so as to de-emphasize the differences in smaller less populous counties. In each plot the degree of shading reflects the degree of difference for each metric.

# Store differences as new columns
ga$BIDEN.TRUMP.VOTE.DIFF <- ga$BIDEN.VOTES — ga$TRUMP.VOTES
ga$BIDEN.TRUMP.PCT.DIFF <- ga$BIDEN.PCT — ga$TRUMP.PCT
# Graph
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”BIDEN.TRUMP.PCT.DIFF”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=0,name=”Biden — Trump Percent Votes”) + theme(legend.position = “right”)
Comparing Trump and Biden Percent Vote Share
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”BIDEN.TRUMP.VOTE.DIFF”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=0,name=”Biden — Trump Raw Votes”) + theme(legend.position = “right”)
Comparing Trump and Biden Raw Vote Difference

Discussion

Looking at the first plot, we can see divisions between the Atlanta Metro Area and the surrounding counties in northern Georgia, where the Black Belt cuts through mid Georgia (Which includes cities such as Columbus, Macon, and Augusta), as well as the more isolated counties such as Clarke County (which contains Athens, home to the University of Georgia), and the counties around Savannah.

However land doesn’t vote, people do. In order to get a better sense of how this map translated into a win for Biden its worth looking at the second plot, which shows the difference in raw votes. From this plot we can see that while Biden lost to Trump in a majority of counties, he ran up massive margins in the city of Atlanta (Atlanta straddles Fulton and DeKalb counties, shown in dark blue), as well as the suburban counties like Cobb, Gwinnett, and Clayton.

It’s worth noting that many of these suburban counties around Atlanta used to be solidly Republican. Former house speaker and general blight on healthy political discourse Newt Gingrich represented the 6th district from Marietta, a suburb north of Atlanta (Trivia tidbit: Gingrich and I share the same alma mater). That these counties have moved towards Democrats in recent years is a major change in the political balance of the state, although I won’t pretend to know what will happen in the future both in terms of the socio-demographic changes and their effects on Georgia politics. What’s important for the time being is taking note of the vote margins around Atlanta as we continue our analysis.

Doing the above for Ossoff/Perdue gives essentially an identical set of maps so we will skip doing that for the time being, although you are free to do so using the code above if you feel so inclined. Additionally, since the special election for the other senate seat involved so many people, I don’t think a head-to-head analysis of Warnock and Loeffler would be as illuminating, so we will skip it as well.

How did the senate candidates do relative to the presidential candidates?

As of the time of writing, Democrats are currently slated have 48 seats going into the special election to Republican’s 50; if they win both the seats will be split 50–50 and Vice President Kamala Harris would be able to cast tie-breaking votes (assuming all votes play out along strictly party lines). This being the case, the entire American political-industrial complex is focused on Georgia, and both sides are in the process of pouring millions of dollars into advertising, canvassing, and other activities in order to win these races. This leaves the billion dollar question: Who is going to end up winning these races? Will Ossoff and Warnock be able to reassemble Biden’s winning coalition or will things revert to the mean?

While I can’t claim to be a qualified forecaster or to be able to make any particularly rigorous predictions about the races, my personal opinion at the time of writing this is that the odds don’t look great for Ossoff and Warnock, and comparing their performance to Biden (and conversely comparing Perdue’s to Trump’s) will explain why. We will use two metrics; the first being the ratio of the senate candidates votes to the respective presidential candidate (to get a sense of general under/overperformance) as well as the raw difference in votes.

# More columns
ga$OSSOFF.BIDEN.DIFF <- ga$OSSOFF.VOTES — ga$BIDEN.VOTES
ga$WARNOCK.BIDEN.DIFF <- ga$WARNOCK.VOTES — ga$BIDEN.VOTES
ga$PERDUE.TRUMP.DIFF <- ga$PERDUE.VOTES — ga$TRUMP.VOTES
ga$OSSOFF.BIDEN.RATIO <- ga$OSSOFF.VOTES / ga$BIDEN.VOTES
ga$WARNOCK.BIDEN.RATIO <- ga$WARNOCK.VOTES / ga$BIDEN.VOTES
ga$PERDUE.TRUMP.RATIO <- ga$PERDUE.VOTES / ga$TRUMP.VOTES
# More graphs
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”OSSOFF.BIDEN.RATIO”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=1,name=”Ossoff / Biden Vote Ratio”) + theme(legend.position = “right”)
Ratio of Ossoff to Biden Votes (Red indicates Ossoff underperformance)
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”OSSOFF.BIDEN.DIFF”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=0,name=”Ossoff — Biden Raw Votes”) + theme(legend.position = “right”)
Ossoff Votes minus Biden Votes(Red indicates Ossoff underperformance)
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”WARNOCK.BIDEN.RATIO”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=1,name=”Warnock / Biden Vote Ratio”) + theme(legend.position = “right”)
Ratio of Warnock to Biden Votes (Red indicates Warnock underperformance)
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”WARNOCK.BIDEN.DIFF”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=0,name=”Warnock — Biden Raw Votes”) + theme(legend.position = “right”)
Warnock Votes minus Biden Votes(Red indicates Warnock underperformance)
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”PERDUE.TRUMP.RATIO”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=1,name=”Perdue / Trump Vote Ratio”) + theme(legend.position = “right”)
Ratio of Perdue to Trump Votes (Blue indicates Perdue overperformance)
plot_usmap(include = c(“GA”),regions=”counties”,data=ga,values=”PERDUE.TRUMP.DIFF”) + scale_fill_gradient2(low=”red”,mid=”white”,high=”blue”,midpoint=0,name=”Perdue — Trump Raw Votes”) + theme(legend.position = “right”)
Perdue Votes minus Trump Votes (Blue indicates Perdue overperformance)

Discussion

If we look at the Ossoff-Biden vote ratio, we see that Ossoff underperformed Biden, well, essentially everywhere (The one exception being Chattooga county in Northwest Georgia, although I don’t foresee that county carrying Ossoff to victory). If we look at the raw votes, we can see what would concern me most if I were an Ossoff strategist; Ossoff underperformed Biden in Atlanta, the place in which he needs to run up a margin if we wants to beat Perdue.

The same story is true for Warnock, although the nature of the special election makes it more difficult to know to what extent this will translate to the runoff. Conversely, we will skip trying to do a Loeffler-Trump ratio analysis given that it would be unfruitful to directly compare her Trump’s.

Looking at things through an “Ossoff strategist” lens again, things get worse when we look at how Perdue did relative to Trump. Plotting a map with the Perdue-Trump ratio, we see that Perdue overperformed Trump in several counties, particularly around Atlanta. Without doing further research to find hard numbers, it would lead us to believe that there exists a demographic of Biden-Perdue voters throughout the state but possibly concentrated in the Atlanta Metro Area. Given that Biden only narrowly beat Trump by about 14,000 votes, it only takes a slight erosion in his coalition to turn the tables in the second election. Particularly if Biden was able to peel off a demographic of otherwise-conservative voters who were making an anti-Trump rather than a pro-Biden vote, it would not be hard to see how both senate races could go to Republicans in January despite going for Biden, even without taking into account other factors such as regression to the mean, differences in turnout between general and special elections, etc. (All of which would seem to favor Republicans).

Conclusion

In saying all of this I don’t want to diminish the hard work that’s happening for these seats, particularly the efforts of groups like the New Georgia Project who are working to engage and register new voters to bring them into the electorate (Perhaps consider donating to them here). Nothing is set in stone, and while I would say Republicans are favored to hold these seats at the moment, that may not be the case if they sit back on their laurels while Democrats go out and canvass, register new voters, and campaign. Conversely, its entirely possible that Democrats could pull off the upset and win the seats, but they have no chance of doing so without improving on their performance in November.

Brief Aside

While I’m not political strategist, and I can’t pretend to know what its like to run a high stakes campaign (I absolutely do not envy the people in charge of these senate campaigns right now), I can’t help but read some of the reporting around Sen. Collins’ reelection in Maine and see possible foreshadowing for Georgia. It seems that in Maine part of Sen. Collins upset (in the sense that polling had her behind or neck-in-neck with Gideon) was due to the fact that voters were turned off by Democrats’ nationalized campaign focusing on Mitch McConnell and the Senate rather than a localized campaign strategy focusing on local issues and the wants of individual constituents. With all the money pouring into Georgia right now and the particular focus on the senate, I would hate to see the races devolve into “McConnell v Schumer” at the expense of the Georgia voters and the issues they care about. Whether or not things will play out this way remains to be seen.

--

--

Matt Thoburn
Matt Thoburn

Written by Matt Thoburn

I enjoy nice beverages, long walks on the beach, and thinking about how the world works (ideally at the same time)

No responses yet