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
@@ -0,0 +1,547 @@
1
+ '''
2
+ File: pairwise_profiles.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: June 3, 2024
5
+
6
+ Functions to reason about profiles of pairwise comparisons.
7
+ '''
8
+
9
+
10
+ from math import ceil
11
+ import numpy as np
12
+ from numba import jit
13
+ import networkx as nx
14
+ from tabulate import tabulate
15
+ import matplotlib.pyplot as plt
16
+ from pref_voting.weighted_majority_graphs import MajorityGraph, MarginGraph, SupportGraph
17
+ from pref_voting.rankings import Ranking
18
+ import os
19
+
20
+ # turn off future warnings.
21
+ # getting the following warning when calling tabulate to display a profile:
22
+ # /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
23
+ # if headers == "keys" and not rows:
24
+ # see https://stackoverflow.com/questions/40659212/futurewarning-elementwise-comparison-failed-returning-scalar-but-in-the-futur
25
+ #
26
+ import warnings
27
+ warnings.simplefilter(action='ignore', category=FutureWarning)
28
+
29
+ class PairwiseBallot:
30
+
31
+ def __init__(self, comparisons, candidates=None, cmap=None):
32
+ """Constructor method for PairwiseBallot.
33
+
34
+ Args:
35
+ comparisons (list): List of tuples, lists, or sets representing pairwise comparisons.
36
+ candidates (list or set, optional): Initial set of candidates. Defaults to None.
37
+ cmap (dict, optional): Mapping of candidates to their names. Defaults to None.
38
+ """
39
+ self._comparisons = []
40
+
41
+ for comp in comparisons:
42
+ if not isinstance(comp, (tuple, list)) or len(comp) != 2:
43
+ raise ValueError("Each element of the list of comparisons should be a tuple or list of length 2.")
44
+
45
+ if all(isinstance(comp[i], (int, str)) for i in [0, 1]):
46
+ self._comparisons.append(({comp[0], comp[1]}, {comp[0]}))
47
+ elif all(isinstance(comp[i], (set, list, tuple)) for i in [0, 1]):
48
+ self._comparisons.append((set(comp[0]), set(comp[1])))
49
+ else:
50
+ raise ValueError("Each element of the list of comparisons should be a tuple of sets or lists of candidates.")
51
+
52
+ if not self._well_formed_comparisons():
53
+ raise ValueError("The pairwise comparisons are not coherent.")
54
+
55
+ self.candidates = sorted(list(set(c for menu, _ in self._comparisons for c in menu)) if candidates is None else candidates)
56
+ self.cmap = cmap if cmap is not None else {c: str(c) for c in self.candidates}
57
+
58
+ def _well_formed_comparisons(self):
59
+ """Check if the pairwise comparisons are all well-formed.
60
+
61
+ Returns:
62
+ bool: True if the pairwise comparisons are well-formed, False otherwise.
63
+ """
64
+ for menu, choice in self._comparisons:
65
+ if not choice.issubset(menu):
66
+ return False
67
+ menus = [menu for menu, _ in self._comparisons]
68
+ return len(menus) == len(set(frozenset(menu) for menu in menus))
69
+
70
+ def num_comparisons(self):
71
+ """Return the number of pairwise comparisons"""
72
+ return len(self._comparisons)
73
+
74
+ def weak_pref(self, c1, c2):
75
+ """Return the revealed weak preference of a menu of choices.
76
+
77
+ Args:
78
+ c1 (str or int): First candidate.
79
+ c2 (str or int): Second candidate.
80
+
81
+ Returns:
82
+ bool: True if there is a weak preference for c1 over c2, False otherwise.
83
+ """
84
+ return any(c1 in menu and c2 in menu and c1 in choice for menu, choice in self._comparisons)
85
+
86
+ def strict_pref(self, c1, c2):
87
+ """Return the revealed strict preference of a menu of choices.
88
+
89
+ Args:
90
+ c1 (str or int): First candidate.
91
+ c2 (str or int): Second candidate.
92
+
93
+ Returns:
94
+ bool: True if there is a strict preference for c1 over c2, False otherwise.
95
+ """
96
+ return self.weak_pref(c1, c2) and not self.weak_pref(c2, c1)
97
+
98
+ def indiff(self, c1, c2):
99
+ """Return the revealed indifference of a menu of choices.
100
+
101
+ Args:
102
+ c1 (str or int): First candidate.
103
+ c2 (str or int): Second candidate.
104
+
105
+ Returns:
106
+ bool: True if there is indifference between c1 and c2, False otherwise.
107
+ """
108
+ return self.weak_pref(c1, c2) and self.weak_pref(c2, c1)
109
+
110
+ def has_comparison(self, c1, c2):
111
+ """Check if there is a comparison between two candidates.
112
+
113
+ Args:
114
+ c1 (str or int): First candidate.
115
+ c2 (str or int): Second candidate.
116
+
117
+ Returns:
118
+ bool: True if there is a comparison between c1 and c2, False otherwise.
119
+ """
120
+ return any(c1 in menu and c2 in menu for menu, _ in self._comparisons)
121
+
122
+ def get_comparison(self, c1, c2):
123
+ """Get the comparison between two candidates.
124
+
125
+ Args:
126
+ c1 (str or int): First candidate.
127
+ c2 (str or int): Second candidate.
128
+
129
+ Returns:
130
+ tuple: The comparison between c1 and c2.
131
+ """
132
+ comp = [(menu, choice) for menu, choice in self._comparisons if c1 in menu and c2 in menu]
133
+ return comp[0] if len(comp) == 1 else None
134
+
135
+ def add_comparison(self, menu, choice):
136
+ """Add a new comparison to the existing comparisons.
137
+
138
+ Args:
139
+ menu (set): A set of candidates representing the menu.
140
+ choice (set): A set of candidates representing the choice set.
141
+
142
+ Raises:
143
+ ValueError: If the new comparison is not coherent with the existing comparisons.
144
+ """
145
+ new_comparison = (set(menu), set(choice))
146
+ self._comparisons.append(new_comparison)
147
+ if not self._well_formed_comparisons():
148
+ self._comparisons.pop()
149
+ raise ValueError("The new comparison is not well-formed given the existing comparisons.")
150
+ self.candidates = sorted(list(set(c for menu, _ in self._comparisons for c in menu)))
151
+
152
+ def add_strict_preference(self, c1, c2):
153
+ """Add a new comparison to the existing comparisons where c1 is strictly preferred to c2.
154
+
155
+ Args:
156
+ c1 (int, str): A candidate
157
+ c2 (int, str): A candidate.
158
+
159
+ Raises:
160
+ ValueError: If the new comparison is not coherent with the existing comparisons.
161
+ """
162
+ self.add_comparison({c1, c2}, {c1})
163
+
164
+ def is_transitive(self, cands):
165
+ """Return True of the comparisons is transitive on the set cands of candidates"""
166
+
167
+ for c1 in cands:
168
+ for c2 in cands:
169
+ for c3 in cands:
170
+ if self.weak_pref(c1, c2) and self.weak_pref(c2, c3) and not self.weak_pref(c1, c3):
171
+ #print(f"preference {c1} >= {c2} and {c2} >= {c3} but not {c1} >= {c3}")
172
+ return False
173
+ return True
174
+
175
+ def is_quasi_transitive(self, cands):
176
+ """Return True of the comparisons is transitive on the set cands of candidates"""
177
+
178
+ for c1 in cands:
179
+ for c2 in cands:
180
+ for c3 in cands:
181
+ if self.strict_pref(c1, c2) and self.strict_pref(c2, c3) and not self.strict_pref(c1, c3):
182
+ #print(f"Strict preference {c1} > {c2} and {c2} > {c3} but not {c1} > {c3}")
183
+ return False
184
+ return True
185
+
186
+ def to_graph(self, curr_cands=None):
187
+ """Return the majority graph of the pairwise comparisons restricted to the candidates in curr_cands."""
188
+ if curr_cands is None:
189
+ curr_cands = self.candidates
190
+ edges = []
191
+ for c1 in curr_cands:
192
+ for c2 in curr_cands:
193
+ if c1 == c2:
194
+ continue
195
+ if self.has_comparison(c1, c2) and self.strict_pref(c1, c2):
196
+ edges.append((c1, c2))
197
+
198
+ return nx.DiGraph(edges)
199
+
200
+ def has_tie(self):
201
+ """Returns True if there is a tie in the pairwise comparisons."""
202
+ for c1 in self.candidates:
203
+ for c2 in self.candidates:
204
+ if c1 != c2 and self.indiff(c1, c2):
205
+ return True
206
+ return False
207
+
208
+ def is_coherent(self):
209
+ """Return True if the comparisons are coherent: If a candidate is compared to another candidate, then that candidate must be compared to all canidates"""
210
+
211
+ for c in self.candidates:
212
+ for menu, _ in self._comparisons:
213
+ if c in menu:
214
+ for c1 in self.candidates:
215
+ if c != c1 and not self.has_comparison(c, c1):
216
+ return False
217
+ return True
218
+
219
+
220
+ def cycles(self, curr_cands = None):
221
+ """Returns the cycles in the pairwise comparisons.
222
+
223
+ This uses the networkx method ``networkx.find_cycle`` to find the cycles in ``self.mg``.
224
+
225
+ """
226
+
227
+ comparison_graph = self.to_graph(curr_cands)
228
+
229
+ return list(nx.simple_cycles(comparison_graph))
230
+
231
+
232
+ def has_cycle(self, curr_cands = None):
233
+ """Returns True if there is a cycle in the comparison graph."""
234
+
235
+ return len(self.cycles(curr_cands=curr_cands)) != 0
236
+
237
+ def to_ranking(self):
238
+ """Return the comparison as a ranking (return an error if comparisons are not transitive)"""
239
+
240
+ assert self.is_transitive(self.candidates), "The comparisons must be transitive to convert to a ranking"
241
+ assert self.is_coherent(), "The comparisons must be coherent to convert to a ranking"
242
+
243
+ c1, c2 = self.candidates[0], self.candidates[1]
244
+ ranking = {}
245
+ if self.strict_pref(c1, c2):
246
+ ranking[c1] = 1
247
+ ranking[c2] = 2
248
+ elif self.strict_pref(c2, c1):
249
+ ranking[c2] = 1
250
+ ranking[c1] = 2
251
+ elif self.indiff(c1, c2):
252
+ ranking[c2] = 1
253
+ ranking[c1] = 1
254
+
255
+ for c in self.candidates:
256
+ prev_rank = 0
257
+ if c not in ranking.keys():
258
+ ranked_last = True
259
+ for c2, r in sorted(ranking.items(), key=lambda r: r[1]):
260
+ if self.strict_pref(c, c2):
261
+ ranking[c] = (prev_rank + r) / 2
262
+ ranked_last = False
263
+ break
264
+ elif self.strict_pref(c2, c):
265
+ prev_rank = r
266
+ elif self.indiff(c, c2):
267
+ ranking[c] = r
268
+ ranked_last = False
269
+ break
270
+ if ranked_last:
271
+ ranking[c] = prev_rank + 1
272
+
273
+ r = Ranking(ranking)
274
+ r.normalize_ranks()
275
+ return r
276
+
277
+ def display(self):
278
+ """Display the pairwise comparisons in a readable format."""
279
+ for menu, choice in self._comparisons:
280
+ menu_str = ", ".join(sorted([self.cmap[c] for c in menu]))
281
+ choice_str = ", ".join(sorted([self.cmap[c] for c in choice]))
282
+ print(f"{{{menu_str}}} -> {{{choice_str}}}")
283
+
284
+
285
+ def __str__(self):
286
+ """Return the comparisons as a string."""
287
+
288
+ str_comparisons = ''
289
+ for menu, choice in self._comparisons:
290
+ menu_str = ", ".join(sorted([self.cmap[c] for c in menu]))
291
+ choice_str = ", ".join(sorted([self.cmap[c] for c in choice]))
292
+ str_comparisons += f"{{{menu_str}}} -> {{{choice_str}}}, "
293
+ return str_comparisons[:-2]
294
+
295
+
296
+ class PairwiseProfile:
297
+ r"""An anonymous profile of pairwise comparisons.
298
+
299
+ Arguments:
300
+ pairwise_comparisons: List of comparisons or PairwiseBallot instances.
301
+ """
302
+
303
+ def __init__(self, pairwise_comparisons, candidates=None, rcounts=None, cmap=None):
304
+ """Constructor method for PairwiseProfile.
305
+
306
+ Args:
307
+ pairwise_comparisons (list): List of lists of pairwise comparisons, or list of PairwiseBallot instances.
308
+ candidates (list or set, optional): List of candidates. Defaults to None.
309
+ rcounts (list, optional): List of counts for each comparison. Defaults to None.
310
+ cmap (dict, optional): Mapping of candidates to their names. Defaults to None.
311
+ """
312
+ self._pairwise_comparisons = []
313
+
314
+ for comps in pairwise_comparisons:
315
+ if isinstance(comps, PairwiseBallot):
316
+ self._pairwise_comparisons.append(comps)
317
+ else:
318
+ self._pairwise_comparisons.append(PairwiseBallot(comps, candidates=candidates))
319
+
320
+ if candidates is None:
321
+ candidates = {c for pc in self._pairwise_comparisons for c in pc.candidates}
322
+ self.candidates = sorted(list(candidates))
323
+
324
+ self.cand_to_cidx = {c: idx for idx, c in enumerate(self.candidates)}
325
+ self.cidx_to_cand = {idx: c for c, idx in self.cand_to_cidx.items()}
326
+
327
+ self._rcounts = rcounts if rcounts is not None else [1] * len(pairwise_comparisons)
328
+
329
+ self._tally = np.array([[np.sum([count for pc, count in zip(self._pairwise_comparisons, self._rcounts) if pc.strict_pref(c1, c2)]) for c2 in self.candidates] for c1 in self.candidates])
330
+
331
+ self.cmap = cmap if cmap is not None else {c: str(c) for c in self.candidates}
332
+
333
+ self.num_voters = np.sum(self._rcounts)
334
+ """The number of voters in the election."""
335
+
336
+ @property
337
+ def comparisons_counts(self):
338
+ """Returns the submitted rankings and the list of counts."""
339
+ return self._pairwise_comparisons, self._rcounts
340
+
341
+ @property
342
+ def pairwise_comparisons(self):
343
+ """Returns a list of all pairwise comparisons"""
344
+
345
+ return [comp for compidx,comp in enumerate(self._pairwise_comparisons)
346
+ for _ in range(self._rcounts[compidx])]
347
+
348
+ def support(self, c1, c2):
349
+ """The number of voters that rank `c1` above `c2`.
350
+
351
+ Args:
352
+ c1 (str or int): The first candidate.
353
+ c2 (str or int): The second candidate.
354
+
355
+ Returns:
356
+ int: Number of voters that rank `c1` above `c2`.
357
+ """
358
+ return self._tally[self.cand_to_cidx[c1]][self.cand_to_cidx[c2]]
359
+
360
+ def margin(self, c1, c2):
361
+ """The number of voters that rank `c1` above `c2` minus the number of voters that rank `c2` above `c1`.
362
+
363
+ Args:
364
+ c1 (str or int): The first candidate.
365
+ c2 (str or int): The second candidate.
366
+
367
+ Returns:
368
+ int: Margin of votes.
369
+ """
370
+ idx1, idx2 = self.cand_to_cidx[c1], self.cand_to_cidx[c2]
371
+ return self._tally[idx1][idx2] - self._tally[idx2][idx1]
372
+
373
+ def majority_prefers(self, c1, c2):
374
+ """Returns true if more voters rank `c1` over `c2` than `c2` over `c1`.
375
+
376
+ Args:
377
+ c1 (str or int): The first candidate.
378
+ c2 (str or int): The second candidate.
379
+
380
+ Returns:
381
+ bool: True if `c1` is majority preferred over `c2`, False otherwise.
382
+ """
383
+ return self.margin(c1, c2) > 0
384
+
385
+ def is_tied(self, c1, c2):
386
+ """Returns True if `c1` is tied with `c2`.
387
+
388
+ Args:
389
+ c1 (str or int): The first candidate.
390
+ c2 (str or int): The second candidate.
391
+
392
+ Returns:
393
+ bool: True if `c1` is tied with `c2`, False otherwise.
394
+ """
395
+ return self.margin(c1, c2) == 0
396
+
397
+ def dominators(self, cand, curr_cands=None):
398
+ """Returns the list of candidates that are majority preferred to `cand` in the profile restricted to the candidates in `curr_cands`.
399
+
400
+ Args:
401
+ cand (str or int): The candidate.
402
+ curr_cands (list, optional): List of candidates to consider. Defaults to None.
403
+
404
+ Returns:
405
+ list: List of candidates that are majority preferred to `cand`.
406
+ """
407
+ candidates = self.candidates if curr_cands is None else curr_cands
408
+ return [c for c in candidates if self.majority_prefers(c, cand)]
409
+
410
+ def dominates(self, cand, curr_cands=None):
411
+ """Returns the list of candidates that `cand` is majority preferred to in the profile restricted to `curr_cands`.
412
+
413
+ Args:
414
+ cand (str or int): The candidate.
415
+ curr_cands (list, optional): List of candidates to consider. Defaults to None.
416
+
417
+ Returns:
418
+ list: List of candidates that `cand` is majority preferred to.
419
+ """
420
+ candidates = self.candidates if curr_cands is None else curr_cands
421
+ return [c for c in candidates if self.majority_prefers(cand, c)]
422
+
423
+ def copeland_scores(self, curr_cands=None, scores=(1, 0, -1)):
424
+ """The Copeland scores in the profile restricted to the candidates in `curr_cands`.
425
+
426
+ Args:
427
+ curr_cands (list, optional): List of candidates to consider. Defaults to None.
428
+ scores (tuple, optional): Scores for win, tie, and loss. Defaults to (1, 0, -1).
429
+
430
+ Returns:
431
+ dict: Dictionary associating each candidate in `curr_cands` with its Copeland score.
432
+ """
433
+ wscore, tscore, lscore = scores
434
+ candidates = self.candidates if curr_cands is None else curr_cands
435
+ c_scores = {c: 0.0 for c in candidates}
436
+ for c1 in candidates:
437
+ for c2 in candidates:
438
+ if self.majority_prefers(c1, c2):
439
+ c_scores[c1] += wscore
440
+ elif self.majority_prefers(c2, c1):
441
+ c_scores[c1] += lscore
442
+ elif c1 != c2:
443
+ c_scores[c1] += tscore
444
+ return c_scores
445
+
446
+ def condorcet_winner(self, curr_cands=None):
447
+ """Returns the Condorcet winner in the profile restricted to `curr_cands` if one exists, otherwise return None.
448
+
449
+ Args:
450
+ curr_cands (list, optional): List of candidates to consider. Defaults to None.
451
+
452
+ Returns:
453
+ str or int: Condorcet winner if one exists, otherwise None.
454
+ """
455
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
456
+ for c1 in curr_cands:
457
+ if all(self.majority_prefers(c1, c2) for c2 in curr_cands if c1 != c2):
458
+ return c1
459
+ return None
460
+
461
+ def weak_condorcet_winner(self, curr_cands=None):
462
+ """Returns a list of the weak Condorcet winners in the profile restricted to `curr_cands`.
463
+
464
+ Args:
465
+ curr_cands (list, optional): List of candidates to consider. Defaults to None.
466
+
467
+ Returns:
468
+ list: List of weak Condorcet winners.
469
+ """
470
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
471
+ return [c1 for c1 in curr_cands if not any(self.majority_prefers(c2, c1) for c2 in curr_cands if c1 != c2)]
472
+
473
+ def condorcet_loser(self, curr_cands=None):
474
+ """Returns the Condorcet loser in the profile restricted to `curr_cands` if one exists, otherwise return None.
475
+
476
+ Args:
477
+ curr_cands (list, optional): List of candidates to consider. Defaults to None.
478
+
479
+ Returns:
480
+ str or int: Condorcet loser if one exists, otherwise None.
481
+ """
482
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
483
+ for c1 in curr_cands:
484
+ if all(self.majority_prefers(c2, c1) for c2 in curr_cands if c1 != c2):
485
+ return c1
486
+ return None
487
+
488
+ def strict_maj_size(self):
489
+ """Returns the strict majority of the number of voters.
490
+
491
+ Returns:
492
+ int: Size of the strict majority.
493
+ """
494
+ return int(self.num_voters / 2 + 1) if self.num_voters % 2 == 0 else int(ceil(float(self.num_voters) / 2))
495
+
496
+ def margin_graph(self):
497
+ """Returns the margin graph of the profile.
498
+
499
+ Returns:
500
+ dict: Margin graph of the profile.
501
+ """
502
+
503
+ return MarginGraph(self.candidates,
504
+ [(c1, c2, self.margin(c1, c2))
505
+ for c1 in self.candidates
506
+ for c2 in self.candidates
507
+ if self.majority_prefers(c1, c2)])
508
+
509
+ def majority_graph(self):
510
+ """Returns the margin graph of the profile.
511
+
512
+ Returns:
513
+ dict: Margin graph of the profile.
514
+ """
515
+
516
+ return MajorityGraph(self.candidates,
517
+ [(c1, c2)
518
+ for c1 in self.candidates
519
+ for c2 in self.candidates
520
+ if self.majority_prefers(c1, c2)])
521
+
522
+ def display(self, cmap=None, style="pretty", curr_cands=None):
523
+ """Display the profile (restricted to `curr_cands`) as an ASCII table.
524
+
525
+ Args:
526
+ cmap (dict, optional): Mapping of candidates to their names. Defaults to None.
527
+ style (str, optional): Style of the display. Defaults to "pretty".
528
+ curr_cands (list, optional): List of candidates to consider. Defaults to None.
529
+ """
530
+ cmap = cmap if cmap is not None else self.cmap
531
+ comparisons, counts = self.comparisons_counts
532
+ for comp_idx, comps in enumerate(comparisons):
533
+ print(f'{counts[comp_idx]}: {comps}')
534
+
535
+ def __add__(self, other_prof):
536
+ """Returns the sum of two profiles.
537
+
538
+ Args:
539
+ other_prof (PairwiseProfile): Another PairwiseProfile instance.
540
+
541
+ Returns:
542
+ PairwiseProfile: The combined profile.
543
+ """
544
+ assert self.candidates == other_prof.candidates, "The two profiles must have the same candidates"
545
+ combined_comparisons = self._pairwise_comparisons + other_prof._pairwise_comparisons
546
+ combined_rcounts = self._rcounts + other_prof._rcounts
547
+ return PairwiseProfile(combined_comparisons, rcounts=combined_rcounts, candidates=self.candidates)
@@ -0,0 +1,105 @@
1
+ '''
2
+ File: prob_voting_method.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: April 14, 2024
5
+
6
+ The ProbabilisticVotingMethod class and helper functions for probabilistic voting methods.
7
+ '''
8
+
9
+ import functools
10
+ import numpy as np
11
+ import inspect
12
+
13
+ class ProbVotingMethod(object):
14
+ """
15
+ A class to add functionality to probabilistic voting methods
16
+
17
+ Args:
18
+ pvm (function): An implementation of a probabilistic voting method. The function should accept any type of profile, and a keyword parameter ``curr_cands`` to find the winner after restricting to ``curr_cands``.
19
+ name (string): The human-readable name of the social welfare function.
20
+
21
+ Returns:
22
+ A dictionary that represents the probability on the set of candidates.
23
+
24
+ """
25
+ def __init__(self, pvm, name = None):
26
+
27
+ self.pvm = pvm
28
+ self.name = name
29
+ self.algorithm = None
30
+
31
+ functools.update_wrapper(self, pvm)
32
+
33
+ def __call__(self, edata, curr_cands = None, **kwargs):
34
+
35
+ if (curr_cands is not None and len(curr_cands) == 0) or len(edata.candidates) == 0:
36
+ return {}
37
+ return self.pvm(edata, curr_cands = curr_cands, **kwargs)
38
+
39
+ def support(self, edata, curr_cands = None, **kwargs):
40
+ """
41
+ Return the sorted list of the set of candidates that have non-zero probability.
42
+ """
43
+
44
+ if (curr_cands is not None and len(curr_cands) == 0) or len(edata.candidates) == 0:
45
+ return []
46
+ prob = self.pvm(edata, curr_cands = curr_cands, **kwargs)
47
+ return sorted([c for c,pr in prob.items() if pr > 0])
48
+
49
+ def choose(self, edata, curr_cands = None, **kwargs):
50
+ """
51
+ Return a randomly chosen element according to the probability.
52
+ """
53
+
54
+ prob = self.pvm(edata, curr_cands = curr_cands, **kwargs)
55
+
56
+ # choose a candidate according to the probability distribution prob
57
+ cands = list(prob.keys())
58
+ probs = [prob[c] for c in cands]
59
+
60
+ return np.random.choice(cands, p = probs)
61
+
62
+ def display(self, edata, curr_cands = None, cmap = None, **kwargs):
63
+ """
64
+ Display the winning set of candidates.
65
+ """
66
+
67
+ cmap = cmap if cmap is not None else edata.cmap
68
+
69
+ prob = self.__call__(edata, curr_cands = curr_cands, **kwargs)
70
+
71
+ if prob is None: # some voting methods may return None if, for instance, it is taking long to compute the winner.
72
+ print(f"{self.name} probability is not available")
73
+ else:
74
+ w_str = f"{self.name} probability is "
75
+ print(w_str + "{" + ", ".join([f"{str(cmap[c])}: {round(pr,3)}" for c,pr in prob.items()]) + "}")
76
+
77
+
78
+ def set_name(self, new_name):
79
+ """Set the name of the social welfare function."""
80
+
81
+ self.name = new_name
82
+
83
+ def set_algorithm(self, algorithm):
84
+ """
85
+ Set the algorithm for the voting method if 'algorithm' is an accepted keyword parameter.
86
+
87
+ Args:
88
+ algorithm: The algorithm to set for the voting method.
89
+ """
90
+ params = inspect.signature(self.pvm).parameters
91
+ if 'algorithm' in params and params['algorithm'].kind in [inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD]:
92
+ self.algorithm = algorithm
93
+ else:
94
+ raise ValueError(f"The method {self.name} does not accept 'algorithm' as a parameter.")
95
+
96
+ def __str__(self):
97
+ return f"{self.name}"
98
+
99
+ def pvm(name = None):
100
+ """
101
+ A decorator used when creating a social welfare function.
102
+ """
103
+ def wrapper(f):
104
+ return ProbVotingMethod(f, name=name)
105
+ return wrapper