pref_voting 1.16.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pref_voting/__init__.py +1 -0
- pref_voting/analysis.py +496 -0
- pref_voting/axiom.py +38 -0
- pref_voting/axiom_helpers.py +129 -0
- pref_voting/axioms.py +10 -0
- pref_voting/c1_methods.py +963 -0
- pref_voting/combined_methods.py +514 -0
- pref_voting/create_methods.py +128 -0
- pref_voting/data/examples/condorcet_winner/minimal_Anti-Plurality.soc +16 -0
- pref_voting/data/examples/condorcet_winner/minimal_Borda.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Bracket_Voting.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs_PUT.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs_TB.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Dowdall.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_TB.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Iterated_Removal_Condorcet_Loser.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Pareto.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Plurality.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_PluralityWRunoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Positive-Negative_Voting.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Simplified_Bucklin.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Superior_Voting.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Weighted_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Anti-Plurality.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Borda.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Bracket_Voting.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs.soc +21 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_PUT.soc +21 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_TB.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Dowdall.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_TB.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Plurality.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_PluralityWRunoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Positive-Negative_Voting.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Simplified_Bucklin.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Weighted_Bucklin.soc +19 -0
- pref_voting/data/voting_methods_properties.json +414 -0
- pref_voting/data/voting_methods_properties.json.lock +0 -0
- pref_voting/dominance_axioms.py +387 -0
- pref_voting/generate_profiles.py +801 -0
- pref_voting/generate_spatial_profiles.py +198 -0
- pref_voting/generate_utility_profiles.py +160 -0
- pref_voting/generate_weighted_majority_graphs.py +506 -0
- pref_voting/grade_methods.py +184 -0
- pref_voting/grade_profiles.py +357 -0
- pref_voting/helper.py +370 -0
- pref_voting/invariance_axioms.py +671 -0
- pref_voting/io/__init__.py +0 -0
- pref_voting/io/readers.py +432 -0
- pref_voting/io/writers.py +256 -0
- pref_voting/iterative_methods.py +2425 -0
- pref_voting/maj_graph_ex1.png +0 -0
- pref_voting/mappings.py +577 -0
- pref_voting/margin_based_methods.py +2345 -0
- pref_voting/monotonicity_axioms.py +872 -0
- pref_voting/num_evaluation_method.py +77 -0
- pref_voting/other_axioms.py +161 -0
- pref_voting/other_methods.py +939 -0
- pref_voting/pairwise_profiles.py +547 -0
- pref_voting/prob_voting_method.py +105 -0
- pref_voting/probabilistic_methods.py +287 -0
- pref_voting/profiles.py +856 -0
- pref_voting/profiles_with_ties.py +1069 -0
- pref_voting/rankings.py +466 -0
- pref_voting/scoring_methods.py +481 -0
- pref_voting/social_welfare_function.py +59 -0
- pref_voting/social_welfare_functions.py +7 -0
- pref_voting/spatial_profiles.py +448 -0
- pref_voting/stochastic_methods.py +99 -0
- pref_voting/strategic_axioms.py +1394 -0
- pref_voting/swf_axioms.py +173 -0
- pref_voting/utility_functions.py +102 -0
- pref_voting/utility_methods.py +178 -0
- pref_voting/utility_profiles.py +333 -0
- pref_voting/variable_candidate_axioms.py +640 -0
- pref_voting/variable_voter_axioms.py +3747 -0
- pref_voting/voting_method.py +355 -0
- pref_voting/voting_method_properties.py +92 -0
- pref_voting/voting_methods.py +8 -0
- pref_voting/voting_methods_registry.py +136 -0
- pref_voting/weighted_majority_graphs.py +1539 -0
- pref_voting-1.16.31.dist-info/METADATA +208 -0
- pref_voting-1.16.31.dist-info/RECORD +92 -0
- pref_voting-1.16.31.dist-info/WHEEL +4 -0
- pref_voting-1.16.31.dist-info/licenses/LICENSE.txt +21 -0
Binary file
|
pref_voting/mappings.py
ADDED
@@ -0,0 +1,577 @@
|
|
1
|
+
'''
|
2
|
+
File: mappings.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: September 23, 20203
|
5
|
+
|
6
|
+
Classes that represent mappings of utilities/grades to candidates.
|
7
|
+
'''
|
8
|
+
|
9
|
+
import functools
|
10
|
+
import numpy as np
|
11
|
+
from pref_voting.rankings import Ranking
|
12
|
+
|
13
|
+
# turn off future warnings.
|
14
|
+
# getting the following warning when calling tabulate to display a profile:
|
15
|
+
# /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/tabulate.py:1027: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
|
16
|
+
# if headers == "keys" and not rows:
|
17
|
+
# see https://stackoverflow.com/questions/40659212/futurewarning-elementwise-comparison-failed-returning-scalar-but-in-the-futur
|
18
|
+
#
|
19
|
+
import warnings
|
20
|
+
warnings.simplefilter(action='ignore', category=FutureWarning)
|
21
|
+
|
22
|
+
# some helper functions
|
23
|
+
|
24
|
+
def val_map_function(v, val_map):
|
25
|
+
return "None" if v is None else val_map[v]
|
26
|
+
|
27
|
+
def default_compare_function(v1, v2):
|
28
|
+
if v1 > v2:
|
29
|
+
return 1
|
30
|
+
elif v2 > v1:
|
31
|
+
return -1
|
32
|
+
else:
|
33
|
+
return 0
|
34
|
+
|
35
|
+
class _Mapping(object):
|
36
|
+
"""
|
37
|
+
A partial function on a set of items.
|
38
|
+
|
39
|
+
Attributes:
|
40
|
+
mapping: a dictionary representing the mapping
|
41
|
+
domain: the domain of the mapping
|
42
|
+
codomain: the codomain of the mapping
|
43
|
+
item_map: a dictionary mapping items to their names
|
44
|
+
val_map: a dictionary mapping values to their names
|
45
|
+
compare_function: a function used to compare values
|
46
|
+
"""
|
47
|
+
def __init__(
|
48
|
+
self,
|
49
|
+
mapping, # a dictionary representing the mapping
|
50
|
+
domain=None, # if domain is None, then it is assumed to be all keys (so mapping is a total function)
|
51
|
+
codomain=None, # if codomain is None, then it is assumed to be any number
|
52
|
+
compare_function=None, # function used to compare values
|
53
|
+
item_map=None, # a dictionary mapping items to their names
|
54
|
+
val_map=None, # a dictionary mapping values to their names
|
55
|
+
):
|
56
|
+
|
57
|
+
assert domain is None or all([x in domain for x in mapping.keys()]), f"Not all keys in {mapping} are in the domain {domain}."
|
58
|
+
|
59
|
+
assert domain is not None or all([isinstance(v, (int, float)) for v in mapping.values()]), f"Not all values in {mapping} are numbers."
|
60
|
+
|
61
|
+
assert codomain is None or all([v in codomain for v in mapping.values()]), f"Not all values in {mapping} are in the codomain {codomain}."
|
62
|
+
|
63
|
+
self.mapping = mapping
|
64
|
+
self.domain = domain if domain is not None else sorted(list(mapping.keys())) # if domain is None, then it is assumed to be all keys (so mapping is a total function)
|
65
|
+
self.codomain = codomain # if codomain is None, then it is assumed to be any number
|
66
|
+
self.item_map = item_map if item_map is not None else {x:str(x) for x in self.domain} # a dictionary mapping items in the domain to their names
|
67
|
+
|
68
|
+
val_map = val_map if val_map is not None else {v:str(v) for v in self.mapping.values()} # a dictionary mapping values to their names
|
69
|
+
self.val_map = functools.partial(val_map_function, val_map = val_map) # a function mapping values to their names
|
70
|
+
|
71
|
+
self.compare_function = compare_function if compare_function is not None else default_compare_function
|
72
|
+
|
73
|
+
def val(self, x):
|
74
|
+
"""
|
75
|
+
The value assigned to x by the mapping. If x is in the domain but not defined by the mapping, then None is returned.
|
76
|
+
"""
|
77
|
+
assert x in self.domain, f"{x} not in the domain {self.domain}"
|
78
|
+
return self.mapping[x] if x in self.mapping.keys() else None
|
79
|
+
|
80
|
+
def has_value(self, x):
|
81
|
+
return x in self.mapping.keys()
|
82
|
+
|
83
|
+
@property
|
84
|
+
def defined_domain(self):
|
85
|
+
return sorted(list(self.mapping.keys()))
|
86
|
+
|
87
|
+
def inverse_image(self, v):
|
88
|
+
"""Return all the elements in the domain that are mapped to v."""
|
89
|
+
return [x for x in self.domain if self.val(x) == v]
|
90
|
+
|
91
|
+
def image(self, items=None):
|
92
|
+
"""
|
93
|
+
The image of the mapping.
|
94
|
+
"""
|
95
|
+
items = self.defined_domain if items is None else items
|
96
|
+
return list([self.val(x) for x in items if self.val(x) is not None])
|
97
|
+
|
98
|
+
@property
|
99
|
+
def range(self):
|
100
|
+
"""
|
101
|
+
The range of the mapping.
|
102
|
+
"""
|
103
|
+
return sorted(list(set(self.mapping.values())))
|
104
|
+
|
105
|
+
def average(self, **kwargs):
|
106
|
+
"""
|
107
|
+
Return the average utility of all elements in alternatives. If alternatives is None, then find the average utility of all elements that are assigned a utility.
|
108
|
+
"""
|
109
|
+
|
110
|
+
items = kwargs.get("items", None) or kwargs.get("candidates", None) or kwargs.get("alternatives", None)
|
111
|
+
|
112
|
+
assert items is None or all([isinstance(self.val(x), (int, float)) or self.val(x) is None for x in items]), "Not all values are numbers."
|
113
|
+
|
114
|
+
img = self.image(items=items)
|
115
|
+
return np.mean(img) if len(img) > 0 else None
|
116
|
+
|
117
|
+
def median(self, **kwargs):
|
118
|
+
"""
|
119
|
+
Return the median utility of all elements in alternatives. If alternatives is None, then find the average utility of all elements that are assigned a utility.
|
120
|
+
"""
|
121
|
+
|
122
|
+
items = kwargs.get("items", None) or kwargs.get("candidates", None) or kwargs.get("alternatives", None)
|
123
|
+
|
124
|
+
assert items is None or all([isinstance(self.val(x), (int, float)) or self.val(x) is None for x in items]), "Not all values are numbers."
|
125
|
+
|
126
|
+
img = self.image(items=items)
|
127
|
+
return np.median(img) if len(img) > 0 else None
|
128
|
+
|
129
|
+
def compare(self, x, y):
|
130
|
+
"""
|
131
|
+
Return 1 if the value of x is greater than the value of y, 0 if they are equal, and -1 if the value of x is less than the value of y.
|
132
|
+
|
133
|
+
If either x or y is not defined, then None is returned.
|
134
|
+
"""
|
135
|
+
assert x in self.domain, f"{x} not in the domain {self.domain}"
|
136
|
+
assert y in self.domain, f"{y} not in the domain {self.domain}"
|
137
|
+
|
138
|
+
return None if (not self.has_value(x) or not self.has_value(y)) else self.compare_function(self.val(x), self.val(y))
|
139
|
+
|
140
|
+
def extended_compare(self, x, y):
|
141
|
+
"""
|
142
|
+
Return 1 if the value of x is greater than the value of y or x has a value and y does not have a value, 0 if they are equal or both do not have values, and -1 if the value of y is greater than the value of x or y has a value and x does not have a value.
|
143
|
+
|
144
|
+
If either x or y is not defined, then None is returned.
|
145
|
+
"""
|
146
|
+
assert x in self.domain, f"{x} not in the domain {self.domain}"
|
147
|
+
assert y in self.domain, f"{y} not in the domain {self.domain}"
|
148
|
+
|
149
|
+
if self.has_value(x) and not self.has_value(y):
|
150
|
+
return 1
|
151
|
+
elif self.has_value(y) and not self.has_value(x):
|
152
|
+
return -1
|
153
|
+
elif not self.has_value(x) and not self.has_value(y):
|
154
|
+
return 0
|
155
|
+
else:
|
156
|
+
return self.compare_function(self.val(x), self.val(y))
|
157
|
+
|
158
|
+
def strict_pref(self, x, y):
|
159
|
+
"""Returns True if ``x`` is strictly preferred to ``y``.
|
160
|
+
|
161
|
+
The return value is True when both ``x`` and ``y`` are assigned values and the value of ``x`` is strictly greater than the utility of ``y`` according to the compare function.
|
162
|
+
"""
|
163
|
+
return self.compare(x, y) == 1
|
164
|
+
|
165
|
+
def extended_strict_pref(self, x, y):
|
166
|
+
"""Returns True if ``x`` is strictly preferred to ``y`` using the extended compare function.
|
167
|
+
|
168
|
+
The return value is True when the value of ``x`` is strictly greater than the value of ``y`` or ``x`` is assigned a value and ``y`` is not assigned a value.
|
169
|
+
"""
|
170
|
+
return self.extended_compare(x, y) == 1
|
171
|
+
|
172
|
+
def indiff(self, x, y):
|
173
|
+
"""Returns True if ``x`` is indifferent with ``y``.
|
174
|
+
|
175
|
+
The return value is True when both ``x`` and ``y`` are assigned values and the value of ``x`` equals the value of ``y``.
|
176
|
+
"""
|
177
|
+
return self.compare(x, y) == 0
|
178
|
+
|
179
|
+
def extended_indiff(self, x, y):
|
180
|
+
"""Returns True if ``x`` is indifferent with ``y`` using the ``extended_compare`` function.
|
181
|
+
|
182
|
+
The return value is True when the value of ``x`` equals the value of ``y`` or both ``x`` and ``y`` are not assigned values.
|
183
|
+
"""
|
184
|
+
return self.extended_compare(x, y) == 0
|
185
|
+
|
186
|
+
def weak_pref(self, x, y):
|
187
|
+
"""Returns True if ``x`` is weakly preferred to ``y``.
|
188
|
+
|
189
|
+
The return value is True when both ``x`` and ``y`` are assigned utilities and the utility of ``x`` is at least as greater than the utility of ``y``.
|
190
|
+
"""
|
191
|
+
|
192
|
+
return self.strict_pref(x, y) or self.indiff(x, y)
|
193
|
+
|
194
|
+
def extended_weak_pref(self, x, y):
|
195
|
+
"""Returns True if ``x`` is weakly preferred to ``y``.
|
196
|
+
|
197
|
+
The return value is True when both ``x`` and ``y`` are assigned utilities and the utility of ``x`` is at least as greater than the utility of ``y``.
|
198
|
+
"""
|
199
|
+
|
200
|
+
return self.extended_strict_pref(x, y) or self.extended_indiff(x, y)
|
201
|
+
|
202
|
+
def _indifference_classes(self, items, use_extended=False):
|
203
|
+
"""
|
204
|
+
Return a list of the indifference classes of the items.
|
205
|
+
"""
|
206
|
+
indiff_classes = list()
|
207
|
+
processed_items = set()
|
208
|
+
compare_fnc = self.extended_compare if use_extended else self.compare
|
209
|
+
for x in items:
|
210
|
+
if x not in processed_items:
|
211
|
+
indiff = [y for y in items if compare_fnc(x, y) == 0]
|
212
|
+
if len(indiff) > 0:
|
213
|
+
indiff_classes.append(indiff)
|
214
|
+
for y in indiff:
|
215
|
+
processed_items.add(y)
|
216
|
+
return indiff_classes
|
217
|
+
|
218
|
+
def sorted_domain(self, extended=False):
|
219
|
+
"""
|
220
|
+
Return a list of the indifference classes sorted according to the compare function (or extended compare function if extended is True).
|
221
|
+
"""
|
222
|
+
indiff_classes = self._indifference_classes(self.domain, use_extended=True) if extended else self._indifference_classes(self.defined_domain)
|
223
|
+
|
224
|
+
compare_fnc = self.extended_compare if extended else self.compare
|
225
|
+
|
226
|
+
key_func = functools.cmp_to_key(lambda xs, ys : compare_fnc(xs[0], ys[0]))
|
227
|
+
|
228
|
+
return sorted(indiff_classes, key=key_func, reverse=True)
|
229
|
+
|
230
|
+
def as_dict(self):
|
231
|
+
"""
|
232
|
+
Return the mapping as a dictionary.
|
233
|
+
"""
|
234
|
+
return {c: self.val(c) for c in self.defined_domain}
|
235
|
+
|
236
|
+
def display_str(self, func_name):
|
237
|
+
"""
|
238
|
+
Return a string representation of the mapping.
|
239
|
+
"""
|
240
|
+
return f"{', '.join([f'{func_name}({self.item_map[x]}) = {self.val_map(self.val(x))}' for x in self.domain])}"
|
241
|
+
|
242
|
+
def __call__(self, x):
|
243
|
+
return self.val(x)
|
244
|
+
|
245
|
+
def __repr__(self):
|
246
|
+
return f"{self.mapping}"
|
247
|
+
|
248
|
+
def __str__(self):
|
249
|
+
return f'{", ".join([f"{self.item_map[x]}:{self.val_map(self.val(x))}" for x in self.domain])}'
|
250
|
+
|
251
|
+
########
|
252
|
+
# Utility Functions
|
253
|
+
########
|
254
|
+
|
255
|
+
class Utility(_Mapping):
|
256
|
+
def __init__(self, utils, **kwargs):
|
257
|
+
"""Constructor method for the Utility class."""
|
258
|
+
|
259
|
+
if "domain" in kwargs and "candidates" in kwargs:
|
260
|
+
raise ValueError("You can only provide either 'domain' or 'candidates', not both.")
|
261
|
+
if "domain" in kwargs:
|
262
|
+
self.domain = kwargs["domain"]
|
263
|
+
elif "candidates" in kwargs:
|
264
|
+
self.domain = kwargs["candidates"]
|
265
|
+
else:
|
266
|
+
self.domain = list(utils.keys())
|
267
|
+
|
268
|
+
self.cmap = {x:str(x) for x in self.domain} if "cmap" not in kwargs else kwargs["cmap"]
|
269
|
+
|
270
|
+
assert self.domain is None or all([x in self.domain for x in utils.keys()]), f"The domain {self.domain} must contain all elements in the utility map {utils}"
|
271
|
+
|
272
|
+
super().__init__(utils, domain=self.domain, item_map = self.cmap)
|
273
|
+
|
274
|
+
@property
|
275
|
+
def candidates(self):
|
276
|
+
return self.domain
|
277
|
+
|
278
|
+
def items_with_util(self, u):
|
279
|
+
"""Returns a list of the items that are assigned the utility ``u``."""
|
280
|
+
return self.inverse_image(u)
|
281
|
+
|
282
|
+
def has_utility(self, x):
|
283
|
+
"""Return True if x has a utility."""
|
284
|
+
return self.has_value(x)
|
285
|
+
|
286
|
+
def remove_cand(self, x):
|
287
|
+
"""Returns a utility with the item ``x`` removed."""
|
288
|
+
|
289
|
+
new_utils = {y: self.val(y) for y in self.defined_domain if y != x}
|
290
|
+
new_domain = [y for y in self.domain if y != x]
|
291
|
+
new_cmap = {y: self.cmap[y] for y in self.cmap.keys() if y != x}
|
292
|
+
return Utility(new_utils, domain=new_domain, cmap=new_cmap)
|
293
|
+
|
294
|
+
def to_approval_ballot(self, prob_to_cont_approving=1.0, decay_rate=0.0):
|
295
|
+
"""
|
296
|
+
Return an approval ballot representation of the mapping. It is assumed that the voter approves of all candidates with a utility greater than the average of the utilities assigned to the candidates.
|
297
|
+
|
298
|
+
The parameter ``prob_to_cont_approving`` is the probability that the voter continues to approve of candidates after the first candidate is approved. The parameter ``decay_rate`` is the exponential decay rate constant in the exponential decay of the probability to continue approving.
|
299
|
+
"""
|
300
|
+
avg_grade = self.average()
|
301
|
+
|
302
|
+
main_approval_set = {x:self.val(x) for x in self.defined_domain if self.val(x) > avg_grade}
|
303
|
+
|
304
|
+
sorted_approval_set = sorted(main_approval_set.items(), key=lambda a: a[1], reverse=True)
|
305
|
+
|
306
|
+
# initialize approval set with the candidate with the highest utility
|
307
|
+
approval_set = [sorted_approval_set[0][0]]
|
308
|
+
|
309
|
+
t = 0
|
310
|
+
for x, u in sorted_approval_set[1:]:
|
311
|
+
if np.random.rand() < prob_to_cont_approving * np.exp(-decay_rate * t):
|
312
|
+
approval_set.append(x)
|
313
|
+
t += 1
|
314
|
+
else:
|
315
|
+
break
|
316
|
+
|
317
|
+
return Grade(
|
318
|
+
{
|
319
|
+
x: 1 if x in approval_set else 0 for x in self.defined_domain
|
320
|
+
},
|
321
|
+
[0, 1],
|
322
|
+
candidates=self.domain,
|
323
|
+
cmap=self.cmap
|
324
|
+
)
|
325
|
+
|
326
|
+
def to_k_approval_ballot(self, k, prob_to_cont_approving=1.0, decay_rate=0.0):
|
327
|
+
"""
|
328
|
+
Return an k-approval ballot representation of the mapping. It is assumed that the voter approves of the top k candidates with a utility greater than the average of the utilities assigned to the candidates.
|
329
|
+
|
330
|
+
The parameter ``prob_to_cont_approving`` is the probability that the voter continues to approve of candidates after the first candidate is approved. The parameter ``decay_rate`` is the exponential decay rate constant in the exponential decay of the probability to continue approving.
|
331
|
+
"""
|
332
|
+
avg_grade = self.average()
|
333
|
+
|
334
|
+
main_approval_set = {x:self.val(x) for x in self.defined_domain if self.val(x) > avg_grade}
|
335
|
+
|
336
|
+
sorted_approval_set = sorted(main_approval_set.items(), key=lambda a: a[1], reverse=True)
|
337
|
+
|
338
|
+
# initialize approval set with the candidate with the highest utility
|
339
|
+
approval_set = [sorted_approval_set[0][0]]
|
340
|
+
|
341
|
+
t = 0
|
342
|
+
for x, u in sorted_approval_set[1:]:
|
343
|
+
if np.random.rand() < prob_to_cont_approving * np.exp(-decay_rate * t):
|
344
|
+
approval_set.append(x)
|
345
|
+
t += 1
|
346
|
+
else:
|
347
|
+
break
|
348
|
+
|
349
|
+
if len(approval_set) == k:
|
350
|
+
break
|
351
|
+
|
352
|
+
return Grade(
|
353
|
+
{
|
354
|
+
x: 1 if x in approval_set else 0 for x in self.defined_domain
|
355
|
+
},
|
356
|
+
[0, 1],
|
357
|
+
candidates=self.domain,
|
358
|
+
cmap=self.cmap
|
359
|
+
)
|
360
|
+
|
361
|
+
def ranking(self):
|
362
|
+
"""Return the ranking generated by this utility function."""
|
363
|
+
return Ranking(
|
364
|
+
{x:idx + 1 for idx, indiff_class in enumerate(self.sorted_domain())
|
365
|
+
for x in indiff_class})
|
366
|
+
|
367
|
+
def extended_ranking(self):
|
368
|
+
"""Return the ranking generated by this utility function."""
|
369
|
+
|
370
|
+
return Ranking(
|
371
|
+
{x:idx + 1 for idx, indiff_class in enumerate(self.sorted_domain(extended=True))
|
372
|
+
for x in indiff_class})
|
373
|
+
|
374
|
+
def has_tie(self, use_extended=False):
|
375
|
+
"""Return True when there are at least two candidates that are assigned the same utility."""
|
376
|
+
return any([len(cs) != 1 for cs in self.sorted_domain(extended=use_extended)])
|
377
|
+
|
378
|
+
def is_linear(self, num_cands):
|
379
|
+
"""Return True when the assignment of utilities is a linear order of ``num_cands`` candidates.
|
380
|
+
"""
|
381
|
+
|
382
|
+
return self.ranking().is_linear(num_cands=num_cands)
|
383
|
+
|
384
|
+
def represents_ranking(self, r, use_extended=False):
|
385
|
+
"""Return True when the utility represents the ranking ``r``."""
|
386
|
+
|
387
|
+
if use_extended:
|
388
|
+
for x in r.cands:
|
389
|
+
for y in r.cands:
|
390
|
+
if r.extended_strict_pref(x, y) and not self.extended_strict_pref(x, y):
|
391
|
+
return False
|
392
|
+
elif r.extended_indiff(x, y) and not self.extended_indiff(x, y):
|
393
|
+
return False
|
394
|
+
|
395
|
+
else:
|
396
|
+
for x in r.cands:
|
397
|
+
if not self.has_utility(x):
|
398
|
+
return False
|
399
|
+
for x in r.cands:
|
400
|
+
for y in r.cands:
|
401
|
+
if r.strict_pref(x, y) and not self.strict_pref(x, y):
|
402
|
+
return False
|
403
|
+
elif r.indiff(x, y) and not self.indiff(x, y):
|
404
|
+
return False
|
405
|
+
return True
|
406
|
+
|
407
|
+
def transformation(self, func):
|
408
|
+
"""
|
409
|
+
Return a new utility function that is the transformation of this utility function by the function ``func``.
|
410
|
+
"""
|
411
|
+
return Utility({
|
412
|
+
x: func(self.val(x)) for x in self.defined_domain
|
413
|
+
},
|
414
|
+
domain = self.domain,
|
415
|
+
cmap=self.cmap)
|
416
|
+
|
417
|
+
def linear_transformation(self, a=1, b=0):
|
418
|
+
"""Return a linear transformation of the utility function: ``a * u(x) + b``.
|
419
|
+
"""
|
420
|
+
|
421
|
+
lin_func = lambda x: a * self.val(x) + b
|
422
|
+
return self.transformation(lin_func)
|
423
|
+
|
424
|
+
def normalize_by_range(self):
|
425
|
+
"""Return a normalized utility function. Applies the *Kaplan* normalization to the utility function:
|
426
|
+
The new utility of an element x of the domain is (u(x) - min({u(x) | x in the domain})) / (max({u(x) | x in the domain })).
|
427
|
+
"""
|
428
|
+
|
429
|
+
max_util = max(self.range)
|
430
|
+
min_util = min(self.range)
|
431
|
+
|
432
|
+
if max_util == min_util:
|
433
|
+
return Utility(
|
434
|
+
{x:0 for x in self.defined_domain},
|
435
|
+
domain = self.domain,
|
436
|
+
cmap=self.cmap)
|
437
|
+
else:
|
438
|
+
return Utility({
|
439
|
+
x: (self.val(x) - min_util) / (max_util - min_util) for x in self.defined_domain
|
440
|
+
},
|
441
|
+
domain = self.domain,
|
442
|
+
cmap=self.cmap)
|
443
|
+
|
444
|
+
def normalize_by_standard_score(self):
|
445
|
+
"""Replace each utility value with its standard score. The standard score of a value is the number of standard deviations it is above the mean.
|
446
|
+
"""
|
447
|
+
|
448
|
+
utility_values = np.array(list(self.image()))
|
449
|
+
|
450
|
+
mean_utility = np.mean(utility_values)
|
451
|
+
std_dev_utility = np.std(utility_values)
|
452
|
+
|
453
|
+
return Utility({
|
454
|
+
x: (self.val(x) - mean_utility) / std_dev_utility for x in self.defined_domain
|
455
|
+
},
|
456
|
+
domain = self.domain,
|
457
|
+
cmap=self.cmap)
|
458
|
+
|
459
|
+
def expectation(self, prob):
|
460
|
+
"""Return the expected utility given a probability distribution ``prob``."""
|
461
|
+
|
462
|
+
assert all([x in self.domain for x in prob.keys()]), "The domain of the probability distribution must be a subset of the domain of the utility function."
|
463
|
+
|
464
|
+
return sum([prob[x] * self.util(x) for x in self.domain if x in prob.keys() and self.has_utility(x)])
|
465
|
+
|
466
|
+
@classmethod
|
467
|
+
def from_linear_ranking(cls, ranking, seed=None):
|
468
|
+
"""
|
469
|
+
Return a utility function that represents the linear ranking.
|
470
|
+
|
471
|
+
Parameters:
|
472
|
+
ranking (List[int]): A list representing the linear ranking.
|
473
|
+
seed (Optional[int]): An optional seed for random number generation.
|
474
|
+
|
475
|
+
Returns:
|
476
|
+
Utility: An instance of the Utility class.
|
477
|
+
"""
|
478
|
+
|
479
|
+
if not (isinstance(ranking, list) or isinstance(ranking, tuple)):
|
480
|
+
raise ValueError("Ranking must be a list.")
|
481
|
+
if not len(set(ranking)) == len(ranking):
|
482
|
+
raise ValueError("Ranking must be a list of unique numbers.")
|
483
|
+
|
484
|
+
num_cands = len(ranking)
|
485
|
+
rng = np.random.default_rng(seed)
|
486
|
+
|
487
|
+
utilities = sorted(rng.random(size=num_cands), reverse=True)
|
488
|
+
u_dict = {c: u for c, u in zip(ranking, utilities)}
|
489
|
+
|
490
|
+
return cls(u_dict)
|
491
|
+
|
492
|
+
def __str__(self):
|
493
|
+
return self.display_str("U")
|
494
|
+
|
495
|
+
######
|
496
|
+
# Grade Functions
|
497
|
+
######
|
498
|
+
|
499
|
+
class Grade(_Mapping):
|
500
|
+
def __init__(
|
501
|
+
self,
|
502
|
+
grade_map,
|
503
|
+
grades,
|
504
|
+
candidates=None,
|
505
|
+
cmap=None,
|
506
|
+
gmap=None,
|
507
|
+
compare_function=None):
|
508
|
+
"""Constructor method for a Grade function."""
|
509
|
+
|
510
|
+
assert all([g in grades for g in grade_map.values()]), f"All the grades in the grade map {grade_map} must be in the grades {grades}"
|
511
|
+
assert candidates is None or all([x in candidates for x in grade_map.keys()]), f"The candidates {candidates} must contain all elements in the grade map {grade_map}"
|
512
|
+
|
513
|
+
self.candidates = sorted(candidates) if candidates is not None else sorted(list(grade_map.keys()))
|
514
|
+
|
515
|
+
self.cmap = {x:str(x) for x in self.candidates} if cmap is None else cmap
|
516
|
+
|
517
|
+
self.grades = grades
|
518
|
+
|
519
|
+
self.gmap = {g:str(g) for g in self.grades} if gmap is None else gmap
|
520
|
+
|
521
|
+
super().__init__(
|
522
|
+
grade_map,
|
523
|
+
domain=self.candidates,
|
524
|
+
codomain=self.grades,
|
525
|
+
item_map=self.cmap,
|
526
|
+
val_map=self.gmap,
|
527
|
+
compare_function=compare_function)
|
528
|
+
|
529
|
+
@property
|
530
|
+
def graded_candidates(self):
|
531
|
+
"""Returns a list of the items that are assigned a grade."""
|
532
|
+
return self.defined_domain
|
533
|
+
|
534
|
+
def candidates_with_grade(self, g):
|
535
|
+
"""Returns a list of the items that are assigned the grade ``g``."""
|
536
|
+
return self.inverse_image(g)
|
537
|
+
|
538
|
+
def has_grade(self, x):
|
539
|
+
"""Return True if x has a grade."""
|
540
|
+
return self.has_value(x)
|
541
|
+
|
542
|
+
def remove_cand(self, x):
|
543
|
+
"""Returns a grade function with the item ``x`` removed."""
|
544
|
+
|
545
|
+
new_grades = {y: self.val(y) for y in self.defined_domain if y != x}
|
546
|
+
new_candidates = [y for y in self.domain if y != x]
|
547
|
+
new_cmap = {y: self.cmap[y] for y in self.cmap.keys() if y != x}
|
548
|
+
return Grade(new_grades, grades=self.grades, candidates=new_candidates, cmap=new_cmap, gmap=self.gmap, compare_function=self.compare_function)
|
549
|
+
|
550
|
+
def ranking(self):
|
551
|
+
"""Return the ranking generated by this grade function."""
|
552
|
+
|
553
|
+
return Ranking(
|
554
|
+
{x:idx + 1 for idx, indiff_class in enumerate(self.sorted_domain())
|
555
|
+
for x in indiff_class})
|
556
|
+
|
557
|
+
def extended_ranking(self):
|
558
|
+
"""Return the ranking generated by this grade function."""
|
559
|
+
|
560
|
+
return Ranking(
|
561
|
+
{x:idx + 1 for idx, indiff_class in enumerate(self.sorted_domain(extended=True))
|
562
|
+
for x in indiff_class})
|
563
|
+
|
564
|
+
def has_tie(self, use_extended=False):
|
565
|
+
"""Return True when the utility has a tie."""
|
566
|
+
return any([len(cs) != 1 for cs in self.sorted_domain(extended=use_extended)])
|
567
|
+
|
568
|
+
def is_linear(self, num_cands):
|
569
|
+
"""Return True when the assignment of grades is a linear order of ``num_cands`` candidates.
|
570
|
+
"""
|
571
|
+
|
572
|
+
return self.ranking().is_linear(num_cands=num_cands)
|
573
|
+
|
574
|
+
def __str__(self):
|
575
|
+
return self.display_str("grade")
|
576
|
+
|
577
|
+
|