Source code for whalrus.rules.rule_bucklin_instant

# -*- coding: utf-8 -*-
"""
Copyright Sylvain Bouveret, Yann Chevaleyre and François Durand
sylvain.bouveret@imag.fr, yann.chevaleyre@dauphine.fr, fradurand@gmail.com

This file is part of Whalrus.

Whalrus is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Whalrus is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Whalrus.  If not, see <http://www.gnu.org/licenses/>.
"""
from whalrus.scorers.scorer import Scorer
from whalrus.scorers.scorer_borda import ScorerBorda
from whalrus.rules.rule_score import RuleScore
from whalrus.rules.rule_bucklin_by_rounds import RuleBucklinByRounds
from whalrus.converters_ballot.converter_ballot_to_order import ConverterBallotToOrder
from whalrus.utils.utils import cached_property, NiceDict, my_division, convert_number
from whalrus.converters_ballot.converter_ballot import ConverterBallot
from whalrus.profiles.profile import Profile


[docs]class RuleBucklinInstant(RuleScore): """ Bucklin's rule (instant version). For each candidate, its median Borda score `m` is computed. Let `x` be the number of voters who give this candidate a Borda score that is greater or equal to `m`. Then the candidate's score is `(m, x)`. Scores are compared lexicographically. When preferences are strict orders, it is equivalent to say that: * The candidate with the lowest median rank is declared the winner. * If several candidates have the lowest median rank, this tie is broken by examining how many voters rank each of them with this rank or better. For another variant of Bucklin's rule, cf. :class:`RuleBucklinByRounds`. Parameters ---------- args Cf. parent class. converter : ConverterBallot Default: :class:`ConverterBallotToOrder`. scorer : Scorer Default: :class:`ScorerBorda` with ``absent_give_points=True``, ``absent_receive_points=None``, ``unordered_give_points=True``, ``unordered_receive_points=False``. default_median : object The default median of a candidate when it receives no score whatsoever. kwargs Cf. parent class. Examples -------- >>> rule = RuleBucklinInstant(ballots=['a > b > c', 'b > a > c', 'c > a > b']) >>> rule.scores_ {'a': (1, 3), 'b': (1, 2), 'c': (0, 3)} >>> rule.winner_ 'a' With the default settings, and when preferences are strict total orders, :class:`RuleBucklinByRounds` and :class:`RuleBucklinInstant` have the same winner (although not necessarily the same order over the candidates). Otherwise, the winners may differ: >>> profile = Profile(ballots=['a > b > c > d', 'b > a ~ d > c', 'c > a ~ d > b'], ... weights=[3, 3, 4]) >>> rule_bucklin_by_rounds = RuleBucklinByRounds(profile) >>> rule_bucklin_by_rounds.detailed_scores_[0] {'a': Fraction(3, 10), 'b': Fraction(3, 10), 'c': Fraction(2, 5), 'd': 0} >>> rule_bucklin_by_rounds.detailed_scores_[1] {'a': Fraction(13, 20), 'b': Fraction(3, 5), 'c': Fraction(2, 5), 'd': Fraction(7, 20)} >>> rule_bucklin_by_rounds.winner_ 'a' >>> rule_bucklin_instant = RuleBucklinInstant(profile) >>> rule_bucklin_instant.scores_ {'a': (Fraction(3, 2), 10), 'b': (2, 6), 'c': (1, 7), 'd': (Fraction(3, 2), 7)} >>> RuleBucklinInstant(profile).winner_ 'b' """ def __init__(self, *args, converter: ConverterBallot = None, scorer: Scorer = None, default_median: object = 0, **kwargs): # Default value if converter is None: converter = ConverterBallotToOrder() if scorer is None: scorer = ScorerBorda(absent_give_points=True, absent_receive_points=None, unordered_give_points=True, unordered_receive_points=False) # Parameters self.scorer = scorer self.default_median = default_median super().__init__(*args, converter=converter, **kwargs) @cached_property def scores_(self) -> NiceDict: levels_ = NiceDict({c: [] for c in self.candidates_}) weights_ = NiceDict({c: [] for c in self.candidates_}) for ballot, weight, voter in self.profile_converted_.items(): for c, level in self.scorer(ballot=ballot, voter=voter, candidates=self.candidates_).scores_.items(): levels_[c].append(level) weights_[c].append(weight) scores_ = NiceDict() for c in self.candidates_: if not levels_[c]: scores_[c] = (self.default_median, 0) continue indexes = self.scorer.scale.argsort(levels_[c]) levels_[c] = [levels_[c][i] for i in indexes] weights_[c] = [weights_[c][i] for i in indexes] total_weight = sum(weights_[c]) half_total_weight = my_division(total_weight, 2) cumulative_weight = 0 median = None for i, weight in enumerate(weights_[c]): cumulative_weight += weight if cumulative_weight >= half_total_weight: median = levels_[c][i] break support = convert_number(sum([ weights_[c][i] for i, level in enumerate(levels_[c]) if self.scorer.scale.ge(level, median)])) scores_[c] = (median, support) return scores_
[docs] def compare_scores(self, one: tuple, another: tuple) -> int: if one == another: return 0 if self.scorer.scale.lt(one[0], another[0]): return -1 if self.scorer.scale.gt(one[0], another[0]): return 1 return -1 if one[1] < another[1] else 1
@cached_property def scores_as_floats_(self) -> NiceDict: """NiceDict: Scores as floats. It is the same as :attr:`scores_`, but converted to floats. Examples -------- >>> rule = RuleBucklinInstant(ballots=['a > b > c', 'b > a > c', 'c > a > b']) >>> rule.scores_as_floats_ {'a': (1.0, 3.0), 'b': (1.0, 2.0), 'c': (0.0, 3.0)} """ def my_float(x): try: return float(x) except ValueError: return x return NiceDict({c: (my_float(s), float(x)) for c, (s, x) in self.scores_.items()})