# -*- 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_levels import ScorerLevels
from whalrus.scales.scale import Scale
from whalrus.scales.scale_from_list import ScaleFromList
from whalrus.rules.rule_score import RuleScore
from whalrus.converters_ballot.converter_ballot_to_levels import ConverterBallotToLevels
from whalrus.utils.utils import cached_property, NiceDict, my_division
from whalrus.converters_ballot.converter_ballot import ConverterBallot
[docs]class RuleMajorityJudgment(RuleScore):
"""
Majority Judgment.
Parameters
----------
args
Cf. parent class.
converter : ConverterBallot
Default: :class:`ConverterBallotToLevels`, with ``scale=scorer.scale``.
scorer : Scorer
Default: :class:`ScorerLevels`. Alternatively, you may provide an argument ``scale``. In that case, the scorer
will be ``ScorerLevels(scale)``.
default_median : object
The median level that a candidate has when it receives absolutely no evaluation whatsoever.
kwargs
Cf. parent class.
Examples
--------
>>> rule = RuleMajorityJudgment([{'a': 1, 'b': 1}, {'a': .5, 'b': .6},
... {'a': .5, 'b': .4}, {'a': .3, 'b': .2}])
>>> rule.scores_as_floats_
{'a': (0.5, -0.25, 0.25), 'b': (0.4, 0.5, -0.25)}
>>> rule.winner_
'a'
For each candidate, its median evaluation `m` is computed. When a candidate has two medians (like candidate `b`
in the above example, with .4 and .6), the lower value is considered. Let `p` (resp. `q`) denote the proportion of
the voters who evaluate the candidate better (resp. worse) than its median. The score of the candidate is the tuple
`(m, p, -q)` if `p > q`, and `(m, -q, p)` otherwise. Scores are compared lexicographically.
For Majority Judgment, verbal evaluation are generally used. The following example is actually the same as
above, but with verbal evaluations instead of grades:
>>> rule = RuleMajorityJudgment([
... {'a': 'Excellent', 'b': 'Excellent'}, {'a': 'Good', 'b': 'Very Good'},
... {'a': 'Good', 'b': 'Acceptable'}, {'a': 'Poor', 'b': 'To Reject'}
... ], scale=ScaleFromList(['To Reject', 'Poor', 'Acceptable', 'Good', 'Very Good', 'Excellent']))
>>> rule.scores_as_floats_
{'a': ('Good', -0.25, 0.25), 'b': ('Acceptable', 0.5, -0.25)}
>>> rule.winner_
'a'
By changing the ``scorer``, you may define a very different rule. The following one rewards the candidate with
best median Borda score (with secondary criteria that are similar to Majority Judgment, i.e. the proportions of
voters who give a candidate more / less than its median Borda score):
>>> from whalrus.scorers.scorer_borda import ScorerBorda
>>> from whalrus.converters_ballot.converter_ballot_to_order import ConverterBallotToOrder
>>> rule = RuleMajorityJudgment(scorer=ScorerBorda(), converter=ConverterBallotToOrder())
>>> rule(['a > b ~ c > d', 'c > a > b > d']).scores_as_floats_
{'a': (2.0, 0.5, 0.0), 'b': (1.0, 0.5, 0.0), 'c': (1.5, 0.5, 0.0), 'd': (0.0, 0.0, 0.0)}
>>> rule.winner_
'a'
"""
def __init__(self, *args, converter: ConverterBallot = None, scorer: Scorer = None,
scale: Scale = None, default_median: object = None, **kwargs):
# Default value
if scorer is None:
scorer = ScorerLevels(scale=scale)
if converter is None:
converter = ConverterBallotToLevels(scale=scorer.scale)
# Parameters
self.scorer = scorer
self.default_median = default_median
super().__init__(*args, converter=converter, **kwargs)
@cached_property
def scores_(self) -> NiceDict:
"""NiceDict: The scores. A :class:`NiceDict` of triples.
"""
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, 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
p = sum([weights_[c][i] for i, level in enumerate(levels_[c]) if self.scorer.scale.gt(level, median)])
q = sum([weights_[c][i] for i, level in enumerate(levels_[c]) if self.scorer.scale.lt(level, median)])
if p > q:
scores_[c] = (median, my_division(p, total_weight), -my_division(q, total_weight))
else:
scores_[c] = (median, -my_division(q, total_weight), my_division(p, total_weight))
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], one[2]) < (another[1], another[2]) 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.
"""
def my_float(x):
try:
return float(x)
except ValueError:
return x
return NiceDict({c: (my_float(s), float(x), float(y)) for c, (s, x, y) in self.scores_.items()})