Elections

Elections are the systems or algorithms by which a PreferenceProfile, or collection of ballots, is converted into an outcome. There are infinitely many different possible election methods, whether the output is a single winner, a set of winners, or a consensus ranking. VoteKit has a host of built-in election methods, as well as the functionality to let you create your own system of election. By the end of this section, you will have been introduced to the STV and Borda elections, learned about the Election object, and created your own election type.

STV

To start, let’s return to the Minneapolis 2013 mayoral race. We first saw this in previous notebooks. As a reminder, this election had 35 named candidates running for one seat, and used an IRV election system (which is mathematically equivalent to a single-winner STV election) to choose the winner. Voters were only allowed to rank their top three candidates.

Let’s load in the cast vote record (CVR) from the election, and do all of the same cleaning we did before.

from votekit.cvr_loaders import load_csv
from votekit.elections import STV
from votekit.cleaning import remove_and_condense

minneapolis_profile = load_csv("mn_2013_cast_vote_record.csv")
minneapolis_profile = remove_and_condense(["undervote", "overvote", "UWI"], minneapolis_profile)

# m = 1 means 1 seat
minn_election = STV(profile=minneapolis_profile, m=1)
print(minn_election)
                                  Status  Round
BETSY HODGES                     Elected     35
MARK ANDREW                   Eliminated     34
DON SAMUELS                   Eliminated     33
CAM WINTON                    Eliminated     32
JACKIE CHERRYHOMES            Eliminated     31
BOB FINE                      Eliminated     30
DAN COHEN                     Eliminated     29
STEPHANIE WOODRUFF            Eliminated     28
MARK V ANDERSON               Eliminated     27
DOUG MANN                     Eliminated     26
OLE SAVIOR                    Eliminated     25
JAMES EVERETT                 Eliminated     24
ALICIA K. BENNETT             Eliminated     23
ABDUL M RAHAMAN "THE ROCK"    Eliminated     22
CAPTAIN JACK SPARROW          Eliminated     21
CHRISTOPHER CLARK             Eliminated     20
TONY LANE                     Eliminated     19
JAYMIE KELLY                  Eliminated     18
MIKE GOULD                    Eliminated     17
KURTIS W. HANNA               Eliminated     16
CHRISTOPHER ROBIN ZIMMERMAN   Eliminated     15
JEFFREY ALAN WAGNER           Eliminated     14
NEAL BAXTER                   Eliminated     13
TROY BENJEGERDES              Eliminated     12
GREGG A. IVERSON              Eliminated     11
MERRILL ANDERSON              Eliminated     10
JOSHUA REA                    Eliminated      9
BILL KAHN                     Eliminated      8
JOHN LESLIE HARTWIG           Eliminated      7
EDMUND BERNARD BRUYERE        Eliminated      6
JAMES "JIMMY" L. STROUD, JR.  Eliminated      5
RAHN V. WORKCUFF              Eliminated      4
BOB "AGAIN" CARNEY JR         Eliminated      3
CYD GORMAN                    Eliminated      2
JOHN CHARLES WILSON           Eliminated      1
/Users/cdonnay/Documents/GitHub/MGGG/VoteKit/src/votekit/pref_profile/pref_profile.py:1109: UserWarning: Profile does not contain rankings but max_ranking_length=3. Setting max_ranking_length to 0.
  warnings.warn(

First, what is this showing? Generally, the winners are listed in the order they were elected, from the top down. Eliminated candidates are filled in in the order they were eliminated, bottom-up. If any candidates are still remaining without having been designated elected or eliminated, they are in a middle category called Remaining. Ties are broken by strength, meaning for instance that if 3 candidates are remaining at the end, they are listed in the order of their first-place in the final election state. This means that this output can be thought of as an aggregate ranking vector produced by applying the election method to the voters’ ranking vectors.

So what exactly is happening in this STV election? STV stands for “single transferable vote.” Voters cast ranked choice ballots. A threshold is set of how much support is required for election; if a candidate crosses the threshold, they are designated as a winner. The threshold in VoteKit defaults to something called the Droop quota. If there are \(N\) voters and \(m\) seats, then Droop quota is computed as \(T=\lfloor N/(m+1)\rfloor +1\). Another option is the Hare quota, which is just \(T=N/m\), which is a little bit larger. Generally, all that is needed of a threshold is that it can’t be the case that \(m+1\) candidates exceed it.

In the first round, the first-place votes for each candidate are tallied. If candidate \(A\) crosses the threshold, they are elected. If there were surplus votes, then the ballots with \(A\) in first place are transfered, with appropriately reduced weight, to the next choice of those voters. If another candidate receives enough transfered support to cross the threshold, they are elected. If no candidate does, the candidate with the fewest first-place votes is removed from all ballots, and their votes are transfered with full weight. This repeats until all seats are filled.

Let’s work out a small example where it is easier to see how STV works. We will use a fractional transfer rule. If the threshold is \(T\) and a candidate received \(rT\) votes in a given round, where \(r>1\), then the excess is \((r-1)T\) and so ballots are now “discounted” to have new weight \((r-1)/r\). For instance if the candidate received 150 votes but only needed 100, there would be 50 “excess” votes. Instead of randomly picking 50 out of 150 ballots to transfer, we transfer them all with a reduced weight of 50/150, or 1/3. Here is a link to a more substantial explainer about ranked choice.

In our example, suppose there are \(N=23\) voters and \(n=7\) candidates running for \(m=3\) seats with the following profile.

from votekit.ballot import Ballot
from votekit.pref_profile import PreferenceProfile

candidates = ["A", "B", "C", "D", "E", "F", "G"]

ballots = [
    Ballot(ranking=[{"A"}, {"B"}], weight=3),
    Ballot(ranking=[{"B"}, {"C"}, {"D"}], weight=8),
    Ballot(ranking=[{"C"}, {"A"}, {"B"}], weight=1),
    Ballot(ranking=[{"D"}, {"E"}], weight=3),
    Ballot(ranking=[{"E"}, {"D"}, {"F"}], weight=1),
    Ballot(ranking=[{"F"}, {"G"}], weight=4),
    Ballot(ranking=[{"G"}, {"E"}, {"F"}], weight=3),
]

profile = PreferenceProfile(ballots=ballots)

print(profile.df)
print("Sum of ballot weights:", profile.total_ballot_wt)
print("Number of candidates:", len(profile.candidates))
print()
election = STV(profile=profile, m=3)

print("Threshold:", election.threshold)
print("Number of rounds", len(election))
print(election)
             Ranking_1 Ranking_2 Ranking_3 Voter Set  Weight
Ballot Index
0                  (A)       (B)       (~)        {}     3.0
1                  (B)       (C)       (D)        {}     8.0
2                  (C)       (A)       (B)        {}     1.0
3                  (D)       (E)       (~)        {}     3.0
4                  (E)       (D)       (F)        {}     1.0
5                  (F)       (G)       (~)        {}     4.0
6                  (G)       (E)       (F)        {}     3.0
Sum of ballot weights: 23.0
Number of candidates: 7

Initial tiebreak was unsuccessful, performing random tiebreak
Threshold: 6
Number of rounds 6
       Status  Round
B     Elected      1
D     Elected      4
F     Elected      6
A   Remaining      6
G  Eliminated      5
C  Eliminated      3
E  Eliminated      2

What this code block did is create an Election object that lets us access all the information, round-by-round, about what would happen under the designated election method. The message about a tiebreak indicates that in some round, a random tiebreak was needed.

We can review it step-by-step instead of all at once. Just from a brief glance at the profile and threshold, we see that candidate B should be elected in the first round. Let’s see this happen in two ways.

First, observe the first-place votes for each candidate. These are stored in the round 0 ElectionState object, which can be accessed as follows.

election.election_states[0].scores
{'A': 3.0, 'B': 8.0, 'C': 1.0, 'D': 3.0, 'E': 1.0, 'F': 4.0, 'G': 3.0}

We can see from this that only B is over the threshold. The other way we can see who wins in the first round is by looking at the next ElectionState.

print("elected", election.election_states[1].elected)
print("\neliminated", election.election_states[1].eliminated)
print("\nremaining", election.election_states[1].remaining)
elected (frozenset({'B'}),)

eliminated (frozenset(),)

remaining (frozenset({'F'}), frozenset({'C', 'G', 'A', 'D'}), frozenset({'E'}))

\(B\) passed the threshold by 2 votes with a total of 8, so the \(B,C,D\) ballot is going to have \(B\) removed and be given weight \(2/8\) (excess/total) times its previous weight of 8. To check this, election objects have a method called get_profile() that returns the PreferenceProfile after a particular round.

election.get_profile(1).df
Ranking_1 Ranking_2 Ranking_3 Voter Set Weight
Ballot Index
0 (C) (D) (~) {} 2.0
1 (G) (E) (F) {} 3.0
2 (C) (A) (~) {} 1.0
3 (A) (~) (~) {} 3.0
4 (F) (G) (~) {} 4.0
5 (D) (E) (~) {} 3.0
6 (E) (D) (F) {} 1.0

Look, \(B\) is now removed from all ballots, and the \(B,C,D\) ballot became \(C,D\) with weight 2. No one has enough votes to cross the 6 threshold, so the candidate with the least support will be eliminated—that is candidate \(E\), with only one first-place vote.

We also introduce the get_step() method which accesses the profile and state of a given round.

print("fpv after round 1:", election.election_states[1].scores)
print("go to the next step\n")

profile, state = election.get_step(2)
print("elected", state.elected)
print("\neliminated", state.eliminated)
print("\nremaining", state.remaining)
print(profile.df)
fpv after round 1: {'C': 3.0, 'D': 3.0, 'G': 3.0, 'E': 1.0, 'F': 4.0, 'A': 3.0}
go to the next step

elected (frozenset(),)

eliminated (frozenset({'E'}),)

remaining (frozenset({'F', 'D'}), frozenset({'C', 'G', 'A'}))
             Ranking_1 Ranking_2 Ranking_3 Voter Set  Weight
Ballot Index
0                  (C)       (D)       (~)        {}     2.0
1                  (G)       (F)       (~)        {}     3.0
2                  (C)       (A)       (~)        {}     1.0
3                  (A)       (~)       (~)        {}     3.0
4                  (F)       (G)       (~)        {}     4.0
5                  (D)       (~)       (~)        {}     3.0
6                  (D)       (F)       (~)        {}     1.0

\(E\) has been removed from all of the ballots. Again, no one crosses the threshold so the candidate with the fewest first-place votes will be eliminated.

print("fpv after round 2:", election.election_states[2].scores)
print("go to the next step\n")


print("elected", election.election_states[3].elected)
print("\neliminated", election.election_states[3].eliminated)
print("\nremaining", election.election_states[3].remaining)
print("\ntiebreak resolution", election.election_states[3].tiebreaks)
print()
print(election.get_profile(3).df)
fpv after round 2: {'C': 3.0, 'G': 3.0, 'A': 3.0, 'F': 4.0, 'D': 4.0}
go to the next step

elected (frozenset(),)

eliminated (frozenset({'C'}),)

remaining (frozenset({'D'}), frozenset({'F', 'A'}), frozenset({'G'}))

tiebreak resolution {frozenset({'C', 'G', 'A'}): (frozenset({'A'}), frozenset({'G'}), frozenset({'C'}))}

Initial tiebreak was unsuccessful, performing random tiebreak
             Ranking_1 Ranking_2 Ranking_3 Voter Set  Weight
Ballot Index
0                  (D)       (~)       (~)        {}     2.0
1                  (G)       (F)       (~)        {}     3.0
2                  (A)       (~)       (~)        {}     1.0
3                  (A)       (~)       (~)        {}     3.0
4                  (F)       (G)       (~)        {}     4.0
5                  (D)       (~)       (~)        {}     3.0
6                  (D)       (F)       (~)        {}     1.0

Note that here, several candidates were tied for the fewest first-place votes at this stage. When this happens in STV, you use the first-place votes from the original profile to break ties. This means C will be eliminated. The tiebreaks parameter records the resolution of the tie; since we are looking for the person with the least first-place votes, the candidate in the final entry of the tuple is eliminated. The reason the message “Initial tiebreak was unsuccessful, performing random tiebreak” appeared is that A and G were tied by first-place votes, and thus a random tiebreak was needed to separate them. This didn’t affect the outcome, since C had the fewest first-place votes.

Try it yourself

Keep printing the first-place votes and running a step of the election until all seats have been filled. At each step, think through why the election state transitioned as it did.

We now change the transfer type. Using the same profile as above, we’ll now use random_transfer. In the default fractional transfer, we reweighted all of the ballots in proportion to the surplus. Here, we will randomly choose the appropriate number of ballots to transfer (the same number as the surplus). Though it sounds strange, this is the method actually used in Cambridge, MA. (Recall that Cambridge has used STV continuously since 1941 so back in the day they probably needed a low-tech physical way to do the transfers.)

from votekit.elections import random_transfer

candidates = ["A", "B", "C", "D", "E", "F", "G"]

ballots = [
    Ballot(ranking=[{"A"}, {"B"}], weight=3),
    Ballot(ranking=[{"B"}, {"C"}, {"D"}], weight=8),
    Ballot(ranking=[{"B"}, {"D"}, {"C"}], weight=8),
    Ballot(ranking=[{"C"}, {"A"}, {"B"}], weight=1),
    Ballot(ranking=[{"D"}, {"E"}], weight=1),
    Ballot(ranking=[{"E"}, {"D"}, {"F"}], weight=1),
    Ballot(ranking=[{"F"}, {"G"}], weight=4),
    Ballot(ranking=[{"G"}, {"E"}, {"F"}], weight=1),
]

profile = PreferenceProfile(ballots=ballots)

print(profile.df)
print("Sum of ballot weights:", profile.total_ballot_wt)
print("Number of candidates:", len(profile.candidates))
print()

election = STV(profile=profile, transfer=random_transfer, m=2)

print(election)
             Ranking_1 Ranking_2 Ranking_3 Voter Set  Weight
Ballot Index
0                  (A)       (B)       (~)        {}     3.0
1                  (B)       (C)       (D)        {}     8.0
2                  (B)       (D)       (C)        {}     8.0
3                  (C)       (A)       (B)        {}     1.0
4                  (D)       (E)       (~)        {}     1.0
5                  (E)       (D)       (F)        {}     1.0
6                  (F)       (G)       (~)        {}     4.0
7                  (G)       (E)       (F)        {}     1.0
Sum of ballot weights: 27.0
Number of candidates: 7

Initial tiebreak was unsuccessful, performing random tiebreak
       Status  Round
B     Elected      1
D     Elected      7
F  Eliminated      6
A  Eliminated      5
C  Eliminated      4
G  Eliminated      3
E  Eliminated      2

Try it yourself

Rerun the code above until you see that different candidates can win under random transfer.

Election

Let’s poke around the Election class a bit more. It contains a lot of useful information about what is happening in an election. We will also introduce the Borda election.

Borda Election

In a Borda election, ranked ballots are converted to a score for a candidate, and then the candidates with the highest scores win. The traditional score vector is \((n,n-1,\dots,1)\): that is, if there are \(n\) candidates, the first-place candidate on a ballot is given \(n\) points, the second place \(n-1\), all the way down to last, who is given \(1\) point. You can change the score vector using the score_vector parameter.

from votekit.elections import Borda
import votekit.ballot_generator as bg

candidates = ["A", "B", "C", "D", "E", "F"]

# recall IAC generates an "all bets are off" profile
iac = bg.ImpartialAnonymousCulture(candidates=candidates)
profile = iac.generate_profile(number_of_ballots=1000)

election = Borda(profile, m=3)
print(election.get_profile(0).df.head(10).to_string())
print()

print(election)
             Ranking_1 Ranking_2 Ranking_3 Ranking_4 Ranking_5 Ranking_6 Voter Set  Weight
Ballot Index
0                  (D)       (A)       (F)       (B)       (C)       (E)        {}     1.0
1                  (A)       (E)       (F)       (B)       (D)       (C)        {}     5.0
2                  (E)       (C)       (A)       (D)       (B)       (F)        {}     2.0
3                  (E)       (C)       (A)       (B)       (F)       (D)        {}     4.0
4                  (C)       (D)       (B)       (F)       (A)       (E)        {}     2.0
5                  (C)       (A)       (B)       (E)       (F)       (D)        {}     6.0
6                  (D)       (F)       (A)       (E)       (B)       (C)        {}     4.0
7                  (B)       (A)       (E)       (C)       (D)       (F)        {}     2.0
8                  (E)       (A)       (F)       (C)       (D)       (B)        {}     2.0
9                  (F)       (D)       (B)       (E)       (A)       (C)        {}     5.0

      Status  Round
D    Elected      1
E    Elected      1
F    Elected      1
C  Remaining      1
A  Remaining      1
B  Remaining      1

The Borda election is one-shot (like plurality), so running a step or the election is equivalent. Let’s see what the election stores.

# the winners up to the given round, -1 means final round
print("Winners:", election.get_elected(-1))

# the eliminated candidates up to the given round
print("Eliminated:", election.get_eliminated(-1))

# the ranking of the candidates up to the given round
print("Ranking:", election.get_ranking(-1))

# the outcome of the given round
print("Outcome of round 1:\n", election.get_status_df(1))
Winners: (frozenset({'F'}), frozenset({'A'}), frozenset({'C'}))
Eliminated: ()
Ranking: (frozenset({'F'}), frozenset({'A'}), frozenset({'C'}), frozenset({'B'}), frozenset({'D'}), frozenset({'E'}))
Outcome of round 1:
       Status  Round
F    Elected      1
A    Elected      1
C    Elected      1
B  Remaining      1
D  Remaining      1
E  Remaining      1

Try it yourself

Using the following preference profile, try changing the score vector of a Borda election. Try replacing 3,2,1 with other Borda weights (decreasing and non-negative) showing that each candidate can be elected.

ballots = [
    Ballot(ranking=[{"A"}, {"B"}, {"C"}], weight=3),
    Ballot(ranking=[{"A"}, {"C"}, {"B"}], weight=2),
    Ballot(ranking=[{"B"}, {"C"}, {"A"}], weight=2),
    Ballot(ranking=[{"C"}, {"B"}, {"A"}], weight=4),
]

profile = PreferenceProfile(ballots=ballots, candidates=["A", "B", "C"])

# borda election
score_vector = [3, 2, 1]
election = Borda(profile, m=1, score_vector=score_vector)
print(election)
      Status  Round
C    Elected      1
B  Remaining      1
A  Remaining      1

Since a Borda election is a one-shot election, most of the information stored in the Election is extraneous, but you can see its utility in an STV election where there are many rounds.

minneapolis_profile = load_csv("mn_2013_cast_vote_record.csv")
minneapolis_profile = remove_and_condense(["undervote", "overvote", "UWI"], minneapolis_profile)

minn_election = STV(profile=minneapolis_profile, m=1)

for i in range(1, 6):
    print(f"Round {i}\n")
    # the winners up to the current round
    print("Winners:", minn_election.get_elected(i))

    # the eliminated candidates up to the current round
    print("Eliminated:", minn_election.get_eliminated(i))

    # the remaining candidates, sorted by first-place votes
    print("Remaining:", minn_election.get_remaining(i))

    # the same information as a df
    print(minn_election.get_status_df(i))

    print()
Round 1

Winners: ()
Eliminated: (frozenset({'JOHN CHARLES WILSON'}),)
Remaining: (frozenset({'BETSY HODGES'}), frozenset({'MARK ANDREW'}), frozenset({'DON SAMUELS'}), frozenset({'CAM WINTON'}), frozenset({'JACKIE CHERRYHOMES'}), frozenset({'BOB FINE'}), frozenset({'DAN COHEN'}), frozenset({'STEPHANIE WOODRUFF'}), frozenset({'MARK V ANDERSON'}), frozenset({'DOUG MANN'}), frozenset({'OLE SAVIOR'}), frozenset({'ABDUL M RAHAMAN "THE ROCK"'}), frozenset({'ALICIA K. BENNETT'}), frozenset({'JAMES EVERETT'}), frozenset({'CAPTAIN JACK SPARROW'}), frozenset({'TONY LANE'}), frozenset({'MIKE GOULD'}), frozenset({'KURTIS W. HANNA'}), frozenset({'JAYMIE KELLY'}), frozenset({'CHRISTOPHER CLARK'}), frozenset({'CHRISTOPHER ROBIN ZIMMERMAN'}), frozenset({'JEFFREY ALAN WAGNER'}), frozenset({'TROY BENJEGERDES'}), frozenset({'GREGG A. IVERSON', 'NEAL BAXTER'}), frozenset({'JOSHUA REA'}), frozenset({'MERRILL ANDERSON'}), frozenset({'BILL KAHN'}), frozenset({'JOHN LESLIE HARTWIG'}), frozenset({'EDMUND BERNARD BRUYERE'}), frozenset({'JAMES "JIMMY" L. STROUD, JR.', 'RAHN V. WORKCUFF'}), frozenset({'BOB "AGAIN" CARNEY JR'}), frozenset({'CYD GORMAN'}))
                                  Status  Round
BETSY HODGES                   Remaining      1
MARK ANDREW                    Remaining      1
DON SAMUELS                    Remaining      1
CAM WINTON                     Remaining      1
JACKIE CHERRYHOMES             Remaining      1
BOB FINE                       Remaining      1
DAN COHEN                      Remaining      1
STEPHANIE WOODRUFF             Remaining      1
MARK V ANDERSON                Remaining      1
DOUG MANN                      Remaining      1
OLE SAVIOR                     Remaining      1
ABDUL M RAHAMAN "THE ROCK"     Remaining      1
ALICIA K. BENNETT              Remaining      1
JAMES EVERETT                  Remaining      1
CAPTAIN JACK SPARROW           Remaining      1
TONY LANE                      Remaining      1
MIKE GOULD                     Remaining      1
KURTIS W. HANNA                Remaining      1
JAYMIE KELLY                   Remaining      1
CHRISTOPHER CLARK              Remaining      1
CHRISTOPHER ROBIN ZIMMERMAN    Remaining      1
JEFFREY ALAN WAGNER            Remaining      1
TROY BENJEGERDES               Remaining      1
GREGG A. IVERSON               Remaining      1
NEAL BAXTER                    Remaining      1
JOSHUA REA                     Remaining      1
MERRILL ANDERSON               Remaining      1
BILL KAHN                      Remaining      1
JOHN LESLIE HARTWIG            Remaining      1
EDMUND BERNARD BRUYERE         Remaining      1
JAMES "JIMMY" L. STROUD, JR.   Remaining      1
RAHN V. WORKCUFF               Remaining      1
BOB "AGAIN" CARNEY JR          Remaining      1
CYD GORMAN                     Remaining      1
JOHN CHARLES WILSON           Eliminated      1

Round 2

Winners: ()
Eliminated: (frozenset({'CYD GORMAN'}), frozenset({'JOHN CHARLES WILSON'}))
Remaining: (frozenset({'BETSY HODGES'}), frozenset({'MARK ANDREW'}), frozenset({'DON SAMUELS'}), frozenset({'CAM WINTON'}), frozenset({'JACKIE CHERRYHOMES'}), frozenset({'BOB FINE'}), frozenset({'DAN COHEN'}), frozenset({'STEPHANIE WOODRUFF'}), frozenset({'MARK V ANDERSON'}), frozenset({'DOUG MANN'}), frozenset({'OLE SAVIOR'}), frozenset({'ABDUL M RAHAMAN "THE ROCK"'}), frozenset({'ALICIA K. BENNETT'}), frozenset({'JAMES EVERETT'}), frozenset({'CAPTAIN JACK SPARROW'}), frozenset({'TONY LANE'}), frozenset({'MIKE GOULD'}), frozenset({'KURTIS W. HANNA'}), frozenset({'JAYMIE KELLY'}), frozenset({'CHRISTOPHER CLARK'}), frozenset({'CHRISTOPHER ROBIN ZIMMERMAN'}), frozenset({'JEFFREY ALAN WAGNER'}), frozenset({'TROY BENJEGERDES'}), frozenset({'GREGG A. IVERSON'}), frozenset({'NEAL BAXTER'}), frozenset({'JOSHUA REA'}), frozenset({'MERRILL ANDERSON'}), frozenset({'BILL KAHN'}), frozenset({'JOHN LESLIE HARTWIG'}), frozenset({'EDMUND BERNARD BRUYERE'}), frozenset({'JAMES "JIMMY" L. STROUD, JR.', 'RAHN V. WORKCUFF'}), frozenset({'BOB "AGAIN" CARNEY JR'}))
                                  Status  Round
BETSY HODGES                   Remaining      2
MARK ANDREW                    Remaining      2
DON SAMUELS                    Remaining      2
CAM WINTON                     Remaining      2
JACKIE CHERRYHOMES             Remaining      2
BOB FINE                       Remaining      2
DAN COHEN                      Remaining      2
STEPHANIE WOODRUFF             Remaining      2
MARK V ANDERSON                Remaining      2
DOUG MANN                      Remaining      2
OLE SAVIOR                     Remaining      2
ABDUL M RAHAMAN "THE ROCK"     Remaining      2
ALICIA K. BENNETT              Remaining      2
JAMES EVERETT                  Remaining      2
CAPTAIN JACK SPARROW           Remaining      2
TONY LANE                      Remaining      2
MIKE GOULD                     Remaining      2
KURTIS W. HANNA                Remaining      2
JAYMIE KELLY                   Remaining      2
CHRISTOPHER CLARK              Remaining      2
CHRISTOPHER ROBIN ZIMMERMAN    Remaining      2
JEFFREY ALAN WAGNER            Remaining      2
TROY BENJEGERDES               Remaining      2
GREGG A. IVERSON               Remaining      2
NEAL BAXTER                    Remaining      2
JOSHUA REA                     Remaining      2
MERRILL ANDERSON               Remaining      2
BILL KAHN                      Remaining      2
JOHN LESLIE HARTWIG            Remaining      2
EDMUND BERNARD BRUYERE         Remaining      2
JAMES "JIMMY" L. STROUD, JR.   Remaining      2
RAHN V. WORKCUFF               Remaining      2
BOB "AGAIN" CARNEY JR          Remaining      2
CYD GORMAN                    Eliminated      2
JOHN CHARLES WILSON           Eliminated      1

Round 3

Winners: ()
Eliminated: (frozenset({'BOB "AGAIN" CARNEY JR'}), frozenset({'CYD GORMAN'}), frozenset({'JOHN CHARLES WILSON'}))
Remaining: (frozenset({'BETSY HODGES'}), frozenset({'MARK ANDREW'}), frozenset({'DON SAMUELS'}), frozenset({'CAM WINTON'}), frozenset({'JACKIE CHERRYHOMES'}), frozenset({'BOB FINE'}), frozenset({'DAN COHEN'}), frozenset({'STEPHANIE WOODRUFF'}), frozenset({'MARK V ANDERSON'}), frozenset({'DOUG MANN'}), frozenset({'OLE SAVIOR'}), frozenset({'ABDUL M RAHAMAN "THE ROCK"'}), frozenset({'ALICIA K. BENNETT'}), frozenset({'JAMES EVERETT'}), frozenset({'CAPTAIN JACK SPARROW'}), frozenset({'TONY LANE'}), frozenset({'MIKE GOULD'}), frozenset({'KURTIS W. HANNA'}), frozenset({'JAYMIE KELLY'}), frozenset({'CHRISTOPHER CLARK'}), frozenset({'CHRISTOPHER ROBIN ZIMMERMAN'}), frozenset({'JEFFREY ALAN WAGNER'}), frozenset({'TROY BENJEGERDES'}), frozenset({'GREGG A. IVERSON'}), frozenset({'NEAL BAXTER'}), frozenset({'JOSHUA REA', 'MERRILL ANDERSON'}), frozenset({'BILL KAHN'}), frozenset({'JOHN LESLIE HARTWIG'}), frozenset({'EDMUND BERNARD BRUYERE'}), frozenset({'JAMES "JIMMY" L. STROUD, JR.'}), frozenset({'RAHN V. WORKCUFF'}))
                                  Status  Round
BETSY HODGES                   Remaining      3
MARK ANDREW                    Remaining      3
DON SAMUELS                    Remaining      3
CAM WINTON                     Remaining      3
JACKIE CHERRYHOMES             Remaining      3
BOB FINE                       Remaining      3
DAN COHEN                      Remaining      3
STEPHANIE WOODRUFF             Remaining      3
MARK V ANDERSON                Remaining      3
DOUG MANN                      Remaining      3
OLE SAVIOR                     Remaining      3
ABDUL M RAHAMAN "THE ROCK"     Remaining      3
ALICIA K. BENNETT              Remaining      3
JAMES EVERETT                  Remaining      3
CAPTAIN JACK SPARROW           Remaining      3
TONY LANE                      Remaining      3
MIKE GOULD                     Remaining      3
KURTIS W. HANNA                Remaining      3
JAYMIE KELLY                   Remaining      3
CHRISTOPHER CLARK              Remaining      3
CHRISTOPHER ROBIN ZIMMERMAN    Remaining      3
JEFFREY ALAN WAGNER            Remaining      3
TROY BENJEGERDES               Remaining      3
GREGG A. IVERSON               Remaining      3
NEAL BAXTER                    Remaining      3
JOSHUA REA                     Remaining      3
MERRILL ANDERSON               Remaining      3
BILL KAHN                      Remaining      3
JOHN LESLIE HARTWIG            Remaining      3
EDMUND BERNARD BRUYERE         Remaining      3
JAMES "JIMMY" L. STROUD, JR.   Remaining      3
RAHN V. WORKCUFF               Remaining      3
BOB "AGAIN" CARNEY JR         Eliminated      3
CYD GORMAN                    Eliminated      2
JOHN CHARLES WILSON           Eliminated      1

Round 4

Winners: ()
Eliminated: (frozenset({'RAHN V. WORKCUFF'}), frozenset({'BOB "AGAIN" CARNEY JR'}), frozenset({'CYD GORMAN'}), frozenset({'JOHN CHARLES WILSON'}))
Remaining: (frozenset({'BETSY HODGES'}), frozenset({'MARK ANDREW'}), frozenset({'DON SAMUELS'}), frozenset({'CAM WINTON'}), frozenset({'JACKIE CHERRYHOMES'}), frozenset({'BOB FINE'}), frozenset({'DAN COHEN'}), frozenset({'STEPHANIE WOODRUFF'}), frozenset({'MARK V ANDERSON'}), frozenset({'DOUG MANN'}), frozenset({'OLE SAVIOR'}), frozenset({'JAMES EVERETT', 'ABDUL M RAHAMAN "THE ROCK"'}), frozenset({'ALICIA K. BENNETT'}), frozenset({'CAPTAIN JACK SPARROW'}), frozenset({'TONY LANE'}), frozenset({'MIKE GOULD'}), frozenset({'KURTIS W. HANNA'}), frozenset({'JAYMIE KELLY'}), frozenset({'CHRISTOPHER CLARK'}), frozenset({'CHRISTOPHER ROBIN ZIMMERMAN'}), frozenset({'JEFFREY ALAN WAGNER'}), frozenset({'NEAL BAXTER'}), frozenset({'TROY BENJEGERDES'}), frozenset({'GREGG A. IVERSON'}), frozenset({'JOSHUA REA'}), frozenset({'MERRILL ANDERSON'}), frozenset({'BILL KAHN'}), frozenset({'JOHN LESLIE HARTWIG'}), frozenset({'EDMUND BERNARD BRUYERE'}), frozenset({'JAMES "JIMMY" L. STROUD, JR.'}))
                                  Status  Round
BETSY HODGES                   Remaining      4
MARK ANDREW                    Remaining      4
DON SAMUELS                    Remaining      4
CAM WINTON                     Remaining      4
JACKIE CHERRYHOMES             Remaining      4
BOB FINE                       Remaining      4
DAN COHEN                      Remaining      4
STEPHANIE WOODRUFF             Remaining      4
MARK V ANDERSON                Remaining      4
DOUG MANN                      Remaining      4
OLE SAVIOR                     Remaining      4
JAMES EVERETT                  Remaining      4
ABDUL M RAHAMAN "THE ROCK"     Remaining      4
ALICIA K. BENNETT              Remaining      4
CAPTAIN JACK SPARROW           Remaining      4
TONY LANE                      Remaining      4
MIKE GOULD                     Remaining      4
KURTIS W. HANNA                Remaining      4
JAYMIE KELLY                   Remaining      4
CHRISTOPHER CLARK              Remaining      4
CHRISTOPHER ROBIN ZIMMERMAN    Remaining      4
JEFFREY ALAN WAGNER            Remaining      4
NEAL BAXTER                    Remaining      4
TROY BENJEGERDES               Remaining      4
GREGG A. IVERSON               Remaining      4
JOSHUA REA                     Remaining      4
MERRILL ANDERSON               Remaining      4
BILL KAHN                      Remaining      4
JOHN LESLIE HARTWIG            Remaining      4
EDMUND BERNARD BRUYERE         Remaining      4
JAMES "JIMMY" L. STROUD, JR.   Remaining      4
RAHN V. WORKCUFF              Eliminated      4
BOB "AGAIN" CARNEY JR         Eliminated      3
CYD GORMAN                    Eliminated      2
JOHN CHARLES WILSON           Eliminated      1

Round 5

Winners: ()
Eliminated: (frozenset({'JAMES "JIMMY" L. STROUD, JR.'}), frozenset({'RAHN V. WORKCUFF'}), frozenset({'BOB "AGAIN" CARNEY JR'}), frozenset({'CYD GORMAN'}), frozenset({'JOHN CHARLES WILSON'}))
Remaining: (frozenset({'BETSY HODGES'}), frozenset({'MARK ANDREW'}), frozenset({'DON SAMUELS'}), frozenset({'CAM WINTON'}), frozenset({'JACKIE CHERRYHOMES'}), frozenset({'BOB FINE'}), frozenset({'DAN COHEN'}), frozenset({'STEPHANIE WOODRUFF'}), frozenset({'MARK V ANDERSON'}), frozenset({'DOUG MANN'}), frozenset({'OLE SAVIOR'}), frozenset({'ABDUL M RAHAMAN "THE ROCK"'}), frozenset({'ALICIA K. BENNETT'}), frozenset({'JAMES EVERETT'}), frozenset({'CAPTAIN JACK SPARROW'}), frozenset({'TONY LANE'}), frozenset({'MIKE GOULD'}), frozenset({'JAYMIE KELLY'}), frozenset({'KURTIS W. HANNA'}), frozenset({'CHRISTOPHER CLARK'}), frozenset({'CHRISTOPHER ROBIN ZIMMERMAN'}), frozenset({'JEFFREY ALAN WAGNER'}), frozenset({'NEAL BAXTER'}), frozenset({'TROY BENJEGERDES'}), frozenset({'GREGG A. IVERSON'}), frozenset({'MERRILL ANDERSON'}), frozenset({'JOSHUA REA'}), frozenset({'BILL KAHN'}), frozenset({'JOHN LESLIE HARTWIG'}), frozenset({'EDMUND BERNARD BRUYERE'}))
                                  Status  Round
BETSY HODGES                   Remaining      5
MARK ANDREW                    Remaining      5
DON SAMUELS                    Remaining      5
CAM WINTON                     Remaining      5
JACKIE CHERRYHOMES             Remaining      5
BOB FINE                       Remaining      5
DAN COHEN                      Remaining      5
STEPHANIE WOODRUFF             Remaining      5
MARK V ANDERSON                Remaining      5
DOUG MANN                      Remaining      5
OLE SAVIOR                     Remaining      5
ABDUL M RAHAMAN "THE ROCK"     Remaining      5
ALICIA K. BENNETT              Remaining      5
JAMES EVERETT                  Remaining      5
CAPTAIN JACK SPARROW           Remaining      5
TONY LANE                      Remaining      5
MIKE GOULD                     Remaining      5
JAYMIE KELLY                   Remaining      5
KURTIS W. HANNA                Remaining      5
CHRISTOPHER CLARK              Remaining      5
CHRISTOPHER ROBIN ZIMMERMAN    Remaining      5
JEFFREY ALAN WAGNER            Remaining      5
NEAL BAXTER                    Remaining      5
TROY BENJEGERDES               Remaining      5
GREGG A. IVERSON               Remaining      5
MERRILL ANDERSON               Remaining      5
JOSHUA REA                     Remaining      5
BILL KAHN                      Remaining      5
JOHN LESLIE HARTWIG            Remaining      5
EDMUND BERNARD BRUYERE         Remaining      5
JAMES "JIMMY" L. STROUD, JR.  Eliminated      5
RAHN V. WORKCUFF              Eliminated      4
BOB "AGAIN" CARNEY JR         Eliminated      3
CYD GORMAN                    Eliminated      2
JOHN CHARLES WILSON           Eliminated      1
/Users/cdonnay/Documents/GitHub/MGGG/VoteKit/src/votekit/pref_profile/pref_profile.py:1109: UserWarning: Profile does not contain rankings but max_ranking_length=3. Setting max_ranking_length to 0.
  warnings.warn(

Conclusion

There are many different possible election methods, both for choosing a single seat or multiple seats. VoteKit has a host of built-in election methods, as well as the functionality to let you create your own kind of election. You have been introduced to the STV and Borda elections and learned about the Election object. This should allow you to model any kind of elections you see in the real world, including rules that have not yet been implemented in VoteKit.

Further Prompts: Creating your own election system

VoteKit can’t be comprehensive in terms of possible election rules. However, with the Election and ElectionState classes, you can create your own. Let’s create a bit of a silly example; to elect \(m\) seats, at each stage of the election we randomly choose one candidate to elect. Most of the methods are handled by the RankingElection class, so we really only need to define how a step works, and how to know when it’s over.

from votekit.elections import RankingElection, ElectionState
from votekit.cleaning import remove_cand
import random


class RandomWinners(RankingElection):
    """
    Simulates an election where we randomly choose winners at each stage.

    Args:
        profile (PreferenceProfile): Profile to run election on.
        m (int, optional): Number of seats to elect.
    """

    def __init__(self, profile: PreferenceProfile, m: int = 1):
        # the super method says call the RankingElection class
        self.m = m
        super().__init__(profile)

    def _is_finished(self) -> bool:
        """
        Determines if another round is needed.

        Returns:
            bool: True if number of seats has been met, False otherwise.
        """
        # need to unpack list of sets
        elected = [c for s in self.get_elected() for c in s]

        if len(elected) == self.m:
            return True

        return False

    def _run_step(
        self, profile: PreferenceProfile, prev_state: ElectionState, store_states=False
    ) -> PreferenceProfile:
        """
        Run one step of an election from the given profile and previous state.

        Args:
            profile (PreferenceProfile): Profile of ballots.
            prev_state (ElectionState): The previous ElectionState.
            store_states (bool, optional): True if `self.election_states` should be updated with the
                ElectionState generated by this round. This should only be True when used by
                `self._run_election()`. Defaults to False.

        Returns:
            PreferenceProfile: The profile of ballots after the round is completed.
        """

        elected_cand = random.choice(profile.candidates)
        new_profile = remove_cand(elected_cand, profile)

        # we only store the states the first time an election is run,
        # but this is all handled by the other methods of the class
        if store_states:
            self.election_states.append(
                ElectionState(
                    round_number=prev_state.round_number + 1,
                    remaining=(frozenset(new_profile.candidates),),
                    elected=(frozenset(elected_cand),),
                )
            )

        return new_profile
candidates = ["A", "B", "C", "D", "E", "F"]
profile = bg.ImpartialCulture(candidates=candidates).generate_profile(1000)

election = RandomWinners(profile=profile, m=3)
print(election)
      Status  Round
E    Elected      1
D    Elected      2
A    Elected      3
F  Remaining      3
C  Remaining      3
B  Remaining      3

Try it yourself

Create an election class called AlphabeticalElection that elects a number of candidates decided by the user simply based on alphabetical order. You mind find it helpful to use the following code which sorts a list of strings:

# Original list of strings
original_list = ["banana", "apple", "grape", "orange"]

# Alphabetically sorted list
sorted_list = sorted(original_list)

# Print the sorted list
print(sorted_list)
['apple', 'banana', 'grape', 'orange']
class AlphabeticaElection(RankingElection):
    """
    Simulates an election where we choose winners alphabetically at each stage.

    Args:
        profile (PreferenceProfile): Profile to run election on.
        m (int, optional): Number of seats to elect.
    """

    def __init__(self, profile: PreferenceProfile, m: int = 1):
        # the super method says call the RankingElection class
        self.m = m
        super().__init__(profile)

    def _is_finished(self) -> bool:
        """
        Determines if another round is needed.

        Returns:
            bool: True if number of seats has been met, False otherwise.
        """
        # need to unpack list of sets
        elected = [c for s in self.get_elected() for c in s]

        if len(elected) == self.m:
            return True

        return False

    def _run_step(
        self, profile: PreferenceProfile, prev_state: ElectionState, store_states=False
    ) -> PreferenceProfile:
        """
        Run one step of an election from the given profile and previous state.

        Args:
            profile (PreferenceProfile): Profile of ballots.
            prev_state (ElectionState): The previous ElectionState.
            store_states (bool, optional): True if `self.election_states` should be updated with the
                ElectionState generated by this round. This should only be True when used by
                `self._run_election()`. Defaults to False.

        Returns:
            PreferenceProfile: The profile of ballots after the round is completed.
        """

        pass