Plurality Quirks

How Voting Systems Work

Chapter 2 ยท Plurality Quirks

How Voting Systems Work 1 ended with a nine-voter election in which the plurality winner was the last choice of a majority. This chapter stays with that rule, the simplest one in the course, and asks what else it does. Plurality is the rule behind the phrase “first past the post”, and it has three famous quirks: it can elect a candidate most voters dislike, it punishes candidates who share voters with a similar rival, and it nudges voters toward tactical choices. We will count each quirk in small Python programs, then simulate thousands of three-way races to see how often the rule disagrees with a head-to-head count. The maths stays at arithmetic 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. Definitions come from the Wikipedia articles on the spoiler effect and on Duverger’s law, fetched with a tool that often returns truncated pages or model-written summaries, so treat the wording as a paraphrase, not a quotation. The elections, districts and candidate positions in the code are assumed or illustrative: invented to show a method, not results from any real vote. The simulations rest on a simple model (one left-to-right line) that real politics does not follow. Statements marked (check) were not confirmed against a source.

The Rule: Most Votes Wins

Under plurality, each voter marks one candidate, and the candidate with the most marks wins, whether or not that is a majority. Nothing else on the ballot is counted. The Wikipedia article on Duverger’s law describes a tendency, which it is careful to call “a tendency rather than an invariant rule”, for plurality voting in single-member districts to favour two-party competition. It names two mechanisms, summarised in the table. The article attributes the law to Maurice Duverger but, as fetched, gave no date for it (check).

MechanismWhat happensWho acts
Mechanicalsmall parties win few seats, so they have little reason to form or lastthe counting rule itself
Psychologicalvoters avoid “wasting” a vote on a party unlikely to win and move to a bigger onethe voters, in advance of the count

The same article lists exceptions, so treat the law as a pattern and not a certainty. In Canada, it says, the Green Party took about 5% of the national vote from 2004 to 2011 and won only one of the 308 seats in the House of Commons, which is the mechanical effect in numbers. It also says strong regional parties can win seats even when only two parties compete in each district, as in Canada, the UK and India.

Vote Splitting

Suppose Rae and Sam appeal to much the same voters and Tom stands apart. If those voters divide their first choices between Rae and Sam, neither may beat Tom, even though most voters would rather have either of them than Tom. Here are 100 voters (illustrative numbers):

from collections import Counter # 100 voters, three candidates. Rae and Sam are similar; Tom is a rival. # Illustrative numbers: invented to show a method, not a real election. ballots = ( [("Rae", "Sam", "Tom")] * 28 + [("Sam", "Rae", "Tom")] * 27 + [("Tom", "Sam", "Rae")] * 45 ) counts = Counter(b[0] for b in ballots) print("first choices:", dict(counts)) winner, top = counts.most_common(1)[0] print(f"plurality winner: {winner} with {top / len(ballots):.0%}") rae_v_tom = sum(1 for b in ballots if b.index("Rae") < b.index("Tom")) print(f"Rae vs Tom head to head: {rae_v_tom} to {len(ballots) - rae_v_tom}") sam_v_tom = sum(1 for b in ballots if b.index("Sam") < b.index("Tom")) print(f"Sam vs Tom head to head: {sam_v_tom} to {len(ballots) - sam_v_tom}") # first choices: {'Rae': 28, 'Sam': 27, 'Tom': 45} # plurality winner: Tom with 45% # Rae vs Tom head to head: 55 to 45 # Sam vs Tom head to head: 55 to 45

Tom wins with 45%, but Rae beats Tom 55 to 45 head to head, and so does Sam. In the language of How Voting Systems Work 1, both Rae and Sam would beat Tom in a straight contest; the plurality count never asks. The next question is who is to blame, and for that we withdraw each candidate in turn and recount.

from collections import Counter ballots = ( [("Rae", "Sam", "Tom")] * 28 + [("Sam", "Rae", "Tom")] * 27 + [("Tom", "Sam", "Rae")] * 45 ) def plurality_winner(ballots): """Winner on first choices; ties are listed together, joined by '/'.""" counts = Counter(b[0] for b in ballots) top = max(counts.values()) return "/".join(sorted(c for c, v in counts.items() if v == top)) def without(ballots, candidate): """The same ballots with one candidate struck off every ranking.""" return [tuple(c for c in b if c != candidate) for b in ballots] print("all three stand:", plurality_winner(ballots)) for gone in ("Rae", "Sam", "Tom"): print(f"{gone} withdraws:", plurality_winner(without(ballots, gone))) # all three stand: Tom # Rae withdraws: Sam # Sam withdraws: Rae # Tom withdraws: Sam

With all three standing Tom wins. If either Rae or Sam withdraws, the other takes the withdrawn candidate’s voters and wins. Rae and Sam are each a spoiler: the Wikipedia article on the spoiler effect describes it as a losing candidate affecting the result “simply by participating” (as fetched). Tom withdrawing changes the winner too, but Tom is the winner, so this is not a spoiler case. The article cites the 2000 US presidential election, where it says Ralph Nader’s campaign is widely cited as a spoiler, but the fetched text gave no vote totals, so none are quoted here (check). It also gives a 2009 mayoral election in Burlington, Vermont, with head-to-head figures of 4,064 to 3,476; that example involves instant-runoff counting, which How Voting Systems Work 3 covers, and I did not verify the figures beyond that one summary (check).

SituationPlurality resultHead-to-head result
Rae and Sam both standTom (45 first places)Rae beats Tom 55 to 45; Sam beats Tom 55 to 45
Only Rae and Tom standRae, 55 to 45, if Sam’s voters back Rae (assumed)Rae
Only Sam and Tom standSam, 55 to 45, if Rae’s voters back Sam (assumed)Sam

Simulating Three-Way Races

One hand-made example proves little, so we need a model that can produce thousands of elections. The simplest is a line from 0 (left) to 1 (right). Voters sit at random points on the line, candidates sit at positions, and each voter ranks the candidates by distance, nearest first. This one-dimensional line is an assumed model; real issues have many dimensions. With voters spread along a line, the candidate nearest the middle voter should win head to head (the median-voter idea; I did not confirm it from a source here, so treat it as a property of the model, (check)). The code searches for the Condorcet winner directly, so it does not rely on that idea.

import random from collections import Counter def run_race(positions, voter_points): """One-dimensional race: each voter ranks candidates by distance.""" ballots = [tuple(sorted(positions, key=lambda c: abs(positions[c] - v))) for v in voter_points] counts = Counter(b[0] for b in ballots) top = max(counts.values()) plural = sorted(c for c, n in counts.items() if n == top) names = sorted(positions) cond = None 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): cond = x return plural, cond random.seed(2) voters = [random.random() for _ in range(101)] # 101 voters spread over 0..1 positions = {"Rae": 0.30, "Sam": 0.40, "Tom": 0.85} plural, cond = run_race(positions, voters) firsts = Counter(min(positions, key=lambda c: abs(positions[c] - v)) for v in voters) print("first choices:", dict(sorted(firsts.items()))) print("plurality:", plural, "| Condorcet winner:", cond) # first choices: {'Rae': 30, 'Sam': 28, 'Tom': 43} # plurality: ['Tom'] | Condorcet winner: Sam

Rae and Sam split the left and centre, Tom collects the right with 43 of 101 first places and wins, but Sam, who sits nearest the middle, beats every rival head to head. Now repeat this with random candidate positions and count how often plurality names someone other than the head-to-head winner:

import random from collections import Counter def run_race(positions, voter_points): ballots = [tuple(sorted(positions, key=lambda c: abs(positions[c] - v))) for v in voter_points] counts = Counter(b[0] for b in ballots) top = max(counts.values()) plural = sorted(c for c, n in counts.items() if n == top) names = sorted(positions) cond = None 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): cond = x return plural, cond random.seed(31) trials = 2000 for k in (2, 3, 4, 5): differ = 0 for _ in range(trials): voters = [random.random() for _ in range(101)] positions = {f"C{i}": random.random() for i in range(k)} plural, cond = run_race(positions, voters) if plural != [cond]: differ += 1 print(f"{k} candidates: plurality differs from the Condorcet winner in {differ / trials:.1%} of {trials} races") # 2 candidates: plurality differs from the Condorcet winner in 0.0% of 2000 races # 3 candidates: plurality differs from the Condorcet winner in 35.8% of 2000 races # 4 candidates: plurality differs from the Condorcet winner in 54.0% of 2000 races # 5 candidates: plurality differs from the Condorcet winner in 65.3% of 2000 races

With two candidates the rules cannot disagree. With three, they disagree in roughly a third of the simulated races, and the rate climbs as candidates are added: 35.8%, 54.0% and 65.3% for three, four and five. The sample is 2000 seeded races per row, so the exact figures belong to this seed and model; the trend, not the digits, is the finding. This is a simulation of an assumed model and says nothing about the frequency in real elections.

Votes Versus Seats

Plurality is usually applied district by district, and a party that is second or third everywhere can win few seats or none. Here are ten districts of 100 voters each with three parties, using invented numbers:

# Votes versus seats: ten single-member districts, 100 voters each. # Illustrative numbers: invented to show a method, not a real election. districts = [ {"Red": 40, "Blue": 35, "Green": 25}, {"Red": 38, "Blue": 36, "Green": 26}, {"Red": 42, "Blue": 33, "Green": 25}, {"Red": 37, "Blue": 38, "Green": 25}, {"Red": 39, "Blue": 34, "Green": 27}, {"Red": 36, "Blue": 39, "Green": 25}, {"Red": 41, "Blue": 34, "Green": 25}, {"Red": 38, "Blue": 37, "Green": 25}, {"Red": 40, "Blue": 35, "Green": 25}, {"Red": 35, "Blue": 38, "Green": 27}, ] votes = {p: sum(d[p] for d in districts) for p in ("Red", "Blue", "Green")} seats = {p: 0 for p in votes} for d in districts: seats[max(d, key=d.get)] += 1 total_votes = sum(votes.values()) total_seats = sum(seats.values()) for p in votes: print(f"{p:5s} votes {votes[p] / total_votes:6.1%} seats {seats[p]:2d} of {total_seats} ({seats[p] / total_seats:.0%})") # Red votes 38.6% seats 7 of 10 (70%) # Blue votes 35.9% seats 3 of 10 (30%) # Green votes 25.5% seats 0 of 10 (0%)
PartyShare of votesShare of seatsWhat happened
Red38.6%70%first in seven districts, mostly by a few votes
Blue35.9%30%close in nearly every district, first in three
Green25.5%0%about a quarter of the electorate, no representation

Green’s 255 votes elected nobody, which is what critics mean by wasted votes. A Green voter who understands this may vote for Blue or Red instead (the psychological effect in the Duverger table), and Exercise 3 asks what that does to the seats.

Common mistakes
Blaming a spoiler candidate for a result when the rule caused it (the same ballots under a different rule would not split); assuming plurality always misses the head-to-head winner (it often agrees, and with two candidates it always does); treating simulation rates from an assumed one-line model as measurements of real elections; reading seat shares as vote shares (they can differ a great deal); and quoting Duverger’s law as an invariant rule when the source calls it a tendency.
Exercise 1

A club of 21 members ranks four candidates (assumed numbers). Write a program that finds the plurality winner, then withdraws each other candidate in turn and reports whether the winner changes. Which candidates are spoilers?

๐Ÿ“„ View solution
Exercise 2

Use the one-line model with 101 random voters. For 3, 4 and 5 candidates, simulate 3000 seeded races and count how often removing one losing candidate changes the plurality winner. Does adding more candidates make spoilers more likely, or does the rate stay roughly flat?

๐Ÿ“„ View solution
Exercise 3

Take the ten-district electorate from this chapter. Move k Green voters in every district to their second choice, Blue, for k from 0 to 10 in steps of 2, and report the seats each time. At what k does Blue overtake Red, and what does that say about the incentive to vote tactically?

๐Ÿ“„ View solution

Quick Reference

  • Plurality / first past the post: one mark per voter, most marks wins, no majority needed
  • Vote splitting: similar candidates share voters and let a rival win with less than half
  • Spoiler: a losing candidate whose presence changes who wins
  • Duverger’s law: a tendency toward two parties under single-member plurality; mechanical and psychological effects
  • Wasted votes: votes that elect nobody, which can leave a quarter of voters without a seat
  • Python habit: write a without(ballots, candidate) helper, then rerun the count to test a spoiler
  • Simulations: state the model, seed the random generator and treat rates as trends, not facts about real elections
Coming next
How Voting Systems Work 3 is about majorities and runoffs: two-round runoffs, instant-runoff (ranked choice) voting, and counting transfers step by step in Python.