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,287 @@
|
|
1
|
+
'''
|
2
|
+
File: voting_methods.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: Nove 21, 2024
|
5
|
+
|
6
|
+
Implementations of probabilistic voting methods.
|
7
|
+
'''
|
8
|
+
|
9
|
+
from pref_voting.prob_voting_method import *
|
10
|
+
from pref_voting.weighted_majority_graphs import MajorityGraph, MarginGraph
|
11
|
+
from scipy.optimize import linprog
|
12
|
+
|
13
|
+
import random
|
14
|
+
import nashpy as nash
|
15
|
+
|
16
|
+
@pvm(name="Random Dictator")
|
17
|
+
def random_dictator(profile, curr_cands = None):
|
18
|
+
'''Returns lottery over the candidates that is proportional to the Plurality scores.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
profile (Profile): A Profile object.
|
22
|
+
curr_cands (list): A list of candidates to restrict the ranking to. If ``None``, then the ranking is over the entire domain of the profile.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
dict: A dictionary mapping candidates to probabilities.
|
26
|
+
'''
|
27
|
+
|
28
|
+
plurality_scores = profile.plurality_scores(curr_cands = curr_cands)
|
29
|
+
total_plurality_scores = sum(list(plurality_scores.values()))
|
30
|
+
|
31
|
+
return {c: plurality_scores[c] / total_plurality_scores for c in plurality_scores.keys()}
|
32
|
+
|
33
|
+
@pvm(name="Random Dictator on the Beta-Uncovered Set")
|
34
|
+
def RaDiUS(profile, curr_cands = None, beta = 0.5):
|
35
|
+
"""
|
36
|
+
Runs the Random Dictator method on the profile restricted to the beta-uncovered set, as proposed by Charikar et al. (https://arxiv.org/abs/2306.17838).
|
37
|
+
|
38
|
+
Args:
|
39
|
+
profile (Profile): An anonymous profile of linear orders
|
40
|
+
curr_cands (List[int], optional): Candidates to consider. Defaults to all candidates if not provided.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
dict: Maps each candidate to their probability of winning under the RaDiUS method.
|
44
|
+
|
45
|
+
"""
|
46
|
+
from pref_voting.margin_based_methods import beta_uncovered_set
|
47
|
+
|
48
|
+
curr_cands = profile.candidates if curr_cands is None else curr_cands
|
49
|
+
|
50
|
+
rd_dist = random_dictator(profile, curr_cands = beta_uncovered_set(profile, curr_cands = curr_cands, beta = beta))
|
51
|
+
|
52
|
+
rd_dist.update({c:0 for c in curr_cands if c not in rd_dist.keys()})
|
53
|
+
|
54
|
+
return rd_dist
|
55
|
+
|
56
|
+
@pvm(name="Proportional Borda")
|
57
|
+
def pr_borda(profile, curr_cands=None):
|
58
|
+
'''Returns lottery over the candidates that is proportional to the Borda scores.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
profile (Profile): A Profile object.
|
62
|
+
curr_cands (list): A list of candidates to restrict the ranking to. If ``None``, then the ranking is over the entire domain of the profile.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
dict: A dictionary mapping candidates to probabilities.
|
66
|
+
|
67
|
+
'''
|
68
|
+
borda_scores = profile.borda_scores(curr_cands=curr_cands)
|
69
|
+
total_borda_scores = sum(list(borda_scores.values()))
|
70
|
+
|
71
|
+
return {c: borda_scores[c] / total_borda_scores for c in borda_scores.keys()}
|
72
|
+
|
73
|
+
def clean_and_normalize(probs, threshold=1e-10):
|
74
|
+
# Set negative or small positive values to zero
|
75
|
+
probs = np.where(probs < threshold, 0, probs)
|
76
|
+
|
77
|
+
# Renormalize to ensure the probabilities sum to 1
|
78
|
+
total = np.sum(probs)
|
79
|
+
if total > 0:
|
80
|
+
probs /= total
|
81
|
+
return probs
|
82
|
+
|
83
|
+
def _maximal_lottery_enumeration(edata, curr_cands=None, margin_transformation=lambda x: x):
|
84
|
+
'''
|
85
|
+
Implementation of maximal lotteries. See http://dss.in.tum.de/files/brandt-research/fishburn_slides.pdf
|
86
|
+
|
87
|
+
Returns a randomly chosen maximal lottery.
|
88
|
+
'''
|
89
|
+
|
90
|
+
candidates = edata.candidates if curr_cands is None else curr_cands
|
91
|
+
m_matrix, cand_to_cidx = edata.strength_matrix(curr_cands=candidates)
|
92
|
+
|
93
|
+
A = np.array([[margin_transformation(m) for m in row] for row in m_matrix])
|
94
|
+
|
95
|
+
# Create the game
|
96
|
+
game = nash.Game(A)
|
97
|
+
equilibria = []
|
98
|
+
try:
|
99
|
+
equilibria = list(game.vertex_enumeration())
|
100
|
+
#print("Vertex Enumeration found equilibria.")
|
101
|
+
except Exception as e:
|
102
|
+
print(f"Vertex Enumeration failed: {e}")
|
103
|
+
|
104
|
+
# Backup method 1: Support Enumeration
|
105
|
+
if not equilibria:
|
106
|
+
try:
|
107
|
+
equilibria = list(game.support_enumeration())
|
108
|
+
#print("Support Enumeration found equilibria.")
|
109
|
+
except Exception as e:
|
110
|
+
print(f"Support Enumeration failed: {e}")
|
111
|
+
|
112
|
+
if len(equilibria) == 0:
|
113
|
+
return {c: 1 / len(candidates) for c in candidates}
|
114
|
+
else:
|
115
|
+
# average the equilibria component-wise to get a single equilibrium
|
116
|
+
eq_probs = [np.mean([eq[0][idx] for eq in equilibria])
|
117
|
+
for idx in range(len(candidates))]
|
118
|
+
|
119
|
+
eq_probs = clean_and_normalize(np.array(eq_probs))
|
120
|
+
|
121
|
+
# Return the result as a dictionary
|
122
|
+
return {c: eq_probs[cand_to_cidx(c)] for c in candidates}
|
123
|
+
|
124
|
+
|
125
|
+
def _maximal_lottery_lp(edata, curr_cands=None, margin_transformation=lambda x: x):
|
126
|
+
'''
|
127
|
+
Implementation of maximal lotteries using linear programming.
|
128
|
+
'''
|
129
|
+
candidates = edata.candidates if curr_cands is None else curr_cands
|
130
|
+
m_matrix, cand_to_cidx = edata.strength_matrix(curr_cands=candidates)
|
131
|
+
|
132
|
+
A = np.array([[margin_transformation(m) for m in row] for row in m_matrix])
|
133
|
+
|
134
|
+
num_cands = len(candidates)
|
135
|
+
c = np.zeros(num_cands + 1)
|
136
|
+
c[-1] = -1 # Coefficient for v in the objective function (maximize v)
|
137
|
+
|
138
|
+
# Inequalities: A^T p - v * 1 >= 0 => A^T p - v * 1 - s = 0 (s >= 0)
|
139
|
+
# We need to convert this to the form: A_ub x <= b_ub
|
140
|
+
|
141
|
+
A_ub = np.hstack([-A.T, np.ones((num_cands, 1))])
|
142
|
+
b_ub = np.zeros(num_cands)
|
143
|
+
|
144
|
+
# Equalities: sum p_i = 1
|
145
|
+
A_eq = np.zeros((1, num_cands + 1))
|
146
|
+
A_eq[0, :num_cands] = 1
|
147
|
+
b_eq = np.array([1])
|
148
|
+
|
149
|
+
bounds = [(0, None)] * num_cands + [(None, None)] # p_i >= 0, v free
|
150
|
+
|
151
|
+
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method='highs')
|
152
|
+
|
153
|
+
if res.success:
|
154
|
+
eq_probs = res.x[:num_cands]
|
155
|
+
# Normalize to account for numerical errors
|
156
|
+
eq_probs = np.maximum(eq_probs, 0)
|
157
|
+
eq_probs /= np.sum(eq_probs)
|
158
|
+
return {c: eq_probs[cand_to_cidx(c)] for c in candidates}
|
159
|
+
else:
|
160
|
+
# If LP fails, default to uniform distribution
|
161
|
+
return {c: 1 / len(candidates) for c in candidates}
|
162
|
+
|
163
|
+
|
164
|
+
@pvm(name="C1 Maximal Lottery")
|
165
|
+
def c1_maximal_lottery(edata, curr_cands=None, algorithm='enumeration'):
|
166
|
+
|
167
|
+
'''Returns the C1 maximal lottery over the candidates. See http://dss.in.tum.de/files/brandt-research/fishburn_slides.pdf.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
edata (Profile, MarginGraph): A Profile object.
|
171
|
+
curr_cands (list): A list of candidates to restrict the ranking to. If ``None``, then the ranking is over the entire domain of the profile.
|
172
|
+
algorithm (str): The algorithm to use. Either 'enumeration' or 'lp'. Defaults to 'enumeration'.
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
dict: A dictionary mapping candidates to probabilities.
|
176
|
+
|
177
|
+
.. note::
|
178
|
+
The 'enumeration' algorithm averages over the extremal maximal lotteries. The 'lp' is faster, but only returns a single maximal lottery (not necessarily the average)
|
179
|
+
|
180
|
+
'''
|
181
|
+
|
182
|
+
if type(edata) == MajorityGraph:
|
183
|
+
# if edata is a MajorityGraph, we need to add margins for the following code to work. The margins do not matter when finding the c1 maximal lottery.
|
184
|
+
|
185
|
+
candidates = edata.candidates if curr_cands is None else curr_cands
|
186
|
+
|
187
|
+
edata = MarginGraph(candidates, [(c1, c2, 1) for c1, c2 in edata.edges if (c1 in candidates and c2 in candidates)])
|
188
|
+
|
189
|
+
if algorithm == 'enumeration':
|
190
|
+
return _maximal_lottery_enumeration(edata, curr_cands=curr_cands, margin_transformation = np.sign)
|
191
|
+
|
192
|
+
elif algorithm == 'lp':
|
193
|
+
return _maximal_lottery_lp(edata, curr_cands=curr_cands, margin_transformation = np.sign)
|
194
|
+
else:
|
195
|
+
raise ValueError(f"Unknown algorithm: {algorithm}")
|
196
|
+
|
197
|
+
@pvm(name="Maximal Lottery")
|
198
|
+
def maximal_lottery(edata, curr_cands=None, algorithm='lp'):
|
199
|
+
'''Returns the maximal lottery over the candidates. See http://dss.in.tum.de/files/brandt-research/fishburn_slides.pdf.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
edata (Profile, MarginGraph): A Profile object.
|
203
|
+
curr_cands (list): A list of candidates to restrict the ranking to. If ``None``, then the ranking is over the entire domain of the profile.
|
204
|
+
algorithm (str): The algorithm to use. Either 'enumeration' or 'lp'. Defaults to 'enumeration'.
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
dict: A dictionary mapping candidates to probabilities.
|
208
|
+
|
209
|
+
.. note::
|
210
|
+
The 'enumeration' algorithm averages over the extremal maximal lotteries. The 'lp' is faster, but only returns a single maximal lottery (not necessarily the average)
|
211
|
+
|
212
|
+
|
213
|
+
'''
|
214
|
+
|
215
|
+
if algorithm == 'enumeration':
|
216
|
+
return _maximal_lottery_enumeration(edata, curr_cands=curr_cands, margin_transformation = lambda x: x)
|
217
|
+
|
218
|
+
elif algorithm == 'lp':
|
219
|
+
return _maximal_lottery_lp(edata, curr_cands=curr_cands, margin_transformation = lambda x: x)
|
220
|
+
else:
|
221
|
+
raise ValueError(f"Unknown algorithm: {algorithm}")
|
222
|
+
|
223
|
+
|
224
|
+
@pvm(name="Random Consensus Builder")
|
225
|
+
def random_consensus_builder(profile, curr_cands=None, beta=0.5):
|
226
|
+
"""Random Consensus Builder (RCB) voting method due to Charikar et al. (https://arxiv.org/abs/2306.17838).
|
227
|
+
|
228
|
+
For each ranking type in the profile, runs the deterministic Consensus Builder voting method using that ranking
|
229
|
+
as the consensus building ranking. The probability of a candidate winning is proportional to the
|
230
|
+
number of voters with rankings that would make that candidate win when used as the consensus
|
231
|
+
building ranking.
|
232
|
+
|
233
|
+
Args:
|
234
|
+
profile (Profile): An anonymous profile of linear orders
|
235
|
+
curr_cands (List[int], optional): Candidates to consider. Defaults to all candidates if not provided.
|
236
|
+
beta (float): Threshold for elimination (default 0.5). When processing candidate i, eliminates a candidate j
|
237
|
+
above i in the consensus building ranking if the proportion of voters preferring i to j is >= beta
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
dict: Maps each candidate to their probability of winning under the RCB method
|
241
|
+
|
242
|
+
.. seealso::
|
243
|
+
:meth:`pref_voting.iterative_methods.consensus_builder`
|
244
|
+
:meth:`pref_voting.stochastic_methods.random_consensus_builder_st`
|
245
|
+
"""
|
246
|
+
from pref_voting.iterative_methods import consensus_builder
|
247
|
+
|
248
|
+
if curr_cands is None:
|
249
|
+
curr_cands = profile.candidates
|
250
|
+
|
251
|
+
# Count how many times each ranking type produces each winner
|
252
|
+
winner_counts = {c: 0 for c in curr_cands}
|
253
|
+
|
254
|
+
# Process each unique ranking type
|
255
|
+
for ranking_type in profile.ranking_types:
|
256
|
+
# Count number of voters with this ranking type
|
257
|
+
num_rankings_with_type = len([r for r in profile.rankings if r == ranking_type])
|
258
|
+
winner = consensus_builder(profile, curr_cands=curr_cands,consensus_building_ranking=ranking_type, beta=beta)[0]
|
259
|
+
winner_counts[winner] += num_rankings_with_type
|
260
|
+
total_count += num_rankings_with_type
|
261
|
+
|
262
|
+
# Convert counts to probabilities
|
263
|
+
return {c: count/profile.num_voters for c, count in winner_counts.items()}
|
264
|
+
|
265
|
+
|
266
|
+
def create_probabilistic_method(vm):
|
267
|
+
"""
|
268
|
+
Create a probabilistic voting method from a voting method.
|
269
|
+
"""
|
270
|
+
|
271
|
+
from pref_voting.voting_method import VotingMethod
|
272
|
+
if type(vm) != VotingMethod:
|
273
|
+
raise TypeError("vm must be a VotingMethod object")
|
274
|
+
|
275
|
+
def _pvm(profile, curr_cands=None, **kwargs):
|
276
|
+
return vm.prob(profile, curr_cands=curr_cands, **kwargs)
|
277
|
+
|
278
|
+
return ProbVotingMethod(_pvm, name=f'{vm.name} with Even Chance Tiebreaking')
|
279
|
+
|
280
|
+
def mixture(pvm1, pvm2, alpha):
|
281
|
+
"""
|
282
|
+
Mixture of the two probabilistic voting methods pvm1 and pvm2 with mixing parameter alpha. With probability alpha, the output is the output of pvm1, and with probability 1-alpha, the output is the output of pvm2.
|
283
|
+
"""
|
284
|
+
def _mixture(profile, curr_cands=None, **kwargs):
|
285
|
+
return {c: alpha * pvm1(profile, curr_cands=curr_cands, **kwargs)[c] + (1-alpha) * pvm2(profile, curr_cands=curr_cands, **kwargs)[c] for c in profile.candidates}
|
286
|
+
|
287
|
+
return ProbVotingMethod(_mixture, name=f'Mixture of {pvm1.name} and {pvm2.name} with alpha={alpha}')
|