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,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}')