Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hare quota option to STV #13

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pyrankvote/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions pyrankvote/multiple_seat_ranking_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)

Expand Down
31 changes: 30 additions & 1 deletion tests/test_multiple_seat_ranking_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down