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,506 @@
1
+ '''
2
+ File: generate_margin_graphs.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: July 14, 2022
5
+ Updated: December 19, 2022
6
+
7
+ Functions to generate a margin graph
8
+
9
+ '''
10
+
11
+
12
+ import networkx as nx
13
+ from itertools import combinations
14
+ from pref_voting.helper import sublists, compositions, enumerate_compositions, convex_lexicographic_sublists
15
+ from pref_voting.weighted_majority_graphs import MarginGraph
16
+ import numpy as np
17
+ from scipy.stats import multivariate_normal
18
+
19
+ def generate_edge_ordered_tournament(num_cands, parity="even"):
20
+ """Generate a random uniquely weighted MarginGraph for ``num_cands`` candidates.
21
+
22
+ :param num_cands: the number of candidates
23
+ :type num_cands: int
24
+ :returns: a uniquely weighted margin graph
25
+ :rtype: MarginGraph
26
+
27
+ .. note:: This function randomly generates a tournament with a linear order over the edges. A **tournament** is an asymmetric directed graph with an edge between every two nodes. The linear order of the edges is represented by assigning to each edge a number :math:`2, \ldots, 2*n`, where :math:`n` is the number of the edges.
28
+ """
29
+
30
+ assert parity in ["even", "odd"], "The parity should be either 'even' or 'odd'."
31
+
32
+ mg = nx.DiGraph()
33
+ mg.add_nodes_from(range(num_cands))
34
+ _edges = list()
35
+ for c1 in range(num_cands):
36
+ for c2 in range(c1+1, num_cands):
37
+ if np.random.choice([True, False]):
38
+ _edges.append((c1, c2))
39
+ else:
40
+ _edges.append((c2, c1))
41
+
42
+ edges = list()
43
+ edge_indices = list(range(len(_edges)))
44
+ np.random.shuffle(edge_indices)
45
+
46
+ for i, e_idx in enumerate(edge_indices):
47
+ edges.append((_edges[e_idx][0], _edges[e_idx][1], 2 * (i+1) if parity == 'even' else 2 * i+1))
48
+
49
+ return MarginGraph(range(num_cands), edges)
50
+
51
+ def generate_margin_graph(num_cands, weight_domain = None, parity = 'even'):
52
+ """Generate a random MarginGraph (allowing for ties in the margins) for ``num_cands`` candidates.
53
+
54
+ Args:
55
+ num_cands (int): the number of candidates
56
+
57
+ Returns:
58
+ MarginGraph
59
+
60
+ """
61
+
62
+ assert parity in ['even', 'odd'], "Parity must be 'even' or 'odd'."
63
+ assert weight_domain is None or isinstance(weight_domain, list) and len(weight_domain) > 0, "The weight_domain must be a list with at least one element."
64
+
65
+ candidates = list(range(num_cands))
66
+ edges = list()
67
+ pairs_of_cands = list(combinations(candidates, 2))
68
+
69
+ if weight_domain is None and parity == 'even':
70
+ weight_domain = [2 * pidx for pidx in range(len(pairs_of_cands) + 1)]
71
+ elif weight_domain is None and parity == 'odd':
72
+ weight_domain = [2 * pidx + 1 for pidx in range(len(pairs_of_cands) + 1)]
73
+ #
74
+
75
+ for c1, c2 in pairs_of_cands:
76
+
77
+ margin = np.random.choice(weight_domain)
78
+
79
+ if margin != 0:
80
+ if np.random.choice([True, False]):
81
+ edges.append((c1, c2, margin))
82
+ else:
83
+ edges.append((c2, c1, margin))
84
+
85
+ return MarginGraph(candidates, edges)
86
+
87
+ def generate_margin_graph_bradley_terry(num_cands, num_voters, score_prob_mod = lambda c: np.random.uniform(0,1)):
88
+ """Generates a margin graph for num_cands candidates by first sampling candidate scores from score_prob_mod and then sampling votes from the Bradley-Terry model using the sampled scores.
89
+
90
+ Args:
91
+ num_cands (int): Number of candidates
92
+ num_voters (int): Number of voters
93
+ score_prob_mod (function, optional): A function that takes a candidate and returns a score. Defaults to lambda c: np.random.uniform(0,1).
94
+
95
+ Returns:
96
+ MarginGraph: A margin graph
97
+ """
98
+
99
+ candidates = list(range(num_cands))
100
+ pairs_of_cands = list(combinations(candidates, 2))
101
+
102
+ cand_score = dict()
103
+ for c in candidates:
104
+ cand_score[c] = score_prob_mod(c)
105
+
106
+ edges = list()
107
+
108
+ for c1, c2 in pairs_of_cands:
109
+
110
+ support_c1_c2 = 0
111
+ support_c2_c1 = 0
112
+
113
+ for n in range(num_voters):
114
+ vote = np.random.choice([1,0], p = [cand_score[c1] / (cand_score[c1] + cand_score[c2]), cand_score[c2] / (cand_score[c1] + cand_score[c2])])
115
+
116
+ if vote == 1:
117
+ support_c1_c2 += 1
118
+ else:
119
+ support_c2_c1 += 1
120
+
121
+ if support_c1_c2 > support_c2_c1:
122
+ edges.append((c1,c2, support_c1_c2 - support_c2_c1))
123
+
124
+ if support_c2_c1 > support_c1_c2:
125
+ edges.append((c2,c1, support_c2_c1 - support_c1_c2))
126
+
127
+ return MarginGraph(candidates, edges)
128
+
129
+ ###
130
+
131
+ # Turn a code into a pair
132
+ def depair(pair_vector, k):
133
+ return pair_vector[k]
134
+
135
+ # This function defines the i,jth entry of the covariance matrix
136
+ def entries(pair_vector, i,j):
137
+ x = depair(pair_vector, i)
138
+ y = depair(pair_vector, j)
139
+ if x[0] == y[0] and x[1] == y[1]:
140
+ return 1
141
+ if x[1] == y[0]:
142
+ return -1/3
143
+ if x[1] == y[1]:
144
+ return 1/3
145
+ if x[0] == y[0]:
146
+ return 1/3
147
+ if x[0] == y[1]:
148
+ return -1/3
149
+ return 0
150
+
151
+ def generate_covariance_matrix(num_candidates):
152
+
153
+ num_pairs = num_candidates *(num_candidates -1)//2
154
+
155
+ # Store the vector mapping codes to pairs
156
+ pair_vector = [0]*num_pairs
157
+
158
+ # Populate the vector of pairs
159
+ k=0
160
+ for i in range(num_candidates):
161
+ for j in range(i+1,num_candidates):
162
+ pair_vector[k] = [i,j]
163
+ k = k+1
164
+
165
+ # Populate the covariance matrix
166
+ cov = np.empty((num_pairs,num_pairs))
167
+ for i in range(num_pairs):
168
+ for j in range(num_pairs):
169
+ cov[i,j] = entries(pair_vector, i,j)
170
+
171
+ return cov
172
+
173
+
174
+ def generate_edge_ordered_tournament_infinite_limit(num_candidates, cov_matrix = None):
175
+ """
176
+ Using the ideas from Section 9 of the paper
177
+ *An Analysis of Random Elections with Large Numbers of Voters* by Matthew Harrison-Trainor
178
+ (https://arxiv.org/abs/2009.02979) and the code provided at
179
+ https://github.com/MatthewHT/RandomMarginGraphs/, generate a qualitative margin graph for
180
+ ``num_candidates`` candidates.
181
+
182
+ .. important::
183
+
184
+ The weights of the generated margin graphs are real numbers, representing a linear ordering of the edges.
185
+ Only qualitative margin graph invariant voting methods, such as Split Cycle, Beat Path, Minimax,
186
+ Ranked Pairs, etc., should be used on the generated graphs.
187
+
188
+ Args:
189
+
190
+ num_candidates (int): the number of candidates
191
+
192
+ Returns:
193
+
194
+ MarginGraph
195
+
196
+ """
197
+
198
+ candidates = range(num_candidates)
199
+ cov_matrix = cov_matrix if cov_matrix is not None else generate_covariance_matrix(num_candidates)
200
+ # random_var is a random variable with the multivariate normal distribution of margin graphs
201
+ random_var = multivariate_normal(None, cov_matrix)
202
+ rv = random_var.rvs()
203
+
204
+ def pair(p):
205
+ return p[1]-2*p[0]-1 + (num_candidates)*(num_candidates+1)//2 - (num_candidates-p[0])*(num_candidates-p[0]+1)//2
206
+
207
+ mg = [[-np.inf for _ in candidates] for _ in candidates]
208
+
209
+ for c1 in candidates:
210
+ for c2 in candidates:
211
+ if (c1 < c2 and rv[pair([c1,c2])] > 0):
212
+ mg[c1][c2] = rv[pair([c1,c2])]
213
+ if (c1 > c2 and rv[pair([c2,c1])] < 0):
214
+ mg[c1][c2] = -rv[pair([c2,c1])]
215
+ if (c1 == c2):
216
+ mg[c1][c2] = 0
217
+
218
+ w_edges = [(c1, c2, mg[c1][c2])
219
+ for c1 in candidates
220
+ for c2 in candidates if c1 != c2 if mg[c1][c2] > 0]
221
+
222
+ return MarginGraph(candidates, w_edges)
223
+
224
+ ## Generating Canonical MarginGraphs without Tied Margins
225
+
226
+ def _enumerate_ceots(num_cands, num_edges, partial_ceot, used_nodes, next_node):
227
+
228
+ # Given a partial ceot P, we can extend it with any new edge (A,B) satisfying one of the following conditions:
229
+
230
+ # 1. A and B have both already been used in edges in P, but neither (A,B) nor (B,A) is in P;
231
+ # 2. A has already been used in an edge in P, and B is the next integer after the largest integer in an edge in P.
232
+ # 3. A is the next integer after the largest integer in an edge in P, and B has already been used in an edge in P;
233
+ # 4. A is the next integer after the largest integer in an edge in P, and B is the next integer after A.
234
+
235
+ if len(partial_ceot) == num_edges:
236
+ yield partial_ceot
237
+
238
+ if len(partial_ceot) < num_edges:
239
+
240
+ if next_node == num_cands:
241
+ available_nodes = used_nodes
242
+
243
+ if next_node < num_cands:
244
+ available_nodes = used_nodes + [next_node]
245
+
246
+ for n in available_nodes:
247
+
248
+ if n == next_node and next_node < num_cands - 1: # If n == next_node, we are in Case 3 or Case 4 above
249
+
250
+ available_nodes = used_nodes + [next_node + 1]
251
+
252
+ for m in available_nodes:
253
+
254
+ if not n==m and not (n,m) in partial_ceot and not (m,n) in partial_ceot:
255
+
256
+ new_ceot = [edge for edge in partial_ceot] + [(n,m)]
257
+
258
+ if not (n == next_node or m == next_node): # Then we are in Case 1 above
259
+
260
+ yield from _enumerate_ceots(num_cands,num_edges,new_ceot,used_nodes,next_node)
261
+
262
+ if (n == next_node or m == next_node) and not m == next_node + 1: # Then we are in Case 2 or 3 above
263
+
264
+ new_used_nodes = list(set(used_nodes + [n,m]))
265
+ new_next_node = next_node + 1
266
+
267
+ yield from _enumerate_ceots(num_cands,num_edges,new_ceot,new_used_nodes,new_next_node)
268
+
269
+ if m == next_node + 1: # Then we are in Case 4 above
270
+
271
+ new_used_nodes = list(set(used_nodes + [n,m]))
272
+ new_next_node = next_node + 2
273
+
274
+ yield from _enumerate_ceots(num_cands,num_edges,new_ceot,new_used_nodes,new_next_node)
275
+
276
+ def _enumerate_ceots_as_edgelist(num_cands):
277
+
278
+ num_edges = (num_cands * (num_cands -1))//2
279
+
280
+ partial_ceot = [(0,1)]
281
+
282
+ used_nodes = [0,1]
283
+
284
+ next_node = 2
285
+
286
+ yield from _enumerate_ceots(num_cands,num_edges,partial_ceot,used_nodes,next_node)
287
+
288
+
289
+ def enumerate_canonical_edge_ordered_tournaments(num_cands, parity = "even"):
290
+ """
291
+ A *canonical* edge-ordered tournament (ceot) is a representative from an isomorphism class of
292
+ linearly edge-ordered tournaments. Enumerate all ceots for ``num_cands`` candidates, representing
293
+ a ceot as a ``MarginGraph`` where the margins represent the linear order of the edges.
294
+
295
+ Args:
296
+ num_cands (int): the number of candidates
297
+ parity (str, optional): The parity of the margins, either 'even' or 'odd'.
298
+
299
+ Returns:
300
+ A generator of ``MarginGraph`` for ``num_candidates``
301
+
302
+ .. warning:: It is only feasible to finish the enumeration for up to 5 candidates.
303
+
304
+ """
305
+
306
+ assert parity in ["odd", "even"], "parity must be either 'odd' or 'even'"
307
+
308
+ for ceot in _enumerate_ceots_as_edgelist(num_cands):
309
+ yield MarginGraph(list(range(num_cands)),
310
+ [(e[0], e[1], 2 * (eidx + 1) if parity == "even" else 2 * eidx + 1)
311
+ for eidx, e in enumerate(reversed(ceot))])
312
+
313
+
314
+ def enumerate_uniquely_weighted_margin_graphs(num_cands, weight_domain):
315
+ """
316
+ Enumerate all representatives from isomorphism classes of uniquely-weighted margin graphs with weights drawn from ``weight_domain``.
317
+
318
+ Args:
319
+ num_cands (int): the number of candidates
320
+ weight_domain (List[int]): The list of weights in the margin graph.
321
+
322
+ Returns:
323
+ A generator of ``MarginGraph`` for ``num_candidates``
324
+
325
+
326
+ .. warning:: It is only feasible to finish the enumeration for up to 5 candidates.
327
+
328
+ """
329
+
330
+ weight_domain = sorted(weight_domain)
331
+
332
+ num_edges = (num_cands * (num_cands - 1)) // 2
333
+
334
+ for ceot in _enumerate_ceots_as_edgelist(num_cands):
335
+
336
+ for weight_list in sublists(weight_domain, num_edges):
337
+ yield MarginGraph(list(range(num_cands)),
338
+ [(e[0], e[1], weight_list[eidx]) for eidx, e in enumerate(reversed(ceot))])
339
+
340
+
341
+ ## Generating Canonical MarginGraphs with Tied Margins
342
+
343
+ def _enumerate_cweots_as_edgelist(num_cands, include_weak_tournaments=True):
344
+
345
+ #Enumerate each canonical weakly edge ordered tournament as a list of lists of tied edges.
346
+ #If include_weak_tournaments = True, then allow weak tournaments in which two nodes may have no edge between them.
347
+
348
+ def edge_match(e1, e2):
349
+ return e1['weight'] == e2['weight']
350
+
351
+ cweots = dict() # For isomorphism checking, keep track of the cweots generated so far.
352
+
353
+ if include_weak_tournaments:
354
+ cweots_with_absent_edges = dict() # For isomorphism checking, keep track of the cweots for which some edges are absent.
355
+
356
+ for ceot in tqdm(list(_enumerate_ceots_as_edgelist(num_cands))):
357
+
358
+ # The sorted list of number of wins by each candidate will be a useful invariant for isomorphism checking below.
359
+ win_vector = tuple(sorted([len([edge for edge in ceot if edge[0] == i]) for i in range(num_cands)]))
360
+
361
+ # Given ceot, we will generate many cweots as follows:
362
+
363
+ # 1. Collect all the convex lexicographic sublists of ceot in order as [L1,...,Ln].
364
+ # It suffices to only consider convex lexicographic sublists because for any cweot,
365
+ # we can obtain a ceot by breaking all ties between edges in a tied group lexicographically.
366
+
367
+ l_sublists = convex_lexicographic_sublists(ceot)
368
+
369
+ # 2. Within each L_i, we want to consider all ways of making consecutive edges tied.
370
+ # Such a way is given by a composition of the integer len(L_i).
371
+ # Thus, we first iterate over all compositions of len(L_i),...,len(L_n).
372
+
373
+ int_list = [len(s) for s in l_sublists]
374
+
375
+ # 3. Since the above approach overgenerates cweots, we will check for isomorphism before adding a cweot to our list.
376
+
377
+ for compositions in enumerate_compositions(int_list):
378
+
379
+ cases = [False, True] if include_weak_tournaments else [False]
380
+
381
+ for consider_weak_tourns in cases:
382
+
383
+ cweot = []
384
+
385
+ for idx, s in enumerate(l_sublists):
386
+
387
+ composition = compositions[idx]
388
+
389
+ for n in composition:
390
+ cweot.append(s[:n])
391
+ s=s[n:]
392
+
393
+ # If we are considering weak tournaments in this case, we remove the last tied group of edges and compute the sorted win-loss vector.
394
+ if consider_weak_tourns:
395
+ win_loss_vector = tuple(sorted([(len([edge for edge in ceot if edge[0] == i and edge not in cweot[-1]]),len([edge for edge in ceot if edge[1] == i and edge not in cweot[-1]])) for i in range(num_cands)]))
396
+ cweot = cweot[:-1]
397
+
398
+ G = nx.DiGraph()
399
+ weight = len(ceot)
400
+ for group in cweot:
401
+ for edge in group:
402
+ G.add_edge(edge[0], edge[1], weight=weight)
403
+ weight = weight-1
404
+
405
+ add_graph = True
406
+
407
+ # Next we check whether G is isomorphic to a cweot G2 already generated.
408
+ # We only need to check those cweots G2 that have (i) the same sorted Copeland scores and
409
+ # (ii) the same list of numbers of edges in each tied group as G,
410
+ # since these are necessary conditions for isomorphism.
411
+
412
+ tie_sizes = tuple([len(s) for s in cweot])
413
+
414
+ if not consider_weak_tourns:
415
+
416
+ invariant = (win_vector, tie_sizes)
417
+
418
+ if invariant not in cweots.keys():
419
+ cweots[invariant] = []
420
+
421
+ for idx, G2 in enumerate(cweots[invariant]):
422
+ if nx.is_isomorphic(G, G2, edge_match=edge_match):
423
+ add_graph = False
424
+ break
425
+
426
+ if add_graph:
427
+ cweots[invariant].append(G)
428
+ yield cweot
429
+
430
+ if consider_weak_tourns:
431
+
432
+ invariant = (win_loss_vector, tie_sizes)
433
+
434
+ if invariant not in cweots_with_absent_edges.keys():
435
+ cweots_with_absent_edges[invariant] = []
436
+
437
+ for idx, G2 in enumerate(cweots_with_absent_edges[invariant]):
438
+ if nx.is_isomorphic(G, G2, edge_match=edge_match):
439
+ add_graph = False
440
+ break
441
+
442
+ if add_graph:
443
+ cweots_with_absent_edges[invariant].append(G)
444
+ yield cweot
445
+
446
+ def enumerate_canonical_weakly_edge_ordered_tournaments(num_cands, parity = "even", include_weak_tournaments = True):
447
+ """
448
+ A *canonical* weakly edge-ordered tournament (cweot) is a representative from an isomorphism class of
449
+ weakly edge-ordered tournaments. Enumerate all cweots for ``num_cands`` candidates, representing
450
+ a cweot as a ``MarginGraph`` where the margins represent the order of the edges.
451
+
452
+ If include_weak_tournaments = True, then allow weak tournaments in which two nodes may have no edge between them.
453
+
454
+ Args:
455
+ num_cands (int): the number of candidates
456
+ parity (str, optional): The parity of the margins, either 'even' or 'odd'.
457
+
458
+ Returns:
459
+ A generator of ``MarginGraph`` for ``num_candidates``
460
+
461
+ .. warning:: It is only feasible to finish the enumeration for up to 4 candidates.
462
+
463
+ """
464
+
465
+ assert parity in ["odd", "even"], "parity must be either 'odd' or 'even'"
466
+
467
+ for cweot in _enumerate_cweots_as_edgelist(num_cands, include_weak_tournaments=include_weak_tournaments):
468
+
469
+ weighted_edges = list()
470
+
471
+ for idx, group in enumerate(reversed(cweot)):
472
+ for e in group:
473
+ weighted_edge = (e[0], e[1], 2 * (idx + 1) if parity == "even" else 2 * idx + 1)
474
+ weighted_edges.append(weighted_edge)
475
+
476
+ yield MarginGraph(list(range(num_cands)), weighted_edges)
477
+
478
+ def enumerate_margin_graphs(num_cands, weight_domain, include_weak_tournaments = True):
479
+ """
480
+ Enumerate all representatives from isomorphism classes of margin graphs with weights drawn from ``weight_domain``.
481
+
482
+ Args:
483
+ num_cands (int): the number of candidates
484
+ weight_domain (List[int]): The list of weights in the margin graph.
485
+
486
+ Returns:
487
+ A generator of ``MarginGraph`` for ``num_candidates``
488
+
489
+ .. warning:: It is only feasible to finish the enumeration for up to 4 candidates.
490
+
491
+ """
492
+
493
+ weight_domain = sorted(weight_domain)
494
+
495
+ for cweot in _enumerate_cweots_as_edgelist(num_cands, include_weak_tournaments = include_weak_tournaments):
496
+
497
+ for weight_list in sublists(weight_domain, len(cweot)):
498
+
499
+ weighted_edges = list()
500
+
501
+ for idx, group in enumerate(reversed(cweot)):
502
+ for e in group:
503
+ weighted_edge = (e[0], e[1], weight_list[idx])
504
+ weighted_edges.append(weighted_edge)
505
+
506
+ yield MarginGraph(list(range(num_cands)), weighted_edges)
@@ -0,0 +1,184 @@
1
+ '''
2
+ File: grade_methods.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: September 24, 2023
5
+
6
+ Implementations of grading methods (also called evaluative methods).
7
+ '''
8
+ from pref_voting.voting_method import *
9
+ from itertools import product
10
+
11
+ @vm(name="Score Voting")
12
+ def score_voting(gprofile, curr_cands=None, evaluation_method="sum"):
13
+ """Return the candidates with the largest scores, where scores are evaluated using the ``evaluation_method``, where the default is summing the scores of the candidates. If ``curr_cands`` is provided, then the score vote is restricted to the candidates in ``curr_cands``.
14
+ """
15
+
16
+ curr_cands = gprofile.candidates if curr_cands is None else curr_cands
17
+ if evaluation_method == "sum":
18
+ evaluation_method_func = gprofile.sum
19
+ elif evaluation_method == "mean" or evaluation_method == "average":
20
+ evaluation_method_func = gprofile.avg
21
+ elif evaluation_method == "median": # returns lower median
22
+ evaluation_method_func = gprofile.median
23
+
24
+ scores = {
25
+ c: evaluation_method_func(c)
26
+ for c in curr_cands if gprofile.has_grade(c)
27
+ }
28
+
29
+ max_score = max(scores.values())
30
+
31
+ return sorted([c for c in scores.keys() if scores[c] == max_score])
32
+
33
+ @vm(name="Approval")
34
+ def approval(gprofile, curr_cands=None):
35
+ """Return the approval vote of the grade profile ``gprofile``. If ``curr_cands`` is provided, then the approval vote is restricted to the candidates in ``curr_cands``.
36
+
37
+ .. warning::
38
+ Approval Vote only works on Grade Profiles that are based on 2 grades: 0 and 1.
39
+
40
+ """
41
+ assert sorted(gprofile.grades) == [0, 1], "The grades in the profile must be {0, 1}."
42
+
43
+ return score_voting(gprofile, curr_cands=curr_cands, evaluation_method="sum")
44
+
45
+ @vm(name="Dis&approval")
46
+ def dis_and_approval(gprofile, curr_cands=None):
47
+ """Return the Dis&approval vote of the grade profile ``gprofile``. If ``curr_cands`` is provided, then the dis&approval vote is restricted to the candidates in ``curr_cands``. See https://link.springer.com/article/10.1007/s00355-013-0766-7 for more information.
48
+
49
+ .. warning::
50
+ Dis&approval only works on Grade Profiles that are based on 2 grades: -1 and 1.
51
+
52
+ """
53
+ assert sorted(gprofile.grades) == [-1, 0, 1], "The grades in the profile must be {-1, 0, 1}."
54
+
55
+ return score_voting(gprofile, curr_cands=curr_cands, evaluation_method="sum")
56
+
57
+ @vm(name="Cumulative Voting")
58
+ def cumulative_voting(gprofile, curr_cands=None, max_total_grades=5):
59
+ """Return the cumulative vote winner of the grade profile ``gprofile``. This is the candidates with the largest sum of the grades where each voter submits a ballot of scores that sum to ``max_total_grades``. If ``curr_cands`` is provided, then the cumulative vote is restricted to the candidates in ``curr_cands``."""
60
+ assert sorted(gprofile.grades) == list(range(max_total_grades + 1)) and np.sum(gprofile.grades) == max_total_grades , f"For cumulative voting, the sum the grades must be {max_total_grades}."
61
+
62
+ return score_voting(gprofile, curr_cands=curr_cands, evaluation_method="sum")
63
+
64
+ @vm(name="STAR")
65
+ def star(gprofile, curr_cands=None):
66
+ """ Identify the top two candidates according to the sum of the grades for each candidate. Then hold a runoff between the top two candidates where the candidate that is ranked above the other by the most voters is the winner. The candidates that move to the runoff round are: the candidate(s) with the largest sum of the grades and the candidate(s) with the 2nd largest sum of the grades (or perhaps tied for the largest sum of the grades). In the case of multiple candidates tied for the largest or 2nd largest sum of the grades, use parallel-universe tiebreaking: a candidate is a Star Vote winner if it is a winner in some head-to-head runoff as described. If the candidates are all tied for the largest sum of the grades, then all candidates are winners.
67
+
68
+ See https://starvoting.us for more information.
69
+
70
+ If ``curr_cands`` is provided, then the winners is restricted to the candidates in ``curr_cands``.
71
+
72
+ .. warning::
73
+ Star Vote only works on Grade Profiles that are based on 6 grades: 0, 1, 2, 3, 4, and 5.
74
+ """
75
+
76
+ assert sorted(gprofile.grades) == [0, 1, 2, 3, 4, 5], "The grades in the profile must be {0, 1, 2, 3, 4, 5}."
77
+
78
+ curr_cands = gprofile.candidates if curr_cands is None else curr_cands
79
+
80
+ if len(curr_cands) == 1:
81
+ return list(curr_cands)
82
+
83
+ cand_to_scores = {
84
+ c: gprofile.sum(c)
85
+ for c in curr_cands if gprofile.has_grade(c)
86
+ }
87
+
88
+ scores = sorted(list(set(cand_to_scores.values())), reverse=True)
89
+
90
+ max_score = scores[0]
91
+ first = [c for c in cand_to_scores.keys() if cand_to_scores[c] == max_score]
92
+
93
+ second = list()
94
+ if len(first) == 1:
95
+ second_score = scores[1]
96
+ second = [c for c in cand_to_scores.keys() if cand_to_scores[c] == second_score]
97
+
98
+ if len(second) > 0:
99
+ all_runoff_pairs = product(first, second)
100
+ else:
101
+ all_runoff_pairs = [(c1,c2) for c1,c2 in product(first, first) if c1 != c2]
102
+
103
+ winners = list()
104
+ for c1, c2 in all_runoff_pairs:
105
+
106
+ if gprofile.margin(c1,c2) > 0:
107
+ winners.append(c1)
108
+ elif gprofile.margin(c1,c2) < 0:
109
+ winners.append(c2)
110
+ elif gprofile.margin(c1,c2) == 0:
111
+ winners.append(c1)
112
+ winners.append(c2)
113
+
114
+ return sorted(list(set(winners)))
115
+
116
+
117
+ def tiebreaker_diff(gprofile, cand, median_grade):
118
+ """
119
+ Tiebreaker when the there are multiple candidates with the largest median grade.
120
+ The tiebreaker is the difference between the proportion of voters who grade the candidate higher than the median grade and the proportion of voters who grade the candidate lower than the median grade.
121
+ """
122
+ prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
123
+ prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
124
+
125
+ return prop_proponents - prop_opponents
126
+
127
+ def tiebreaker_relative_shares(gprofile, cand, median_grade):
128
+ """
129
+ Tiebreaker when the there are multiple candidates with the largest median grade.
130
+ Returns the *relative shares* of the proponents and opponents of the candidate.
131
+ """
132
+
133
+ prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
134
+ prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
135
+
136
+ return (prop_proponents - prop_opponents) / (2 * (prop_proponents + prop_opponents))
137
+
138
+ def tiebreaker_normalized_difference(gprofile, cand, median_grade):
139
+ """
140
+ Tiebreaker when the there are multiple candidates with the largest median grade.
141
+ Returns the *normalized difference* of the proponents and opponents of the candidate.
142
+ """
143
+
144
+ prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
145
+ prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
146
+
147
+ return (prop_proponents - prop_opponents) /(2 * (1 - prop_proponents - prop_opponents))
148
+
149
+
150
+ def tiebreaker_majority_judgement(gprofile, cand, median_grade):
151
+ """
152
+ Tiebreaker when the there are multiple candidates with the largest median grade.
153
+ Returns the proportion of voters assigning a higher grade than the median to cand if it is greater than the proportion of voters assigning a lower grade than the median to cand, otherwise return -1 * the proportion of voters assigning a lower grade than the median to cand.
154
+ """
155
+
156
+ prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
157
+ prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
158
+
159
+ if prop_proponents > prop_opponents:
160
+ return prop_proponents
161
+ elif prop_opponents >= prop_proponents:
162
+ return -prop_opponents
163
+
164
+
165
+ def greatest_median(gprofile, curr_cands=None, tb_func = tiebreaker_majority_judgement):
166
+
167
+ """
168
+ Returns the candidate(s) with the greatest median grade. If there is a tie, the tie is broken by the tiebreaker function.
169
+
170
+ """
171
+ median_winners = score_voting(gprofile, curr_cands=curr_cands, evaluation_method="median")
172
+
173
+ if len(median_winners) == 1:
174
+ return median_winners
175
+ else:
176
+ tb_scores = {c: tb_func(gprofile, c, gprofile.median(c)) for c in median_winners}
177
+ return sorted([c for c in tb_scores if tb_scores[c] == max(tb_scores.values())])
178
+
179
+ @vm(name="Majority Judgement")
180
+ def majority_judgement(gprofile, curr_cands=None):
181
+ """
182
+ The Majority Judgement voting method as describe in Balinski and Laraki (https://mitpress.mit.edu/9780262545716/majority-judgment/).
183
+ """
184
+ return greatest_median(gprofile, curr_cands=curr_cands, tb_func = tiebreaker_majority_judgement)