Points and Scores

How Voting Systems Work

Chapter 5 ยท Points and Scores

How Voting Systems Work 4 compared candidates two at a time. This chapter takes the other obvious route: give every candidate points and add them up. The oldest scheme of that kind is the Borda count, usually credited to the French mathematician Jean-Charles de Borda, who put it to the French Academy of Sciences in 1770 as a fairer alternative to plurality voting. Its rival is a family of rules where voters do not rank at all but approve or score. A story often told about Borda is that he said his scheme was meant only for honest men; I have not confirmed that quotation (check). It points at the real issue, though: once points are involved, the voter has a way to shade the points. The maths here is addition and the occasional fraction.

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 methods come from two web searches whose results were mostly encyclopedia-style pages, a maths textbook and a voting-reform group’s comparison chart; fetched pages are often truncated or model-summarised, so treat the wording as a paraphrase, not a quotation. The sources can differ on details, and I mark where. The elections, candidates, ballots and scores in the code are assumed or illustrative: invented to show a method, not results from any real vote. The simulation rests on a simple model that real politics does not follow. Statements marked (check) were not confirmed against a source.

Counting Places: the Borda Count

In the Borda count each voter ranks all k candidates, and a ballot gives k-1 points to the first choice, k-2 to the second, and so on down to 0 for last place. Add the points over all ballots; the highest total wins. With three candidates that is 2, 1 and 0. Here it is on the centre-squeeze profile that appeared in the last two chapters.

from collections import Counter def borda(ballots): """Borda points: with k candidates, first place earns k-1 points and last place earns 0.""" k = len(ballots[0]) points = Counter() for ballot in ballots: for place, candidate in enumerate(ballot): points[candidate] += k - 1 - place return dict(sorted(points.items())) # Illustrative numbers: the centre-squeeze profile from the last two chapters, invented to show a method. squeeze = ( [("Ana", "Bo", "Cy")] * 35 + [("Cy", "Bo", "Ana")] * 33 + [("Bo", "Ana", "Cy")] * 16 + [("Bo", "Cy", "Ana")] * 16 ) points = borda(squeeze) print("first choices:", dict(sorted(Counter(b[0] for b in squeeze).items()))) print("Borda points: ", points) print("Borda winner: ", max(points, key=points.get)) # first choices: {'Ana': 35, 'Bo': 32, 'Cy': 33} # Borda points: {'Ana': 86, 'Bo': 132, 'Cy': 82} # Borda winner: Bo

Bo gets 2 points from 32 first places and 1 point from 68 second places (35 ballots with Bo second behind Ana, 33 behind Cy): 64 + 68 = 132. Ana gets 2 x 35 + 1 x 16 = 86, and Cy gets 2 x 33 + 1 x 16 = 82. Bo, the candidate that pairwise comparison found and that neither runoff rule found, wins on the Borda count. A rule that rewards being ranked high by everyone, not first by a few, naturally favours the compromise candidate. The same trait is why Borda has a reputation for picking broadly acceptable candidates. It is not a guarantee, as the next section shows.

FeaturePluralityBorda countApprovalScore voting
Ballot isone namea full rankinga set of approved namesa number for every candidate
Winner ismost first choicesmost rank pointsmost approvalshighest total (or average) score
Rewardsstrong first-choice supportbeing ranked high by manybeing acceptable to manyhow much voters like a candidate, not just the order
Voter’s main decisionwho to vote forthe order of all candidateswhere to draw the approval linewhat number to give each candidate

When Points Mislead

The sources I found agree that the Borda count can fail the majority criterion (a candidate ranked first by more than half the voters should win), and that it can fail to elect a Condorcet winner. One search also gave a Borda failure of independence of irrelevant alternatives. The first of these is easy to build. Ten invented voters: six rank Ana, Bo, Cy; four rank Bo, Cy, Ana. Ana has 60% of the first choices and beats both rivals head to head, six ballots to four. The code also computes the Dowdall variant, which gives 1 point for first, 1/2 for second, 1/3 for third. Sources I found say it is used in Nauru, with those weights, to elect members in multi-member districts; here I apply it to a single winner only to show how the weights change the result.

from collections import Counter def borda(ballots): """Borda points: with k candidates, first place earns k-1 points and last place earns 0.""" k = len(ballots[0]) points = Counter() for ballot in ballots: for place, candidate in enumerate(ballot): points[candidate] += k - 1 - place return dict(sorted(points.items())) def dowdall(ballots): """Dowdall points: first place earns 1, second 1/2, third 1/3, and so on.""" points = Counter() for ballot in ballots: for place, candidate in enumerate(ballot): points[candidate] += 1 / (place + 1) return {c: round(p, 2) for c, p in sorted(points.items())} # Illustrative numbers: ten invented ballots in which one candidate has a majority of first choices. ballots = [("Ana", "Bo", "Cy")] * 6 + [("Bo", "Cy", "Ana")] * 4 first = Counter(b[0] for b in ballots) print("first choices:", dict(sorted(first.items())), "(10 ballots)") for name, rule in (("Borda", borda), ("Dowdall", dowdall)): scores = rule(ballots) print(f"{name:<8}", scores, "->", max(scores, key=scores.get)) # first choices: {'Ana': 6, 'Bo': 4} (10 ballots) # Borda {'Ana': 12, 'Bo': 14, 'Cy': 4} -> Bo # Dowdall {'Ana': 7.33, 'Bo': 7.0, 'Cy': 4.0} -> Ana

Borda gives Bo 14 points and Ana 12, so Bo wins even though Ana was the first choice of a majority. Ana gets 2 points from each of the six who rank Ana first and 0 from the four who rank Ana last, 12 in all, while Bo gets 1 point from each of the six and 2 from each of the four, 6 + 8 = 14. The Dowdall weights drop faster after first place (1, 0.5, 0.33) than the evenly spaced Borda weights (2, 1, 0), so a first place counts for relatively more, and Ana wins with 7.33 to 7.00. Neither weighting is the correct one. They are different statements about how much second place is worth.

Common mistakes
Calling the Borda winner “the most popular” when it may have fewer first choices than anyone; forgetting that the point weights are a choice (the Dowdall weights are another, and sources describe further variants); mixing up k-1 points for first with k points for first (the winner is the same but the totals are not); and treating one invented example as proof that a method “always” behaves that way.

Adding a near-copy

Because Borda counts places, adding a candidate changes every other candidate’s points, even a candidate who cannot win. Imagine nine voters choosing between Ana and Bo: five prefer Ana and four prefer Bo. Now a near-copy of Bo, called Bo2, joins the race. The voters who like Bo rank Bo first and Bo2 second; the Ana voters rank Bo2 last.

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, candidate in enumerate(ballot): points[candidate] += k - 1 - place return dict(sorted(points.items())) # Illustrative numbers: nine invented ballots; then a near-copy of Bo ("Bo2") is added. before = [("Ana", "Bo")] * 5 + [("Bo", "Ana")] * 4 after = [("Ana", "Bo", "Bo2")] * 5 + [("Bo", "Bo2", "Ana")] * 4 for label, ballots in (("two candidates", before), ("with a near-copy of Bo", after)): scores = borda(ballots) print(f"{label:<24}", scores, "->", max(scores, key=scores.get)) # two candidates {'Ana': 5, 'Bo': 4} -> Ana # with a near-copy of Bo {'Ana': 10, 'Bo': 13, 'Bo2': 4} -> Bo

With two candidates Ana wins 5 to 4. With the near-copy added, nobody changed their mind about Ana versus Bo, but Bo now collects 2 points from each of the four supporters and 1 point (second place) from each of the five Ana voters, 8 + 5 = 13, while Ana stays on 5 x 2 = 10, and Bo wins. Plurality had the opposite weakness in How Voting Systems Work 2: a similar candidate split a vote. Here a similar candidate adds points. In both cases the number of candidates, not just the voters’ opinions, shapes the outcome. This is one reason that anyone able to nominate extra candidates can influence a Borda election, and why How Voting Systems Work 6 looks at strategy more closely.

Approving and Scoring

Two further families drop the ranking. In approval voting each voter marks every candidate they approve of, with no limit, and the candidate with the most approvals wins. In score voting (also called range voting) each voter gives every candidate a number, for example 0 to 10, and the highest total or average wins. I recall some American cities having adopted approval voting for certain elections, with Fargo, North Dakota and St. Louis, Missouri named, but my searches did not confirm it (check). The sources I found say that, unlike ranked rules, approval and score voting can avoid some of Arrow’s trouble with independence of irrelevant alternatives if voters score each candidate on their own fixed scale. They also say a candidate who would lose every one-on-one contest can still win under approval voting. I did not check either claim beyond those summaries.

The key to both is the voter’s private decision. Below, seven invented voters give honest scores from 0 to 10. A score-voting count is simply the total. An approval count needs a rule for turning a score into yes or no, so the code tries four cut-offs: “approve anyone I scored at least 4”, and so on.

# Illustrative numbers: seven invented voters score three candidates from 0 (hate) to 10 (love). scores = { "Voter 1": {"Ana": 10, "Bo": 6, "Cy": 0}, "Voter 2": {"Ana": 10, "Bo": 5, "Cy": 1}, "Voter 3": {"Ana": 9, "Bo": 6, "Cy": 2}, "Voter 4": {"Ana": 0, "Bo": 7, "Cy": 10}, "Voter 5": {"Ana": 1, "Bo": 6, "Cy": 9}, "Voter 6": {"Ana": 2, "Bo": 8, "Cy": 8}, "Voter 7": {"Ana": 3, "Bo": 8, "Cy": 7}, } names = ["Ana", "Bo", "Cy"] top = {c: sum(1 for s in scores.values() if s[c] == max(s.values())) for c in names} print("top-scored by:", top) totals = {c: sum(s[c] for s in scores.values()) for c in names} print("score totals: ", totals, "->", max(totals, key=totals.get)) for cutoff in (4, 6, 8, 9): approvals = {c: sum(1 for s in scores.values() if s[c] >= cutoff) for c in names} best = max(approvals.values()) winners = [c for c in names if approvals[c] == best] print(f"approve at {cutoff}+:", approvals, "->", " and ".join(winners)) # top-scored by: {'Ana': 3, 'Bo': 2, 'Cy': 3} # score totals: {'Ana': 35, 'Bo': 46, 'Cy': 37} -> Bo # approve at 4+: {'Ana': 3, 'Bo': 7, 'Cy': 4} -> Bo # approve at 6+: {'Ana': 3, 'Bo': 6, 'Cy': 4} -> Bo # approve at 8+: {'Ana': 3, 'Bo': 2, 'Cy': 3} -> Ana and Cy # approve at 9+: {'Ana': 3, 'Bo': 0, 'Cy': 2} -> Ana

By score total Bo wins with 46 against 37 for Cy and 35 for Ana, though Bo is the top choice of only two voters (Voter 6 scored Bo and Cy equally, 8 each, which is why the top-scored counts add to eight, not seven). Under approval Bo wins when the line is generous, at 4+ or 6+, since nearly everyone finds Bo acceptable. At 8+ the voters approve only the candidates they love, Bo drops to two approvals, and Ana and Cy tie with three. At 9+ Ana wins alone. The ballots did not change; only where each voter draws the line did. That is the point to keep in mind: approval and score voting reward how much and how widely voters like a candidate, and the result depends on the voters’ own sense of scale. Real voters would not all use the same cut-off, and some might exaggerate scores to help a favourite (an idea for How Voting Systems Work 6).

Which Rule Finds the Centre?

A simulation can compare how often each scoring rule picks the Condorcet winner. It uses the “line” model from earlier: four candidates and 101 voters at random points on a line, with voters ranking by distance. All 2,000 elections had a Condorcet winner, as in How Voting Systems Work 4. The approval rule is that each voter approves candidates within 0.25 of their own position (an arbitrary choice). Score voting gives each candidate a score of minus the distance from the voter, which is an honest and fully consistent scale, a strong assumption.

import random from collections import Counter def pairwise(ballots): names = sorted(ballots[0]) return {(x, y): sum(1 for b in ballots if b.index(x) < b.index(y)) for x in names for y in names if x != y} def condorcet(ballots): wins, n = pairwise(ballots), len(ballots) for x in ballots[0]: if all(wins[x, y] > n / 2 for y in ballots[0] if y != x): return x return None random.seed(31) trials, voters, k = 2000, 101, 4 names = [f"C{i}" for i in range(k)] agree = Counter() found = 0 for _ in range(trials): pos = {c: random.random() for c in names} ups = [random.random() for _ in range(voters)] ballots = [tuple(sorted(names, key=lambda c: abs(pos[c] - v))) for v in ups] cw = condorcet(ballots) if cw is None: continue found += 1 plural = Counter(b[0] for b in ballots) borda = Counter() for b in ballots: for place, c in enumerate(b): borda[c] += k - 1 - place approval = Counter(c for v in ups for c in names if abs(pos[c] - v) <= 0.25) score = {c: -sum(abs(pos[c] - v) for v in ups) for c in names} for label, tally in (("plurality", plural), ("Borda", borda), ("approval (within 0.25)", approval), ("score (nearness)", score)): if max(tally, key=tally.get) == cw: agree[label] += 1 print(f"elections with a Condorcet winner: {found} of {trials}") for label in ("plurality", "Borda", "approval (within 0.25)", "score (nearness)"): print(f"{label:<24} picks it {agree[label] / found:6.1%}") # elections with a Condorcet winner: 2000 of 2000 # plurality picks it 49.6% # Borda picks it 84.4% # approval (within 0.25) picks it 57.2% # score (nearness) picks it 95.9%

Plurality finds the Condorcet winner about half the time in this model (49.6%), Borda about 84%, a fixed-width approval rule about 57%, and score voting by nearness about 96%. Take that with care. The line model is far tidier than any electorate; the gap between score voting and the others depends on every voter using the same honest scale, and the approval figure moves if the 0.25 width changes. The lesson is not “score voting is best”. It is that a scoring rule keeps information a plain first-choice count throws away, and whether that information is reliable depends on how voters behave.

Exercise 1

Twenty-five members rank five candidates (assumed numbers). Compute the plurality, Borda and Dowdall scores, name each method’s winner, and say whether all three agree.

๐Ÿ“„ View solution
Exercise 2

Eight voters score three candidates from 0 to 10 (assumed numbers). Find the score-voting winner by total, then run approval voting at every cut-off from 1 to 10 and list the cut-offs at which approval does not give the score winner alone.

๐Ÿ“„ View solution
Exercise 3

Run 3,000 seeded elections of five candidates and 51 voters under two assumed models (the line and random ballots). When a Condorcet winner exists, count how often plurality and how often Borda pick it, and print the share of elections that have one.

๐Ÿ“„ View solution

Quick Reference

  • Borda count: k-1 points for first, down to 0 for last; the most points wins
  • Dowdall: 1, 1/2, 1/3, ... for first, second, third; a Borda variant with a steeper drop
  • Approval voting: mark any number of candidates; the most approvals wins
  • Score voting: give each candidate a number; the highest total or average wins
  • Borda weakness: can elect someone with no majority of first choices, and an added near-copy can change the winner
  • Approval and score weakness: the result depends on each voter’s own cut-off or scale
  • Always state: the point weights, the approval cut-off or score range, and how ties are broken
Coming next
How Voting Systems Work 6 is about paradoxes and impossibility: Arrow’s theorem, strategic voting, and testing voting methods against fairness criteria by code.