Re-districting in Winston-Salem

In this post I explore a potential outcomes to the composition of the Winston-Salem city council.

Political
Author
Affiliation
Published

April 8, 2019

Doi

Introduction

There was a recent bill introduced in the North Carolina General Assembly to reorganise the city counsel by two Republican state members of the General Assembly. The status quo is that there are a total of eight wards, each with a seat on the city counsel. The mayor votes only when there is a tie. As things are, the current city counsel is composed of four Black individuals and four white men, with a white man as mayor. The proposal from the General Assembly is to collapse several of the ward seats into five wards and then create three permanent at-large city counsel positions. Additionally, the mayor would have a vote on all matters, not just in ties.

There is a lot going on here that this post cannot unpack. The biggest issue is truely the political landscape in North Carolina where the Supreme Court of the United States of America is actively hearing a case on gerrymandering where a member of the general assembly admitted to voter packing. Additionally, Winston-Salem has an interesting relationship with racial and political sorting. All of these elements are at play with this new proposed structure. Additionaly, the members whose wards would be collapsed would be the three Black women who are currently on the city counsel. This paired with reducing terms to two hears makes it logistically harder to run campaigns (shorter term lengths mean more campaigning, at-large positions favor people with cash to campaign all over the city, etc). Just a lot happening that can’t be completely unpacked. See here for more details.

Analysis

I am going to take some historical voting records and see given the new ward proposal how the seats would have fallen. Again, this is a quick analysis and more work could defitely be done on this.

library(tidyverse)
── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ──
✓ ggplot2 3.3.5.9000     ✓ purrr   0.3.4.9000
✓ tibble  3.1.6.9001     ✓ dplyr   1.0.8.9000
✓ tidyr   1.1.4.9000     ✓ stringr 1.4.0.9000
✓ readr   2.1.2          ✓ forcats 0.5.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()
library(lubridate)

Attaching package: 'lubridate'
The following objects are masked from 'package:base':

    date, intersect, setdiff, union

Current State

And now because the records of each primary are made public, I will download them and read them in. Excuse the messiness; I imagine there is an easier way to do this, but the NC Board of Elections elected to change their output format between the years, so it is quicker just to read the files in one at a time as there aren’t too many of them. All of the data are available here if you would like to play along.

my_files <- list.files("data", full.names = T, pattern = "results_")

Now to read them in, fix a few things and then combine them. I will write a quick helper function though…

clean_ncbe <- function(df){
  df %>% 
  janitor::clean_names() %>% 
  rename(contest_name = contest, 
         choice_party = party) %>% 
  select(county, precinct, contest_name, contest_type, 
         total_votes, choice, choice_party)
}

Now to read them in.1

results_2010 <- read_csv(my_files[[1]])%>% 
  clean_ncbe()
Rows: 188007 Columns: 15
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (7): county, precinct, contest_type, contest, choice, party, district
dbl (8): runoff_status, recount_status, winner_status, Election Day, One Sto...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
results_2011 <- read_csv(my_files[[2]])%>% 
  clean_ncbe()
Rows: 37125 Columns: 15
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (7): county, precinct, contest_type, contest, choice, party, district
dbl (8): runoff_status, recount_status, winner_status, Election Day, One Sto...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
results_2012 <- read_csv(my_files[[2]])%>% 
  clean_ncbe()
Rows: 37125 Columns: 15
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (7): county, precinct, contest_type, contest, choice, party, district
dbl (8): runoff_status, recount_status, winner_status, Election Day, One Sto...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
z <- lapply( my_files[4:8], function(x) data.table::fread(x, sep = "\t"))

z <- data.table::rbindlist(z, fill =TRUE) %>% 
  janitor::clean_names()

We can then put the data together as follows:

all_results <-bind_rows(results_2010, results_2011, 
                        results_2012, z)

This data frame includes all precinct level reporting for each election. For the sake of this analysis I want to reduce my scope down to just Forsyth County, the home county of Winston-Salem.

forsyth_results <- all_results %>% 
  filter(grepl("FORSYTH",county)) %>% 
  mutate(precinct = str_extract(precinct, "\\d{2,}")) %>% 
  left_join(proposed_wards, by = c("precinct" = "pct_label")) %>% 
  mutate(my_date = lubridate::mdy(election_date),
         election = lubridate::year(my_date))

Upon inspection of the data it is interesting to note that straight party tickets were once allowed. This practice was outlawed by the North Carolina General Assembly in 2013 (Lynch n.d.).

Further, if I just wanted to look look a city counsel elections, the subject of this analysis I could do the following actions.

city_consel_elections <- forsyth_results %>% 
  filter(grepl("WARD", contest_name)) %>% 
  filter(!grepl("HIGH POINT", contest_name))

Just as a reminder the last city counsel elections occured in 2016. Previous legislation moved the city counsel elections to presidential election years. Additionaly, citizens have the option of writing in candidates, so that can explain some of the votes for “others.”

Looking at the overall 2016 city counsel elections we can see the following:

city_consel_elections %>% 
  mutate(choice_party = ifelse(is.na(choice_party), "Other", choice_party)) %>% 
  group_by(election, choice_party) %>% 
  summarise(total_vote = sum(total_votes, na.rm = T)) %>% 
  spread(choice_party, total_vote, 0) %>% 
  mutate(DEM_vote_share = scales::percent(DEM / (DEM + REP + V1))) %>% 
  knitr::kable(caption = "Vote Share Overall, 2016 City Counsel Elections")
`summarise()` has grouped output by 'election'. You can override using the
`.groups` argument.
Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if `.name_repair` is omitted as of tibble 2.0.0.
Using compatibility `.name_repair`.
This warning is displayed once every 8 hours.
Call `lifecycle::last_lifecycle_warnings()` to see where this warning was generated.
Vote Share Overall, 2016 City Counsel Elections
election V1 DEM REP DEM_vote_share
2016 1656 80697 23660 76%

So all in all Winston-Salem has a strong lean towards the Democratic Party. Digging deeper we can look at the individual wards.

city_consel_elections %>% 
  mutate(choice_party = ifelse(is.na(choice_party)|choice_party=="", "Other", choice_party)) %>% 
  group_by(election, contest_name, choice_party) %>% 
  summarise(total_vote = sum(total_votes, na.rm = T)) %>% 
  spread(choice_party, total_vote, 0) %>% 
  ungroup() %>% 
  mutate(contest_name = str_remove(contest_name, "WINSTON SALEM CITY COUNCIL - ")) %>% 
  mutate(DEM_vote_share = scales::percent(DEM / (DEM + REP + Other))) %>% 
  knitr::kable(caption = "Vote Share By Ward, 2016 City Counsel Elections")
`summarise()` has grouped output by 'election', 'contest_name'. You can
override using the `.groups` argument.
Vote Share By Ward, 2016 City Counsel Elections
election contest_name DEM Other REP DEM_vote_share
2016 EAST WARD 11673 204 0 98.28%
2016 NORTH WARD 15454 209 0 98.67%
2016 NORTHEAST WARD 13276 397 0 97.10%
2016 NORTHWEST WARD 9410 21 5984 61.04%
2016 SOUTH WARD 9832 38 4374 69.03%
2016 SOUTHEAST WARD 9044 184 0 98.01%
2016 SOUTHWEST WARD 12008 295 0 97.60%
2016 WEST WARD 0 308 13302 0.00%

We can see pretty easily from this analysis that Winston-Salem is pretty solidly democratic, with the exception of the West ward where not a single vote was cast of a democratic candidate. The only truly competitive districts were the South and Northwest wards which still strongly favoured democratic candidates. However, one of the major take-aways here is that there is evidence of strong population sorting occuring with solidly democratic neighborhoods and solidly republican neighborhoods.

New Proposal

So let’s see how this all plays out under the new proposed wards. If take the 2016 election and group by Wards we see that there still will only be one strong Republican ward, Ward 5. One thing that occludes a stronger take on this is that absentee ballots are not assigned to a precinct.

city_consel_elections %>% 
  mutate(choice_party = ifelse(is.na(choice_party)|choice_party=="", "Other", choice_party)) %>% 
  group_by(election, new_ward, choice_party) %>% 
  summarise(total_vote = sum(total_votes)) %>% 
  spread(choice_party, total_vote) %>% 
  ungroup() %>% 
  mutate(DEM_vote_share = scales::percent(DEM / (DEM + REP + Other))) %>% 
  knitr::kable(caption = "Vote Share By Ward, 2016 City Counsel Elections")
`summarise()` has grouped output by 'election', 'new_ward'. You can override
using the `.groups` argument.
Vote Share By Ward, 2016 City Counsel Elections
election new_ward DEM Other REP DEM_vote_share
2016 Ward 1 16865 228 5396 74.99%
2016 Ward 2 18669 361 58 97.80%
2016 Ward 3 15064 308 61 97.61%
2016 Ward 4 16376 233 3786 80.29%
2016 Ward 5 6904 375 11444 36.87%
2016 NA 6819 151 2915 68.98%

And we can see that while many absentee votes are democratic the margins could say some of the above districts to be more competitive. Because of this it is probably worth looking and historical voting.

city_consel_elections %>% 
  mutate(choice_party = ifelse(is.na(choice_party)|choice_party=="", "Other", choice_party)) %>% 
  group_by(contest_name, choice_party) %>% 
  summarise( total_ab = sum(absentee_by_mail)) %>% 
  mutate(percent = scales::percent(total_ab/sum(total_ab))) %>% 
  mutate(metric = paste0(percent, " n= ", total_ab)) %>% 
  select(-total_ab, -percent) %>% 
  spread(choice_party, metric) %>% 
  knitr::kable(caption = "Breakdown of Absentee Ballots, 2016 City Counsel Elections")
`summarise()` has grouped output by 'contest_name'. You can override using the
`.groups` argument.
Breakdown of Absentee Ballots, 2016 City Counsel Elections
contest_name DEM Other REP
WINSTON SALEM CITY COUNCIL - EAST WARD 98% n= 971 2% n= 19 NA
WINSTON SALEM CITY COUNCIL - NORTH WARD 98% n= 1043 2% n= 19 NA
WINSTON SALEM CITY COUNCIL - NORTHEAST WARD 98% n= 1194 2% n= 23 NA
WINSTON SALEM CITY COUNCIL - NORTHWEST WARD 58% n= 1070 0% n= 2 42% n= 773
WINSTON SALEM CITY COUNCIL - SOUTH WARD 60% n= 772 0% n= 5 40% n= 517
WINSTON SALEM CITY COUNCIL - SOUTHEAST WARD 98% n= 755 2% n= 16 NA
WINSTON SALEM CITY COUNCIL - SOUTHWEST WARD 97% n= 1014 3% n= 27 NA
WINSTON SALEM CITY COUNCIL - WEST WARD NA 2% n= 40 98% n= 1625

NC Voter History

nc_voter_vistory <- read_tsv("data/ncvhis34.txt")
Rows: 1060196 Columns: 15
── Column specification ────────────────────────────────────────────────────────
Delimiter: "\t"
chr (13): county_desc, voter_reg_num, election_lbl, election_desc, voting_me...
dbl  (2): county_id, voted_county_id

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Additionally, let’s use the 2016 presidential election using the voter history files from the NC Board of Elections here.

nc_voter_vistory %>% 
  left_join(proposed_wards) %>% 
  filter(!is.na(new_ward)) %>% 
  filter(election_desc == "11/08/2016 GENERAL") %>% 
  filter(!is.na(voted_party_desc)) %>% 
  mutate(voted_party_desc = ifelse(voted_party_desc=="LIBERTARIAN",
                                   "REPUBLICAN", voted_party_desc)) %>% 
  group_by(new_ward, voted_party_desc) %>% 
  summarise (n = n()) %>% 
  mutate(vote_share = round(n/sum(n)*100,0)) %>% 
  knitr::kable(caption = "Vote Share By New Ward, 2016 Voters Party Affiliations")
Joining, by = "pct_label"
`summarise()` has grouped output by 'new_ward'. You can override using the
`.groups` argument.
Vote Share By New Ward, 2016 Voters Party Affiliations
new_ward voted_party_desc n vote_share
Ward 1 DEMOCRATIC 16212 47
Ward 1 REPUBLICAN 9950 29
Ward 1 UNAFFILIATED 8307 24
Ward 2 DEMOCRATIC 20381 62
Ward 2 REPUBLICAN 6449 20
Ward 2 UNAFFILIATED 5970 18
Ward 3 DEMOCRATIC 14668 47
Ward 3 REPUBLICAN 9429 30
Ward 3 UNAFFILIATED 7106 23
Ward 4 DEMOCRATIC 12544 48
Ward 4 REPUBLICAN 6497 25
Ward 4 UNAFFILIATED 7269 28
Ward 5 DEMOCRATIC 12856 33
Ward 5 REPUBLICAN 15220 39
Ward 5 UNAFFILIATED 10896 28

Again, what is interesting here is that all but one of the Wards start with a clearly identified Democratic plurality.

Voter Efficiency

While not the best metric we can look at the efficiency gap. So looking at the current situation we see that there is a:

city_consel_elections %>% 
  mutate(choice_party = ifelse(choice_party %in% c("DEM", "REP"), choice_party,
                               "other")) %>% 
  group_by(election, contest_name, choice_party) %>% 
  summarise(total_vote = sum(total_votes, na.rm = T)) %>% 
  spread(choice_party, total_vote, 0) %>% 
  mutate(winning = (DEM+other+REP)/2+1) %>% 
  rowwise() %>% 
  mutate(DEM_wasted_vote = ifelse(DEM - winning<0,DEM,DEM - winning),
         REP_wasted_vote = ifelse(REP - winning<0,REP,REP - winning)) %>% 
  ungroup() %>% 
  summarise(total_voter= sum(REP+other +DEM),
            wasted_dem = sum(DEM_wasted_vote),
            wasted_rep = sum(REP_wasted_vote)) -> current_eff
`summarise()` has grouped output by 'election', 'contest_name'. You can
override using the `.groups` argument.
scales::percent((current_eff$wasted_rep-current_eff$wasted_dem)/current_eff$total_voter)
[1] "-17%"

And if we move to the proposed organization we see:

forsyth_results %>% 
  filter(!is.na(new_ward)) %>% 
  filter(grepl(x = contest_name, "PRESIDENT")) %>% 
  mutate(choice_party = ifelse(choice_party %in% c("DEM", "REP"), choice_party,
                               "other")) %>% 
  group_by(election, new_ward, choice_party) %>% 
  summarise(total = sum(total_votes)) %>% 
  ungroup() %>% 
  spread(choice_party, total) %>% 
  mutate(election = ifelse(is.na(election), 2012, election)) %>% 
  mutate(DEM_share = DEM/ (DEM+other+REP)) %>% 
  mutate(winning = (DEM+other+REP)/2+1) %>% 
  rowwise() %>% 
  mutate(DEM_wasted_vote = ifelse(DEM - winning<0,DEM,DEM - winning),
         REP_wasted_vote = ifelse(REP - winning<0,REP,REP - winning)) %>% 
    ungroup() %>% 
  summarise(total_voter= sum(REP+other +DEM),
            wasted_dem = sum(DEM_wasted_vote),
            wasted_rep = sum(REP_wasted_vote))->efficiency
`summarise()` has grouped output by 'election', 'new_ward'. You can override
using the `.groups` argument.
scales::percent((efficiency$wasted_rep-efficiency$wasted_dem)/efficiency$total_voter)
[1] "16%"

So no real change in the efficiency gap.

References

Lynch, Harry. n.d. “Elimination of Straight-Ticket Voting Could Leave More Ballots Incomplete.” Newsobserver. Accessed April 8, 2019. https://www.newsobserver.com/news/politics-government/election/article95286262.html.

Footnotes

  1. Reall what I should be doing here is writing a function that inspects what kind of delimiter is being used in the file and then import accordingly. However, I’m a bit lazy at the moment and not going to do that. The plus sides for doing that method is most importantly it is more reprocible and scalable should I go back and look at previous data. Additionally, I could do a map function to read and apply the newly written function rather than writing piecemeal as I am now.↩︎

Reuse

Citation

BibTeX citation:
@online{dewitt2019,
  author = {Michael DeWitt},
  title = {Re-Districting in {Winston-Salem}},
  date = {2019-04-08},
  url = {https://michaeldewittjr.com/programming/2019-04-08-re-districting-in-winston-salem},
  doi = {10.5281/zenodo.2635742},
  langid = {en}
}
For attribution, please cite this work as:
Michael DeWitt. 2019. “Re-Districting in Winston-Salem.” April 8, 2019. https://doi.org/10.5281/zenodo.2635742.