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.
.. code:: ipython3
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.
.. code:: ipython3
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.
.. code:: ipython3
election = STV(profile, n_seats=2)
election
.. parsed-literal::
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.
.. code:: ipython3
animation = STVAnimation(election)
animation.render()
.. parsed-literal::
.. raw:: html
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.
.. code:: ipython3
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.
.. code:: ipython3
animation2 = STVAnimation(election, focus="all")
animation2.render()
.. parsed-literal::
.. raw:: html
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 ``nicknames`` argument.
- Maybe we want a lighter color scheme. We can feed the
``LIGHT_PALETTE`` color palette to the ``color_palette`` argument.
- 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:
.. code:: ipython3
# 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()
.. parsed-literal::
.. raw:: html