kopia lustrzana https://github.com/martinlackner/apportionment
possible to raise exceptions when ties occur
rodzic
f92f07b46a
commit
e29a7e74b3
|
@ -7,19 +7,29 @@ import math
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
|
||||||
|
|
||||||
|
METHODS = ["quota", "largest_remainder", "dhondt", "saintelague",
|
||||||
|
"modified_saintelague", "huntington", "adams", "dean"]
|
||||||
|
|
||||||
|
|
||||||
|
class TiesException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def compute(method, votes, seats, parties=string.ascii_letters,
|
def compute(method, votes, seats, parties=string.ascii_letters,
|
||||||
threshold=None, verbose=True):
|
threshold=None, tiesallowed=True, verbose=True):
|
||||||
filtered_votes = apply_threshold(votes, threshold)
|
filtered_votes = apply_threshold(votes, threshold)
|
||||||
if method == "quota":
|
if method == "quota":
|
||||||
return quota(filtered_votes, seats, parties, verbose)
|
return quota(filtered_votes, seats, parties, tiesallowed, verbose)
|
||||||
elif method in ["lrm", "hamilton", "largest_remainder"]:
|
elif method in ["lrm", "hamilton", "largest_remainder"]:
|
||||||
return largest_remainder(filtered_votes, seats, parties, verbose)
|
return largest_remainder(filtered_votes, seats, parties,
|
||||||
|
tiesallowed, verbose)
|
||||||
elif method in ["dhondt", "jefferson", "saintelague", "webster",
|
elif method in ["dhondt", "jefferson", "saintelague", "webster",
|
||||||
"modified_saintelague",
|
"modified_saintelague",
|
||||||
"huntington", "hill", "adams", "dean",
|
"huntington", "hill", "adams", "dean",
|
||||||
"smallestdivisor", "harmonicmean", "equalproportions",
|
"smallestdivisor", "harmonicmean", "equalproportions",
|
||||||
"majorfractions", "greatestdivisors"]:
|
"majorfractions", "greatestdivisors"]:
|
||||||
return divisor(filtered_votes, seats, method, parties, verbose)
|
return divisor(filtered_votes, seats, method, parties,
|
||||||
|
tiesallowed, verbose)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("apportionment method " + method +
|
raise NotImplementedError("apportionment method " + method +
|
||||||
" not known")
|
" not known")
|
||||||
|
@ -73,19 +83,20 @@ def within_quota(votes, representatives, parties=string.ascii_letters,
|
||||||
|
|
||||||
# Largest remainder method (Hamilton method)
|
# Largest remainder method (Hamilton method)
|
||||||
def largest_remainder(votes, seats, parties=string.ascii_letters,
|
def largest_remainder(votes, seats, parties=string.ascii_letters,
|
||||||
verbose=True):
|
tiesallowed=True, verbose=True):
|
||||||
if verbose:
|
if verbose:
|
||||||
print("\nLargest remainder method with Hare quota (Hamilton)")
|
print("\nLargest remainder method with Hare quota (Hamilton)")
|
||||||
q = Fraction(sum(votes), seats)
|
q = Fraction(sum(votes), seats)
|
||||||
quotas = [Fraction(p, q) for p in votes]
|
quotas = [Fraction(p, q) for p in votes]
|
||||||
representatives = [int(qu.numerator)//int(qu.denominator) for qu in quotas]
|
representatives = [int(qu.numerator)//int(qu.denominator) for qu in quotas]
|
||||||
|
|
||||||
|
ties = False
|
||||||
if sum(representatives) < seats:
|
if sum(representatives) < seats:
|
||||||
remainders = [a-b for a, b in zip(quotas, representatives)]
|
remainders = [a-b for a, b in zip(quotas, representatives)]
|
||||||
cutoff = sorted(remainders, reverse=True)[seats-sum(representatives)-1]
|
cutoff = sorted(remainders, reverse=True)[seats-sum(representatives)-1]
|
||||||
tiebreaking_message = (" tiebreaking in order of: " +
|
tiebreaking_message = (" tiebreaking in order of: " +
|
||||||
str(parties[:len(votes)]) +
|
str(parties[:len(votes)]) +
|
||||||
"\n ties broken in favor of: ")
|
"\n ties broken in favor of: ")
|
||||||
ties = False
|
|
||||||
for i in range(len(votes)):
|
for i in range(len(votes)):
|
||||||
if sum(representatives) == seats and remainders[i] >= cutoff:
|
if sum(representatives) == seats and remainders[i] >= cutoff:
|
||||||
if not ties:
|
if not ties:
|
||||||
|
@ -101,6 +112,9 @@ def largest_remainder(votes, seats, parties=string.ascii_letters,
|
||||||
if ties and verbose:
|
if ties and verbose:
|
||||||
print(tiebreaking_message[:-2])
|
print(tiebreaking_message[:-2])
|
||||||
|
|
||||||
|
if ties and not tiesallowed:
|
||||||
|
raise TiesException("Tie occurred")
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
__print_results(representatives, parties)
|
__print_results(representatives, parties)
|
||||||
|
|
||||||
|
@ -109,7 +123,7 @@ def largest_remainder(votes, seats, parties=string.ascii_letters,
|
||||||
|
|
||||||
# Divisor methods
|
# Divisor methods
|
||||||
def divisor(votes, seats, method, parties=string.ascii_letters,
|
def divisor(votes, seats, method, parties=string.ascii_letters,
|
||||||
verbose=True):
|
tiesallowed=True, verbose=True):
|
||||||
representatives = [0] * len(votes)
|
representatives = [0] * len(votes)
|
||||||
if method in ["dhondt", "jefferson", "greatestdivisors"]:
|
if method in ["dhondt", "jefferson", "greatestdivisors"]:
|
||||||
if verbose:
|
if verbose:
|
||||||
|
@ -127,9 +141,8 @@ def divisor(votes, seats, method, parties=string.ascii_letters,
|
||||||
if verbose:
|
if verbose:
|
||||||
print("\nHuntington-Hill method")
|
print("\nHuntington-Hill method")
|
||||||
if seats < len(votes):
|
if seats < len(votes):
|
||||||
representatives = __divzero_fewerseatsthanparties(votes,
|
representatives = __divzero_fewerseatsthanparties(
|
||||||
seats, parties,
|
votes, seats, parties, tiesallowed, verbose)
|
||||||
verbose)
|
|
||||||
else:
|
else:
|
||||||
representatives = [1 if p > 0 else 0 for p in votes]
|
representatives = [1 if p > 0 else 0 for p in votes]
|
||||||
divisors = [math.sqrt((i+1)*(i+2)) for i in range(seats)]
|
divisors = [math.sqrt((i+1)*(i+2)) for i in range(seats)]
|
||||||
|
@ -137,9 +150,8 @@ def divisor(votes, seats, method, parties=string.ascii_letters,
|
||||||
if verbose:
|
if verbose:
|
||||||
print("\nAdams method")
|
print("\nAdams method")
|
||||||
if seats < len(votes):
|
if seats < len(votes):
|
||||||
representatives = __divzero_fewerseatsthanparties(votes,
|
representatives = __divzero_fewerseatsthanparties(
|
||||||
seats, parties,
|
votes, seats, parties, tiesallowed, verbose)
|
||||||
verbose)
|
|
||||||
else:
|
else:
|
||||||
representatives = [1 if p > 0 else 0 for p in votes]
|
representatives = [1 if p > 0 else 0 for p in votes]
|
||||||
divisors = [i+1 for i in range(seats)]
|
divisors = [i+1 for i in range(seats)]
|
||||||
|
@ -147,9 +159,8 @@ def divisor(votes, seats, method, parties=string.ascii_letters,
|
||||||
if verbose:
|
if verbose:
|
||||||
print("\nDean method")
|
print("\nDean method")
|
||||||
if seats < len(votes):
|
if seats < len(votes):
|
||||||
representatives = __divzero_fewerseatsthanparties(votes,
|
representatives = __divzero_fewerseatsthanparties(
|
||||||
seats, parties,
|
votes, seats, parties, tiesallowed, verbose)
|
||||||
verbose)
|
|
||||||
else:
|
else:
|
||||||
representatives = [1 if p > 0 else 0 for p in votes]
|
representatives = [1 if p > 0 else 0 for p in votes]
|
||||||
divisors = [Fraction(2 * (i+1) * (i+2), 2 * (i+1) + 1)
|
divisors = [Fraction(2 * (i+1) * (i+2), 2 * (i+1) + 1)
|
||||||
|
@ -180,6 +191,8 @@ def divisor(votes, seats, method, parties=string.ascii_letters,
|
||||||
for i in range(len(votes)):
|
for i in range(len(votes)):
|
||||||
if sum(representatives) == seats and minweight in weights[i]:
|
if sum(representatives) == seats and minweight in weights[i]:
|
||||||
if not ties:
|
if not ties:
|
||||||
|
if not tiesallowed:
|
||||||
|
raise TiesException("Tie occurred")
|
||||||
tiebreaking_message = tiebreaking_message[:-2]
|
tiebreaking_message = tiebreaking_message[:-2]
|
||||||
tiebreaking_message += "\n to the disadvantage of: "
|
tiebreaking_message += "\n to the disadvantage of: "
|
||||||
ties = True
|
ties = True
|
||||||
|
@ -190,6 +203,9 @@ def divisor(votes, seats, method, parties=string.ascii_letters,
|
||||||
if ties and verbose:
|
if ties and verbose:
|
||||||
print(tiebreaking_message[:-2])
|
print(tiebreaking_message[:-2])
|
||||||
|
|
||||||
|
if ties and not tiesallowed:
|
||||||
|
raise TiesException("Tie occurred")
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
__print_results(representatives, parties)
|
__print_results(representatives, parties)
|
||||||
|
|
||||||
|
@ -197,7 +213,8 @@ def divisor(votes, seats, method, parties=string.ascii_letters,
|
||||||
|
|
||||||
|
|
||||||
# required for methods with 0 divisors (Adams, Huntington-Hill)
|
# required for methods with 0 divisors (Adams, Huntington-Hill)
|
||||||
def __divzero_fewerseatsthanparties(votes, seats, parties, verbose):
|
def __divzero_fewerseatsthanparties(votes, seats, parties,
|
||||||
|
tiesallowed, verbose):
|
||||||
representatives = [0] * len(votes)
|
representatives = [0] * len(votes)
|
||||||
if verbose:
|
if verbose:
|
||||||
print(" fewer seats than parties; " + str(seats) +
|
print(" fewer seats than parties; " + str(seats) +
|
||||||
|
@ -216,6 +233,8 @@ def __divzero_fewerseatsthanparties(votes, seats, parties, verbose):
|
||||||
tiebreaking_message += "\n to the disadvantage of: "
|
tiebreaking_message += "\n to the disadvantage of: "
|
||||||
ties = True
|
ties = True
|
||||||
tiebreaking_message += parties[i] + ", "
|
tiebreaking_message += parties[i] + ", "
|
||||||
|
if ties and not tiesallowed:
|
||||||
|
raise TiesException("Tie occurred")
|
||||||
if ties and verbose:
|
if ties and verbose:
|
||||||
print(tiebreaking_message[:-2])
|
print(tiebreaking_message[:-2])
|
||||||
return representatives
|
return representatives
|
||||||
|
@ -225,43 +244,50 @@ def __divzero_fewerseatsthanparties(votes, seats, parties, verbose):
|
||||||
# ( see Balinski, M. L., & Young, H. P. (1975).
|
# ( see Balinski, M. L., & Young, H. P. (1975).
|
||||||
# The quota method of apportionment.
|
# The quota method of apportionment.
|
||||||
# The American Mathematical Monthly, 82(7), 701-730.)
|
# The American Mathematical Monthly, 82(7), 701-730.)
|
||||||
def quota(votes, seats, parties=string.ascii_letters, verbose=True):
|
def quota(votes, seats, parties=string.ascii_letters,
|
||||||
|
tiesallowed=True, verbose=True):
|
||||||
if verbose:
|
if verbose:
|
||||||
print("\nQuota method")
|
print("\nQuota method")
|
||||||
representatives = [0] * len(votes)
|
representatives = [0] * len(votes)
|
||||||
tied = []
|
while sum(representatives) < seats:
|
||||||
for k in range(1, seats+1):
|
|
||||||
quotas = [Fraction(votes[i], representatives[i]+1)
|
quotas = [Fraction(votes[i], representatives[i]+1)
|
||||||
for i in range(len(votes))]
|
for i in range(len(votes))]
|
||||||
# check if upper quota is violated
|
# check if upper quota is violated
|
||||||
for i in range(len(votes)):
|
for i in range(len(votes)):
|
||||||
upperquota = int(math.ceil(float(votes[i]) *
|
upperquota = int(math.ceil(float(votes[i]) *
|
||||||
k / sum(votes)))
|
(sum(representatives)+1)
|
||||||
|
/ sum(votes)))
|
||||||
if representatives[i] >= upperquota:
|
if representatives[i] >= upperquota:
|
||||||
quotas[i] = 0
|
quotas[i] = 0
|
||||||
chosen = [i for i in range(len(votes))
|
|
||||||
if quotas[i] == max(quotas)]
|
maxquotas = [i for i in range(len(votes))
|
||||||
if verbose:
|
if quotas[i] == max(quotas)]
|
||||||
if len(chosen) > 1:
|
|
||||||
tied.append(representatives[chosen[0]])
|
nextrep = maxquotas[0]
|
||||||
else:
|
representatives[nextrep] += 1
|
||||||
tied = []
|
|
||||||
representatives[chosen[0]] += 1
|
if len(maxquotas) > 1 and not tiesallowed:
|
||||||
|
raise TiesException("Tie occurred")
|
||||||
|
|
||||||
# print tiebreaking information
|
# print tiebreaking information
|
||||||
if verbose and len(tied) > 0:
|
if verbose and len(maxquotas) > 1:
|
||||||
|
quotas_now = [Fraction(votes[i], representatives[i]+1)
|
||||||
|
for i in range(len(votes))]
|
||||||
tiebreaking_message = (" tiebreaking in order of: " +
|
tiebreaking_message = (" tiebreaking in order of: " +
|
||||||
str(parties[:len(votes)]) +
|
str(parties[:len(votes)]) +
|
||||||
"\n ties broken in favor of: ")
|
"\n ties broken in favor of: ")
|
||||||
for i in range(len(tied)):
|
ties_favor = [i for i in range(len(votes))
|
||||||
tiebreaking_message += tied[i] + ", "
|
if quotas_now[i] == quotas_now[nextrep]]
|
||||||
|
for i in ties_favor:
|
||||||
|
tiebreaking_message += str(parties[i]) + ", "
|
||||||
tiebreaking_message = (tiebreaking_message[:-2] +
|
tiebreaking_message = (tiebreaking_message[:-2] +
|
||||||
"\n to the disadvantage of: ")
|
"\n to the disadvantage of: ")
|
||||||
for i in range(1, len(chosen)):
|
for i in maxquotas[1:]:
|
||||||
tiebreaking_message += tied[i] + ", "
|
tiebreaking_message += str(parties[i]) + ", "
|
||||||
print(tiebreaking_message)
|
print(tiebreaking_message[:-2])
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
__print_results(representatives, parties)
|
__print_results(representatives, parties)
|
||||||
|
|
||||||
return representatives
|
return representatives
|
||||||
|
|
|
@ -5,10 +5,6 @@ import unittest
|
||||||
import apportionment.methods as app
|
import apportionment.methods as app
|
||||||
|
|
||||||
|
|
||||||
METHODS = ["quota", "largest_remainder", "dhondt", "saintelague",
|
|
||||||
"modified_saintelague", "huntington", "adams", "dean"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestApprovalMultiwinner(unittest.TestCase):
|
class TestApprovalMultiwinner(unittest.TestCase):
|
||||||
|
|
||||||
def test_all_implemented(self):
|
def test_all_implemented(self):
|
||||||
|
@ -32,7 +28,7 @@ class TestApprovalMultiwinner(unittest.TestCase):
|
||||||
|
|
||||||
votes = [14, 28, 7, 35]
|
votes = [14, 28, 7, 35]
|
||||||
seats = 12
|
seats = 12
|
||||||
for method in METHODS:
|
for method in app.METHODS:
|
||||||
result = app.compute(method, votes,
|
result = app.compute(method, votes,
|
||||||
seats, verbose=False)
|
seats, verbose=False)
|
||||||
self.assertEqual(result, [2, 4, 1, 5],
|
self.assertEqual(result, [2, 4, 1, 5],
|
||||||
|
@ -43,7 +39,7 @@ class TestApprovalMultiwinner(unittest.TestCase):
|
||||||
|
|
||||||
votes = [0, 14, 28, 0, 0]
|
votes = [0, 14, 28, 0, 0]
|
||||||
seats = 6
|
seats = 6
|
||||||
for method in METHODS:
|
for method in app.METHODS:
|
||||||
result = app.compute(method, votes,
|
result = app.compute(method, votes,
|
||||||
seats, verbose=False)
|
seats, verbose=False)
|
||||||
self.assertEqual(result, [0, 2, 4, 0, 0],
|
self.assertEqual(result, [0, 2, 4, 0, 0],
|
||||||
|
@ -54,13 +50,13 @@ class TestApprovalMultiwinner(unittest.TestCase):
|
||||||
|
|
||||||
votes = [10, 9, 8, 8, 11, 12]
|
votes = [10, 9, 8, 8, 11, 12]
|
||||||
seats = 3
|
seats = 3
|
||||||
for method in METHODS:
|
for method in app.METHODS:
|
||||||
result = app.compute(method, votes,
|
result = app.compute(method, votes,
|
||||||
seats, verbose=False)
|
seats, verbose=False)
|
||||||
self.assertEqual(result, [1, 0, 0, 0, 1, 1],
|
self.assertEqual(result, [1, 0, 0, 0, 1, 1],
|
||||||
msg=method + " failed")
|
msg=method + " failed")
|
||||||
|
|
||||||
# example taken from
|
# examples taken from
|
||||||
# Balinski, M. L., & Young, H. P. (1975).
|
# Balinski, M. L., & Young, H. P. (1975).
|
||||||
# The quota method of apportionment.
|
# The quota method of apportionment.
|
||||||
# The American Mathematical Monthly, 82(7), 701-730.
|
# The American Mathematical Monthly, 82(7), 701-730.
|
||||||
|
@ -111,7 +107,7 @@ class TestApprovalMultiwinner(unittest.TestCase):
|
||||||
|
|
||||||
votes = [2, 1, 1, 2, 2]
|
votes = [2, 1, 1, 2, 2]
|
||||||
seats = 2
|
seats = 2
|
||||||
for method in METHODS:
|
for method in app.METHODS:
|
||||||
result = app.compute(method, votes,
|
result = app.compute(method, votes,
|
||||||
seats, verbose=False)
|
seats, verbose=False)
|
||||||
self.assertEqual(result, [1, 0, 0, 1, 0],
|
self.assertEqual(result, [1, 0, 0, 1, 0],
|
||||||
|
@ -161,8 +157,28 @@ class TestApprovalMultiwinner(unittest.TestCase):
|
||||||
r2 = app.compute("modified_saintelague", votes,
|
r2 = app.compute("modified_saintelague", votes,
|
||||||
seats, verbose=False) # [4, 0]
|
seats, verbose=False) # [4, 0]
|
||||||
self.assertNotEqual(r1, r2,
|
self.assertNotEqual(r1, r2,
|
||||||
"Saintelague and its modified variant"
|
"Sainte Lague and its modified variant"
|
||||||
+ "should produce differents results.")
|
+ "should produce different results.")
|
||||||
|
|
||||||
|
def test_no_ties_allowed(self):
|
||||||
|
self.longMessage = True
|
||||||
|
votes = [11, 11, 11]
|
||||||
|
seats = 4
|
||||||
|
for method in app.METHODS:
|
||||||
|
with self.assertRaises(app.TiesException,
|
||||||
|
msg=method + " failed"):
|
||||||
|
app.compute(method, votes, seats,
|
||||||
|
tiesallowed=False, verbose=False)
|
||||||
|
|
||||||
|
def test_no_ties_allowed2(self):
|
||||||
|
self.longMessage = True
|
||||||
|
votes = [12, 12, 11, 12]
|
||||||
|
seats = 3
|
||||||
|
for method in app.METHODS:
|
||||||
|
self.assertEqual(
|
||||||
|
app.compute(method, votes, seats,
|
||||||
|
tiesallowed=False, verbose=False),
|
||||||
|
[1, 1, 0, 1], msg=method + " failed")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Ładowanie…
Reference in New Issue