Mapping Wisconsin Presidential Election Results

Wisconsin played a major role in the presidential election, but the result is not due to major differences with its Midwest neighbor.

“Your home state did us no favors…”

As a Wisconsinite transplanted into the highest density of left-leaning voters in the country (District of Columbia) I’ve heard from several friends over the weeks in the aftermath of the US presidential election that voters in my home state should shoulder responsibility for contributing to Donald Trump winning an electoral majority.

The “winner-take-all” basis in which electoral votes are allocated means the margin of victory in each state is null - extra votes don’t matter - which contributes to an outcome in which a winning candidate can have fewer total votes than another nominee.

Big Idea

The urban/rural divide has been a popular explanation for Donald Trump’s electoral victory in the 2016 presidential election. Does this theory explain why Trump won Wisconsin and Clinton won Minnesota?

This post seeks to explore this idea. First, a bit of background. These two Midwestern neighbors have striking population, demographic, and social indicator similarities. Recent gubernatorial and congressional elections in both states have resulted in major party shifts. The policies of Wisconsin’s Republican governor Scott Walker and Minnesota’s Democratic governor Mark Dayton and their impact on state success has inspired several comparisons and juxtaposed analyses. While each state has elected Republican or Independent candidates as governors in the last few decades, the Democratic nominee has won each state in 7 straight presidential elections from 1988 to 2012.

Let’s try to unpack the voting results in each state in 2016.

Wisconsin Voting Results

I started this analysis using Julia Silge’s Utah Election Mapping script, then supplemented these analyses with urban and rural county population numbers from the 2010 Census. First, I loaded the R package libraries. Next, I read in the county level 2016 presidential election results and 2010 census data:

library(readr)
library(choroplethr)
library(choroplethrMaps)
library(ggplot2)
library(ggthemes)
library(RColorBrewer)
library(gridExtra)
library(dplyr)
library(stringr)
library(tidyr)
library(extrafont)
extrafont::loadfonts(quiet = TRUE)

all_results <- read_csv("https://raw.githubusercontent.com/mkearney/presidential_election_county_results_2016/master/pres16results.csv")

urban <- read.table("http://www2.census.gov/geo/docs/reference/ua/PctUrbanRural_County.txt",
                  header=TRUE,sep=",", colClasses = "character")
urban$fips <- paste0(urban$STATE,urban$COUNTY)
urban <- urban %>% select(POP_COU, POPPCT_URBAN, fips)
urban[,1:2] <- sapply(urban[,1:2], as.numeric)
all_results <- left_join(all_results,urban)

Once the data is read and joined together through the fips code in each data set, we can subset all_results to determine the percent of votes allocated for the two major party candidates, as well as the two most popular candidates from alternative parties:

wisconsin <- all_results %>% 
  filter(str_detect(fips, "^55"))
wisconsin_spread <- wisconsin %>% 
  filter(cand %in% c("Donald Trump", "Hillary Clinton", "Gary Johnson", "Jill Stein")) %>%
  select(fips, cand, pct) %>% 
  mutate(pct = pct * 100,
         region = as.numeric(fips)) %>%
  spread(cand, pct) %>%
  select(-fips)

wisconsin_spread
## # A tibble: 72 × 5
##    region `Donald Trump` `Gary Johnson` `Hillary Clinton` `Jill Stein`
## *   <dbl>          <dbl>          <dbl>             <dbl>        <dbl>
## 1   55001       59.19660       2.048085          37.39982    0.7915306
## 2   55003       43.25006       2.296240          52.18269    1.7663386
## 3   55005       60.38465       3.224660          35.00489    0.7506440
## 4   55007       43.46223       1.959751          52.18628    1.9492150
## 5   55009       52.70085       3.907543          41.85040    1.0753194
## 6   55011       58.50311       3.149834          36.56986    1.0692097
## 7   55013       62.07134       2.626448          33.81122    0.7684367
## 8   55015       58.06122       3.980476          36.45238    0.7718794
## 9   55017       56.91589       3.835277          37.73315    0.8388675
## 10  55019       63.81016       3.291999          31.18541    0.7897845
## # ... with 62 more rows

Now that our data has been reshaped, we can further subset by candidate to create county-level voting maps, using Ari Lamstein’s choroplethr package.

wisconsin_spread$value <- wisconsin_spread$`Donald Trump`
choro1 = CountyChoropleth$new(wisconsin_spread)
choro1$set_zoom("wisconsin")
choro1$title = "Donald Trump"
choro1$set_num_colors(1)
choro1$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro1$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Reds"))

wisconsin_spread$value <- wisconsin_spread$`Hillary Clinton`
choro2 = CountyChoropleth$new(wisconsin_spread)
choro2$set_zoom("wisconsin")
choro2$title = "Hillary Clinton"
choro2$set_num_colors(1)
choro2$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro2$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Blues"))

wisconsin_spread$value <- wisconsin_spread$`Gary Johnson`
choro3 = CountyChoropleth$new(wisconsin_spread)
choro3$set_zoom("wisconsin")
choro3$title = "Gary Johnson"
choro3$set_num_colors(1)
choro3$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro3$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Purples"))

wisconsin_spread$value <- wisconsin_spread$`Jill Stein`
choro4 = CountyChoropleth$new(wisconsin_spread)
choro4$set_zoom("wisconsin")
choro4$title = "Jill Stein"
choro4$set_num_colors(1)
choro4$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro4$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Greens"))

grid.arrange(choro1$render() + theme(text=element_text(family="DejaVu Sans Mono")), 
             choro2$render() + theme(text=element_text(family="DejaVu Sans Mono")), 
             choro3$render() + theme(text=element_text(family="DejaVu Sans Mono")), 
             choro4$render() + theme(text=element_text(family="DejaVu Sans Mono")), ncol = 2, nrow =2)
plot of chunk Wisconsin Presidential Election Results by County

A bar graph more accurately displays the results by candidate. Wisconsin’s 10 electoral votes were decided by only 27257 votes! That is a really small margin.

votes <- wisconsin %>%
  filter(cand %in% c("Donald Trump", "Hillary Clinton", "Gary Johnson","Jill Stein")) %>%
  mutate(cand = factor(cand, levels = c("Donald Trump", "Hillary Clinton", "Gary Johnson","Jill Stein"))) %>%
  group_by(cand) %>%
  summarise(sum = sum(votes))

ggplot(votes, aes(cand, sum, fill = cand)) +
  geom_bar(stat = "identity", alpha = 0.8) +
  theme_tufte(base_family = "DejaVu Sans Mono") +
  scale_fill_manual(values = c("red3", "navyblue", "#6A51A3","darkgreen")) +
  theme(legend.position="none") +
  labs(title = "Total Votes Cast in Wisconsin", y = "Number of votes", x = NULL)
plot of chunk Wisconsin Presidential Election Results Bar Chart

Wisconsin and Minnesota Voting Results

To get a better sense of how the results compare, lets perform the same analyses using both Wisconsin and Minnesota data.

minnesotawisconsin <- all_results %>% 
  filter(str_detect(fips, c("^55")) | str_detect(fips, c("^27")))
minnesotawisconsin_spread <- minnesotawisconsin %>% 
  filter(cand %in% c("Donald Trump", "Hillary Clinton", "Gary Johnson", "Jill Stein")) %>%
  select(fips, cand, pct) %>% 
  mutate(pct = pct * 100,
         region = as.numeric(fips)) %>%
  spread(cand, pct) %>%
  select(-fips)

minnesotawisconsin_spread$value <- minnesotawisconsin_spread$`Donald Trump`
choro1 = CountyChoropleth$new(minnesotawisconsin_spread)
choro1$set_zoom(c("minnesota","wisconsin"))
choro1$title = "Donald Trump"
choro1$set_num_colors(1)
choro1$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro1$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Reds"))

minnesotawisconsin_spread$value <- minnesotawisconsin_spread$`Hillary Clinton`
choro2 = CountyChoropleth$new(minnesotawisconsin_spread)
choro2$set_zoom(c("minnesota","wisconsin"))
choro2$title = "Hillary Clinton"
choro2$set_num_colors(1)
choro2$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro2$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Blues"))

minnesotawisconsin_spread$value <- minnesotawisconsin_spread$`Gary Johnson`
choro3 = CountyChoropleth$new(minnesotawisconsin_spread)
choro3$set_zoom(c("minnesota","wisconsin"))
choro3$title = "Gary Johnson"
choro3$set_num_colors(1)
choro3$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro3$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Purples"))

minnesotawisconsin_spread$value <- minnesotawisconsin_spread$`Jill Stein`
choro4 = CountyChoropleth$new(minnesotawisconsin_spread)
choro4$set_zoom(c("minnesota","wisconsin"))
choro4$title = "Jill Stein"
choro4$set_num_colors(1)
choro4$ggplot_polygon = geom_polygon(aes(fill = value), color = NA)
choro4$ggplot_scale = scale_fill_gradientn(name = "Percent", 
                                           colours = brewer.pal(8, "Greens"))

grid.arrange(choro1$render() + theme(text=element_text(family="DejaVu Sans Mono")), 
             choro2$render() + theme(text=element_text(family="DejaVu Sans Mono")), 
             choro3$render() + theme(text=element_text(family="DejaVu Sans Mono")), 
             choro4$render() + theme(text=element_text(family="DejaVu Sans Mono")), ncol = 2, nrow =2)
plot of chunk Wisconsin and Minnesota Presidential Election Results by County

Mapping the allocation of votes by county shows Wisconsin and Minnesota have very similar county-level maps. Each has strong results favoring Clinton in major metropolitan areas, including Milwaukee, Madison, and Minneapolis/St Paul. Based solely on each state’s map, I’d posit that Minnesota seems the more likely Trump-majority. This perspective is deceiving though, as the distribution of votes by county is skewed. Each map displays the percent of votes for each candidate by county, neglecting the county’s total votes. More than 78 percent of Menominee County, WI voters selected Hilary Clinton, resulting in Northeastern Wisconsin’s dark blue polygon, but totaled only 1279 votes.

Lets break this down further by exploring the allocation of votes by county.

Vote Distributions by County

Focusing on the two major candidates, the two states also have similar vote distributions. Each of these figures displays the percentage of votes for Donald Trump (red) and Hilary Clinton (blue), with each bubble sized by percentage of total state vote, organized by the percentage of votes for Donald Trump.

minnesota <- subset(minnesotawisconsin, st=="MN")
minnesota$votePct <- (minnesota$votes/sum(minnesota$votes)) * 100
minnesota <- minnesota %>% arrange(desc(votes))
vals = minnesota[ minnesota$cand == 'Donald Trump', ]
minnesota$county <- factor(minnesota$county, levels = rev(vals$county))
minnesotaSubset <- subset(minnesota, cand %in% c("Donald Trump", "Hillary Clinton"))
mnScatter <- ggplot(minnesotaSubset, aes(county, votePct,color = cand)) +
  geom_point(aes(size = votes)) +
  scale_y_continuous(limits=c(0,15)) +
  theme_tufte(base_family = "DejaVu Sans Mono") +
  scale_color_manual(values = c("red3", "navyblue")) +
  scale_size_continuous(guide = FALSE) +
  labs(title = "Percentage of Total Votes Cast for Clinton and Trump By County",
       subtitle = "Minnesota",
       y = "Total Vote Percentage",
       x = NULL) +
  theme(axis.text.x=element_blank(),
        axis.ticks.x=element_blank(),
        legend.title = element_blank(),
        plot.title =  element_text(size = 16),
        plot.subtitle =  element_text(size = 13))

wisconsin$votePct <- (wisconsin$votes/sum(wisconsin$votes)) * 100
wisconsin <- wisconsin %>% arrange(desc(votes))
vals = wisconsin[ wisconsin$cand == 'Donald Trump', ]
wisconsin$county <- factor(wisconsin$county, levels = rev(vals$county))
wisconsinSubset <- subset(wisconsin, cand %in% c("Donald Trump", "Hillary Clinton"))
wiScatter <- ggplot(wisconsinSubset, aes(county, votePct,color = cand)) +
  geom_point(aes(size = votes)) +
  scale_y_continuous(limits=c(0,15)) +
  theme_tufte(base_family = "DejaVu Sans Mono") +
  scale_color_manual(values = c("red3", "navyblue")) +
  scale_size_continuous(guide = FALSE) +
  labs(subtitle = "Wisconsin",
       y = "Total Vote Percentage",
       x = "County (ordered by Total Vote Percentage\n for Donald Trump (smallest to largest))") +
  theme(axis.text.x=element_blank(),
        axis.ticks.x=element_blank(),
        legend.title = element_blank(),
        plot.title = element_text(size = 20),
        plot.subtitle = element_text(size = 13))

grid.arrange(mnScatter,wiScatter,ncol=1)
plot of chunk scatter plotting

Two things of note from these figures:

  • The county with Minnesota’s most votes significantly backed Clinton over Trump (14.7% and 6.6% of the total votes cast in the state respectively). Meanwhile, the top-voting Wisconsin’s county also garnered significant support for Clinton over Trump, but contributed only 9.8% and 4.3% of the total votes cast in the state respectively.
  • Trump earned more votes than Clinton in 7 of Wisconsin’s 10 largest counties, including Waukesha - the third largest (4.9% and 2.7% of the total votes cast in the state respectively). Only 4 out of the top 10 most populous counties in Minnesota favored Trump.

The above figure seems to imply that the counties with larger populations in each state had a significant impact on each state’s victor; while most Wisconsin counties favored Trump, most Minnesota counties favored Clinton. Perhaps different levels of urban density might result in a better explanation?

The 2010 Census breaks up metropolitan areas into segments: small, medium, and large. An area must have a population of at least 50,000 to be considered an urbanized metropolitan area, and is further separated if it’s population is greater than 250,000. For this analysis, small metropolitan areas consist of those ranging in population from 50,000 to 250,000, while populations greater than 250,000 are combined into one category.

minnesotawisconsin <- all_results %>% 
  filter(str_detect(fips, c("^55")) | str_detect(fips, c("^27")))

minnesotawisconsin$urbanClass <- ifelse(minnesotawisconsin$POP_COU < 50000, "Not a Metropolitan Area",
                                 ifelse(minnesotawisconsin$POP_COU > 50000 & minnesotawisconsin$POP_COU < 250000,
                                        "Small Metropolitan Area","Medium to Large Metropolitan Area"))
# Quick idiot check to make sure our ifelse statement didn't go awry
table(round(minnesotawisconsin$POP_COU,-5), minnesotawisconsin$urbanClass)
##          
##           Medium to Large Metropolitan Area Not a Metropolitan Area Small Metropolitan Area
##   0                                       0                     920                       0
##   1e+05                                   0                       0                     241
##   2e+05                                   0                       0                      69
##   3e+05                                   9                       0                       0
##   4e+05                                  16                       0                       0
##   5e+05                                  16                       0                       0
##   9e+05                                   7                       0                       0
##   1200000                                 9                       0                       0

After a quick table to ensure correct recoding of urban classification, I grouped the data by candidate, state, and urban classification to better isolate the voting distributions.

minnesotawisconsin$urbanClass <- factor(minnesotawisconsin$urbanClass,
                                        levels = c("Medium to Large Metropolitan Area", "Small Metropolitan Area", "Not a Metropolitan Area"))

mnwiUrban <- minnesotawisconsin %>%
  filter(cand %in% c("Donald Trump", "Hillary Clinton", "Gary Johnson", "Jill Stein")) %>%
  mutate(cand = factor(cand, levels = c("Donald Trump", "Hillary Clinton", "Gary Johnson", "Jill Stein"),
                       labels = c("Trump", "Clinton", "Johnson", "Stein"))) %>%
  group_by(cand, st,urbanClass) %>%
  summarise(sumVotes = sum(votes), population = sum(POP_COU))

mnwiUrban <- mnwiUrban %>%
  group_by(st, urbanClass) %>%
  mutate(percent = sumVotes/sum(sumVotes))

mnwiUrban$st <- factor(mnwiUrban$st, levels = c("MN", "WI"), labels = c("Minnesota", "Wisconsin"))

p <- ggplot(mnwiUrban, aes(cand, sumVotes,fill = cand))  +
  geom_bar(stat = "identity", alpha = 0.8) + 
  facet_grid(st ~ urbanClass) +
  theme_tufte(base_family = "DejaVu Sans Mono") +
  scale_fill_manual(values = c("red3", "navyblue", "#6A51A3","darkgreen")) +
  theme(legend.position="none",
        strip.text.x = element_text(size = 12),
        strip.text.y = element_text(size = 14),
        plot.title =  element_text(size = 16)) +
  geom_text(aes(label=paste0(round(percent,3)*100,"%")), nudge_y = 50000,family = "DejaVu Sans Mono") +
  labs(title = "Distribution of Votes Cast in Minnesota and Wisconsin Counties By Urban Classification", y = "Number of votes", x = NULL) + 
  scale_y_continuous(limits=c(0,1000000))

## Add in some annotations - since we're facetting we need to make a dummy data.frame with the same factor levels and plot positions as those used in the figure
anns <- data.frame(cand = factor("Gary Johnson", levels = c("Donald Trump", "Hillary Clinton", "Gary Johnson", "Jill Stein"),
                                 labels = c("Trump", "Clinton", "Johnson", "Stein")),
                   st = factor("MN", levels = c("MN", "WI"), labels = c("Minnesota", "Wisconsin")),
                   sumVotes = 700000,
                   lab = "For each State and Urban Class:\nBar Height - Total Votes\n\n  Percent - Portion of votes",
                   urbanClass = factor("Not a Metropolitan Area",levels = c("Medium to Large Metropolitan Area",
                                                                            "Small Metropolitan Area", "Not a Metropolitan Area")))

## Apply the annotations to the top right facet (i.e. Minnesota, Not a Metropolitan Area). Swoopy arrows are fun!
p + geom_text(data = anns,aes(label =lab), family = "DejaVu Sans Mono", size = 3) +
  geom_curve(data=anns, aes(x=2.1, xend=1.5, y=690000, yend=100000), curvature=0.3, arrow=arrow(length=unit(0.03, "npc"))) +
  geom_curve(data=anns, aes(x=2.4, xend=3, y=580000, yend=110000), curvature=-0.3, arrow=arrow(length=unit(0.03, "npc")))
plot of chunk Presidential Election Results by Urban Area

The data shows a sizable rural/urban divide in Minnesota and Wisconsin. Both states voted for each candidate at nearly the exact same rate in medium/large and small metropolitan areas. The major difference is how the population in each state is distributed:

mnwiUrban %>%
    select(st,urbanClass, sumVotes, population) %>%
    group_by(st,urbanClass) %>%
    summarise(total = sum(sumVotes), metroPopulationTotal = unique(population)) %>%
    mutate(stateVoteAllocationByMetro = total/sum(total),
           populationPctByMetro = metroPopulationTotal/sum(metroPopulationTotal),
           votingPctByMetro = total/metroPopulationTotal)
## Source: local data frame [6 x 7]
## Groups: st [2]
## 
##          st                        urbanClass   total metroPopulationTotal stateVoteAllocationByMetro populationPctByMetro votingPctByMetro
##      <fctr>                            <fctr>   <int>                <dbl>                      <dbl>                <dbl>            <dbl>
## 1 Minnesota Medium to Large Metropolitan Area 1318717              2390461                  0.4644240            0.4506966        0.5516580
## 2 Minnesota           Small Metropolitan Area  854434              1579726                  0.3009134            0.2978409        0.5408748
## 3 Minnesota           Not a Metropolitan Area  666317              1333738                  0.2346626            0.2514625        0.4995861
## 4 Wisconsin Medium to Large Metropolitan Area  972116              1825699                  0.3318823            0.3210310        0.5324624
## 5 Wisconsin           Small Metropolitan Area 1411946              2766790                  0.4820411            0.4865125        0.5103192
## 6 Wisconsin           Not a Metropolitan Area  545037              1094497                  0.1860767            0.1924564        0.4979794

While the highest portion of votes in Minnesota came from counties defined as medium and large metropolitan areas (46.4%), those defined as small metropolitan areas contributed the most votes in Wisconsin (48.2%). These voting distributions match up nicely with the percentage of each state’s population by metropolitan area, as seen in the two variables stateVoteAllocationByMetro and populationPctByMetro - these distributions contributed to the outcome of each state’s election. The rate at which different types of urban areas supported a particular candidate is nearly identical between Minnesota and Wisconsin.

Conclusion

While there are major differences in how rural and urban voters supported particular candidates, this division is not unique to states that cracked the “Midwestern Blue-State Firewall” in the 2016 presidential election. In Minnesota and Wisconsin, larger metropolitan areas voted strongly in favor of Clinton, while smaller and non-urban areas strongly favored Trump. Further exploration is necessary to make more granular conclusions, but in these two states the differing distributions of populations into small and medium/large metropolitan areas may help explain each candidate’s victory.

Next Steps

Data with eligible-voter populations and demographic information at the county level might further supplement this exploration by providing a more detailed picture of rural and urban voters beyond raw population and vote totals. Outlets such as FiveThirtyEight have used this type of granular data to inform predictive forecasts of elections. Thoughts or questions about the post? Contact me on twitter or leave a message in the comments section.