Exemple #1
0
    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())
Exemple #3
0
    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())
Exemple #11
0
    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())
Exemple #12
0
    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())
Exemple #13
0
    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())