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,2345 @@
1
+ '''
2
+ File: margin_based_methods.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: January 10, 2022
5
+ Update: August 31, 2025
6
+
7
+ Implementations of voting methods that work on both profiles and margin graphs.
8
+ '''
9
+
10
+ from pref_voting.voting_method import *
11
+ from pref_voting.weighted_majority_graphs import MajorityGraph, MarginGraph
12
+ from pref_voting.probabilistic_methods import maximal_lottery, c1_maximal_lottery
13
+ from pref_voting.helper import get_mg, SPO
14
+ import math
15
+ from itertools import product, permutations, combinations, chain
16
+ import networkx as nx
17
+ from pref_voting.voting_method_properties import ElectionTypes
18
+ import multiprocessing as mp
19
+ from multiprocessing import Pool
20
+ from functools import partial
21
+
22
+ @vm(name = "Minimax",
23
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH]
24
+ )
25
+ def minimax(edata, curr_cands = None, strength_function = None):
26
+ """
27
+ The Minimax winners are the candidates with the smallest maximum pairwise loss. That is, for each candidate :math:`a`, find the biggest margin of a candidate :math:`b` over :math:`a`, then elect the candidate(s) with the smallest such loss. Also known as the Simpson-Kramer Rule.
28
+
29
+ Args:
30
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
31
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
32
+
33
+ Returns:
34
+ A sorted list of candidates
35
+
36
+ .. seealso::
37
+
38
+ :meth:`pref_voting.margin_based_methods.minimax_scores`
39
+
40
+ :Example:
41
+
42
+ .. plot:: margin_graphs_examples/mg_ex_minimax.py
43
+ :context: reset
44
+ :include-source: True
45
+
46
+
47
+ .. code-block::
48
+
49
+ from pref_voting.margin_based_methods import minimax
50
+
51
+ minimax.display(prof)
52
+
53
+
54
+ .. exec_code::
55
+ :hide_code:
56
+
57
+ from pref_voting.profiles import Profile
58
+ from pref_voting.margin_based_methods import minimax
59
+
60
+ prof = Profile([[3, 0, 1, 2], [1, 3, 2, 0], [1, 3, 0, 2], [1, 2, 0, 3], [3, 2, 0, 1], [0, 2, 1, 3]], [1, 1, 1, 1, 2, 1])
61
+
62
+ minimax.display(prof)
63
+
64
+ """
65
+
66
+ candidates = edata.candidates if curr_cands is None else curr_cands
67
+ strength_function = edata.margin if strength_function is None else strength_function
68
+
69
+ scores = {c: max([strength_function(_c, c) for _c in edata.dominators(c) if _c in candidates]) if any([_c in edata.dominators(c) for _c in candidates]) else 0
70
+ for c in candidates}
71
+ min_score = min(scores.values())
72
+ return sorted([c for c in candidates if scores[c] == min_score])
73
+
74
+
75
+ @vm(name = "Minimax (Support)",
76
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES]
77
+ )
78
+ def minimax_support(edata, curr_cands = None):
79
+ """
80
+ The Minimax method using the support function for the strength_function.
81
+ """
82
+
83
+ return minimax(edata, curr_cands = curr_cands, strength_function = edata.support)
84
+
85
+
86
+ def minimax_scores(edata, curr_cands = None, score_method="margins"):
87
+ """Return the minimax scores for each candidate, where the minimax score for :math:`c` is -1 * the maximum pairwise majority loss.
88
+
89
+ Args:
90
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
91
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
92
+ score_method (str, optional): Options include "margins" (the default), "winning" assigns to each candidate :math:`c` the maximum support of a candidate majority preferred to :math:`c`, and "pairwise_opposition" assigns to each candidate :math:`c` the maximum support of any candidate over :math:`c`. These scores only lead to different results on non-linear profiles.
93
+
94
+ Returns:
95
+ A dictionary associating each candidate with its minimax score.
96
+
97
+ .. seealso::
98
+
99
+ :meth:`pref_voting.margin_based_methods.minimax`
100
+
101
+ :Example:
102
+
103
+ .. plot:: margin_graphs_examples/mg_ex_minimax.py
104
+ :context: reset
105
+ :include-source: True
106
+
107
+
108
+ .. code-block::
109
+
110
+ from pref_voting.margin_based_methods import minimax_scores, minimax
111
+
112
+ minimax.display(prof)
113
+ print(minimax_scores(prof))
114
+
115
+
116
+ .. exec_code::
117
+ :hide_code:
118
+
119
+ from pref_voting.profiles import Profile
120
+ from pref_voting.margin_based_methods import minimax, minimax_scores
121
+
122
+ prof = Profile([[3, 0, 1, 2], [1, 3, 2, 0], [1, 3, 0, 2], [1, 2, 0, 3], [3, 2, 0, 1], [0, 2, 1, 3]], [1, 1, 1, 1, 2, 1])
123
+
124
+ minimax.display(prof)
125
+ print(minimax_scores(prof))
126
+
127
+ """
128
+
129
+ candidates = edata.candidates if curr_cands is None else curr_cands
130
+
131
+ if len(candidates) == 1:
132
+ return {c: 0 for c in candidates}
133
+
134
+ # there are different scoring functions that can be used to measure the worse loss for each
135
+ # candidate. These all produce the same set of winners when voters submit linear orders.
136
+ score_functions = {
137
+ "winning": lambda cs, c: max([edata.support(_c,c) for _c in cs]) if len(cs) > 0 else 0,
138
+ "margins": lambda cs, c: max([edata.margin(_c,c) for _c in cs]) if len(cs) > 0 else 0,
139
+ "pairwise_opposition": lambda cs, c: max([edata.support(_c,c) for _c in cs])
140
+ }
141
+
142
+ cands = {
143
+ "winning": lambda c: edata.dominators(c, curr_cands = curr_cands),
144
+ "margins": lambda c: edata.dominators(c, curr_cands = curr_cands),
145
+ "pairwise_opposition": lambda c: [_c for _c in candidates if _c != c]
146
+ }
147
+
148
+ return {c: -1 * score_functions[score_method](cands[score_method](c), c) for c in candidates}
149
+
150
+ @vm(name = "Leximax",
151
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
152
+ def leximax(edata, curr_cands = None):
153
+ """Return the candidate(s) with the worst loss is smallest, per Minimax; if there are multiple candidates with the same worst loss, then return the candidate(s) whose second-worst loss is smallest, and so on.
154
+
155
+ Args:
156
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
157
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
158
+
159
+ Returns:
160
+ A sorted list of candidates
161
+
162
+ .. note::
163
+ This method is discussed (under the name `Leximin') in https://arxiv.org/abs/2411.19857.
164
+ """
165
+
166
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
167
+
168
+ sequence_of_margins_dict = {cand: sorted([edata.margin(other_cand, cand) for other_cand in curr_cands if other_cand != cand], reverse = True) for cand in curr_cands}
169
+
170
+ candidates_with_minimal_sequence_of_margins = [cand for cand in curr_cands if sequence_of_margins_dict[cand] == min(sequence_of_margins_dict.values())]
171
+
172
+ return sorted(candidates_with_minimal_sequence_of_margins)
173
+
174
+ @vm(name = "Most Wins, Smallest Loss",
175
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH]
176
+ )
177
+ def MWSL(edata, half_point_for_ties = True, lexicographic_tie_breaker = True, curr_cands = None):
178
+ """Return the candidate with the most head-to-head wins. If multiple candidates tie for the most wins, return the one with the smallest head-to-head loss.
179
+
180
+ If half_point_for_ties = True, then each head-to-head tie counts for 1/2 of a win.
181
+
182
+ If lexicographic_tie_breaker = True, then if among the candidates with the most wins, there are multiple candidates with the smallest loss, then we break the tie in favor of the candidate whose second-smallest loss is smallest, and so on.
183
+
184
+ Args:
185
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
186
+ half_point_for_ties (bool, optional): If True, then each head-to-head tie counts for 1/2 of a win.
187
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
188
+
189
+ Returns:
190
+ A sorted list of candidates
191
+
192
+ .. note::
193
+ This method was proposed in https://www.arxiv.org/abs/2508.17095.
194
+ """
195
+
196
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
197
+
198
+ if half_point_for_ties:
199
+ score = edata.copeland_scores(curr_cands = curr_cands, scores = (1,0.5,0))
200
+ else:
201
+ score = edata.copeland_scores(curr_cands = curr_cands, scores = (1,0,0))
202
+
203
+ most_wins = [c for c in curr_cands if score[c] == max(score.values())]
204
+
205
+ if len(most_wins) == 1:
206
+ return most_wins
207
+
208
+ if not lexicographic_tie_breaker:
209
+ # find the smallest loss of each candidate in most_wins
210
+ smallest_loss_dict = {}
211
+ for c in most_wins:
212
+ defeaters_of_c = edata.dominators(c, curr_cands = curr_cands)
213
+ if len(defeaters_of_c) > 0:
214
+ smallest_loss_dict[c] = min([edata.margin(d,c) for d in defeaters_of_c])
215
+ else:
216
+ smallest_loss_dict[c] = 0 # if c has no loss, we count their "smallest loss" as 0
217
+
218
+ # pick the candidate(s) with the smallest loss
219
+ return [c for c in most_wins if smallest_loss_dict[c] == min(smallest_loss_dict.values())]
220
+
221
+ else:
222
+ # find the sequence of losses for each candidate, from smallest to largest
223
+ sequence_of_losses_dict = {}
224
+ for c in most_wins:
225
+ defeaters_of_c = edata.dominators(c, curr_cands = curr_cands)
226
+ if len(defeaters_of_c) > 0:
227
+ sequence_of_losses_dict[c] = sorted([edata.margin(d,c) for d in defeaters_of_c], reverse = True)
228
+ else:
229
+ sequence_of_losses_dict[c] = [0]
230
+
231
+ # pick the candidate(s) with the smallest sequence of losses
232
+ return [c for c in most_wins if sequence_of_losses_dict[c] == min(sequence_of_losses_dict.values())]
233
+
234
+
235
+ def maximal_elements(g):
236
+ """return the nodes in g with no incoming arrows."""
237
+ return [n for n in g.nodes if g.in_degree(n) == 0]
238
+
239
+
240
+ def _beat_path_basic(edata,
241
+ curr_cands = None,
242
+ strength_function = None):
243
+ """An implementation of the Beat Path method that uses a basic algorithm. This is not efficient for large graphs.
244
+
245
+ Args:
246
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
247
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
248
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
249
+
250
+ Returns:
251
+ A sorted list of candidates.
252
+
253
+ """
254
+ candidates = edata.candidates if curr_cands is None else curr_cands
255
+ strength_function = edata.margin if strength_function is None else strength_function
256
+
257
+ mg = get_mg(edata, curr_cands = curr_cands)
258
+
259
+ beat_paths_weights = {c: {c2:0 for c2 in candidates if c2 != c} for c in candidates}
260
+ for c in candidates:
261
+ for other_c in beat_paths_weights[c].keys():
262
+ all_paths = list(nx.all_simple_paths(mg, c, other_c))
263
+ if len(all_paths) > 0:
264
+ beat_paths_weights[c][other_c] = max([min([strength_function(p[i], p[i+1])
265
+ for i in range(0,len(p)-1)])
266
+ for p in all_paths])
267
+
268
+ winners = list()
269
+ for c in candidates:
270
+ if all([beat_paths_weights[c][c2] >= beat_paths_weights[c2][c] for c2 in candidates if c2 != c]):
271
+ winners.append(c)
272
+ return sorted(list(winners))
273
+
274
+ def _beat_path_floyd_warshall(
275
+ edata,
276
+ curr_cands = None,
277
+ strength_function = None):
278
+ """An implementation of Beat Path using a variation of the Floyd-Warshall Algorithm
279
+ See https://en.wikipedia.org/wiki/Schulze_method#Implementation)
280
+
281
+ Args:
282
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
283
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
284
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
285
+
286
+ Returns:
287
+ A sorted list of candidates.
288
+
289
+ """
290
+
291
+ candidates = edata.candidates if curr_cands is None else curr_cands
292
+ strength_function = edata.margin if strength_function is None else strength_function
293
+
294
+ s_matrix = [[-np.inf for _ in candidates] for _ in candidates]
295
+ for c1_idx, c1 in enumerate(candidates):
296
+ for c2_idx, c2 in enumerate(candidates):
297
+ if (edata.majority_prefers(c1, c2) or c1 == c2):
298
+ s_matrix[c1_idx][c2_idx] = strength_function(c1, c2)
299
+ strength = list(map(lambda i : list(map(lambda j : j , i)) , s_matrix))
300
+ for i_idx, i in enumerate(candidates):
301
+ for j_idx, j in enumerate(candidates):
302
+ if i!= j:
303
+ for k_idx, k in enumerate(candidates):
304
+ if i!= k and j != k:
305
+ strength[j_idx][k_idx] = max(strength[j_idx][k_idx], min(strength[j_idx][i_idx],strength[i_idx][k_idx]))
306
+ winners = {i:True for i in candidates}
307
+ for i_idx, i in enumerate(candidates):
308
+ for j_idx, j in enumerate(candidates):
309
+ if i!=j:
310
+ if strength[j_idx][i_idx] > strength[i_idx][j_idx]:
311
+ winners[i] = False
312
+ return sorted([c for c in candidates if winners[c]])
313
+
314
+ def _schwartz_sequential_dropping(edata, curr_cands = None, strength_function = None):
315
+
316
+ """The Schwartz Sequential Dropping algorithm. See https://en.wikipedia.org/wiki/Schulze_method#Ties_and_alternative_implementations.
317
+
318
+ Args:
319
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
320
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
321
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
322
+
323
+ Returns:
324
+ A sorted list of candidates.
325
+ """
326
+ from pref_voting.c1_methods import gocha
327
+
328
+ strength_function = edata.margin if strength_function is None else strength_function
329
+
330
+ mg = edata if isinstance(edata, MarginGraph) else edata.margin_graph()
331
+ schwartz = gocha(mg, curr_cands = curr_cands)
332
+
333
+ if len(schwartz) == 1:
334
+ return schwartz
335
+
336
+ pos_schwartz_strengths = [strength_function(c,d) for c in schwartz for d in schwartz if strength_function(c,d) > 0]
337
+
338
+ if len(pos_schwartz_strengths) == 0:
339
+ return sorted(schwartz)
340
+
341
+ max_schwartz_strength = max(pos_schwartz_strengths)
342
+ min_schwartz_strength = min(pos_schwartz_strengths)
343
+
344
+ if max_schwartz_strength == min_schwartz_strength:
345
+ return sorted(schwartz)
346
+
347
+ else:
348
+ new_mg = MarginGraph(schwartz,[(c,d, strength_function(c,d)) for c in schwartz for d in schwartz if strength_function(c,d) > min_schwartz_strength])
349
+ return _schwartz_sequential_dropping(new_mg, schwartz)
350
+
351
+ @vm(name="Beat Path",
352
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
353
+ def beat_path(
354
+ edata,
355
+ curr_cands = None,
356
+ strength_function = None,
357
+ algorithm = 'floyd_warshall'):
358
+
359
+ """For candidates :math:`a` and :math:`b`, a **path** from :math:`a` to :math:`b` is a sequence
360
+ :math:`x_1, \ldots, x_n` of distinct candidates with :math:`x_1=a` and :math:`x_n=b` such that
361
+ for :math:`1\leq k\leq n-1`, :math:`x_k` is majority preferred to :math:`x_{k+1}`. The **strength of a path**
362
+ is the minimal margin along that path. Say that :math:`a` defeats :math:`b` according to Beat Path if the the strength of the strongest path from :math:`a` to :math:`b` is greater than the strength of the strongest path from :math:`b` to :math:`a`. Then the candidates that are undefeated according to Beat Path are the winners. Also known as the Schulze Rule.
363
+
364
+ Args:
365
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
366
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
367
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
368
+ algorithm (str): Specify which algorithm to use. Options are 'floyd_warshall' (the default), 'basic', and 'schwartz_sequential_dropping'.
369
+
370
+ Returns:
371
+ A sorted list of candidates.
372
+
373
+ .. seealso::
374
+
375
+ :meth:`pref_voting.margin_based_methods.beat_path_defeat`
376
+
377
+ :Example:
378
+
379
+ .. plot:: margin_graphs_examples/mg_ex_bp_rp.py
380
+ :context: reset
381
+ :include-source: True
382
+
383
+
384
+ .. code-block::
385
+
386
+ from pref_voting.margin_based_methods import beat_path
387
+
388
+ beat_path.display(mg)
389
+
390
+
391
+ .. exec_code::
392
+ :hide_code:
393
+
394
+ from pref_voting.weighted_majority_graphs import MarginGraph
395
+ from pref_voting.margin_based_methods import beat_path
396
+
397
+ mg = MarginGraph([0, 1, 2, 3], [(0, 2, 3), (1, 0, 5), (2, 1, 5), (2, 3, 1), (3, 0, 3), (3, 1, 1)])
398
+
399
+ beat_path.display(mg)
400
+ beat_path.display(mg, algorithm='floyd_warshall')
401
+ beat_path.display(mg, algorithm='basic')
402
+ """
403
+
404
+ if algorithm == 'floyd_warshall':
405
+ return _beat_path_floyd_warshall(edata, curr_cands = curr_cands, strength_function = strength_function)
406
+ elif algorithm == 'basic':
407
+ return _beat_path_basic(edata, curr_cands = curr_cands, strength_function = strength_function)
408
+ elif algorithm == 'schwartz_sequential_dropping':
409
+ return _schwartz_sequential_dropping(edata, curr_cands = curr_cands, strength_function = strength_function)
410
+ else:
411
+ raise ValueError("Invalid algorithm specified.")
412
+
413
+ def beat_path_defeat(edata, curr_cands = None, strength_function = None):
414
+ """Returns the defeat relation for Beat Path.
415
+
416
+ Args:
417
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
418
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
419
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
420
+
421
+ Returns:
422
+ A networkx DiGraph representing the Beat Path defeat relation.
423
+
424
+ .. seealso::
425
+
426
+ :meth:`pref_voting.margin_based_methods.beat_path`, :meth:`pref_voting.margin_based_methods.beat_path_Floyd_Warshall`
427
+
428
+ :Example:
429
+
430
+ .. plot:: margin_graphs_examples/mg_ex_bp_defeat.py
431
+ :context: reset
432
+ :include-source: True
433
+
434
+ """
435
+
436
+ candidates = edata.candidates if curr_cands is None else curr_cands
437
+ strength_function = edata.margin if strength_function is None else strength_function
438
+
439
+ s_matrix = [[-np.inf for _ in candidates] for _ in candidates]
440
+ for c1_idx, c1 in enumerate(candidates):
441
+ for c2_idx, c2 in enumerate(candidates):
442
+ if (edata.majority_prefers(c1, c2) or c1 == c2):
443
+ s_matrix[c1_idx][c2_idx] = strength_function(c1, c2)
444
+ strength = list(map(lambda i : list(map(lambda j : j , i)) , s_matrix))
445
+ for i_idx, i in enumerate(candidates):
446
+ for j_idx, j in enumerate(candidates):
447
+ if i!= j:
448
+ for k_idx, k in enumerate(candidates):
449
+ if i!= k and j != k:
450
+ strength[j_idx][k_idx] = max(strength[j_idx][k_idx], min(strength[j_idx][i_idx],strength[i_idx][k_idx]))
451
+
452
+ defeat_graph = nx.DiGraph()
453
+ defeat_graph.add_nodes_from(candidates)
454
+
455
+ for i_idx, i in enumerate(candidates):
456
+ for j_idx, j in enumerate(candidates):
457
+ if i!=j:
458
+ if strength[j_idx][i_idx] > strength[i_idx][j_idx]:
459
+ defeat_graph.add_weighted_edges_from([(j,i,s_matrix[j_idx][i_idx])])
460
+
461
+ return defeat_graph
462
+
463
+
464
+
465
+ def has_strong_path(A, source, target, k):
466
+ """Given a square matrix A, return True if there is a path from source to target in the associated directed graph where each edge has a weight greater than or equal to k, and False otherwise."""
467
+
468
+ n = A.shape[0] # assume A is a square matrix
469
+ visited = np.zeros(n, dtype=bool)
470
+
471
+ def dfs(node):
472
+ if node == target:
473
+ return True
474
+ visited[node] = True
475
+ for neighbor, weight in enumerate(A[node, :]):
476
+ if A[node][neighbor] > A[neighbor][node] and weight >= k and not visited[neighbor]:
477
+ if dfs(neighbor):
478
+ return True
479
+ return False
480
+
481
+ return dfs(source)
482
+
483
+ def _split_cycle_basic(
484
+ edata,
485
+ curr_cands = None,
486
+ strength_function = None):
487
+ """An implementation of Split Cycle based on the mathematical definition.
488
+ """
489
+ strength_matrix, cand_to_cindex = edata.strength_matrix(curr_cands = curr_cands, strength_function=strength_function)
490
+
491
+ candidates = edata.candidates if curr_cands is None else curr_cands
492
+
493
+ strength_function = edata.margin if strength_function is None else strength_function
494
+
495
+ potential_winners = set(candidates)
496
+
497
+ for a in candidates:
498
+ for b in candidates:
499
+ if strength_function(b, a) > strength_function(a, b) and not has_strong_path(strength_matrix, cand_to_cindex(a), cand_to_cindex(b), strength_function(b,a)):
500
+ potential_winners.discard(a)
501
+ break
502
+
503
+ return sorted(potential_winners)
504
+
505
+ def _is_cand_split_cycle_defeated(a, strength_matrix):
506
+
507
+ for b in range(strength_matrix.shape[0]):
508
+ if strength_matrix[b][a] > strength_matrix[a][b] and not has_strong_path(strength_matrix, a, b, strength_matrix[b][a]):
509
+ return True
510
+ return False
511
+
512
+
513
+ def batch(iterable, n=1):
514
+ l = len(iterable)
515
+ for ndx in range(0, l, n):
516
+ yield iterable[ndx:min(ndx + n, l)]
517
+
518
+ def process_batch_of_candidates(batch, strength_matrix):
519
+ results = []
520
+ for candidate in batch:
521
+ result = _is_cand_split_cycle_defeated(candidate, strength_matrix)
522
+ results.append(result)
523
+ return results
524
+
525
+ def _split_cycle_basic_parallel(strength_matrix, num_cpus=4):
526
+
527
+ num_cands = strength_matrix.shape[0]
528
+ cands = list(range(num_cands))
529
+ batch_size = num_cands // num_cpus + (num_cands % num_cpus > 0)
530
+ candidate_batches = list(batch(cands, batch_size))
531
+ with Pool(num_cpus) as pool:
532
+ batch_args = [(batch, strength_matrix)
533
+ for batch in candidate_batches]
534
+ results = pool.starmap(process_batch_of_candidates, batch_args)
535
+ # Flatten the list of results
536
+ sc_defeat_data = [item for sublist in results for item in sublist]
537
+
538
+ return sorted([c for c in cands if not sc_defeat_data[c]])
539
+
540
+ def _split_cycle_floyd_warshall(
541
+ edata,
542
+ curr_cands = None,
543
+ strength_function = None):
544
+ """An implementation of Split Cycle based on the Floyd-Warshall Algorithm.
545
+
546
+ See https://github.com/epacuit/splitcycle and the paper https://arxiv.org/abs/2004.02350 for more information.
547
+
548
+ """
549
+
550
+ candidates = edata.candidates if curr_cands is None else curr_cands
551
+ strength_function = edata.margin if strength_function is None else strength_function
552
+
553
+ weak_condorcet_winners = {c:True for c in candidates}
554
+ s_matrix = [[-np.inf for _ in candidates] for _ in candidates]
555
+
556
+ # initialize the s_matrix
557
+ for c1_idx, c1 in enumerate(candidates):
558
+ for c2_idx, c2 in enumerate(candidates):
559
+ if (edata.majority_prefers(c1, c2) or c1 == c2):
560
+ s_matrix[c1_idx][c2_idx] = strength_function(c1, c2)
561
+ weak_condorcet_winners[c2] = weak_condorcet_winners[c2] and (c1 == c2) # Weak Condorcet winners are Split Cycle winners
562
+
563
+ strength = list(map(lambda i : list(map(lambda j : j , i)) , s_matrix))
564
+ for i_idx, i in enumerate(candidates):
565
+ for j_idx, j in enumerate(candidates):
566
+ if i!= j:
567
+ if not weak_condorcet_winners[j]: # weak Condorcet winners are Split Cycle winners
568
+ for k_idx, k in enumerate(candidates):
569
+ if i != k and j != k:
570
+ strength[j_idx][k_idx] = max(strength[j_idx][k_idx], min(strength[j_idx][i_idx],strength[i_idx][k_idx]))
571
+ winners = {i:True for i in candidates}
572
+ for i_idx, i in enumerate(candidates):
573
+ for j_idx, j in enumerate(candidates):
574
+ if i != j:
575
+ if s_matrix[j_idx][i_idx] > strength[i_idx][j_idx]: # the main difference with Beat Path
576
+ winners[i] = False
577
+ return sorted([c for c in candidates if winners[c]])
578
+
579
+ @vm(name="Split Cycle",
580
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
581
+ def split_cycle(
582
+ edata,
583
+ curr_cands=None,
584
+ strength_function=None,
585
+ algorithm='basic',
586
+ num_cpus=4):
587
+
588
+ """A **majority cycle** is a sequence :math:`x_1, \ldots ,x_n` of distinct candidates with :math:`x_1=x_n` such that for :math:`1 \leq k \leq n-1`, :math:`x_k` is majority preferred to :math:`x_{k+1}`. The Split Cycle winners are determined as follows:
589
+
590
+ If candidate x has a positive margin over y and (x,y) is not the weakest edge in a cycle, then x defeats y. Equivalently, if x has a positive margin over y and there is no path from y back to x of strength at least the margin of x over y, then x defeats y.
591
+
592
+ The candidates that are undefeated are the Split Cycle winners.
593
+
594
+ See https://github.com/epacuit/splitcycle and the paper https://arxiv.org/abs/2004.02350 for more information.
595
+
596
+ Args:
597
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
598
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
599
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
600
+ algorithm (str): Specify which algorithm to use. Options are 'basic' (the default) and 'floyd_warshall'.
601
+
602
+ Returns:
603
+ A sorted list of candidates.
604
+
605
+ .. seealso::
606
+
607
+ :meth:`pref_voting.margin_based_methods.split_cycle_defeat`
608
+
609
+ :Example:
610
+
611
+ .. plot:: margin_graphs_examples/mg_ex_bp_rp.py
612
+ :context: reset
613
+ :include-source: True
614
+
615
+ .. code-block::
616
+
617
+ from pref_voting.margin_based_methods import split_cycle
618
+
619
+ split_cycle.display(mg)
620
+
621
+
622
+ .. exec_code::
623
+ :hide_code:
624
+
625
+ from pref_voting.weighted_majority_graphs import MarginGraph
626
+ from pref_voting.margin_based_methods import split_cycle
627
+
628
+ mg = MarginGraph([0, 1, 2, 3], [(0, 2, 3), (1, 0, 5), (2, 1, 5), (2, 3, 1), (3, 0, 3), (3, 1, 1)])
629
+
630
+ split_cycle.display(mg)
631
+ split_cycle.display(mg, algorithm='basic')
632
+ split_cycle.display(mg, algorithm='floyd_warshall')
633
+ """
634
+
635
+ if algorithm == 'basic':
636
+ return _split_cycle_basic(edata, curr_cands = curr_cands, strength_function = strength_function)
637
+ elif algorithm == 'floyd_warshall':
638
+ return _split_cycle_floyd_warshall(edata, curr_cands = curr_cands, strength_function = strength_function)
639
+ elif algorithm == 'basic_parallel':
640
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
641
+ strength_matrix, cand_to_cindex = edata.strength_matrix(curr_cands = curr_cands, strength_function=strength_function)
642
+ cindx_to_cand = {cand_to_cindex(c):c for c in curr_cands}
643
+ sc_ws = _split_cycle_basic_parallel(strength_matrix,num_cpus=num_cpus)
644
+ return sorted([cindx_to_cand[c] for c in sc_ws])
645
+ else:
646
+ raise ValueError("Invalid algorithm specified.")
647
+
648
+
649
+ def split_cycle_defeat(edata, curr_cands = None, strength_function = None):
650
+ """
651
+ Returns the Split Cycle defeat relation.
652
+
653
+ See https://arxiv.org/abs/2008.08451 for an extended discussion of this notion of defeat in an election.
654
+
655
+ Args:
656
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
657
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
658
+
659
+ Returns:
660
+ A networkx DiGraph representing the Split Cycle defeat relation.
661
+
662
+ .. seealso::
663
+
664
+ :meth:`pref_voting.margin_based_methods.split_cycle`, :meth:`pref_voting.margin_based_methods.split_cycle_Floyd_Warshall`
665
+
666
+ :Example:
667
+
668
+ .. plot:: margin_graphs_examples/mg_ex_sc_defeat.py
669
+ :context: reset
670
+ :include-source: True
671
+
672
+ """
673
+
674
+ candidates = edata.candidates if curr_cands is None else curr_cands
675
+ strength_function = edata.margin if strength_function is None else strength_function
676
+
677
+ weak_condorcet_winners = {c:True for c in candidates}
678
+ s_matrix = [[-np.inf for _ in candidates] for _ in candidates]
679
+
680
+ # initialize the s_matrix
681
+ for c1_idx, c1 in enumerate(candidates):
682
+ for c2_idx, c2 in enumerate(candidates):
683
+ if (edata.majority_prefers(c1, c2) or c1 == c2):
684
+ s_matrix[c1_idx][c2_idx] = strength_function(c1, c2)
685
+ weak_condorcet_winners[c2] = weak_condorcet_winners[c2] and (c1 == c2) # weak Condorcet winners are Split Cycle winners
686
+
687
+ strength = list(map(lambda i : list(map(lambda j : j , i)) , s_matrix))
688
+ for i_idx, i in enumerate(candidates):
689
+ for j_idx, j in enumerate(candidates):
690
+ if i!= j:
691
+ if not weak_condorcet_winners[j]: # weak Condorcet winners are Split Cycle winners
692
+ for k_idx, k in enumerate(candidates):
693
+ if i != k and j != k:
694
+ strength[j_idx][k_idx] = max(strength[j_idx][k_idx], min(strength[j_idx][i_idx],strength[i_idx][k_idx]))
695
+
696
+ defeat_graph = nx.DiGraph()
697
+ defeat_graph.add_nodes_from(candidates)
698
+
699
+ for i_idx, i in enumerate(candidates):
700
+ for j_idx, j in enumerate(candidates):
701
+ if i != j:
702
+ if s_matrix[j_idx][i_idx] > strength[i_idx][j_idx]: # the main difference with Beat Path
703
+ defeat_graph.add_weighted_edges_from([(j,i,s_matrix[j_idx][i_idx])])
704
+
705
+ return defeat_graph
706
+
707
+
708
+ # flatten a 2d list - turn a 2d list into a single list of items
709
+ flatten = lambda l: [item for sublist in l for item in sublist]
710
+
711
+ def does_create_cycle(g, edge):
712
+ '''return True if adding the edge to g create a cycle.
713
+ it is assumed that edge is already in g'''
714
+ source = edge[0]
715
+ target = edge[1]
716
+ for n in g.predecessors(source):
717
+ if nx.has_path(g, target, n):
718
+ return True
719
+ return False
720
+
721
+
722
+
723
+ def powerset(iterable):
724
+ """
725
+ Return the powerset of ``iterable``
726
+
727
+ powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)
728
+ """
729
+ s = list(iterable)
730
+ return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
731
+
732
+
733
+ def is_stack(edata, cand_list, curr_cands=None):
734
+ """
735
+ A **stack** is a linear order :math:`L` on the candidate such that for all candidates :math:`a` and :math:`b`, if :math:`aLb`, then there are distinct candidates :math:`x_1,\dots,x_n` with :math:`x_1=a` and :math:`x_n=b` such that :math:`x_i L x_{i+1}` and for all :math:`i\in \{1,\dots, n-1\}`, the margin of :math:`x_1` over :math:`x_{i+1}` is greater than or equal to the margin of :math:`b` over :math:`a`.
736
+
737
+ This definition is due to Zavist and Tideman 1989, and is used as an alternative characterization of Ranked Pairs: :math:`a` is a Ranked Pairs winner if and only if :math:`a` is the maximum element of some stack.
738
+
739
+ Args:
740
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
741
+ cand_list (list): The list of candidates that may be a stack
742
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
743
+
744
+ Returns:
745
+ True if ``cand_list`` is a stack and False otherwise
746
+
747
+ :Example:
748
+
749
+ .. plot:: margin_graphs_examples/mg_ex_rp_stacks.py
750
+ :context: reset
751
+ :include-source: True
752
+
753
+
754
+ .. exec_code::
755
+
756
+ from pref_voting.weighted_majority_graphs import MarginGraph
757
+ from pref_voting.margin_based_methods import is_stack
758
+ from itertools import permutations
759
+
760
+ mg = MarginGraph([0, 1, 2], [(0, 1, 2), (1, 2, 4), (2, 0, 2)])
761
+
762
+ for clist in permutations(mg.candidates):
763
+ print(f"{clist} {'is' if is_stack(mg, clist) else 'is not'} a stack")
764
+
765
+ """
766
+
767
+ candidates = curr_cands if curr_cands is not None else edata.candidates
768
+ cand_pairs = [(a, b) if cand_list.index(a) < cand_list.index(b) else (b, a) for a, b in combinations(candidates, 2)]
769
+
770
+ for a, b in cand_pairs:
771
+ other_cands = [c for c in candidates if c != a and c != b]
772
+ found_path = False
773
+
774
+ sublist = cand_list[cand_list.index(a) + 1:cand_list.index(b)]
775
+
776
+ for indices in powerset(range(len(sublist))):
777
+
778
+ path = [a] + [sublist[i] for i in sorted(indices)] + [b]
779
+ margins = [edata.margin(xi, path[i + 1]) for i, xi in enumerate(path[0:-1])]
780
+ if all([cand_list.index(xi) < cand_list.index(path[i+1]) for i, xi in enumerate(path[0:-1])]) and all([m >= edata.margin(b, a) for m in margins]):
781
+ found_path = True
782
+ break
783
+ if not found_path:
784
+ return False
785
+ return True
786
+
787
+ def _ranked_pairs_from_stacks(edata, curr_cands = None):
788
+ """Find the Ranked Pairs winners by iterating over all permutations of candidates (restricted to ``curr_cands`` if not None), and checking if the list is a stack.
789
+
790
+ Args:
791
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
792
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
793
+
794
+ Returns:
795
+ A sorted list of candidates.
796
+
797
+ .. seealso::
798
+
799
+ :meth:`pref_voting.margin_based_methods.is_stack`
800
+
801
+
802
+ """
803
+
804
+ candidates = curr_cands if curr_cands is not None else edata.candidates
805
+ winners = list()
806
+ for clist in permutations(candidates):
807
+ isstack = is_stack(edata, clist, curr_cands = curr_cands)
808
+ if isstack:
809
+ winners.append(clist[0])
810
+
811
+ return sorted(list(set(winners)))
812
+
813
+ def _ranked_pairs_basic(
814
+ edata,
815
+ curr_cands = None,
816
+ strength_function = None):
817
+ """An implementation of Ranked Pairs that uses a basic algorithm.
818
+
819
+ Args:
820
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
821
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
822
+
823
+ Returns:
824
+ A sorted list of candidates.
825
+
826
+ """
827
+ candidates = edata.candidates if curr_cands is None else curr_cands
828
+ cidx_to_cand = {cidx: c for cidx, c in enumerate(candidates)}
829
+ cand_to_cidx = {c: cidx for cidx, c in enumerate(candidates)}
830
+ strength_function = edata.margin if strength_function is None else strength_function
831
+
832
+ cw = edata.condorcet_winner(curr_cands=curr_cands)
833
+ # Ranked Pairs is Condorcet consistent, so simply return the Condorcet winner if exists
834
+ if cw is not None:
835
+ winners = [cw]
836
+ else:
837
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates
838
+ if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
839
+ winners = list()
840
+ if len(w_edges) > 0:
841
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
842
+ sorted_edges = [[e for e in w_edges if e[2] == s] for s in strengths]
843
+ tbs = product(*[permutations(edges) for edges in sorted_edges])
844
+ for tb in tbs:
845
+ edges = flatten(tb)
846
+ rp_defeat = SPO(len(candidates))
847
+ for e0,e1,s in edges:
848
+ if not rp_defeat.P[cand_to_cidx[e1]][cand_to_cidx[e0]]:
849
+ rp_defeat.add(cand_to_cidx[e0],cand_to_cidx[e1])
850
+ winners.append(cidx_to_cand[rp_defeat.initial_elements()[0]])
851
+ else:
852
+ winners = candidates
853
+ return sorted(list(set(winners)))
854
+
855
+
856
+ @vm(name="Ranked Pairs",
857
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
858
+ def ranked_pairs(
859
+ edata,
860
+ curr_cands=None,
861
+ strength_function=None,
862
+ algorithm='basic'):
863
+ """
864
+ Order the edges in the margin graph from largest to smallest and lock them in in that order, skipping edges that create a cycle. If there are ties in the margins, break the ties using a tie-breaking rule: a linear ordering over the edges. A candidate is a Ranked Pairs winner if it wins according to some tie-breaking rule. Also known as Tideman's Rule.
865
+
866
+ .. warning::
867
+ This method can take a very long time to find winners.
868
+
869
+ Args:
870
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
871
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
872
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
873
+ algorithm (str, optional): Specify which algorithm to use. Options are 'basic' (the default) and 'from_stacks'.
874
+
875
+ Returns:
876
+ A sorted list of candidates.
877
+
878
+ .. seealso::
879
+
880
+ :meth:`pref_voting.margin_based_methods.ranked_pairs_with_test`, :meth:`pref_voting.margin_based_methods.ranked_pairs_zt`, :meth:`pref_voting.margin_based_methods.ranked_pairs_defeats`
881
+
882
+ :Example:
883
+
884
+ .. plot:: margin_graphs_examples/mg_ex_bp_rp.py
885
+ :context: reset
886
+ :include-source: True
887
+
888
+
889
+ .. code-block::
890
+
891
+ from pref_voting.margin_based_methods import ranked_pairs
892
+
893
+ ranked_pairs.display(mg)
894
+ ranked_pairs.display(mg, algorithm='basic')
895
+ ranked_pairs.display(mg, algorithm='from_stacks')
896
+
897
+
898
+ .. exec_code::
899
+ :hide_code:
900
+
901
+ from pref_voting.weighted_majority_graphs import MarginGraph
902
+ from pref_voting.margin_based_methods import ranked_pairs
903
+
904
+ mg = MarginGraph([0, 1, 2, 3], [(0, 2, 3), (1, 0, 5), (2, 1, 5), (2, 3, 1), (3, 0, 3), (3, 1, 1)])
905
+
906
+ ranked_pairs.display(mg)
907
+ ranked_pairs.display(mg, algorithm='basic')
908
+ ranked_pairs.display(mg, algorithm='from_stacks')
909
+
910
+ """
911
+
912
+ if algorithm == 'basic':
913
+ return _ranked_pairs_basic(edata, curr_cands = curr_cands, strength_function = strength_function)
914
+ elif algorithm == 'from_stacks':
915
+ return _ranked_pairs_from_stacks(edata, curr_cands = curr_cands)
916
+ else:
917
+ raise ValueError("Invalid algorithm specified.")
918
+
919
+ @vm(name="Ranked Pairs",
920
+ skip_registration=True)
921
+ def ranked_pairs_with_test(
922
+ edata,
923
+ curr_cands=None,
924
+ strength_function=None):
925
+ """Find the Ranked Pairs winners, but include a test to determined if it will take too long to compute the Ranked Pairs winners. If the calculation of the winners will take too long, return None.
926
+
927
+ .. important::
928
+ This voting method that might return None rather than a list of candidates.
929
+
930
+ Args:
931
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
932
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
933
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
934
+
935
+ Returns:
936
+ A sorted list of candidates.
937
+
938
+ .. seealso::
939
+
940
+ :meth:`pref_voting.margin_based_methods.ranked_pairs_with_test`, :meth:`pref_voting.margin_based_methods.ranked_pairs_zt`, :meth:`pref_voting.margin_based_methods.ranked_pairs_defeats`
941
+
942
+ :Example:
943
+
944
+ .. plot:: margin_graphs_examples/mg_ex_rp_with_t.py
945
+ :context: reset
946
+ :include-source: True
947
+
948
+
949
+ .. code-block::
950
+
951
+ from pref_voting.margin_based_methods import ranked_pairs_with_test
952
+
953
+ ranked_pairs_with_test.display(mg)
954
+
955
+
956
+ .. exec_code::
957
+ :hide_code:
958
+
959
+ from pref_voting.weighted_majority_graphs import MarginGraph
960
+ from pref_voting.margin_based_methods import ranked_pairs_with_test
961
+
962
+ mg = MarginGraph([0, 1, 2, 3], [(1, 2, 2), (1, 3, 2), (2, 0, 2)])
963
+
964
+ ranked_pairs_with_test.display(mg)
965
+
966
+
967
+ """
968
+ candidates = edata.candidates if curr_cands is None else curr_cands
969
+ cidx_to_cand = {cidx: c for cidx, c in enumerate(candidates)}
970
+ cand_to_cidx = {c: cidx for cidx, c in enumerate(candidates)}
971
+
972
+ strength_function = edata.margin if strength_function is None else strength_function
973
+
974
+ cw = edata.condorcet_winner(curr_cands = curr_cands)
975
+ # Ranked Pairs is Condorcet consistent, so simply return the Condorcet winner if exists
976
+ if cw is not None:
977
+ winners = [cw]
978
+ else:
979
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates
980
+ if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
981
+ winners = list()
982
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
983
+ sorted_edges = [[e for e in w_edges if e[2] == s] for s in strengths]
984
+ if np.prod([math.factorial(len(es)) for es in sorted_edges]) > 3000:
985
+ return None
986
+ else:
987
+ tbs = product(*[permutations(edges) for edges in sorted_edges])
988
+ for tb in tbs:
989
+ edges = flatten(tb)
990
+ rp_defeat = SPO(len(candidates))
991
+ for e0,e1,s in edges:
992
+ if not rp_defeat.P[cand_to_cidx[e1]][cand_to_cidx[e0]]:
993
+ rp_defeat.add(cand_to_cidx[e0],cand_to_cidx[e1])
994
+ winners.append(cidx_to_cand[rp_defeat.initial_elements()[0]])
995
+ return sorted(list(set(winners)))
996
+
997
+ def ranked_pairs_defeats(edata, curr_cands = None, strength_function = None, add_reverse_of_removed_edges = False):
998
+ """
999
+ Returns the Ranked Pairs defeat relations produced by the Ranked Pairs algorithm.
1000
+
1001
+ If add_reverse_of_removed_edges is True, we add the reverse of any majority edge that is removed during the Ranked Pairs algorithm. Otherwise, we do not add the reverse of any majority edge that is removed.
1002
+
1003
+ .. important::
1004
+ Unlike the other functions that return a single defeat relation, this returns a list of defeat relations.
1005
+
1006
+ Args:
1007
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1008
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1009
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1010
+
1011
+ Returns:
1012
+ A list of networkx DiGraphs representing the Ranked Pairs defeat relations.
1013
+
1014
+ .. seealso::
1015
+
1016
+ :meth:`pref_voting.margin_based_methods.ranked_pairs`, :meth:`pref_voting.margin_based_methods.ranked_pairs_with_test`
1017
+
1018
+ :Example:
1019
+
1020
+ .. plot:: margin_graphs_examples/mg_ex_rp_defeats.py
1021
+ :context: reset
1022
+ :include-source: True
1023
+
1024
+ .. exec_code::
1025
+
1026
+ from pref_voting.weighted_majority_graphs import MarginGraph
1027
+ from pref_voting.margin_based_methods import ranked_pairs_defeats
1028
+
1029
+ mg = MarginGraph([0, 1, 2, 3], [(0, 1, 10), (0, 2, 2), (1, 3, 4), (2, 1, 6), (2, 3, 8), (3, 0, 4)])
1030
+ rp_defeats = ranked_pairs_defeats(mg)
1031
+
1032
+ for rpd in rp_defeats:
1033
+ print(rpd.edges)
1034
+
1035
+ """
1036
+
1037
+ candidates = edata.candidates if curr_cands is None else curr_cands
1038
+ strength_function = edata.margin if strength_function is None else strength_function
1039
+
1040
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
1041
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
1042
+ sorted_edges = [[e for e in w_edges if e[2] == s] for s in strengths]
1043
+ tbs = product(*[permutations(edges) for edges in sorted_edges])
1044
+ rp_defeats = list()
1045
+ for tb in tbs:
1046
+ edges = flatten(tb)
1047
+ rp_defeat = nx.DiGraph()
1048
+ for e in edges:
1049
+ rp_defeat.add_edge(e[0], e[1])
1050
+ if does_create_cycle(rp_defeat, e):
1051
+ rp_defeat.remove_edge(e[0], e[1])
1052
+ if add_reverse_of_removed_edges:
1053
+ rp_defeat.add_edge(e[1], e[0])
1054
+
1055
+ rp_defeats.append(rp_defeat)
1056
+ return rp_defeats
1057
+
1058
+ @vm(name="Ranked Pairs TB",
1059
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
1060
+ def ranked_pairs_tb(
1061
+ edata,
1062
+ curr_cands = None,
1063
+ tie_breaker = None,
1064
+ strength_function = None):
1065
+ """
1066
+ Ranked Pairs with a fixed linear order on the candidates to break any ties in the margins.
1067
+ Since the tie_breaker is a linear order, this method is resolute.
1068
+ If no tie_breaker is provided, then the tie_breaker is the sorted list of candidates.
1069
+
1070
+ Args:
1071
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1072
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``.
1073
+ tie_breaker (List): A linear order on the candidates to break any ties in the margins. If not provided, then the tie_breaker is the sorted list of candidates.
1074
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1075
+
1076
+ Returns:
1077
+ A sorted list of candidates.
1078
+
1079
+ .. seealso::
1080
+
1081
+ :meth:`pref_voting.margin_based_methods.ranked_pairs`, :meth:`pref_voting.margin_based_methods.ranked_pairs_with_test`
1082
+
1083
+ .. exec_code::
1084
+
1085
+ from pref_voting.profiles import Profile
1086
+ from pref_voting.margin_based_methods import ranked_pairs_tb, ranked_pairs_zt
1087
+
1088
+ prof = Profile([[2, 3, 1, 0], [0, 3, 1, 2], [1, 3, 2, 0], [2, 1, 3, 0]], [1, 1, 1, 1])
1089
+
1090
+ prof.display()
1091
+
1092
+ ranked_pairs_tb.display(prof)
1093
+ ranked_pairs_tb.display(prof, tie_breaker = [3, 2, 1, 0])
1094
+ ranked_pairs_zt.display(prof)
1095
+
1096
+ """
1097
+
1098
+ candidates = edata.candidates if curr_cands is None else curr_cands
1099
+ cidx_to_cand = {cidx: c for cidx, c in enumerate(candidates)}
1100
+ cand_to_cidx = {c: cidx for cidx, c in enumerate(candidates)}
1101
+
1102
+ strength_function = edata.margin if strength_function is None else strength_function
1103
+
1104
+ tb_ranking = tie_breaker if tie_breaker is not None else sorted(list(candidates))
1105
+
1106
+ cw = edata.condorcet_winner(curr_cands=curr_cands)
1107
+ # Ranked Pairs is Condorcet consistent, so simply return the Condorcet winner if exists
1108
+ if cw is not None:
1109
+ winners = [cw]
1110
+ else:
1111
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates
1112
+ if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
1113
+ winners = list()
1114
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
1115
+
1116
+ rp_defeat = SPO(len(candidates))
1117
+
1118
+ for s in strengths:
1119
+ edges = [e for e in w_edges if e[2] == s]
1120
+
1121
+ # break ties using the lexicographic ordering on tuples given tb_ranking
1122
+ sorted_edges = sorted(edges, key = lambda e: (tb_ranking.index(e[0]), tb_ranking.index(e[1])), reverse=False)
1123
+ for e0,e1,s in sorted_edges:
1124
+ if not rp_defeat.P[cand_to_cidx[e1]][cand_to_cidx[e0]]:
1125
+ rp_defeat.add(cand_to_cidx[e0],cand_to_cidx[e1])
1126
+ winners.append(cidx_to_cand[rp_defeat.initial_elements()[0]])
1127
+
1128
+ return sorted(list(set(winners)))
1129
+
1130
+ @vm(name="Ranked Pairs ZT",
1131
+ input_types=[ElectionTypes.PROFILE])
1132
+ def ranked_pairs_zt(
1133
+ profile,
1134
+ curr_cands = None,
1135
+ strength_function = None):
1136
+ """Ranked pairs where a fixed voter breaks any ties in the margins. It is always the voter in position 0 that breaks the ties. Since voters have strict preferences, this method is resolute. This is known as Ranked Pairs ZT, for Zavist Tideman.
1137
+
1138
+ Args:
1139
+ edata (Profile): A profile of linear orders
1140
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1141
+
1142
+ Returns:
1143
+ A sorted list of candidates.
1144
+
1145
+ .. seealso::
1146
+
1147
+ :meth:`pref_voting.margin_based_methods.ranked_pairs`, :meth:`pref_voting.margin_based_methods.ranked_pairs_with_test`
1148
+
1149
+ .. exec_code::
1150
+
1151
+ from pref_voting.profiles import Profile
1152
+ from pref_voting.margin_based_methods import ranked_pairs_tb, ranked_pairs_zt
1153
+
1154
+ prof = Profile([[2, 3, 1, 0], [0, 3, 1, 2], [1, 3, 2, 0], [2, 1, 3, 0]], [1, 1, 1, 1])
1155
+
1156
+ prof.display()
1157
+
1158
+ ranked_pairs_tb.display(prof)
1159
+ ranked_pairs_tb.display(prof, tie_breaker = [3, 2, 1, 0])
1160
+ ranked_pairs_zt.display(prof)
1161
+
1162
+
1163
+ """
1164
+ candidates = profile.candidates if curr_cands is None else curr_cands
1165
+
1166
+ # the tie-breaker is always the first voter.
1167
+ tb_ranking = tuple([c for c in list(profile._rankings[0]) if c in candidates])
1168
+
1169
+ return ranked_pairs_tb(profile, curr_cands = curr_cands, tie_breaker = tb_ranking, strength_function = strength_function)
1170
+
1171
+ def ranked_pairs_defeat_tb(edata, curr_cands = None, tie_breaker = None, strength_function = None, return_list = False):
1172
+ """
1173
+ Returns the Ranked Pairs defeat relation produced by the Ranked Pairs algorithm with a fixed tie-breaker.
1174
+
1175
+ If no tie_breaker is provided, then the tie_breaker is the sorted list of candidates.
1176
+
1177
+ If return_list is True, then return the defeat relation as a list instead of a DiGraph.
1178
+
1179
+ Args:
1180
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1181
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1182
+ tie_breaker (List): A linear order on the candidates to break any ties in the margins. If not provided, then the tie_breaker is the sorted list of candidates.
1183
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1184
+ return_list (bool, optional): If True, then return a list. If False, return a networkx DiGraph.
1185
+
1186
+ Returns:
1187
+ A networkx DiGraph representing the Ranked Pairs defeat relation.
1188
+
1189
+ """
1190
+
1191
+ candidates = edata.candidates if curr_cands is None else curr_cands
1192
+ cidx_to_cand = {cidx: c for cidx, c in enumerate(candidates)}
1193
+ cand_to_cidx = {c: cidx for cidx, c in enumerate(candidates)}
1194
+
1195
+ strength_function = edata.margin if strength_function is None else strength_function
1196
+
1197
+ tb_ranking = tie_breaker if tie_breaker is not None else sorted(list(candidates))
1198
+
1199
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates
1200
+ if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
1201
+
1202
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
1203
+
1204
+ rp_defeat = SPO(len(candidates))
1205
+
1206
+ for s in strengths:
1207
+ edges = [e for e in w_edges if e[2] == s]
1208
+ # break ties using the lexicographic ordering on tuples given tb_ranking
1209
+ sorted_edges = sorted(edges, key = lambda e: (tb_ranking.index(e[0]), tb_ranking.index(e[1])), reverse=False)
1210
+ for e0,e1,s in sorted_edges:
1211
+ if not rp_defeat.P[cand_to_cidx[e1]][cand_to_cidx[e0]]:
1212
+ rp_defeat.add(cand_to_cidx[e0],cand_to_cidx[e1])
1213
+
1214
+ if return_list:
1215
+ return rp_defeat.to_list(cmap = cidx_to_cand)
1216
+ else:
1217
+ return rp_defeat.to_networkx(cmap = cidx_to_cand)
1218
+
1219
+ @vm(name="River",
1220
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
1221
+ def river(edata, curr_cands = None, strength_function = None):
1222
+ """
1223
+ Order the edges in the weak margin graph from largest to smallest and lock them in in that order, skipping edges that create a cycle *and edges in which there is already an edge pointing to the target*. Break ties using a tie-breaking linear ordering over the edges. A candidate is a River winner if it wins according to some tie-breaking rule. See https://electowiki.org/wiki/River.
1224
+
1225
+ Args:
1226
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1227
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1228
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1229
+
1230
+ Returns:
1231
+ A sorted list of candidates.
1232
+
1233
+ :Example:
1234
+
1235
+ .. exec_code::
1236
+
1237
+ from pref_voting.weighted_majority_graphs import MarginGraph
1238
+ from pref_voting.margin_based_methods import river, ranked_pairs
1239
+
1240
+ mg = MarginGraph([0, 1, 2, 3], [(0, 2, 2), (0, 3, 8), (1, 0, 12), (2, 3, 12), (3, 1, 6)])
1241
+
1242
+ ranked_pairs.display(mg)
1243
+ river.display(mg)
1244
+
1245
+ """
1246
+ candidates = edata.candidates if curr_cands is None else curr_cands
1247
+ cidx_to_cand = {cidx: c for cidx, c in enumerate(candidates)}
1248
+ cand_to_cidx = {c: cidx for cidx, c in enumerate(candidates)}
1249
+
1250
+ strength_function = edata.margin if strength_function is None else strength_function
1251
+
1252
+ cw = edata.condorcet_winner(curr_cands=curr_cands)
1253
+ # Ranked Pairs is Condorcet consistent, so simply return the Condorcet winner if exists
1254
+ if cw is not None:
1255
+ winners = [cw]
1256
+ else:
1257
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates
1258
+ if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
1259
+ winners = list()
1260
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
1261
+ sorted_edges = [[e for e in w_edges if e[2] == s] for s in strengths]
1262
+ tbs = product(*[permutations(edges) for edges in sorted_edges])
1263
+ for tb in tbs:
1264
+ edges = flatten(tb)
1265
+ rv_defeat = SPO(len(candidates))
1266
+ for e0,e1,s in edges:
1267
+ if not rv_defeat.P[cand_to_cidx[e1]][cand_to_cidx[e0]] and len(rv_defeat.preds[cand_to_cidx[e1]]) == 0:
1268
+ rv_defeat.add(cand_to_cidx[e0],cand_to_cidx[e1])
1269
+ winners.append(cidx_to_cand[rv_defeat.initial_elements()[0]])
1270
+
1271
+ return sorted(list(set(winners)))
1272
+
1273
+ def river_defeats(edata, curr_cands = None, strength_function = None):
1274
+ """
1275
+ Returns the River defeat relations produced by the River algorithm.
1276
+
1277
+ .. important::
1278
+ Unlike the other functions that return a single defeat relation, this returns a list of defeat relations.
1279
+
1280
+ Args:
1281
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1282
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1283
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1284
+
1285
+ Returns:
1286
+ A networkx DiGraph representing the River defeat relation.
1287
+ """
1288
+
1289
+ candidates = edata.candidates if curr_cands is None else curr_cands
1290
+ strength_function = edata.margin if strength_function is None else strength_function
1291
+
1292
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
1293
+
1294
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
1295
+ sorted_edges = [[e for e in w_edges if e[2] == s] for s in strengths]
1296
+ tbs = product(*[permutations(edges) for edges in sorted_edges])
1297
+
1298
+ river_defeats = list()
1299
+ for tb in tbs:
1300
+ edges = flatten(tb)
1301
+ river_defeat = nx.DiGraph()
1302
+ for e in edges:
1303
+ if e[1] not in river_defeat.nodes or len(list(river_defeat.in_edges(e[1]))) == 0:
1304
+ river_defeat.add_edge(e[0], e[1], weight=e[2])
1305
+ if does_create_cycle(river_defeat, e):
1306
+ river_defeat.remove_edge(e[0], e[1])
1307
+
1308
+ river_defeats.append(river_defeat)
1309
+
1310
+ return river_defeats
1311
+
1312
+ @vm(name="River",
1313
+ skip_registration=True)
1314
+ def river_with_test(edata, curr_cands = None, strength_function = None):
1315
+ """Find the River winners with a test to determined if it will take too long to compute the River winners. If the calculation of the winners will take too long, return None.
1316
+
1317
+ .. important::
1318
+ This voting method that might return None rather than a list of candidates.
1319
+
1320
+ Args:
1321
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1322
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1323
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1324
+
1325
+ Returns:
1326
+ A sorted list of candidates.
1327
+
1328
+ .. seealso::
1329
+
1330
+ :meth:`pref_voting.margin_based_methods.ranked_pairs_with_test`, :meth:`pref_voting.margin_based_methods.river`
1331
+
1332
+ """
1333
+ candidates = edata.candidates if curr_cands is None else curr_cands
1334
+ cidx_to_cand = {cidx: c for cidx, c in enumerate(candidates)}
1335
+ cand_to_cidx = {c: cidx for cidx, c in enumerate(candidates)}
1336
+
1337
+ strength_function = edata.margin if strength_function is None else strength_function
1338
+
1339
+ cw = edata.condorcet_winner(curr_cands=curr_cands)
1340
+ # Ranked Pairs is Condorcet consistent, so simply return the Condorcet winner if exists
1341
+ if cw is not None:
1342
+ winners = [cw]
1343
+ else:
1344
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates
1345
+ if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
1346
+ winners = list()
1347
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
1348
+ sorted_edges = [[e for e in w_edges if e[2] == s] for s in strengths]
1349
+ if np.prod([math.factorial(len(es)) for es in sorted_edges]) > 3000:
1350
+ return None
1351
+ else:
1352
+ tbs = product(*[permutations(edges) for edges in sorted_edges])
1353
+ for tb in tbs:
1354
+ edges = flatten(tb)
1355
+ rv_defeat = SPO(len(candidates))
1356
+ for e0,e1,s in edges:
1357
+ if not rv_defeat.P[cand_to_cidx[e1]][cand_to_cidx[e0]] and len(rv_defeat.preds[cand_to_cidx[e1]]) == 0:
1358
+ rv_defeat.add(cand_to_cidx[e0],cand_to_cidx[e1])
1359
+ winners.append(cidx_to_cand[rv_defeat.initial_elements()[0]])
1360
+ return sorted(list(set(winners)))
1361
+
1362
+ @vm(name="River TB",
1363
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
1364
+ def river_tb(edata, curr_cands = None, tie_breaker = None, strength_function = None):
1365
+ """
1366
+ River with a fixed linear order on the candidates to break any ties in the margins. Since the tie_breaker is a linear order, this method is resolute.
1367
+
1368
+ Args:
1369
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1370
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1371
+ tie_breaker (List[int], optional): A linear order on the candidates. If not set, then the candidates are sorted in ascending order.
1372
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1373
+
1374
+ Returns:
1375
+ A sorted list of candidates.
1376
+
1377
+ """
1378
+ candidates = edata.candidates if curr_cands is None else curr_cands
1379
+ cidx_to_cand = {cidx: c for cidx, c in enumerate(candidates)}
1380
+ cand_to_cidx = {c: cidx for cidx, c in enumerate(candidates)}
1381
+ strength_function = edata.margin if strength_function is None else strength_function
1382
+
1383
+ tb_ranking = tie_breaker if tie_breaker is not None else sorted(list(candidates))
1384
+
1385
+ cw = edata.condorcet_winner(curr_cands=curr_cands)
1386
+ # River is Condorcet consistent, so simply return the Condorcet winner if exists
1387
+ if cw is not None:
1388
+ winners = [cw]
1389
+ else:
1390
+ w_edges = [(c1, c2, strength_function(c1, c2)) for c1 in candidates for c2 in candidates if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))]
1391
+ winners = list()
1392
+ strengths = sorted(list(set([e[2] for e in w_edges])), reverse=True)
1393
+
1394
+ rv_defeat = SPO(len(candidates))
1395
+
1396
+ for s in strengths:
1397
+ edges = [e for e in w_edges if e[2] == s]
1398
+
1399
+ # break ties using the lexicographic ordering on tuples given tb_ranking
1400
+ sorted_edges = sorted(edges, key = lambda e: (tb_ranking.index(e[0]), tb_ranking.index(e[1])), reverse=False)
1401
+ for e0,e1,s in sorted_edges:
1402
+ if not rv_defeat.P[cand_to_cidx[e1]][cand_to_cidx[e0]] and len(rv_defeat.preds[cand_to_cidx[e1]]) == 0:
1403
+ rv_defeat.add(cand_to_cidx[e0],cand_to_cidx[e1])
1404
+ winners.append(cidx_to_cand[rv_defeat.initial_elements()[0]])
1405
+ return sorted(list(set(winners)))
1406
+
1407
+ @vm(name="River ZT",
1408
+ input_types=[ElectionTypes.PROFILE])
1409
+ def river_zt(profile, curr_cands = None, strength_function = None):
1410
+ """River where a fixed voter breaks any ties in the margins. It is always the voter in position 0 that breaks the ties. Since voters have strict preferences, this method is resolute.
1411
+
1412
+ Args:
1413
+ edata (Profile): A profile of linear orders
1414
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1415
+
1416
+ Returns:
1417
+ A sorted list of candidates.
1418
+
1419
+ .. seealso::
1420
+
1421
+ :meth:`pref_voting.margin_based_methods.river`, :meth:`pref_voting.margin_based_methods.river_with_test`, :meth:`pref_voting.margin_based_methods.ranked_pairs`
1422
+
1423
+
1424
+ """
1425
+ candidates = profile.candidates if curr_cands is None else curr_cands
1426
+
1427
+ # the tie-breaker is always the first voter.
1428
+ tb_ranking = tuple([c for c in list(profile._rankings[0]) if c in candidates])
1429
+
1430
+ return river_tb(profile, curr_cands = curr_cands, tie_breaker = tb_ranking, strength_function = strength_function)
1431
+
1432
+
1433
+ # Simple Stable Voting
1434
+ def _simple_stable_voting(curr_cands,
1435
+ sorted_matches,
1436
+ mem_sv_winners):
1437
+ '''
1438
+ Determine the Simple Stable Voting winners while keeping track
1439
+ of the winners in any subprofiles checked during computation.
1440
+ '''
1441
+
1442
+ sv_winners = list()
1443
+
1444
+ if len(curr_cands) == 1:
1445
+ mem_sv_winners[tuple(curr_cands)] = curr_cands
1446
+ return curr_cands, mem_sv_winners
1447
+
1448
+ margin_witnessing_win = -math.inf
1449
+
1450
+ for a, b, s in sorted_matches:
1451
+ if s < margin_witnessing_win:
1452
+ break
1453
+ if a not in sv_winners:
1454
+ cands_minus_b = [c for c in curr_cands if c != b]
1455
+ cands_minus_b_key = tuple(sorted(cands_minus_b))
1456
+ if cands_minus_b_key not in mem_sv_winners.keys():
1457
+ ws, mem_sv_winners = _simple_stable_voting(curr_cands = cands_minus_b,
1458
+ sorted_matches = [(a, c, s) for a, c, s in sorted_matches if a != b and c != b],
1459
+ mem_sv_winners = mem_sv_winners)
1460
+ mem_sv_winners[cands_minus_b_key] = ws
1461
+ else:
1462
+ ws = mem_sv_winners[cands_minus_b_key]
1463
+ if a in ws:
1464
+ sv_winners.append(a)
1465
+ margin_witnessing_win = s
1466
+
1467
+ return sv_winners, mem_sv_winners
1468
+
1469
+
1470
+ @vm(name = "Simple Stable Voting")
1471
+ def _simple_stable_voting_with_condorcet_check(
1472
+ edata,
1473
+ curr_cands = None,
1474
+ strength_function = None):
1475
+ """Simple Stable Voting is Condorcet consistent. It is faster to skip executing the recursive algorithm when there is a Condorcet winnerFirst check if there is a Condorcet winner. If so, return the Condorcet winner, otherwise find the Simple Stable Voting winner using _simple_stable_voting
1476
+
1477
+ Args:
1478
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1479
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1480
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1481
+
1482
+ Returns:
1483
+ A sorted list of candidates.
1484
+
1485
+ """
1486
+
1487
+ cw = edata.condorcet_winner(curr_cands = curr_cands)
1488
+ if cw is not None:
1489
+ return [cw]
1490
+ else:
1491
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
1492
+ strength_function = edata.margin if strength_function is None else strength_function
1493
+
1494
+ matches = [(a, b, strength_function(a, b)) for a in curr_cands for b in curr_cands if a != b]
1495
+ sorted_matches = sorted(matches, reverse=True, key=lambda m_w_weight: m_w_weight[2])
1496
+
1497
+ return sorted(_simple_stable_voting(curr_cands = curr_cands,
1498
+ sorted_matches = sorted_matches,
1499
+ mem_sv_winners = {})[0])
1500
+
1501
+
1502
+ def _simple_stable_voting_basic(edata, curr_cands = None, strength_function = None):
1503
+ """Implementation of Simple Stable Voting from https://arxiv.org/abs/2108.00542.
1504
+
1505
+ Args:
1506
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1507
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1508
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1509
+
1510
+ Returns:
1511
+ A sorted list of candidates.
1512
+
1513
+ """
1514
+
1515
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
1516
+ strength_function = edata.margin if strength_function is None else strength_function
1517
+
1518
+ matches = [(a, b, strength_function(a, b)) for a in curr_cands for b in curr_cands if a != b]
1519
+ sorted_matches = sorted(matches, reverse=True, key=lambda m_w_weight: m_w_weight[2])
1520
+
1521
+ return sorted(_simple_stable_voting(curr_cands = curr_cands,
1522
+ sorted_matches = sorted_matches,
1523
+ mem_sv_winners = {})[0])
1524
+
1525
+ @vm(name = "Simple Stable Voting",
1526
+ input_types = [ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
1527
+ def simple_stable_voting(
1528
+ edata,
1529
+ curr_cands=None,
1530
+ strength_function=None,
1531
+ algorithm = 'basic'):
1532
+
1533
+ """Implementation of Simple Stable Voting from https://arxiv.org/abs/2108.00542.
1534
+
1535
+ Simple Stable Voting is a recursive voting method defined as follows:
1536
+
1537
+ 1. If there is only one candidate in the profile, then that candidate is the winner.
1538
+ 2. Order the pairs :math:`(a,b)` of candidates from largest to smallest value of the margin of :math:`a` over :math:`b`, and declare as Simple Stable Voting winners the candidate(s) :math:`a` from the earliest pair(s) :math:`(a,b)` such that :math:`a` is a Simple Stable Voting winner in the election without :math:`b`.
1539
+
1540
+ Args:
1541
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1542
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1543
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1544
+ algorithm (str, optional): Specify which algorithm to use. Options are 'basic' (the default) and 'with_condorcet_check'.
1545
+
1546
+ Returns:
1547
+ A sorted list of candidates.
1548
+
1549
+ .. seealso::
1550
+
1551
+ :meth:`pref_voting.margin_based_methods.stable_voting`
1552
+
1553
+ :Example:
1554
+
1555
+ .. exec_code::
1556
+
1557
+ from pref_voting.weighted_majority_graphs import MarginGraph
1558
+ from pref_voting.margin_based_methods import simple_stable_voting
1559
+
1560
+ mg = MarginGraph([0, 1, 2, 3], [(0, 3, 8), (1, 0, 10), (2, 0, 4), (2, 1, 8), (3, 1, 8)])
1561
+
1562
+ simple_stable_voting.display(mg)
1563
+ simple_stable_voting.display(mg, algorithm='basic')
1564
+ simple_stable_voting.display(mg, algorithm='with_condorcet_check')
1565
+
1566
+ """
1567
+
1568
+ if algorithm == 'basic':
1569
+ return _simple_stable_voting_basic(edata, curr_cands = curr_cands, strength_function = strength_function)
1570
+ elif algorithm == 'with_condorcet_check':
1571
+ return _simple_stable_voting_with_condorcet_check(edata, curr_cands = curr_cands, strength_function = strength_function)
1572
+ else:
1573
+ raise ValueError("Invalid algorithm specified.")
1574
+
1575
+ # Simple Stable Voting with explanation
1576
+ def _simple_stable_voting_with_explanation(curr_cands,
1577
+ sorted_matches,
1578
+ mem_sv_winners,
1579
+ mem_elim_dict):
1580
+ '''
1581
+ Determine the Simple Stable Voting winners while keeping track
1582
+ of the winners in any subprofiles checked during computation
1583
+ and building up elimination dictionaries (associating with each
1584
+ winner the candidates eliminated before reaching that winner).
1585
+ '''
1586
+
1587
+ sv_winners = list()
1588
+
1589
+ if len(curr_cands) == 1:
1590
+ mem_sv_winners[tuple(curr_cands)] = curr_cands
1591
+ mem_elim_dict[tuple(curr_cands)] = {c: [] for c in curr_cands}
1592
+ return curr_cands, mem_sv_winners, {c: [] for c in curr_cands}, mem_elim_dict
1593
+
1594
+ margin_witnessing_win = -math.inf
1595
+
1596
+ new_elim_dict = dict()
1597
+
1598
+ for a, b, s in sorted_matches:
1599
+ if s < margin_witnessing_win:
1600
+ break
1601
+ if a not in sv_winners:
1602
+ cands_minus_b = [c for c in curr_cands if c != b]
1603
+ cands_minus_b_key = tuple(sorted(cands_minus_b))
1604
+ if cands_minus_b_key not in mem_sv_winners.keys():
1605
+ ws, mem_sv_winners, elim_dict, mem_elim_dict = _simple_stable_voting_with_explanation(curr_cands = cands_minus_b,
1606
+ sorted_matches = [(a, c, s) for a, c, s in sorted_matches if a != b and c != b],
1607
+ mem_sv_winners = mem_sv_winners,
1608
+ mem_elim_dict=mem_elim_dict
1609
+ )
1610
+ mem_sv_winners[cands_minus_b_key] = ws
1611
+ mem_elim_dict[cands_minus_b_key] = elim_dict
1612
+ else:
1613
+ ws = mem_sv_winners[cands_minus_b_key]
1614
+ elim_dict = mem_elim_dict[cands_minus_b_key]
1615
+ if a in ws:
1616
+ sv_winners.append(a)
1617
+ margin_witnessing_win = s
1618
+ new_elim_dict[a] = [b] + elim_dict[a]
1619
+
1620
+ return sv_winners, mem_sv_winners, new_elim_dict, mem_elim_dict
1621
+
1622
+ def _simple_stable_voting_with_condorcet_check_with_explanation(
1623
+ edata,
1624
+ curr_cands = None,
1625
+ strength_function = None):
1626
+ """Simple Stable Voting is Condorcet consistent. It is faster to skip executing the recursive algorithm when there is a Condorcet winner.
1627
+
1628
+ Args:
1629
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1630
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1631
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1632
+
1633
+ Returns:
1634
+ A sorted list of candidates.
1635
+
1636
+ """
1637
+
1638
+ cw = edata.condorcet_winner(curr_cands = curr_cands)
1639
+ if cw is not None:
1640
+ return [cw], {cw: list()}
1641
+ else:
1642
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
1643
+ strength_function = edata.margin if strength_function is None else strength_function
1644
+
1645
+ matches = [(a, b, strength_function(a, b)) for a in curr_cands for b in curr_cands if a != b]
1646
+ sorted_matches = sorted(matches, reverse=True, key=lambda m_w_weight: m_w_weight[2])
1647
+
1648
+ ws, mem_sv_winners, elim_dict, mem_elim_dict = _simple_stable_voting_with_explanation(curr_cands = curr_cands,
1649
+ sorted_matches = sorted_matches,
1650
+ mem_sv_winners = {},
1651
+ mem_elim_dict = {}
1652
+ )
1653
+
1654
+ return sorted(ws), elim_dict
1655
+
1656
+
1657
+ def _simple_stable_voting_basic_with_explanation(edata, curr_cands = None, strength_function = None):
1658
+ """Implementation of Simple Stable Voting from https://arxiv.org/abs/2108.00542.
1659
+
1660
+ Args:
1661
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1662
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1663
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1664
+
1665
+ Returns:
1666
+ A sorted list of candidates.
1667
+
1668
+ """
1669
+
1670
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
1671
+ strength_function = edata.margin if strength_function is None else strength_function
1672
+
1673
+ matches = [(a, b, strength_function(a, b)) for a in curr_cands for b in curr_cands if a != b]
1674
+ sorted_matches = sorted(matches, reverse=True, key=lambda m_w_weight: m_w_weight[2])
1675
+
1676
+ ws, mem_sv_winners, elim_dict, mem_elim_dict = _simple_stable_voting_with_explanation(curr_cands = curr_cands,
1677
+ sorted_matches = sorted_matches,
1678
+ mem_sv_winners = {},
1679
+ mem_elim_dict = {}
1680
+ )
1681
+
1682
+ return sorted(ws), elim_dict
1683
+
1684
+ def simple_stable_voting_with_explanation(
1685
+ edata,
1686
+ curr_cands=None,
1687
+ strength_function=None,
1688
+ algorithm = 'basic',
1689
+ ):
1690
+
1691
+ """Implementation of Simple Stable Voting from https://arxiv.org/abs/2108.00542.
1692
+
1693
+ Simple Stable Voting is a recursive voting method defined as follows:
1694
+
1695
+ 1. If there is only one candidate in the profile, then that candidate is the winner.
1696
+ 2. Order the pairs :math:`(a,b)` of candidates from largest to smallest value of the margin of :math:`a` over :math:`b`, and declare as Simple Stable Voting winners the candidate(s) :math:`a` from the earliest pair(s) :math:`(a,b)` such that :math:`a` is a Simple Stable Voting winner in the election without :math:`b`.
1697
+
1698
+ This function outputs not only the winning candidates but also an "explanation", which is a dictionary associating with each winning candidate a list of candidates that were eliminated before reaching the base case of the recursion. Note that if there are tied margins, there may be multiple elimination lists witnessing the same winnner, but the function will only output one of them.
1699
+
1700
+ Also note that if algorithm = 'with_condorcet_check' and there is a Condorcet winner, then the dictionary associated with the Condorcet winner and empty list, reflecting the fact that no eliminations were necessary to compute the winner.
1701
+
1702
+ Args:
1703
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1704
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1705
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
1706
+ algorithm (str, optional): Specify which algorithm to use. Options are 'basic' (the default) and 'with_condorcet_check'.
1707
+
1708
+ Returns:
1709
+ A sorted list of candidates plus a dictionary associating with each winning candidate a list.
1710
+
1711
+ .. seealso::
1712
+ :meth:`pref_voting.margin_based_methods.simple_stable_voting`
1713
+ :meth:`pref_voting.margin_based_methods.stable_voting`
1714
+
1715
+ :Example:
1716
+
1717
+ .. exec_code::
1718
+
1719
+ from pref_voting.weighted_majority_graphs import MarginGraph
1720
+ from pref_voting.margin_based_methods import simple_stable_voting
1721
+
1722
+ mg = MarginGraph([0, 1, 2, 3], [(0, 3, 8), (1, 0, 10), (2, 0, 4), (2, 1, 8), (3, 1, 8)])
1723
+
1724
+ simple_stable_voting.display(mg)
1725
+ simple_stable_voting.display(mg, algorithm='basic')
1726
+ simple_stable_voting.display(mg, algorithm='with_condorcet_check')
1727
+
1728
+ """
1729
+
1730
+ if algorithm == 'basic':
1731
+ return _simple_stable_voting_basic_with_explanation(edata, curr_cands = curr_cands, strength_function = strength_function)
1732
+ elif algorithm == 'with_condorcet_check':
1733
+ return _simple_stable_voting_with_condorcet_check_with_explanation(edata, curr_cands = curr_cands, strength_function = strength_function)
1734
+ else:
1735
+ raise ValueError("Invalid algorithm specified.")
1736
+
1737
+ # Stable Voting
1738
+ def _stable_voting(edata,
1739
+ curr_cands,
1740
+ strength_function,
1741
+ sorted_matches,
1742
+ mem_sv_winners,
1743
+ terminate_early,
1744
+ favor_weak_condorcet_winners):
1745
+ '''
1746
+ Determine the Stable Voting winners for the profile while keeping track of the winners in any subprofiles checked during computation.
1747
+
1748
+ If terminate_early is True, then the algorithm will terminate early if there is only one undefeated candidate.
1749
+
1750
+ If favor_weak_condorcet_winners is True, then if there are weak Condorcet winners, the algorithm will only consider pairs (A,B) where A is a weak Condorcet winner.
1751
+ '''
1752
+
1753
+ sv_winners = list()
1754
+
1755
+ if len(curr_cands) == 1:
1756
+ mem_sv_winners[tuple(curr_cands)] = curr_cands
1757
+ return curr_cands, mem_sv_winners
1758
+
1759
+ if favor_weak_condorcet_winners and len(edata.weak_condorcet_winner(curr_cands=curr_cands)) > 0:
1760
+ undefeated_candidates = edata.weak_condorcet_winner(curr_cands=curr_cands)
1761
+ else:
1762
+ undefeated_candidates = split_cycle(edata, curr_cands=curr_cands, strength_function=strength_function)
1763
+
1764
+ # Early termination if there is only one undefeated candidate
1765
+ if terminate_early and len(undefeated_candidates) == 1:
1766
+ mem_sv_winners[tuple(undefeated_candidates)] = undefeated_candidates
1767
+ return undefeated_candidates, mem_sv_winners
1768
+
1769
+ margin_witnessing_win = -math.inf
1770
+
1771
+ for a, b, s in sorted_matches:
1772
+ if s < margin_witnessing_win:
1773
+ break
1774
+ if a in undefeated_candidates and a not in sv_winners:
1775
+ cands_minus_b = [c for c in curr_cands if c != b]
1776
+ cands_minus_b_key = tuple(sorted(cands_minus_b))
1777
+ if cands_minus_b_key not in mem_sv_winners:
1778
+ ws, mem_sv_winners = _stable_voting(edata,
1779
+ curr_cands=cands_minus_b,
1780
+ strength_function=strength_function,
1781
+ sorted_matches=[(x, y, s) for x, y, s in sorted_matches if x != b and y != b],
1782
+ mem_sv_winners=mem_sv_winners,
1783
+ terminate_early=terminate_early,
1784
+ favor_weak_condorcet_winners=favor_weak_condorcet_winners
1785
+ )
1786
+ mem_sv_winners[cands_minus_b_key] = ws
1787
+ else:
1788
+ ws = mem_sv_winners[cands_minus_b_key]
1789
+ if a in ws:
1790
+ sv_winners.append(a)
1791
+ margin_witnessing_win = s
1792
+
1793
+ return sv_winners, mem_sv_winners
1794
+
1795
+ def _stable_voting_with_condorcet_check(
1796
+ edata,
1797
+ curr_cands=None,
1798
+ strength_function=None,
1799
+ terminate_early=False,
1800
+ favor_weak_condorcet_winners=False):
1801
+ """
1802
+ Stable Voting is Condorcet consistent. It is faster to skip executing the recursive algorithm when there is a Condorcet winner.
1803
+
1804
+ Args:
1805
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1806
+ curr_cands (List[int], optional): Find the winners for the profile restricted to these candidates.
1807
+ strength_function (function, optional): The strength function to calculate the strength of a path.
1808
+ terminate_early (bool, optional): If True, terminate early when there is only one undefeated candidate.
1809
+ favor_weak_condorcet_winners (bool, optional): If True, then if there are any weak Condorcet winners,
1810
+ only consider pairs (A,B) where A is a weak Condorcet winner.
1811
+ Returns:
1812
+ A sorted list of candidates.
1813
+ """
1814
+ cw = edata.condorcet_winner(curr_cands=curr_cands)
1815
+ if cw is not None:
1816
+ return [cw]
1817
+ else:
1818
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
1819
+ strength_function = edata.margin if strength_function is None else strength_function
1820
+
1821
+ matches = [(a, b, strength_function(a, b))
1822
+ for a in curr_cands for b in curr_cands if a != b]
1823
+ sorted_matches = sorted(matches, reverse=True, key=lambda m: m[2])
1824
+
1825
+ winners, _ = _stable_voting(edata,
1826
+ curr_cands=curr_cands,
1827
+ strength_function=strength_function,
1828
+ sorted_matches=sorted_matches,
1829
+ mem_sv_winners={},
1830
+ terminate_early=terminate_early,
1831
+ favor_weak_condorcet_winners=favor_weak_condorcet_winners
1832
+ )
1833
+ return sorted(winners)
1834
+
1835
+ def _stable_voting_basic(
1836
+ edata,
1837
+ curr_cands=None,
1838
+ strength_function=None,
1839
+ terminate_early=False,
1840
+ favor_weak_condorcet_winners=False):
1841
+ """Implementation of Stable Voting from https://arxiv.org/abs/2108.00542.
1842
+
1843
+ Args:
1844
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1845
+ curr_cands (List[int], optional): Find the winners for the profile restricted to these candidates.
1846
+ strength_function (function, optional): The strength function to calculate the strength of a path.
1847
+ terminate_early (bool, optional): If True, terminate early when there is only one undefeated candidate.
1848
+
1849
+ Returns:
1850
+ A sorted list of candidates.
1851
+ """
1852
+
1853
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
1854
+ strength_function = edata.margin if strength_function is None else strength_function
1855
+
1856
+ matches = [(a, b, strength_function(a, b))
1857
+ for a in curr_cands for b in curr_cands if a != b]
1858
+ sorted_matches = sorted(matches, reverse=True, key=lambda m: m[2])
1859
+
1860
+ winners, _ = _stable_voting(edata,
1861
+ curr_cands=curr_cands,
1862
+ strength_function=strength_function,
1863
+ sorted_matches=sorted_matches,
1864
+ mem_sv_winners={},
1865
+ terminate_early=terminate_early,
1866
+ favor_weak_condorcet_winners=favor_weak_condorcet_winners
1867
+ )
1868
+ return sorted(winners)
1869
+
1870
+ @vm(name="Stable Voting",
1871
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
1872
+ def stable_voting(
1873
+ edata,
1874
+ curr_cands=None,
1875
+ strength_function=None,
1876
+ algorithm='with_condorcet_check_and_early_termination',
1877
+ favor_weak_condorcet_winners=False):
1878
+ """Implementation of Stable Voting from https://arxiv.org/abs/2108.00542.
1879
+
1880
+ Stable Voting is a recursive voting method defined as follows:
1881
+
1882
+ 1. If there is only one candidate in the profile, then that candidate is the winner.
1883
+ 2. Order the pairs (a, b) of candidates from largest to smallest margin of a over b such that a is undefeated according to Split Cycle, and declare as Stable Voting winners the candidate(s) a from the earliest pair(s) (a, b) such that a is a Simple Stable Voting winner in the election without b.
1884
+
1885
+ If the algorithm 'with_condorcet_check' is specified, then the algorithm will first check if there is a Condorcet winner and return that candidate if there is one.
1886
+
1887
+ If the algorithm 'with_early_termination' is specified, then the algorithm will terminate early if there is only one undefeated candidate.
1888
+
1889
+ If the algorithm 'with_condorcet_check_and_early_termination' (the default) is specified, then the algorithm will first check if there is a Condorcet winner and return that candidate if there is one. It will also terminate early if there is only one undefeated candidate.
1890
+
1891
+ Args:
1892
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1893
+ curr_cands (List[int], optional): Find the winners for the profile restricted to these candidates.
1894
+ strength_function (function, optional): The strength function to calculate the strength of a path.
1895
+ algorithm (str, optional): Specify which algorithm to use:
1896
+ - 'basic'
1897
+ - 'with_condorcet_check'
1898
+ - 'with_early_termination'
1899
+ - 'with_condorcet_check_and_early_termination'
1900
+
1901
+ Returns:
1902
+ A sorted list of candidates.
1903
+
1904
+ .. seealso::
1905
+ :meth:`simple_stable_voting`
1906
+
1907
+ Example:
1908
+
1909
+ from pref_voting.weighted_majority_graphs import MarginGraph
1910
+ from pref_voting.margin_based_methods import stable_voting
1911
+
1912
+ mg = MarginGraph(
1913
+ candidates=[0, 1, 2, 3],
1914
+ edges=[(0, 3, 8), (1, 0, 10), (2, 0, 4),
1915
+ (2, 1, 8), (3, 1, 8)]
1916
+ )
1917
+
1918
+ stable_voting.display(mg)
1919
+ stable_voting.display(mg, algorithm='basic')
1920
+ stable_voting.display(mg, algorithm='with_condorcet_check')
1921
+ stable_voting.display(mg, algorithm='with_early_termination')
1922
+ stable_voting.display(mg, algorithm='with_condorcet_check_and_early_termination')
1923
+ """
1924
+
1925
+ if algorithm == 'basic':
1926
+ return _stable_voting_basic(
1927
+ edata,
1928
+ curr_cands=curr_cands,
1929
+ strength_function=strength_function,
1930
+ terminate_early=False,
1931
+ favor_weak_condorcet_winners=favor_weak_condorcet_winners
1932
+ )
1933
+ elif algorithm == 'with_condorcet_check':
1934
+ return _stable_voting_with_condorcet_check(
1935
+ edata,
1936
+ curr_cands=curr_cands,
1937
+ strength_function=strength_function,
1938
+ terminate_early=False,
1939
+ favor_weak_condorcet_winners=favor_weak_condorcet_winners
1940
+ )
1941
+ elif algorithm == 'with_early_termination':
1942
+ return _stable_voting_basic(
1943
+ edata,
1944
+ curr_cands=curr_cands,
1945
+ strength_function=strength_function,
1946
+ terminate_early=True,
1947
+ favor_weak_condorcet_winners=favor_weak_condorcet_winners
1948
+ )
1949
+ elif algorithm == 'with_condorcet_check_and_early_termination':
1950
+ return _stable_voting_with_condorcet_check(
1951
+ edata,
1952
+ curr_cands=curr_cands,
1953
+ strength_function=strength_function,
1954
+ terminate_early=True,
1955
+ favor_weak_condorcet_winners=favor_weak_condorcet_winners
1956
+ )
1957
+ else:
1958
+ raise ValueError("Invalid algorithm specified.")
1959
+
1960
+ # Stable Voting with explanation
1961
+ def _stable_voting_with_explanation(edata,
1962
+ curr_cands,
1963
+ strength_function,
1964
+ sorted_matches,
1965
+ mem_sv_winners,
1966
+ mem_elim_dict,
1967
+ terminate_early):
1968
+ '''
1969
+ Determine the Stable Voting winners for the profile while keeping track of the winners in any subprofiles checked during computation.
1970
+
1971
+ If terminate_early is True, then the algorithm will terminate early if there is only one undefeated candidate.
1972
+ '''
1973
+
1974
+ sv_winners = list()
1975
+
1976
+ if len(curr_cands) == 1:
1977
+ mem_sv_winners[tuple(curr_cands)] = curr_cands
1978
+ mem_elim_dict[tuple(curr_cands)] = {c: list() for c in curr_cands}
1979
+ return curr_cands, mem_sv_winners, {c: list() for c in curr_cands}, mem_elim_dict
1980
+
1981
+ undefeated_candidates = split_cycle(edata, curr_cands = curr_cands, strength_function = strength_function)
1982
+
1983
+ if terminate_early and len(undefeated_candidates) == 1:
1984
+ mem_sv_winners[tuple(undefeated_candidates)] = undefeated_candidates
1985
+ mem_elim_dict[tuple(undefeated_candidates)] = {c: list() for c in undefeated_candidates}
1986
+ return undefeated_candidates, mem_sv_winners, {c: list() for c in undefeated_candidates}, mem_elim_dict
1987
+
1988
+ margin_witnessing_win = -math.inf
1989
+
1990
+ new_elim_dict = dict()
1991
+
1992
+ for a, b, s in sorted_matches:
1993
+ if s < margin_witnessing_win:
1994
+ break
1995
+ if a in undefeated_candidates and a not in sv_winners:
1996
+ cands_minus_b = [c for c in curr_cands if c != b]
1997
+ cands_minus_b_key = tuple(sorted(cands_minus_b))
1998
+ if cands_minus_b_key not in mem_sv_winners.keys():
1999
+ ws, mem_sv_winners, elim_dict, mem_elim_dict = _stable_voting_with_explanation(edata,
2000
+ curr_cands = cands_minus_b,
2001
+ strength_function = strength_function,
2002
+ sorted_matches = [(a, c, s) for a, c, s in sorted_matches if a != b and c != b],
2003
+ mem_sv_winners = mem_sv_winners,
2004
+ mem_elim_dict = mem_elim_dict,
2005
+ terminate_early = terminate_early
2006
+ )
2007
+
2008
+ mem_sv_winners[cands_minus_b_key] = ws
2009
+ mem_elim_dict[cands_minus_b_key] = elim_dict
2010
+ else:
2011
+ ws = mem_sv_winners[cands_minus_b_key]
2012
+ elim_dict = mem_elim_dict[cands_minus_b_key]
2013
+
2014
+ if a in ws:
2015
+ sv_winners.append(a)
2016
+ margin_witnessing_win = s
2017
+ new_elim_dict[a] = [b] + elim_dict[a]
2018
+
2019
+ return sv_winners, mem_sv_winners, new_elim_dict, mem_elim_dict
2020
+
2021
+ def _stable_voting_with_condorcet_check_with_explanation(
2022
+ edata,
2023
+ curr_cands=None,
2024
+ strength_function=None,
2025
+ terminate_early=True):
2026
+ """
2027
+ Stable Voting is Condorcet consistent. It is faster to skip executing the recursive algorithm when there is a Condorcet winner.
2028
+
2029
+ Args:
2030
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
2031
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2032
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
2033
+
2034
+ Returns:
2035
+ A sorted list of candidates.
2036
+
2037
+ """
2038
+ cw = edata.condorcet_winner(curr_cands = curr_cands)
2039
+ if cw is not None:
2040
+ return [cw], {cw: list()}
2041
+ else:
2042
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
2043
+ strength_function = edata.margin if strength_function is None else strength_function
2044
+
2045
+ matches = [(a, b, strength_function(a, b)) for a in curr_cands for b in curr_cands if a != b]
2046
+ sorted_matches = sorted(matches, reverse=True, key=lambda m_w_weight: m_w_weight[2])
2047
+
2048
+ ws, mem_sv_winners, elim_dict, mem_elim_dict = _stable_voting_with_explanation(edata,
2049
+ curr_cands = curr_cands,
2050
+ strength_function = strength_function,
2051
+ sorted_matches = sorted_matches,
2052
+ mem_sv_winners = {},
2053
+ mem_elim_dict = {},
2054
+ terminate_early = terminate_early
2055
+ )
2056
+
2057
+ return sorted(ws), elim_dict
2058
+
2059
+ def _stable_voting_basic_with_explanation(
2060
+ edata,
2061
+ curr_cands = None,
2062
+ strength_function = None,
2063
+ terminate_early = True):
2064
+ """Implementation of Stable Voting from https://arxiv.org/abs/2108.00542.
2065
+
2066
+ Args:
2067
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
2068
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2069
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
2070
+
2071
+ Returns:
2072
+ A sorted list of candidates.
2073
+
2074
+ """
2075
+
2076
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
2077
+ strength_function = edata.margin if strength_function is None else strength_function
2078
+
2079
+ matches = [(a, b, strength_function(a, b)) for a in curr_cands for b in curr_cands if a != b]
2080
+ sorted_matches = sorted(matches, reverse=True, key=lambda m_w_weight: m_w_weight[2])
2081
+
2082
+ ws, mem_sv_winners, elim_dict, mem_elim_dict = _stable_voting_with_explanation(edata,
2083
+ curr_cands = curr_cands,
2084
+ strength_function = strength_function,
2085
+ sorted_matches = sorted_matches,
2086
+ mem_sv_winners = {},
2087
+ mem_elim_dict = {},
2088
+ terminate_early = terminate_early
2089
+ )
2090
+
2091
+ return sorted(ws), elim_dict
2092
+
2093
+ def stable_voting_with_explanation(
2094
+ edata,
2095
+ curr_cands=None,
2096
+ strength_function=None,
2097
+ algorithm='basic'):
2098
+ """Implementation of Stable Voting from https://arxiv.org/abs/2108.00542.
2099
+
2100
+ Stable Voting is a recursive voting method defined as follows:
2101
+
2102
+ 1. If there is only one candidate in the profile, then that candidate is the winner.
2103
+ 2. Order the pairs :math:`(a,b)` of candidates from largest to smallest value of the margin of :math:`a` over :math:`b` such that :math:`a` is undefeated according to Split Cycle, and declare as Stable Voting winners the candidate(s) :math:`a` from the earliest pair(s) :math:`(a,b)` such that :math:`a` is a Simple Stable Voting winner in the election without :math:`b`.
2104
+
2105
+ If the algorithm 'with_condorcet_check' is specified, then the algorithm will first check if there is a Condorcet winner and return that candidate if there is one.
2106
+
2107
+ If the algorithm 'with_early_termination' is specified, then the algorithm will terminate early if there is only one undefeated candidate.
2108
+
2109
+ If the algorithm 'with_condorcet_check_and_early_termination' (the default) is specified, then the algorithm will first check if there is a Condorcet winner and return that candidate if there is one. It will also terminate early if there is only one undefeated candidate.
2110
+
2111
+ Args:
2112
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
2113
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2114
+ strength_function (function, optional): The strength function to be used to calculate the strength of a path. The default is the margin method of ``edata``. This only matters when the ballots are not linear orders.
2115
+ algorithm (str, optional): Specify which algorithm to use:
2116
+ - 'basic'
2117
+ - 'with_condorcet_check'
2118
+ - 'with_early_termination'
2119
+ - 'with_condorcet_check_and_early_termination'
2120
+
2121
+ Returns:
2122
+ A sorted list of candidates plus a dictionary associating with each winning a candidate x the list of candidates that were eliminated before reaching x.
2123
+
2124
+ .. seealso::
2125
+
2126
+ :meth:`pref_voting.margin_based_methods.simple_stable_voting`
2127
+
2128
+
2129
+ :Example:
2130
+
2131
+ .. exec_code::
2132
+
2133
+ from pref_voting.weighted_majority_graphs import MarginGraph
2134
+ from pref_voting.margin_based_methods import stable_voting
2135
+
2136
+ mg = MarginGraph([0, 1, 2, 3], [(0, 3, 8), (1, 0, 10), (2, 0, 4), (2, 1, 8), (3, 1, 8)])
2137
+
2138
+ stable_voting.display(mg)
2139
+ stable_voting.display(mg, algorithm='basic')
2140
+ stable_voting.display(mg, algorithm='with_condorcet_check')
2141
+ stable_voting.display(mg, algorithm='with_early_termination')
2142
+ stable_voting.display(mg, algorithm='with_condorcet_check_and_early_termination')
2143
+
2144
+ """
2145
+
2146
+ if algorithm == 'basic':
2147
+ return _stable_voting_basic_with_explanation(edata, curr_cands = curr_cands, strength_function = strength_function, terminate_early=False)
2148
+ elif algorithm == 'with_condorcet_check':
2149
+ return _stable_voting_with_condorcet_check_with_explanation(edata, curr_cands = curr_cands, strength_function = strength_function, terminate_early=False)
2150
+ elif algorithm == "with_early_termination":
2151
+ return _stable_voting_basic_with_explanation(edata, curr_cands = curr_cands, strength_function = strength_function, terminate_early=True)
2152
+ elif algorithm == "with_condorcet_check_and_early_termination":
2153
+ return _stable_voting_with_condorcet_check_with_explanation(edata, curr_cands = curr_cands, strength_function = strength_function, terminate_early=True)
2154
+ else:
2155
+ raise ValueError("Invalid algorithm specified.")
2156
+
2157
+
2158
+ @vm(name="Essential Set",
2159
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
2160
+ def essential(edata, curr_cands = None, threshold = 0.0000001):
2161
+ """The Essential Set is the support of the (chosen) C2 maximal lottery.
2162
+
2163
+ Args:
2164
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin_matrix` attribute.
2165
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2166
+
2167
+ Returns:
2168
+ A sorted list of candidates.
2169
+
2170
+ """
2171
+ ml = maximal_lottery(edata, curr_cands=curr_cands)
2172
+
2173
+ return sorted([c for c in ml.keys() if ml[c] > threshold])
2174
+
2175
+ @vm(name="Weighted Covering",
2176
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
2177
+ def weighted_covering(edata, curr_cands=None):
2178
+ """According to Weighted Covering, x defeats y if the margin of x over y is positive and for every other z, the margin of x over z is greater than or equal to the margin of y over z.
2179
+
2180
+ Args:
2181
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
2182
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2183
+
2184
+ Returns:
2185
+ A sorted list of candidates.
2186
+
2187
+ .. note::
2188
+ See, e.g., Bhaskar Dutta and Jean-Francois Laslier, "Comparison functions and choice correspondences," Social Choice and Welfare, 16:513–532, 1999, doi:10.1007/s003550050158, and Raúl Pérez-Fernández and Bernard De Baets, "The supercovering relation, the pairwise winner, and more missing links between Borda and Condorcet," Social Choice and Welfare, 50:329–352, 2018, doi:10.1007/s00355-017-1086-0.
2189
+ """
2190
+
2191
+ candidates = edata.candidates if curr_cands is None else curr_cands
2192
+
2193
+ uc_set = list()
2194
+
2195
+ for y in candidates:
2196
+ is_in_ucs = True
2197
+ for x in edata.dominators(y, curr_cands = curr_cands):
2198
+ # check if x covers y, i.e., for every z, margin(x, z) >= margin(y, z)
2199
+ covers = True
2200
+ for z in candidates:
2201
+ if edata.margin(x, z) < edata.margin(y, z):
2202
+ covers = False
2203
+ break
2204
+
2205
+ if covers:
2206
+ is_in_ucs = False
2207
+ break
2208
+
2209
+ if is_in_ucs:
2210
+ uc_set.append(y)
2211
+
2212
+ return sorted(uc_set)
2213
+
2214
+ @vm(name="beta-Uncovered Set",
2215
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES]
2216
+ )
2217
+ def beta_uncovered_set(edata, curr_cands = None, beta = 0.5):
2218
+
2219
+ """Another weighted version of the uncovered set (different from weighted_covering) due to Munagala and Wang (https://arxiv.org/abs/1905.01401, also see Section 5.2 of https://arxiv.org/abs/2306.17838).
2220
+
2221
+ The beta-uncovered set is the set of candidates that are not beta-covered by any other candidate. Candidate x beta-covers a candidate y if (i) the fraction of voters who rank x above y is at least beta and (ii) for any candidate z, if the fraction of voters who rank z above x is at least beta, then the fraction of voters who rank z above y is at least beta.
2222
+
2223
+ Args:
2224
+ edata (Profile, ProfileWithTies): Any election data that has a support method.
2225
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2226
+ beta (float, optional): The beta parameter. The default is 0.5.
2227
+
2228
+ Returns:
2229
+ A sorted list of candidates.
2230
+
2231
+ """
2232
+
2233
+ candidates = edata.candidates if curr_cands is None else curr_cands
2234
+
2235
+ beta_uncovered_set = list()
2236
+
2237
+ for y in candidates:
2238
+ is_in_bucs = True
2239
+ for x in candidates:
2240
+ if edata.support(x, y)/edata.num_voters >= beta:
2241
+ # check if x beta-covers y, i.e., for every z, if the fraction of voters preferring z to x is at least beta, then the fraction of voters preferring z to y is at least beta
2242
+ beta_covers = True
2243
+ for z in candidates:
2244
+ if edata.support(z, x)/edata.num_voters >= beta and edata.support(z, y)/edata.num_voters < beta:
2245
+ beta_covers = False
2246
+ break
2247
+
2248
+ if beta_covers:
2249
+ is_in_bucs = False
2250
+ break
2251
+
2252
+ if is_in_bucs:
2253
+ beta_uncovered_set.append(y)
2254
+
2255
+ return sorted(beta_uncovered_set)
2256
+
2257
+ @vm(name = "Loss-Trimmer Voting",
2258
+ input_types = [ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
2259
+ def loss_trimmer(edata, curr_cands = None):
2260
+ """Iteratively eliminate the candidate with the largest sum of margins of loss until a Condorcet winner is found. In this version of the method, parallel-universe tiebreaking is used if there are multiple candidates with the largest sum of margins of loss.
2261
+
2262
+ Args:
2263
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a margin method.
2264
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2265
+
2266
+ Returns:
2267
+ A sorted list of candidates
2268
+
2269
+ .. note::
2270
+ Method proposed by Richard B. Darlington in "The Case for the Loss-Trimmer Voting System."
2271
+
2272
+ """
2273
+
2274
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
2275
+
2276
+ weak_cw = edata.weak_condorcet_winner(curr_cands = curr_cands)
2277
+ # If there are weak Condorcet winners, return those candidates
2278
+ if edata.weak_condorcet_winner(curr_cands = curr_cands) is not None:
2279
+ return sorted(weak_cw)
2280
+
2281
+ # Otherwise, calculate the sum of margins of loss for each candidate
2282
+ sum_of_margins_of_loss = {cand: sum([edata.margin(other_cand, cand) for other_cand in curr_cands if edata.margin(other_cand, cand) > 0]) for cand in curr_cands}
2283
+
2284
+ # Find the candidates with the largest sum of margins of loss
2285
+ max_sum_of_margins_of_loss = max(sum_of_margins_of_loss.values())
2286
+ biggest_losers = [cand for cand in curr_cands if sum_of_margins_of_loss[cand] == max_sum_of_margins_of_loss]
2287
+
2288
+ winners = []
2289
+
2290
+ # For each biggest loser, calculate the winners after removing that candidate. The union of these sets is the set of winners.
2291
+ for bl in biggest_losers:
2292
+ winners_without_bl = loss_trimmer(edata, curr_cands = [cand for cand in curr_cands if cand != bl])
2293
+ winners += winners_without_bl
2294
+
2295
+ return sorted(list(set(winners)))
2296
+
2297
+ @vm(name = "Simplified Dodgson",
2298
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
2299
+ def simplified_dodgson(edata, curr_cands = None):
2300
+ """Return the Condorcet winner, if one exists, and otherwise return the candidate(s) with the smallest sum of margins of loss.
2301
+
2302
+ Args:
2303
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
2304
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2305
+
2306
+ Returns:
2307
+ A sorted list of candidates
2308
+
2309
+ .. note::
2310
+ This method was proposed by Nicolaus Tideman in https://doi.org/10.1007/BF00433944.
2311
+ """
2312
+
2313
+ curr_cands = edata.candidates if curr_cands is None else curr_cands
2314
+
2315
+ if edata.condorcet_winner(curr_cands = curr_cands) is not None:
2316
+ return [edata.condorcet_winner(curr_cands = curr_cands)]
2317
+
2318
+ sum_of_margins_of_loss = {cand: sum([edata.margin(other_cand, cand) for other_cand in curr_cands if edata.margin(other_cand, cand) >= 0]) for cand in curr_cands}
2319
+
2320
+ smallest_sum_of_margins_of_loss = min(sum_of_margins_of_loss.values())
2321
+
2322
+ candidates_with_smallest_sum_of_margins_of_loss = [cand for cand in curr_cands if sum_of_margins_of_loss[cand] == smallest_sum_of_margins_of_loss]
2323
+
2324
+ return sorted(candidates_with_smallest_sum_of_margins_of_loss)
2325
+
2326
+ def distance_to_margin_graph(edata, rel, exp = 1, curr_cands = None):
2327
+ """
2328
+ Calculate the distance of ``rel`` (a relation) to the majority graph of ``edata``.
2329
+ """
2330
+ candidates = edata.candidates if curr_cands is None else curr_cands
2331
+
2332
+ if type(edata) == MajorityGraph and exp == 0:
2333
+ # if edata is a MajorityGraph, we need to add margins for the following code to work. The margins do not matter when exp==0.
2334
+ edata = MarginGraph(candidates, [(c1, c2, 1) for c1, c2 in edata.edges if (c1 in candidates and c2 in candidates)])
2335
+ penalty = 0
2336
+ for a,b in combinations(candidates, 2):
2337
+ if edata.majority_prefers(a, b) and (b,a) in rel:
2338
+ penalty += (edata.margin(a, b) ** exp)
2339
+ elif edata.majority_prefers(b, a) and (a,b) in rel:
2340
+ penalty += (edata.margin(b, a) ** exp)
2341
+ elif edata.majority_prefers(a, b) and (a,b) not in rel and (b,a) not in rel:
2342
+ penalty += (edata.margin(a, b) ** exp) / 2
2343
+ elif edata.majority_prefers(b, a) and (a,b) not in rel and (b,a) not in rel:
2344
+ penalty += (edata.margin(b, a) ** exp) / 2
2345
+ return penalty