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
@@ -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
|