Source code for whalrus.profiles.profile

# -*- 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.converters_ballot.converter_ballot_general import ConverterBallotGeneral
from whalrus.utils.utils import cached_property, DeleteCacheMixin, convert_number
from whalrus.ballots.ballot import Ballot
from whalrus.ballots.ballot_order import BallotOrder
from typing import Union, Iterator
from numbers import Number


[docs]class Profile(DeleteCacheMixin): """ A profile of ballots. Parameters ---------- ballots : iterable Typically, it is a list, but it can also be a :class:`Profile`. Its elements must be :class:`Ballot` objects or, more generally, inputs that can be interpreted by :class:`ConverterBallotGeneral`. weights : list A list of numbers representing the weights of the ballots. Default: if :attr:`ballots` is a Profile, then use the weights of this profile; otherwise, all weights are 1. voters : list A list representing the voters corresponding to the ballots. Default: if :attr:`ballots` is a Profile, then use the voters of this profile; otherwise, all voters are None. Examples -------- Most general syntax: >>> profile = Profile( ... ballots=[BallotOrder('a > b ~ c'), BallotOrder('a ~ b > c')], ... weights=[2, 1], ... voters=['Alice', 'Bob'] ... ) >>> print(profile) Alice (2): a > b ~ c Bob (1): a ~ b > c In the following example, each ballot illustrates a different syntax: >>> profile = Profile([ ... ['a', 'b', 'c'], ... ('b', 'c', 'a'), ... 'c > a > b', ... ]) >>> print(profile) a > b > c b > c > a c > a > b Profiles have a list-like behavior in the sense that they implement ``__len__``, ``__getitem__``, ``__setitem__`` and ``__delitem__``: >>> profile = Profile(['a > b', 'b > a', 'a ~ b']) >>> len(profile) 3 >>> profile[0] BallotOrder(['a', 'b'], candidates={'a', 'b'}) >>> profile[0] = 'a ~ b' >>> print(profile) a ~ b b > a a ~ b >>> del profile[0] >>> print(profile) b > a a ~ b Profiles can be concatenated: >>> profile = Profile(['a > b', 'b > a']) + ['a ~ b'] >>> print(profile) a > b b > a a ~ b Profiles can be multiplied by a scalar, which multiplies the weights: >>> profile = Profile(['a > b', 'b > a']) * 3 >>> print(profile) (3): a > b (3): b > a """ def __init__(self, ballots: Union[list, 'Profile'], weights: list = None, voters: list = None): converter = ConverterBallotGeneral() self._ballots = [converter(b) for b in ballots] if weights is None: if isinstance(ballots, Profile): weights = ballots.weights else: weights = [1] * len(ballots) else: weights = [convert_number(w) for w in weights] self._weights = weights if voters is None: if isinstance(ballots, Profile): self._voters = ballots.voters else: self._voters = [None] * len(ballots) else: self._voters = voters @property def ballots(self) -> list: """list of Ballot: The ballots. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile.ballots [BallotOrder(['a', 'b'], candidates={'a', 'b'}), BallotOrder(['b', 'a'], candidates={'a', 'b'})] """ return self._ballots @property def weights(self) -> list: """list of Number: The weights. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile.weights [1, 1] """ return self._weights @property def voters(self) -> list: """list: The voters. Examples -------- >>> profile = Profile(['a > b', 'b > a'], voters=['Alice', 'Bob']) >>> profile.voters ['Alice', 'Bob'] """ return self._voters @cached_property def has_weights(self) -> bool: """bool: Presence of non-trivial weights. True iff at least one weight is not 1. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile.has_weights False """ return any([weight != 1 for weight in self.weights]) @cached_property def has_voters(self) -> bool: """bool: Presence of explicit voters. True iff at least one voter is not None. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile.has_voters False """ return any([voter is not None for voter in self.voters]) # Representation # ============== def __repr__(self) -> str: return 'Profile(ballots=%r, weights=%r, voters=%r)' % (self.ballots, self.weights, self.voters) def __str__(self) -> str: def i_to_str(i): prefix_elements = [] if self.has_voters: prefix_elements.append(str(self.voters[i])) if self.has_weights: prefix_elements.append('(' + str(self.weights[i]) + ')') prefix = ' '.join(prefix_elements) if prefix: prefix += ': ' return prefix + str(self.ballots[i]) return '\n'.join([i_to_str(i) for i in range(len(self.ballots))]) # List-like behavior # ==================
[docs] def append(self, ballot: object, weight: Number=1, voter: object=None) -> None: """ Append a ballot to the profile. Parameters ---------- ballot : object A ballot or, more generally, an input that can be interpreted by :class:`ConverterBallotGeneral`. weight : Number The weight of the ballot. voter : object The voter. Examples -------- >>> profile = Profile(['a > b']) >>> profile.append('b > a') >>> print(profile) a > b b > a """ self._ballots.append(ConverterBallotGeneral()(ballot)) self._weights.append(convert_number(weight)) self._voters.append(voter) self.delete_cache()
[docs] def remove(self, ballot: object=None, voter: object=None) -> None: """ Remove a ballot from the profile. If only the ballot is specified, remove the first matching ballot in the profile. If only the voter is specified, remove the first ballot whose voter matches the given voter. If both are specified, remove the first ballot matching both descriptions. Parameters ---------- ballot : object The ballot or, more generally, an input that can be interpreted by :class:`ConverterBallotGeneral`. voter : object The voter. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile.remove('b > a') >>> print(profile) a > b """ if ballot is None: i = next(i for i, v in enumerate(self.voters) if v == voter) elif voter is None: i = next(i for i, b in enumerate(self.ballots) if b == ConverterBallotGeneral()(ballot)) else: i = next(i for i, b in enumerate(self.ballots) if b == ConverterBallotGeneral()(ballot) and self.voters[i] == voter) del self._ballots[i] del self._voters[i] del self._weights[i] self.delete_cache()
def __len__(self) -> int: """int: Length. The number of ballots in the profile. Examples -------- >>> profile = Profile(['a > b', 'a > b', 'b > a']) >>> len(profile) 3 """ return len(self.ballots) def __getitem__(self, item: int) -> Ballot: """Get. Parameters ---------- item : int Returns ------- Ballot The corresponding ballot (this function does not return the weight or the voter). Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile[0] BallotOrder(['a', 'b'], candidates={'a', 'b'}) """ return self.ballots[item] def __setitem__(self, key: int, value: object) -> None: """Set. Set the corresponding ballot (it does not change the weight or the voter). Parameters ---------- key : int value : object The new ballot or, more generally, an input that is understandable by a :class:`ConverterBallotGeneral`. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile[0] = 'a ~ b' >>> print(profile) a ~ b b > a """ self._ballots[key] = ConverterBallotGeneral()(value) self.delete_cache() def __delitem__(self, key: int) -> None: """ Delete. Delete the corresponding ballot. Parameters ---------- key : int Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> del profile[0] >>> print(profile) b > a """ del self._ballots[key] del self._weights[key] del self._voters[key] self.delete_cache() # Dict-like behavior
[docs] def items(self) -> Iterator: """ Items of the profile. Returns ------- Iterator A zip of triples (ballot, weight, voter). Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> for ballot, weight, voter in profile.items(): ... print('Ballot %s, weight %s, voter %s.' % (ballot, weight, voter)) Ballot a > b, weight 1, voter None. Ballot b > a, weight 1, voter None. """ return zip(self.ballots, self.weights, self.voters)
# Some basic operations # ===================== def __add__(self, other: Union['Profile', list]) -> 'Profile': """ Concatenate with another profile. Parameters ---------- other : Profile or list Another Profile (or a list of ballots). Returns ------- Profile This profile, followed by the other profile. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> profile2 = Profile(['a ~ b']) >>> print(profile + profile2) a > b b > a a ~ b """ if isinstance(other, list): other = Profile(other) return Profile(ballots=self.ballots + other.ballots, weights=self.weights + other.weights, voters=self.voters + other.voters) def __mul__(self, other: Number) -> 'Profile': """ Multiply the weights. Parameters ---------- other : Number Returns ------- Profile This profile, with weights multiplied by the number. Examples -------- >>> profile = Profile(['a > b', 'b > a']) >>> print(profile * 3) (3): a > b (3): b > a """ other = convert_number(other) return Profile(ballots=self.ballots, weights=[convert_number(w * other) for w in self.weights], voters=self.voters)