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,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)]
|