Paradoxes and Limits

How Voting Systems Work

Chapter 6 ยท Paradoxes and Limits

Every method in How Voting Systems Work 2 to 5 had a weak spot: vote splitting for plurality, the centre squeeze for runoffs, cycles for pairwise contests, easy point-shading for Borda. It is natural to ask whether a cleverer rule could have none of them. The mathematical answer, for ranked ballots, is a discouraging “not quite”. This chapter looks at the two best-known results, Arrow’s theorem (about fairness) and the Gibbard–Satterthwaite theorem (about honesty), and then does something more useful than quoting them: it writes small programs that test each method against named fairness criteria and count how often it fails. The maths stays at counting and comparing.

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 the theorems come from two web searches whose results were mostly lecture notes, encyclopedia mirrors and an advocacy group’s article; fetched pages are often truncated or model-summarised, so treat the wording as a paraphrase, not a quotation. The sources broadly agree on the theorems; one search result stated the list of Arrow’s conditions only partly, so that list is from general knowledge (check), and the advocacy source argues that one failure matters little, which I mark below as one side of a dispute. The ballots in the code are assumed or illustrative, and the random elections use a model (every ballot equally likely) that real politics does not follow. Statements marked (check) were not confirmed against a source.

The Theorems in Plain Words

Arrow’s theorem, published by the economist Kenneth Arrow in his 1951 book Social Choice and Individual Values, concerns rules that turn everyone’s full rankings of three or more options into one ranking for the group. In the usual statement, no such rule satisfies all of these at once: it works for every possible set of ballots (unrestricted domain); if everyone prefers X to Y then the group does (unanimity, or Pareto efficiency); the group’s order of X and Y depends only on how voters ordered X and Y (independence of irrelevant alternatives, IIA); and no single voter always decides (non-dictatorship). The sources I found agree on the result and the 1951 date; the exact wording of the four conditions varies between textbooks. They also note the theorem can be avoided by restricting the ballots allowed, for example to preferences that follow a single left-right line.

The Gibbard–Satterthwaite theorem (credited to Allan Gibbard in 1973 and Mark Satterthwaite in 1975) is about honesty. Roughly: with three or more possible winners, any rule that can elect any of them and is not a dictatorship will sometimes give some voter a reason to put down a ballot that is not their true ranking. Neither theorem says every method is equally bad, or that failures are common. They say a perfect method cannot exist. How often and how badly each one fails is a question for counting, which is what the rest of this chapter does.

IdeaPlain meaningWhere we met it
Majority criterionsomeone ranked first by more than half the voters should winHow Voting Systems Work 5 (Borda fails)
Condorcet criteriona candidate who beats every rival one-on-one should winHow Voting Systems Work 4
Monotonicityranking a winner higher should never make it losethis chapter
IIAthe group’s order of two options depends only on voters’ order of those twoHow Voting Systems Work 2 and 5 (near-copies)
Strategy-proofnessno voter ever gains by lyingthis chapter

Independence: Test It by Code

A criterion becomes testable once you state it as a check on two profiles. For IIA: take a profile, change only where one third candidate (Cy) appears on some ballots, so that no voter’s Ana-versus-Bo order moves, and see whether the method’s verdict on Ana versus Bo flips. The code below does that for the Borda count with five invented voters. It tries every way of moving Cy on the two ballot types and prints the first one that flips the verdict.

from itertools import product def borda(ballots): """Borda points: with k candidates, first place earns k-1 points and last place earns 0.""" k = len(ballots[0]) points = {c: 0 for c in ballots[0]} for ballot in ballots: for place, c in enumerate(ballot): points[c] += k - 1 - place return points def move(ballot, who, slot): """Return the ballot with `who` taken out and re-inserted at position `slot`.""" rest = [c for c in ballot if c != who] rest.insert(slot, who) return tuple(rest) # Illustrative numbers: five invented voters in two groups. Only Cy moves; Ana versus Bo never changes. groups = [(3, ("Ana", "Bo", "Cy")), (2, ("Bo", "Cy", "Ana"))] before = [b for n, b in groups for _ in range(n)] print("before:", borda(before), "-> Ana vs Bo winner:", "Ana" if borda(before)["Ana"] > borda(before)["Bo"] else "Bo") for slots in product(range(3), repeat=len(groups)): after = [move(b, "Cy", s) for (n, b), s in zip(groups, slots) for _ in range(n)] pts = borda(after) if (pts["Ana"] > pts["Bo"]) != (borda(before)["Ana"] > borda(before)["Bo"]): print("after: ", pts, "-> Ana vs Bo winner:", "Ana" if pts["Ana"] > pts["Bo"] else "Bo", "| Cy slots:", slots) break # before: {'Ana': 6, 'Bo': 7, 'Cy': 2} -> Ana vs Bo winner: Bo # after: {'Cy': 10, 'Ana': 3, 'Bo': 2} -> Ana vs Bo winner: Ana | Cy slots: (0, 0)

Before, Bo wins the Borda count 7 to 6 over Ana. Then Cy is moved to the top of both kinds of ballot, and the order of Ana and Bo in each ballot is unchanged (three voters still prefer Ana to Bo, two still prefer Bo). Yet Ana now beats Bo 3 to 2 on points. The voters said nothing new about Ana or Bo; the points changed because Cy’s position changed. That is an IIA failure. The Borda count is not alone: one search result said plurality and ranked runoffs fail IIA as well, which How Voting Systems Work 2 showed in the vote-splitting form (check).

Strategic Voting

The oldest tactic is the compromise vote under plurality. Imagine 100 invented voters: 40 prefer Ana, 35 prefer Bo, 25 prefer Cy and would rank Bo second. If everyone is honest Ana wins. If the 25 move to Bo, Bo wins.

from collections import Counter def plurality(ballots): tally = Counter(b[0] for b in ballots) return max(sorted(tally), key=tally.get) # Illustrative numbers: 100 invented voters. Cy cannot win; Ana and Bo are close. honest = ([("Ana", "Bo", "Cy")] * 40 + [("Bo", "Ana", "Cy")] * 35 + [("Cy", "Bo", "Ana")] * 25) print("honest tally: ", dict(sorted(Counter(b[0] for b in honest).items())), "->", plurality(honest)) # The 25 Cy voters prefer Bo to Ana. If they vote for Bo instead of Cy ("compromise"): shifted = ([("Ana", "Bo", "Cy")] * 40 + [("Bo", "Ana", "Cy")] * 35 + [("Bo", "Cy", "Ana")] * 25) print("compromise vote:", dict(sorted(Counter(b[0] for b in shifted).items())), "->", plurality(shifted)) # Is that better for those 25 voters by their own honest ranking? rank = ("Cy", "Bo", "Ana") print("their view:", rank.index(plurality(honest)), "(place of honest winner) vs", rank.index(plurality(shifted)), "(place of new winner); lower is better") # honest tally: {'Ana': 40, 'Bo': 35, 'Cy': 25} -> Ana # compromise vote: {'Ana': 40, 'Bo': 60} -> Bo # their view: 2 (place of honest winner) vs 1 (place of new winner); lower is better

The numbers are zero-based places: the honest winner, Ana, was last on the 25 Cy voters’ true ballots (place 2), and Bo, their second choice, is place 1. By lying, the 25 voters swapped an outcome they liked least for one they liked more. Unless they are sure of the other voters’ plans this is a gamble, and a poll can persuade many people to make the same move. Another tactic, burying, is to rank a dangerous rival last on purpose; the exercises below try it under the Borda count.

Common mistakes
Reading the theorems as “all voting systems are equally bad”; treating one invented example as the rate at which a method fails; forgetting that an alphabetical tie-break (used in the code) is itself a rule that can decide results; and assuming a method that is hard to manipulate by one voter is also hard for a coordinated group.

Counting Failures

Now the whole test bench. The code draws 3,000 random elections of nine voters and three candidates, each ballot chosen uniformly from the six possible orders. When a Condorcet winner exists, it checks whether plurality, Borda and instant-runoff elected that candidate; when some candidate has a majority of first choices it checks the same. Ties are broken alphabetically, which is an assumption of this chapter.

import random from collections import Counter from itertools import permutations def plurality(ballots): tally = Counter(b[0] for b in ballots) return max(sorted(tally), key=tally.get) def borda(ballots): k = len(ballots[0]) points = {c: 0 for c in sorted(ballots[0])} for b in ballots: for place, c in enumerate(b): points[c] += k - 1 - place return max(points, key=points.get) def irv(ballots): alive = set(ballots[0]) while True: tally = Counter({c: 0 for c in alive}) for b in ballots: tally[next(c for c in b if c in alive)] += 1 top = max(sorted(tally), key=tally.get) if tally[top] * 2 > len(ballots) or len(alive) == 1: return top alive.remove(min(sorted(tally), key=tally.get)) methods = {"plurality": plurality, "Borda": borda, "instant-runoff": irv} random.seed(6) names, voters, trials = ("Ana", "Bo", "Cy"), 9, 3000 orders = list(permutations(names)) fails = Counter() has = Counter() for _ in range(trials): ballots = [random.choice(orders) for _ in range(voters)] # Condorcet winner, if any cw = next((x for x in names if all(sum(b.index(x) < b.index(y) for b in ballots) * 2 > voters for y in names if y != x)), None) first = Counter(b[0] for b in ballots) maj = next((c for c, n in first.items() if n * 2 > voters), None) has['Condorcet'] += cw is not None has['majority'] += maj is not None for label, rule in methods.items(): w = rule(ballots) if cw and w != cw: fails[label, "Condorcet"] += 1 if maj and w != maj: fails[label, "majority"] += 1 for crit in ("Condorcet", "majority"): print(f"{crit} criterion: {has[crit]} of {trials} elections have such a winner; failures:", {label: fails[label, crit] for label in methods}) # Condorcet criterion: 2775 of 3000 elections have such a winner; failures: {'plurality': 514, 'Borda': 227, 'instant-runoff': 109} # majority criterion: 1298 of 3000 elections have such a winner; failures: {'plurality': 0, 'Borda': 50, 'instant-runoff': 0}

A Condorcet winner existed in 2,775 of 3,000 elections. Plurality missed it in 514 of those (about 18.5%), Borda in 227 (about 8.2%) and instant-runoff in 109 (about 3.9%). A first-choice majority winner existed in 1,298 elections; plurality and instant-runoff always chose it, and Borda missed it in 50 (about 3.9%). That matches the textbook picture: plurality and runoffs meet the majority criterion, Borda does not, and none of the three always meets the Condorcet criterion. But the percentages belong to this random-ballot model, in which voters have no shared political shape at all. Real electorates are more structured, and the rates would change.

One Voter Lying

The Gibbard–Satterthwaite theorem says opportunities to gain by lying exist. This simulation measures how often a single voter could use one. For each of 1,000 random elections of seven voters and three candidates, it tries every possible ballot for every voter, keeping the others fixed, and asks whether any lie leads to a winner that voter truly ranks higher.

import random from collections import Counter from itertools import permutations def plurality(ballots): tally = Counter(b[0] for b in ballots) return max(sorted(tally), key=tally.get) def borda(ballots): k = len(ballots[0]) points = {c: 0 for c in sorted(ballots[0])} for b in ballots: for place, c in enumerate(b): points[c] += k - 1 - place return max(points, key=points.get) def irv(ballots): alive = set(ballots[0]) while True: tally = Counter({c: 0 for c in alive}) for b in ballots: tally[next(c for c in b if c in alive)] += 1 top = max(sorted(tally), key=tally.get) if tally[top] * 2 > len(ballots) or len(alive) == 1: return top alive.remove(min(sorted(tally), key=tally.get)) def can_gain(rule, ballots): """True if some single voter can get a better winner (by their true ranking) by lying.""" honest = rule(ballots) for i, truth in enumerate(ballots): for lie in permutations(truth): trial = ballots[:i] + [lie] + ballots[i + 1:] if truth.index(rule(trial)) < truth.index(honest): return True return False random.seed(6) names, voters, trials = ("Ana", "Bo", "Cy"), 7, 1000 orders = list(permutations(names)) count = Counter() for _ in range(trials): ballots = [random.choice(orders) for _ in range(voters)] for label, rule in (("plurality", plurality), ("Borda", borda), ("instant-runoff", irv)): count[label] += can_gain(rule, ballots) for label in ("plurality", "Borda", "instant-runoff"): print(f"{label:<15} a lone voter can gain by lying in {count[label] / trials:5.1%} of {trials} elections") # plurality a lone voter can gain by lying in 25.3% of 1000 elections # Borda a lone voter can gain by lying in 24.3% of 1000 elections # instant-runoff a lone voter can gain by lying in 11.2% of 1000 elections

Plurality had a profitable lie in 25.3% of elections, Borda in 24.3%, and instant-runoff in 11.2%. Instant-runoff did better here, in line with a source I found saying it resists some tactics such as burying, but note what the test counts: only a lone voter, only three candidates, only seven voters. Coordinated groups, more candidates and strategies that need guessing others’ votes are outside it. The result is a measurement of one model, not a verdict on any system.

Monotonicity: a stranger failure

Instant-runoff has a quirk that none of the others shows here: it can fail monotonicity, meaning that a candidate can lose because some voters ranked them higher. Sources I found agree on the failure. The advocacy group FairVote argues it matters little in practice and that other properties matter more; that is one side of a real dispute, and I did not check how often the failure arises in real elections (check). The code searches random 21-voter elections for one, skipping any profile where a tie for last place would need a tie-break, and then lifts the winner one place on more and more of the ballots that do not rank it first.

import random from collections import Counter from itertools import permutations def irv(ballots): """Instant-runoff winner, or None if a tie for last place has to be broken.""" alive = set(ballots[0]) while True: tally = Counter({c: 0 for c in alive}) for b in ballots: tally[next(c for c in b if c in alive)] += 1 top = max(tally.values()) if top * 2 > len(ballots) or len(alive) == 1: return next(c for c in tally if tally[c] == top) low = min(tally.values()) losers = [c for c in tally if tally[c] == low] if len(losers) > 1: return None alive.remove(losers[0]) def raise_one(ballot, who): """Move `who` up one place on this ballot.""" i = ballot.index(who) b = list(ballot) b[i - 1], b[i] = b[i], b[i - 1] return tuple(b) # Illustrative: random 3-candidate profiles (an assumed model); ties are skipped, not broken. random.seed(6) names, voters = ("Ana", "Bo", "Cy"), 21 orders = list(permutations(names)) found = None for _ in range(50000): ballots = [random.choice(orders) for _ in range(voters)] w = irv(ballots) if w is None: continue movable = [i for i, b in enumerate(ballots) if b[0] != w] for r in range(1, len(movable) + 1): trial = [raise_one(b, w) if i in movable[:r] else b for i, b in enumerate(ballots)] result = irv(trial) if result is not None and result != w: found = (ballots, w, r, trial, result) break if found: break if found: ballots, w, r, trial, result = found print("ballots:", dict(sorted(Counter(ballots).items()))) print("winner before:", w) print(f"{r} ballots lift {w} one place -> new winner:", result) print("first choices before:", dict(sorted(Counter(b[0] for b in ballots).items()))) print("first choices after: ", dict(sorted(Counter(b[0] for b in trial).items()))) else: print("no failure found") # ballots: {('Ana', 'Bo', 'Cy'): 1, ('Ana', 'Cy', 'Bo'): 5, ('Bo', 'Ana', 'Cy'): 5, ('Bo', 'Cy', 'Ana'): 2, ('Cy', 'Ana', 'Bo'): 5, ('Cy', 'Bo', 'Ana'): 3} # winner before: Cy # 2 ballots lift Cy one place -> new winner: Ana # first choices before: {'Ana': 6, 'Bo': 7, 'Cy': 8} # first choices after: {'Ana': 6, 'Bo': 5, 'Cy': 10}

Before, Ana is eliminated first (6 first choices against 7 and 8). Ana’s five “Ana, Cy, Bo” ballots move to Cy, so Cy wins with 13. After two ballots lift Cy a place, so that Cy has 10 first choices, Bo (5) is eliminated instead, Bo’s remaining five voters all rank Ana next, and Ana wins 11 to 10. Cy gained support and lost. The cause is that the candidate who is removed first changes with the first-choice counts, so extra first choices can move who is eliminated and where those votes then go.

Exercise 1

Thirteen voters rank four candidates (assumed numbers). For plurality, Borda and instant-runoff, find the winner, then drop each other candidate in turn and report whether the winner changes. Which methods pass this independence test on this profile?

๐Ÿ“„ View solution
Exercise 2

Nine voters rank three candidates (assumed numbers). Show that under the Borda count one voter can gain by reordering their own ballot. Print the voter’s lie and the points before and after, and say what the tie-break did.

๐Ÿ“„ View solution
Exercise 3

Run 3,000 seeded elections of nine voters and three candidates with random ballots. In each, lift the winner to first place on more and more of the ballots that do not rank it first, and count the elections where the winner then loses, for plurality, Borda and instant-runoff.

๐Ÿ“„ View solution

Quick Reference

  • Arrow’s theorem: for three or more options, no ranked rule has unrestricted domain, unanimity, IIA and non-dictatorship together (1951)
  • Gibbard–Satterthwaite: a non-dictatorial rule with three or more possible winners lets some voter gain by lying (1973 and 1975)
  • Majority criterion: plurality and instant-runoff satisfy it; the Borda count does not
  • Condorcet criterion: plurality, Borda and instant-runoff all can fail it
  • Monotonicity: instant-runoff can fail it; plurality did not in the test
  • Compromise vote and burying: the plurality and Borda tactics shown above
  • Always state: the ballot model, number of voters and candidates, and the tie-break rule when quoting a failure rate
Coming next
How Voting Systems Work 7 is about dividing seats: apportionment, the Hamilton, Jefferson, Webster and Huntington-Hill methods, and the Alabama paradox.