Animating STV Elections
For all their benefits, STV elections can be difficult to follow. Each round, support flows from elected or eliminated candidates to the rest. It can be difficult to parse how the election went just by looking at the raw data. For this reason, it is common to make simple animations of STV elections in order to visualize the flow of support between candidates and ultimately to understand what happened.
VoteKit offers an interface for automatically generating such animations of STV elections. In this notebook, we will see how to use it.
Installing VoteKit’s Animation Capabilities
The animation capabilities rest on optional dependencies. To install VoteKit with these optional dependencies, run
pip install votekit[animation]
VoteKit’s animations use a Python package called Manim. On Linux, Manim may require some system-level dependencies. Use your package manager. On Ubuntu, for instance, the dependencies can be installed with
apt-get install -y build-essential pkg-config libcairo2-dev libpango1.0-dev
Our First Animation
Before we can see the animation interface in action, we’ll need an STV election on which to run it.
First we make the necessary imports.
from votekit.pref_profile import RankProfile
from votekit.ballot import RankBallot
from votekit.elections import STV
from votekit.animations import STVAnimation, LIGHT_PALETTE
Now we’ll need some ballots. In previous notebooks, we’ve seen how to get ballot profiles from real-world data, and how to generate ballot profiles from simulations. For this example, we’ll just make some ballots up.
profile = RankProfile(
ballots=(
RankBallot(ranking=({"Orange"}, {"Pear"}, {"Cake"}), weight=2),
RankBallot(ranking=({"Orange"}, {"Burger"}), weight=1),
RankBallot(ranking=({"Pear"}, {"Strawberry"}, {"Cake"}), weight=8),
RankBallot(ranking=({"Pear"},), weight=2),
RankBallot(ranking=({"Strawberry"}, {"Orange"}, {"Pear"}), weight=1),
RankBallot(ranking=({"Cake"}, {"Chocolate"}), weight=3),
RankBallot(ranking=({"Chocolate"}, {"Cake"}, {"Burger"}), weight=1),
RankBallot(ranking=({"Burger"}, {"Chicken"}), weight=4),
RankBallot(ranking=({"Chicken"}, {"Chocolate"}, {"Burger"}), weight=3),
RankBallot(ranking=({"Cake"},), weight=2),
RankBallot(ranking=({"Pear"}, {"Burger"}), weight=1),
),
)
Now let’s simulate an STV election on these ballots. Let’s elect, say, two candidates.
election = STV(profile, n_seats=2)
election
Status Round
Pear Elected 1
Cake Elected 7
Burger Eliminated 6
Orange Eliminated 5
Chicken Eliminated 4
Strawberry Eliminated 3
Chocolate Eliminated 2
Looking at the simulated election above, it looks like Pear was elected in the first round, and Cake was elected in the very last round after a long string of eliminations. This is a pretty thin description of the election process. Maybe we can learn more from watching the election play out in an animation.
Let’s do it! All we have to do is make an STVAnimation object from
our election and render it. This cell should take a couple minutes to
run.
animation = STVAnimation(election)
animation.render()
One particular step in the animation may have been confusing. When Burger was eliminated, none of their votes transferred to other candidates; they all vanished. This is because Burger was the last candidate listed on all of the votes that were supporting them at the moment. When Burger was eliminated, there was nowhere else for those votes to go, so they disappear from the process. We say those votes were exhausted.
In general, when a candidate is elected or eliminated, the votes that don’t list a next option get exhausted and disappear from the system. For instance, in the above animation, we can also see some of Pear’s excess votes get exhausted after they get elected in round 1.
Let’s save this video.
animation.save("video.mp4")
This is a fine video, but only a few candidates show up in the animation. Where are all the other candidates?
By default, STVAnimation doesn’t show candidates that were mentioned
on so few ballots that they couldn’t concievably meet the quota. That
is, it only shows the viable candidates. If we’d like to see the rest,
we can change which candidates are on-screen with the focus flag.
animation2 = STVAnimation(election, focus="all")
animation2.render()
The focus argument provides a few options. See the rest of the
options in the documentation.
Customization
STVAnimation provides several customization options.
Maybe we want to add a title card to the animation to play at the beginning.
Maybe some candidates have long names and we’d like to use nicknames. Then we can feed a dictionary to the
nicknamesargument.Maybe we want a lighter color scheme. We can feed the
LIGHT_PALETTEcolor palette to thecolor_paletteargument.Maybe we like the light color palette, but we want the green used for winners to be a little brighter. We can edit the LIGHT_PALETTE to make this happen.
It’s a bit strange to see the candidate Orange in a color other than orange. Maybe we’d like to stipulate that Orange be represented with a specific shade of orange.
Let’s make all of these customizations:
# Pick a title
title = "STV Election for Two Seats"
# Pick a nickname for the candidate "Strawberry"
nicknames = {"Strawberry": "Berry"}
# Use the light palette
color_palette = LIGHT_PALETTE
# ... but change the winner color to a brighter green
color_palette.winner = "#59FF00" # Hex codes, RGB and rgb triples will all work
# Set a custom color for the "Orange" candidate
candidate_colors = {"Orange": "#C85000"}
# Now we make the animation and render
animation3 = STVAnimation(
election,
title=title,
focus="all",
color_palette=color_palette,
candidate_colors=candidate_colors,
)
animation3.render()