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,801 @@
|
|
1
|
+
"""
|
2
|
+
File: gen_profiles.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: December 7, 2020
|
5
|
+
Updated: May 25, 2025
|
6
|
+
|
7
|
+
Functions to generate profiles
|
8
|
+
|
9
|
+
"""
|
10
|
+
|
11
|
+
from itertools import combinations
|
12
|
+
from pref_voting.profiles import Profile
|
13
|
+
from pref_voting.generate_spatial_profiles import generate_spatial_profile
|
14
|
+
from pref_voting.generate_utility_profiles import linear_utility
|
15
|
+
import numpy as np
|
16
|
+
import math
|
17
|
+
import random
|
18
|
+
from scipy.stats import gamma
|
19
|
+
from itertools import permutations
|
20
|
+
from pref_voting.helper import weak_compositions, weak_orders
|
21
|
+
|
22
|
+
from pref_voting.profiles_with_ties import ProfileWithTies
|
23
|
+
from ortools.linear_solver import pywraplp
|
24
|
+
from prefsampling.ordinal import impartial, impartial_anonymous, urn, plackett_luce, didi, stratification, single_peaked_conitzer, single_peaked_walsh, single_peaked_circle, single_crossing, euclidean, mallows
|
25
|
+
|
26
|
+
from prefsampling.core.euclidean import EuclideanSpace
|
27
|
+
from collections import Counter
|
28
|
+
|
29
|
+
# ############
|
30
|
+
# wrapper functions to interface with preflib tools for generating profiles
|
31
|
+
# ############
|
32
|
+
|
33
|
+
|
34
|
+
# Given the number m of candidates and a phi in [0,1],
|
35
|
+
# compute the expected number of swaps in a vote sampled
|
36
|
+
# from the Mallows model
|
37
|
+
def find_expected_number_of_swaps(num_candidates, phi):
|
38
|
+
res = phi * num_candidates / (1 - phi)
|
39
|
+
for j in range(1, num_candidates + 1):
|
40
|
+
res = res + (j * (phi**j)) / ((phi**j) - 1)
|
41
|
+
return res
|
42
|
+
|
43
|
+
|
44
|
+
# Given the number m of candidates and a absolute number of
|
45
|
+
# expected swaps exp_abs, this function returns a value of
|
46
|
+
# phi such that in a vote sampled from Mallows model with
|
47
|
+
# this parameter the expected number of swaps is exp_abs
|
48
|
+
def phi_from_relphi(num_candidates, relphi=None, seed=None):
|
49
|
+
|
50
|
+
rng = np.random.default_rng(seed)
|
51
|
+
if relphi is None:
|
52
|
+
relphi = rng.uniform(0.001, 0.999)
|
53
|
+
if relphi == 1:
|
54
|
+
return 1
|
55
|
+
exp_abs = relphi * (num_candidates * (num_candidates - 1)) / 4
|
56
|
+
low = 0
|
57
|
+
high = 1
|
58
|
+
while low <= high:
|
59
|
+
mid = (high + low) / 2
|
60
|
+
cur = find_expected_number_of_swaps(num_candidates, mid)
|
61
|
+
if abs(cur - exp_abs) < 1e-5:
|
62
|
+
return mid
|
63
|
+
# If x is greater, ignore left half
|
64
|
+
if cur < exp_abs:
|
65
|
+
low = mid
|
66
|
+
|
67
|
+
# If x is smaller, ignore right half
|
68
|
+
elif cur > exp_abs:
|
69
|
+
high = mid
|
70
|
+
|
71
|
+
# If we reach here, then the element was not present
|
72
|
+
return -1
|
73
|
+
|
74
|
+
# Return a list of phis from the relphi value
|
75
|
+
def phis_from_relphi(num_candidates, num, relphi=None, seed=None):
|
76
|
+
|
77
|
+
rng = np.random.default_rng(seed)
|
78
|
+
if relphi is None:
|
79
|
+
relphis = rng.uniform(0.001, 0.999, size=num)
|
80
|
+
else:
|
81
|
+
relphis = [relphi] * num
|
82
|
+
|
83
|
+
return [phi_from_relphi(num_candidates, relphi=relphis[n]) for n in range(num)]
|
84
|
+
|
85
|
+
|
86
|
+
def get_rankings(num_candidates, num_voters, **kwargs):
|
87
|
+
"""
|
88
|
+
Get the rankings for a given number of candidates and voters using
|
89
|
+
the [prefsampling library](https://comsoc-community.github.io/prefsampling/index.html).
|
90
|
+
|
91
|
+
Args:
|
92
|
+
num_candidates (int): The number of candidates.
|
93
|
+
num_voters (int): The number of voters.
|
94
|
+
kwargs (dict): Any parameters for the probability model.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
list: A list of rankings.
|
98
|
+
"""
|
99
|
+
|
100
|
+
if 'probmodel' in kwargs:
|
101
|
+
probmodel = kwargs['probmodel']
|
102
|
+
elif 'probmod' in kwargs: # for backwards compatibility
|
103
|
+
probmodel = kwargs['probmod']
|
104
|
+
else:
|
105
|
+
probmodel = "impartial"
|
106
|
+
|
107
|
+
if 'seed' in kwargs:
|
108
|
+
seed = kwargs['seed']
|
109
|
+
else:
|
110
|
+
seed = None
|
111
|
+
|
112
|
+
if probmodel == "IC" or probmodel == 'impartial':
|
113
|
+
|
114
|
+
rankings = impartial(num_voters,
|
115
|
+
num_candidates,
|
116
|
+
seed=seed)
|
117
|
+
|
118
|
+
elif probmodel == "IAC" or probmodel == 'impartial_anonymous':
|
119
|
+
|
120
|
+
rankings = impartial_anonymous(num_voters,
|
121
|
+
num_candidates,
|
122
|
+
seed=seed)
|
123
|
+
elif probmodel == "MALLOWS" or probmodel == 'mallows':
|
124
|
+
|
125
|
+
impartial_central_vote = True
|
126
|
+
if 'phi' in kwargs:
|
127
|
+
phi = kwargs['phi']
|
128
|
+
else:
|
129
|
+
phi = 1.0
|
130
|
+
|
131
|
+
if 'normalise_phi' in kwargs:
|
132
|
+
normalise_phi = kwargs['normalise_phi']
|
133
|
+
else:
|
134
|
+
normalise_phi = False
|
135
|
+
|
136
|
+
if 'central_vote' in kwargs:
|
137
|
+
central_vote = kwargs['central_vote']
|
138
|
+
impartial_central_vote = False
|
139
|
+
else:
|
140
|
+
central_vote = None
|
141
|
+
|
142
|
+
rankings = mallows(num_voters,
|
143
|
+
num_candidates,
|
144
|
+
phi,
|
145
|
+
normalise_phi=normalise_phi,
|
146
|
+
central_vote=central_vote,
|
147
|
+
impartial_central_vote=impartial_central_vote,
|
148
|
+
seed=seed)
|
149
|
+
|
150
|
+
|
151
|
+
elif probmodel == "MALLOWS-0.8":
|
152
|
+
|
153
|
+
phi = 0.8
|
154
|
+
impartial_central_vote = True
|
155
|
+
if 'normalise_phi' in kwargs:
|
156
|
+
normalise_phi = kwargs['normalise_phi']
|
157
|
+
else:
|
158
|
+
normalise_phi = False
|
159
|
+
|
160
|
+
if 'central_vote' in kwargs:
|
161
|
+
central_vote = kwargs['central_vote']
|
162
|
+
impartial_central_vote = False
|
163
|
+
else:
|
164
|
+
central_vote = None
|
165
|
+
|
166
|
+
rankings = mallows(num_voters,
|
167
|
+
num_candidates,
|
168
|
+
phi,
|
169
|
+
normalise_phi=normalise_phi,
|
170
|
+
central_vote=central_vote,
|
171
|
+
impartial_central_vote=impartial_central_vote,
|
172
|
+
seed=seed)
|
173
|
+
|
174
|
+
elif probmodel == "MALLOWS-0.2":
|
175
|
+
|
176
|
+
phi = 0.2
|
177
|
+
impartial_central_vote = True
|
178
|
+
if 'normalise_phi' in kwargs:
|
179
|
+
normalise_phi = kwargs['normalise_phi']
|
180
|
+
else:
|
181
|
+
normalise_phi = False
|
182
|
+
|
183
|
+
if 'central_vote' in kwargs:
|
184
|
+
central_vote = kwargs['central_vote']
|
185
|
+
impartial_central_vote = False
|
186
|
+
else:
|
187
|
+
central_vote = None
|
188
|
+
|
189
|
+
rankings = mallows(num_voters,
|
190
|
+
num_candidates,
|
191
|
+
phi,
|
192
|
+
normalise_phi=normalise_phi,
|
193
|
+
central_vote=central_vote,
|
194
|
+
impartial_central_vote=impartial_central_vote,
|
195
|
+
seed=seed)
|
196
|
+
|
197
|
+
elif probmodel == "MALLOWS-R":
|
198
|
+
|
199
|
+
rng = np.random.default_rng(seed)
|
200
|
+
phi = rng.uniform(0.001, 0.999)
|
201
|
+
impartial_central_vote = True
|
202
|
+
|
203
|
+
if 'normalise_phi' in kwargs:
|
204
|
+
normalise_phi = kwargs['normalise_phi']
|
205
|
+
else:
|
206
|
+
normalise_phi = False
|
207
|
+
|
208
|
+
if 'central_vote' in kwargs:
|
209
|
+
central_vote = kwargs['central_vote']
|
210
|
+
impartial_central_vote = False
|
211
|
+
else:
|
212
|
+
central_vote = None
|
213
|
+
|
214
|
+
rankings = mallows(num_voters,
|
215
|
+
num_candidates,
|
216
|
+
phi,
|
217
|
+
normalise_phi=normalise_phi,
|
218
|
+
central_vote=central_vote,
|
219
|
+
impartial_central_vote=impartial_central_vote,
|
220
|
+
seed=seed)
|
221
|
+
|
222
|
+
elif probmodel == "MALLOWS-RELPHI":
|
223
|
+
|
224
|
+
impartial_central_vote = True
|
225
|
+
if 'relphi' in kwargs:
|
226
|
+
relphi = kwargs['relphi']
|
227
|
+
else:
|
228
|
+
relphi = None
|
229
|
+
|
230
|
+
if 'normalise_phi' in kwargs:
|
231
|
+
normalise_phi = kwargs['normalise_phi']
|
232
|
+
else:
|
233
|
+
normalise_phi = False
|
234
|
+
|
235
|
+
if 'central_vote' in kwargs:
|
236
|
+
central_vote = kwargs['central_vote']
|
237
|
+
impartial_central_vote = False
|
238
|
+
else:
|
239
|
+
central_vote = None
|
240
|
+
|
241
|
+
phi = phi_from_relphi(num_candidates, relphi=relphi, seed=seed)
|
242
|
+
|
243
|
+
rankings = mallows(num_voters,
|
244
|
+
num_candidates,
|
245
|
+
phi,
|
246
|
+
normalise_phi=normalise_phi,
|
247
|
+
central_vote=central_vote,
|
248
|
+
impartial_central_vote=impartial_central_vote,
|
249
|
+
seed=seed)
|
250
|
+
|
251
|
+
elif probmodel == "MALLOWS-RELPHI-0.375":
|
252
|
+
|
253
|
+
relphi = 0.375
|
254
|
+
impartial_central_vote = True
|
255
|
+
if 'normalise_phi' in kwargs:
|
256
|
+
normalise_phi = kwargs['normalise_phi']
|
257
|
+
else:
|
258
|
+
normalise_phi = False
|
259
|
+
|
260
|
+
if 'central_vote' in kwargs:
|
261
|
+
central_vote = kwargs['central_vote']
|
262
|
+
impartial_central_vote = False
|
263
|
+
else:
|
264
|
+
central_vote = None
|
265
|
+
|
266
|
+
phi = phi_from_relphi(num_candidates, relphi=relphi, seed=seed)
|
267
|
+
|
268
|
+
rankings = mallows(num_voters,
|
269
|
+
num_candidates,
|
270
|
+
phi,
|
271
|
+
normalise_phi=normalise_phi,
|
272
|
+
central_vote=central_vote,
|
273
|
+
impartial_central_vote=impartial_central_vote,
|
274
|
+
seed=seed)
|
275
|
+
|
276
|
+
|
277
|
+
elif probmodel == "MALLOWS-RELPHI-R":
|
278
|
+
|
279
|
+
impartial_central_vote = True
|
280
|
+
if 'normalise_phi' in kwargs:
|
281
|
+
normalise_phi = kwargs['normalise_phi']
|
282
|
+
else:
|
283
|
+
normalise_phi = False
|
284
|
+
|
285
|
+
if 'central_vote' in kwargs:
|
286
|
+
central_vote = kwargs['central_vote']
|
287
|
+
impartial_central_vote = False
|
288
|
+
else:
|
289
|
+
central_vote = None
|
290
|
+
|
291
|
+
phi = phi_from_relphi(num_candidates, relphi=None, seed=seed)
|
292
|
+
|
293
|
+
rankings = mallows(num_voters,
|
294
|
+
num_candidates,
|
295
|
+
phi,
|
296
|
+
normalise_phi=normalise_phi,
|
297
|
+
central_vote=central_vote,
|
298
|
+
impartial_central_vote=impartial_central_vote,
|
299
|
+
seed=seed)
|
300
|
+
|
301
|
+
|
302
|
+
elif probmodel == "URN" or probmodel == 'urn':
|
303
|
+
|
304
|
+
if 'alpha' in kwargs:
|
305
|
+
alpha = kwargs['alpha']
|
306
|
+
else:
|
307
|
+
alpha = 0.0
|
308
|
+
|
309
|
+
rankings = urn(num_voters,
|
310
|
+
num_candidates,
|
311
|
+
alpha,
|
312
|
+
seed=seed)
|
313
|
+
|
314
|
+
elif probmodel == "URN-10":
|
315
|
+
|
316
|
+
alpha = 10
|
317
|
+
rankings = urn(num_voters,
|
318
|
+
num_candidates,
|
319
|
+
alpha,
|
320
|
+
seed=seed)
|
321
|
+
|
322
|
+
elif probmodel == "URN-0.3":
|
323
|
+
|
324
|
+
alpha = round(math.factorial(num_candidates) * 0.3)
|
325
|
+
rankings = urn(num_voters,
|
326
|
+
num_candidates,
|
327
|
+
alpha,
|
328
|
+
seed=seed)
|
329
|
+
|
330
|
+
elif probmodel == "URN-R":
|
331
|
+
|
332
|
+
rng = np.random.default_rng(seed)
|
333
|
+
alpha = round(math.factorial(num_candidates) * gamma.rvs(0.8, random_state=rng))
|
334
|
+
rankings = urn(num_voters,
|
335
|
+
num_candidates,
|
336
|
+
alpha,
|
337
|
+
seed=seed)
|
338
|
+
|
339
|
+
elif probmodel == "plackett_luce":
|
340
|
+
|
341
|
+
if 'alphas' not in kwargs:
|
342
|
+
raise ValueError("Error: alphas parameter missing. A value must be specified for each candidate indicating their relative quality.")
|
343
|
+
#RaiseValueError()
|
344
|
+
else:
|
345
|
+
alphas = kwargs['alphas']
|
346
|
+
|
347
|
+
rankings = plackett_luce(num_voters,
|
348
|
+
num_candidates,
|
349
|
+
alphas,
|
350
|
+
seed=seed)
|
351
|
+
|
352
|
+
elif probmodel == "didi":
|
353
|
+
|
354
|
+
if 'alphas' not in kwargs:
|
355
|
+
raise ValueError("Error: alphas parameter missing. A value must be specified for each candidate indicating each candidate's quality.")
|
356
|
+
else:
|
357
|
+
alphas = kwargs['alphas']
|
358
|
+
|
359
|
+
rankings = didi(num_voters,
|
360
|
+
num_candidates,
|
361
|
+
alphas,
|
362
|
+
seed=seed)
|
363
|
+
|
364
|
+
elif probmodel == "stratification":
|
365
|
+
|
366
|
+
if 'weight' not in kwargs:
|
367
|
+
raise ValueError("Error: weight parameter missing. The weight parameter specifies the size of the upper class of candidates.")
|
368
|
+
else:
|
369
|
+
weight = kwargs['weight']
|
370
|
+
|
371
|
+
rankings = stratification(num_voters,
|
372
|
+
num_candidates,
|
373
|
+
weight,
|
374
|
+
seed=seed)
|
375
|
+
|
376
|
+
elif probmodel == "single_peaked_conitzer":
|
377
|
+
|
378
|
+
rankings = single_peaked_conitzer(num_voters,
|
379
|
+
num_candidates,
|
380
|
+
seed=seed)
|
381
|
+
|
382
|
+
elif probmodel == "SinglePeaked" or probmodel == "single_peaked_walsh":
|
383
|
+
|
384
|
+
rankings = single_peaked_walsh(num_voters,
|
385
|
+
num_candidates,
|
386
|
+
seed=seed)
|
387
|
+
|
388
|
+
elif probmodel == "single_peaked_circle":
|
389
|
+
|
390
|
+
rankings = single_peaked_circle(num_voters,
|
391
|
+
num_candidates,
|
392
|
+
seed=seed)
|
393
|
+
|
394
|
+
elif probmodel == "single_crossing":
|
395
|
+
|
396
|
+
rankings = single_crossing(num_voters,
|
397
|
+
num_candidates,
|
398
|
+
seed=seed)
|
399
|
+
|
400
|
+
elif probmodel == "euclidean":
|
401
|
+
|
402
|
+
euclidean_spaces = {
|
403
|
+
"gaussian_ball": EuclideanSpace.GAUSSIAN_BALL,
|
404
|
+
"gaussian_cube": EuclideanSpace.GAUSSIAN_CUBE,
|
405
|
+
"unbounded_gaussian": EuclideanSpace.UNBOUNDED_GAUSSIAN,
|
406
|
+
"uniform_ball": EuclideanSpace.UNIFORM_BALL,
|
407
|
+
"uniform_cube": EuclideanSpace.UNIFORM_CUBE,
|
408
|
+
"uniform_sphere": EuclideanSpace.UNIFORM_SPHERE,
|
409
|
+
}
|
410
|
+
|
411
|
+
if 'space' in kwargs:
|
412
|
+
space = kwargs['space']
|
413
|
+
else:
|
414
|
+
space = "uniform_ball"
|
415
|
+
|
416
|
+
if 'dimension' in kwargs:
|
417
|
+
dimension = kwargs['dimension']
|
418
|
+
else:
|
419
|
+
dimension = 2
|
420
|
+
|
421
|
+
rankings = euclidean(num_voters,
|
422
|
+
num_candidates,
|
423
|
+
voters_positions=euclidean_spaces[space],
|
424
|
+
candidates_positions=euclidean_spaces[space],
|
425
|
+
num_dimensions=dimension,
|
426
|
+
seed=seed)
|
427
|
+
|
428
|
+
else:
|
429
|
+
raise ValueError("Error: The probability model is not recognized.")
|
430
|
+
|
431
|
+
return rankings
|
432
|
+
|
433
|
+
def generate_profile(num_candidates,
|
434
|
+
num_voters,
|
435
|
+
anonymize=False,
|
436
|
+
num_profiles=1,
|
437
|
+
**kwargs):
|
438
|
+
"""
|
439
|
+
Generate profiles using the prefsampling library.
|
440
|
+
|
441
|
+
Args:
|
442
|
+
num_candidates (int): The number of candidates.
|
443
|
+
num_voters (int): The number of voters.
|
444
|
+
anonymize (bool): If True, anonymize the profiles.
|
445
|
+
num_profiles (int): The number of profiles to generate.
|
446
|
+
kwargs (dict): Any parameters for the probability model.
|
447
|
+
|
448
|
+
Returns:
|
449
|
+
list: A list of profiles or a single profile if num_profiles is 1.
|
450
|
+
"""
|
451
|
+
|
452
|
+
profs = [Profile(get_rankings(num_candidates,
|
453
|
+
num_voters,
|
454
|
+
**kwargs))
|
455
|
+
for _ in range(num_profiles)]
|
456
|
+
|
457
|
+
if anonymize:
|
458
|
+
profs = [prof.anonymize() for prof in profs]
|
459
|
+
|
460
|
+
return profs[0] if num_profiles == 1 else profs
|
461
|
+
|
462
|
+
def generate_profile_with_groups(
|
463
|
+
num_candidates,
|
464
|
+
num_voters,
|
465
|
+
probmodels,
|
466
|
+
weights=None,
|
467
|
+
seed=None,
|
468
|
+
num_profiles=1,
|
469
|
+
anonymize=False):
|
470
|
+
|
471
|
+
"""
|
472
|
+
Generate profiles with groups of voters generated from different probability models.
|
473
|
+
The probability of selecting a probability model is proportional its weight in the list weight.
|
474
|
+
|
475
|
+
Args:
|
476
|
+
num_candidates (int): The number of candidates.
|
477
|
+
num_voters (int): The number of voters.
|
478
|
+
probmodels (list): A list of dictionaries specifying a probability model.
|
479
|
+
weights (list): A list of weights for each probability model.
|
480
|
+
seed (int): The random seed.
|
481
|
+
num_profiles (int): The number of profiles to generate.
|
482
|
+
anonymize (bool): If True, anonymize the profiles.
|
483
|
+
"""
|
484
|
+
if weights is None:
|
485
|
+
weights = [1] * len(probmodels)
|
486
|
+
|
487
|
+
assert len(weights)==len(probmodels), "The number of weights must be equal to the number of probmodels"
|
488
|
+
|
489
|
+
probs = [w / sum(weights) for w in weights]
|
490
|
+
|
491
|
+
rng = np.random.default_rng(seed)
|
492
|
+
|
493
|
+
profs = list()
|
494
|
+
for _ in range(num_profiles):
|
495
|
+
selected_probmodels = rng.choice(probmodels, num_voters, p=probs)
|
496
|
+
|
497
|
+
selected_probmodels_num = Counter([tuple((k,v) if type(v) != list else (k, tuple(v)) for k,v in pm.items()) for pm in selected_probmodels])
|
498
|
+
|
499
|
+
rankings = list()
|
500
|
+
for pm_data, nv in selected_probmodels_num.items():
|
501
|
+
rankings = rankings + list(get_rankings(num_candidates, nv, **dict(pm_data)))
|
502
|
+
|
503
|
+
prof = Profile(rankings)
|
504
|
+
if anonymize:
|
505
|
+
prof = prof.anonymize()
|
506
|
+
profs.append(prof)
|
507
|
+
|
508
|
+
return profs[0] if num_profiles == 1 else profs
|
509
|
+
|
510
|
+
####
|
511
|
+
# Enumerating profiles
|
512
|
+
####
|
513
|
+
|
514
|
+
def enumerate_anon_profile(num_cands, num_voters):
|
515
|
+
"""A generator that enumerates all anonymous profiles with num_cands candidates and num_voters voters.
|
516
|
+
|
517
|
+
Args:
|
518
|
+
num_cands (int): Number of candidates.
|
519
|
+
num_voters (int): Number of voters.
|
520
|
+
|
521
|
+
Yields:
|
522
|
+
Profile: An anonymous profile.
|
523
|
+
"""
|
524
|
+
|
525
|
+
ballot_types = list(permutations(range(num_cands)))
|
526
|
+
num_ballot_types = len(ballot_types)
|
527
|
+
|
528
|
+
for comp in weak_compositions(num_voters, num_ballot_types):
|
529
|
+
instantiated_ballot_types = [ballot_types[idx] for idx, i in enumerate(comp) if i != 0]
|
530
|
+
nonzerocomp = [i for i in comp if i != 0]
|
531
|
+
yield Profile(instantiated_ballot_types, rcounts = nonzerocomp)
|
532
|
+
|
533
|
+
def canonical_ballot_multiset(profile: Profile) -> tuple:
|
534
|
+
"""
|
535
|
+
Lexicographically minimal multiset of (ranking, count) pairs across
|
536
|
+
all candidate permutations.
|
537
|
+
"""
|
538
|
+
m = profile.num_cands
|
539
|
+
rankings, counts = profile.rankings_counts
|
540
|
+
counts = counts.astype(int)
|
541
|
+
|
542
|
+
best = None
|
543
|
+
|
544
|
+
# For each ranking that could potentially be the "canonical" one (0,1,2,...)
|
545
|
+
for r in rankings:
|
546
|
+
# Find the permutation that maps r to (0,1,2,...,m-1)
|
547
|
+
perm = {int(r[i]): i for i in range(m)}
|
548
|
+
|
549
|
+
# Apply this inverse permutation to all rankings
|
550
|
+
canon = tuple(sorted(
|
551
|
+
(tuple(perm[c] for c in ranking.tolist()), int(k))
|
552
|
+
for ranking, k in zip(rankings, counts)
|
553
|
+
))
|
554
|
+
|
555
|
+
if best is None or canon < best:
|
556
|
+
best = canon
|
557
|
+
|
558
|
+
return best
|
559
|
+
|
560
|
+
def enumerate_anon_neutral_profile(num_cands: int, num_voters: int):
|
561
|
+
"""
|
562
|
+
A generator that yields one representative per neutrality-orbit of anonymous profiles.
|
563
|
+
"""
|
564
|
+
seen = set()
|
565
|
+
for prof in enumerate_anon_profile(num_cands, num_voters):
|
566
|
+
key = canonical_ballot_multiset(prof)
|
567
|
+
if key not in seen:
|
568
|
+
seen.add(key)
|
569
|
+
yield prof
|
570
|
+
|
571
|
+
|
572
|
+
def enumerate_anon_profile_with_ties(num_cands, num_voters):
|
573
|
+
"""A generator that enumerates all anonymous profiles--allowing ties in ballots--with num_cands candidates and num_voters voters
|
574
|
+
|
575
|
+
Args:
|
576
|
+
num_cands (int): Number of candidates.
|
577
|
+
num_voters (int): Number of voters.
|
578
|
+
|
579
|
+
Yields:
|
580
|
+
ProfileWithTies: An anonymous profile.
|
581
|
+
"""
|
582
|
+
|
583
|
+
ballot_types = list(weak_orders(range(num_cands)))
|
584
|
+
num_ballot_types = len(ballot_types)
|
585
|
+
|
586
|
+
for comp in weak_compositions(num_voters, num_ballot_types):
|
587
|
+
instantiated_ballot_types = [ballot_types[idx] for idx, i in enumerate(comp) if i != 0]
|
588
|
+
nonzerocomp = [i for i in comp if i != 0]
|
589
|
+
yield ProfileWithTies(instantiated_ballot_types, rcounts = nonzerocomp)
|
590
|
+
|
591
|
+
def _weakorder_to_levels(order):
|
592
|
+
"""Convert a weak order dict -> tuple of rank-levels."""
|
593
|
+
if not order:
|
594
|
+
return tuple()
|
595
|
+
max_rank = max(order.values())
|
596
|
+
return tuple(
|
597
|
+
tuple(sorted(c for c, r in order.items() if r == lev))
|
598
|
+
for lev in range(max_rank + 1)
|
599
|
+
)
|
600
|
+
|
601
|
+
def _canonical_multiset_with_ties(profile):
|
602
|
+
"""Canonical key for a *ProfileWithTies* under candidate permutations."""
|
603
|
+
m = profile.num_cands
|
604
|
+
ballots, counts = profile.rankings_counts
|
605
|
+
ballots = list(ballots)
|
606
|
+
counts = [int(k) for k in counts]
|
607
|
+
|
608
|
+
best = None
|
609
|
+
for perm in permutations(range(m)):
|
610
|
+
relabel = dict(zip(range(m), perm))
|
611
|
+
|
612
|
+
canon = tuple(sorted(
|
613
|
+
(
|
614
|
+
_weakorder_to_levels(
|
615
|
+
{relabel[c]: r for c, r in (
|
616
|
+
ballot if isinstance(ballot, dict) else ballot.rmap
|
617
|
+
).items()}
|
618
|
+
),
|
619
|
+
k,
|
620
|
+
)
|
621
|
+
for ballot, k in zip(ballots, counts)
|
622
|
+
))
|
623
|
+
if best is None or canon < best:
|
624
|
+
best = canon
|
625
|
+
return best
|
626
|
+
|
627
|
+
def enumerate_anon_neutral_profile_with_ties(num_cands, num_voters):
|
628
|
+
"""A generator that yields one representative per neutrality-orbit of anonymous profiles allowing ties.
|
629
|
+
"""
|
630
|
+
seen = set()
|
631
|
+
for prof in enumerate_anon_profile_with_ties(num_cands, num_voters):
|
632
|
+
key = _canonical_multiset_with_ties(prof)
|
633
|
+
if key not in seen:
|
634
|
+
seen.add(key)
|
635
|
+
yield prof
|
636
|
+
|
637
|
+
|
638
|
+
####
|
639
|
+
# Generating ProfilesWithTies
|
640
|
+
####
|
641
|
+
|
642
|
+
|
643
|
+
def strict_weak_orders(A):
|
644
|
+
if not A: # i.e., A is empty
|
645
|
+
yield []
|
646
|
+
return
|
647
|
+
for k in range(1, len(A) + 1):
|
648
|
+
for B in combinations(A, k): # i.e., all nonempty subsets B
|
649
|
+
for order in strict_weak_orders(set(A) - set(B)):
|
650
|
+
yield [B] + order
|
651
|
+
|
652
|
+
|
653
|
+
def generate_truncated_profile(
|
654
|
+
num_cands,
|
655
|
+
num_voters,
|
656
|
+
max_num_ranked=3,
|
657
|
+
probmod="IC"):
|
658
|
+
|
659
|
+
"""Generate a :class:`ProfileWithTies` with ``num_cands`` candidates and ``num_voters``.
|
660
|
+
The ballots will be truncated linear orders of the candidates. Returns a :class:`ProfileWithTies` that uses extended strict preference (so all ranked candidates are strictly preferred to any candidate that is not ranked).
|
661
|
+
|
662
|
+
Args:
|
663
|
+
num_cands (int): The number of candidates to include in the profile.
|
664
|
+
num_voters (int): The number of voters to include in the profile.
|
665
|
+
max_num_ranked (int, default=3): The maximum level to truncate the linear ranking.
|
666
|
+
probmod (str): optional (default "IC")
|
667
|
+
|
668
|
+
Returns:
|
669
|
+
ProfileWithTies
|
670
|
+
|
671
|
+
:Example:
|
672
|
+
|
673
|
+
.. exec_code::
|
674
|
+
|
675
|
+
from pref_voting.generate_profiles import generate_truncated_profile
|
676
|
+
|
677
|
+
prof = generate_truncated_profile(6, 7)
|
678
|
+
prof.display()
|
679
|
+
|
680
|
+
prof = generate_truncated_profile(6, 7, max_num_ranked=6)
|
681
|
+
prof.display()
|
682
|
+
|
683
|
+
:Possible Values of probmod:
|
684
|
+
|
685
|
+
- "IC" (Impartial Culture): each randomly generated linear order of all candidates is truncated at a level from 1 to max_num_ranked, where the probability of truncating at level t is the number of truncated linear orders of length t divided by the number of truncated linear orders of length from 1 to max_num_ranked. Then a voter is equally likely to get any of the truncated linear orders of length from 1 to max_num_ranked.
|
686
|
+
- "RT" (Random Truncation): each randomly generated linear order of all candidates is truncated at a level that is randomly chosen from 1 to max_num_ranked.
|
687
|
+
|
688
|
+
"""
|
689
|
+
|
690
|
+
if max_num_ranked > num_cands:
|
691
|
+
max_num_ranked = num_cands
|
692
|
+
|
693
|
+
if probmod == "IC":
|
694
|
+
num_rankings_of_length = dict()
|
695
|
+
|
696
|
+
for n in range(1, max_num_ranked + 1):
|
697
|
+
num_rankings_of_length[n] = 1
|
698
|
+
for i in range(num_cands,num_cands-n, -1):
|
699
|
+
num_rankings_of_length[n] *= i
|
700
|
+
|
701
|
+
num_all_rankings = sum([num_rankings_of_length[n] for n in range(1, max_num_ranked + 1)])
|
702
|
+
probabilities = [num_rankings_of_length[n] / num_all_rankings for n in range(1, max_num_ranked + 1)]
|
703
|
+
|
704
|
+
lprof = generate_profile(num_cands, num_voters)
|
705
|
+
|
706
|
+
rmaps = list()
|
707
|
+
for r in lprof.rankings:
|
708
|
+
|
709
|
+
if probmod == "RT":
|
710
|
+
truncate_at = random.choice(range(1, max_num_ranked + 1))
|
711
|
+
|
712
|
+
if probmod == "IC":
|
713
|
+
truncate_at = random.choices(range(1, max_num_ranked + 1), weights=probabilities, k=1)[0]
|
714
|
+
|
715
|
+
truncated_r = r[0:truncate_at]
|
716
|
+
|
717
|
+
rmap = {c: _r + 1 for _r, c in enumerate(truncated_r)}
|
718
|
+
|
719
|
+
rmaps.append(rmap)
|
720
|
+
|
721
|
+
prof = ProfileWithTies(
|
722
|
+
rmaps,
|
723
|
+
cmap=lprof.cmap,
|
724
|
+
candidates=lprof.candidates
|
725
|
+
)
|
726
|
+
prof.use_extended_strict_preference()
|
727
|
+
return prof
|
728
|
+
|
729
|
+
####
|
730
|
+
# Generating Profile from ordinal margin graph
|
731
|
+
####
|
732
|
+
|
733
|
+
def minimal_profile_from_edge_order(cands, edge_order):
|
734
|
+
"""Given a list of candidates and a list of edges (positive margin edges only) in order of descending strength, find a minimal profile whose ordinal margin graph has that edge order.
|
735
|
+
|
736
|
+
Args:
|
737
|
+
cands (list): list of candidates
|
738
|
+
edge_order (list): list of edges in order of descending strength
|
739
|
+
|
740
|
+
Returns:
|
741
|
+
Profile: a profile whose ordinal margin graph has the given edge order
|
742
|
+
"""
|
743
|
+
|
744
|
+
solver = pywraplp.Solver.CreateSolver("SAT")
|
745
|
+
|
746
|
+
num_cands = len(cands)
|
747
|
+
rankings = list(permutations(range(num_cands)))
|
748
|
+
|
749
|
+
ranking_to_var = dict()
|
750
|
+
infinity = solver.infinity()
|
751
|
+
for ridx, r in enumerate(rankings):
|
752
|
+
_v = solver.IntVar(0.0, infinity, f"x{ridx}")
|
753
|
+
ranking_to_var[r] = _v
|
754
|
+
|
755
|
+
nv = solver.IntVar(0.0, infinity, "nv")
|
756
|
+
equations = list()
|
757
|
+
for c1 in cands:
|
758
|
+
for c2 in cands:
|
759
|
+
if c1 != c2:
|
760
|
+
if (c1,c2) in edge_order:
|
761
|
+
rankings_c1_over_c2 = [ranking_to_var[r] for r in rankings if r.index(c1) < r.index(c2)]
|
762
|
+
rankings_c2_over_c1 = [ranking_to_var[r] for r in rankings if r.index(c2) < r.index(c1)]
|
763
|
+
equations.append(sum(rankings_c1_over_c2) - sum(rankings_c2_over_c1) >= 1)
|
764
|
+
|
765
|
+
for c3 in cands:
|
766
|
+
for c4 in cands:
|
767
|
+
if c3 != c4:
|
768
|
+
if (c1,c2) in edge_order and (c3,c4) in edge_order and edge_order.index((c1,c2)) < edge_order.index((c3,c4)):
|
769
|
+
rankings_c3_over_c4 = [ranking_to_var[r] for r in rankings if r.index(c3) < r.index(c4)]
|
770
|
+
rankings_c4_over_c3 = [ranking_to_var[r] for r in rankings if r.index(c4) < r.index(c3)]
|
771
|
+
equations.append(sum(rankings_c1_over_c2) - sum(rankings_c2_over_c1) >= sum(rankings_c3_over_c4) - sum(rankings_c4_over_c3) + 1)
|
772
|
+
|
773
|
+
equations.append(nv == sum(list(ranking_to_var.values())))
|
774
|
+
|
775
|
+
for eq in equations:
|
776
|
+
solver.Add(eq)
|
777
|
+
|
778
|
+
solver.Minimize(nv)
|
779
|
+
|
780
|
+
status = solver.Solve()
|
781
|
+
|
782
|
+
if status == pywraplp.Solver.INFEASIBLE:
|
783
|
+
print("Error: Did not find a solution.")
|
784
|
+
return None
|
785
|
+
|
786
|
+
if status != pywraplp.Solver.OPTIMAL:
|
787
|
+
print("Warning: Did not find an optimal solution.")
|
788
|
+
|
789
|
+
_ranks = list()
|
790
|
+
_rcounts = list()
|
791
|
+
|
792
|
+
for r,v in ranking_to_var.items():
|
793
|
+
|
794
|
+
if v.solution_value() > 0:
|
795
|
+
_ranks.append(r)
|
796
|
+
_rcounts.append(int(v.solution_value()))
|
797
|
+
if not v.solution_value().is_integer():
|
798
|
+
print("ERROR: Found non integer, ", v.solution_value())
|
799
|
+
return None
|
800
|
+
|
801
|
+
return Profile(_ranks, rcounts = _rcounts)
|