def test_reallocation_of_surplus_votes(self): """ A single round should reallocate surplus votes from all candidates that have a surplus and then exclude the candidate with the fewest votes. """ vacancies = 3 candidates = ('Oranges', 'Apples', 'Pears', 'Lemons', 'Limes') votes = 9 * (('Oranges', 'Pears'), ) + \ 8 * (('Apples', 'Lemons'), ) + \ 1 * (('Lemons', ), ) expected_results = { 'provisionally_elected': { 'Oranges': 5, 'Apples': 5 }, 'continuing': { 'Pears': 4, 'Lemons': 4 }, 'excluded': { 'Limes': 0 } } stv_round = Round(vacancies, candidates, votes) stv_round.run() print stv_round.quota print stv_round.results() self.assertEqual(expected_results, stv_round.results())
def test_reallocation_of_votes_skips_provisionally_elected_candidates( self): """ When multiple candidates have been provisionally elected their surplus votes needs to be reallocated for each of them in turn. When the reallocation happens it should skip over the other candidates that have already been provisionally elected. """ vacancies = 3 candidates = ('Anna', 'Amy', 'Steve', 'Norm', 'Dom') votes = 5 * (('Anna', 'Norm', 'Amy'), ) + \ 3 * (('Norm', 'Amy'), ) + \ 2 * (('Amy', ), ) # Both Anna and Norm should be elected initially stv_round = Round(vacancies, candidates, votes) stv_round._provisionally_elect_candidates() expected_results = { 'provisionally_elected': { 'Anna': 5, 'Norm': 3 }, 'continuing': { 'Amy': 2, 'Dom': 0, 'Steve': 0 }, 'excluded': {} } self.assertEqual(expected_results, stv_round.results()) # When we reallocate Anna's votes they should go straight to Amy, # rather than going to Norm stv_round._reassign_votes_from_candidate_with_highest_surplus() expected_results = { 'provisionally_elected': { 'Anna': 3, 'Norm': 3 }, 'continuing': { 'Amy': 4, 'Dom': 0, 'Steve': 0 }, 'excluded': {} } self.assertEqual(expected_results, stv_round.results())
def test_bulk_eliminiation_resolves_tied_loser_failures(self): votes = (('A', 'D'), ('A', 'D'), ('B', 'E'), ('B', 'E'), ('C', 'F'), ('C', 'F'), ('C', 'F'), ('D', ), ('D', ), ('D', ), ('D', ), ('D', ), ('D', ), ('D', ), ('D', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('E', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', ), ('F', )) candidates = ['A', 'B', 'C', 'D', 'E', 'F'] vacancies = 2 stv_round = Round(vacancies, candidates, votes) stv_round.run() expected_results = { 'provisionally_elected': {}, 'continuing': { 'D': 8, 'E': 12, 'F': 13 }, 'excluded': { 'A': 2, 'B': 2, 'C': 3 }, } self.assertEqual(expected_results, stv_round.results())
def test_exclude_candidate_with_fewest_votes(self): """ Check that the method moves the candidate with the fewest vote total to excluded """ votes = (('Chocolate', ), ('Chocolate', ), ('Chocolate', ), ('Chocolate', ), ('Fruit', ), ('Fruit', ), ('Vegetables', )) candidates = ('Vegetables', 'Chocolate', 'Fruit') vacancies = 2 expected_results = { 'provisionally_elected': {}, 'continuing': { 'Chocolate': 4, 'Fruit': 2 }, 'excluded': { 'Vegetables': 1 } } stv_round = Round(vacancies, candidates, votes) stv_round._exclude_candidates_with_fewest_votes() self.assertEqual(expected_results, stv_round.results())
def test_reallocate_fractional_votes(self): """ This is the case where a candidates second preferences are split between other candidates so fractions of votes are reallocated """ votes = [ ['Amy', 'James'], ['Amy', 'James'], ['Amy', 'James'], ['Amy', 'James'], ['Amy', 'David'], ['Amy', 'David'], ['Amy', 'David'], ] candidates = ['Amy', 'James', 'David'] vacancies = 2 expected_result = { 'provisionally_elected': { 'Amy': 3 }, 'continuing': { 'James': Fraction(16, 7), 'David': Fraction(12, 7) }, 'excluded': {} } stv_round = Round(vacancies, candidates, votes) stv_round._provisionally_elect_candidates() stv_round._reassign_votes_from_candidate_with_highest_surplus() self.assertEqual(expected_result, stv_round.results())
def test_provisionally_elect_candidates_auto_fills_vacancies(self): """ If there are as many remaining vacancies as remaining candidates then all remaining candidates should be elected even if they don't meet the quota. In this example, the quota is 3 votes and we expected candidate C to be elected even though they only have 2 votes. """ votes = (('A', ), ('A', ), ('A', ), ('A', ), ('B', ), ('B', ), ('B', ), ('C', ), ('C', )) candidates = ('A', 'B', 'C') vacancies = 3 stv_round = Round(vacancies, candidates, votes) stv_round._provisionally_elect_candidates() expected_totals = { 'provisionally_elected': { 'A': 4, 'B': 3, 'C': 2 }, 'continuing': {}, 'excluded': {} } self.assertEqual(expected_totals, stv_round.results())
def test_provisionally_elect_candidates(self): """ Tests that candidates at or above the quota are marked as provisionally elected """ votes = (('A', ), ('A', ), ('A', ), ('A', ), ('B', ), ('B', ), ('B', ), ('C', )) candidates = ('A', 'B', 'C') vacancies = 2 stv_round = Round(vacancies, candidates, votes) stv_round._provisionally_elect_candidates() expected_totals = { 'provisionally_elected': { 'A': 4, 'B': 3 }, 'continuing': { 'C': 1 }, 'excluded': {} } self.assertEqual(expected_totals, stv_round.results())
def test_exhausted_votes_are_not_reallocated(self): """ A vote with no further preferences for other candidates shouldn't be reallocated. In this example Anna starts with 9 votes which means she has 3 surplus votes above the quota of 6. Second preference choices for Anna's votes are split equally between no preference, Steve and Norm. Steve and Norm should get 1 vote each and the final vote should disappear. """ votes = ( ('Anna', ), ('Anna', ), ('Anna', ), ('Anna', 'Steve'), ('Anna', 'Steve'), ('Anna', 'Steve'), ('Anna', 'Norm'), ('Anna', 'Norm'), ('Anna', 'Norm'), ('Steve', ), ('Steve', ), ('Steve', ), ('Steve', ), ('Norm', ), ('Norm', ), ('Norm', ), ) candidates = ('Anna', 'Steve', 'Norm') vacancies = 2 expected_results = { 'provisionally_elected': { 'Anna': 6 }, 'continuing': { 'Steve': 5, 'Norm': 4 }, 'excluded': {} } stv_round = Round(vacancies, candidates, votes) stv_round._provisionally_elect_candidates() stv_round._reassign_votes_from_candidate_with_highest_surplus() self.assertEqual(expected_results, stv_round.results())
def test_reallocate_surplus_votes(self): """ This test is where only one of the candidates has met the quota. We are testing that their surplus votes are redistributed correctly. """ votes = [ ['Green', 'Blue', 'Yellow', 'Red'], ['Green', 'Blue', 'Yellow', 'Red'], ['Green', 'Blue', 'Yellow', 'Red'], ['Green', 'Blue', 'Yellow', 'Red'], ['Green', 'Blue', 'Red', 'Yellow'], ['Green', 'Blue', 'Red', 'Yellow'], ['Green', 'Yellow', 'Red', 'Blue'], ['Green', 'Yellow', 'Red', 'Blue'], ['Green', 'Yellow', 'Blue', 'Red'], ['Red', 'Blue', 'Yellow', 'Green'], ['Red', 'Blue', 'Green', 'Yellow'], ['Blue', 'Green', 'Red', 'Yellow'], ['Blue', 'Yellow', 'Red', 'Green'], ['Yellow', 'Blue', 'Red', 'Green'], ['Yellow', 'Green', 'Red', 'Blue'], ['Yellow', 'Red', 'Green', 'Blue'], ['Yellow', 'Blue', 'Green', 'Red'], ] candidates = ('Green', 'Blue', 'Yellow', 'Red') vacancies = 2 expected_reallocated_totals = { 'provisionally_elected': { 'Green': 6 }, 'continuing': { 'Red': 2, 'Blue': 4, 'Yellow': 5 }, 'excluded': {} } stv_round = Round(vacancies, candidates, votes) stv_round._provisionally_elect_candidates() stv_round._reassign_votes_from_candidate_with_highest_surplus() self.assertEqual(expected_reallocated_totals, stv_round.results())
def test_tied_really_low_fewest_candidates_excludes_both(self): """ In this case, two candidates are tied for last place but they have so few votes they couldn't win. The calculation here is - if their votes added together are not enough to reach the next candidate or the quota, we don't have to worry about who to eliminate first and can eliminate both at the same time. """ votes = (('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Beatles', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Rolling Stones', ), ('Killers', ), ('Killers', ), ('Killers', ), ('Killers', ), ('Killers', ), ('Blur', ), ('Blur', ), ('Pulp', ), ('Pulp', )) candidates = ('Beatles', 'Rolling Stones', 'Killers', 'Blur', 'Pulp') vacancies = 3 # Note that we expect them to be in continuing # rather than provisionally elected as we are # just calling the method, not the whole round expected_results = { 'provisionally_elected': {}, 'continuing': { 'Beatles': 12, 'Rolling Stones': 9, 'Killers': 5, }, 'excluded': { 'Blur': 2, 'Pulp': 2 } } stv_round = Round(vacancies, candidates, votes) stv_round._exclude_candidates_with_fewest_votes() self.assertEqual(expected_results, stv_round.results())
def test_candidates_should_be_elected_once_there_is_one_per_vacanc(self): """ As soon as there are the same number of remaining candidates as vacancies the election is completed with all of the remaining candidates elected as winners. Note that this makes it possible for a candidate to be elected even without enough votes to reach the quota, as in this example. Surplus votes for candidates A and B become exhausted votes because there is no further preference to reallocate them to. Candidate D is then excluded because they have the fewest votes. Since this leaves only three candidates for three vacancies candidate C is declared elected, even though they have only one vote compared to the quota of three. """ vacancies = 3 candidates = ('A', 'B', 'C', 'D') votes = 5 * (('A', 'B'), ) + \ 4 * (('B', 'A'), ) + \ 1 * (('C', ), ) expected_results = { 'provisionally_elected': { 'A': 3, 'B': 3, 'C': 1 }, 'continuing': {}, 'excluded': { 'D': 0 }, } stv = Round(vacancies, candidates, votes) stv.run() self.assertEqual(expected_results, stv.results()) self.assertTrue(stv.all_vacancies_filled())
def test_tied_winners_should_cause_election_to_fail_without_a_random_generator( self): """ When there's a tie between winners arbitrarily choosing one to reallocate surplus from first may impact the election result. If a random generator is not passed in then a FailedElectionError should be thrown in cases of ambiguity. When a random generator is provided it should be used to break ties of winners. This test demonstrates both outcomes of a random tie being broken by mocking two versions of a random generator. """ vacancies = 4 candidates = ('A', 'B', 'C', 'D', 'E') votes = 9 * (('A', 'C'), ) + \ 9 * (('B', 'C', 'D'), ) + \ 3 * (('C', ), ) + \ 2 * (('D', ), ) + \ 3 * (('E', ), ) stv_round = Round(vacancies, candidates, votes) with self.assertRaises(FailedElectionError): stv_round.run() class MockRandom(object): def choice(self, sequence): return sequence[0] expected_results = { 'provisionally_elected': { 'A': 6, 'B': 6, 'C': 6, 'D': 5 }, 'continuing': {}, 'excluded': { 'E': 3 } } stv_round = Round(vacancies, candidates, votes, random=MockRandom()) stv_round.run() self.assertEqual(expected_results, stv_round.results()) class MockRandom(object): def choice(self, sequence): return sequence[1] expected_results = { 'provisionally_elected': { 'A': 6, 'B': 6, 'C': 6, 'E': 3 }, 'continuing': {}, 'excluded': { 'D': 2 } } stv_round = Round(vacancies, candidates, votes, random=MockRandom()) stv_round.run() self.assertEqual(expected_results, stv_round.results())
def test_reallocate_candidate_reaching_quota(self): """ This test steps through the run round algorithm, demonstrating each step. Only one candidate reaches the quota initially, but reallocation causes another to reach quota and require reallocation of their now surplus votes. Note that this reallocation of Mars's now surplus votes requires their devaluing to have been recorded, i.e. it's not 8 at the end of the first iteration, it's 11 votes worth 8/11ths each. """ votes = [ ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Crunchie'], ['Galaxy', 'Mars', 'Bounty'], ] vacancies = 3 candidates = ['Mars', 'Bounty', 'Galaxy', 'Crunchie'] # At this stage, we have calculated initial totals but # done no more work with the votes stv_round = Round(vacancies, candidates, votes) initial_totals = { 'provisionally_elected': {}, 'continuing': { 'Mars': 0, 'Bounty': 0, 'Galaxy': 11, 'Crunchie': 0, }, 'excluded': {} } self.assertEqual(initial_totals, stv_round.results()) # Now we run the first pass of calculating which # candidates are provisionally elected stv_round._provisionally_elect_candidates() first_provisional_election_totals = { 'provisionally_elected': { 'Galaxy': 11, }, 'continuing': { 'Mars': 0, 'Bounty': 0, 'Crunchie': 0, }, 'excluded': {} } self.assertEqual(first_provisional_election_totals, stv_round.results()) # Now we reassign Galaxy's surplus votes to the # next preferences of votes for Galaxy. # Note that the 8 for Mars is actually 11 votes each worth 8/11ths. stv_round._reassign_votes_from_candidate_with_highest_surplus() first_reallocation_totals = { 'provisionally_elected': { 'Galaxy': 3, }, 'continuing': { 'Mars': 8, 'Bounty': 0, 'Crunchie': 0, }, 'excluded': {} } self.assertEqual(first_reallocation_totals, stv_round.results()) # After reallocation, we run a provisional election again to see if any # new candidates have reached the quota stv_round._provisionally_elect_candidates() second_provisional_election_totals = { 'provisionally_elected': { 'Galaxy': 3, 'Mars': 8, }, 'continuing': { 'Bounty': 0, 'Crunchie': 0, }, 'excluded': {} } self.assertEqual(second_provisional_election_totals, stv_round.results()) # Now Mars has reached the quota, we need to reallocate # Mars's surplus votes to the next preferences of votes # for Mars. # However, votes that have been allocated to Mars from Galaxy # are not worth 1, they are worth 8/11 because of their # previous reallocation from Galaxy. stv_round._reassign_votes_from_candidate_with_highest_surplus() second_reallocation_totals = { 'provisionally_elected': { 'Galaxy': 3, 'Mars': 3, }, 'continuing': { 'Bounty': Fraction(5,11), 'Crunchie': 4 + Fraction(6, 11) }, 'excluded': {} } self.assertEqual(second_reallocation_totals, stv_round.results()) # We now provisonally elect candidates again to see if anyone else has # reached the quota - Crunchie has stv_round._provisionally_elect_candidates() third_provisional_election_total = { 'provisionally_elected': { 'Galaxy': 3, 'Mars': 3, 'Crunchie': 4 + Fraction(6, 11) }, 'continuing': { 'Bounty': Fraction(5,11), }, 'excluded': {} } self.assertEqual(third_provisional_election_total, stv_round.results()) # Now that we have three provisionally elected candidates for the three # vacancies, the election is over self.assertTrue(stv_round.all_vacancies_filled())