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.
Files changed (92) hide show
  1. pref_voting/__init__.py +1 -0
  2. pref_voting/analysis.py +496 -0
  3. pref_voting/axiom.py +38 -0
  4. pref_voting/axiom_helpers.py +129 -0
  5. pref_voting/axioms.py +10 -0
  6. pref_voting/c1_methods.py +963 -0
  7. pref_voting/combined_methods.py +514 -0
  8. pref_voting/create_methods.py +128 -0
  9. pref_voting/data/examples/condorcet_winner/minimal_Anti-Plurality.soc +16 -0
  10. pref_voting/data/examples/condorcet_winner/minimal_Borda.soc +17 -0
  11. pref_voting/data/examples/condorcet_winner/minimal_Bracket_Voting.soc +20 -0
  12. pref_voting/data/examples/condorcet_winner/minimal_Bucklin.soc +19 -0
  13. pref_voting/data/examples/condorcet_winner/minimal_Coombs.soc +20 -0
  14. pref_voting/data/examples/condorcet_winner/minimal_Coombs_PUT.soc +20 -0
  15. pref_voting/data/examples/condorcet_winner/minimal_Coombs_TB.soc +20 -0
  16. pref_voting/data/examples/condorcet_winner/minimal_Dowdall.soc +19 -0
  17. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff.soc +18 -0
  18. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_PUT.soc +18 -0
  19. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_TB.soc +18 -0
  20. pref_voting/data/examples/condorcet_winner/minimal_Iterated_Removal_Condorcet_Loser.soc +17 -0
  21. pref_voting/data/examples/condorcet_winner/minimal_Pareto.soc +17 -0
  22. pref_voting/data/examples/condorcet_winner/minimal_Plurality.soc +18 -0
  23. pref_voting/data/examples/condorcet_winner/minimal_PluralityWRunoff_PUT.soc +18 -0
  24. pref_voting/data/examples/condorcet_winner/minimal_Positive-Negative_Voting.soc +17 -0
  25. pref_voting/data/examples/condorcet_winner/minimal_Simplified_Bucklin.soc +18 -0
  26. pref_voting/data/examples/condorcet_winner/minimal_Superior_Voting.soc +19 -0
  27. pref_voting/data/examples/condorcet_winner/minimal_Weighted_Bucklin.soc +19 -0
  28. pref_voting/data/examples/condorcet_winner/minimal_resolute_Anti-Plurality.soc +17 -0
  29. pref_voting/data/examples/condorcet_winner/minimal_resolute_Borda.soc +17 -0
  30. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bracket_Voting.soc +20 -0
  31. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bucklin.soc +19 -0
  32. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs.soc +21 -0
  33. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_PUT.soc +21 -0
  34. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_TB.soc +20 -0
  35. pref_voting/data/examples/condorcet_winner/minimal_resolute_Dowdall.soc +18 -0
  36. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff.soc +18 -0
  37. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_PUT.soc +18 -0
  38. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_TB.soc +18 -0
  39. pref_voting/data/examples/condorcet_winner/minimal_resolute_Plurality.soc +18 -0
  40. pref_voting/data/examples/condorcet_winner/minimal_resolute_PluralityWRunoff_PUT.soc +18 -0
  41. pref_voting/data/examples/condorcet_winner/minimal_resolute_Positive-Negative_Voting.soc +17 -0
  42. pref_voting/data/examples/condorcet_winner/minimal_resolute_Simplified_Bucklin.soc +20 -0
  43. pref_voting/data/examples/condorcet_winner/minimal_resolute_Weighted_Bucklin.soc +19 -0
  44. pref_voting/data/voting_methods_properties.json +414 -0
  45. pref_voting/data/voting_methods_properties.json.lock +0 -0
  46. pref_voting/dominance_axioms.py +387 -0
  47. pref_voting/generate_profiles.py +801 -0
  48. pref_voting/generate_spatial_profiles.py +198 -0
  49. pref_voting/generate_utility_profiles.py +160 -0
  50. pref_voting/generate_weighted_majority_graphs.py +506 -0
  51. pref_voting/grade_methods.py +184 -0
  52. pref_voting/grade_profiles.py +357 -0
  53. pref_voting/helper.py +370 -0
  54. pref_voting/invariance_axioms.py +671 -0
  55. pref_voting/io/__init__.py +0 -0
  56. pref_voting/io/readers.py +432 -0
  57. pref_voting/io/writers.py +256 -0
  58. pref_voting/iterative_methods.py +2425 -0
  59. pref_voting/maj_graph_ex1.png +0 -0
  60. pref_voting/mappings.py +577 -0
  61. pref_voting/margin_based_methods.py +2345 -0
  62. pref_voting/monotonicity_axioms.py +872 -0
  63. pref_voting/num_evaluation_method.py +77 -0
  64. pref_voting/other_axioms.py +161 -0
  65. pref_voting/other_methods.py +939 -0
  66. pref_voting/pairwise_profiles.py +547 -0
  67. pref_voting/prob_voting_method.py +105 -0
  68. pref_voting/probabilistic_methods.py +287 -0
  69. pref_voting/profiles.py +856 -0
  70. pref_voting/profiles_with_ties.py +1069 -0
  71. pref_voting/rankings.py +466 -0
  72. pref_voting/scoring_methods.py +481 -0
  73. pref_voting/social_welfare_function.py +59 -0
  74. pref_voting/social_welfare_functions.py +7 -0
  75. pref_voting/spatial_profiles.py +448 -0
  76. pref_voting/stochastic_methods.py +99 -0
  77. pref_voting/strategic_axioms.py +1394 -0
  78. pref_voting/swf_axioms.py +173 -0
  79. pref_voting/utility_functions.py +102 -0
  80. pref_voting/utility_methods.py +178 -0
  81. pref_voting/utility_profiles.py +333 -0
  82. pref_voting/variable_candidate_axioms.py +640 -0
  83. pref_voting/variable_voter_axioms.py +3747 -0
  84. pref_voting/voting_method.py +355 -0
  85. pref_voting/voting_method_properties.py +92 -0
  86. pref_voting/voting_methods.py +8 -0
  87. pref_voting/voting_methods_registry.py +136 -0
  88. pref_voting/weighted_majority_graphs.py +1539 -0
  89. pref_voting-1.16.31.dist-info/METADATA +208 -0
  90. pref_voting-1.16.31.dist-info/RECORD +92 -0
  91. pref_voting-1.16.31.dist-info/WHEEL +4 -0
  92. pref_voting-1.16.31.dist-info/licenses/LICENSE.txt +21 -0
Binary file
@@ -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
+