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,448 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import matplotlib.patheffects as path_effects
4
+ import seaborn as sns
5
+ from pref_voting.utility_functions import *
6
+ from pref_voting.utility_profiles import UtilityProfile
7
+
8
+ class SpatialProfile(object):
9
+ """
10
+ A spatial profile is a set of candidates and voters in a multi-dimensional space. Each voter and candidate is assigned vector of floats representing their position on each issue.
11
+
12
+ Args:
13
+ cand_pos (dict): A dictionary mapping each candidate to their position in the space.
14
+ voter_pos (dict): A dictionary mapping each voter to their position in the space.
15
+
16
+ Attributes:
17
+ candidates (list): A list of candidates.
18
+ voters (list): A list of voters.
19
+ cand_pos (dict): A dictionary mapping each candidate to their position in the space.
20
+ voter_pos (dict): A dictionary mapping each voter to their position in the space.
21
+ num_dims (int): The number of dimensions in the space.
22
+ cand_types (dict): A dictionary mapping each candidate to their type (e.g., party affiliation).
23
+
24
+ """
25
+ def __init__(self, cand_pos, voter_pos, candidate_types=None):
26
+
27
+ cand_dims = [len(v) for v in cand_pos.values()]
28
+ voter_dims = [len(v) for v in voter_pos.values()]
29
+
30
+ assert len(cand_dims) > 0, "There must be at least one candidate."
31
+ assert len(set(cand_dims)) == 1, "All candidate positions must have the same number of dimensions."
32
+ assert len(voter_dims) > 0, "There must be at least one voter."
33
+ assert len(set(voter_dims)) == 1, "All voter positions must have the same number of dimensions."
34
+ assert cand_dims[0] == voter_dims[0], "Candidate and voter positions must have the same number of dimensions."
35
+
36
+ self.candidates = sorted(list(cand_pos.keys()))
37
+ self.voters = sorted(list(voter_pos.keys()))
38
+ self.cand_pos = cand_pos
39
+ self.voter_pos = voter_pos
40
+ self.num_dims = len(list(cand_pos.values())[0])
41
+ self.candidate_types = candidate_types or {c:'unknown' for c in self.candidates}
42
+
43
+ @property
44
+ def num_cands(self):
45
+ """
46
+ Returns the number of candidates in the profile.
47
+ """
48
+ return len(self.candidates)
49
+
50
+ @property
51
+ def num_voters(self):
52
+ """
53
+ Returns the number of voters in the profile.
54
+ """
55
+ return len(self.voters)
56
+
57
+ def voter_position(self, v):
58
+ """
59
+ Given a voter v, returns their position in the space.
60
+ """
61
+ return self.voter_pos[v]
62
+
63
+ def candidate_position(self, c):
64
+ """
65
+ Given a candidate c, returns their position in the space.
66
+ """
67
+ return self.cand_pos[c]
68
+
69
+ def candidate_type(self, c):
70
+ """
71
+ Given a candidate c, returns their type.
72
+ """
73
+ return self.candidate_types[c]
74
+
75
+ def set_candidate_types(self, cand_types):
76
+ """
77
+ Sets the types of each candidate.
78
+ """
79
+
80
+ assert set(cand_types.keys()) == set(self.candidates), "The candidate types must be specified for all candidates."
81
+
82
+ self.candidate_types = cand_types
83
+
84
+ def to_utility_profile(self,
85
+ utility_function = None,
86
+ uncertainty_function=None,
87
+ batch=False,
88
+ return_virtual_cand_positions=False):
89
+ """
90
+ Returns a utility profile corresponding to the spatial profile.
91
+
92
+ Args:
93
+ utility_function (callable, optional): A function that takes two vectors and returns a float. The default utility function is the quadratic utility function.
94
+ uncertainty_function (callable, optional): A function that models uncertainty and returns covariance parameters.
95
+ batch (bool, optional): If True, generate positions in batches. Default is False.
96
+ return_virtual_cand_positions (bool, optional): If True, return virtual candidate positions. Default is False.
97
+
98
+ Returns:
99
+ UtilityProfile: A utility profile corresponding to the spatial profile.
100
+ (optional) Tuple[UtilityProfile, dict]: The utility profile and virtual candidate positions if `return_virtual_cand_positions` is True.
101
+ """
102
+ import numpy as np
103
+ from pref_voting.generate_spatial_profiles import generate_covariance
104
+
105
+ utility_function = utility_function or quadratic_utility
106
+
107
+ if uncertainty_function is not None:
108
+ virtual_cand_positions = {}
109
+ for c in self.candidates:
110
+ if batch:
111
+ covariance = generate_covariance(self.num_dims, *uncertainty_function(self, c, self.voters[0]))
112
+ positions = np.random.multivariate_normal(self.candidate_position(c), covariance, size=len(self.voters))
113
+ else:
114
+ positions = [np.random.multivariate_normal(self.candidate_position(c), generate_covariance(self.num_dims, *uncertainty_function(self, c, v))) for v in self.voters]
115
+ virtual_cand_positions[c] = positions
116
+
117
+ utility_profile = [
118
+ {c: utility_function(self.voter_position(v), virtual_cand_positions[c][vidx]) for c in self.candidates}
119
+ for vidx, v in enumerate(self.voters)
120
+ ]
121
+
122
+ if return_virtual_cand_positions:
123
+ return UtilityProfile(utility_profile), virtual_cand_positions
124
+ else:
125
+ return UtilityProfile(utility_profile)
126
+ else:
127
+ utility_profile = [
128
+ {c: utility_function(np.array(self.voter_position(v)), np.array(self.candidate_position(c))) for c in self.candidates}
129
+ for v in self.voters
130
+ ]
131
+ return UtilityProfile(utility_profile)
132
+
133
+ def add_candidate(self, candidate_positions, add_multiple_candidates = False):
134
+ """
135
+ Add a candidate to the spatial profile.
136
+
137
+ Args:
138
+ candidate_positions (list): A list of candidate positions
139
+ """
140
+
141
+ if add_multiple_candidates:
142
+ assert all([len(pos) == self.num_dims for pos in candidate_positions]), f"Candidates positions ({candidate_positions}) must be the same dimension as the profile dimension ({self.num_dims})"
143
+
144
+ starting_cand_name = self.num_cands
145
+
146
+ for c_pos in candidate_positions:
147
+ self.cand_pos[starting_cand_name] = c_pos
148
+ starting_cand_name += 1
149
+
150
+ elif not add_multiple_candidates:
151
+
152
+ assert len(candidate_positions) == self.num_dims, f"Candidates position ({candidate_positions}) must be the same dimension as the profile dimension ({self.num_dims})"
153
+
154
+ starting_cand_name = self.num_cands
155
+ self.cand_pos[starting_cand_name] = candidate_positions
156
+
157
+ self.candidates = sorted(list(self.cand_pos.keys()))
158
+
159
+ def move_candidate(self, cand, new_cand_pos):
160
+ """
161
+ Move cand to a new position
162
+ """
163
+
164
+ assert len(new_cand_pos) == self.num_dims, f"The new position {new_cand_pos} must be the same as the profile dimension: {self.num_dims}"
165
+
166
+ assert cand in self.candidates, f"Candidate {cand} is not in the profile."
167
+
168
+ self.cand_pos[cand] = new_cand_pos
169
+
170
+
171
+ def to_string(self):
172
+ """
173
+ Returns a string representation of the spatial profile.
174
+ """
175
+
176
+ sp_str = ''
177
+ for c in self.candidates:
178
+ sp_str += f'C-{c}:{",".join([str(x) for x in self.candidate_position(c)])}_'
179
+ for v in self.voters:
180
+ sp_str += f'V-{v}:{",".join([str(x) for x in self.voter_position(v)])}_'
181
+ return sp_str[:-1]
182
+
183
+
184
+ @classmethod
185
+ def from_string(cls, sp_str):
186
+ """
187
+ Returns a spatial profile described by ``sp_str``.
188
+
189
+ ``sp_str`` must be in the format produced by the :meth:`pref_voting.SpatialProfile.write` function.
190
+ """
191
+
192
+ cand_positions = {}
193
+ voter_positions = {}
194
+
195
+ sp_data = sp_str.split('_')
196
+
197
+ for d in sp_data:
198
+ if d.startswith("C-"):
199
+ cand,positions = d.split(':')
200
+ cand_positions[int(cand[2:])] = np.array([float(x) for x in positions.split(',')])
201
+ elif d.startswith("V-"):
202
+ voter,positions = d.split(':')
203
+ voter_positions[int(voter[2:])] = np.array([float(x) for x in positions.split(',')])
204
+
205
+ return cls(cand_positions, voter_positions)
206
+
207
+ def view(self, show_cand_labels=False, show_voter_labels=False, bin_width=None, dpi=150):
208
+ """
209
+ Displays the spatial model in a 1D, 2D, or 3D plot.
210
+
211
+ Args:
212
+ show_cand_labels (optional, bool): If True, displays the labels of each candidate. The default is False.
213
+ show_voter_labels (optional, bool): If True, displays the labels of each voter. The default is False.
214
+ Note: In 1D visualizations, voter labels are disabled regardless of this setting.
215
+ bin_width (optional, float): Width of bins for grouping voters in 1D visualization. If None, a suitable width is calculated.
216
+ dpi (optional, int): Resolution in dots per inch. Default is 150.
217
+ """
218
+ assert self.num_dims <= 3, "Can only view profiles with 1, 2, or 3 dimensions"
219
+ sns.set_theme(style="darkgrid")
220
+
221
+ # Define the candidate color consistently across all dimensions
222
+ candidate_color = "red"
223
+
224
+ if self.num_dims == 1:
225
+ # Get all voter positions
226
+ voter_positions = [self.voter_position(v)[0] for v in self.voters]
227
+
228
+ # Calculate histogram data
229
+ if bin_width is None:
230
+ # Auto-calculate a reasonable bin width based on data range
231
+ position_range = max(voter_positions) - min(voter_positions)
232
+ bin_width = max(position_range / 20, 0.05)
233
+
234
+ # Create histogram data - exact counts
235
+ bins = {}
236
+ for pos in voter_positions:
237
+ # Round to nearest bin
238
+ binned_pos = round(pos / bin_width) * bin_width
239
+ bins[binned_pos] = bins.get(binned_pos, 0) + 1
240
+
241
+ # Get the bin positions and counts
242
+ bin_positions = list(bins.keys())
243
+ bin_counts = list(bins.values())
244
+ max_count = max(bin_counts) if bin_counts else 1
245
+
246
+ # Create figure with sufficient space for labels and high resolution
247
+ fig, ax = plt.subplots(figsize=(10, 6), dpi=dpi)
248
+
249
+ # Calculate the space needed for candidates at the top
250
+ candidate_area_height = max_count * 0.3
251
+
252
+ # Plot the bars for voters
253
+ bars = ax.bar(
254
+ bin_positions,
255
+ bin_counts,
256
+ width=bin_width*0.8,
257
+ alpha=0.6,
258
+ color="blue",
259
+ label="Voters"
260
+ )
261
+
262
+ # Calculate position for candidates at the top
263
+ top_line_y = max_count + 0.2
264
+ candidate_y_pos = top_line_y + candidate_area_height * 0.3
265
+ candidate_positions = [self.candidate_position(c)[0] for c in self.candidates]
266
+
267
+ # Plot candidates above the histogram
268
+ cand_scatter = ax.scatter(candidate_positions, [candidate_y_pos] * len(self.candidates),
269
+ color=candidate_color, marker='X', s=100, zorder=5)
270
+
271
+ # Create a custom legend box
272
+ legend = ax.legend([cand_scatter, bars], ['Candidates', 'Voters'],
273
+ loc='upper right',
274
+ bbox_to_anchor=(0.99, 0.99))
275
+
276
+ # Draw the figure to get legend position for label placement
277
+ fig.canvas.draw()
278
+
279
+ # Get legend position for detecting label overlap
280
+ if legend:
281
+ legend_bbox = legend.get_window_extent().transformed(ax.transData.inverted())
282
+ legend_left = legend_bbox.x0
283
+ legend_right = legend_bbox.x1
284
+ legend_width = legend_right - legend_left
285
+ else:
286
+ legend_left = float('inf')
287
+ legend_right = float('inf')
288
+ legend_width = 0
289
+
290
+ if show_cand_labels:
291
+ # Add labels to each candidate
292
+ for c in self.candidates:
293
+ pos = self.candidate_position(c)[0]
294
+
295
+ # Buffer to detect legend overlap
296
+ legend_buffer = 0.05 * legend_width
297
+
298
+ # Check if candidate is under or near the legend
299
+ if (pos >= legend_left - legend_buffer) and (pos <= legend_right + legend_buffer):
300
+ # Place label below the marker to avoid legend overlap
301
+ ax.annotate(c, (pos, candidate_y_pos), xytext=(0, -25),
302
+ textcoords='offset points', ha='center', va='top',
303
+ fontsize=13, fontweight='bold', color=candidate_color)
304
+ else:
305
+ # Otherwise place it above
306
+ ax.annotate(c, (pos, candidate_y_pos), xytext=(0, 15),
307
+ textcoords='offset points', ha='center', va='bottom',
308
+ fontsize=13, fontweight='bold', color=candidate_color)
309
+
310
+ # Set axis labels
311
+ ax.set_xlabel('Position')
312
+ ax.set_ylabel('Number of voters')
313
+
314
+ # Configure y-axis to show integers for the histogram area
315
+ ax.yaxis.set_major_locator(plt.MaxNLocator(integer=True))
316
+
317
+ # Set y-limits to include the candidate area
318
+ ax.set_ylim(0, top_line_y + candidate_area_height)
319
+
320
+ # Hide y-ticks in the candidate area
321
+ yticks = [t for t in ax.get_yticks() if t <= max_count]
322
+ ax.set_yticks(yticks)
323
+
324
+ # Adjust the figure layout
325
+ plt.tight_layout(rect=[0, 0, 1, 0.97])
326
+
327
+ plt.show()
328
+
329
+ elif self.num_dims == 2:
330
+
331
+ fig, ax = plt.subplots(figsize=(10, 6), dpi=dpi)
332
+
333
+ # Get voter positions
334
+ x_voters = [self.voter_position(v)[0] for v in self.voters]
335
+ y_voters = [self.voter_position(v)[1] for v in self.voters]
336
+
337
+ # Plot voters with semi-transparency to visualize density
338
+ voter_scatter = ax.scatter(x_voters, y_voters,
339
+ color="blue", alpha=0.2,
340
+ edgecolor="black", linewidth=0.3,
341
+ s=30,
342
+ label="Voters")
343
+
344
+ # Plot candidates
345
+ x_cand = [self.candidate_position(c)[0] for c in self.candidates]
346
+ y_cand = [self.candidate_position(c)[1] for c in self.candidates]
347
+ cand_scatter = ax.scatter(x_cand, y_cand,
348
+ color=candidate_color, marker='X', s=100,
349
+ edgecolor="white", linewidth=0.7,
350
+ zorder=5,
351
+ label="Candidates")
352
+
353
+ # Create a legend
354
+ ax.legend([cand_scatter, voter_scatter], ['Candidates', 'Voters'], loc='upper right')
355
+
356
+ if show_cand_labels:
357
+ for c in self.candidates:
358
+ pos = self.candidate_position(c)
359
+ text = ax.annotate(c, (pos[0], pos[1]), xytext=(0, 10),
360
+ textcoords='offset points', ha='center', va='bottom',
361
+ fontsize=11, fontweight='bold', color=candidate_color)
362
+
363
+ # Add white outline to text for better visibility
364
+ text.set_path_effects([
365
+ path_effects.Stroke(linewidth=1.5, foreground='white'),
366
+ path_effects.Normal()
367
+ ])
368
+
369
+ if show_voter_labels:
370
+ for v in self.voters:
371
+ pos = self.voter_position(v)
372
+ ax.annotate(v + 1, (pos[0], pos[1]), fontsize=8)
373
+
374
+ ax.set_xlabel('Dimension 1')
375
+ ax.set_ylabel('Dimension 2')
376
+ plt.tight_layout()
377
+ plt.show()
378
+
379
+ elif self.num_dims == 3:
380
+
381
+ fig = plt.figure(figsize=(10, 6), dpi=dpi)
382
+ ax = fig.add_subplot(111, projection='3d')
383
+
384
+ # Fetch all positions
385
+ x_voters = [self.voter_position(v)[0] for v in self.voters]
386
+ y_voters = [self.voter_position(v)[1] for v in self.voters]
387
+ z_voters = [self.voter_position(v)[2] for v in self.voters]
388
+
389
+ x_cand = [self.candidate_position(c)[0] for c in self.candidates]
390
+ y_cand = [self.candidate_position(c)[1] for c in self.candidates]
391
+ z_cand = [self.candidate_position(c)[2] for c in self.candidates]
392
+
393
+ # Plot voters with high transparency for better visibility through clusters
394
+ voter_scatter = ax.scatter(x_voters, y_voters, z_voters,
395
+ color="blue", alpha=0.1,
396
+ edgecolor="black", linewidth=0.5,
397
+ s=30,
398
+ label="Voters")
399
+
400
+ # Plot candidate markers with white outline for better visibility
401
+ cand_scatter = ax.scatter(x_cand, y_cand, z_cand,
402
+ color=candidate_color, marker="X", s=40,
403
+ edgecolor="white", linewidth=0.7,
404
+ label="Candidates")
405
+
406
+ # Add voter labels if requested
407
+ if show_voter_labels:
408
+ for v in self.voters:
409
+ pos = self.voter_position(v)
410
+ ax.text(pos[0], pos[1], pos[2], str(v + 1), fontsize=8)
411
+
412
+ # Add candidate labels
413
+ if show_cand_labels:
414
+ for c in self.candidates:
415
+ pos = self.candidate_position(c)
416
+ text = ax.text(pos[0], pos[1], pos[2] + 0.15, c,
417
+ fontsize=11, fontweight='bold', color=candidate_color,
418
+ ha='center', va='bottom')
419
+
420
+ # Add white outline to text for better visibility
421
+ text.set_path_effects([
422
+ path_effects.Stroke(linewidth=1.5, foreground='white'),
423
+ path_effects.Normal()
424
+ ])
425
+
426
+ # Create legend
427
+ ax.legend([cand_scatter, voter_scatter], ['Candidates', 'Voters'], loc='upper right')
428
+
429
+ # Set axis labels
430
+ ax.set_xlabel('Dimension 1')
431
+ ax.set_ylabel('Dimension 2')
432
+ ax.set_zlabel('Dimension 3')
433
+
434
+ plt.tight_layout()
435
+ plt.show()
436
+
437
+ def display(self):
438
+ """
439
+ Displays the positions of each candidate and voter in the profile.
440
+
441
+ """
442
+ print("Candidates: ")
443
+ for c in self.candidates:
444
+ print("Candidate ", c, " position: ", self.candidate_position(c))
445
+
446
+ print("\nVoters: ")
447
+ for v in self.voters:
448
+ print("Voter ", v, " position: ", self.voter_position(v))
@@ -0,0 +1,99 @@
1
+ '''
2
+ File: stochastic_methods.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: November 22, 2024
5
+
6
+ Implementations of voting methods that output winners stochastically (unlike probabilistic methods, which output a probability distribution in the form of a dictionary).
7
+ '''
8
+
9
+ from pref_voting.voting_method import *
10
+ from pref_voting.iterative_methods import consensus_builder
11
+ from pref_voting.probabilistic_methods import maximal_lottery, RaDiUS
12
+ import math
13
+
14
+ @vm(name="Random Consensus Builder (Stochastic)")
15
+ def random_consensus_builder_st(profile, curr_cands=None, beta=0.5):
16
+
17
+ """Version of the Random Consensus Builder (RCB) voting method due to Charikar et al. (https://arxiv.org/abs/2306.17838) that actually chooses a winner stochastically rather than outputting a probability distribution.
18
+
19
+ Args:
20
+
21
+ profile (Profile): An anonymous profile of linear orders
22
+ curr_cands (List[int], optional): Candidates to consider. Defaults to all candidates if not provided.
23
+ beta (float): Threshold for elimination (default 0.5). When processing candidate i, eliminates a candidate j
24
+ above i in the consensus building ranking if the proportion of voters preferring i to j is >= beta
25
+
26
+ Returns:
27
+ A sorted list of candidates.
28
+
29
+ .. seealso::
30
+ :meth:`pref_voting.iterative_methods.consensus_builder`
31
+ :meth:`pref_voting.probabilistic_methods.random_consensus_builder`
32
+
33
+ """
34
+ consensus_building_ranking = random.choice(profile.rankings)
35
+
36
+ return consensus_builder(profile, curr_cands=curr_cands, consensus_building_ranking=consensus_building_ranking, beta=beta)
37
+
38
+ @vm(name="Maximal Lotteries mixed with Random Consensus Builder")
39
+ def MLRCB(profile, curr_cands=None, p = 1 / math.sqrt(2), B = math.sqrt(2) - 1/2):
40
+
41
+ """With probability p, choose the winner from the Maximal Lotteries distribution. With probability 1-p, run the stochastic version of Random Consensus Builder with beta chosen uniformly from (1/2, B). Ths method comes from Theorem 4 of Charikar et al. (https://arxiv.org/abs/2306.17838).
42
+
43
+ Args:
44
+
45
+ profile (Profile): An anonymous profile of linear orders
46
+ curr_cands (List[int], optional): Candidates to consider. Defaults to all candidates if not provided.
47
+ p (float): Probability of choosing the winner from the Maximal Lotteries distribution
48
+ B (float): Upper bound for elimination threshold in the Random Consensus Builder method
49
+
50
+ Returns:
51
+ A sorted list of candidates.
52
+ """
53
+
54
+ if random.random() < p:
55
+ return [maximal_lottery.choose(profile, curr_cands=curr_cands)]
56
+
57
+ else:
58
+ beta = random.uniform(0.5, B)
59
+ return random_consensus_builder_st(profile, curr_cands=curr_cands, beta=beta)
60
+
61
+ @vm(name="Maximal Lotteries mixed with RaDiUS")
62
+ def MLRaDiUS(profile, curr_cands=None):
63
+
64
+ """For p, B, and the probability distribution over beta given in the proof of Theorem 5 of Charikar et al. (https://arxiv.org/abs/2306.17838), choose the winner from the Maximal Lotteries distribution with probability p; with probability 1-p, run the RaDiUS method with beta chosen according to the distribution over beta.
65
+
66
+ Args:
67
+ profile (Profile): An anonymous profile of linear orders
68
+ curr_cands (List[int], optional): Candidates to consider. Defaults to all candidates if not provided.
69
+
70
+ Returns:
71
+ A sorted list of candidates.
72
+ """
73
+ # Parameters
74
+ B = 0.876353 # given in the proof of Theorem 5 of Charikar et al. (https://arxiv.org/abs/2306.17838)
75
+
76
+ # Calculate p as per proof
77
+ ln3 = np.log(3)
78
+ LB = np.log((1 + B) / (1 - B))
79
+ I = 0.5 * (LB - ln3) # Integral value
80
+ p = 1 / (1 + I)
81
+
82
+ def sample_beta(B):
83
+ # Generate a single uniform random number
84
+ u = np.random.uniform(0, 1)
85
+
86
+ # Compute E(u)
87
+ Eu = np.exp(u * (LB - ln3) + ln3)
88
+
89
+ # Compute beta sample
90
+ beta_sample = (Eu - 1) / (Eu + 1)
91
+
92
+ return beta_sample
93
+
94
+ if random.random() < p:
95
+ return [maximal_lottery.choose(profile, curr_cands=curr_cands)]
96
+
97
+ else:
98
+ beta = sample_beta(B)
99
+ return [RaDiUS.choose(profile, curr_cands=curr_cands, beta=beta)]