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.
── 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.
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.
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.
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()
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.
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.
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:
`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.
`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.
`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:
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.↩︎