Majorities and Runoffs

How Voting Systems Work

Chapter 3 ยท Majorities and Runoffs

How Voting Systems Work 2 showed plurality electing a candidate whom most voters ranked below a rival. A natural repair is to insist on a majority: more than half. There are two common ways to get one. The two-round system holds a second election between the leaders; instant-runoff voting, usually called ranked-choice voting in the United States, simulates that second election from ranked ballots in a single trip to the polls. This chapter writes both as short Python functions, prints every transfer so you can follow the count, and then measures how often each one picks the head-to-head winner in the simulated races from the last chapter. The maths is counting and percentages.

What was run, and what to trust
Every Python snippet in this chapter and in the three exercise solutions was executed, and the output in the comments is what Python printed. Facts about real systems come from web search summaries (Wikipedia pages, a university maths text, advocacy pages), and such fetched pages are often truncated or model-summarised, so treat the wording as a paraphrase, not a quotation. The elections, candidates and ballot counts in the code are assumed or illustrative: invented to show a method, not results from any real vote. The simulation rests on a simple one-line model that real politics does not follow. Statements marked (check) were not confirmed against a source.

The Two-Round System

In the two-round system every voter marks one candidate. If someone has a majority, they win. If not, the top two go to a second round, and the winner of that round has more than half the votes, because only two candidates remain. A summary of the French presidential rules says exactly this: when no candidate has an absolute majority in the first round, a second round is held two weeks later between the two candidates with the most votes, and, as of 2022, every direct presidential election had gone to a second round. The same summary says French legislative races differ, with candidates who reach 12.5% of registered voters advancing, so three or four can be in the second round (check: not confirmed beyond that one summary).

Our function counts first choices, checks for a majority, and otherwise compares the top two finalists using the ballots. Notice the assumption in the docstring: it lets each voter back whichever finalist they ranked higher, which assumes nobody changes their mind between the rounds. In reality they can, and that is part of the point of holding two rounds. The first profile is the 100-voter split from How Voting Systems Work 2; the second is a new one with a candidate in the middle.

from collections import Counter def two_round(ballots): """Round 1: first choices. A majority wins outright; otherwise the top two meet in round 2. Assumption: voters keep the same rankings in round 2 and back whichever finalist they rank higher.""" n = len(ballots) first = Counter(b[0] for b in ballots) print(" round 1:", dict(sorted(first.items()))) leader, top = first.most_common(1)[0] if top > n / 2: print(f" {leader} has a majority, no second round") return leader a, b = [c for c, _ in first.most_common(2)] a_votes = sum(1 for x in ballots if x.index(a) < x.index(b)) print(f" round 2: {a} {a_votes}, {b} {n - a_votes}") return a if a_votes > n - a_votes else b # Illustrative numbers: invented to show a method, not a real election. split = ( [("Rae", "Sam", "Tom")] * 28 + [("Sam", "Rae", "Tom")] * 27 + [("Tom", "Sam", "Rae")] * 45 ) squeeze = ( [("Ana", "Bo", "Cy")] * 35 + [("Cy", "Bo", "Ana")] * 33 + [("Bo", "Ana", "Cy")] * 16 + [("Bo", "Cy", "Ana")] * 16 ) for name, profile in (("split vote", split), ("centre squeeze", squeeze)): print(name) print(" winner:", two_round(profile)) # split vote # round 1: {'Rae': 28, 'Sam': 27, 'Tom': 45} # round 2: Tom 45, Rae 55 # winner: Rae # centre squeeze # round 1: {'Ana': 35, 'Bo': 32, 'Cy': 33} # round 2: Ana 51, Cy 49 # winner: Ana

In the split-vote profile the second round does what the plurality count could not: Rae, the other half of the divided pair, meets Tom and wins 55 to 45. In the second profile Bo is first on 32 ballots, last of the three, and is knocked out. Ana and Cy go through, and Ana wins 51 to 49. Look at the ballots again: 65 of the 100 voters (33 + 16 + 16) rank Bo above Ana, so Bo would have beaten Ana head to head, and the same count shows Bo beating Cy by a wide margin too. This is called a centre squeeze: the candidate most voters find acceptable is eliminated because few call them first. Remember that for later.

Instant-Runoff Voting

Instant-runoff voting (IRV) asks voters to rank candidates on one ballot. The count then proceeds in rounds: each ballot counts for its highest-ranked candidate still standing; if nobody holds a majority of the ballots in play, the candidate with the fewest votes is eliminated and their ballots move to the next preference on each; repeat until someone does. Each pass is a transfer. Australia’s House of Representatives is described in the Wikipedia article on optional preferential voting as using this rule, with full preferential voting, which means voters must rank every candidate and ballots never run out. Maine uses ranked-choice voting for some elections (check: I did not confirm which ones).

FeatureTwo-round systemInstant-runoff voting
Ballotsone mark per rounda ranking, filled in once
Visits to the pollsone or twoone
Who goes throughthe top two on first choiceseveryone, until the last-place candidate is dropped each round
Voters can change their mindyes, between roundsno, the ranking is fixed (assumed typical rules)
Number of roundsat most twoup to one fewer than the number of candidates

Here is IRV in Python. The function prints each round, which is the point of the chapter: you can see exactly where each ballot goes. Ties for last place are broken alphabetically here (assumed; real rules use lots, earlier counts and other devices).

from collections import Counter def irv(ballots, verbose=True): """Instant-runoff: drop the candidate with the fewest top choices, pass their ballots on, repeat until someone holds a majority of the ballots still in play. Assumption: a tie for last place is broken alphabetically (real rules differ).""" remaining = {c for b in ballots for c in b} round_no = 1 while True: counts = Counter({c: 0 for c in remaining}) for b in ballots: for c in b: # first candidate still standing on this ballot if c in remaining: counts[c] += 1 break active = sum(counts.values()) leader = sorted(counts, key=lambda c: (-counts[c], c))[0] if verbose: line = ", ".join(f"{c} {counts[c]}" for c in sorted(counts)) print(f"round {round_no}: {line} (in play {active}, exhausted {len(ballots) - active})") if counts[leader] > active / 2: if verbose: print(f" {leader} has {counts[leader]} of {active}: winner") return leader fewest = min(counts.values()) out = sorted(c for c in remaining if counts[c] == fewest)[0] if verbose: print(f" eliminate {out}") remaining.discard(out) round_no += 1 # Illustrative numbers: invented to show a method, not a real election. split = ( [("Rae", "Sam", "Tom")] * 28 + [("Sam", "Rae", "Tom")] * 27 + [("Tom", "Sam", "Rae")] * 45 ) print("split vote") irv(split) squeeze = ( [("Ana", "Bo", "Cy")] * 35 + [("Cy", "Bo", "Ana")] * 33 + [("Bo", "Ana", "Cy")] * 16 + [("Bo", "Cy", "Ana")] * 16 ) print("centre squeeze") irv(squeeze) # split vote # round 1: Rae 28, Sam 27, Tom 45 (in play 100, exhausted 0) # eliminate Sam # round 2: Rae 55, Tom 45 (in play 100, exhausted 0) # Rae has 55 of 100: winner # centre squeeze # round 1: Ana 35, Bo 32, Cy 33 (in play 100, exhausted 0) # eliminate Bo # round 2: Ana 51, Cy 49 (in play 100, exhausted 0) # Ana has 51 of 100: winner

On the split profile IRV drops Sam (27), passes those ballots to Rae, and Rae wins 55 to 45: the same result as the two-round count, with no second election. On the centre-squeeze profile it also drops Bo and elects Ana 51 to 49, the same candidate the two-round system elected, and the same candidate who loses head to head to Bo. So fixing the split does not guarantee the head-to-head winner.

Ballots that run out

If voters need not rank everyone, a ballot can run out of candidates: it is exhausted, and drops out of the count. The majority is then measured against the ballots still in play, not all those cast. The Maine and Burlington figures I found were mostly from advocacy pages, so the exhaustion rates I saw (for example 10.7% of ballots in Maine’s 2018 governor primary, per an MIT analysis of that primary, and 151 of 8,980 in Burlington after the first round, per an advocacy page) are unverified (check).

from collections import Counter def irv(ballots, verbose=True): """Instant-runoff: drop the candidate with the fewest top choices, pass their ballots on, repeat until someone holds a majority of the ballots still in play. Assumption: a tie for last place is broken alphabetically (real rules differ).""" remaining = {c for b in ballots for c in b} round_no = 1 while True: counts = Counter({c: 0 for c in remaining}) for b in ballots: for c in b: # first candidate still standing on this ballot if c in remaining: counts[c] += 1 break active = sum(counts.values()) leader = sorted(counts, key=lambda c: (-counts[c], c))[0] if verbose: line = ", ".join(f"{c} {counts[c]}" for c in sorted(counts)) print(f"round {round_no}: {line} (in play {active}, exhausted {len(ballots) - active})") if counts[leader] > active / 2: if verbose: print(f" {leader} has {counts[leader]} of {active}: winner") return leader fewest = min(counts.values()) out = sorted(c for c in remaining if counts[c] == fewest)[0] if verbose: print(f" eliminate {out}") remaining.discard(out) round_no += 1 # Illustrative numbers: invented. Voters may rank only some candidates. ballots = ( [("Ana",)] * 30 + [("Bo", "Cy")] * 28 + [("Cy", "Bo")] * 22 + [("Dee",)] * 20 ) print("winner:", irv(ballots)) # round 1: Ana 30, Bo 28, Cy 22, Dee 20 (in play 100, exhausted 0) # eliminate Dee # round 2: Ana 30, Bo 28, Cy 22 (in play 80, exhausted 20) # eliminate Cy # round 3: Ana 30, Bo 50 (in play 80, exhausted 20) # Bo has 50 of 80: winner # winner: Bo

Bo wins with 50 of the 80 ballots in play, which is a majority of those, but only 50 of the 100 ballots cast, which is not a majority of all of them. Another way to say this is that the headline “winner has a majority” depends on which total you divide by. Always report both numbers.

Real Elections

A UNL maths text on alternative voting methods describes Burlington, Vermont’s 2009 mayoral election, where Andy Montroll was eliminated and his transferred votes lifted Bob Kiss over Kurt Wright. How Voting Systems Work 2 mentioned the head-to-head figures from the Wikipedia summary (4,064 to 3,476); I have not verified them. Sources also differ on timing: one says the city adopted ranked-choice voting for mayoral elections in 2005, while another describes the 2009 contest as the second such election, which suggests an earlier one (check). The lesson is the same as the centre squeeze above: a candidate can be eliminated first and still be the one most voters prefer to the winner.

How Often Do They Pick the Head-to-Head Winner?

We reuse the one-line model from How Voting Systems Work 2: 101 voters at random points on a line, candidates at random positions, voters ranking by distance. For each race we find the Condorcet winner (the candidate who beats every rival head to head; with 101 voters on a line one exists) and ask how often plurality, the two-round system and IRV choose someone else.

import random from collections import Counter def first_choices(ballots): return Counter(b[0] for b in ballots) def plurality(ballots): counts = first_choices(ballots) top = max(counts.values()) return sorted(c for c, v in counts.items() if v == top) def runoff(ballots): n = len(ballots) counts = first_choices(ballots) ranked = sorted(counts, key=lambda c: (-counts[c], c)) if counts[ranked[0]] > n / 2: return ranked[0] a, b = ranked[:2] a_votes = sum(1 for x in ballots if x.index(a) < x.index(b)) return a if a_votes > n - a_votes else b def irv(ballots): remaining = set(ballots[0]) while True: counts = Counter({c: 0 for c in remaining}) for b in ballots: for c in b: if c in remaining: counts[c] += 1 break leader = sorted(counts, key=lambda c: (-counts[c], c))[0] if counts[leader] > len(ballots) / 2: return leader fewest = min(counts.values()) remaining.discard(sorted(c for c in remaining if counts[c] == fewest)[0]) def condorcet(ballots): names = sorted(ballots[0]) for x in names: if all(sum(1 for b in ballots if b.index(x) < b.index(y)) > len(ballots) / 2 for y in names if y != x): return x random.seed(31) trials = 2000 for k in (3, 4, 5): miss = {"plurality": 0, "two-round": 0, "instant-runoff": 0} for _ in range(trials): voters = [random.random() for _ in range(101)] pos = {f"C{i}": random.random() for i in range(k)} ballots = [tuple(sorted(pos, key=lambda c: abs(pos[c] - v))) for v in voters] target = condorcet(ballots) if plurality(ballots) != [target]: miss["plurality"] += 1 if runoff(ballots) != target: miss["two-round"] += 1 if irv(ballots) != target: miss["instant-runoff"] += 1 row = " ".join(f"{m} {n / trials:5.1%}" for m, n in miss.items()) print(f"{k} candidates: {row}") # 3 candidates: plurality 35.9% two-round 17.5% instant-runoff 17.5% # 4 candidates: plurality 53.6% two-round 29.0% instant-runoff 33.5% # 5 candidates: plurality 64.2% two-round 36.7% instant-runoff 44.1%

Plurality fails in 35.9%, 53.6% and 64.2% of the races, close to the rates seen in the last chapter. Both majority rules do much better: with three candidates the two-round system and IRV each miss in 17.5% of races, but with four and five candidates the gap between them opens and IRV misses more often (33.5% and 44.1%, against 29.0% and 36.7%). The figures depend on this seed and this model, so the trend is what to take away: majority rules help, and neither is free of the problem. Whether the same pattern holds in real elections is unknown from this simulation.

RuleGuarantees a majority?Can elect someone who loses a head-to-head contest?
Pluralitynoyes (How Voting Systems Work 2)
Two-roundyes, in round twoyes (centre squeeze)
Instant-runoffamong ballots still in play, not necessarily among all castyes (centre squeeze)

There is one more oddity, which Exercise 3 hunts for by machine. IRV is said to fail the monotonicity criterion, which means that ranking a candidate higher can sometimes make them lose (check: I took this from memory, not from a source). The exercise finds an explicit example among random profiles and prints it, so you can verify it by hand.

Common mistakes
Assuming a runoff or IRV always picks the head-to-head winner (the centre squeeze shows it need not); calling an IRV winner’s share a majority without saying what it is a majority of (ballots in play or ballots cast); assuming voters in a two-round election vote the same way in both rounds; breaking a last-place tie without saying how; and treating simulation rates from an assumed one-line model as measurements of real elections.
Exercise 1

Thirty members rank four candidates (assumed numbers). Write a program that prints the first-round counts, announces an outright winner if there is a majority, and otherwise runs the second round between the top two. Then check whether the winner beats every other candidate head to head.

๐Ÿ“„ View solution
Exercise 2

Run the instant-runoff function on a 40-voter, five-candidate profile in which some ballots rank only one or two candidates (assumed numbers). Print every round, and state how many ballots are exhausted when the winner is declared. Is the winner’s count a majority of all 40 ballots?

๐Ÿ“„ View solution
Exercise 3

Search seeded random 21-voter, three-candidate profiles for an instant-runoff monotonicity failure: a winner who would lose if one group of voters who ranked them below first moved them up a single place. Print the profile and both results.

๐Ÿ“„ View solution

Quick Reference

  • Two-round system: majority wins outright; otherwise the top two meet in round two
  • Instant-runoff (ranked choice): drop the last-place candidate, transfer their ballots to the next preference, repeat until someone holds a majority
  • Transfer: moving an eliminated candidate’s ballots to each ballot’s next remaining choice
  • Exhausted ballot: a ballot with no remaining ranked candidate; the majority is then of ballots in play
  • Centre squeeze: a widely acceptable candidate is eliminated because few rank them first
  • Python habit: print every round while developing a count, then hide it behind a verbose flag for simulations
  • Always state: the tie-breaking rule, and what a “majority” is a majority of
Coming next
How Voting Systems Work 4 is about pairwise contests: Condorcet winners, cycles, the Copeland and Minimax methods, and building a pairwise matrix.