Source code for svvamp.preferences.profile

# -*- coding: utf-8 -*-
"""
Created on Mon Sep 22 15:24:52 2014
Copyright François Durand 2014-2018
fradurand@gmail.com

This file is part of SVVAMP.

    SVVAMP 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.

    SVVAMP 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 SVVAMP.  If not, see <http://www.gnu.org/licenses/>.
"""

import itertools
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# noinspection PyUnresolvedReferences
from mpl_toolkits.mplot3d import Axes3D

from svvamp.utils.printing import printm, print_title
from svvamp.utils import my_log
from svvamp.utils.util_cache import cached_property
from svvamp.utils.misc import initialize_random_seeds
from svvamp.utils.misc import preferences_ut_to_preferences_rk, preferences_rk_to_preferences_borda_rk, \
    preferences_ut_to_preferences_borda_ut, preferences_ut_to_matrix_duels_ut, is_resistant_condorcet, \
    matrix_victories_to_smith_set
from svvamp.utils.pseudo_bool import equal_true
from svvamp.preferences.plurality_elimination_engine_profile import PluralityEliminationEngineProfile


[docs] class Profile(my_log.MyLog): """Create a profile of voters with preferences over some candidates. Parameters ---------- preferences_ut : list of list (or 2d ndarray) 2d array of floats. ``preferences_ut[v, c]`` is the utility of candidate ``c`` as seen by voter ``v``. preferences_rk : list of list (or 2d ndarray) 2d array of integers. ``preferences_rk[v, k]`` is the candidate at rank ``k`` for voter ``v``. preferences_borda_rk : list of list (or 2d ndarray) 2d array of integers. ``preferences_borda_rk[v, c]`` gains 1 point for each candidate ``d`` such that voter ``v`` ranks ``c`` before ``d``. So, these Borda scores are between ``0`` and ``C - 1``. log_creation : object Any type (string, list...). Some comments. labels_candidates : list of str Names of the candidates. sort_voters : bool If True, then when the profile is created, voters are immediately sorted, first by their strict order of preference (their row in :attr:`~svvamp.Population.preferences_rk`), then by their weak order of preference (their row in :attr:`~svvamp.Population.preferences_borda_ut`). Note that two voters having the same strict order may have different weak orders, and vice versa. The objective of this sorting is to accelerate some algorithms (typically Individual Manipulation). It can also be convenient to visualize the profile. Notes ----- You may enter ``preferences_ut``, ``preferences_rk`` or both to define the preferences of the population. In all cases, instead of ``preferences_rk``, you may enter ``preferences_borda_rk``. If you provide ``preferences_rk`` only, then ``preferences_ut`` is set to the corresponding Borda scores (:attr:`~svvamp.Population.preferences_borda_rk`). If you provide ``preferences_ut`` only, then ``preferences_rk`` is naturally derived from utilities. If voter ``v`` has a greater utility for candidate ``c`` than for candidate ``d``, then she ranks ``c`` before ``d``. If voter ``v`` attributes the same utility to several candidates, then the first time the attribute ``preferences_rk`` is called, a random ranking will be decided for these tied candidates (once and for all). If you provide both, then SVVAMP DOES NOT CHECK that they are consistent. You should ensure that they are, in the sense that if ``v`` ranks ``c`` before ``d``, then she her utility for ``c`` must be at least equal to her utility for ``d``. Similarly, you can provide both ``preferences_rk`` and ``preferences_borda_rk``, but SVVAMP DOES NOT CHECK that they are consistent. ``preferences_rk`` will be used for sincere voting when a voting system accepts only strict orders. In contrast, for manipulation purposes, ``preferences_ut`` is always used , which means that indifference is taken into account as such to determine the interest in manipulation. If voter ``v`` attributes the same utility to candidates ``w`` and ``c``, and if ``w`` is the sincere winner in an election, then ``v`` is not interested in a manipulation for ``c``. If all voters have a strict order of preference (in the sense of ut), then for functions below having variants with suffix ``_ut`` or ``_rk``, the two variants are equivalent. In some voting systems and in some of the attributes below, we use a process referred as **candidate tie-breaking** or **CTB** in SVVAMP. It means that lowest-index candidates are favored. When using CTB, if candidates ``c`` and ``d`` are tied (for example, in an election using Plurality), then ``c`` is favored over ``d`` iff ``c < d``. Implications between majority favorite and Condorcet criteria (cf. corresponding attributes below). :: majority_favorite_ut ==> majority_favorite_ut_ctb || || || || V || || || Resistant Cond. V V || || majority_favorite_rk ==> majority_favorite_rk_ctb || || || || || V || || V Condorcet_ut_abs ==> Condorcet_ut_abs_ctb || || || || || || || V V V V || V Condorcet_rk ==> Condorcet_rk_ctb V Condorcet_ut_rel ==> Condorcet_ut_rel_ctb || V Weak Condorcet || V Condorcet-admissible If all voters have strict orders of preference (in the sense of ut) and if there is an odd number of voters, then: * ``majority_favorite_ut``, ``majority_favorite_rk``, ``majority_favorite_ut_ctb`` and ``majority_favorite_rk_ctb`` are equivalent, * ``Condorcet_ut_abs``, ``Condorcet_ut_abs_ctb``, ``Condorcet_rk``, ``Condorcet_rk_ctb``, ``Condorcet_ut_rel``, ``Condorcet_ut_rel_ctb``, ``Weak Condorcet`` and ``Condorcet-admissible`` are equivalent. Examples -------- >>> profile = Profile(preferences_borda_rk=[[2, 1, 0], [1, 0, 2]]) >>> profile.preferences_rk array([[0, 1, 2], [2, 0, 1]]) >>> profile = Profile(preferences_ut=[[1, .3, 0], [.7, 0, 1]]) >>> profile.preferences_rk array([[0, 1, 2], [2, 0, 1]]) >>> profile = Profile() >>> profile.preferences_rk Traceback (most recent call last): ValueError: Please provide at least preferences_rk, preferences_borda_rk or preferences_ut. """ def __init__(self, preferences_ut=None, preferences_rk=None, preferences_borda_rk=None, log_creation=None, labels_candidates=None, sort_voters=False): super().__init__(log_identity="PROFILE") if preferences_ut is None: self._preferences_ut_input = None else: self._preferences_ut_input = np.array(preferences_ut) if preferences_rk is None: self._preferences_rk_input = None else: self._preferences_rk_input = np.array(preferences_rk) if preferences_borda_rk is None: self._preferences_borda_rk_input = None else: self._preferences_borda_rk_input = np.array(preferences_borda_rk) self.log_creation = log_creation self._labels_candidates = labels_candidates self._sort_voters = sort_voters # %% Basic variables @cached_property def n_v(self): """int : Number of voters.""" if self._preferences_rk_input is not None: n_v = self._preferences_rk_input.shape[0] else: n_v = self._preferences_ut_input.shape[0] # if n_v < 2: # raise ValueError("A profile must have at least 2 voters.") return n_v @cached_property def n_c(self): """int : Number of candidates.""" if self._preferences_rk_input is not None: n_c = self._preferences_rk_input.shape[1] else: n_c = self._preferences_ut_input.shape[1] if n_c < 2: raise ValueError("A profile must have at least 2 candidates.") return n_c @cached_property def _preferences_rk_unsorted(self): """preferences_rk, before sorting by voter.""" if self._preferences_rk_input is not None: return self._preferences_rk_input if self._preferences_borda_rk_input is not None: return preferences_ut_to_preferences_rk(self._preferences_borda_rk_input) if self._preferences_ut_input is not None: return preferences_ut_to_preferences_rk(self._preferences_ut_input) raise ValueError('Please provide at least preferences_rk, preferences_borda_rk or preferences_ut.') @cached_property def _preferences_borda_rk_unsorted(self): """preferences_borda_rk, before sorting by voter.""" if self._preferences_borda_rk_input is not None: return self._preferences_borda_rk_input else: return preferences_rk_to_preferences_borda_rk(self._preferences_rk_unsorted) @cached_property def _preferences_ut_unsorted(self): """preferences_ut, before sorting by voter.""" if self._preferences_ut_input is not None: return self._preferences_ut_input else: return self._preferences_borda_rk_unsorted @cached_property def _preferences_borda_ut_unsorted(self): """preferences_borda_ut, before sorting by voter.""" return preferences_ut_to_preferences_borda_ut(self._preferences_ut_unsorted) @cached_property def _index_sort_voters(self): """Index used to sort by voter.""" # Sort voters by strict ranking, then by weak order (deducted from utility) sort_criterion = np.concatenate( (self._preferences_rk_unsorted, self._preferences_borda_ut_unsorted), axis=1 ).tolist() # noinspection PyUnresolvedReferences indexes = sorted(range(self.n_v), key=sort_criterion.__getitem__) return indexes @cached_property def preferences_rk(self): """2d array of integers. ``preferences_rk[v, k]`` is the candidate at rank ``k`` for voter ``v``. For example, ``preferences_rk[v, 0]`` is ``v``'s preferred candidate. """ if self._sort_voters: return self._preferences_rk_unsorted[self._index_sort_voters, ::] else: return self._preferences_rk_unsorted @cached_property def preferences_ut(self): """2d array of floats. ``preferences_ut[v, c]`` is the utility of candidate ``c`` as seen by voter ``v``.""" if self._sort_voters: return self._preferences_ut_unsorted[self._index_sort_voters, ::] else: return self._preferences_ut_unsorted @cached_property def preferences_borda_rk(self): """2d array of integers. ``preferences_borda_rk[v, c]`` gains 1 point for each candidate ``d`` such that voter ``v`` ranks ``c`` before ``d``. So, these Borda scores are between ``0`` and ``C - 1``. """ if self._sort_voters: return self._preferences_borda_rk_unsorted[self._index_sort_voters, ::] else: return self._preferences_borda_rk_unsorted @cached_property def preferences_borda_ut(self): """2d array of floats. ``preferences_borda_ut[v, c]`` gains 1 point for each ``d`` such that ``v`` strictly prefers ``c`` to ``d`` (in the sense of utilities), and 0.5 point for each ``d`` such that ``v`` is indifferent between ``c`` and ``d``. So, these Borda scores are between ``0`` and ``C - 1``. """ if self._sort_voters: return self._preferences_borda_ut_unsorted[self._index_sort_voters, ::] else: return self._preferences_borda_ut_unsorted @property def labels_candidates(self): """List of :attr:`~svvamp.Population.n_c` strings (names of the candidates). """ if self._labels_candidates is None: self._labels_candidates = [str(x) for x in range(self.n_c)] return self._labels_candidates @labels_candidates.setter def labels_candidates(self, value): self._labels_candidates = value # %% Identify voters with the same ordinal preferences. @cached_property def v_has_same_ordinal_preferences_as_previous_voter(self): """1d array of booleans. ``v_has_same_ordinal_preferences_as_previous_voter[v]`` is ``True`` iff voter ``v`` has the same preference strict order (row in :attr:`~svvamp.Population.preferences_rk`) and the same preference weak order (row in :attr:`~svvamp.Population.preferences_borda_ut`) as voter ``v-1``. By convention, it is ``False`` for voter ``0``. >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.v_has_same_ordinal_preferences_as_previous_voter array([False, True]) """ self.mylog("Compute v_has_same_ordinal_preferences_as_previous_voter", 1) return np.concatenate(( [False], np.logical_and( np.all(self.preferences_rk[range(self.n_v - 1), :] == self.preferences_rk[range(1, self.n_v), :], 1), np.all(self.preferences_borda_ut[range(self.n_v - 1), :] == self.preferences_borda_ut[range(1, self.n_v), :], 1) ) )) # %% Plurality scores @cached_property def plurality_scores_rk(self): """1d array of int. ``plurality_scores_rk[c]`` is the number of voters for whom ``c`` is the top-ranked candidate (with voter tie-breaking). >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.plurality_scores_rk array([2, 0, 0]) """ self.mylog("Compute Plurality scores (rk)", 1) return np.array(np.bincount(self.preferences_rk[:, 0], minlength=self.n_c), dtype=int) @cached_property def plurality_scores_ut(self): """1d array of int. ``plurality_scores_ut[c]`` is the number of voters who strictly prefer ``c`` to all other candidates. If a voter has several candidates with maximal utility, then none of them receives any point. >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[2, 1, 0], [0, 1, 1]]) >>> profile.plurality_scores_ut array([1, 0, 0]) """ self.mylog("Compute Plurality scores (ut)", 1) ut_max = np.max(self.preferences_ut, axis=1) nb_favorites = np.sum(self.preferences_ut == ut_max[:, np.newaxis], axis=1) favorite = np.argmax(self.preferences_ut, axis=1) return np.array(np.bincount(favorite[nb_favorites == 1], minlength=self.n_c), dtype=int) # %% Matrix of duels @cached_property def matrix_duels_ut(self): """2d array of integers. ``matrix_duels_ut[c, d]`` is the number of voters who have a strictly greater utility for ``c`` than for ``d``. By convention, diagonal coefficients are set to ``0``. >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.matrix_duels_ut array([[0, 2, 2], [0, 0, 2], [0, 0, 0]]) """ self.mylog("Compute matrix of duels", 1) return preferences_ut_to_matrix_duels_ut(self.preferences_borda_ut) @cached_property def matrix_duels_rk(self): """2d array of integers. ``matrix_duels_rk[c, d]`` is the number of voters who rank candidate ``c`` before ``d`` (in the sense of :attr:`~svvamp.Population.preferences_rk`). By convention, diagonal coefficients are set to 0. >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.matrix_duels_rk array([[0, 2, 2], [0, 0, 2], [0, 0, 0]]) """ self.mylog("Compute matrix of duels (with strict orders)", 1) return preferences_ut_to_matrix_duels_ut(self.preferences_borda_rk) @cached_property def matrix_victories_ut_abs(self): """2d array of values in {0, 0.5, 1}. Matrix of absolute victories based on :attr:`~svvamp.Population.matrix_duels_ut`. ``matrix_victories_ut_abs[c, d]`` is: * 1 iff ``matrix_duels_ut[c, d] > V / 2``. * 0.5 iff ``matrix_duels_ut[c, d] = V / 2``. * 0 iff ``matrix_duels_ut[c, d] < V / 2``. By convention, diagonal coefficients are set to 0. Examples -------- A victory: >>> profile = Profile(preferences_ut=[[1, 0], [1, 0]]) >>> profile.matrix_victories_ut_abs array([[0., 1.], [0., 0.]]) A half-victory, but the other candidate has lost: >>> profile = Profile(preferences_ut=[[1, 0], [42, 42]]) >>> profile.matrix_victories_ut_abs array([[0. , 0.5], [0. , 0. ]]) A half-victory for each candidate: >>> profile = Profile(preferences_ut=[[1, 0], [0, 1]]) >>> profile.matrix_victories_ut_abs array([[0. , 0.5], [0.5, 0. ]]) A defeat for each candidate: >>> profile = Profile(preferences_ut=[[0, 0], [0, 0]]) >>> profile.matrix_victories_ut_abs array([[0., 0.], [0., 0.]]) """ self.mylog("Compute matrix_victories_ut_abs", 1) return (np.multiply(0.5, self.matrix_duels_ut >= self.n_v / 2) + np.multiply(0.5, self.matrix_duels_ut > self.n_v / 2)) @cached_property def matrix_victories_ut_abs_ctb(self): """2d array of values in {0, 1}. Matrix of absolute victories based on :attr:`~svvamp.Population.matrix_duels_ut`, with tie-breaks on candidates. ``matrix_victories_ut_abs_ctb[c, d]`` is: * 1 iff ``matrix_duels_ut[c, d] > V / 2``, or ``matrix_duels_ut[c, d] = V / 2`` and ``c < d``. * 0 iff ``matrix_duels_ut[c, d] < V / 2``, or ``matrix_duels_ut[c, d] = V / 2`` and ``d < c``. By convention, diagonal coefficients are set to 0. Examples -------- A victory (without tie-breaking): >>> profile = Profile(preferences_ut=[[1, 0], [1, 0]]) >>> profile.matrix_victories_ut_abs_ctb array([[0., 1.], [0., 0.]]) A victory with tie-breaking: >>> profile = Profile(preferences_ut=[[1, 0], [0, 1]]) >>> profile.matrix_victories_ut_abs_ctb array([[0., 1.], [0., 0.]]) Another case of victory with tie-breaking: >>> profile = Profile(preferences_ut=[[1, 0], [42, 42]]) >>> profile.matrix_victories_ut_abs_ctb array([[0., 1.], [0., 0.]]) A defeat for each candidate: >>> profile = Profile(preferences_ut=[[0, 0], [0, 0]]) >>> profile.matrix_victories_ut_abs_ctb array([[0., 0.], [0., 0.]]) """ self.mylog("Compute matrix_victories_ut_abs_ctb", 1) result = np.zeros((self.n_c, self.n_c)) for c in range(self.n_c): for d in range(self.n_c): if c == d: continue if self.matrix_duels_ut[c, d] > self.n_v / 2 or (self.matrix_duels_ut[c, d] == self.n_v / 2 and c < d): result[c, d] = 1 return result @cached_property def matrix_victories_ut_rel(self): """2d array of values in {0, 0.5, 1}. Matrix of relative victories based on :attr:`~svvamp.Population.matrix_duels_ut`. ``matrix_victories_ut_rel[c, d]`` is: * 1 iff ``matrix_duels_ut[c, d] > matrix_duels_ut[d, c]``. * 0.5 iff ``matrix_duels_ut[c, d] = matrix_duels_ut[d, c]``. * 0 iff ``matrix_duels_ut[c, d] < matrix_duels_ut[d, c]``. By convention, diagonal coefficients are set to 0. Examples -------- A relative victory (which is not an absolute victory): >>> profile = Profile(preferences_ut=[[1, 0], [0, 0], [0, 0]]) >>> profile.matrix_victories_ut_rel array([[0., 1.], [0., 0.]]) A tie: >>> profile = Profile(preferences_ut=[[1, 0], [0, 1], [0, 0]]) >>> profile.matrix_victories_ut_rel array([[0. , 0.5], [0.5, 0. ]]) """ self.mylog("Compute matrix_victories_ut_rel", 1) return ( np.multiply(0.5, self.matrix_duels_ut >= self.matrix_duels_ut.T) + np.multiply(0.5, self.matrix_duels_ut > self.matrix_duels_ut.T) - np.multiply(0.5, np.eye(self.n_c)) ) @cached_property def matrix_victories_ut_rel_ctb(self): """2d array of values in {0, 1}. Matrix of relative victories based on :attr:`~svvamp.Population.matrix_duels_ut`, with tie-breaks on candidates. ``matrix_victories_ut_rel_ctb[c, d]`` is: * 1 iff ``matrix_duels_ut[c, d] > matrix_duels_ut[d, c]``, or ``matrix_duels_ut[c, d] = matrix_duels_ut[d, c]`` and ``c < d``. * 0 iff ``matrix_duels_ut[c, d] < matrix_duels_ut[d, c]``, or ``matrix_duels_ut[c, d] = matrix_duels_ut[d, c]`` and ``d < c``. By convention, diagonal coefficients are set to 0. Examples -------- A relative victory (without tie-breaking): >>> profile = Profile(preferences_ut=[[1, 0], [0, 0], [0, 0]]) >>> profile.matrix_victories_ut_rel_ctb array([[0., 1.], [0., 0.]]) A relative victory with tie-breaking: >>> profile = Profile(preferences_ut=[[1, 0], [0, 1], [0, 0]]) >>> profile.matrix_victories_ut_rel_ctb array([[0., 1.], [0., 0.]]) """ self.mylog("Compute matrix_victories_ut_rel_ctb", 1) result = np.zeros((self.n_c, self.n_c)) for c in range(self.n_c): for d in range(c + 1, self.n_c): if self.matrix_duels_ut[c, d] >= self.matrix_duels_ut[d, c]: result[c, d] = 1 result[d, c] = 0 else: result[c, d] = 0 result[d, c] = 1 return result @cached_property def matrix_victories_rk(self): """2d array of values in {0, 0.5, 1}. Matrix of victories based on :attr:`~svvamp.Population.matrix_duels_rk`. ``matrix_victories_rk[c, d]`` is: * 1 iff ``matrix_duels_rk[c, d] > matrix_duels_rk[d, c]``, i.e. iff ``matrix_duels_rk[c, d] > V / 2``. * 0.5 iff ``matrix_duels_rk[c, d] = matrix_duels_rk[d, c]``, i.e. iff ``matrix_duels_rk[c, d] = V / 2``. * 0 iff ``matrix_duels_rk[c, d] < matrix_duels_rk[d, c]``, i.e. iff ``matrix_duels_rk[c, d] < V / 2``. By convention, diagonal coefficients are set to 0. Examples -------- A victory: >>> profile = Profile(preferences_rk=[[0, 1], [0, 1]]) >>> profile.matrix_victories_rk array([[0., 1.], [0., 0.]]) A tie: >>> profile = Profile(preferences_rk=[[0, 1], [1, 0]]) >>> profile.matrix_victories_rk array([[0. , 0.5], [0.5, 0. ]]) """ self.mylog("Compute matrix_victories_rk", 1) return (np.multiply(0.5, self.matrix_duels_rk >= self.n_v / 2) + np.multiply(0.5, self.matrix_duels_rk > self.n_v / 2)) @cached_property def matrix_victories_rk_ctb(self): """2d array of values in {0, 1}. Matrix of victories based on :attr:`~svvamp.Population.matrix_duels_rk`, with tie-breaks on candidates. ``matrix_victories_rk_ctb[c, d]`` is: * 1 iff ``matrix_duels_rk[c, d] > matrix_duels_rk[d, c]``, or ``matrix_duels_rk[c, d] = matrix_duels_rk[d, c]`` and ``c < d``. * 0 iff ``matrix_duels_rk[c, d] < matrix_duels_rk[d, c]``, or ``matrix_duels_rk[c, d] = matrix_duels_rk[d, c]`` and ``d < c``. By convention, diagonal coefficients are set to 0. Examples -------- A victory (without tie-breaking): >>> profile = Profile(preferences_rk=[[0, 1], [0, 1]]) >>> profile.matrix_victories_rk_ctb array([[0., 1.], [0., 0.]]) A victory with tie-breaking: >>> profile = Profile(preferences_rk=[[0, 1], [1, 0]]) >>> profile.matrix_victories_rk_ctb array([[0., 1.], [0., 0.]]) """ self.mylog("Compute matrix_victories_rk_ctb", 1) result = np.zeros((self.n_c, self.n_c)) for c in range(self.n_c): for d in range(c + 1, self.n_c): if self.matrix_duels_rk[c, d] >= self.n_v / 2: result[c, d] = 1 result[d, c] = 0 else: result[c, d] = 0 result[d, c] = 1 return result # %% Existence of Condorcet order @cached_property def exists_condorcet_order_rk_ctb(self): """Boolean. True iff in matrix :attr:`~svvamp.Population.matrix_victories_rk_ctb`, there is a candidate with ``n_c - 1`` victories, a candidate with ``n_c - 2`` victories, etc. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_order_rk_ctb True See Also -------- :attr:`~svvamp.Population.not_exists_condorcet_order_rk_ctb`. """ self.mylog("Compute exists_condorcet_order_rk_ctb", 1) temp = np.copy(self.matrix_victories_rk_ctb.sum(1)) temp.sort() return np.array_equal(range(self.n_c), temp) @cached_property def not_exists_condorcet_order_rk_ctb(self): """Boolean. Cf. :attr:`~svvamp.Population.exists_condorcet_order_rk_ctb`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_order_rk_ctb False """ return not self.exists_condorcet_order_rk_ctb @cached_property def exists_condorcet_order_rk(self): """Boolean. True iff in matrix :attr:`~svvamp.Population.matrix_victories_rk`, there is a candidate with ``n_c - 1`` victories, a candidate with ``n_c - 2`` victories, etc. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_order_rk True See Also -------- :attr:`~svvamp.Population.not_exists_condorcet_order_rk`. """ self.mylog("Compute exists_condorcet_order_rk", 1) temp = np.copy(self.matrix_victories_rk.sum(1)) temp.sort() return np.array_equal(range(self.n_c), temp) @cached_property def not_exists_condorcet_order_rk(self): """Boolean. Cf. :attr:`~svvamp.Population.exists_condorcet_order_rk`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_order_rk False """ return not self.exists_condorcet_order_rk @cached_property def exists_condorcet_order_ut_abs(self): """Boolean. True iff in matrix :attr:`~svvamp.Population.matrix_victories_ut_abs`, there is a candidate with ``n_c - 1`` victories, a candidate with ``n_c - 2`` victories, etc. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_order_ut_abs True See Also -------- :attr:`~svvamp.Population.not_exists_condorcet_order_ut_abs`. """ self.mylog("Compute exists_condorcet_order_ut_abs", 1) temp = np.copy(self.matrix_victories_ut_abs.sum(1)) temp.sort() return np.array_equal(range(self.n_c), temp) @cached_property def not_exists_condorcet_order_ut_abs(self): """Boolean. Cf. :attr:`~svvamp.Population.exists_condorcet_order_ut_abs`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_order_ut_abs False """ return not self.exists_condorcet_order_ut_abs @cached_property def exists_condorcet_order_ut_abs_ctb(self): """Boolean. True iff in matrix :attr:`~svvamp.Population.matrix_victories_ut_abs_ctb`, there is a candidate with ``n_c - 1`` victories, a candidate with ``n_c - 2`` victories, etc. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_order_ut_abs_ctb True See Also -------- :attr:`~svvamp.Population.not_exists_condorcet_order_ut_abs_ctb`. """ self.mylog("Compute exists_condorcet_order_ut_abs_ctb", 1) temp = np.copy(self.matrix_victories_ut_abs_ctb.sum(1)) temp.sort() return np.array_equal(range(self.n_c), temp) @cached_property def not_exists_condorcet_order_ut_abs_ctb(self): """Boolean. Cf. :attr:`~svvamp.Population.exists_condorcet_order_ut_abs_ctb`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_order_ut_abs_ctb False """ return not self.exists_condorcet_order_ut_abs_ctb @cached_property def exists_condorcet_order_ut_rel(self): """Boolean. True iff in matrix :attr:`~svvamp.Population.matrix_victories_ut_rel`, there is a candidate with ``n_c - 1`` victories, a candidate with ``n_c - 2`` victories, etc. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_order_ut_rel True See Also -------- :attr:`~svvamp.Population.not_exists_condorcet_order_ut_rel`. """ self.mylog("Compute exists_condorcet_order_ut_rel", 1) temp = np.copy(self.matrix_victories_ut_rel.sum(1)) temp.sort() return np.array_equal(range(self.n_c), temp) @cached_property def not_exists_condorcet_order_ut_rel(self): """Boolean. Cf. :attr:`~svvamp.Population.exists_condorcet_order_ut_rel`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_order_ut_rel False """ return not self.exists_condorcet_order_ut_rel @cached_property def exists_condorcet_order_ut_rel_ctb(self): """Boolean. True iff in matrix :attr:`~svvamp.Population.matrix_victories_ut_rel_ctb`, there is a candidate with ``n_c - 1`` victories, a candidate with ``n_c - 2`` victories, etc. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_order_ut_rel_ctb True See Also -------- :attr:`~svvamp.Population.not_exists_condorcet_order_ut_rel_ctb`. """ self.mylog("Compute exists_condorcet_order_ut_rel_ctb", 1) temp = np.copy(self.matrix_victories_ut_rel_ctb.sum(1)) temp.sort() return np.array_equal(range(self.n_c), temp) @cached_property def not_exists_condorcet_order_ut_rel_ctb(self): """Boolean. Cf. :attr:`~svvamp.Population.exists_condorcet_order_ut_rel_ctb`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_order_ut_rel_ctb False """ return not self.exists_condorcet_order_ut_rel_ctb # %% Condorcet winner and variants @cached_property def condorcet_admissible_candidates(self): """1d array of booleans. ``condorcet_admissible_candidates[c]`` is ``True`` iff candidate ``c`` is Condorcet-admissible, i.e. iff no candidate ``d`` has an absolute victory against ``c`` (in the sense of :attr:`~svvamp.Population.matrix_victories_ut_abs`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.condorcet_admissible_candidates array([ True, False, False]) See Also -------- :attr:`~svvamp.Population.nb_condorcet_admissible`, :attr:`~svvamp.Population.exists_condorcet_admissible`, :attr:`~svvamp.Population.not_exists_condorcet_admissible`. """ self.mylog("Compute Condorcet-admissible candidates", 1) return np.all(self.matrix_victories_ut_abs <= 0.5, 0) @cached_property def nb_condorcet_admissible(self): """Integer (number of Condorcet-admissible candidates, :attr:`~svvamp.Population.condorcet_admissible_candidates`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.nb_condorcet_admissible 1 """ self.mylog("Compute number of Condorcet-admissible candidates", 1) return int(np.sum(self.condorcet_admissible_candidates)) @cached_property def exists_condorcet_admissible(self): """Boolean (``True`` iff there is at least one Condorcet-admissible candidate, :attr:`~svvamp.Population.condorcet_admissible_candidates`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_admissible True """ return bool(np.any(self.condorcet_admissible_candidates)) @cached_property def not_exists_condorcet_admissible(self): """Boolean (``True`` iff there is no Condorcet-admissible candidate, :attr:`~svvamp.Population.condorcet_admissible_candidates`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_admissible False """ return not self.exists_condorcet_admissible @cached_property def weak_condorcet_winners(self): """1d array of booleans. ``weak_condorcet_winners[c]`` is ``True`` iff candidate ``c`` is a weak Condorcet winner, i.e. iff no candidate ``d`` has a relative victory against ``c`` (in the sense of :attr:`~svvamp.Population.matrix_victories_ut_rel`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.weak_condorcet_winners array([ True, False, False]) See Also -------- :attr:`~svvamp.Population.nb_weak_condorcet_winners`, :attr:`~svvamp.Population.exists_weak_condorcet_winner`, :attr:`~svvamp.Population.not_exists_weak_condorcet_winner`. """ self.mylog("Compute weak Condorcet winners", 1) return np.all(self.matrix_victories_ut_rel <= 0.5, 0) @cached_property def nb_weak_condorcet_winners(self): """Integer (number of weak Condorcet winners, :attr:`~svvamp.Population.weak_condorcet_winners`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.nb_weak_condorcet_winners 1 """ self.mylog("Compute number of weak Condorcet winners", 1) return int(np.sum(self.weak_condorcet_winners)) @cached_property def exists_weak_condorcet_winner(self): """Boolean (``True`` iff there is at least one weak Condorcet winner, :attr:`~svvamp.Population.weak_condorcet_winners`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_weak_condorcet_winner True """ return bool(np.any(self.weak_condorcet_winners)) @cached_property def not_exists_weak_condorcet_winner(self): """Boolean (``True`` iff there is no weak Condorcet winner, :attr:`~svvamp.Population.weak_condorcet_winners`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_weak_condorcet_winner False """ return not self.exists_weak_condorcet_winner @cached_property def condorcet_winner_rk_ctb(self): """Integer or ``NaN``. Candidate who has only victories in :attr:`~svvamp.Population.matrix_victories_rk_ctb`. If there is no such candidate, then ``NaN``. Examples -------- >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.condorcet_winner_rk_ctb 0 No Condorcet winner: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_rk_ctb nan See Also -------- :attr:`~svvamp.Population.exists_condorcet_winner_rk_ctb`, :attr:`~svvamp.Population.not_exists_condorcet_winner_rk_ctb`. """ self.mylog("Compute condorcet_winner_rk_ctb", 1) for c in range(self.n_c): # The whole COLUMN must be 0. if np.array_equiv(self.matrix_victories_rk_ctb[:, c], 0): return c return np.nan @cached_property def exists_condorcet_winner_rk_ctb(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.condorcet_winner_rk_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_winner_rk_ctb True """ return not np.isnan(self.condorcet_winner_rk_ctb) @cached_property def not_exists_condorcet_winner_rk_ctb(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.condorcet_winner_rk_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_winner_rk_ctb False """ return bool(np.isnan(self.condorcet_winner_rk_ctb)) @cached_property def condorcet_winner_rk(self): """Integer or ``NaN``. Candidate who has only victories in :attr:`~svvamp.Population.matrix_victories_rk`. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.condorcet_winner_rk 0 No Condorcet winner: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_rk nan See Also -------- :attr:`~svvamp.Population.exists_condorcet_winner_rk`, :attr:`~svvamp.Population.not_exists_condorcet_winner_rk`. """ self.mylog("Compute condorcet_winner_rk", 1) for c in range(self.n_c): # The whole COLUMN must be 0. if np.array_equiv(self.matrix_victories_rk[:, c], 0): return c return np.nan @cached_property def exists_condorcet_winner_rk(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.condorcet_winner_rk`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_winner_rk True """ return not np.isnan(self.condorcet_winner_rk) @cached_property def not_exists_condorcet_winner_rk(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.condorcet_winner_rk`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_winner_rk False """ return bool(np.isnan(self.condorcet_winner_rk)) @cached_property def condorcet_winner_ut_rel_ctb(self): """Integer or ``NaN``. Candidate who has only victories in :attr:`~svvamp.Population.matrix_victories_ut_rel_ctb`. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.condorcet_winner_ut_rel_ctb 0 No Condorcet winner: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_ut_rel_ctb nan See Also -------- :attr:`~svvamp.Population.exists_condorcet_winner_ut_rel_ctb`, :attr:`~svvamp.Population.not_exists_condorcet_winner_ut_rel_ctb`. """ self.mylog("Compute condorcet_winner_ut_rel_ctb", 1) for c in range(self.n_c): # The whole COLUMN must be 0. if np.array_equiv(self.matrix_victories_ut_rel_ctb[:, c], 0): return c return np.nan @cached_property def exists_condorcet_winner_ut_rel_ctb(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.condorcet_winner_ut_rel_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_winner_ut_rel_ctb True """ return not np.isnan(self.condorcet_winner_ut_rel_ctb) @cached_property def not_exists_condorcet_winner_ut_rel_ctb(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.condorcet_winner_ut_rel_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_winner_ut_rel_ctb False """ return bool(np.isnan(self.condorcet_winner_ut_rel_ctb)) @cached_property def condorcet_winner_ut_rel(self): """Integer or ``NaN``. Candidate who has only victories in :attr:`~svvamp.Population.matrix_victories_ut_rel`. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.condorcet_winner_ut_rel 0 No Condorcet winner: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_ut_rel nan See Also -------- :attr:`~svvamp.Population.exists_condorcet_winner_ut_rel`, :attr:`~svvamp.Population.not_exists_condorcet_winner_ut_rel`. """ self.mylog("Compute condorcet_winner_ut_rel", 1) for c in range(self.n_c): # The whole COLUMN must be 0. if np.array_equiv(self.matrix_victories_ut_rel[:, c], 0): return c return np.nan @cached_property def exists_condorcet_winner_ut_rel(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.condorcet_winner_ut_rel`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_winner_ut_rel True """ return not np.isnan(self.condorcet_winner_ut_rel) @cached_property def not_exists_condorcet_winner_ut_rel(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.condorcet_winner_ut_rel`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_winner_ut_rel False """ return bool(np.isnan(self.condorcet_winner_ut_rel)) @cached_property def condorcet_winner_ut_abs_ctb(self): """Integer or ``NaN``. Candidate who has only victories in :attr:`~svvamp.Population.matrix_victories_ut_abs_ctb`. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.condorcet_winner_ut_abs_ctb 0 No Condorcet winner: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_ut_abs_ctb nan See Also -------- :attr:`~svvamp.Population.exists_condorcet_winner_ut_abs_ctb`, :attr:`~svvamp.Population.not_exists_condorcet_winner_ut_abs_ctb` """ self.mylog("Compute condorcet_winner_ut_abs_ctb", 1) for c in range(self.n_c): if np.array_equiv(self.matrix_victories_ut_abs_ctb[c, np.array(range(self.n_c)) != c], 1): return c return np.nan @cached_property def exists_condorcet_winner_ut_abs_ctb(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.condorcet_winner_ut_abs_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_winner_ut_abs_ctb True """ return not np.isnan(self.condorcet_winner_ut_abs_ctb) @cached_property def not_exists_condorcet_winner_ut_abs_ctb(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.condorcet_winner_ut_abs_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_winner_ut_abs_ctb False """ return bool(np.isnan(self.condorcet_winner_ut_abs_ctb)) @cached_property def condorcet_winner_ut_abs(self): """Integer or ``NaN``. Candidate who has only victories in :attr:`~svvamp.Population.matrix_victories_ut_abs`. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.condorcet_winner_ut_abs 0 No Condorcet winner: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_ut_abs nan See Also -------- :attr:`~svvamp.Population.exists_condorcet_winner_ut_abs`, :attr:`~svvamp.Population.not_exists_condorcet_winner_ut_abs`. """ self.mylog("Compute Condorcet winner", 1) for c in range(self.n_c): if np.array_equiv(self.matrix_victories_ut_abs[c, np.array(range(self.n_c)) != c], 1): return c return np.nan @cached_property def exists_condorcet_winner_ut_abs(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.condorcet_winner_ut_abs`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_condorcet_winner_ut_abs True """ return not np.isnan(self.condorcet_winner_ut_abs) @cached_property def not_exists_condorcet_winner_ut_abs(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.condorcet_winner_ut_abs`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_condorcet_winner_ut_abs False """ return bool(np.isnan(self.condorcet_winner_ut_abs)) # %% Resistant Condorcet winner @cached_property def resistant_condorcet_winner(self): """Integer or ``NaN``. Resistant Condorcet Winner. If there is no such candidate, then ``NaN``. A Condorcet winner ``w`` (in the sense of :attr:`~svvamp.Population.condorcet_winner_ut_abs`) is resistant iff in any Condorcet voting system (in the same sense), the profile is not manipulable (cf. Durand et al.). This is equivalent to say that for any pair ``(c, d)`` of other distinct candidates, there is a strict majority of voters who simultaneously: 1) Do not prefer ``c`` to ``w``, 2) And prefer ``w`` to ``d``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.resistant_condorcet_winner 0 No resistant Condorcet winner: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.resistant_condorcet_winner nan See Also -------- :attr:`~svvamp.Population.exists_resistant_condorcet_winner`, :attr:`~svvamp.Population.not_exists_resistant_condorcet_winner`. """ self.mylog("Compute Resistant Condorcet winner", 1) if is_resistant_condorcet(self.condorcet_winner_ut_abs, self.preferences_ut): return self.condorcet_winner_ut_abs return np.nan @cached_property def exists_resistant_condorcet_winner(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.resistant_condorcet_winner`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_resistant_condorcet_winner True """ return not np.isnan(self.resistant_condorcet_winner) @cached_property def not_exists_resistant_condorcet_winner(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.resistant_condorcet_winner`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_resistant_condorcet_winner False """ return bool(np.isnan(self.resistant_condorcet_winner)) @cached_property def threshold_c_prevents_w_condorcet_ut_abs(self): """2d array of integers. Threshold for ``c``-manipulators to prevent ``w`` from being a Condorcet winner (in the sense of :attr:`~svvamp.Population.condorcet_winner_ut_abs`). Intuitively, the question is the following: in an election where ``w`` is the winner, how many ``c``-manipulators are needed to prevent ``w`` from being a Condorcet winner? We start with the sub-population of :math:`n_s` 'sincere' voters, i.e. not preferring ``c`` to ``w``. The precise question is: how many ``c``-manipulators :math:`n_m` must we add in order to create a non-victory for ``w`` against some candidate ``d`` :math:`\\neq` ``w`` (possibly ``c`` herself)? In the following, :math:`| c > d |` denotes the number of voters who strictly prefer candidate ``c`` to ``d``. We need: .. math:: | \\text{sincere voters for whom } w > d | \\leq \\frac{n_s + n_m}{2}. I.e.: .. math:: | \\text{non } c > w \\text{ and } w > d | \\leq \\frac{| \\text{non } c > w | + n_m}{2}. I.e.: .. math:: n_m \\geq 2 \\cdot | \\text{non } c > w \\text{ and } w > d | - | \\text{non } c > w |. One candidate ``d`` is enough, so: ``threshold_c_prevents_w_Condorcet_ut_abs[c, w]`` :math:`= 2 \\cdot \\min_{d \\neq w} |w \\geq c \\text{ and } w > d| - |w \\geq c|`. If this result is negative, it means that even without ``c``-manipulators, ``w`` is not a Condorcet winner. In that case, threshold is set to 0 instead. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.threshold_c_prevents_w_condorcet_ut_abs array([[0, 0, 0], [2, 0, 0], [2, 0, 0]]) """ self.mylog("Compute threshold_c_prevents_w_Condorcet_ut_abs", 1) result = np.full((self.n_c, self.n_c), 42) for w in range(self.n_c): for c in range(self.n_c): if c == w: result[c, w] = 0 continue v_does_not_prefer_c_to_w = (self.preferences_ut[:, w] >= self.preferences_ut[:, c]) for d in range(self.n_c): if d == w: continue # But d == c is allowed (useful if self.C == 2) v_prefers_w_to_d = (self.preferences_ut[:, w] > self.preferences_ut[:, d]) threshold_c_makes_w_not_win_against_d = ( np.multiply(2, np.sum(np.logical_and(v_does_not_prefer_c_to_w, v_prefers_w_to_d))) - np.sum(v_does_not_prefer_c_to_w) ) result[c, w] = np.minimum(result[c, w], threshold_c_makes_w_not_win_against_d) result = np.maximum(result, 0) return result # %% Smith Set @cached_property def smith_set_rk_ctb(self): """List. Smith set computed from :attr:`~svvamp.Population.matrix_victories_rk_ctb`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2, 3], [1, 2, 0, 3], [2, 0, 1, 3]]) >>> profile.smith_set_rk_ctb [0, 1, 2] """ return matrix_victories_to_smith_set(self.matrix_victories_rk_ctb) @cached_property def smith_set_rk(self): """List. Smith set computed from :attr:`~svvamp.Population.matrix_victories_rk`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2, 3], [1, 2, 0, 3], [2, 0, 1, 3]]) >>> profile.smith_set_rk [0, 1, 2] """ return matrix_victories_to_smith_set(self.matrix_victories_rk) @cached_property def smith_set_ut_rel_ctb(self): """List. Smith set computed from :attr:`~svvamp.Population.matrix_victories_ut_rel_ctb`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2, 3], [1, 2, 0, 3], [2, 0, 1, 3]]) >>> profile.smith_set_ut_rel_ctb [0, 1, 2] """ return matrix_victories_to_smith_set(self.matrix_victories_ut_rel_ctb) @cached_property def smith_set_ut_rel(self): """List. Smith set computed from :attr:`~svvamp.Population.matrix_victories_ut_rel`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2, 3], [1, 2, 0, 3], [2, 0, 1, 3]]) >>> profile.smith_set_ut_rel [0, 1, 2] """ return matrix_victories_to_smith_set(self.matrix_victories_ut_rel) @cached_property def smith_set_ut_abs_ctb(self): """List. Smith set computed from :attr:`~svvamp.Population.matrix_victories_ut_abs_ctb`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2, 3], [1, 2, 0, 3], [2, 0, 1, 3]]) >>> profile.smith_set_ut_abs_ctb [0, 1, 2] """ return matrix_victories_to_smith_set(self.matrix_victories_ut_abs_ctb) @cached_property def smith_set_ut_abs(self): """List. Smith set computed from :attr:`~svvamp.Population.matrix_victories_ut_abs`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2, 3], [1, 2, 0, 3], [2, 0, 1, 3]]) >>> profile.smith_set_ut_abs [0, 1, 2] """ return matrix_victories_to_smith_set(self.matrix_victories_ut_abs) # %% Majority favorite @cached_property def majority_favorite_rk_ctb(self): """Integer or ``NaN``. Candidate who has strictly more than n_v/2 in :attr:`~svvamp.Population.plurality_scores_rk`. Can also be candidate 0 with exactly n_v/2. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.majority_favorite_rk_ctb 0 No majority favorite: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.majority_favorite_rk_ctb nan See Also -------- :attr:`~svvamp.Population.exists_majority_favorite_rk_ctb`, :attr:`~svvamp.Population.not_exists_majority_favorite_rk_ctb`. """ self.mylog("Compute majority favorite (rk_ctb)", 1) c = int(np.argmax(self.plurality_scores_rk)) score_c = self.plurality_scores_rk[c] if score_c > self.n_v / 2 or (c == 0 and score_c == self.n_v / 2): return c return np.nan @cached_property def exists_majority_favorite_rk_ctb(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.majority_favorite_rk_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_majority_favorite_rk_ctb True """ return not np.isnan(self.majority_favorite_rk_ctb) @cached_property def not_exists_majority_favorite_rk_ctb(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.majority_favorite_rk_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_majority_favorite_rk_ctb False """ return bool(np.isnan(self.majority_favorite_rk_ctb)) @cached_property def majority_favorite_ut_ctb(self): """Integer or ``NaN``. Candidate who has strictly more than n_v/2 in :attr:`~svvamp.Population.plurality_scores_ut`. Can also be candidate 0 with exactly n_v/2. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.majority_favorite_ut_ctb 0 No majority favorite: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.majority_favorite_ut_ctb nan See Also -------- :attr:`~svvamp.Population.exists_majority_favorite_ut_ctb`, :attr:`~svvamp.Population.not_exists_majority_favorite_ut_ctb`. """ self.mylog("Compute majority favorite (ut_ctb)", 1) c = int(np.argmax(self.plurality_scores_ut)) score_c = self.plurality_scores_ut[c] if score_c > self.n_v / 2 or (c == 0 and score_c == self.n_v / 2): return c return np.nan @cached_property def exists_majority_favorite_ut_ctb(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.majority_favorite_ut_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_majority_favorite_ut_ctb True """ return not np.isnan(self.majority_favorite_ut_ctb) @cached_property def not_exists_majority_favorite_ut_ctb(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.majority_favorite_ut_ctb`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_majority_favorite_ut_ctb False """ return bool(np.isnan(self.majority_favorite_ut_ctb)) @cached_property def majority_favorite_rk(self): """Integer or ``NaN``. Candidate who has strictly more than n_v/2 in :attr:`~svvamp.Population.plurality_scores_rk`. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.majority_favorite_rk 0 No majority favorite: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.majority_favorite_rk nan See Also -------- :attr:`~svvamp.Population.exists_majority_favorite_rk`, :attr:`~svvamp.Population.not_exists_majority_favorite_rk`. """ self.mylog("Compute majority favorite (rk)", 1) c = int(np.argmax(self.plurality_scores_rk)) score_c = self.plurality_scores_rk[c] if score_c > self.n_v / 2: return c return np.nan @cached_property def exists_majority_favorite_rk(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.majority_favorite_rk`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_majority_favorite_rk True """ return not np.isnan(self.majority_favorite_rk) @cached_property def not_exists_majority_favorite_rk(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.majority_favorite_rk`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_majority_favorite_rk False """ return bool(np.isnan(self.majority_favorite_rk)) @cached_property def majority_favorite_ut(self): """Integer or ``NaN``. Candidate who has strictly more than n_v/2 in :attr:`~svvamp.Population.plurality_scores_ut`. If there is no such candidate, then ``NaN``. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.majority_favorite_ut 0 No majority favorite: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.majority_favorite_ut nan See Also -------- :attr:`~svvamp.Population.exists_majority_favorite_ut`, :attr:`~svvamp.Population.not_exists_majority_favorite_ut`. """ self.mylog("Compute majority favorite (ut)", 1) c = int(np.argmax(self.plurality_scores_ut)) score_c = self.plurality_scores_ut[c] if score_c > self.n_v / 2: return c return np.nan @cached_property def exists_majority_favorite_ut(self): """Boolean (``True`` iff there is a :attr:`~svvamp.Population.majority_favorite_ut`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.exists_majority_favorite_ut True """ return not np.isnan(self.majority_favorite_ut) @cached_property def not_exists_majority_favorite_ut(self): """Boolean (``True`` iff there is no :attr:`~svvamp.Population.majority_favorite_ut`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.not_exists_majority_favorite_ut False """ return bool(np.isnan(self.majority_favorite_ut)) # %% Total utilities @cached_property def total_utility_c(self): """1d array of numbers. ``total_utility_c[c]`` is the total utility for candidate ``c`` (i.e. the sum of ``c``'s column in matrix :attr:`~svvamp.Population.preferences_ut`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.total_utility_c array([ 9, 11, 3]) """ self.mylog("Compute total utility of candidates", 1) return np.sum(self.preferences_ut, 0) @cached_property def total_utility_min(self): """Float. ``total_utility_min`` is the minimum of :attr:`~svvamp.Population.total_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.total_utility_min 3.0 """ self.mylog("Compute total_utility_min", 1) return float(np.min(self.total_utility_c)) @cached_property def total_utility_max(self): """Float. ``total_utility_max`` is the maximum of :attr:`~svvamp.Population.total_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.total_utility_max 11.0 """ self.mylog("Compute total_utility_max", 1) return float(np.max(self.total_utility_c)) @cached_property def total_utility_mean(self): """Float. ``total_utility_mean`` is the mean of :attr:`~svvamp.Population.total_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.total_utility_mean == (9 + 11 + 3) / 3 True """ self.mylog("Compute total_utility_mean", 1) return float(np.mean(self.total_utility_c)) @cached_property def total_utility_std(self): """Float. ``total_utility_std`` is the standard deviation of :attr:`~svvamp.Population.total_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.total_utility_std == float(np.std([9, 11, 3])) True """ self.mylog("Compute total_utility_std", 1) return float(np.std(self.total_utility_c, ddof=0)) # %% Relative social welfare @cached_property def relative_social_welfare_c(self): """1d array of numbers. ``relative_social_welfare_c[c]`` is equal to ``total_utility_c[c]``, but renormalized so that the minimum is 0 and the maximum is 1. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.relative_social_welfare_c array([0.75, 1. , 0. ]) By convention, if all candidates have the same social welfare, then their relative social welfare is 1: >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[2, 1, 1], [1, 2, 2]]) >>> profile.relative_social_welfare_c array([1, 1, 1]) """ self.mylog("Compute relative social welfare of candidates", 1) if self.total_utility_min == self.total_utility_max: return np.ones(self.total_utility_c.shape, dtype=self.total_utility_c.dtype) return (self.total_utility_c - self.total_utility_min) / (self.total_utility_max - self.total_utility_min) # %% Mean utilities @cached_property def mean_utility_c(self): """1d array of floats. ``mean_utility_c[c]`` is the mean utility for candidate ``c`` (i.e. the mean of ``c``'s column in matrix :attr:`~svvamp.Population.preferences_ut`). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.mean_utility_c array([4.5, 5.5, 1.5]) """ self.mylog("Compute mean utility of candidates", 1) return np.mean(self.preferences_ut, 0) @cached_property def mean_utility_min(self): """Float. ``mean_utility_min`` is the minimum of :attr:`~svvamp.Population.mean_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.mean_utility_min 1.5 """ self.mylog("Compute mean_utility_min", 1) return float(np.min(self.mean_utility_c)) @cached_property def mean_utility_max(self): """Float. ``mean_utility_max`` is the maximum of :attr:`~svvamp.Population.mean_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.mean_utility_max 5.5 """ self.mylog("Compute mean_utility_max", 1) return float(np.max(self.mean_utility_c)) @cached_property def mean_utility_mean(self): """Float. ``mean_utility_mean`` is the mean of :attr:`~svvamp.Population.mean_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.mean_utility_mean == (4.5 + 5.5 + 1.5) / 3 True """ self.mylog("Compute mean_utility_mean", 1) return float(np.mean(self.mean_utility_c)) @cached_property def mean_utility_std(self): """Float. ``mean_utility_std`` is the standard deviation of :attr:`~svvamp.Population.mean_utility_c`. Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_ut=[[5, 1, 2], [4, 10, 1]]) >>> profile.mean_utility_std == float(np.std([4.5, 5.5, 1.5])) True """ self.mylog("Compute mean_utility_std", 1) return float(np.std(self.mean_utility_c, ddof=0)) # %% Borda scores @cached_property def borda_score_c_rk(self): """1d array of integers. ``borda_score_c_rk[c]`` is the total Borda score of candidate ``c`` (using :attr:`~svvamp.Population.preferences_borda_rk`, i.e. strict preferences). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.borda_score_c_rk array([4, 2, 0]) """ self.mylog("Compute Borda scores of the candidates (rankings)", 1) return np.sum(self.matrix_duels_rk, 1) @cached_property def borda_score_c_ut(self): """1d array of floats. ``borda_score_c_ut[c]`` is the total Borda score of candidate ``c`` (using :attr:`~svvamp.Population.preferences_borda_ut`, i.e. weak preferences). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.borda_score_c_ut array([4., 2., 0.]) """ self.mylog("Compute Borda scores of the candidates (weak orders)", 1) return np.sum(self.preferences_borda_ut, 0) @cached_property def _tuple_decreasing_borda_score_rk(self): candidates_by_decreasing_borda_score_rk = np.array(np.argsort(- self.borda_score_c_rk, kind='mergesort'), dtype=int) decreasing_borda_scores_rk = self.borda_score_c_rk[candidates_by_decreasing_borda_score_rk] return candidates_by_decreasing_borda_score_rk, decreasing_borda_scores_rk @cached_property def candidates_by_decreasing_borda_score_rk(self): """1d array of integers. ``candidates_by_decreasing_borda_score_rk[k]`` is the candidate ranked ``k``\ :sup:`th` by decreasing Borda score (using :attr:`~svvamp.Population.borda_score_c_rk`, i.e. strict preferences). For example, ``candidates_by_decreasing_borda_score_rk[0]`` is the candidate with highest Borda score (rk). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.candidates_by_decreasing_borda_score_rk array([0, 1, 2]) """ self.mylog("Compute candidates_by_decreasing_borda_score_rk", 1) return self._tuple_decreasing_borda_score_rk[0] @cached_property def decreasing_borda_scores_rk(self): """1d array of integers. ``decreasing_borda_scores_rk[k]`` is the ``k``-th Borda score (using :attr:`~svvamp.Population.borda_score_c_rk`, i.e. strict preferences) by decreasing order. For example, ``decreasing_borda_scores_rk[0]`` is the highest Borda score for a candidate (rk). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.decreasing_borda_scores_rk array([4, 2, 0]) """ self.mylog("Compute decreasing_borda_scores_rk", 1) return self._tuple_decreasing_borda_score_rk[1] @cached_property def _tuple_decreasing_borda_score_ut(self): candidates_by_decreasing_borda_score_ut = np.array(np.argsort(- self.borda_score_c_ut, kind='mergesort'), dtype=int) decreasing_borda_scores_ut = self.borda_score_c_ut[candidates_by_decreasing_borda_score_ut] return candidates_by_decreasing_borda_score_ut, decreasing_borda_scores_ut @cached_property def candidates_by_decreasing_borda_score_ut(self): """1d array of integers. ``candidates_by_decreasing_borda_score_ut[k]`` is the candidate ranked ``k``\ :sup:`th` by decreasing Borda score (using :attr:`~svvamp.Population.borda_score_c_ut`, i.e. weak preferences). For example, ``candidates_by_decreasing_borda_score_ut[0]`` is the candidate with highest Borda score (ut). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.candidates_by_decreasing_borda_score_ut array([0, 1, 2]) """ self.mylog("Compute candidates_by_decreasing_borda_score_ut", 1) return self._tuple_decreasing_borda_score_ut[0] @cached_property def decreasing_borda_scores_ut(self): """1d array of integers. ``decreasing_borda_scores_ut[k]`` is the ``k``-th Borda score (using :attr:`~svvamp.Population.borda_score_c_ut`, i.e. weak preferences) by decreasing order. For example, ``decreasing_borda_scores_ut[0]`` is the highest Borda score for a candidate (rk). Examples -------- >>> from svvamp import Profile >>> profile = Profile(preferences_rk=[[0, 1, 2], [0, 1, 2]]) >>> profile.decreasing_borda_scores_ut array([4., 2., 0.]) """ self.mylog("Compute decreasing_borda_scores_ut", 1) return self._tuple_decreasing_borda_score_ut[1] # %% Plot a population
[docs] def plot3(self, indexes=None, normalize=True, use_labels=True): """Plot utilities for 3 candidates (with approval limit). Parameters ---------- indexes : list List of 3 candidates. If None, defaults to [0, 1, 2]. normalize : bool Cf. below. use_labels : bool If ``True``, then :attr:`~svvamp.Population.labels_candidates` is used to label the plot. Otherwise, candidates are simply represented by their index. Notes ----- Each red point of the plot represents a voter ``v``. Its position is :attr:`~svvamp.Population.preferences_ut`\\ ``[v, indexes]``. If ``normalize`` is ``True``, then each position is normalized before plotting so that its Euclidean norm is equal to 1. The equator (in blue) is the set of points :math:`\\mathbf{u}` such that :math:`\\sum {u_i}^2 = 1` and :math:`\\sum u_i = 0`, i.e. the unit circle of the plan that is orthogonal to the main diagonal [1, 1, 1]. Other blue circles are the frontiers between the 6 different strict total orders on the candidates. Examples -------- Typical usage: >>> initialize_random_seeds() >>> preferences_ut_test = np.random.randint(-5, 5, (10, 5)) >>> profile = Profile(preferences_ut=preferences_ut_test, ... labels_candidates=['Alice', 'Bob', 'Catherine', 'Dave', 'Ellen']) >>> profile.plot3(indexes=[0, 1, 2]) If `indexes` is not given, it defaults to [0, 1, 2]: >>> profile.plot3() You can ignore the labels of the candidates and simply use their numbers: >>> profile.plot3(use_labels=False) References ---------- Durand et al., 'Geometry on the Utility Space'. """ if indexes is None: _indexes = [0, 1, 2] else: _indexes = indexes # Define figure fig = plt.figure() ax = fig.add_subplot(111, projection='3d') # North-South axis (not plotted anymore) # ax.plot(xs=[-1, 1], ys=[-1, 1], zs=[-1, 1], c='k') # Equator theta = np.linspace(0, 2 * np.pi, 200) xs = np.cos(theta) / np.sqrt(2) - np.sin(theta) / np.sqrt(6) ys = - np.cos(theta) / np.sqrt(2) - np.sin(theta) / np.sqrt(6) zs = 2 * np.sin(theta) / np.sqrt(6) ax.scatter(xs, ys, zs, s=7, c='b') # Frontiers between strict orders for i in range(3): pole = np.full(3, 1 / np.sqrt(3)) ortho = np.ones(3) ortho[i] = -2 ortho = ortho / np.sqrt(6) xs = np.cos(theta) * pole[0] + np.sin(theta) * ortho[0] ys = np.cos(theta) * pole[1] + np.sin(theta) * ortho[1] zs = np.cos(theta) * pole[2] + np.sin(theta) * ortho[2] ax.scatter(xs, ys, zs, s=7, c='b') # Voters mat_temp = np.copy(self.preferences_ut[:, _indexes]).astype(float) self.mylogm('mat_temp =', mat_temp, 3) if normalize: for v in range(mat_temp.shape[0]): norm = np.sqrt(np.sum(mat_temp[v, :]**2)) if norm > 0: mat_temp[v, :] /= norm self.mylogm('mat_temp =', mat_temp, 3) ax.scatter(mat_temp[:, 0], mat_temp[:, 1], mat_temp[:, 2], s=40, c='r', marker='o') # Axes and labels if use_labels: ax.set_xlabel(self.labels_candidates[_indexes[0]] + ' (' + str(_indexes[0]) + ')') ax.set_ylabel(self.labels_candidates[_indexes[1]] + ' (' + str(_indexes[1]) + ')') ax.set_zlabel(self.labels_candidates[_indexes[2]] + ' (' + str(_indexes[2]) + ')') else: ax.set_xlabel('Candidate ' + str(_indexes[0])) ax.set_ylabel('Candidate ' + str(_indexes[1])) ax.set_zlabel('Candidate ' + str(_indexes[2])) temp = 1 / np.sqrt(2) for best in range(3): for worst in range(3): if best == worst: continue other = 3 - best - worst position = np.zeros(3) position[best] = temp position[worst] = - temp ax.text(position[0], position[1], position[2], ' ' + str(_indexes[best]) + ' > ' + str(_indexes[other]) + ' > ' + str(_indexes[worst]))
# plt.show()
[docs] def plot4(self, indexes=None, normalize=True, use_labels=True): """Plot utilities for 4 candidates (without approval limit). Parameters ---------- indexes : list List of 4 candidates. If None, defaults to [0, 1, 2, 3]. normalize : bool Cf. below. use_labels : bool If ``True``, then :attr:`~svvamp.Population.labels_candidates` is used to label the plot. Otherwise, candidates are simply represented by their index. Notes ----- Each red point of the plot represents a voter ``v``. * :attr:`~svvamp.Population.preferences_ut`\\ ``[v, indexes]`` is sent to the hyperplane that is orthogonal to [1, 1, 1, 1] (by orthogonal projection), which discards information related to approval limit and keeps only the relative preferences between candidates. * The plot is done in this 3d hyperplane. In practice, we use a mirror symmetry that exchanges [1, 1, 1, 1] and [0, 0, 0, 1]. This way, the image vector is orthogonal to [0, 0, 0, 1] and can be plotted in the first 3 dimensions. * If ``normalize`` is True, then the image vector is normalized before plotting so that its Euclidean norm is equal to 1. Blue lines are the frontiers between the 24 different strict total orders on the candidates ('permutohedron'). Examples -------- Typical usage: >>> initialize_random_seeds() >>> preferences_ut_test = np.random.randint(-5, 5, (10, 5)) >>> profile = Profile(preferences_ut=preferences_ut_test, ... labels_candidates=['Alice', 'Bob', 'Catherine', 'Dave', 'Ellen']) >>> profile.plot4(indexes=[0, 1, 2, 4]) If `indexes` is not given, it defaults to [0, 1, 2, 3]: >>> profile.plot4() You can ignore the labels of the candidates and simply use their numbers: >>> profile.plot4(use_labels=False) It is possible to avoid normalization: >>> profile.plot4(normalize=False) References ---------- Durand et al., 'Geometry on the Utility Space'. """ if indexes is None: _indexes = [0, 1, 2, 3] else: _indexes = indexes # Define figure fig = plt.figure() ax = fig.add_subplot(111, projection='3d') # Voters # We transform the population matrix to send it to R^3 # 0. Keep only the specified candidates mat_temp = np.copy(self.preferences_ut[:, _indexes]).astype(float) # 1. Send each voter to the hyperplane orthogonal to (1, 1, 1, 1) mat_temp -= np.mean(mat_temp, 1)[:, np.newaxis] # 2. Use symmetry by a hyperplane to exchange (1, 1, 1, 1) and (0, 0, 0, 1) # unitary_parallel = [1, 1, 1, 1] / sqrt(4) - [0, 0, 0, 1] then normalize. unitary_parallel = np.array([0.5, 0.5, 0.5, -0.5]) # unitary_parallel = unitary_parallel / np.sqrt(np.sum(unitary_parallel**2)) temp_parallel = np.outer(np.sum(mat_temp * unitary_parallel[np.newaxis, :], 1), unitary_parallel) mat_temp -= 2 * temp_parallel # 3. Normalize if asked if normalize: for v in range(mat_temp.shape[0]): norm = np.sqrt(np.sum(mat_temp[v, :]**2)) if norm > 0: mat_temp[v, :] /= norm # 4. Now just do not care about last coordinate, it is 0. ax.scatter(mat_temp[:, 0], mat_temp[:, 1], mat_temp[:, 2], s=40, c='r', marker='o') # Permutohedron xs = [] ys = [] zs = [] def add_line(v1, v2): """v1 and v2 are vertices.""" if normalize: theta_tot = np.arccos(np.sum(v1 * v2)) ortho = v2 - np.sum(v2 * v1) * v1 ortho = ortho / np.sqrt(np.sum(ortho**2)) theta = np.linspace(0, theta_tot, 100) xs.extend(np.cos(theta) * v1[0] + np.sin(theta) * ortho[0]) ys.extend(np.cos(theta) * v1[1] + np.sin(theta) * ortho[1]) zs.extend(np.cos(theta) * v1[2] + np.sin(theta) * ortho[2]) else: xs.extend(np.linspace(v1[0], v2[0], 50)) ys.extend(np.linspace(v1[1], v2[1], 50)) zs.extend(np.linspace(v1[2], v2[2], 50)) def image(vertex): result = np.multiply(vertex, 2) - 1 result = result - np.mean(result) the_temp_parallel = np.sum(result*unitary_parallel) * unitary_parallel result -= 2 * the_temp_parallel if normalize: result = result / np.sqrt(np.sum(result**2)) return result for i in range(4): for j in range(4): if j == i: continue # Vertex 1 of type [1, 0, 0, 0] vertex1 = image(np.array(range(4)) == i) if use_labels: pass ax.text(vertex1[0], vertex1[1], vertex1[2], ' Favorite = ' + self.labels_candidates[_indexes[i]]) else: ax.text(vertex1[0], vertex1[1], vertex1[2], ' Favorite = ' + str(_indexes[i])) # Vertex 2 of type [1, 1, 1, 0] vertex2 = np.ones(4) vertex2[j] = 0 vertex2 = image(vertex2) add_line(vertex1, vertex2) # Vertex 2 of type [1, 1, 0, 0] vertex2 = np.zeros(4) vertex2[i] = 1 vertex2[j] = 1 vertex2 = image(vertex2) add_line(vertex1, vertex2) # Vertex 1 of type [0, 1, 1, 1] vertex1 = image(np.array(range(4)) != i) # Vertex 2 of type [1, 1, 0, 0] vertex2 = np.ones(4) vertex2[i] = 0 vertex2[j] = 0 vertex2 = image(vertex2) add_line(vertex1, vertex2) ax.scatter(xs, ys, zs, s=7, c='b') # Conclude plt.axis('off')
# plt.show() # %% Demo
[docs] def demo(self, log_depth=1): """Demonstrate the methods of :class:`~svvamp.Population` class. Parameters ---------- log_depth : int Integer from 0 (basic info) to 3 (verbose). Examples -------- >>> initialize_random_seeds() >>> preferences_ut_test = np.random.randint(-5, 5, (10, 5)) >>> profile = Profile(preferences_ut=preferences_ut_test, ... labels_candidates=['Alice', 'Bob', 'Catherine', 'Dave', 'Ellen'], ... sort_voters=True) >>> profile.demo(log_depth=0) # doctest: +NORMALIZE_WHITESPACE <BLANKLINE> ************************ * Basic properties * ************************ n_v = 10 n_c = 5 labels_candidates = ['Alice', 'Bob', 'Catherine', 'Dave', 'Ellen'] preferences_ut = [[ 4 -2 0 -3 -1] [ 4 4 -5 -1 2] [ 0 4 3 4 -1] [-2 -2 2 -5 -4] [-2 -3 2 -3 -5] [-3 -2 3 -4 -2] [ 2 1 3 3 -4] [-2 -5 -2 0 -5] [ 1 2 2 3 -4] [ 0 -5 -2 -2 2]] preferences_borda_ut = [[4. 1. 3. 0. 2. ] [3.5 3.5 0. 1. 2. ] [1. 3.5 2. 3.5 0. ] [2.5 2.5 4. 0. 1. ] [3. 1.5 4. 1.5 0. ] [1. 2.5 4. 0. 2.5] [2. 1. 3.5 3.5 0. ] [2.5 0.5 2.5 4. 0.5] [1. 2.5 2.5 4. 0. ] [3. 0. 1.5 1.5 4. ]] preferences_borda_rk = [[4 1 3 0 2] [3 4 0 1 2] [1 4 2 3 0] [3 2 4 0 1] [3 1 4 2 0] [1 3 4 0 2] [2 1 3 4 0] [2 0 3 4 1] [1 2 3 4 0] [3 0 2 1 4]] preferences_rk = [[0 2 4 1 3] [1 0 4 3 2] [1 3 2 0 4] [2 0 1 4 3] [2 0 3 1 4] [2 1 4 0 3] [3 2 0 1 4] [3 2 0 4 1] [3 2 1 0 4] [4 0 2 3 1]] v_has_same_ordinal_preferences_as_previous_voter = [False False False False False False False False False False] <BLANKLINE> ************************ * Plurality scores * ************************ preferences_rk (reminder) = [[0 2 4 1 3] [1 0 4 3 2] [1 3 2 0 4] [2 0 1 4 3] [2 0 3 1 4] [2 1 4 0 3] [3 2 0 1 4] [3 2 0 4 1] [3 2 1 0 4] [4 0 2 3 1]] plurality_scores_rk = [1 2 3 3 1] majority_favorite_rk = nan majority_favorite_rk_ctb = nan <BLANKLINE> preferences_borda_ut (reminder) = [[4. 1. 3. 0. 2. ] [3.5 3.5 0. 1. 2. ] [1. 3.5 2. 3.5 0. ] [2.5 2.5 4. 0. 1. ] [3. 1.5 4. 1.5 0. ] [1. 2.5 4. 0. 2.5] [2. 1. 3.5 3.5 0. ] [2.5 0.5 2.5 4. 0.5] [1. 2.5 2.5 4. 0. ] [3. 0. 1.5 1.5 4. ]] plurality_scores_ut = [1 0 3 2 1] majority_favorite_ut = nan majority_favorite_ut_ctb = nan <BLANKLINE> ******************** * Borda scores * ******************** preferences_borda_rk (reminder) = [[4 1 3 0 2] [3 4 0 1 2] [1 4 2 3 0] [3 2 4 0 1] [3 1 4 2 0] [1 3 4 0 2] [2 1 3 4 0] [2 0 3 4 1] [1 2 3 4 0] [3 0 2 1 4]] borda_score_c_rk = [23 18 28 19 12] Remark: Borda scores above are computed with the matrix of duels. Check: np.sum(self.preferences_borda_rk, 0) = [23 18 28 19 12] decreasing_borda_scores_rk = [28 23 19 18 12] candidates_by_decreasing_borda_score_rk = [2 0 3 1 4] <BLANKLINE> preferences_borda_ut (reminder) = [[4. 1. 3. 0. 2. ] [3.5 3.5 0. 1. 2. ] [1. 3.5 2. 3.5 0. ] [2.5 2.5 4. 0. 1. ] [3. 1.5 4. 1.5 0. ] [1. 2.5 4. 0. 2.5] [2. 1. 3.5 3.5 0. ] [2.5 0.5 2.5 4. 0.5] [1. 2.5 2.5 4. 0. ] [3. 0. 1.5 1.5 4. ]] borda_score_c_ut = [23.5 18.5 27. 19. 12. ] decreasing_borda_scores_ut = [27. 23.5 19. 18.5 12. ] candidates_by_decreasing_borda_score_ut = [2 0 3 1 4] <BLANKLINE> ***************** * Utilities * ***************** preferences_ut (reminder) = [[ 4 -2 0 -3 -1] [ 4 4 -5 -1 2] [ 0 4 3 4 -1] [-2 -2 2 -5 -4] [-2 -3 2 -3 -5] [-3 -2 3 -4 -2] [ 2 1 3 3 -4] [-2 -5 -2 0 -5] [ 1 2 2 3 -4] [ 0 -5 -2 -2 2]] total_utility_c = [ 2 -8 6 -8 -22] total_utility_min = -22.0 total_utility_max = 6.0 total_utility_mean = -6.0 total_utility_std = 9.715966241192895 <BLANKLINE> ******************************************* * Condorcet notions based on rankings * ******************************************* preferences_rk (reminder) = [[0 2 4 1 3] [1 0 4 3 2] [1 3 2 0 4] [2 0 1 4 3] [2 0 3 1 4] [2 1 4 0 3] [3 2 0 1 4] [3 2 0 4 1] [3 2 1 0 4] [4 0 2 3 1]] matrix_duels_rk = [[0 6 3 6 8] [4 0 2 5 7] [7 8 0 5 8] [4 5 5 0 5] [2 3 2 5 0]] matrix_victories_rk = [[0. 1. 0. 1. 1. ] [0. 0. 0. 0.5 1. ] [1. 1. 0. 0.5 1. ] [0. 0.5 0.5 0. 0.5] [0. 0. 0. 0.5 0. ]] condorcet_winner_rk = nan exists_condorcet_order_rk = False matrix_victories_rk_ctb = [[0. 1. 0. 1. 1.] [0. 0. 0. 1. 1.] [1. 1. 0. 1. 1.] [0. 0. 0. 0. 1.] [0. 0. 0. 0. 0.]] condorcet_winner_rk_ctb = 2 exists_condorcet_order_rk_ctb = True <BLANKLINE> *************************************** * Relative Condorcet notions (ut) * *************************************** preferences_borda_ut (reminder) = [[4. 1. 3. 0. 2. ] [3.5 3.5 0. 1. 2. ] [1. 3.5 2. 3.5 0. ] [2.5 2.5 4. 0. 1. ] [3. 1.5 4. 1.5 0. ] [1. 2.5 4. 0. 2.5] [2. 1. 3.5 3.5 0. ] [2.5 0.5 2.5 4. 0.5] [1. 2.5 2.5 4. 0. ] [3. 0. 1.5 1.5 4. ]] matrix_duels_ut = [[0 5 3 6 8] [3 0 2 4 6] [6 7 0 4 8] [4 4 4 0 5] [2 2 2 5 0]] matrix_victories_ut_rel = [[0. 1. 0. 1. 1. ] [0. 0. 0. 0.5 1. ] [1. 1. 0. 0.5 1. ] [0. 0.5 0.5 0. 0.5] [0. 0. 0. 0.5 0. ]] condorcet_winner_ut_rel = nan exists_condorcet_order_ut_rel = False matrix_victories_ut_rel_ctb = [[0. 1. 0. 1. 1.] [0. 0. 0. 1. 1.] [1. 1. 0. 1. 1.] [0. 0. 0. 0. 1.] [0. 0. 0. 0. 0.]] condorcet_winner_ut_rel_ctb = 2 exists_condorcet_order_ut_rel_ctb = True <BLANKLINE> *************************************** * Absolute Condorcet notions (ut) * *************************************** matrix_duels_ut (reminder) = [[0 5 3 6 8] [3 0 2 4 6] [6 7 0 4 8] [4 4 4 0 5] [2 2 2 5 0]] matrix_victories_ut_abs = [[0. 0.5 0. 1. 1. ] [0. 0. 0. 0. 1. ] [1. 1. 0. 0. 1. ] [0. 0. 0. 0. 0.5] [0. 0. 0. 0.5 0. ]] condorcet_admissible_candidates = [False False True False False] nb_condorcet_admissible = 1 weak_condorcet_winners = [False False True False False] nb_weak_condorcet_winners = 1 condorcet_winner_ut_abs = nan exists_condorcet_order_ut_abs = False resistant_condorcet_winner = nan threshold_c_prevents_w_Condorcet_ut_abs = [[0 0 0 2 0] [0 0 0 0 0] [2 0 0 2 0] [0 0 2 0 0] [0 0 0 1 0]] matrix_victories_ut_abs_ctb = [[0. 1. 0. 1. 1.] [0. 0. 0. 0. 1.] [1. 1. 0. 0. 1.] [0. 0. 0. 0. 1.] [0. 0. 0. 0. 0.]] condorcet_winner_ut_abs_ctb = nan exists_condorcet_order_ut_abs_ctb = False <BLANKLINE> ********************************************** * Implications between Condorcet notions * ********************************************** maj_fav_ut (False) ==> maj_fav_ut_ctb (False) || || || || || V V || || maj_fav_rk (False) ==> maj_fav_rk_ctb (False) || V || || || Resistant Condorcet (False) || || || || || V || || V Condorcet_ut_abs (False) ==> Condorcet_ut_abs_ctb (False) || || || || || || || V V V V || || Condorcet_rk (False) ==> Condorcet_rk_ctb (True) || V V Condorcet_ut_rel (False) ==> Condorcet_ut_rel_ctb (True) || V Weak Condorcet (True) || V Condorcet-admissible (True) """ old_log_depth = self.log_depth self.log_depth = log_depth print_title("Basic properties") print("n_v =", self.n_v) print("n_c =", self.n_c) print("labels_candidates =", self.labels_candidates) printm("preferences_ut =", self.preferences_ut) printm("preferences_borda_ut =", self.preferences_borda_ut) printm("preferences_borda_rk =", self.preferences_borda_rk) printm("preferences_rk =", self.preferences_rk) printm("v_has_same_ordinal_preferences_as_previous_voter =", self.v_has_same_ordinal_preferences_as_previous_voter) print_title("Plurality scores") printm("preferences_rk (reminder) =", self.preferences_rk) print("plurality_scores_rk =", self.plurality_scores_rk) print("majority_favorite_rk =", self.majority_favorite_rk) print("majority_favorite_rk_ctb =", self.majority_favorite_rk_ctb) print("") printm("preferences_borda_ut (reminder) =", self.preferences_borda_ut) print("plurality_scores_ut =", self.plurality_scores_ut) print("majority_favorite_ut =", self.majority_favorite_ut) print("majority_favorite_ut_ctb =", self.majority_favorite_ut_ctb) print_title("Borda scores") printm("preferences_borda_rk (reminder) =", self.preferences_borda_rk) printm("borda_score_c_rk =", self.borda_score_c_rk) print("Remark: Borda scores above are computed with the matrix of duels.") printm("Check: np.sum(self.preferences_borda_rk, 0) =", np.sum(self.preferences_borda_rk, 0)) printm("decreasing_borda_scores_rk =", self.decreasing_borda_scores_rk) printm("candidates_by_decreasing_borda_score_rk =", self.candidates_by_decreasing_borda_score_rk) print("") printm("preferences_borda_ut (reminder) =", self.preferences_borda_ut) printm("borda_score_c_ut =", self.borda_score_c_ut) printm("decreasing_borda_scores_ut =", self.decreasing_borda_scores_ut) printm("candidates_by_decreasing_borda_score_ut =", self.candidates_by_decreasing_borda_score_ut) print_title("Utilities") printm("preferences_ut (reminder) =", self.preferences_ut) printm("total_utility_c = ", self.total_utility_c) print("total_utility_min =", self.total_utility_min) print("total_utility_max =", self.total_utility_max) print("total_utility_mean =", self.total_utility_mean) print("total_utility_std =", self.total_utility_std) print_title("Condorcet notions based on rankings") printm("preferences_rk (reminder) =", self.preferences_rk) printm("matrix_duels_rk =", self.matrix_duels_rk) printm("matrix_victories_rk =", self.matrix_victories_rk) print("condorcet_winner_rk =", self.condorcet_winner_rk) print("exists_condorcet_order_rk =", self.exists_condorcet_order_rk) printm("matrix_victories_rk_ctb =", self.matrix_victories_rk_ctb) print("condorcet_winner_rk_ctb =", self.condorcet_winner_rk_ctb) print("exists_condorcet_order_rk_ctb =", self.exists_condorcet_order_rk_ctb) print_title("Relative Condorcet notions (ut)") printm("preferences_borda_ut (reminder) =", self.preferences_borda_ut) printm("matrix_duels_ut =", self.matrix_duels_ut) printm("matrix_victories_ut_rel =", self.matrix_victories_ut_rel) print("condorcet_winner_ut_rel =", self.condorcet_winner_ut_rel) print("exists_condorcet_order_ut_rel =", self.exists_condorcet_order_ut_rel) printm("matrix_victories_ut_rel_ctb =", self.matrix_victories_ut_rel_ctb) print("condorcet_winner_ut_rel_ctb =", self.condorcet_winner_ut_rel_ctb) print("exists_condorcet_order_ut_rel_ctb =", self.exists_condorcet_order_ut_rel_ctb) print_title("Absolute Condorcet notions (ut)") printm("matrix_duels_ut (reminder) =", self.matrix_duels_ut) printm("matrix_victories_ut_abs =", self.matrix_victories_ut_abs) printm("condorcet_admissible_candidates = ", self.condorcet_admissible_candidates) print("nb_condorcet_admissible =", self.nb_condorcet_admissible) printm("weak_condorcet_winners =", self.weak_condorcet_winners) print("nb_weak_condorcet_winners =", self.nb_weak_condorcet_winners) print("condorcet_winner_ut_abs =", self.condorcet_winner_ut_abs) print("exists_condorcet_order_ut_abs =", self.exists_condorcet_order_ut_abs) print("resistant_condorcet_winner =", self.resistant_condorcet_winner) printm("threshold_c_prevents_w_Condorcet_ut_abs =", self.threshold_c_prevents_w_condorcet_ut_abs) printm("matrix_victories_ut_abs_ctb =", self.matrix_victories_ut_abs_ctb) print("condorcet_winner_ut_abs_ctb =", self.condorcet_winner_ut_abs_ctb) print("exists_condorcet_order_ut_abs_ctb =", self.exists_condorcet_order_ut_abs_ctb) print_title("Implications between Condorcet notions") # maj_fav_ut (False) ==> maj_fav_ut_ctb (False) # || || || || # || V V || # || maj_fav_rk (False) ==> maj_fav_rk_ctb (False) || # V || || || # Resistant Condorcet (False) || # || || || || # V || || V # Condorcet_ut_abs (False) ==> Condorcet_ut_abs_ctb (False) # || || || || || || # || V V V V || # || Condorcet_rk (False) ==> Condorcet_rk_ctb (False) || # V V # Condorcet_ut_rel (False) ==> Condorcet_ut_rel_ctb (False) # || # V # Weak Condorcet (False) # || # V # Condorcet-admissible (False) def display_bool(value): return '(True) ' if equal_true(value) else '(False)' print('maj_fav_ut ' + display_bool(self.majority_favorite_ut) + ' ==> ' + 'maj_fav_ut_ctb ' + display_bool(self.majority_favorite_ut_ctb)) print(' || || || ||') print(' || V V ||') print(' || maj_fav_rk ' + display_bool(self.majority_favorite_rk) + ' ==> ' + 'maj_fav_rk_ctb ' + display_bool(self.majority_favorite_rk_ctb) + ' ||') print(' V || || ||') print('Resistant Condorcet ' + display_bool(self.resistant_condorcet_winner) + ' ||') print(' || || || ||') print(' V || || V') print('Condorcet_ut_abs ' + display_bool(self.condorcet_winner_ut_abs) + ' ==> ' 'Condorcet_ut_abs_ctb ' + display_bool(self.condorcet_winner_ut_abs_ctb) + '') print(' || || || || || ||') print(' || V V V V ||') print(' || Condorcet_rk ' + display_bool(self.exists_condorcet_winner_rk) + ' ==> Condorcet_rk_ctb ' + display_bool(self.exists_condorcet_winner_rk_ctb) + ' ||') print(' V V') print('Condorcet_ut_rel ' + display_bool(self.exists_condorcet_winner_ut_rel) + ' ==> Condorcet_ut_rel_ctb ' + display_bool(self.exists_condorcet_winner_ut_rel_ctb)) print(' ||') print(' V') print('Weak Condorcet ' + display_bool(self.exists_weak_condorcet_winner)) print(' ||') print(' V') print('Condorcet-admissible ' + display_bool(self.exists_condorcet_admissible)) self.log_depth = old_log_depth
# %% For developers
[docs] def to_doctest_string(self, ut=True, rk=True): """Convert to string, in the doctest format. Parameters ---------- ut : bool Whether to print `preferences_ut`. rk : bool Whether to print `preferences_rk`. Returns ------- str A string that can be copied-pasted to make a doctest. Examples -------- >>> profile = Profile(preferences_rk=[[0, 1], [0, 1]]) >>> print('.' + profile.to_doctest_string()) # doctest: +NORMALIZE_WHITESPACE . >>> profile = Profile(preferences_ut=[ ... [1, 0], ... [1, 0], ... ], preferences_rk=[ ... [0, 1], ... [0, 1], ... ]) """ s = ' >>> profile = Profile(' arguments = [] if ut: argument = '' argument += 'preferences_ut=[\n' argument += ( repr(self.preferences_ut) .replace('array([', ' ... ') .replace('\n ', '\n ... ') .replace('])', ',') ) argument += '\n ... ]' arguments.append(argument) if rk: argument = '' argument += 'preferences_rk=[\n' argument += ( repr(self.preferences_rk) .replace('array([', ' ... ') .replace('\n ', '\n ... ') .replace('])', ',') ) argument += '\n ... ]' arguments.append(argument) s += ', '.join(arguments) s += ')' return s
def to_csv(self, file_name): df = pd.DataFrame(self.preferences_ut, columns=self.labels_candidates) df.to_csv(file_name, sep=';') def plurality_elimination_engine(self): return PluralityEliminationEngineProfile(self) @cached_property def c_might_be_there_when_cw_is_eliminated_irv_style(self): """Whether a candidate c might be present in a round where the Condorcet winner is eliminated, IRV-style. Returns ------- ndarray or None If there is no :attr:`condorcet_winner_rk_ctb`, then return None. If there is a :attr:`condorcet_winner_rk_ctb`, denote her `w`. `c_might_be_there_when_cw_rk_is_eliminated_irv_style[c]` is True iff there exists a subset of candidates including `c` and `w` such that, in a Plurality election over this subset of candidates, `w` has at most `n_v / card(subset)` votes. What is interesting is the negation: if it is False, then it is impossible for `c` to manipulate in several IRV-related rules, because `c` cannot be there when `w` is eliminated. Examples -------- When there is no Condorcet winner (rk ctb), then the output is None: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_rk_ctb nan >>> print(profile.c_might_be_there_when_cw_is_eliminated_irv_style) None """ if not self.exists_condorcet_winner_rk_ctb: return None w = self.condorcet_winner_rk_ctb c_might_be_there_when_w_is_eliminated_irv_style = np.zeros(self.n_c, dtype=bool) c_might_be_there_when_w_is_eliminated_irv_style[w] = True # To allow for np.all(), but remove at the end other_candidates = [c for c in range(self.n_c) if c != w] for n_opponents in range(2, self.n_c): for subset in itertools.combinations(other_candidates, n_opponents): candidates_r = np.array([w] + list(subset)) preferences_borda_rk_r = self.preferences_borda_rk[:, candidates_r] score_w_r = np.sum(np.argmax(preferences_borda_rk_r, axis=1) == 0) if score_w_r <= self.n_v / (n_opponents + 1): c_might_be_there_when_w_is_eliminated_irv_style[np.array(subset)] = True if np.all(c_might_be_there_when_w_is_eliminated_irv_style): c_might_be_there_when_w_is_eliminated_irv_style[w] = False return c_might_be_there_when_w_is_eliminated_irv_style c_might_be_there_when_w_is_eliminated_irv_style[w] = False return c_might_be_there_when_w_is_eliminated_irv_style @cached_property def exists_irv_immune_candidate(self): """Whether there exists a 'IRV-immune' candidate. A candidate `w` is IRV-immune iff she is Condorcet winner (rk ctb) and for any other candidate `c`, we have that `c_might_be_there_when_cw_is_eliminated_irv_style[c] ` is False. In other words, there is no subset of candidates containing `w` such that `w` has at most `n_v / card(subset)` plurality votes. Examples -------- >>> profile = Profile(preferences_rk=[[0, 2, 1], [0, 2, 1], [1, 2, 0], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_rk_ctb 2 >>> profile.exists_irv_immune_candidate False """ return self.exists_condorcet_order_rk_ctb and not(np.any(self.c_might_be_there_when_cw_is_eliminated_irv_style)) @cached_property def necessary_coalition_size_to_break_irv_immunity(self): """Necessary coalition size to break IRV immunity. Returns ------- ndarray or None If there is no :attr:`condorcet_winner_rk_ctb`, then return None. If there is a :attr:`condorcet_winner_rk_ctb`, denote her `w`. For a given `c`, consider the voters who would be sincere in case of manipulation for `c`, i.e. those who do not prefer `c` to `w`. Now, consider a subset of candidates that includes `c` and `w`. The sincere voters ensure that `w` has a certain plurality score `s`. To eliminate `w` in this configuration, it is necessary that the total number of voters (sincere + manipulators) is greater than `s` multiplied by the number of candidates in the subset. Implicitly, this implies a necessary number of manipulators. Taking the minimal value of this necessary number over all possible subsets, this gives `necessary_coalition_size_to_break_irv_immunity[c]`. In an IRV-style election, it is necessary to have at least this number of manipulators to manipulate in favor of `c`. Examples -------- When there is no Condorcet winner (rk ctb), then the output is None: >>> profile = Profile(preferences_rk=[[0, 1, 2], [1, 2, 0], [2, 0, 1]]) >>> profile.condorcet_winner_rk_ctb nan >>> print(profile.necessary_coalition_size_to_break_irv_immunity) None """ if not self.exists_condorcet_winner_rk_ctb: return None w = self.condorcet_winner_rk_ctb result = np.zeros(self.n_c, dtype=int) for c in range(self.n_c): if c == w: continue other_candidates = [d for d in range(self.n_c) if d != c and d != w] n_s = self.n_v - self.matrix_duels_ut[c, w] voters_s = (self.preferences_ut[:, w] >= self.preferences_ut[:, c]) n_m = None for n_opponents in range(0, self.n_c - 1): nb_candidates_r = n_opponents + 2 for subset in itertools.combinations(other_candidates, n_opponents): candidates_r = np.array([w] + [c] + list(subset)) preferences_borda_rk_s_r = self.preferences_borda_rk[voters_s, :][:, candidates_r] score_w_s_r = np.sum(np.argmax(preferences_borda_rk_s_r, axis=1) == 0) n_m_new = score_w_s_r * nb_candidates_r - n_s if n_m is None: n_m = n_m_new else: n_m = min(n_m, n_m_new) result[c] = n_m return result