Visualisation of ranked choice voting in R
By: David Mulholland
Tags: Voting, Data Science, Editors Pick, Visualization, Politics

Visualisation of ranked choice voting in R

Tables with gt and animation with tweenr

I recently returned to the R package avr , which runs a range of alternative voting procedures, to add more functionality, and in the process, got to grips with two visualisation packages: gt and tweenr . Each provides a different solution to the problem I had, which was how to take a count table from the multiple rounds of a Single Transferable Vote (STV) count, like this

and make it visually appealing.

Nicer table formatting

gt is a comprehensive approach to styling tables for HTML output. The basic usage is to prepare a data frame, pipe to gt() to turn the tabular data into HTML, and provide styling options through further piped operations. Some examples of how this works are

df %>% gt() %>%

#set width of columns named with a digit (the round number)

cols_width(

matches('\\d') ~ px(30)

) %>%

#change font to bold in cells selected by both column and row

text_transform(

locations = cells_body(matches('\\d')),

function(x) ifelse(x=='E', sprintf("<span style='font-weight: bold'>%s</span>",x), x)

) %>%

#add background colour to cells based on value

data_color(

columns = vars(`1`),

colors = scales::col_numeric(

palette = c('snow','pink'),

domain = NULL

),

alpha = 1

) %>%

#add a column group header

tab_spanner(

label = "STV rounds",

columns = matches('\\d')

) %>% ...

While this to a great extent can be used as an alternative to kableExtra , one particularly nice feature of the latter that can be used with gt is its sparkline functions, such as spec_hist . This article provides a great explanation of how to convert the output, resulting in this code in my case:

... %>%

mutate(Preferences = purrr::map(Candidate, function(c) get_candidate_ballot_profile(votes_as_ballots, c, n_candidates)),

Preferences = purrr::map(Preferences, kableExtra::spec_hist, col='lightgray', breaks=seq(0.5, n_candidates+0.5, 1), lim=c(0.5, n_candidates+0.5)),

Preferences = purrr::map(Preferences, 'svg_text'),

Preferences = purrr::map(Preferences, gt::html)) %>%

gt() %>% ...

where get_candidate_ballot_profile converts the vote data from a list of ballots to a list of counts of numeric preferences for each candidate. map is used to apply the transformations to each candidate (row) in the data frame.

Without too much work, I was able to produce this version of the count table:

The sparkline histograms show the full preference information for each candidate, and the shading on the first preference votes column highlights the most important part of the count process (since the majority of a candidate’s final vote total usually comes from first preference votes).

Some great examples of what can be produced with gt with a bit more effort can be seen in the results of the recent RStudio table contest.

Animated count

To show the STV count as an animation, I wanted to use a dot plot and show individual votes moving from one candidate to another. Although avr processes lists of ballots (lists of preferences), I based the animation on only the output count table (see above), so that it could also be used on real-world votes, where the individual ballots are not available.

The basic usage of tweenr to create an animated gif is

#build a data frame from the input data, repeated across multiple frames

animation_data <- df %>%

filter(...get data for the first state...) %>%

keep_state(10) %>%

tween_state(

df %>% filter(...get data for the second state...),

ease = 'cubic-in-out', nframes = 15, id = id_column_name

) %>%

keep_state(10) %>%

etc. #save a plot of each frame

for (i in 1:max(animation_data$.frame)) {

animation_data %>% filter(.frame==i) %>% ggplot() + ...

ggsave(filename=sprintf('frame%03d.png',i), ...)

} #convert the images to a gif using imagemagick

animation::im.convert("frame*.png", output = out_gif_path, convert='convert')

The tween_state and keep_state functions replicate the input data frame for a specified number of frames, and in the case of tween_state , numeric values are incremented to move from the previous state to the new state, in a manner controlled by the ease parameter.

When applied to a plot, this can move objects in the plot (points, etc.) according to their id in the input data frame, but only if we are in control of the positioning of the objects in the plot in each frame; therefore, ( gg )plot geoms that position the data automatically for the user, such as geom_dotplot , don’t work well with the animation package. Instead, I constructed a dot plot manually using geom_point , to allow different points (ids) to move between different pairs of candidates. This required some unwieldy code to generate coordinates for each vote at each round of the count: candidates need to be ordered vertically according to their position in the count as of that round, and when votes are transferred from a source candidate to a target candidate, they should be added to the end of the target candidate’s line of dots.

The result is not particularly interesting for this toy example vote, but the method can easily be applied to much larger votes (see below), by using points to represent batches of more than one vote, depending on the total number of votes involved in the count. Through the animation, it is possible to see how one candidate’s transferred votes are divided among the remaining candidates.

Note that there is a newer package from the same author, gganimate , which builds upon tweenr . For many use cases this package is very convenient, because if the input data is in tidy format, the transition_states function can be added to a ggplot object in a very similar way to facetting, and movement of individual points can be controlled using the group aesthetic. However, in my case I could not find an easy way to handle the varying candidate order on the y-axis or the round-dependent annotation of candidates as ‘Elected’, so I stuck with the greater control of applying tweenr functions manually, looping through frames and saving one image per frame, and converting these to a gif using imagemagick (via the animation package).

The count animation function can also be used for larger, real-world examples, if the count table is supplied manually (knowledge of the individual ballots is not required). Here is the 2017 Northern Ireland Assembly vote in the Upper Bann constituency, where 5 seats were available. Candidates are coloured by political party.

Or this nail-biter in the 2017 Scottish council elections, which came down to 13 votes, out of 11,000, for the last of the three seats. While finding this example, I realised that STV, globally, is not as widely used as I had thought, or as I think it should be.

Summary

The avr package can process votes using STV and several other ranked choice voting methods, and generate a report of the results, including the tabular display presented above, and analysis of the vote transfers that occurred, which gives insight into the relationships between the candidates (who tends to transfer votes to whom). It is available on Github.