Voting map by Census Block Groups
2020 U.S. Presidential election: Cook County, Illinois
In this post I’m going to explore the tigris and leaflet packages to make some maps. I live in Cook County, Illinois, and I thought it would be interesting to create a relatively granular map of the county. After a little research, I found a detailed dataset containing projected votes for the 2020 U.S. Presidential election at the U.S. Census block group level.
To quote the Census Bureau, “Block Groups are statistical divisions of census tracts, are generally defined to contain between 600 and 3,000 people, and are used to present data and control block numbering”. This is exactly the type of detail I need to create maps with a high level of granularity.
Firstly, I load the packages I need. The tigris package contains the shapefiles for making the maps. A shapefile is a vector data storage format for storing the location, shape, and attributes of geographic features. The leaflet package has advanced mapping functionality.
# Load libraries required for this post
library(tidyverse)
library(tigris)
library(leaflet)Next, I load the locally stored data. I originally downloaded the .csv file from Harvard’s dataverse. The Voting and Election Science team on Harvard’s dataverse created a dataset where they compiled state 2020 U.S. Presidential election results and geographies. These were then projected onto U.S. Census block group geographies to form the final dataset. Votes are attributed based upon the proportion of the voting precinct’s area that intersects the corresponding block group.
# Load the data
# It contains voting projections at the Census Block group level
bg_voting_data <- read_csv("2021blockgroupvoting.csv")Beacuse the dataset contains the block group GEOID, it can be joined together with the corresponding shapefiles needed to create a map. In order to do this, I use the tigris R package, which has shapefiles at the block group level. I select only data for Cook county.
# Pull the block group shapefile data for Cook county
# Remove one block group that covers only the Lake Michigan part of Cook county
cook_bg <- block_groups(state = "IL", county = "Cook") %>%
filter(!GEOID == "170319900000") # remove area over Lake MichiganThe next step in the data preparation is to reduce the nationwide block group level voting dataset down to just Cook county and to add variables to determine the winning party (‘winner’) and the margin of victory (‘winning margin’).
# Using an inner join, join the cook county shapefiles to the voting data
# bg_voting_data - 233,866 block groups for the whole U.S.
# cook_bg - 4002 block groups for Cook county, IL
bg_voting_data_cook <- bg_voting_data %>%
inner_join(cook_bg, by = c("BLOCKGROUP_GEOID" = "GEOID"))
# Create a winner column populated with either REP or DEM
vote_res <- bg_voting_data_cook[, 1:4]
bg_voting_data_cook$winner <- colnames(vote_res)[max.col(vote_res, ties.method = "first")]
# Calculate the difference between the REP and DEM vote percentages,
# per block group
bg_voting_data_cook <- bg_voting_data_cook %>%
mutate(total_votes = select(., REP:OTH) %>%
rowSums(na.rm = TRUE)) %>%
mutate(rep_perc = (REP / total_votes) * 100) %>%
mutate(dem_perc = (DEM / total_votes) * 100) %>%
mutate(win_margin = dem_perc - rep_perc)
# Reduce the number of variables to keep only the ones I need
bg_voting_data_cook <- bg_voting_data_cook %>%
select("BLOCKGROUP_GEOID", "winner", "win_margin", "REP", "DEM")
# Create the final dataset by joining the voting data back to the
# Cook county shapefile dataset
cook_bg_2020 <- cook_bg %>%
inner_join(bg_voting_data_cook, by = c("GEOID" = "BLOCKGROUP_GEOID")) # %>%I use ggplot2 to create the first map. It shows the block groups by winning party. Red for Republican and blue for Democrat.
# Create a ggplot to map the block group votes
gg_bg_2020 <- ggplot() +
geom_sf(
data = cook_bg_2020, aes(fill = winner),
color = "black", size = 0.1
) + # block groups
scale_fill_manual(values = c("#0015bc", "#e9141d")) +
theme(
axis.title.y = element_blank(),
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
axis.title.x = element_blank(),
axis.text.x = element_blank(),
axis.ticks.x = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.background = element_blank()
) +
labs(
title = "2020 U.S. Presidential Election Results ",
subtitle = "Census block group level; Cook county, Illinois",
fill = "Winning Party"
)
gg_bg_2020
While the map above gives a good picture overall, clearly Democrats are more popular with Cook county voters than Republicans, it is difficult to obtain more specific information and fully benefit from the block group level detail.
Therefore, in order to leverage the detail in the dataset, it makes sense to use the leaflet R package to create a map overlaid with the block group level voting data. The two main benefits of doing this are:
- the ability to zoom into a location of interest
- the ability to hover over the block groups to see the actual voting numbers
# Create a leaflet map overlaid with voting data at block group level
# Map the colorFactor colors to categorical variables
# These are the two colors for the overlay
pal <- colorFactor(
palette = c("#0015bc", "#e9141d"),
levels = c("DEM", "REP")
)
# Create the hover tooltip labels
labels <- sprintf(
"<strong>%s</strong><br/><strong>%s</strong> %s<br/><strong>%s</strong> %s",
"Result", "REP: ", round(cook_bg_2020$REP), "DEM: ", round(cook_bg_2020$DEM)
) %>% lapply(htmltools::HTML)
# Generate the map with the block group voting overlay
leaflet(cook_bg_2020) %>%
addTiles() %>%
addPolygons(
color = "black", weight = 1,
fillColor = ~ pal(winner),
fillOpacity = 0.3,
highlightOptions = highlightOptions(
weight = 3,
fillOpacity = 0.7
),
label = labels,
labelOptions = labelOptions(
style = list(
"font-weight" = "normal",
padding = "3px 8px"
),
textsize = "15px",
direction = "auto"
)
)