[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Tabulate ranked voting in Python

[This program simulates tabulation for ranked voting in Python.]

New York City's June 22, 2021 Primary Election used a ranked voting ballot and it's becoming more popular lately. This example shows how you might find the winner in a ranked voting election.

What Is Ranked Voting?

In ranked voting (aka ranked-choice voting or preferential voting), each voter ranks the candidates into first, second, third, and other choices. To decide the winner, you repeat these steps:

  1. For each voter, find the highest ranked candidate that is still in the running and add one to that candidate's tally.
  2. If a candidate has more than 50% of the total votes, then that candidate is the winner.
  3. If no candidate has more than 50% of the total votes, you find the candidate(s) with the fewest votes and eliminate them from the running.
You repeat those steps until you find a winner.

Note that you might eliminate multiple candidates in a single round if they have exactly the same smallest number of votes.

It is also possible to end in a tie. This happens if two or more candidates split the vote exactly evenly. For example, if there are 3,000 ballots and three remaining candidates, each of whom has exactly 1,000 votes, then you cannot determine the last place candidate to eliminate.

However, both eliminating multiple candidates in a single round and ending in a tie are very unlikely if there are many voters.

Note also that, if there are N candidates, then there can be at most N - 1 rounds. During each round, at least one candidate is eliminated, so after N - 1 rounds there can be at most one candidate remaining. (Often there will be a winner earlier.)

Why?

You can read about the pros and cons of ranked voting on Wikipedia and other places, but here's one important advantage.

Ranked voting avoids penalizing similar candidates who split the vote. For example, suppose an election has four candidates, three of whom (Anderson, Baker, and Campbell) have similar peace-and-harmony platforms and one of whom, Thanos, is very different, running on a destroy-half-of-the-universe platform. Now suppose 71% of the people prefer the peace-and-harmony platform and the votes come out as: Anderson 24%, Baker 21%, Campbell 26%, and Thanos 29%.

In a plurality voting system where the candidate with the most votes wins, Thanos would win even though 71% of the voters oppose the destroy-half-of-the-universe platform.

There are several ways you could avoid an outcome where a minority candidate wins. You could have a runoff election between the two most popular candidates. In this example, Campbell would win. Note that this might not be the "best" outcome either if all of the Baker voters prefer Anderson as their second choice. In that case, a majority might prefer Anderson to Campbell, but at least they have some input in the runoff.

Ranked voting is another strategy for handling this issue. It also has the added advantage that you can perform the rounds without going back to the ballot box, which takes extra time and effort.

The Ballot Class

The example uses the following Ballot class to hold a voter's choices.

import random class Ballot(): def __init__(self, num_choices): # Pick random choices. self.choices = list(range(num_choices)) random.shuffle(self.choices) def __str__(self): return f'{self.choices}' def top_choice(self, eliminated): '''Find this ballot's top non-disqualified choice.''' for candidate in self.choices: if not eliminated[candidate]: return candidate return None

The choices list holds the indices of the candidates in the voter's ranking. For example, if the array holds 1, 3, 2, 0, then the voter's first choice is candidate 1, the second choice is candidate 3, and so forth. For the test program, the class's constructor initializes the choice list randomly.

The top_choice method returns the voter's top choice given an array showing which candidates have been eliminated in earlier rounds. For example, eliminated[2] is True if candidate number 2 has been eliminated.

This method simply loops through the voter's choices in rank order. When it finds a candidate that has not been eliminated, it returns that candidate's index.

The method should not fail to find a candidate because the program should not eliminate all of the candidates.

Making Ballots

The following make_ballots function simply creates a given number of ballots each holding a given number of choices.

def make_ballots(num_ballots, num_choices): '''Create some random ballots.''' return [Ballot(num_choices) for i in range(num_ballots)]

Displaying Status

The following show_counts method displays the current candidate vote tallies.

def show_counts(ballots, eliminated): '''Count the number of votes for each choice and return the counts.''' # Count each ballot's top choice. num_choices = len(ballots[0].choices) counts = [0 for i in range(num_choices)] for ballot in ballots: top_choice = ballot.top_choice(eliminated) if top_choice is not None: counts[top_choice] += 1 # Display the counts. for i in range(num_choices): print(counts[i], end=' ') print() return counts

The code creates a counts list to hold the counts for each choice and then loops through the ballots. For each ballot, it calls the ballot's top_choice function to get that ballot's current top choice. If the result is not None, the code increments that choice's counts value.

After it has counted up the current votes, the code loops through the counts list and displays each choice's count. The function also returns the counts list so the calling code can process it.

Simulating Votes

The following simulate_votes method performs the simulation.

import math def simulate_votes(num_ballots, num_choices): # Make random ballots. needed_to_win = math.floor(num_ballots / 2 + 1) print(f'Needed to win: {needed_to_win}') ballots = make_ballots(num_ballots, num_choices) # Initially no choice is eliminated. eliminated = [False for i in range(num_choices)] # Run through the rounds. winner = None for i in range(num_choices): # Display the current counts. print(f'Round {i+1}') counts = show_counts(ballots, eliminated) # See if there is a winner. for j in range(num_choices): if counts[j] >= needed_to_win: winner = j break if winner is not None: break # There was no winner. Eliminate the least popular choice. # Find the smallest vote count. eligible_counts = [counts[k] for k in range(num_choices) if not eliminated[k]] if len(eligible_counts) == 0: print('All choices have been eliminated') break smallest = min(eligible_counts) for k in range(num_choices): if counts[k] == smallest: eliminated[k] = True print(f'Eliminating {k} which has {counts[k]} votes') print() if winner is not None: print(f'The winner is {winner} with {counts[winner]} votes')

This code calculates the number of votes needed to win. The expression math.floor(num_ballots / 2 + 1) returns 1 more than half of the number of ballots. For example, for 100 ballots it returns 51 and for 101 ballots it also returns 51.

Next the code creates the desired number of random ballots. It then makes an empty eliminated list so it can keep track of the candidates that have been eliminated.

The method then enters a loop that runs for at most num_choices rounds. Within each round, it calls show_counts to display the current standings.

Next, the code loops through the counts to see if any candidate has enough votes to win. If someone has won, the code sets winner and breaks out of the loop.

If there is no winner yet, the code uses a list comprehension to select the counts of the candidates who have not yet been eliminated. It uses min find the smallest of those counts.

The code then loops through the counts entries and, if a value equals the smallest value, it eliminates the corresponding candidate.

Eventually the program will either find a winner with at least 50% + 1 votes, or all of the remaining candidates will have the same number of votes and we end in a tie. I'm not sure how you would handle that. A runoff election among the tied candidates would probably break things loose again (because some voters will probably change their voting strategy given another chance) so the algorithm could continue.

Note that the program assumes all ballots have ranked every choice. For example, if some ballots only have 3 choices ranked, then after a few rounds that ballot might not have any candidates remaining. In that case, the total number of possible votes will be reduced so the number needed to win should change.

Summary

With all of that setup, the main program is very short:

simulate_votes(100, 5)

Ranked voting may have some problems with special cases (like ties) and voters may be reluctant to try a new system, but it does have some advantages. For example, it avoids a separate runoff election (which is time-consuming and expensive) and prevents an unpopular candidate from winning when other candidates split the vote. Of course the electoral college is a whole different issue.😉

As always, download the example to see all of the details.

© 2024 Rocky Mountain Computer Consulting, Inc. All rights reserved.