Consider a small country with 5 states, two of which are much larger than the others. We need to apportion 70 representatives. We will apportion using both Webster’s method and the Huntington-Hill method.
# Math in Society 4.5 -- One population table, every apportionment method
# Hamilton and Lowndes share out leftovers; Jefferson, Adams, Webster and
# Huntington-Hill hunt for a modified divisor. Here the divisor methods are run in
# their equivalent "hand out one seat at a time to the highest priority" form.
from math import sqrt
pops = {"A": 300500, "B": 200000, "C": 50000, "D": 38000, "E": 21500}
SEATS = 70 # EDIT: the house size, or edit the populations above
def leftover_method(p, s, lowndes):
divisor = sum(p.values()) / s
quota = {k: v / divisor for k, v in p.items()}
got = {k: int(q) for k, q in quota.items()}
rank = (lambda k: (-(quota[k] - got[k]) / max(got[k], 1), k)) if lowndes \
else (lambda k: (-(quota[k] - got[k]), k))
for k in sorted(p, key=rank)[:s - sum(got.values())]:
got[k] += 1
return got
def divisor_method(p, s, priority):
got = {k: 0 for k in p}
for _ in range(s):
got[max(p, key=lambda k: (priority(p[k], got[k]), p[k]))] += 1
return got
BIG = float("inf")
methods = {
"Hamilton": lambda p, s: leftover_method(p, s, False),
"Lowndes": lambda p, s: leftover_method(p, s, True),
"Jefferson": lambda p, s: divisor_method(p, s, lambda w, n: w / (n + 1)),
"Adams": lambda p, s: divisor_method(p, s, lambda w, n: BIG if n == 0 else w / n),
"Webster": lambda p, s: divisor_method(p, s, lambda w, n: w / (n + 0.5)),
"Hunt-Hill": lambda p, s: divisor_method(p, s,
lambda w, n: BIG if n == 0 else w / sqrt(n * (n + 1))),
}
std = sum(pops.values()) / SEATS
names = list(pops)
print(f"{SEATS} seats, standard divisor {sum(pops.values()):,} / {SEATS} = {std:,.3f}\n")
print(f"{'':<11}" + "".join(f"{k:>9}" for k in names) + f"{'total':>9}")
print(f"{'population':<11}" + "".join(f"{pops[k]:>9,}" for k in names))
print(f"{'quota':<11}" + "".join(f"{pops[k]/std:>9.3f}" for k in names))
print("-" * (11 + 9 * (len(names) + 1)))
results = {}
for m, fn in methods.items():
results[m] = fn(pops, SEATS)
flags = [k for k in names if not (pops[k]/std - 1 < results[m][k] < pops[k]/std + 1)]
note = " quota rule VIOLATED: " + ", ".join(flags) if flags else ""
print(f"{m:<11}" + "".join(f"{results[m][k]:>9}" for k in names)
+ f"{sum(results[m].values()):>9}{note}")
print("\nSame people, same seats, different answers:")
for k in names:
answers = {results[m][k] for m in methods}
if len(answers) > 1:
print(f" State {k}: quota {pops[k]/std:>7.3f}"
f" -> anywhere from {min(answers)} to {max(answers)} seats")
print("\nAdams rounds everything up and so leans small; Jefferson cuts everything off")
print("and so leans large; Webster and Huntington-Hill sit in between, and here they")
print("disagree by one seat -- Webster gives it to the largest state, Huntington-Hill")
print("to the smallest.")
print("Try it: set SEATS = 71 and re-run, then 69. The disagreements move around.")