Republican Donald Trump easily won Tennessee in both the 2020 and 2024 presidential races. But this analysis suggests he won in a different way each time.
You can explore the maps below to compare the two elections in terms of Republican and Democratic county-level gains and losses compared to the preceding presidential race. Specifically:
Drag a map’s slider left or right to see which counties produced fewer, more, or the same number of votes for each party compared to the most-recent presidential contest.
Click on a county to see vote counts for each election.
Zoom and pan the map as needed. One possibly helpful tactic is to zoom in on a county, then position the slider so that you can look at the county’s Democratic and Republican vote totals simultaneously.
This map shows county-level vote shifts by party for the 2016 and 2020 elections. In 2016, Trump beat Democratic nominee Hillary Clinton. In 2020, Trump lost to Democratic nominee Joe Biden.
This map shows county-level vote shifts by party for the 2020 and 2024 elections. In 2020, Trump lost to Democratic nominee Joe Biden. In 2024, Trump beat to Democratic nominee Kamala Harris.
The full script is below. Data come from the Tennessee Secretary of State website. The 2024 election data are still unofficial. Let look at what’s going on in each major chunk.
As usual, the script opens by installing, if needed, and loading the various packages the script requires. You probably have all of these installed by now, but it never hurts to check.
# Required packages
if (!require("tidyverse"))
install.packages("tidyverse")
if (!require("mapview"))
install.packages("mapview")
if (!require("sf"))
install.packages("sf")
if (!require("leaflet"))
install.packages("leaflet")
if (!require("leaflet.extras2"))
install.packages("leaflet.extras2")
if (!require("plotly"))
install.packages("plotly")
if (!require("tidycensus"))
install.packages("tidycensus")
library(tidyverse)
library(mapview)
library(sf)
library(leaflet)
library(leafpop)
library(readxl)
library(plotly)
library(tidycensus)
You’ll need a county-level map of Tennessere, and the most convenient place to get one is the U.S. Census Bureau’s American Community Survey dataset. The map retrieval will happen much later in the script, but I thought it might be best to have you go ahead and transmit your API key now.
As usualy, remember to replace
PasteYourAPIKeyBetweenTheseQuoteMarks
with your actual API
key.
# Go ahead and transmit your Census API key
# so you don't forget to do it later when getting
# the map you will need:
census_api_key("PasteYourAPIKeyBetweenTheseQuoteMarks")
The 2016 and 2020 datasets for this analysis come directly from the Tennessee Secretary of State website. In state government, the secretary of state is in charge of running elections. Both datasets come from the precinct-level spreadsheets posted in the Elections / Historical Election Results area of the web site.
We’re after county-level data, not precinct-level data, so the code
also aggregates the precinct-level data by county using the
group_by()
function from the dplyer
package,
which is included in the tidyverse
package.
The code also filters the data for just the Democratic and Republican candidate vote tallies. Independent candidate vote tallies are ignored.
Finally, the code fixes a data entry error. In the 2020 data, “DeKalb” is misspelled as “Dekalb.” Unless fixed, the misspelling would result in missing data later, when the 2016, 2020 and 2024 files are merged.
# Download and import election data
# from TN Secretary of State web site:
# https://sos.tn.gov/elections/results
# Get 2016 data
download.file(
"https://sos-tn-gov-files.s3.amazonaws.com/StateGeneralbyPrecinctNov2016.xlsx",
"RawElectionData2016.xlsx",
quiet = TRUE,
mode = "wb"
)
RawElectionData2016 <- read_xlsx("RawElectionData2016.xlsx")
# Filter, calculate, and select
# to get data of interest
# then store results in MyData dataframe
MyData2016 <- RawElectionData2016%>%
filter(OFFICENAME == "United States President",
CANDGROUP == "1") %>%
mutate(
Rep16 = PVTALLY1,
Dem16 = PVTALLY2,
Total16 = Rep16 + Dem16) %>%
select(COUNTY, PRECINCT, OFFICENAME, Rep16, Dem16, Total16)
CountyData2016 <- MyData2016 %>%
select(COUNTY, Rep16, Dem16, Total16) %>%
group_by(COUNTY) %>%
summarize(across(everything(), sum))
# Get 2020 data
download.file(
"https://sos-tn-gov-files.tnsosfiles.com/Nov2020PrecinctDetail.xlsx",
"RawElectionData2020.xlsx",
quiet = TRUE,
mode = "wb"
)
RawElectionData2020 <- read_xlsx("RawElectionData2020.xlsx", sheet = "SOFFICEL")
# Filter, calculate, and select
# to get data of interest
# then store results in MyData dataframe
MyData2020 <- RawElectionData2020%>%
filter(OFFICENAME == "United States President",
CANDGROUP == "1") %>%
mutate(
Rep20 = PVTALLY1,
Dem20 = PVTALLY2,
Total20 = Rep20 + Dem20) %>%
select(COUNTY, PRECINCT, OFFICENAME, Rep20, Dem20, Total20)
MyData2020 <- MyData2020 %>%
mutate(COUNTY = case_when(COUNTY == "Dekalb" ~ "DeKalb",
TRUE ~ COUNTY))
CountyData2020 <- MyData2020%>%
select(COUNTY, Rep20, Dem20, Total20) %>%
group_by(COUNTY) %>%
summarize(across(everything(), sum))
After being checked and certified, the 2024 election results will show up in the same place as the 2016 and 2020 data. For now, getting the 2024 unofficial results requires clicking the Unofficial Election Results as they are reported by the counties on November 5th link on the main page, choosing the “President” tab, then clicking the View County Breakdown link.
You get an inconveniently untidy .html table that I had to convert to a tidy .csv dataset using some clever manipulation in Excel. Conveniently for you, the code will download the tidy .csv dataset from my GitHub page.
# Get 2024 data
CountyData2024 <- read_csv("https://raw.githubusercontent.com/drkblake/Data/refs/heads/main/CountyData2024.csv")
For this analysis to work, we need the 2016, 2020 and 2024 data in a
single data file, with one row per county. This code gets the job done
using the left_join()
function and puts the results in a
data frame called AllData
.
# Merge Data Files
AllData <- left_join(CountyData2016, CountyData2020, by = "COUNTY")
AllData <- left_join(AllData, CountyData2024, by = "COUNTY")
This code uses the mutate()
function to calculates the
differences in votes for Democratic candidates from election to
election, then does the same thing for the differences in votes for
Republican candidates. Along the way, it uses the
case_when()
function to flag each difference as a “Loss,”
“Gain” or “No change.”
AllData <- AllData %>%
mutate(
Rep16to20 = Rep20-Rep16,
Dem16to20 = Dem20-Dem16,
Rep20to24 = Rep24-Rep20,
Dem20to24 = Dem24-Dem20,
Rep20finish = case_when(
Rep16to20 < 0 ~ "Loss",
Rep16to20 > 0~ "Gain",
TRUE ~ "No change"),
Dem20finish = case_when(
Dem16to20 < 0 ~ "Loss",
Dem16to20 > 0~ "Gain",
TRUE ~ "No change"),
Rep24finish = case_when(
Rep20to24 < 0 ~ "Loss",
Rep20to24 > 0~ "Gain",
TRUE ~ "No change"),
Dem24finish = case_when(
Dem20to24 < 0 ~ "Loss",
Dem20to24 > 0~ "Gain",
TRUE ~ "No change"))
Be sure you have sent R your Census API key, using the code above. If you haven’t, this part isn’t going to work.
The code grabs a county-level map of Tennessee from the U.S. Census
Bureau. A bit of trivia: the DP05_0048
variable it uses
counts the number of people with Japanese ancestry. I swiped the code
from an earlier, unrelated project. The Japanese ancestry variable will
get discarded almost immediately. It’s just a place holder in the
code.
After some reformatting, the code uses the left_join()
function to put the map and data files together. The
select()
function drops all except the variables needed for
the map.
# Get a county map
CountyMap <- get_acs(geography = "county",
state = "TN",
variables = c(Japanese_ = "DP05_0048"),
year = 2022,
survey = "acs5",
output = "wide",
geometry = TRUE)
CountyMap <- CountyMap %>%
mutate(COUNTY = (str_remove(NAME," County, Tennessee"))) %>%
left_join(AllData, CountyMap, by = "COUNTY") %>%
select(COUNTY,
Rep16, Dem16, Total16,
Rep20, Dem20, Total20,
Rep24, Dem24, Total24,
Rep16to20, Dem16to20,
Rep20to24, Dem20to24,
Rep20finish,Dem20finish,
Rep24finish,Dem24finish,
geometry)
You’ve seen map code before by now. It builds on the
mapview()
function from the mapview
package.
What’s new here is the code for making a “slider map,” meaning a map
that really consistes of two overlaid maps, with a “slider bar” that
uncovers one or the other. It’s a great way to show change over time or,
in this case, the difference between Democratic voting and Republican
voting.
The relevant code looks like this:
Map16to20Dem | Map16to20Rep
Both maps have to be made in earlier code. The map on the left will be the one you uncover by sliding the bar right. The map on the right side will be the one you uncover by sliding the bar left.
# 2020 Map
Map16to20Rep <- mapview(
CountyMap,
zcol = "Rep20finish",
col.regions = "red",
layer.name = "Rep 2020",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Rep16", "Rep20", "Rep16to20")
)
)
mypalette = colorRampPalette(c('blue', 'lightblue'))
Map16to20Dem <- mapview(
CountyMap,
zcol = "Dem20finish",
col.regions = mypalette,
layer.name = "Dem 2020",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Dem16", "Dem20", "Dem16to20")
)
)
Map16to20Dem | Map16to20Rep
# 2024 Map
mypalette = colorRampPalette(c('red', 'pink'))
Map20to24Rep <- mapview(
CountyMap,
zcol = "Rep24finish",
col.regions = mypalette,
layer.name = "Rep 2024",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Rep20", "Rep24", "Rep20to24")
)
)
mypalette = colorRampPalette(c('blue', 'lightblue'))
Map20to24Dem <- mapview(
CountyMap,
zcol = "Dem24finish",
col.regions = mypalette,
layer.name = "Dem 2024",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Dem20", "Dem24", "Dem20to24")
)
)
Map20to24Dem | Map20to24Rep
# Required packages
if (!require("tidyverse"))
install.packages("tidyverse")
if (!require("mapview"))
install.packages("mapview")
if (!require("sf"))
install.packages("sf")
if (!require("leaflet"))
install.packages("leaflet")
if (!require("leaflet.extras2"))
install.packages("leaflet.extras2")
if (!require("plotly"))
install.packages("plotly")
if (!require("tidycensus"))
install.packages("tidycensus")
library(tidyverse)
library(mapview)
library(sf)
library(leaflet)
library(leafpop)
library(readxl)
library(plotly)
library(tidycensus)
# Go ahead and transmit your Census API key
# so you don't forget to do it later when getting
# the map you will need:
census_api_key("PasteYourAPIKeyBetweenTheseQuoteMarks")
# Download and import election data
# from TN Secretary of State web site:
# https://sos.tn.gov/elections/results
# Get 2016 data
download.file(
"https://sos-tn-gov-files.s3.amazonaws.com/StateGeneralbyPrecinctNov2016.xlsx",
"RawElectionData2016.xlsx",
quiet = TRUE,
mode = "wb"
)
RawElectionData2016 <- read_xlsx("RawElectionData2016.xlsx")
# Filter, calculate, and select
# to get data of interest
# then store results in MyData dataframe
MyData2016 <- RawElectionData2016%>%
filter(OFFICENAME == "United States President",
CANDGROUP == "1") %>%
mutate(
Rep16 = PVTALLY1,
Dem16 = PVTALLY2,
Total16 = Rep16 + Dem16) %>%
select(COUNTY, PRECINCT, OFFICENAME, Rep16, Dem16, Total16)
CountyData2016 <- MyData2016 %>%
select(COUNTY, Rep16, Dem16, Total16) %>%
group_by(COUNTY) %>%
summarize(across(everything(), sum))
# Get 2020 data
download.file(
"https://sos-tn-gov-files.tnsosfiles.com/Nov2020PrecinctDetail.xlsx",
"RawElectionData2020.xlsx",
quiet = TRUE,
mode = "wb"
)
RawElectionData2020 <- read_xlsx("RawElectionData2020.xlsx", sheet = "SOFFICEL")
# Filter, calculate, and select
# to get data of interest
# then store results in MyData dataframe
MyData2020 <- RawElectionData2020%>%
filter(OFFICENAME == "United States President",
CANDGROUP == "1") %>%
mutate(
Rep20 = PVTALLY1,
Dem20 = PVTALLY2,
Total20 = Rep20 + Dem20) %>%
select(COUNTY, PRECINCT, OFFICENAME, Rep20, Dem20, Total20)
MyData2020 <- MyData2020 %>%
mutate(COUNTY = case_when(COUNTY == "Dekalb" ~ "DeKalb",
TRUE ~ COUNTY))
CountyData2020 <- MyData2020%>%
select(COUNTY, Rep20, Dem20, Total20) %>%
group_by(COUNTY) %>%
summarize(across(everything(), sum))
# Get 2024 data
CountyData2024 <- read_csv("https://raw.githubusercontent.com/drkblake/Data/refs/heads/main/CountyData2024.csv")
# Merge Data Files
AllData <- left_join(CountyData2016, CountyData2020, by = "COUNTY")
AllData <- left_join(AllData, CountyData2024, by = "COUNTY")
AllData <- AllData %>%
mutate(
Rep16to20 = Rep20-Rep16,
Dem16to20 = Dem20-Dem16,
Rep20to24 = Rep24-Rep20,
Dem20to24 = Dem24-Dem20,
Rep20finish = case_when(
Rep16to20 < 0 ~ "Loss",
Rep16to20 > 0~ "Gain",
TRUE ~ "No change"),
Dem20finish = case_when(
Dem16to20 < 0 ~ "Loss",
Dem16to20 > 0~ "Gain",
TRUE ~ "No change"),
Rep24finish = case_when(
Rep20to24 < 0 ~ "Loss",
Rep20to24 > 0~ "Gain",
TRUE ~ "No change"),
Dem24finish = case_when(
Dem20to24 < 0 ~ "Loss",
Dem20to24 > 0~ "Gain",
TRUE ~ "No change"))
# Get a county map
CountyMap <- get_acs(geography = "county",
state = "TN",
variables = c(Japanese_ = "DP05_0048"),
year = 2022,
survey = "acs5",
output = "wide",
geometry = TRUE)
CountyMap <- CountyMap %>%
mutate(COUNTY = (str_remove(NAME," County, Tennessee"))) %>%
left_join(AllData, CountyMap, by = "COUNTY") %>%
select(COUNTY,
Rep16, Dem16, Total16,
Rep20, Dem20, Total20,
Rep24, Dem24, Total24,
Rep16to20, Dem16to20,
Rep20to24, Dem20to24,
Rep20finish,Dem20finish,
Rep24finish,Dem24finish,
geometry)
# 2020 Map
Map16to20Rep <- mapview(
CountyMap,
zcol = "Rep20finish",
col.regions = "red",
layer.name = "Rep 2020",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Rep16", "Rep20", "Rep16to20")
)
)
mypalette = colorRampPalette(c('blue', 'lightblue'))
Map16to20Dem <- mapview(
CountyMap,
zcol = "Dem20finish",
col.regions = mypalette,
layer.name = "Dem 2020",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Dem16", "Dem20", "Dem16to20")
)
)
Map16to20Dem | Map16to20Rep
# 2024 Map
mypalette = colorRampPalette(c('red', 'pink'))
Map20to24Rep <- mapview(
CountyMap,
zcol = "Rep24finish",
col.regions = mypalette,
layer.name = "Rep 2024",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Rep20", "Rep24", "Rep20to24")
)
)
mypalette = colorRampPalette(c('blue', 'lightblue'))
Map20to24Dem <- mapview(
CountyMap,
zcol = "Dem24finish",
col.regions = mypalette,
layer.name = "Dem 2024",
popup = popupTable(
CountyMap,
feature.id = FALSE,
row.numbers = FALSE,
zcol = c("COUNTY", "Dem20", "Dem24", "Dem20to24")
)
)
Map20to24Dem | Map20to24Rep