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.
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:
electelect and ward doesn’t matchelect seem to be possibly not uniqueelect, and several wards in wards with an ID of -1elect <- elect %>%
mutate(total_votes = rowSums(.[2:9])) %>%
mutate(across(2:9, ~ .x/total_votes*100, .names = "{col}_pct"))
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"
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.