Skip to content
Merged
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
42 changes: 17 additions & 25 deletions src/vse_sim/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,37 +525,27 @@ def runIrv(self, remaining, ncand):
remaining = self.eliminateCandidate(remaining, toEliminate)
return results

def resultsFor(self, voters, chooser, tally=None, **kwargs):
"""Tabulate the rank-vector ballots produced by this simulator.

IRV's public ``results`` API accepts candidate IDs in preference
order, including partial rankings. The simulator's honest and
strategic ballot producers instead return rank vectors, so translate
those ballots before invoking the tabulator.
"""
def orderingChooser(cls, voter, chooserTally):
ballot = chooser(cls, voter, chooserTally)
return sorted(range(len(ballot)), key=lambda candidate: ballot[candidate],
reverse=True)

orderingChooser.__name__ = chooser.__name__
return super().resultsFor(voters, orderingChooser, tally, **kwargs)

@staticmethod
def winner(results):
"""Return the winner from IRV's winner-first finish ordering.
def rankVectorToPreference(ballot):
"""Return candidate IDs in descending preference order from a rank vector."""
return sorted(range(len(ballot)), key=lambda candidate: ballot[candidate],
reverse=True)

Unlike score methods, :meth:`runIrv` returns candidate IDs ordered by
finish, with the winner at index zero.
"""
return results[0]
@staticmethod
def finishOrderToResults(finishOrder):
"""Convert winner-first finish order to high-is-better candidate scores."""
ncand = len(finishOrder)
results = [-1] * ncand
for score, candidate in enumerate(reversed(finishOrder)):
results[candidate] = score
return results

def results(self, ballots, **kwargs):
"""IRV results.

>>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
[2, 1, 0]
>>> Irv().winner([2,0,1])
[0, 1, 2]
>>> Irv().results([[0,1,2]])[2]
2
>>> Irv().results([[0,1,2],[2,1,0]])[1]
0
Expand All @@ -564,7 +554,9 @@ def results(self, ballots, **kwargs):
"""
if type(ballots) is not list:
ballots = list(ballots)
return self.runIrv(self.buildPreferenceSchedule(ballots), len(ballots[0]))
rankings = [self.rankVectorToPreference(ballot) for ballot in ballots]
finishOrder = self.runIrv(self.buildPreferenceSchedule(rankings), len(ballots[0]))
return self.finishOrderToResults(finishOrder)

@staticmethod #cls is provided explicitly, not through binding
@rememberBallot
Expand Down
18 changes: 17 additions & 1 deletion tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from scripts.recalculate_irv_pages import recalculate
from vse_sim.core import SideTally
from vse_sim.diagnostics import TRACE, setDebug, trace
from vse_sim.methods import Mav, Schulze, Score
from vse_sim.methods import Irv, Mav, Schulze, Score
from vse_sim.simulation import CsvBatch, seedRandomGenerators
from vse_sim.strategies import ProbChooser, beHon, beStrat
from vse_sim.voter_models import Electorate, Voter
Expand Down Expand Up @@ -75,6 +75,22 @@ def test_mav_cutoffs_are_scoped_to_generated_ballot_function():
assert low_ballot(Mav, Voter([-2, -1]), SideTally()) == expected


def test_irv_results_keep_simulator_score_contract_for_strategy():
method = Irv()
voters = Electorate(
[Voter([0, 1, 2])] * 4
+ [Voter([2, 1, 0])] * 3
+ [Voter([1, 2, 0])] * 2
)

results = method.resultsFor(voters, method.honBallot)["results"]
polls = sorted(enumerate(results), key=lambda candidate_result: -candidate_result[1])

assert results == [2, 0, 1]
assert method.winner(results) == 0
assert polls[0][0] == 0


@pytest.mark.parametrize(
"probabilities",
[
Expand Down
Loading