Voting Systems: A Python program

Polling beyond plurality

Angelina
Random Thoughts
7 min readAug 17, 2020

--

Photo by Element5 Digital from Pexels

My name is Angelina Zoght and I’m a high school student living in Vancouver, Canada. Months before the COVID-19 pandemic began, my family moved to Vancouver from the nation’s capital of Ottawa so my dad could secure a job at AWS. Speaking of my dad, he’s been blogging here for a while. Check him out!

Anyways, one of my many pastimes is coding, whether it’s pushing projects to GitHub, experimenting with packages/libraries, or watching Tech With Tim videos (no matter how long you’ve been coding for, you should check out his channel too). I can code with Java, Go, and Python, but I’ve been more focused on coding with Python during this social-distancing period.

Since the American election is coming up this fall 2020 (but us Canadians, will have to wait until 2023 for our next federal election), I thought I’d share one of the Python projects I’ve been working on this summer: a basic vote count system.

The program simulates four vote counting systems: Plurality, Instant Runoff, Borda Count, and Condorcet. Yes, there’s more than one voting system out there!

Plurality is widely used around the globe and is the most common. Instant Runoff, Borda Count, and Condorcet voting systems need something a little different to work. They need…

Ranked ballots!

https://giphy.com/gifs/netflix-3og0IU2CCxcCsu0bgk

It’s a different kind of ballot than the ones we’re familiar with, but basically, it’s a ballot where instead of picking your preferred choice, you rank the choices provided. You can leave candidates empty in real life, but my program assumes that voters will rank all choices.

Example: In a poll to determine who the favourite guy from the classic ’90s sitcom F.R.I.E.N.D.S. is, one voter ranks their ballot like so. <candidate>-<rank>

Ballot #1:

Chandler -3

Joey -1

Ross -2

Another voter ranks theirs like this:

Ballot #2:

Chandler -2

Joey -3

Ross -1

In the program, you’d make the ballots like this:

b1 = Ballot(['Chandler','Joey','Ross'],[3,1,2])
b2 = Ballot(['Chandler','Joey','Ross'],[2,3,1])
"""
In my original version of this program, my ballots were dictionaries, but now the Ballot is a straight-up class with two arrays of equal length as parameters.
"""

After making your ballots with for loops, randomization, or whatever, you’ll need to add them to a ballot registry like so.

r = BallotRegistry()
r.addBallot(b1)
r.addBallot(b2)

Once your ballot registry is ready, you can make a vote counter, with all the vote count methods included, adding the ballot registry as a parameter.

v = VoteCount(r)
v.plurality()
v.instantRunoff()
v.bordaCount()
v.condorcet()
"""
Instead of returning the winner of the vote, each method built into the VoteCount class prints the winner, but at least you’ll know who the winner is.
"""

For all the VoteCount methods, an instance of the Ballot class separate from the registry, named count_register, is created within the function. Its candidates parameter is the same as all ballots in the registry, but its votes parameter isn’t ranks. Instead, it’s a list of counts/scores/total votes for each candidate. The count register comes in handy the most at the end of each method, when the winner is determined based on which candidate has the highest score in count_register.

Alright, now for the fun part! Here’s how each method works!

If you’re curious to see how these methods work in the real world, check out this video from PBS Infinite Series, which explains the four methods I’ve coded in detail, as well as how voting systems can give you different results.

Plurality

As I’ve mentioned earlier, this system continues to be used to elect democracies in North America and most parts of the world. It’s the only system out of the four that doesn’t require ranked ballots. The minimum requirement for this to work, though, is that only one candidate on the ballot is ranked 1, since the program adds candidates ranked 1 to count_register.

def plurality(self):  
count_register = self.__makecountregister__()
for b in self.registry.br:
for r in range(len(b.votes)):
if b.votes[r] == 1:
count_register.votes[r] += 1
break

winner = 0

for c in range(len(count_register.votes)):
if count_register.votes[c] > count_register.votes[winner]:
winner = c

print("Winner is", count_register.candidates[winner])

It’s the most simple system, so not a lot of code, and very easy to implement.

Instant Runoff

This ranked-ballot voting system works in rounds. During each round o (where o begins at 1), candidates ranked o are added to count_register. At the end of each round, the candidate with the lowest votes in count_register is eliminated by simply setting its votes to -1 (Maybe I should’ve just deleted them completely). If it has been more than 1 rounds and there is a majority (i.e. one candidate has most of the votes), then the candidate with the most votes in count_register is declared the winner. Otherwise, the last candidate standing wins (I’ll admit, I haven’t tested to see if that was possible).

def instantrunoff(self): 
count_register = self.__makecountregister__()
for o in range(1, len(count_register.candidates)):
for b in self.registry.br:
for r in range(len(b.votes)):
if b.votes[r] == o and count_register.candidates[r] != -1:
count_register.votes[r] += 1
break
least = 0

if o > 1 and hasMajority(count_register):
break

for
c in range(len(count_register.votes)):
if count_register.votes[c] < count_register.votes[least] and count_register.votes[c] != -1:
least = c

count_register.votes[least] = -1

winner = 0
for c in range(len(count_register.votes)):
if count_register.votes[c] > count_register.votes[winner]:
winner = c

print("Winner is", count_register.candidates[winner])

The hasMajority() function doesn’t belong to a class. It just checks if there is a candidate in count_register (or any instance of the Ballot class) with most of the total votes. Here it is if you’re curious:

def hasMajority(b: Ballot) -> bool:  
majThreshold = 0
for v in range(len(b.votes)):
if b.votes[v] != -1:
majThreshold += b.votes[v]
majThreshold = majThreshold // 2
biggest = 0
for v in range(len(b.votes)):
if b.votes[v] > b.votes[biggest]:
biggest = v
if b.votes[biggest] >= majThreshold:
return True
return False

The interesting thing about this system is that it will guarantee a majority to determine the winner. There is some controversy about if majority governments (the winning party has the most votes) are better than minority governments (the winning party has less than half of votes). Both types of governments can have a huge impact on policy. In Canada’s last election, Justin Trudeau’s government switched from majority to minority, which means his party will need to seek other parties for agreement on their proposed policies. With one candidate always having most of the votes with Instant Runoff Voting, it could mean total influence (not to be biased here)!

Borda Count

This system, developed (but not first used) by Frenchman Jean-Charles de Borda, gives a weight in reverse proportion to each rank (I googled, and there’s a variation where the weight is reverse proportion minus 1, so the least ranked is given a weight of 0. I feel like I could have used that variation for this code, but I dunno, does it matter?). The method makes a “Borda list” based on the possible ranks, and applies it to each ballot with a for loop, within a for loop, within a for loop . Then again, the winner is whoever has the highest weighted value in count_register.

def bordacount(self):  
count_register = self.__makecountregister__()
bordalist = [] # borda weights for each rank
for i in reversed(range(len(count_register.votes))):
bordalist.append(i + 1)

for o in range(1, len(count_register.candidates)):
for b in self.registry.br:
for r in range(len(b.votes)):
if b.votes[r] == o:
count_register.votes[r] += bordalist[o - 1]
break
winner = 0
for c in range(len(count_register.votes)):
if count_register.votes[c] > count_register.votes[winner]:
winner = c

print("Winner is", count_register.candidates[winner])

This method is cool, but based on further research, I saw that it can result in a tie. I doubt that it’s very likely, correct me if I’m wrong; fortunately, I love it simply because of the fact it has weights!

https://media.giphy.com/media/lpWeC20cUd49G/giphy.gif

Not those weights, but you know what I mean.

Condorcet

This system, also created by a Frenchman, works by pairing candidates against one another.

Sticking to the F.R.I.E.N.D.S. poll example, that would mean the possible rounds are Chandler vs. Joey, Ross vs. Chandler, and Joey vs. Ross.

Because I love random number generation, I’ve decided the make the order of my round list random, although there’s another, less random way I could make the round list.

In each round, if one candidate is ranked higher than the other in a ballot, they get a point. Else, the other does. In other words, the winner is whoever is ranked higher than the other the most within the registry.

For example, if the round was “Chandler vs. Joey”, and the ballot is Ballot #1, Joey is ranked higher than Chandler, so Joey gets a point.

But if the next ballot was Ballot #2, then Chandler would get a point.

The winner of the round is then added to count_register. Whoever won the most rounds is the winner of the Condorcet vote.

def condorcet(self):  
count_register = self.__makecountregister__()
roundList = [] # list of possible
fillingList = True
lengthofRoundList = 0.5 * (len(count_register.candidates) ** 2) - 0.5 * len(count_register.candidates)
while fillingList:
r1 = random.randint(0, len(count_register.candidates) - 1)
r2 = random.randint(0, len(count_register.candidates) - 1)
if r1 != r2:
if not containsBoth(roundList, r1, r2):
roundList.append([r1, r2])
if len(roundList) == lengthofRoundList:
fillingList = False

for
ro in roundList:
c1 = 0
c2 = 0
for b in self.registry.br:
if isHigherThan(b, ro[0], ro[1]):
c1 += 1
else:
c2 += 1
if c1 > c2:
count_register.votes[ro[0]] += 1
if c2 > c1:
count_register.votes[ro[1]] += 1

biggest_num = 0
for v in range(len(count_register.votes)):
if count_register.votes[v] > biggest_num:
biggest_num = count_register.votes[v]

count = 0
for v in range(len(count_register.votes)):
if count_register.votes[v] == biggest_num:
count += 1

if count > 1:
print("It's a tie!")
else:
winner = ""
for
c in range(len(
count_register.candidates)):
if count_register.votes[c] == biggest_num:
winner = count_register.candidates[c]

print("Winner is", winner)

The only drawback of this voting system is that it can result in a tie shared by all candidates. Luckily, my simple fix to a tie is…well…printing “It’s a tie!”.

You can fix ties yourself by a) debugging to see who has the tie and picking the winner at random, b) adding a tiebreaker ballot, c) using a different vote count system.

If you want to see the full thing on GitHub check out my repo.

--

--