voting/voting/apportionment.py

202 wiersze
6.9 KiB
Python

"""Apportionment methods."""
from math import ceil
from math import floor
from math import modf
from math import sqrt
from operator import itemgetter
def adams(votes, seats):
"""Apportion seats using the Adams method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
divisor = 1.0 * sum(votes) / seats
decs, lower = zip(*[modf(1.0 * v / divisor) for v in votes])
upper = [ceil(1.0 * v / divisor) for v in votes]
surplus = int(sum(upper) - seats)
while surplus > 0:
# divisors that would remove another seat to each group
divisors = [1.0 * vote / max(floor(allocated + dec), 1) for allocated, dec, vote in zip(lower, decs, votes)]
# argsort low to high
divs = [i[0] for i in sorted(enumerate(divisors), key=itemgetter(1))]
for k in divs:
if divisors[k] > divisor:
idx = k
break
# if they're all zero, subtract from the largest group
else:
largest = [i[0] for i in sorted(enumerate(votes), key=itemgetter(1))]
idx = largest[-1]
divisor = divisors[idx]
decs, lower = zip(*[modf(1.0 * v / divisor) for v in votes])
upper = [ceil(1.0 * v / divisor) for v in votes]
surplus = int(sum(upper) - seats)
return upper
def dhondt(votes, seats):
"""Apportion seats using the D'Hondt method.
Identical to the Jefferson method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
return jefferson(votes, seats)
def hagenbach_bischoff(votes, seats):
"""Apportion seats using the Hagenbach-Bischoff method.
Identical to the Jefferson method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
return jefferson(votes, seats)
def hamilton(votes, seats):
"""Apportion seats using the Hamilton method.
Known also as the Vinton method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
divisor = 1.0 * sum(votes) / seats
decs, lower = zip(*[modf(1.0 * v / divisor) for v in votes])
lower = [int(l) for l in lower]
unallocated = int(seats - sum(lower))
if unallocated > 0:
# argsort high to low
leftovers = [i[0] for i in sorted(enumerate(decs), key=itemgetter(1))][::-1]
for k in range(unallocated):
lower[leftovers[k]] += 1
return lower
def huntington_hill(votes, seats):
"""Apportion seats using the Huntington-Hill method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
if seats < len(votes):
raise ValueError("Must be at least one seat per group.")
elif seats == len(votes):
return [1] * len(votes)
divisor = 1.0 * sum(votes) / seats
decs, lower = zip(*[modf(1.0 * v / divisor) for v in votes])
quotients = [d + l for d, l in zip(decs, lower)]
geo_means = [sqrt(ceil(q) * floor(q)) for q in quotients]
assigned = [ceil(q) if q >= g else floor(q) for q, g in zip(quotients, geo_means)]
unallocated = int(seats - sum(assigned))
if unallocated > 0:
# divisors that would add another seat to each group
geo_means_next = [sqrt(ceil(q + 1) * floor(q + 1)) for q in quotients]
diffs = [
1.0 * vote / max(gn, 1) if q >= g else 1.0 * vote / max(g, 1)
for vote, q, g, gn in zip(votes, quotients, geo_means, geo_means_next)
]
# argsort
divs = [i[0] for i in sorted(enumerate(diffs), key=itemgetter(1))][::-1]
for k in range(unallocated):
assigned[divs[k]] += 1
elif unallocated < 0:
unallocated = abs(unallocated)
# divisors that would subtract a seat from each group
geo_means_next = [sqrt(ceil(q - 1) * floor(q - 1)) for q in quotients]
diffs = [
1.0 * vote / max(gn, 1) if q < g else 1.0 * vote / max(g, 1)
for vote, q, g, gn in zip(votes, quotients, geo_means, geo_means_next)
]
# sort fix to prevent allocations to be zero to any one group
diffs = [float("inf") if d < divisor else d for d in diffs]
# argsort
divs = [i[0] for i in sorted(enumerate(diffs), key=itemgetter(1))]
for k in range(unallocated):
assigned[divs[k]] -= 1
return assigned
def jefferson(votes, seats):
"""Apportion seats using the Jefferson method.
Known also as the D'Hondt method or Hagenbach-Bischoff method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
allocated = [0] * len(votes)
while sum(allocated) < seats:
quotients = [1.0 * vote / (allocated[idx] + 1) for idx, vote in enumerate(votes)]
idx_max = quotients.index(max(quotients))
allocated[idx_max] += 1
return allocated
def sainte_lague(votes, seats):
"""Apportion seats using the Sainte-Lague method.
Identical to the Webster method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
return webster(votes, seats)
def vinton(votes, seats):
"""Apportion seats using the Vinton method.
Identical to the Hamilton method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
return hamilton(votes, seats)
def webster(votes, seats):
"""Apportion seats using the Webster method.
Known also as the Sainte-Lague method.
:param list votes: a list of vote counts
:param int seats: the number of seats to apportion
"""
divisor = 1.0 * sum(votes) / seats
decs, lower = zip(*[modf(1.0 * v / divisor) for v in votes])
lower = [int(l) for l in lower]
assigned = [round(l + d) for l, d in zip(lower, decs)]
unallocated = int(seats - sum(assigned))
if unallocated > 0:
# divisors that would add another seat to each group
diffs = [
1.0 * vote / (i + 1.5) if dec >= 0.5 else 1.0 * vote / (i + 0.5) for i, dec, vote in zip(lower, decs, votes)
]
# argsort
divs = [i[0] for i in sorted(enumerate(diffs), key=itemgetter(1))][::-1]
for k in range(unallocated):
assigned[divs[k]] += 1
elif unallocated < 0:
overallocated = abs(unallocated)
# divisors that would subtract a seat from each group
diffs = [
1.0 * vote / (i + 0.5) if dec >= 0.5 else 1.0 * vote / (i - 0.5) for i, dec, vote in zip(lower, decs, votes)
]
# argsort
divs = [i[0] for i in sorted(enumerate(diffs), key=itemgetter(1))]
# deallocate seats without bringing seat allocation below zero
div_idx = 0
while overallocated > 0:
if assigned[divs[div_idx]] > 0:
assigned[divs[div_idx]] -= 1
overallocated -= 1
else:
div_idx += 1
return assigned