Source code for poisson_approval.strategies.StrategyThreshold

from poisson_approval.constants.basic_constants import *
from poisson_approval.strategies.StrategyTwelve import StrategyTwelve
from poisson_approval.utils.DictPrintingInOrderIgnoringZeros import DictPrintingInOrderIgnoringZeros
from poisson_approval.utils.DictPrintingInOrderIgnoringNone import DictPrintingInOrderIgnoringNone
from poisson_approval.utils.UtilBallots import ballot_one, ballot_two, ballot_one_two, ballot_one_three
from poisson_approval.utils.UtilCache import cached_property


# noinspection PyUnresolvedReferences
[docs]class StrategyThreshold(StrategyTwelve): """A threshold strategy (for a cardinal profile). For each ranking, there is a ``threshold`` and a ``ratio_optimistic``. E.g. assume that for ranking ``abc``, the threshold is 0.4 and the ratio is 0.2. It means that: * A voter ``abc`` votes for `ab` if their utility for `b` is strictly greater than 0.4, * A voter ``abc`` votes for `a` if their utility for `b` is strictly lower than 0.4, * Voters ``abc`` whose utility for `b` is equal to 0.4 are split: a ratio 0.2 are optimistic, they vote only for `a` (they behave as if the pivot `ab` was very likely); and a ratio 0.8 are pessimistic, they vote for `ab` (they behave as if the pivot `bc` was very likely). For a given ranking, the threshold and/or the ratio may be None, which means that they are not specified for this strategy. Parameters ---------- d : dict Cf. examples below for the different types of input syntax. d_weak_order_ballot : dict Key: weak order. Value: strategy. A strategy can be a valid ballot, ``SPLIT`` or ``''`` if the behavior of these voters is not specified in the strategy. This is useful in two cases only: for "haters" (e.g. ``'a~b>c'``) in Plurality, and for "lovers" (e.g. ``'a>b~c'``) in Anti-Plurality. In all other cases, voters with a weak order have a dominant strategy and they automatically use it. About ``'SPLIT'``: for example, in Plurality, ``'a~b>c': SPLIT`` means that half of voters with weak order `abc` cast a ballot for `a`, and the other half for `b`. ratio_optimistic : Number Cf. examples below for the different types of input syntax. profile : Profile, optional The "context" in which the strategy is used. voting_rule : str The voting rule. Possible values are ``APPROVAL``, ``PLURALITY`` and ``ANTI_PLURALITY``. Default: the same voting rule as `profile` if a profile is specified, ``APPROVAL`` otherwise. Examples -------- The two following examples illustrate different ways to define the same profile. The first possible type of input syntax is a dict that maps a ranking to a tuple (``threshold``, ``ratio_optimistic``). It corresponds exactly to the attribute :attr:`d_ranking_t_threshold_ratio_optimistic`: >>> strategy = StrategyThreshold({'abc': (0.4, 0.2), 'bac': (0.51, 0.2), 'cab': (1, 0.2)}) >>> print(strategy) <abc: utility-dependent (0.4, 0.2), bac: utility-dependent (0.51, 0.2), cab: c> The second possible type of input syntax is a dict that maps a ranking to a threshold. All rankings have the same ratio, given by the parameter `ratio_optimistic`: >>> strategy = StrategyThreshold({'abc': 0.4, 'bac': 0.51, 'cab': 1}, ratio_optimistic=0.2) >>> print(strategy) <abc: utility-dependent (0.4, 0.2), bac: utility-dependent (0.51, 0.2), cab: c> Some operations on the strategy: >>> strategy StrategyThreshold({'abc': (0.4, 0.2), 'bac': (0.51, 0.2), 'cab': (1, 0.2)}) >>> strategy.abc 'utility-dependent' >>> strategy.a_bc 'a' >>> strategy.ab_c 'ab' >>> strategy.d_ranking_threshold['abc'] 0.4 It is possible not to specify the ratios of optimistic voters: >>> strategy = StrategyThreshold({'abc': 0.4, 'bac': 0.51}) >>> strategy StrategyThreshold({'abc': 0.4, 'bac': 0.51}) >>> print(strategy) <abc: utility-dependent (0.4), bac: utility-dependent (0.51)> If this strategy is applied to a profile where a positive share of voters have ranking `abc` and a utility 0.2 for their middle candidate `b`, it will raise an error because the ratio of optimistic voters is needed in that case. """ def __init__(self, d, d_weak_order_ballot=None, ratio_optimistic=None, profile=None, voting_rule=None): voting_rule = self._get_voting_rule_(profile, voting_rule) # Prepare the dictionaries of thresholds and ratios self.d_ranking_threshold = DictPrintingInOrderIgnoringNone({ranking: None for ranking in RANKINGS}) self.d_ranking_ratio_optimistic = DictPrintingInOrderIgnoringNone({ranking: None for ranking in RANKINGS}) for ranking, value in d.items(): if isinstance(value, tuple): self.d_ranking_threshold[ranking], self.d_ranking_ratio_optimistic[ranking] = value else: self.d_ranking_threshold[ranking] = value self.d_ranking_ratio_optimistic[ranking] = ratio_optimistic # Prepare the dictionary of ballots d_ranking_ballot = DictPrintingInOrderIgnoringZeros() for ranking, threshold in self.d_ranking_threshold.items(): if threshold is None: d_ranking_ballot[ranking] = '' elif threshold == 1: if voting_rule in {APPROVAL, PLURALITY}: d_ranking_ballot[ranking] = ballot_one(ranking) elif voting_rule == ANTI_PLURALITY: d_ranking_ballot[ranking] = ballot_one_three(ranking) else: raise NotImplementedError elif threshold == 0: if voting_rule in {APPROVAL, ANTI_PLURALITY}: d_ranking_ballot[ranking] = ballot_one_two(ranking) elif voting_rule == PLURALITY: d_ranking_ballot[ranking] = ballot_two(ranking) else: raise NotImplementedError else: d_ranking_ballot[ranking] = UTILITY_DEPENDENT # Call parent class super().__init__(d_ranking_ballot=d_ranking_ballot, d_weak_order_ballot=d_weak_order_ballot, profile=profile, voting_rule=voting_rule) @cached_property def d_ranking_t_threshold_ratio_optimistic(self): """dict : Thresholds and ratios of optimistic voters. Key : ranking, e.g. ``'abc'``. Value: tuple (``threshold``, ``ratio_optimistic``). """ return DictPrintingInOrderIgnoringNone({ ranking: (self.d_ranking_threshold[ranking], self.d_ranking_ratio_optimistic[ranking]) for ranking in RANKINGS }) def __eq__(self, other): """Equality test. Parameters ---------- other : object Returns ------- bool True if this strategy is equal to `other`. Examples -------- >>> strategy = StrategyThreshold({'abc': (0.4, 0.2), 'bac': (0.51, 0.2), 'cab': (1, 0.2)}) >>> strategy == StrategyThreshold({'abc': (0.4, 0.2), 'bac': (0.51, 0.2), 'cab': (1, 0.2)}) True """ return (isinstance(other, StrategyThreshold) and self.d_ranking_threshold == other.d_ranking_threshold and self.d_ranking_ratio_optimistic == other.d_ranking_ratio_optimistic and self.d_weak_order_ballot == other.d_weak_order_ballot and self.voting_rule == other.voting_rule) def __repr__(self): """ Examples -------- >>> strategy = StrategyThreshold({'abc': (0.4, 0.2)}, d_weak_order_ballot={'a~b>c': 'a'}, ... voting_rule=PLURALITY) >>> repr(strategy) "StrategyThreshold({'abc': (0.4, 0.2)}, d_weak_order_ballot={'a~b>c': 'a'}, voting_rule='Plurality')" """ d = DictPrintingInOrderIgnoringNone({ k: t[0] if t[1] is None else t for k, t in self.d_ranking_t_threshold_ratio_optimistic.items()}) arguments = repr(d) arguments_weak_orders = repr(self.d_weak_order_ballot) if len(arguments_weak_orders) > 2: arguments += ', d_weak_order_ballot=' + arguments_weak_orders if self.voting_rule != APPROVAL: arguments += ', voting_rule=%r' % self.voting_rule return 'StrategyThreshold(%s)' % arguments def __str__(self): """ Examples -------- >>> strategy = StrategyThreshold({'abc': (0.4, 0.2)}, d_weak_order_ballot={'a~b>c': 'a'}, ... voting_rule=PLURALITY) >>> str(strategy) '<abc: utility-dependent (0.4, 0.2), a~b>c: a> (Plurality)' """ def t_threshold_ratio_to_string(t): return str(t[0]) if t[1] is None else '%s, %s' % t arguments = ', '.join([ '%s: %s' % (ranking, self.d_ranking_ballot[ranking]) + (' (%s)' % t_threshold_ratio_to_string(self.d_ranking_t_threshold_ratio_optimistic[ranking]) if self.d_ranking_ballot[ranking] == UTILITY_DEPENDENT else '') for ranking in sorted(self.d_ranking_ballot) if self.d_ranking_ballot[ranking] ]) arguments_weak_orders = str(self.d_weak_order_ballot)[1:-1] if len(arguments_weak_orders) > 0: arguments += ', ' + arguments_weak_orders result = '<%s>' % arguments if self.profile is not None: result += ' ==> ' + str(self.winners) if self.voting_rule != APPROVAL: result += ' (%s)' % self.voting_rule return result def _repr_pretty_(self, p, cycle): # pragma: no cover - Only for notebooks # https://stackoverflow.com/questions/41453624/tell-ipython-to-use-an-objects-str-instead-of-repr-for-output p.text(str(self) if not cycle else '...')