Wisconsin had primary elections for the State Assembly and Senate on August 11. A good opportunity to practice creating some maps. I’ll focus on the race for Senate District 26.

Data

Results by ward are available from the Dane County Clerk’s website; geographies for the wards are available on the City of Madison OpenData portal.

library(tidyverse)
## -- Attaching packages ------------------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.3.0     v purrr   0.3.3
## v tibble  3.0.1     v dplyr   1.0.1
## v tidyr   1.1.1     v stringr 1.4.0
## v readr   1.3.1     v forcats 0.5.0
## -- Conflicts ---------------------------------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
library(readxl)
library(tmap)
library(sf)
## Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
elect <- read_xlsx("data/ElectionResults.xlsx", 
                           skip = 7, 
                           col_names = c("precincts",
                                         "moe",
                                         "elm",
                                         "bur",
                                         "ben",
                                         "roy",
                                         "dav",
                                         "ime",
                                         "wri"))

wards <- read_sf("data/Wards.shp")

elect
## # A tibble: 82 x 9
##    precincts           moe   elm   bur   ben   roy   dav   ime   wri
##    <chr>             <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
##  1 C Fitchburg Wd 20     0     0     0     0     0     0     0     0
##  2 C Madison Wd 020      3     6    10     4    36     0     1     0
##  3 C Madison Wd 028     43   170    84    72   156    11    12     1
##  4 C Madison Wd 029     70   455   113   226   447     5    38     1
##  5 C Madison Wd 030     34   103    29    45   151     3    19     0
##  6 C Madison Wd 039     62   295    67   126   255    11    31     1
##  7 C Madison Wd 040     54   400   123   310   616     6    54     1
##  8 C Madison Wd 041     89   451   159   330   604     9    50     2
##  9 C Madison Wd 042     66   559   119   153   443    12    37     0
## 10 C Madison Wd 043     20    81    13    10    54     2     4     0
## # ... with 72 more rows
wards %>% 
  filter(SEN_DIST == 26)
## Simple feature collection with 95 features and 13 fields
## geometry type:  MULTIPOLYGON
## dimension:      XY
## bbox:           xmin: -89.57117 ymin: 42.99815 xmax: -89.31599 ymax: 43.12373
## geographic CRS: WGS 84
## # A tibble: 95 x 14
##    OBJECTID  WARD ALD_DIST SUP_DIST ASM_DIST SEN_DIST ADDRESS HANDICAP_A
##  *    <int> <int>    <int>    <int>    <int>    <int> <chr>   <chr>     
##  1    19175   154        1       15       78       26 <NA>    <NA>      
##  2    19193    20       15        6       76       26 <NA>    <NA>      
##  3    19201    28       12       12       76       26 <NA>    <NA>      
##  4    19202    30       12       12       76       26 <NA>    <NA>      
##  5    19211    39        6        6       76       26 <NA>    <NA>      
##  6    19212    40        6        6       76       26 <NA>    <NA>      
##  7    19213    41        6        6       76       26 <NA>    <NA>      
##  8    19214    42        6        6       76       26 <NA>    <NA>      
##  9    19215    43        6        1       76       26 <NA>    <NA>      
## 10    19216    44        2        2       76       26 <NA>    <NA>      
## # ... with 85 more rows, and 6 more variables: SPEC_COMME <chr>,
## #   PHONE_NO <chr>, BALLOT_COL <chr>, SHAPESTAre <dbl>, SHAPESTLen <dbl>,
## #   geometry <MULTIPOLYGON [°]>

Data looks okay, but there are mismatches to deal with:

Adding percentages

elect <- elect %>% 
  mutate(total_votes = rowSums(.[2:9])) %>% 
  mutate(across(2:9, ~ .x/total_votes*100, .names = "{col}_pct"))

Who won?

Answering the question who won in each district requires making the elect data long, then grouping by precinct, and then doing a comparison of the actual number of votes with the max number of votes in each precinct. Not exactly straightforward, but for a multi-candidate race, I can’t think of a more elegant option.

winners <- elect %>% 
  pivot_longer(2:9, names_to = "candidate", values_to = "votes") %>% 
  group_by(precincts) %>% 
  filter(votes != 0) %>% 
  filter(votes == max(votes)) %>% 
  select(-ends_with("pct")) %>% 
  mutate(winning_percent = round(votes/total_votes*100, 1))

winners
## # A tibble: 79 x 5
## # Groups:   precincts [79]
##    precincts        total_votes candidate votes winning_percent
##    <chr>                  <dbl> <chr>     <dbl>           <dbl>
##  1 C Madison Wd 020          60 roy          36            60  
##  2 C Madison Wd 028         549 elm         170            31  
##  3 C Madison Wd 029        1355 elm         455            33.6
##  4 C Madison Wd 030         384 roy         151            39.3
##  5 C Madison Wd 039         848 elm         295            34.8
##  6 C Madison Wd 040        1564 roy         616            39.4
##  7 C Madison Wd 041        1694 roy         604            35.7
##  8 C Madison Wd 042        1389 elm         559            40.2
##  9 C Madison Wd 043         184 elm          81            44  
## 10 C Madison Wd 044         936 roy         322            34.4
## # ... with 69 more rows

Each ward was one by either Kelda Roys or Nada Elmikasfi:

unique(winners$candidate)
## [1] "roy" "elm"

Map it

Who won their districts?

df <- winners %>% 
  mutate(ID = as.integer(str_extract(precincts, "[0-9]{3}"))) %>% 
  mutate(candidate = case_when(candidate == "elm" ~ "Nada Elmikashfi",
                               candidate == "roy" ~ "Kelda Roys"))

df2 <- wards %>% 
  inner_join(df, by = c("WARD" = "ID"))

tmap_mode("view")
## tmap mode set to interactive viewing
tm_shape(df2) +
  tm_polygons("candidate", 
              legend.title = "Winning candidate",
              popup.vars = "winning_percent")

What was the winning margin of Kelda Roys over Nada Elmikashfi?

elect2 <- elect %>% 
  mutate(margin = roy_pct - elm_pct,
         ID = as.integer(str_extract(precincts, "[0-9]{3}")))

df3 <- wards %>% 
  inner_join(elect2, by = c("WARD" = "ID"))

tm_shape(df3) +
  tm_polygons("margin", title = "Winning margin Kelda Roys")
## Variable(s) "margin" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full spectrum of the color palette.