Tutorial 10: Elections
Setup: before you start
Create a new empty project in Spyder, and call it “Tutorial_10”.
Now create a new program file,
election.py
.
You will need to download the following files:
Right-click on each of the links and select “Save link as…”, saving each file in your Election project directory.
Election day: politics on Isla Bonita
It is election day in the People’s Republic of La Isla Bonita. All of their political parties are known by their colours:
- BLUE are the conservatives;
- RED are the socialists;
- GREEN are the ecologists;
- WHITE are the neo-fascists;
- PURPLE are the populists who claim not to be politicians.
The Bonitans have all cast their votes using electronic machines, and they have been collected together in a file. Our job is to take this data and compute the winner of the election, comparing the results under several different voting systems.
Exercises
Exercise 1: Votes
The first step is to define a class of objects modelling individual votes. The votes in Isla Bonita allow voters to express their list of preferences, rather than just their first choice. Of course, a voter is always allowed to list no parties in their vote if they can’t stand the thought of any of them winning: this is called informal voting, or voting blank.
Copy the following beginning of a class definition,
including the docstring, into your election.py
:
class Vote:
"""A single vote object.
Data attributes:
- preference_list: a list of the preferred parties for this voter,
in descending order of preference.
"""
Now add the following four methods
to the Vote
class:
__init__
, which takes a list of preferences and initializes thepreference_list
attribute of the object. (the declaration should start withdef __init__(self, preference_list):
__str__
, which produces a printable string using the following rule:- if
self.preference_list
is empty, then we return the string'Blank'
- otherwise, we return a string composed of the strings in
self.preference_list
in order, separated by the string' > '
(that is, a space, followed by a greater-than, followed by another space; see below for examples).
- if
__repr__
, which produces a string which, if typed into Python, would produce aVote
object with the samepreference_list
attribute.first_preference
, which returns the first party in thepreference_list
, orNone
if the list is empty.
Hint: Try using the .join
method of
strings to complete your __str__
method. (Recall that
sep.join(seq)
returns the concatenation of the strings in
seq
with sep
between each of them.)
Test your code.
First, we test what happens with a trivial (empty) vote:
In [1]: v_1 = Vote([])
In [2]: v_1.preference_list
Out[2]: []
In [3]: str(v_1)
Out[3]: 'Blank'
In [4]: repr(v_1)
Out[4]: 'Vote([])'
In [5]: v_1.first_preference() is None
Out[5]: True
(Note that IPython never explicitly shows None
as an
output; we need to compare directly against None
to detect
it.)
Let’s continue with a simple, but nontrivial, vote:
In [6]: v_2 = Vote(['RED'])
In [7]: v_2.preference_list
Out [7]: ['RED']
In [8]: str(v_2)
Out[8]: 'RED'
In [9]: repr(v_2)
Out[9]: "Vote(['RED'])"
In [10]: v_2.first_preference()
Out[10]: 'RED'
Now let’s check that our code isn’t confused by multiple preferences:
In [11]: v_3 = Vote(['GREEN', 'RED', 'BLUE'])
In [12]: v_3.preference_list
Out [12]: ['GREEN', 'RED', 'BLUE']
In [13]: str(v_3)
Out[13]: 'GREEN > RED > BLUE'
In [14]: repr(v_3)
Out[14]: "Vote(['GREEN', 'RED', 'BLUE'])"
In [15]: v_3.first_preference()
Out[15]: 'GREEN'
Upload your election.py file:
Exercise 2: Setting up the election process
Now we define a class of objects to model elections. These objects will distribute the votes into piles.
Copy the following beginning of a class definition
into your election.py
file:
class Election:
"""A basic election class.
Data attributes:
- parties: a list of party names
- blank: a list of blank votes
- piles: a dictionary with party names for keys
and lists of votes (allocated to the parties) for values
"""
def __init__(self, parties):
self.parties = parties
self.blank = []
self.piles = {name: [] for name in self.parties}
We have defined the __init__
method of
Election
to save you some time; do not modify it
(yet).
The most interesting data attribute of an Election
object is piles
, which is a dictionary containing the votes
distributed according to their (first) preference. The keys are the five
party names, and the values are the lists of votes whose preference is
for the given party. The blank votes are kept aside in a separate list,
in the attribute blank
. (Unlike in France, blank votes are
officially counted and reported in Isla Bonita.)
During the election, there are fundamental things we need to be able
to do: adding votes to the election (and sorting them into the correct
pile), and tracking the state of the vote count as votes are added (and
later, between rounds). But when counting large numbers of votes, it is
useless to print out the entire contents of the piles
dictionary including each individual Vote
object:
all we want is the party names and the sizes of their vote
piles.
Add two new methods to your Election
class:
add_vote
, which takes aVote
object and appends it at the end the appropriate pile in theElection
(that is, the pile corresponding to the first party listed in theVote
’s preferences). If theVote
has an empty preference list, then theVote
is appended to theblank
list.status
, which takes no arguments, and which returns a dictionary whose keys are the names of the parties in the piles, and whose values are the number of votes currently held by that party.
def add_vote(self, vote):
"""Append the vote to the corresponding pile."""
pass # remove this line and complete the method
def status(self):
"""Return the current status of the election:
a dictionary mapping each of the party names in the piles
to the number of votes in their pile.
"""
pass # remove this line and complete the method
Test your method. First, for a brand-new election object with no votes counted yet, we should have
In [16]: e_1 = Election(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [17]: e_1.status()
Out[17]: {'BLUE': 0, 'GREEN': 0, 'PURPLE': 0, 'RED': 0, 'WHITE': 0}
To get some more interesting results, we can directly insert some
votes into thepiles
. (This is not how we vote in practice,
but it helps for testing.)
In [18]: e_1 = Election(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [19]: e_1.add_vote(Vote(['RED', 'GREEN']))
In [20]: e_1.add_vote(Vote(['RED', 'PURPLE']))
In [21]: e_1.add_vote(Vote(['GREEN', 'RED']))
In [22]: e_1.add_vote(Vote([]))
In [23]: e_1.status()
Out[23]: {'BLUE': 0, 'GREEN': 1, 'PURPLE': 0, 'RED': 2, 'WHITE': 0}
In [24]: e_1.blank
Out[24]: [Vote([])]
Upload your election.py file:
Exercise 3: Reading in votes from a file
Isla Bonitans vote electronically. When the polls close on election day, we are given a file containing all of the votes. Each line of the file contains zero or more party names, separated by spaces; the line represents a single vote, with the party names on that line in descending order of preference. For example,
- a blank line corresponds to the vote
Vote([])
- the line
RED BLUE GREEN PURPLE
corresponds to the voteVote(['RED', 'BLUE', 'GREEN', 'PURPLE'])
.
Add a method add_votes_from_file
to
your Election
class. This method will read in the contents
of a given file, and add each of the votes in it to the correct pile of
the election (using the add_vote
method). Your
add_votes_from_file
method should begin as follows:
def add_votes_from_file(self, filename):
"""
Convert each line of the file into a Vote,
and append each of the votes to the correct pile.
"""
pass # remove this line and complete the method
The voting software used on Isla Bonita prevents anyone from voting for non-existent parties, so we don’t have to worry about any party names other than RED, GREEN, BLUE, PURPLE, or WHITE.
Test your function. Your should be able to replicate the following hehaviour in the console exactly:
In [25]: e_2 = Election(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [26]: e_2.add_votes_from_file('votes-10.txt')
In [27]: e_2.status()
Out[27]: {'BLUE': 2, 'GREEN': 1, 'PURPLE': 0, 'RED': 4, 'WHITE': 3}
Check your output carefully!
If we look deeper into the piles of votes, we should see
In [28]: e_2.piles
Out[28]:
{'BLUE': [Vote(['BLUE']), Vote(['BLUE', 'PURPLE', 'WHITE'])],
'GREEN': [Vote(['GREEN', 'RED'])],
'PURPLE': [],
'RED': [Vote(['RED', 'GREEN']),
Vote(['RED', 'WHITE', 'PURPLE']),
Vote(['RED', 'BLUE']),
Vote(['RED', 'GREEN', 'WHITE'])],
'WHITE': [Vote(['WHITE', 'BLUE']),
Vote(['WHITE', 'BLUE', 'PURPLE']),
Vote(['WHITE', 'PURPLE'])]}
In [29]: e_2.blank
Out[29]: [Vote([])]
Upload your election.py file:
First-past-the-post voting
The first kind of voting system we will implement is the simplest of all, known as first past the post voting. The principle is quite brutal: whoever gets the most votes wins, even if they do not get a majority. This system is used in many places, including British parliamentary elections.
Under this system, only the first preference of a vote is counted; all of the other preferences are simply ignored.
An example election under first-past-the-post rules:
For example, suppose we start with these ten preference lists:
['BLUE', 'WHITE', 'PURPLE', 'GREEN', 'RED']
,['GREEN', 'RED']
,['BLUE', 'PURPLE']
,['RED']
,[]
(a blank vote),['RED', 'GREEN']
,['BLUE', 'WHITE', 'PURPLE']
,['WHITE', 'PURPLE']
,['WHITE', 'GREEN', 'RED']
,[]
(a blank vote).
After the initial count, putting votes into piles according to their first preference, we have:
- RED pile: votes 4 and 6
- BLUE pile: votes 1, 3, and 7
- WHITE pile: votes 8 and 9
- GREEN pile: vote 2
- PURPLE pile: no votes
- blank pile: votes 5 and 10
The BLUEs have won this election (despite their failure to achieve a majority!).
Exercise 4: Running first-past-the-post elections
It’s time to count some votes! As we indicated above, a first-past-the-post election is brutally simple: whoever gets the most votes win.
Copy the following class definition into your
election.py
file:
class FirstPastThePostElection(Election):
"""
First-past-the-post elections: whoever gets the most first-preference votes wins.
"""
Notice that we are using inheritance here:
FirstPastThePostElection
is a subclass of
Election
, so it inherits Election
’s
__init__
, status
, add_vote
, and
add_votes_from_file
methods. We have the right to override
any of these by defining new methods with the same names in
FirstPastThePostElection
, but we will not need to do that
here.
Define a new method, winner
, in the
FirstPastThePostElection
class (and not in
Election
):
def winner(self):
"""The winner of this (first-past-the-post) election,
or None if the election is tied.
"""
Test your method. You should be able to reproduce the following behaviour in the console exactly. First, if there are no votes, then there is no winner:
In [30]: e_1 = FirstPastThePostElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [31]: e_1.winner() is None
Out[31]: True
Next, we can find the winner of the election we loaded in the previous exercise:
In [32]: e_1.add_votes_from_file('votes-10.txt')
In [33]: e_1.status()
Out[33]: {'BLUE': 2, 'GREEN': 1, 'PURPLE': 0, 'RED': 4, 'WHITE': 3}
In [34]: e_1.winner()
Out[34]: 'RED'
Now we can try some elections with more votes:
In [35]: e_2 = FirstPastThePostElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [36]: e_2.add_votes_from_file('votes-100.txt')
In [37]: e_2.status()
Out[37]: {'BLUE': 22, 'GREEN': 9, 'PURPLE': 15, 'RED': 26, 'WHITE': 16}
In [38]: e_2.winner()
Out[38]: 'RED'
Remember that when the vote is tied for first place, nobody wins:
In [39]: e_3 = FirstPastThePostElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [40]: e_3.add_votes_from_file('votes-tie.txt')
In [41]: e_3.status()
Out[41]: {'BLUE': 38, 'GREEN': 35, 'PURPLE': 30, 'RED': 35, 'WHITE': 38}
In [42]: e_3.winner() is None
Out[42]: True
But when there are a lot of random votes counted, we’re less likely to have that problem…
In [43]: e_4 = FirstPastThePostElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [44]: e_4.add_votes_from_file('votes-1000.txt')
In [45]: e_4.status()
Out[45]: {'BLUE': 176, 'GREEN': 167, 'PURPLE': 151, 'RED': 150, 'WHITE': 167}
In [46]: e_4.winner()
Out[46]: 'BLUE'
Upload your election.py file:
Weighted voting
Bonitans were not vary happy about the fact that after they have carefully ordered parties in their preferences (after long doubts and deliberations), the only thing that counted was which party was their first choice. They decided to try a different scheme: for each vote, the first party gets 5 points, the second party gets 4 points, …, the fifth party (if there is any) gets 1 point. The winner is the party with the maximal number of points. If there is a tie, then the winner is chosen to be the first one in the lexicographic ordering. For example, if GREEN and RED have the same number of points, GREEN wins because ‘G’ appears before ‘R’ in the alphabet.
Exercise 5: Elections with weights
Let’s define a new subclass of Election
s using weighted
voting. Copy the following class into your election.py
file:
class WeightedElection(Election):
"""
Weighted elections: each vote contributes to a party's total
according to that party's position among the vote's preferences.
"""
When working with weighted votes, the basic status
method from the Election
base class is not helpful. We will
override it with a new status
method
specific to WeightedElection
. Now define
and complete the new method in WeightedElection
:
def status(self):
"""Returns a dictionary with keys being the parties
and values being the number of points (counted using
the weighted scheme) that the party got.
"""
Now, define and complete a method to compute the
winner of a WeightedElection
:
def winner(self):
"""Return the winner of this weighted election."""
Hint: the standard comparison operator
<
applied to a pair of strings will compare them
lexicographically. In particular, the sorted
function
applied to a list of strings, will sort them lexicographically.
Test your methods. You should be able to reproduce the following behaviour in the console exactly. First, if there are no votes, the winner will be the lexicographically first party, the BLUEs:
In [47]: e_1 = WeightedElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [48]: e_1.status()
Out[48]: {'BLUE': 0, 'GREEN': 0, 'PURPLE': 0, 'RED': 0, 'WHITE': 0}
In [49]: e_1.winner()
Out[49]: 'BLUE'
Next, we can find winners for the elections we have under this system:
In [50]: e_1.add_votes_from_file('votes-10.txt')
In [51]: e_1.status()
Out[51]: {'BLUE': 22, 'GREEN': 13, 'PURPLE': 14, 'RED': 24, 'WHITE': 25}
In [52]: e_1.winner()
Out[52]: 'WHITE'
Now we can try some elections with more votes:
In [53]: e_2 = WeightedElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [54]: e_2.add_votes_from_file('votes-100.txt')
In [55]: e_2.status()
Out[55]: {'BLUE': 209, 'GREEN': 138, 'PURPLE': 176, 'RED': 192, 'WHITE': 177}
In [56]: e_2.winner()
Out[56]: 'BLUE'
Upload your election.py file:
Preferential (“instant-runoff”) voting
In each of the first-past-the-post elections above, the winning party won the election despite having only a small percentage of the popular vote. (In the final example above, the BLUEs won with less than 18% of the vote!) The Bonitans are therefore moving to a system based on preferential “instant-runoff” voting, similar to the way Australian parliamentary elections work.
Preferential voting works as follows. As usual, each Bonitan’s vote
contains a list of their preferred parties, in descending order of
preference. For example, ['GREEN', 'RED', 'PURPLE']
means
that:
- this voter wants GREEN to win;
- but if they couldn’t have GREEN, then their second choice would be RED;
- and if they couldn’t have GREEN or RED, then they would prefer PURPLE.
- If they couldn’t have GREEN, RED, or PURPLE, then they have no other preference; they wouldn’t give their vote to WHITE or BLUE.
As usual, each party starts with an empty pile of votes, and there is
a special pile for the blank votes; there is also a new pile,
dead
(the use of this will become clear shortly). Then we
procede with a series of rounds:
In the first round, each vote is distributed to the pile belonging to the party of its first preference, much as we would do for a first-past-the-post election. At this point we pause and report the first-round totals for each party.
Now, in the next round, the party with the fewest
votes in its pile is eliminated. But the votes in its pile are
not wasted: each vote in its pile is redistributed to
another pile, according to its next preference. If it has no remaining
preferences, then the vote is moved to the dead
pile. The
pile of the eliminated party is then removed.
We then repeat the redistribution procedure, eliminating one party at a time and redistributing its votes over the remaining piles, until there is only one pile left; that party is declared the winner.
(You might wonder why we keep the dead votes, rather than simply throwing them away. This is for accountability’s sake: no votes should ever be destroyed, in case there is a legal challenge and we need to re-count the votes.)
In the unlikely event that more than one party has the same minimal number of votes, then the party to be eliminated is decided somewhat arbitrarily: in Isla Bonita, they eliminate the first such pile they encounter. In practice, when large numbers of votes are involved, this rule is almost never invoked (and we will not test your code with any elections with ties in rounds).
An example election
For example, suppose we start with these ten preference lists:
['BLUE', 'WHITE', 'PURPLE', 'GREEN', 'RED']
,['GREEN', 'RED']
,['BLUE', 'PURPLE']
,['RED']
,[]
(a blank vote),['RED', 'GREEN']
,['BLUE', 'WHITE', 'PURPLE']
,['WHITE', 'PURPLE']
,['WHITE', 'GREEN', 'RED']
,[]
(a blank vote).
After the first round, we have:
- RED pile: votes 4 and 6
- BLUE pile: votes 1, 3, and 7
- WHITE pile: votes 8 and 9
- GREEN pile: vote 2
- PURPLE pile: no votes
- dead pile: no votes
- blank pile: votes 5 and 10
For the second round, the PURPLE party has the fewest votes: none! So we eliminate it, which doesn’t change much:
- RED pile: votes 4 and 6
- BLUE pile: votes 1, 3, and 7
- WHITE pile: votes 8 and 9
- GREEN pile: vote 2
- dead pile: no votes
- blank pile: votes 5 and 10
For the third round, GREEN has the fewest votes, with one (vote 2), so we eliminate it, redistributing vote 2 to its next preference, which is the RED party. Now we have
- RED pile: votes 2, 4, and 6
- BLUE pile: votes 1, 3, and 7
- WHITE pile: votes 8 and 9
- dead pile: no votes
- blank pile: votes 5 and 10
For the fourth round, WHITE has the fewest votes, with two (votes 8 and 9). Vote 8 is now dead (it has PURPLE as its next preference, but PURPLE has already been eliminated, and it had no other preferences), so we move it to the dead pile. Vote 9 has GREEN as its second preference, but GREEN has also been eliminated; but its third preference was RED, which is still in the race, so we move vote 9 to the RED pile. Now we have
- RED pile: votes 2, 4, 6, and 9
- BLUE pile: votes 1, 3, and 7
- dead pile: vote 8
- blank pile: votes 5 and 10
It is now clear that RED is going to win, but to keep things simple, we still run the fifth round. Votes 3 and 7 both move to the dead pile, while vote 1 moves to RED. We now have
- RED pile: votes 1, 2, 4, 6, and 9
- dead pile: votes 3, 7, 8
- blank pile: votes 5 and 10
The RED party have won the election.
Exercise 6: Expressing a preference
We need to be able to quickly decide which pile to put a vote into. This was simple in first-past-the-post voting, because only the first preferences mattered, and no parties were ever eliminated; but now we need to be more subtle.
Add a new method preference
to the
Vote
class (not the Election
class,
or any of its subclasses!). Given a list or set of party names,
preference
will return the first of these names to appear
in self.preference_list
, or the special value
None
if none of these names appear in the list. Your
preference
method should begin as follows:
def preference(self, names):
"""Return the item in names that occurs first in the preference list,
or None if no item in names appears.
"""
pass # remove this line and complete the method
Test your function as follows.
First, let’s check that when we offer parties that are not included
in the preference_list
, we get None
:
In [57]: v_1 = Vote([])
In [58]: v_1.preference({'BLUE', 'RED'}) is None
Out[58] True
Next, let’s set up a more interesting vote, and try for preferences beyond the first:
In [59]: v_2 = Vote(['BLUE', 'RED', 'PURPLE'])
In [60]: v_2.preference({'GREEN', 'RED', 'PURPLE'})
Out[60]: 'RED'
In [61]: v_2.preference({'PURPLE', 'GREEN'})
Out[61]: 'PURPLE'
In [62]: v_2.preference({'GREEN'}) is None
Out[62]: True
You are now finished with the Vote
class; we will
not modify it any further.
Upload your election.py file:
Exercise 7: Eliminating and redistributing
The first round of a preferential election is already carried out by
the add_votes
method of the Election
object.
Now we need to implement the following rounds of the election, so that
we can converge towards a winner. Recall the main algorithm for moving
from one round to the next: once the party to be eliminated has been
determined, then for each vote in its pile, we try to find its next
preference among the remaining parties, and if there is no preference
then we move the vote to the dead
pile.
The first step is to define a new subclass
PreferentialElection
. These elections need a new pile for
“dead” votes, so we need to extend the __init__
method from
Election
to add this new data attribute:
class PreferentialElection(Election):
"""
Simple preferential/instant-runoff elections.
"""
def __init__(self, parties):
super().__init__(parties) # Initialize as for Elections
self.dead = []
Now define and complete a new method
eliminate
in your PreferentialElection
class:
def eliminate(self, party):
"""Remove the given party from piles, and redistribute its
votes among the parties not yet eliminated, according to
their preferences. If all preferences have been eliminated,
then add the vote to the dead list.
"""
pass # remove this line and complete the method
Test your eliminate
method. Continuing
the example above, we can use eliminate
to run an election
by hand:
In [63]: e_1 = PreferentialElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [64]: e_1.add_votes_from_file('votes-10.txt')
In [65]: e_1.status()
Out[65]: {'BLUE': 2, 'GREEN': 1, 'PURPLE': 0, 'RED': 4, 'WHITE': 3}
In [66]: e_1.eliminate('PURPLE') # Eliminate the smallest pile
In [67]: e_1.status()
Out[67]: {'BLUE': 2, 'GREEN': 1, 'RED': 4, 'WHITE': 3}
In [68]: e_1.eliminate('GREEN') # Eliminate new smallest pile
In [69]: e_1.status()
Out[69]: {'BLUE': 2, 'RED': 5, 'WHITE': 3}
In [70]: e_1.eliminate('BLUE') # Eliminate new smallest pile
In [71]: e_1.status()
Out[71]: {'RED': 5, 'WHITE': 4}
In [72]: e_1.dead # We have gone from 10 live to 9 live votes
Out[72]: [Vote(['BLUE'])]
In [73]: e_1.eliminate('WHITE') # Eliminate new smallest pile
In [74]: e_1.status() # We have a winner
Out[74]: {'RED': 5}
In [75]: e_1.dead # Inspect the dead votes
Out[75]:
[Vote(['BLUE']),
Vote(['WHITE', 'BLUE']),
Vote(['WHITE', 'BLUE', 'PURPLE']),
Vote(['WHITE', 'PURPLE']),
Vote(['BLUE', 'PURPLE', 'WHITE'])]
Upload your election.py file:
Exercise 8: The winner is…
As you would have noticed while testing the previous exercise, running an election by hand is tedious. To automatise the election, we need a method to determine which party to eliminate, and then a method that loops until the winning party has been found.
Define and complete the method
round_loser
in your PreferentialElection
class:
def round_loser(self):
"""Return the name of the party to be eliminated from the next round."""
Your round_loser
method should return the name of the
party in piles
with the smallest number of votes in its
pile. Remember, Bonitans are somewhat naive: if there is more than one
party with the same minimal number of votes, then they just choose the
one to be eliminated arbitrarily: you don’t have to implement any sort
of tie-breaking algorithm here.
Test your round_loser
method:
In [76]: e_1 = PreferentialElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [77]: e_1.add_votes_from_file('votes-10.txt')
In [78]: e_1.status()
Out[78]: {'BLUE': 2, 'GREEN': 1, 'PURPLE': 0, 'RED': 4, 'WHITE': 3}
In [79]: e_1.round_loser()
Out[79]: 'PURPLE'
In [80]: e_1.eliminate('PURPLE') # Eliminate the smallest pile
In [81]: e_1.status()
Out[81]: {'BLUE': 2, 'GREEN': 1, 'RED': 4, 'WHITE': 3}
In [82]: e_1.round_loser()
Out[82]: 'GREEN'
In [83]: e_1.eliminate('GREEN') # Eliminate new smallest pile
In [84]: e_1.status()
Out[84]: {'BLUE': 2, 'RED': 5, 'WHITE': 3}
Now we are ready to define and complete what we
really want: a winner
method in
PreferentialElection
.
def winner(self):
"""Run a preferential election based on the current piles of votes,
and return the winning party.
"""
Your winner
method should repeatedly choose a round
loser and eliminate that party, until there is only one party left; and
then it should return that party.
Test your winner
method:
In [85]: e_1 = PreferentialElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [86]: e_1.add_votes_from_file('votes-10.txt')
In [87]: e_1.winner()
Out[87]: 'RED'
Now let’s test it on a slightly bigger election.
In [88]: e_2 = PreferentialElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [89]: e_2.add_votes_from_file('votes-100.txt')
In [90]: e_2.status()
Out[90]: {'BLUE': 22, 'GREEN': 9, 'PURPLE': 15, 'RED': 26, 'WHITE': 16}
In [91]: e_2.winner()
Out[91]: 'BLUE'
Notice that this time, RED had the most first-preference votes, but BLUE ended up overtaking them once all the preferences had been distributed.
Now let’s try another, bigger, election:
In [92]: e_3 = PreferentialElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [93]: e_3.add_votes_from_file('votes-1000.txt')
In [94]: e_3.status()
Out[94]: {'BLUE': 176, 'GREEN': 167, 'PURPLE': 151, 'RED': 150, 'WHITE': 167}
In [95]: e_3.winner()
Out[95]: 'BLUE'
This time BLUE started out ahead, and they stayed ahead.
Upload your election.py file:
Exercise 9: Constitutional crisis
As we noted above, Bonitans are somewhat naive: if there is more than one party with the same minimal number of votes, then they just choose one arbitrarily. Historically, this has never been a problem: at worst, it has been a matter of deciding which of two microparties with no votes should be eliminated first, and this has no outcome on the final result.
But the 2017 election was a complete fiasco: the last two parties remaining had exactly the same number of votes, which meant that the arbitrary choice of elimination meant an arbitrary choice of government. To make sure this never happens again, they have a new rule to resolve the ambiguity: in the event of a tied minimal vote count, the party to be eliminated shall be the party with the fewest first-preference votes in its pile. If this does not break the tie, then the party whose name appears first in lexicographical (alphabetical) order will be eliminated.
Modify your round_loser
method to take
this into account. This should be all that is required to make your
PreferentialElection
’s winner
method compliant
with the updated rules.
To test your code, try simulating the election with
votes in the file votes-con.txt
.
In [96]: e_c = PreferentialElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [97]: e_c.add_votes_from_file('votes-con.txt')
In [98]: e_c.status()
Out[98]: {'BLUE': 6, 'GREEN': 3, 'PURPLE': 3, 'RED': 5, 'WHITE': 3}
At this point, the GREENs, PURPLEs, and WHITEs are tied for last. Since this is the first round, they all have the same number of first-preference votes; so we should eliminate the first in alphabetical order, which is the GREENs.
In [99]: e_c.round_loser()
Out[99]: 'GREEN'
In [100]: e_c.eliminate('GREEN')
In [101]: e_c.status()
Out[101]: {'BLUE': 6, 'PURPLE': 4, 'RED': 6, 'WHITE': 4}
Now the PURPLEs and WHITEs are tied for last (again). Both had the same number of first-preference votes; so again, we should eliminate the first in alphabetical order, which is the PURPLEs.
In [102]: e_c.round_loser()
Out[102]: 'PURPLE'
In [103]: e_c.eliminate('PURPLE')
In [104]: e_c.status()
Out[104]: {'BLUE': 6, 'RED': 8, 'WHITE': 6}
Now BLUE and WHITE are tied for last. But BLUE got 6 first-round votes, while WHITE only got 3; so we should be eliminating WHITE.
In [105]: e_c.round_loser()
Out[105]: 'WHITE'
In [106]: e_c.eliminate('WHITE')
In [107]: e_c.status()
Out[107]: {'BLUE': 8, 'RED': 10}
Clearly now RED wins.
In [108]: e_c.winner()
Out[108]: 'RED'
Upload your election.py file:
Two-round runoff voting
Consider a two-round voting system, similar to the French presidential elections. The idea is as follows:
- First, the votes are distributed into piles according to their first preference.
- Then, we keep the two parties with the highest vote totals; every other party is eliminated, and the votes in their piles are redistributed according to their preferences for the two leading parties.
Ties are possible on both steps. They should be resolved as follows:
- On the first step, the parties are sorted according to the following rule:
- first, they are sorted according to the number of first preferences;
- if some parties have the same number of first preferences, they are sorted according to the number of points they get in the weighted scheme (see Exercise 5);
- if some parties have the same number of first preferences and the same number of points according to the weighted scheme, they are sorted lexicographically.
- On the second step, if two remaining parties have the same number of votes, they are compared by the first preferences, and then lexicographically.
For example, suppose we start with these nine preference lists:
['BLUE']
,['BLUE', 'RED']
,['BLUE', 'RED']
,['GREEN', 'WHITE']
,['GREEN', 'WHITE']
,['WHITE', 'GREEN']
,['WHITE', 'GREEN']
,[]
(a blank vote),['RED', 'GREEN', 'WHITE']
.
Then we will have the following numbers of the first preferences: 3 for BLUE, 2 for GREEN, 0 for PURPLE, 1 for RED, and 2 for WHITE. In order to determine the relative ranking of GREEN and WHITE, we should count the points according to the weighted scheme for them: 22 for GREEN and 21 for WHITE.
Overall, we have the following ranking after the end of the first round:
- BLUE
- GREEN
- WHITE
- RED
- PURPLE
Thus, BLUE and GREEN are the remaining parties for the second round. In the second round, we count preferences for them and get: 3 for BLUE and 5 for GREEN, so GREEN is the winner.
Optional Exercise 10: Two-round runoff voting
Implement the two-round runoff system described above.
Define a new subclass TwoRoundElection
extending the Election
class, and then add a new
method winner
to your
TwoRoundElection
class returning the winner.
Hint: the sorted
function applied to
tuples works as follows: it first sorts according to the first elements
in the tuples, then the elements with the same first element are sorted
according to the second element, etc. For example:
In [109]: a = [(4, 3), (5, 1), (4, 2), (1, 10)]
In [110]: sorted(a)
Out[110]: [(1, 10), (4, 2), (4, 3), (5, 1)]
This can be very convenient to implement the ranking of the parties described above.
To test your code, try simulating the election with
votes in the files votes-10.txt
and
votes-100.txt
.
In [111]: e = TwoRoundElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [112]: e.add_votes_from_file('votes-10.txt')
In [113]: e.winner()
Out[113]: 'RED'
In [114]: e = TwoRoundElection(['BLUE', 'GREEN', 'PURPLE', 'RED', 'WHITE'])
In [115]: e.add_votes_from_file('votes-100.txt')
In [116]: e.winner()
Out[116]: 'BLUE'
Upload your election.py file:
Optional Exercise 11: What next?
There are almost as many voting systems in the world as there are democratic governments. This is perhaps natural: Arrow’s impossibility theorem tells us that there cannot exist a perfect voting system (that respects ordered preferences).
Look at the Wikipedia page on electoral systems, and try implementing an election system from your own country. If yours has already been covered by the exercises above, then you might enjoy trying to implement a single transferable vote system, as is used in New Zealand (for example). Upload your election.py file: