knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.retina = 5,
  fig.align = "center"
)

The votevizr package provides tools to visualize election results in systems where voters rank candidates (ordinal voting systems). This class of systems includes:

For positional methods, these tools make sense only for three-candidate elections. For Condorcet and RCV, they can also be used when there is an identifiable top three candidates.

The goal: visualizing ordinal election results

In November 2018, YouGov ran a poll in which UK respondents were asked to rank three options for the UK's future relationship with the EU. Those options were

UK-wide, Ben Lauderdale estimated that the proportions for each preference ordering were:

brexit_prefs <- list("Remain > Deal > No deal" = .375, 
               "No deal > Deal > Remain" = .228, 
               "Deal > No deal > Remain" = .212, 
               "Remain > No deal > Deal" = .087, 
               "Deal > Remain > No deal" = .059, 
               "No deal > Remain > Deal" = .038)
df <- data.frame("Ranking" = names(brexit_prefs), 
           "Proportion" = unlist(brexit_prefs),
           row.names = NULL) 
knitr::kable(df)

It would be useful to have a diagram that shows how ordinal preference shares like these produce a winner in various voting systems. Such a diagram could clarify why different voting rules might produce a different winner from the same ballots, illustrate how changes in preferences could produce a different outcome in a given voting system, and illuminate properties of voting systems such as non-monotonicity, join-inconsistency, and strategic voting.

The difficulty is that the poll result is a 5-dimensional object (6 proportions that sum to 1), so we have to simplify it somehow to make a useful diagram. (If incomplete rankings are possible, there could be 8 dimensions.)

Brief explanation of my solution

My approach is to

This approach is explained in detail in this paper (working paper version here).

Using votevizr for the Brexit poll

First, specify the ordinal preference data in a named list:

brexit_prefs <- list("Remain > Deal > No deal" = .375, 
               "No deal > Deal > Remain" = .228, 
               "Deal > No deal > Remain" = .212, 
               "Remain > No deal > Deal" = .087, 
               "Deal > Remain > No deal" = .059, 
               "No deal > Remain > Deal" = .038)

The name of each list element is a string combining candidate names (here, "Remain", "Deal", "No deal") in the relevant order. Candidate names should be separated by an unambiguous string (here it's " > ", but it could be "_", "|", etc).

To save you some typing, you can also access this data by typing data(brexit_prefs):

data(brexit_prefs)
brexit_prefs

You can now use votevizr::qplot_votevizr() to make plots. (If you want more flexibility, you could also use votevizr::first_preference_win_regions() to extract the coordinates of first-preference win regions and build your own plots from scratch. See votevizr: under the hood.)

If we'll be using votevizr::qplot_votevizr() to make plots, we need to load not only votevizr but also ggplot2 and dplyr:

library(votevizr)
library(ggplot2)
library(dplyr)

Then a call to votevizr::qplot_votevizr() shows who would have won an RCV election:

brexit_prefs %>% 
  qplot_votevizr(split = " > ", method = "RCV")

This tells us that if there were an RCV election with ballots that reflected the poll result above, the winner would have been "Remain" or "Deal" -- it was essentially a tie. In fact it was a tie in two respects: "No deal" and "Deal" received essentially the same first-preference support (both well behind "Remain"); if "No deal" finished second "Remain" would win easily, while if "Deal" finished second "Remain" and "Deal" would essentially tie in the final count. To really understand how the figure shows this, you'll have to look at my paper.

Interestingly, "Remain" would win more comfortably not just if it won more support (with the dot moving down and to the right) but also if "No deal" won more support (moving the dot down and to the left): this is because "No deal" would then have eliminated "Deal" in the first round, and "Remain" beats "No Deal" more easily than it defeats "Deal".

If instead we used Borda count, "Deal" would win easily:

brexit_prefs %>% 
  qplot_votevizr(split = " > ", method = "Borda")

Here is a hypothetical positional method where a first ranking gets 1 point, a last ranking gets 0 points, and a second ranking gets 2/5 of a point, which would produce a near tie between Remain and Deal:

brexit_prefs %>% 
  qplot_votevizr(split = " > ", method = "positional", s = 2/5)

Here we have the Condorcet method:

brexit_prefs %>% 
  qplot_votevizr(split = " > ", method = "Condorcet")

Note the white triangle where there is no winner due to a Condorcet cycle. We can specify how the winner is chosen in that case -- in this case, by Borda count:

brexit_prefs %>% 
  qplot_votevizr(split = " > ", method = "Condorcet", if_cycle = "Borda")

To alter the mapping of alternatives to vertices, use the vertex_varnames argument; to make the labels different from the ones in the named list, use the vertex_labels argument.[^padding] For example:

brexit_prefs %>% 
  qplot_votevizr(split = " > ", method = "Condorcet", if_cycle = "kemeny", 
                 vertex_varnames = c("Deal", "Remain", "No deal"),
                 vertex_labels = c("D", "R", "ND"))

[^padding]: Adjust the label_offset and padding arguments if necessary to fine-tune where the labels are placed and how much space there is for them.

Another illustration: San Fransciso mayoral election, 2018

We'll use votevizr::qplot_votevizr() to visualize the competition among the top three candidates in the 2018 San Fransciso mayoral election.

Access the data by typing data(sf_result):

data(sf_result)
sf_result

Voters were permitted to submit incomplete rankings (indeed, they were only permitted to rank three candidates), so by the time we get to the final three candidates (Breed, Leno, and Kim) there are many ballots listing only one candidate. We omit the third-ranked candidate in each case (a "Breed > Leno > Kim" ballot is equivalent to a "Breed > Leno" ballot).

Now make some plots. First, RCV:

qplot_votevizr(sf_result, split = "_", method = "RCV")

Then Condorcet, with no winner if there is a cycle:

qplot_votevizr(sf_result, split = "_", method = "Condorcet")

Or Borda count in the event of a cycle:

qplot_votevizr(sf_result, split = "_", method = "Condorcet", if_cycle = "Borda")

Or the Kemeny-Young method in the event of a cycle (zooming in to the key area):

qplot_votevizr(sf_result, split = "_", method = "Condorcet", if_cycle = "kemeny") + 
  coord_cartesian(xlim = c(2/5, 2/3), ylim = c(2/15, 6/15))

Want to know more?

Read my paper Social Choice and Welfare (preprint, published), which explains the figures in more detail and discusses previous attempts to represent election results in ternary diagrams.

See also the Under the hood vignette for more about how votevizr works, especially if you want more flexibility in making plots.



aeggers/votevizr documentation built on June 4, 2021, 9:10 p.m.