From 65adda9b6511292889aeebecb1819cc0ca5b91d9 Mon Sep 17 00:00:00 2001 From: TetraK1 Date: Sat, 26 Dec 2020 14:01:30 +0000 Subject: [PATCH] Add hare quota option to STV --- pyrankvote/helpers.py | 6 ++++ pyrankvote/multiple_seat_ranking_methods.py | 13 ++++----- tests/test_multiple_seat_ranking_methods.py | 31 ++++++++++++++++++++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/pyrankvote/helpers.py b/pyrankvote/helpers.py index 5155e41..e694255 100644 --- a/pyrankvote/helpers.py +++ b/pyrankvote/helpers.py @@ -86,6 +86,12 @@ class CompareMethodIfEqual: Random = "Random" MostSecondChoiceVotes = "MostSecondChoiceVotes" +class RankingQuota: + def Droop(voters, seats): + return voters / (seats+1) + + def Hare(voters, seats): + return voters / seats class NoCandidatesLeftInRaceError(RuntimeError): pass diff --git a/pyrankvote/multiple_seat_ranking_methods.py b/pyrankvote/multiple_seat_ranking_methods.py index ff0d126..ed7457b 100644 --- a/pyrankvote/multiple_seat_ranking_methods.py +++ b/pyrankvote/multiple_seat_ranking_methods.py @@ -7,7 +7,7 @@ """ from typing import List -from pyrankvote.helpers import CompareMethodIfEqual, ElectionManager, ElectionResults +from pyrankvote.helpers import CompareMethodIfEqual, ElectionManager, ElectionResults, RankingQuota from pyrankvote.models import Candidate, Ballot import math @@ -125,7 +125,8 @@ def single_transferable_vote( ballots: List[Ballot], number_of_seats: int, compare_method_if_equal=CompareMethodIfEqual.MostSecondChoiceVotes, - pick_random_if_blank=False + pick_random_if_blank=False, + ranking_quota=RankingQuota.Droop ) -> ElectionResults: """ Single transferable vote (STV) is a multiple candidate election method, that elected the candidate that can @@ -134,9 +135,7 @@ def single_transferable_vote( If only one candidate can be elected, this method is the same as Instant runoff voting. Voters rank candidates and are granted as one vote each. If a candidate gets more votes than the threshold for being - elected, the candidate is proclaimed as winner. This function uses the Droop quota, where - - droop_quota = votes/(seats+1) + elected, the candidate is proclaimed as winner. If one candidate get more votes than the threshold the excess votes are transfered to voters that voted for this candidate's 2nd (or 3rd, 4th etc) alternative. If no candidate get over the threshold, the candidate with fewest votes @@ -157,7 +156,7 @@ def single_transferable_vote( election_results = ElectionResults() voters, seats = manager.get_number_of_non_exhausted_ballots(), number_of_seats - votes_needed_to_win: float = voters / float((seats + 1)) # Drop quota + votes_needed_to_win = ranking_quota(voters, seats) # Remove worst candidate until same number of candidates left as electable # While it is more candidates left than electable @@ -175,7 +174,7 @@ def single_transferable_vote( votes_for_candidate = candidates_in_race_votes[i] is_last_candidate = i == len(candidates_in_race) - 1 - # Elect candidates with more votes than Drop quota + # Elect candidates with more votes than the quota if (votes_for_candidate - rounding_error) >= votes_needed_to_win: candidates_to_elect.append(candidate) diff --git a/tests/test_multiple_seat_ranking_methods.py b/tests/test_multiple_seat_ranking_methods.py index 503658a..acd8434 100644 --- a/tests/test_multiple_seat_ranking_methods.py +++ b/tests/test_multiple_seat_ranking_methods.py @@ -3,7 +3,7 @@ import pyrankvote from pyrankvote import Candidate, Ballot -from pyrankvote.helpers import CandidateStatus +from pyrankvote.helpers import CandidateStatus, RankingQuota class TestPreferentialBlockVoting(unittest.TestCase): @@ -336,6 +336,35 @@ def test_case3(self): self.assertEqual(2, len(winners), "Function should return a list with two items") self.assertListEqual([per, paal], winners, "Winners should be Per and Pål") + def test_hare_quota(self): + #data taken from https://en.wikipedia.org/wiki/Comparison_of_the_Hare_and_Droop_quotas#Scenario_1 + Andrea = Candidate("Andrea") + Carter = Candidate("Carter") + Brad = Candidate("Brad") + Delilah = Candidate("Delilah") + Scott = Candidate("Scott") + Jennifer = Candidate("Jennifer") + + candidates = [Andrea, Carter, Brad, Delilah, Scott, Jennifer] + + ballots = ( + 31*[Ballot(ranked_candidates=[Andrea, Carter, Brad])] + + 30*[Ballot(ranked_candidates=[Carter, Andrea, Brad])] + + 2*[Ballot(ranked_candidates=[Brad, Andrea, Carter])] + + 20*[Ballot(ranked_candidates=[Delilah, Scott, Jennifer])] + + 20*[Ballot(ranked_candidates=[Scott, Delilah, Jennifer])] + + 17*[Ballot(ranked_candidates=[Jennifer, Delilah, Scott])] + ) + + election_result = pyrankvote.single_transferable_vote( + candidates, ballots, number_of_seats=5, ranking_quota=RankingQuota.Hare + ) + winners = election_result.get_winners() + + self.assertEqual(len(winners), 5, 'There should be 5 winners') + + self.assertEqual(winners, [Andrea, Carter, Delilah, Scott, Jennifer], "Winners should be Andrea, Carter, Delilah, Scott, Jennifer") + def test_example(self): popular_moderate = Candidate("William, popular moderate") moderate2 = Candidate("John, moderate")