fixed handling of ties in quota method: ties are now reported whenever they occur.

It is currently not possible to find out whether there is more than one Quota apportionment
(also, ran black formatter)
pull/7/head
Martin Lackner 2021-12-21 12:33:36 +01:00
rodzic f447b5c9db
commit 0b5ea8669b
6 zmienionych plików z 263 dodań i 200 usunięć

Wyświetl plik

@ -13,7 +13,7 @@ seats = 20
methods = ["quota", "largest_remainder", "dhondt", "saintelague", "adams"] methods = ["quota", "largest_remainder", "dhondt", "saintelague", "adams"]
iterator = combinations(range(1, maxvoters+1), parties) iterator = combinations(range(1, maxvoters + 1), parties)
for iterations, votes in enumerate(iterator): for iterations, votes in enumerate(iterator):
apportionments = set() apportionments = set()
@ -21,8 +21,10 @@ for iterations, votes in enumerate(iterator):
for method in methods: for method in methods:
try: # in case of ties an exception occurs because tiesallowed=False try: # in case of ties an exception occurs because tiesallowed=False
apportionments.add( apportionments.add(
tuple(app.compute(method, votes, seats, tuple(
tiesallowed=False, verbose=False))) app.compute(method, votes, seats, tiesallowed=False, verbose=False)
)
)
except Exception: except Exception:
pass pass
@ -36,6 +38,6 @@ print("votes = {}".format(votes))
print("found in {} iterations\n\n".format(iterations)) print("found in {} iterations\n\n".format(iterations))
for method in methods: for method in methods:
print("{:>20s}: {}".format(method, print(
app.compute(method, votes, seats, "{:>20s}: {}".format(method, app.compute(method, votes, seats, verbose=False))
verbose=False))) )

Wyświetl plik

@ -7,12 +7,12 @@ with open("./nr_wahlen.txt", "r") as f:
for line in f: for line in f:
year, partynames, votes, officialresult = eval(line) year, partynames, votes, officialresult = eval(line)
print(year) print(year)
result = app.compute("dhondt", votes, result = app.compute(
183, "dhondt", votes, 183, parties=partynames, threshold=0.04, verbose=True
parties=partynames, )
threshold=.04,
verbose=True)
# actual results # actual results
print("Identical with official result: " print(
+ (str(tuple(result) == tuple(officialresult))) "Identical with official result: "
+ "\n\n") + (str(tuple(result) == tuple(officialresult)))
+ "\n\n"
)

Wyświetl plik

@ -8,15 +8,19 @@ print("See https://www.knesset.gov.il/lexicon/eng/seats_eng.htm\n")
with open("knesset.txt", "r") as f: with open("knesset.txt", "r") as f:
for line in f: for line in f:
knesset_nr, partynames, votes, officialresult, threshold = \ knesset_nr, partynames, votes, officialresult, threshold = eval(line)
eval(line)
print("Knesset #" + str(knesset_nr) + ":") print("Knesset #" + str(knesset_nr) + ":")
result = app.compute("dhondt", votes, result = app.compute(
sum(officialresult), "dhondt",
parties=partynames, votes,
threshold=threshold, sum(officialresult),
verbose=True) parties=partynames,
threshold=threshold,
verbose=True,
)
# actual results # actual results
print("Identical with official result: " print(
+ (str(tuple(result) == tuple(officialresult))) "Identical with official result: "
+ "\n\n") + (str(tuple(result) == tuple(officialresult)))
+ "\n\n"
)

Wyświetl plik

@ -4,12 +4,19 @@ import apportionment.methods as app
votes = [1, 3, 6, 7, 78] votes = [1, 3, 6, 7, 78]
seats = 20 seats = 20
print("votes", "."*(25 - len("votes")), votes, "\n") print("votes", "." * (25 - len("votes")), votes, "\n")
print(seats, "seats", "\n") print(seats, "seats", "\n")
print("apportionment results:") print("apportionment results:")
for method in ["quota", "largest_remainder", "dhondt", for method in [
"saintelague", "huntington", "adams", "dean"]: "quota",
"largest_remainder",
"dhondt",
"saintelague",
"huntington",
"adams",
"dean",
]:
result = app.compute(method, votes, seats, verbose=False) result = app.compute(method, votes, seats, verbose=False)
print(method, "."*(25 - len(method)), result) print(method, "." * (25 - len(method)), result)

Wyświetl plik

@ -7,32 +7,55 @@ import math
from fractions import Fraction from fractions import Fraction
METHODS = ["quota", "largest_remainder", "dhondt", "saintelague", METHODS = [
"modified_saintelague", "huntington", "adams", "dean"] "quota",
"largest_remainder",
"dhondt",
"saintelague",
"modified_saintelague",
"huntington",
"adams",
"dean",
]
class TiesException(Exception): class TiesException(Exception):
pass pass
def compute(method, votes, seats, parties=string.ascii_letters, def compute(
threshold=None, tiesallowed=True, verbose=True): method,
votes,
seats,
parties=string.ascii_letters,
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, tiesallowed, 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, return largest_remainder(filtered_votes, seats, parties, tiesallowed, verbose)
tiesallowed, verbose) elif method in [
elif method in ["dhondt", "jefferson", "saintelague", "webster", "dhondt",
"modified_saintelague", "jefferson",
"huntington", "hill", "adams", "dean", "saintelague",
"smallestdivisor", "harmonicmean", "equalproportions", "webster",
"majorfractions", "greatestdivisors"]: "modified_saintelague",
return divisor(filtered_votes, seats, method, parties, "huntington",
tiesallowed, verbose) "hill",
"adams",
"dean",
"smallestdivisor",
"harmonicmean",
"equalproportions",
"majorfractions",
"greatestdivisors",
]:
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")
def apply_threshold(votes, threshold): def apply_threshold(votes, threshold):
@ -52,14 +75,14 @@ def apply_threshold(votes, threshold):
def __print_results(representatives, parties): def __print_results(representatives, parties):
print("apportionment:")
for i in range(len(representatives)): for i in range(len(representatives)):
print(" "+str(parties[i])+": "+str(representatives[i])) print(" " + str(parties[i]) + ": " + str(representatives[i]))
# verifies whether a given assignment of representatives # verifies whether a given assignment of representatives
# is within quota # is within quota
def within_quota(votes, representatives, parties=string.ascii_letters, def within_quota(votes, representatives, parties=string.ascii_letters, verbose=True):
verbose=True):
n = sum(votes) n = sum(votes)
seats = sum(representatives) seats = sum(representatives)
within = True within = True
@ -67,36 +90,51 @@ def within_quota(votes, representatives, parties=string.ascii_letters,
upperquota = int(math.ceil(float(votes[i]) * seats / n)) upperquota = int(math.ceil(float(votes[i]) * seats / n))
if representatives[i] > upperquota: if representatives[i] > upperquota:
if verbose: if verbose:
print("upper quota of party", parties[i], print(
"violated: quota is", float(votes[i]) * seats / n, "upper quota of party",
"but has", representatives[i], "representatives") parties[i],
"violated: quota is",
float(votes[i]) * seats / n,
"but has",
representatives[i],
"representatives",
)
within = False within = False
lowerquota = int(math.floor(float(votes[i]) * seats / n)) lowerquota = int(math.floor(float(votes[i]) * seats / n))
if representatives[i] < lowerquota: if representatives[i] < lowerquota:
if verbose: if verbose:
print("lower quota of party", parties[i], print(
"violated: quota is", float(votes[i]) * seats / n, "lower quota of party",
"but has only", representatives[i], "representatives") parties[i],
"violated: quota is",
float(votes[i]) * seats / n,
"but has only",
representatives[i],
"representatives",
)
within = False within = False
return within return within
# Largest remainder method (Hamilton method) # Largest remainder method (Hamilton method)
def largest_remainder(votes, seats, parties=string.ascii_letters, def largest_remainder(
tiesallowed=True, verbose=True): votes, seats, parties=string.ascii_letters, 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 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 = (
str(parties[:len(votes)]) + " tiebreaking in order of: "
"\n ties broken in favor of: ") + str(parties[: len(votes)])
+ "\n ties broken in favor of: "
)
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:
@ -122,49 +160,54 @@ def largest_remainder(votes, seats, parties=string.ascii_letters,
# Divisor methods # Divisor methods
def divisor(votes, seats, method, parties=string.ascii_letters, def divisor(
tiesallowed=True, verbose=True): votes, seats, method, parties=string.ascii_letters, 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:
print("\nD'Hondt (Jefferson) method") print("\nD'Hondt (Jefferson) method")
divisors = [i+1 for i in range(seats)] divisors = [i + 1 for i in range(seats)]
elif method in ["saintelague", "webster", "majorfractions"]: elif method in ["saintelague", "webster", "majorfractions"]:
if verbose: if verbose:
print("\nSainte Lague (Webster) method") print("\nSainte Lague (Webster) method")
divisors = [2*i+1 for i in range(seats)] divisors = [2 * i + 1 for i in range(seats)]
elif method in ["modified_saintelague"]: elif method in ["modified_saintelague"]:
if verbose: if verbose:
print("\nModified Sainte Lague (Webster) method") print("\nModified Sainte Lague (Webster) method")
divisors = [1.4] + [2*i+1 for i in range(1, seats)] divisors = [1.4] + [2 * i + 1 for i in range(1, seats)]
elif method in ["huntington", "hill", "equalproportions"]: elif method in ["huntington", "hill", "equalproportions"]:
if verbose: if verbose:
print("\nHuntington-Hill method") print("\nHuntington-Hill method")
if seats < len(votes): if seats < len(votes):
representatives = __divzero_fewerseatsthanparties( representatives = __divzero_fewerseatsthanparties(
votes, seats, parties, tiesallowed, verbose) votes, seats, parties, tiesallowed, 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)]
elif method in ["adams", "smallestdivisor"]: elif method in ["adams", "smallestdivisor"]:
if verbose: if verbose:
print("\nAdams method") print("\nAdams method")
if seats < len(votes): if seats < len(votes):
representatives = __divzero_fewerseatsthanparties( representatives = __divzero_fewerseatsthanparties(
votes, seats, parties, tiesallowed, verbose) votes, seats, parties, tiesallowed, 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)]
elif method in ["dean", "harmonicmean"]: elif method in ["dean", "harmonicmean"]:
if verbose: if verbose:
print("\nDean method") print("\nDean method")
if seats < len(votes): if seats < len(votes):
representatives = __divzero_fewerseatsthanparties( representatives = __divzero_fewerseatsthanparties(
votes, seats, parties, tiesallowed, verbose) votes, seats, parties, tiesallowed, 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 = [
for i in range(seats)] Fraction(2 * (i + 1) * (i + 2), 2 * (i + 1) + 1) for i in range(seats)
]
else: else:
raise NotImplementedError("divisor method " + method + " not known") raise NotImplementedError("divisor method " + method + " not known")
@ -185,9 +228,11 @@ def divisor(votes, seats, method, parties=string.ascii_letters,
ties = False ties = False
# dealing with ties # dealing with ties
if seats > sum(representatives): if seats > sum(representatives):
tiebreaking_message = (" tiebreaking in order of: " + tiebreaking_message = (
str(parties[:len(votes)]) + " tiebreaking in order of: "
"\n ties broken in favor of: ") + str(parties[: len(votes)])
+ "\n ties broken in favor of: "
)
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:
@ -213,15 +258,17 @@ 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, def __divzero_fewerseatsthanparties(votes, seats, parties, tiesallowed, verbose):
tiesallowed, verbose):
representatives = [0] * len(votes) representatives = [0] * len(votes)
if verbose: if verbose:
print(" fewer seats than parties; " + str(seats) + print(
" strongest parties receive one seat") " fewer seats than parties; "
+ str(seats)
+ " strongest parties receive one seat"
)
tiebreaking_message = " ties broken in favor of: " tiebreaking_message = " ties broken in favor of: "
ties = False ties = False
mincount = sorted(votes, reverse=True)[seats-1] mincount = sorted(votes, reverse=True)[seats - 1]
for i in range(len(votes)): for i in range(len(votes)):
if sum(representatives) < seats and votes[i] >= mincount: if sum(representatives) < seats and votes[i] >= mincount:
if votes[i] == mincount: if votes[i] == mincount:
@ -240,54 +287,52 @@ def __divzero_fewerseatsthanparties(votes, seats, parties,
return representatives return representatives
# The quota method def quota(votes, seats, parties=string.ascii_letters, tiesallowed=True, verbose=True):
# ( see Balinski, M. L., & Young, H. P. (1975). """The quota method
# The quota method of apportionment. see Balinski, M. L., & Young, H. P. (1975).
# The American Mathematical Monthly, 82(7), 701-730.) The quota method of apportionment.
def quota(votes, seats, parties=string.ascii_letters, The American Mathematical Monthly, 82(7), 701-730.)
tiesallowed=True, verbose=True):
Warning: tiesallowed is not supported here (difficult to implement)
"""
if not tiesallowed:
raise NotImplementedError(
"parameter tiesallowed not supported for Quota method"
)
if verbose: if verbose:
print("\nQuota method") print("\nQuota method")
representatives = [0] * len(votes) representatives = [0] * len(votes)
while sum(representatives) < seats: while sum(representatives) < seats:
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(
(sum(representatives)+1) math.ceil(float(votes[i]) * (sum(representatives) + 1) / sum(votes))
/ sum(votes))) )
if representatives[i] >= upperquota: if representatives[i] >= upperquota:
quotas[i] = 0 quotas[i] = 0
maxquotas = [i for i in range(len(votes)) maxquotas = [i for i in range(len(votes)) if quotas[i] == max(quotas)]
if quotas[i] == max(quotas)]
nextrep = maxquotas[0] nextrep = maxquotas[0]
# print tiebreaking information
if verbose and len(maxquotas) > 1:
print(
"tiebreaking necessary in round "
+ str(sum(representatives) + 1)
+ ":"
+ " tiebreaking in order of: "
+ str(parties[: len(votes)])
+ "\n ties broken in favor of: "
+ str(parties[nextrep])
+ "\n to the disadvantage of: "
+ ", ".join(parties[i] for i in maxquotas[1:])
)
representatives[nextrep] += 1 representatives[nextrep] += 1
if len(maxquotas) > 1 and not tiesallowed:
raise TiesException("Tie occurred")
# print tiebreaking information
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: " +
str(parties[:len(votes)]) +
"\n ties broken in favor of: ")
ties_favor = [i for i in range(len(votes))
if quotas_now[i] == quotas_now[nextrep]]
for i in ties_favor:
tiebreaking_message += str(parties[i]) + ", "
tiebreaking_message = (tiebreaking_message[:-2] +
"\n to the disadvantage of: ")
for i in maxquotas[1:]:
tiebreaking_message += str(parties[i]) + ", "
print(tiebreaking_message[:-2])
if verbose: if verbose:
__print_results(representatives, parties) __print_results(representatives, parties)
return representatives return representatives

Wyświetl plik

@ -6,22 +6,33 @@ import apportionment.methods as app
class TestApprovalMultiwinner(unittest.TestCase): class TestApprovalMultiwinner(unittest.TestCase):
def test_all_implemented(self): def test_all_implemented(self):
ALLMETHODSSTRINGS = ["quota", "lrm", "hamilton", "largest_remainder", ALLMETHODSSTRINGS = [
"dhondt", "jefferson", "saintelague", "webster", "quota",
"huntington", "hill", "adams", "dean", "lrm",
"smallestdivisor", "harmonicmean", "hamilton",
"equalproportions", "majorfractions", "largest_remainder",
"greatestdivisors", "modified_saintelague"] "dhondt",
"jefferson",
"saintelague",
"webster",
"huntington",
"hill",
"adams",
"dean",
"smallestdivisor",
"harmonicmean",
"equalproportions",
"majorfractions",
"greatestdivisors",
"modified_saintelague",
]
votes = [1] votes = [1]
seats = 1 seats = 1
for method in ALLMETHODSSTRINGS: for method in ALLMETHODSSTRINGS:
result = app.compute(method, votes, result = app.compute(method, votes, seats, verbose=False)
seats, verbose=False) self.assertEqual(result, [1], msg=method + " does not exist")
self.assertEqual(result, [1],
msg=method + " does not exist")
def test_weak_proportionality(self): def test_weak_proportionality(self):
self.longMessage = True self.longMessage = True
@ -29,10 +40,8 @@ class TestApprovalMultiwinner(unittest.TestCase):
votes = [14, 28, 7, 35] votes = [14, 28, 7, 35]
seats = 12 seats = 12
for method in app.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], msg=method + " failed")
self.assertEqual(result, [2, 4, 1, 5],
msg=method + " failed")
def test_zero_parties(self): def test_zero_parties(self):
self.longMessage = True self.longMessage = True
@ -40,10 +49,8 @@ class TestApprovalMultiwinner(unittest.TestCase):
votes = [0, 14, 28, 0, 0] votes = [0, 14, 28, 0, 0]
seats = 6 seats = 6
for method in app.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], msg=method + " failed")
self.assertEqual(result, [0, 2, 4, 0, 0],
msg=method + " failed")
def test_fewerseatsthanparties(self): def test_fewerseatsthanparties(self):
self.longMessage = True self.longMessage = True
@ -51,10 +58,8 @@ 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 app.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], msg=method + " failed")
self.assertEqual(result, [1, 0, 0, 0, 1, 1],
msg=method + " failed")
# examples taken from # examples taken from
# Balinski, M. L., & Young, H. P. (1975). # Balinski, M. L., & Young, H. P. (1975).
@ -63,44 +68,42 @@ class TestApprovalMultiwinner(unittest.TestCase):
def test_balinski_young_example1(self): def test_balinski_young_example1(self):
self.longMessage = True self.longMessage = True
RESULTS = {"quota": [52, 44, 2, 1, 1], RESULTS = {
"largest_remainder": [51, 44, 2, 2, 1], "quota": [52, 44, 2, 1, 1],
"dhondt": [52, 45, 1, 1, 1], "largest_remainder": [51, 44, 2, 2, 1],
"saintelague": [51, 43, 2, 2, 2], "dhondt": [52, 45, 1, 1, 1],
"modified_saintelague": [51, 43, 2, 2, 2], "saintelague": [51, 43, 2, 2, 2],
"huntington": [51, 43, 2, 2, 2], "modified_saintelague": [51, 43, 2, 2, 2],
"adams": [51, 43, 2, 2, 2], "huntington": [51, 43, 2, 2, 2],
"dean": [51, 43, 2, 2, 2] "adams": [51, 43, 2, 2, 2],
} "dean": [51, 43, 2, 2, 2],
}
votes = [5117, 4400, 162, 161, 160] votes = [5117, 4400, 162, 161, 160]
seats = 100 seats = 100
for method in RESULTS.keys(): for method in RESULTS.keys():
result = app.compute(method, votes, result = app.compute(method, votes, seats, verbose=False)
seats, verbose=False) self.assertEqual(result, RESULTS[method], msg=method + " failed")
self.assertEqual(result, RESULTS[method],
msg=method + " failed")
def test_balinski_young_example2(self): def test_balinski_young_example2(self):
self.longMessage = True self.longMessage = True
RESULTS = {"quota": [10, 7, 5, 3, 1], RESULTS = {
"largest_remainder": [9, 7, 5, 4, 1], "quota": [10, 7, 5, 3, 1],
"dhondt": [10, 7, 5, 3, 1], "largest_remainder": [9, 7, 5, 4, 1],
"saintelague": [9, 8, 5, 3, 1], "dhondt": [10, 7, 5, 3, 1],
"modified_saintelague": [9, 8, 5, 3, 1], "saintelague": [9, 8, 5, 3, 1],
"huntington": [9, 7, 6, 3, 1], "modified_saintelague": [9, 8, 5, 3, 1],
"adams": [9, 7, 5, 3, 2], "huntington": [9, 7, 6, 3, 1],
"dean": [9, 7, 5, 4, 1] "adams": [9, 7, 5, 3, 2],
} "dean": [9, 7, 5, 4, 1],
}
votes = [9061, 7179, 5259, 3319, 1182] votes = [9061, 7179, 5259, 3319, 1182]
seats = 26 seats = 26
for method in RESULTS.keys(): for method in RESULTS.keys():
result = app.compute(method, votes, result = app.compute(method, votes, seats, verbose=False)
seats, verbose=False) self.assertEqual(result, RESULTS[method], msg=method + " failed")
self.assertEqual(result, RESULTS[method],
msg=method + " failed")
def test_tiebreaking(self): def test_tiebreaking(self):
self.longMessage = True self.longMessage = True
@ -108,78 +111,80 @@ class TestApprovalMultiwinner(unittest.TestCase):
votes = [2, 1, 1, 2, 2] votes = [2, 1, 1, 2, 2]
seats = 2 seats = 2
for method in app.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], msg=method + " failed")
self.assertEqual(result, [1, 0, 0, 1, 0],
msg=method + " failed")
def test_within_quota(self): def test_within_quota(self):
votes = [5117, 4400, 162, 161, 160] votes = [5117, 4400, 162, 161, 160]
representatives = [51, 44, 2, 2, 1] representatives = [51, 44, 2, 2, 1]
self.assertTrue(app.within_quota(votes, representatives, self.assertTrue(app.within_quota(votes, representatives, verbose=False))
verbose=False))
representatives = [52, 45, 1, 1, 1] representatives = [52, 45, 1, 1, 1]
self.assertFalse(app.within_quota(votes, representatives, self.assertFalse(app.within_quota(votes, representatives, verbose=False))
verbose=False))
representatives = [52, 43, 2, 1, 2] representatives = [52, 43, 2, 1, 2]
self.assertFalse(app.within_quota(votes, representatives, self.assertFalse(app.within_quota(votes, representatives, verbose=False))
verbose=False))
def test_threshold(self): def test_threshold(self):
votes = [41, 56, 3] votes = [41, 56, 3]
seats = 60 seats = 60
threshold = 0.03 threshold = 0.03
filtered_votes = app.apply_threshold(votes, threshold) filtered_votes = app.apply_threshold(votes, threshold)
self.assertEqual(filtered_votes, [41, 56, 3], self.assertEqual(filtered_votes, [41, 56, 3], "Threshold cut too much.")
"Threshold cut too much.")
threshold = 0.031 threshold = 0.031
filtered_votes = app.apply_threshold(votes, threshold) filtered_votes = app.apply_threshold(votes, threshold)
self.assertEqual(filtered_votes, [41, 56, 0], self.assertEqual(
"Threshold was not applied correctly.") filtered_votes, [41, 56, 0], "Threshold was not applied correctly."
)
method = "dhondt" method = "dhondt"
threshold = 0 threshold = 0
unfiltered_result = app.compute(method, votes, seats, unfiltered_result = app.compute(
threshold=threshold, method, votes, seats, threshold=threshold, verbose=False
verbose=False) )
threshold = 0.04 threshold = 0.04
filtered_result = app.compute(method, votes, seats, filtered_result = app.compute(
threshold=threshold, method, votes, seats, threshold=threshold, verbose=False
verbose=False) )
self.assertNotEqual(unfiltered_result, filtered_result, self.assertNotEqual(
"Result did not change despite threshold") unfiltered_result,
filtered_result,
"Result did not change despite threshold",
)
def test_saintelague_difference(self): def test_saintelague_difference(self):
votes = [6, 1] votes = [6, 1]
seats = 4 seats = 4
r1 = app.compute("saintelague", votes, r1 = app.compute("saintelague", votes, seats, verbose=False) # [3, 1]
seats, verbose=False) # [3, 1] r2 = app.compute("modified_saintelague", votes, seats, verbose=False) # [4, 0]
r2 = app.compute("modified_saintelague", votes, self.assertNotEqual(
seats, verbose=False) # [4, 0] r1,
self.assertNotEqual(r1, r2, r2,
"Sainte Lague and its modified variant" "Sainte Lague and its modified variant"
+ "should produce different results.") + "should produce different results.",
)
def test_no_ties_allowed(self): def test_no_ties_allowed(self):
self.longMessage = True self.longMessage = True
votes = [11, 11, 11] votes = [11, 11, 11]
seats = 4 seats = 4
for method in app.METHODS: for method in app.METHODS:
with self.assertRaises(app.TiesException, if method == "quota":
msg=method + " failed"): continue
app.compute(method, votes, seats, with self.assertRaises(app.TiesException, msg=method + " failed"):
tiesallowed=False, verbose=False) app.compute(method, votes, seats, tiesallowed=False, verbose=False)
def test_no_ties_allowed2(self): def test_no_ties_allowed2(self):
self.longMessage = True self.longMessage = True
votes = [12, 12, 11, 12] votes = [12, 12, 11, 12]
seats = 3 seats = 3
for method in app.METHODS: for method in app.METHODS:
if method == "quota":
continue
self.assertEqual( self.assertEqual(
app.compute(method, votes, seats, app.compute(method, votes, seats, tiesallowed=False, verbose=False),
tiesallowed=False, verbose=False), [1, 1, 0, 1],
[1, 1, 0, 1], msg=method + " failed") msg=method + " failed",
)
if __name__ == '__main__': if __name__ == "__main__":
unittest.main() unittest.main()