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