With 5 candidates, there are 10 comparisons to make:
# Math in Society 2.10 -- Copeland's Method, and every method side by side.
# One election, five ways of counting it. EDIT any column and re-run.
from itertools import combinations
base = [(3, "BCADE"), (4, "CADBE"), (4, "BDCAE"), (6, "DCAEB"), (2, "BEACD"), (1, "EADBC")]
# The 6 D-voters swap their top two, promoting C over D. Nothing else changes.
tweak = [(n, "CDAEB" if r == "DCAEB" else r) for n, r in base]
def names(s): return sorted({c for _, r in s for c in r})
def head_to_head(s, x, y):
a = sum(n for n, r in s if x in r and (y not in r or r.index(x) < r.index(y)))
b = sum(n for n, r in s if y in r and (x not in r or r.index(y) < r.index(x)))
return a, b
def plurality(s):
t = {c: 0 for c in names(s)}
for n, r in s: t[r[0]] += n
return max(t, key=lambda c: (t[c], c))
def borda(s):
cs = names(s); t = {c: 0 for c in cs}
for n, r in s:
for i, c in enumerate(r): t[c] += (len(cs) - i) * n
for c in cs:
if c not in r: t[c] += n # unranked candidates get the last-place point
return max(t, key=lambda c: (t[c], c)), t
def irv(s):
alive = set(names(s))
while True:
t = {c: 0 for c in alive}
for n, r in s:
for c in r:
if c in alive: t[c] += n; break
lead = max(t, key=lambda c: (t[c], c))
if t[lead] * 2 > sum(t.values()) or len(alive) == 1: return lead
low = min(t.values())
alive.discard(sorted(c for c in alive if t[c] == low)[0])
def copeland(s):
cs = names(s); pts = {c: 0.0 for c in cs}; wins = {c: 0 for c in cs}; log = []
for x, y in combinations(cs, 2):
a, b = head_to_head(s, x, y)
if a > b: pts[x] += 1; wins[x] += 1; note = f"{x} gets 1 point"
elif b > a: pts[y] += 1; wins[y] += 1; note = f"{y} gets 1 point"
else: pts[x] += .5; pts[y] += .5; note = "tie: 1/2 point each"
log.append(f" {x} vs {y}: {a:>3} to {b:<3} {note}")
cond = [c for c in cs if wins[c] == len(cs) - 1]
return max(pts, key=lambda c: (pts[c], c)), pts, log, (cond[0] if cond else None)
def report(s, title):
cw, pts, log, cond = copeland(s)
bw, bt = borda(s)
print(f"{title} ({sum(n for n, _ in s)} ballots)")
print("\n".join(log))
print(" Copeland points: " + ", ".join(f"{c}={pts[c]:g}" for c in names(s)))
print(" Borda points: " + ", ".join(f"{c}={bt[c]}" for c in names(s)))
print(f" plurality {plurality(s)} IRV {irv(s)} Borda {bw} "
f"Copeland {cw} Condorcet {cond or 'none'}\n")
report(base, "Original advertising vote")
report(tweak, "After the 6-voter block promotes C over D")
print("""Four defensible methods, three different winners, one set of ballots -- and no
Condorcet winner at all, since D loses to nobody but only ties A. Copeland still
returns an answer where the Condorcet criterion cannot. In the second block, six
voters reordering their top two moves plurality, IRV and Copeland all onto C.
Try it: replace `base` with the vacation club from the start of the chapter,
base = [(1, 'AOH'), (3, 'AHO'), (3, 'OHA'), (3, 'HAO')]
Plurality elects Anaheim; Copeland elects Hawaii, the Condorcet winner.""")
Totaling these up:
A gets points
B gets points
C gets points
D gets points
E gets points
Using Copeland’s Method, we declare D as the winner.
Notice that in this case, D is not a Condorcet Winner. While Copeland’s method will also select a Condorcet Candidate as the winner, the method still works in cases where there is no Condorcet Winner.