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,2425 @@
1
+ '''
2
+ File: iterative_methods.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: January 6, 2022
5
+ Update: October 2, 2023
6
+
7
+ Implementations of iterative voting methods.
8
+ '''
9
+ from pref_voting.voting_method import *
10
+ from pref_voting.voting_method import _num_rank_last, _num_rank_first
11
+ from pref_voting.profiles import _borda_score, _find_updated_profile
12
+ from pref_voting.margin_based_methods import split_cycle, minimax_scores
13
+ from pref_voting.c1_methods import top_cycle, gocha
14
+ from pref_voting.rankings import Ranking
15
+ from pref_voting.social_welfare_function import swf
16
+ import copy
17
+ from itertools import permutations, product
18
+ import numpy as np
19
+ from pref_voting.voting_method_properties import ElectionTypes
20
+ from pref_voting.profiles import Profile
21
+ from pref_voting.profiles_with_ties import ProfileWithTies
22
+
23
+ def _instant_runoff_basic(profile,curr_cands = None):
24
+ "The basic implementation of instant runoff"
25
+ # need the total number of all candidates in a profile to check when all candidates have been removed
26
+ num_cands = profile.num_cands
27
+ candidates = profile.candidates if curr_cands is None else curr_cands
28
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
29
+
30
+ strict_maj_size = profile.strict_maj_size()
31
+
32
+ rs, rcounts = profile.rankings_counts # get all the ranking data
33
+
34
+ winners = [c for c in candidates
35
+ if _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
36
+
37
+ while len(winners) == 0:
38
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c) for c in candidates
39
+ if not isin(cands_to_ignore,c)}
40
+ min_plurality_score = min(plurality_scores.values())
41
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
42
+ if plurality_scores[c] == min_plurality_score])
43
+
44
+ # remove cands with lowest plurality score
45
+ cands_to_ignore = np.concatenate((cands_to_ignore, lowest_first_place_votes), axis=None)
46
+ if len(cands_to_ignore) == num_cands: # removed all of the candidates
47
+ winners = sorted(lowest_first_place_votes)
48
+ else:
49
+ winners = [c for c in candidates
50
+ if not isin(cands_to_ignore,c) and _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
51
+
52
+ return sorted(winners)
53
+
54
+ def _instant_runoff_recursive(profile, curr_cands = None):
55
+ "A recursive implementation of instant runoff"
56
+ candidates = curr_cands if curr_cands is not None else profile.candidates
57
+ cands_to_ignore = np.array([c for c in profile.candidates if c not in candidates])
58
+ rs, rcounts = profile.rankings_counts # get all the ranking data
59
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c) for c in candidates if not isin(cands_to_ignore,c)}
60
+ min_plurality_score = min(plurality_scores.values())
61
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
62
+ if plurality_scores[c] == min_plurality_score])
63
+
64
+ if len(lowest_first_place_votes) == len(candidates):
65
+ return sorted(lowest_first_place_votes)
66
+
67
+ else:
68
+ return _instant_runoff_recursive(profile, [c for c in candidates if c not in lowest_first_place_votes])
69
+
70
+
71
+ def _instant_runoff_for_truncated_linear_orders(profile, curr_cands = None, threshold = None, hide_warnings = True):
72
+ """
73
+ Instant Runoff for Truncated Linear Orders. Iteratively remove the candidates with the fewest number of first place votes, until there is a candidate with more than the threshold number of first-place votes.
74
+ If a threshold is not set, then it is strictly more than half of the non-empty ballots.
75
+
76
+ Args:
77
+ profile (ProfileWithTies): An anonymous profile with no ties in the ballots (note that ProfileWithTies allows for truncated linear orders).
78
+ threshold (int, float, optional): The threshold needed to win the election. If it is not set, then it is strictly more than half of the remaining ballots.
79
+ hide_warnings (bool, optional): Show or hide the warnings when more than one candidate is eliminated in a round.
80
+
81
+ Returns:
82
+ A sorted list of candidates
83
+
84
+ .. note:: This is the simultaneous version of instant runoff, not the parallel-universe tiebreaking version. It is intended to be run on profiles with large number of voters in which there is a very low probability of a tie in the fewest number of first place votes. A warning is displayed when more than one candidate is eliminated.
85
+
86
+ :Example:
87
+
88
+ .. exec_code::
89
+
90
+ from pref_voting.profiles_with_ties import ProfileWithTies
91
+ from pref_voting.iterative_methods import instant_runoff_for_truncated_linear_orders
92
+
93
+ prof = ProfileWithTies([{0:1, 1:1},{0:1, 1:2, 2:3, 3:4}, {0:1, 1:3, 2:3}, {3:2}, {0:1}, {0:1}, {}, {}])
94
+ prof.display()
95
+
96
+ tprof, report = prof.truncate_overvotes()
97
+ for r, new_r, count in report:
98
+ print(f"{r} --> {new_r}: {count}")
99
+ tprof.display()
100
+ instant_runoff_for_truncated_linear_orders.display(tprof)
101
+
102
+
103
+ """
104
+
105
+ assert all([not r.has_overvote() for r in profile.rankings]), "Instant Runoff is only defined when all the ballots are truncated linear orders."
106
+
107
+ curr_cands = profile.candidates if curr_cands is None else curr_cands
108
+
109
+ # we need to remove empty rankings during the algorithm, so make a copy of the profile
110
+ prof2 = copy.deepcopy(profile)
111
+
112
+ _prof = prof2.remove_candidates([c for c in profile.candidates if c not in curr_cands])
113
+
114
+ # remove the empty rankings
115
+ _prof.remove_empty_rankings()
116
+
117
+ threshold = threshold if threshold is not None else _prof.strict_maj_size()
118
+
119
+ remaining_candidates = _prof.candidates
120
+
121
+ pl_scores = _prof.plurality_scores()
122
+ max_pl_score = max(pl_scores.values())
123
+
124
+ while max_pl_score < threshold:
125
+
126
+ reduced_prof = _prof.remove_candidates([c for c in _prof.candidates if c not in remaining_candidates])
127
+
128
+ # after removing the candidates, there might be some empty ballots.
129
+ reduced_prof.remove_empty_rankings()
130
+
131
+ pl_scores = reduced_prof.plurality_scores()
132
+ min_pl_score = min(pl_scores.values())
133
+
134
+ cands_to_remove = [c for c in pl_scores.keys() if pl_scores[c] == min_pl_score]
135
+
136
+ if not hide_warnings and len(cands_to_remove) > 1:
137
+ print(f"Warning: multiple candidates removed in a round: {', '.join(map(str,cands_to_remove))}")
138
+
139
+ if len(cands_to_remove) == len(reduced_prof.candidates):
140
+ # all remaining candidates have the same plurality score.
141
+ break
142
+
143
+ # possibly update the threshold, so that it is a strict majority of the remaining ballots
144
+ threshold = threshold if threshold is not None else reduced_prof.strict_maj_size()
145
+ max_pl_score = max(pl_scores.values())
146
+
147
+ remaining_candidates = [c for c in remaining_candidates if c not in cands_to_remove]
148
+
149
+
150
+ reduced_prof = _prof.remove_candidates([c for c in _prof.candidates if c not in remaining_candidates])
151
+
152
+ # after removing the candidates, there might be some empty ballots.
153
+ reduced_prof.remove_empty_rankings()
154
+
155
+ pl_scores = reduced_prof.plurality_scores()
156
+
157
+ max_pl_score = max(pl_scores.values())
158
+
159
+ return sorted([c for c in pl_scores.keys() if pl_scores[c] == max_pl_score])
160
+
161
+ @vm(name = "Instant Runoff",
162
+ input_types=[ElectionTypes.PROFILE])
163
+ def instant_runoff(profile, curr_cands = None, algorithm = "basic", **kwargs):
164
+ """
165
+ If there is a majority winner then that candidate is the winner. If there is no majority winner, then remove all candidates that are ranked first by the fewest number of voters. Continue removing candidates with the fewest number first-place votes until there is a candidate with a majority of first place votes.
166
+
167
+ .. important::
168
+ If there is more than one candidate with the fewest number of first-place votes, then *all* such candidates are removed from the profile.
169
+
170
+ Args:
171
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
172
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
173
+ algorithm (str, optional): The algorithm to use. Options are "basic" and "recursive". The default is "basic".
174
+
175
+ Returns:
176
+ A sorted list of candidates
177
+
178
+ .. seealso::
179
+
180
+ Instant Runoff is also known as "Ranked Choice", "Hare", and "Alternative Vote".
181
+
182
+ Related functions: :func:`pref_voting.iterative_methods.instant_runoff_tb`, :func:`pref_voting.iterative_methods.instant_runoff_put`, :func:`pref_voting.iterative_methods.instant_runoff_with_explanation`
183
+
184
+ :Example:
185
+
186
+ .. exec_code::
187
+
188
+ from pref_voting.profiles import Profile
189
+ from pref_voting.iterative_methods import instant_runoff, ranked_choice, alternative_vote, hare
190
+
191
+ prof = Profile([[2, 1, 0], [0, 2, 1], [1, 2, 0]], [1, 2, 2])
192
+
193
+ prof.display()
194
+ instant_runoff.display(prof)
195
+ ranked_choice.display(prof)
196
+ alternative_vote.display(prof)
197
+ hare.display(prof)
198
+
199
+ """
200
+ if isinstance(profile, Profile):
201
+ if algorithm == "basic":
202
+ return _instant_runoff_basic(profile, curr_cands = curr_cands)
203
+
204
+ elif algorithm == "recursive":
205
+ return _instant_runoff_recursive(profile, curr_cands = curr_cands)
206
+
207
+ else:
208
+ raise ValueError("Algorithm must be either 'basic' or 'recursive'.")
209
+ elif isinstance(profile, ProfileWithTies):
210
+ return _instant_runoff_for_truncated_linear_orders(profile, curr_cands = curr_cands, **kwargs)
211
+ # Create some aliases for instant runoff
212
+ instant_runoff.set_name("Hare")
213
+ hare = copy.deepcopy(instant_runoff)
214
+ hare.skip_registration = True
215
+ instant_runoff.set_name("Ranked Choice")
216
+ ranked_choice = copy.deepcopy(instant_runoff)
217
+ ranked_choice.skip_registration = True
218
+ instant_runoff.set_name("Alternative Vote")
219
+ alternative_vote = copy.deepcopy(instant_runoff)
220
+ alternative_vote.skip_registration = True
221
+
222
+
223
+ # reset the name Instant Runoff
224
+ instant_runoff.set_name("Instant Runoff")
225
+
226
+ @swf(name = "Instant Runoff Ranking")
227
+ def instant_runoff_ranking(profile, curr_cands = None):
228
+ """Returns the reverse of the elimination order in the instant runoff voting process.
229
+
230
+ Args:
231
+ profile (Profile): An anonymous Profile of linear orders on a set of candidates
232
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
233
+
234
+ Returns:
235
+ A Ranking of the candidates.
236
+ """
237
+
238
+ candidates = curr_cands if curr_cands is not None else profile.candidates
239
+ cands_to_ignore = np.array([c for c in profile.candidates if c not in candidates])
240
+ rs, rcounts = profile.rankings_counts # get all the ranking data
241
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c) for c in candidates if not isin(cands_to_ignore,c)}
242
+ min_plurality_score = min(plurality_scores.values())
243
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
244
+ if plurality_scores[c] == min_plurality_score])
245
+
246
+ if len(lowest_first_place_votes) == len(candidates):
247
+ full_tie = Ranking({c:0 for c in candidates})
248
+ return full_tie
249
+
250
+ else:
251
+ rec_ranking = instant_runoff_ranking(profile, [c for c in candidates if c not in lowest_first_place_votes])
252
+ max_rank = max(rec_ranking.ranks)
253
+ rec_ranking_dict = rec_ranking.rmap
254
+ ranking = Ranking({c: rec_ranking_dict[c] if not isin(lowest_first_place_votes,c) else max_rank+1 for c in candidates})
255
+
256
+ return ranking
257
+
258
+ @vm(name = "Instant Runoff TB",
259
+ input_types=[ElectionTypes.PROFILE])
260
+ def instant_runoff_tb(profile, curr_cands = None, tie_breaker = None):
261
+ """Instant Runoff (``instant_runoff``) with tie breaking: If there is more than one candidate with the fewest number of first-place votes, then remove the candidate with lowest in the tie_breaker ranking from the profile.
262
+
263
+ Args:
264
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
265
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
266
+ tie_breaker (List[int]): A list of the candidates in the profile to be used as a tiebreaker.
267
+
268
+ Returns:
269
+ A sorted list of candidates
270
+
271
+ :Example:
272
+
273
+ .. exec_code::
274
+
275
+ from pref_voting.profiles import Profile
276
+ from pref_voting.iterative_methods import instant_runoff, instant_runoff_tb
277
+
278
+ prof = Profile([[1, 2, 0], [2, 1, 0], [0, 1, 2]], [1, 1, 1])
279
+
280
+ prof.display()
281
+ print("no tiebreaker")
282
+ instant_runoff.display(prof)
283
+ print("tie_breaker = [0, 1, 2]")
284
+ instant_runoff_tb.display(prof)
285
+ print("tie_breaker = [1, 2, 0]")
286
+ instant_runoff_tb.display(prof, tie_breaker=[1, 2, 0])
287
+
288
+ """
289
+
290
+ # the tie_breaker is any linear order (i.e., list) of the candidates
291
+ tb = tie_breaker if tie_breaker is not None else list(range(profile.num_cands))
292
+
293
+ # need the total number of all candidates in a profile to check when all candidates have been removed
294
+ num_cands = profile.num_cands
295
+
296
+ candidates = profile.candidates if curr_cands is None else curr_cands
297
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
298
+
299
+ strict_maj_size = profile.strict_maj_size()
300
+
301
+ rs, rcounts = profile.rankings_counts # get all the ranking data
302
+
303
+ winners = [c for c in candidates
304
+ if _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
305
+
306
+ while len(winners) == 0:
307
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c) for c in candidates if not isin(cands_to_ignore,c)}
308
+ min_plurality_score = min(plurality_scores.values())
309
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
310
+ if plurality_scores[c] == min_plurality_score])
311
+
312
+ cand_to_remove = lowest_first_place_votes[0]
313
+ for c in lowest_first_place_votes[1:]:
314
+ if tb.index(c) < tb.index(cand_to_remove):
315
+ cand_to_remove = c
316
+
317
+ # remove cands with lowest plurality winners
318
+ cands_to_ignore = np.concatenate((cands_to_ignore, cand_to_remove), axis=None)
319
+ if len(cands_to_ignore) == num_cands: #all the candidates where removed
320
+ winners = sorted(lowest_first_place_votes)
321
+ else:
322
+ winners = [c for c in candidates
323
+ if not isin(cands_to_ignore,c) and _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
324
+
325
+ return sorted(winners)
326
+
327
+ @vm(name = "Instant Runoff PUT",
328
+ input_types=[ElectionTypes.PROFILE])
329
+ def instant_runoff_put(profile, curr_cands = None):
330
+ """
331
+ Instant Runoff (:func:`instant_runoff`) with parallel universe tie-breaking (PUT), defined recursively: if there is a candidate with a strict majority of first-place votes, that candidate is the IRV-PUT winner; otherwise a candidate x is an IRV-PUT winner if there is some candidate y with a minimal number of first-place votes such that after removing y from the profile, x is an IRV-PUT winner.
332
+
333
+ Args:
334
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
335
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
336
+
337
+ Returns:
338
+ A sorted list of candidates
339
+
340
+ .. warning::
341
+ This will take a long time on profiles with many candidates having the same plurality scores
342
+
343
+ :Example:
344
+
345
+ .. exec_code::
346
+
347
+ from pref_voting.profiles import Profile
348
+ from pref_voting.iterative_methods import instant_runoff, instant_runoff_tb, instant_runoff_put
349
+
350
+ prof = Profile([[1, 2, 0], [2, 1, 0], [0, 1, 2]], [1, 1, 1])
351
+
352
+ prof.display()
353
+ print("no tiebreaker")
354
+ instant_runoff.display(prof)
355
+ print("tie_breaker = [0, 1, 2]")
356
+ instant_runoff_tb.display(prof, tie_breaker=[0, 1, 2])
357
+ print("tie_breaker = [0, 2, 1]")
358
+ instant_runoff_tb.display(prof, tie_breaker=[0, 2, 1])
359
+ print("tie_breaker = [1, 0, 2]")
360
+ instant_runoff_tb.display(prof, tie_breaker=[1, 0, 2])
361
+ print("tie_breaker = [1, 2, 0]")
362
+ instant_runoff_tb.display(prof, tie_breaker=[1, 2, 0])
363
+ print("tie_breaker = [2, 0, 1]")
364
+ instant_runoff_tb.display(prof, tie_breaker=[2, 0, 1])
365
+ print("tie_breaker = [2, 1, 0]")
366
+ instant_runoff_tb.display(prof, tie_breaker=[2, 1, 0])
367
+ print()
368
+ instant_runoff_put.display(prof)
369
+
370
+
371
+ """
372
+
373
+ candidates = profile.candidates if curr_cands is None else curr_cands
374
+
375
+ plurality_scores = profile.plurality_scores(candidates)
376
+
377
+ strict_maj_size = profile.strict_maj_size()
378
+ majority_winner = [cand for cand, score in plurality_scores.items() if score >= strict_maj_size]
379
+
380
+ if len(majority_winner) > 0:
381
+ return majority_winner
382
+
383
+ original_num_cands = len(candidates)
384
+
385
+ # immediately eliminate candidates with plurality score 0
386
+ # this is safe, because every elimination order will eliminate all these candidates first (in some order)
387
+ candidates = [cand for cand in candidates if plurality_scores[cand] > 0]
388
+ if len(candidates) < original_num_cands:
389
+ # if we removed some candidates, we need to update the plurality scores
390
+ plurality_scores = profile.plurality_scores(candidates)
391
+
392
+ # plurality losers
393
+ worst_score = min(plurality_scores.values())
394
+ cands_to_remove = [cand for cand, value in plurality_scores.items() if value == worst_score]
395
+
396
+ winners = []
397
+ for cand_to_remove in cands_to_remove:
398
+ new_winners = instant_runoff_put(profile, curr_cands = [c for c in candidates if not c == cand_to_remove])
399
+ winners = winners + new_winners
400
+
401
+ return sorted(set(winners))
402
+
403
+
404
+ # Create some aliases for instant runoff
405
+ instant_runoff_put.set_name("Hare PUT")
406
+ hare_put = copy.deepcopy(instant_runoff_put)
407
+ hare_put.skip_registration = True
408
+ instant_runoff_put.set_name("Ranked Choice PUT")
409
+ ranked_choice_put = copy.deepcopy(instant_runoff_put)
410
+ ranked_choice_put.skip_registration = True
411
+
412
+ # reset the name Instant Runoff
413
+ instant_runoff_put.set_name("Instant Runoff PUT")
414
+
415
+
416
+ def instant_runoff_with_explanation(profile, curr_cands = None):
417
+ """
418
+ Instant Runoff with an explanation. In addition to the winner(s), return the order in which the candidates are eliminated as a list of lists.
419
+
420
+ Args:
421
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
422
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
423
+
424
+ Returns:
425
+ A sorted list of candidates
426
+
427
+ A list describing the order in which candidates are eliminated
428
+
429
+ :Example:
430
+
431
+ .. exec_code::
432
+
433
+ from pref_voting.profiles import Profile
434
+ from pref_voting.iterative_methods import instant_runoff, instant_runoff_with_explanation
435
+
436
+
437
+ prof = Profile([[2, 1, 0], [0, 2, 1], [1, 2, 0]], [1, 2, 2])
438
+ prof.display()
439
+ instant_runoff.display(prof)
440
+ ws, exp = instant_runoff_with_explanation(prof)
441
+ print(f"winning set: {ws}")
442
+ print(f"order of elimination: {exp}")
443
+
444
+ prof = Profile([[1, 2, 0], [2, 1, 0], [0, 1, 2]], [1, 1, 1])
445
+ prof.display()
446
+ instant_runoff.display(prof)
447
+ ws, exp = instant_runoff_with_explanation(prof)
448
+ print(f"winning set: {ws}")
449
+ print(f"order of elimination: {exp}")
450
+
451
+ prof = Profile([[2, 0, 1, 3], [2, 0, 3, 1], [3, 0, 1, 2], [3, 2, 1, 0], [0, 2, 1, 3]], [1, 1, 1, 1, 1])
452
+ prof.display()
453
+ instant_runoff.display(prof)
454
+ ws, exp = instant_runoff_with_explanation(prof)
455
+ print(f"winning set: {ws}")
456
+ print(f"order of elimination: {exp}")
457
+
458
+ """
459
+ # need the total number of all candidates in a profile to check when all candidates have been removed
460
+ num_cands = profile.num_cands
461
+
462
+ candidates = profile.candidates if curr_cands is None else curr_cands
463
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
464
+
465
+ strict_maj_size = profile.strict_maj_size()
466
+
467
+ rs, rcounts = profile.rankings_counts # get all the ranking data
468
+
469
+
470
+ winners = [c for c in candidates
471
+ if _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
472
+ elims_list = list()
473
+
474
+ while len(winners) == 0:
475
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c) for c in candidates
476
+ if not isin(cands_to_ignore,c)}
477
+ min_plurality_score = min(plurality_scores.values())
478
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
479
+ if plurality_scores[c] == min_plurality_score])
480
+
481
+ elims_list.append(list(lowest_first_place_votes))
482
+
483
+ # remove cands with lowest plurality winners
484
+ cands_to_ignore = np.concatenate((cands_to_ignore, lowest_first_place_votes), axis=None)
485
+ if len(cands_to_ignore) == num_cands: # removed all of the candidates
486
+ winners = sorted(lowest_first_place_votes)
487
+ else:
488
+ winners = [c for c in candidates
489
+ if not isin(cands_to_ignore,c) and _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
490
+
491
+ return sorted(winners), elims_list
492
+
493
+ @vm(name="Instant Runoff (Truncated Linear Orders)",
494
+ input_types=[ElectionTypes.TRUNCATED_LINEAR_PROFILE])
495
+ def instant_runoff_for_truncated_linear_orders(profile, curr_cands = None, threshold = None, hide_warnings = True):
496
+ """
497
+ Instant Runoff for Truncated Linear Orders. Iteratively remove the candidates with the fewest number of first place votes, until there is a candidate with more than the threshold number of first-place votes.
498
+ If a threshold is not set, then it is strictly more than half of the non-empty ballots.
499
+
500
+ Args:
501
+ profile (ProfileWithTies): An anonymous profile with no ties in the ballots (note that ProfileWithTies allows for truncated linear orders).
502
+ threshold (int, float, optional): The threshold needed to win the election. If it is not set, then it is strictly more than half of the remaining ballots.
503
+ hide_warnings (bool, optional): Show or hide the warnings when more than one candidate is eliminated in a round.
504
+
505
+ Returns:
506
+ A sorted list of candidates
507
+
508
+ .. note:: This is the simultaneous version of instant runoff, not the parallel-universe tiebreaking version. It is intended to be run on profiles with large number of voters in which there is a very low probability of a tie in the fewest number of first place votes. A warning is displayed when more than one candidate is eliminated.
509
+
510
+ :Example:
511
+
512
+ .. exec_code::
513
+
514
+ from pref_voting.profiles_with_ties import ProfileWithTies
515
+ from pref_voting.iterative_methods import instant_runoff_for_truncated_linear_orders
516
+
517
+ prof = ProfileWithTies([{0:1, 1:1},{0:1, 1:2, 2:3, 3:4}, {0:1, 1:3, 2:3}, {3:2}, {0:1}, {0:1}, {}, {}])
518
+ prof.display()
519
+
520
+ tprof, report = prof.truncate_overvotes()
521
+ for r, new_r, count in report:
522
+ print(f"{r} --> {new_r}: {count}")
523
+ tprof.display()
524
+ instant_runoff_for_truncated_linear_orders.display(tprof)
525
+
526
+
527
+ """
528
+
529
+ assert all([not r.has_overvote() for r in profile.rankings]), "Instant Runoff is only defined when all the ballots are truncated linear orders."
530
+
531
+ curr_cands = profile.candidates if curr_cands is None else curr_cands
532
+
533
+ # we need to remove empty rankings during the algorithm, so make a copy of the profile
534
+ prof2 = copy.deepcopy(profile)
535
+
536
+ _prof = prof2.remove_candidates([c for c in profile.candidates if c not in curr_cands])
537
+
538
+ # remove the empty rankings
539
+ _prof.remove_empty_rankings()
540
+
541
+ threshold = threshold if threshold is not None else _prof.strict_maj_size()
542
+
543
+ remaining_candidates = _prof.candidates
544
+
545
+ pl_scores = _prof.plurality_scores()
546
+ max_pl_score = max(pl_scores.values())
547
+
548
+ while max_pl_score < threshold:
549
+
550
+ reduced_prof = _prof.remove_candidates([c for c in _prof.candidates if c not in remaining_candidates])
551
+
552
+ # after removing the candidates, there might be some empty ballots.
553
+ reduced_prof.remove_empty_rankings()
554
+
555
+ pl_scores = reduced_prof.plurality_scores()
556
+ min_pl_score = min(pl_scores.values())
557
+
558
+ cands_to_remove = [c for c in pl_scores.keys() if pl_scores[c] == min_pl_score]
559
+
560
+ if not hide_warnings and len(cands_to_remove) > 1:
561
+ print(f"Warning: multiple candidates removed in a round: {', '.join(map(str,cands_to_remove))}")
562
+
563
+ if len(cands_to_remove) == len(reduced_prof.candidates):
564
+ # all remaining candidates have the same plurality score.
565
+ break
566
+
567
+ # possibly update the threshold, so that it is a strict majority of the remaining ballots
568
+ threshold = threshold if threshold is not None else reduced_prof.strict_maj_size()
569
+ max_pl_score = max(pl_scores.values())
570
+
571
+ remaining_candidates = [c for c in remaining_candidates if c not in cands_to_remove]
572
+
573
+
574
+ reduced_prof = _prof.remove_candidates([c for c in _prof.candidates if c not in remaining_candidates])
575
+
576
+ # after removing the candidates, there might be some empty ballots.
577
+ reduced_prof.remove_empty_rankings()
578
+
579
+ pl_scores = reduced_prof.plurality_scores()
580
+
581
+ max_pl_score = max(pl_scores.values())
582
+
583
+ return sorted([c for c in pl_scores.keys() if pl_scores[c] == max_pl_score])
584
+
585
+ def top_n_instant_runoff_for_truncated_linear_orders(
586
+ profile,
587
+ n,
588
+ curr_cands = None,
589
+ threshold = None,
590
+ hide_warnings = True):
591
+ """
592
+ Returns the top n candidates according to the Instant Runoff method: Iteratively remove candidates until there are at most n candidates left. Note that since there may be multiple candidates with the lowest plurality score, it may not be possible to reduce to exactly n candidates, in which case the function will return None.
593
+ """
594
+
595
+ assert all([not r.has_overvote() for r in profile.rankings]), "Instant Runoff is only defined when all the ballots are truncated linear orders."
596
+
597
+ curr_cands = profile.candidates if curr_cands is None else curr_cands
598
+
599
+ if len(curr_cands) <= n:
600
+ return sorted(curr_cands)
601
+
602
+ # we need to remove empty rankings during the algorithm, so make a copy of the profile
603
+ prof2 = copy.deepcopy(profile)
604
+
605
+ _prof = prof2.remove_candidates([c for c in profile.candidates if c not in curr_cands])
606
+
607
+ # remove the empty rankings
608
+ _prof.remove_empty_rankings()
609
+
610
+ remaining_candidates = _prof.candidates
611
+
612
+ pl_scores = _prof.plurality_scores()
613
+
614
+ while len(remaining_candidates) > n:
615
+ reduced_prof = _prof.remove_candidates([c for c in _prof.candidates if c not in remaining_candidates])
616
+
617
+ # after removing the candidates, there might be some empty ballots.
618
+ reduced_prof.remove_empty_rankings()
619
+
620
+ pl_scores = reduced_prof.plurality_scores()
621
+ min_pl_score = min(pl_scores.values())
622
+ cands_to_remove = [c for c in pl_scores.keys() if pl_scores[c] == min_pl_score]
623
+
624
+ if not hide_warnings and len(cands_to_remove) > 1:
625
+ print(f"Warning: multiple candidates removed in a round: {', '.join(map(str,cands_to_remove))}")
626
+
627
+ if len(cands_to_remove) == len(reduced_prof.candidates):
628
+ # all remaining candidates have the same plurality score.
629
+ remaining_candidates = reduced_prof.candidates
630
+ break
631
+
632
+ remaining_candidates = [c for c in remaining_candidates if c not in cands_to_remove]
633
+
634
+ if len(remaining_candidates) != n:
635
+ if not hide_warnings:
636
+ print(f"Warning: cannot reduce to exactly {n} candidates.")
637
+ return None
638
+
639
+ return sorted(remaining_candidates)
640
+
641
+
642
+ @vm(name="Bottom-Two-Runoff Instant Runoff",
643
+ input_types=[ElectionTypes.PROFILE])
644
+ def bottom_two_runoff_instant_runoff(profile, curr_cands = None):
645
+ """Find the two candidates with the lowest two plurality scores, remove the one who loses head-to-head to the other, and repeat until a single candidate remains.
646
+
647
+ If there is a tie for lowest or second lowest plurality score, consider all head-to-head matches between a candidate with lowest and a candidate with second lowest plurality score, and remove all the losers of the head-to-head matches, unless this would remove all candidates.
648
+
649
+ .. note::
650
+ BTR-IRV is a Condorcet consistent voting method, i.e., if a Condorcet winner exists, then BTR-IRV will elect the Condorcet winner.
651
+
652
+ .. seealso::
653
+
654
+ Related functions: :func:`pref_voting.iterative_methods.bottom_two_runoff_instant_runoff_put`
655
+
656
+ Args:
657
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
658
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
659
+
660
+ Returns:
661
+ A sorted list of candidates
662
+ """
663
+ candidates = profile.candidates if curr_cands is None else curr_cands
664
+
665
+ if len(candidates) == 1:
666
+ return candidates
667
+
668
+ plurality_scores = profile.plurality_scores(candidates)
669
+ worst_score = min(plurality_scores.values())
670
+ cands_with_lowest_plurality_score = [cand for cand, value in plurality_scores.items() if value == worst_score]
671
+
672
+ if len(cands_with_lowest_plurality_score) > 1:
673
+ cands_with_second_lowest_plurality_score = cands_with_lowest_plurality_score
674
+ else:
675
+ second_lowest_plurality_score = sorted(plurality_scores.values())[1]
676
+ cands_with_second_lowest_plurality_score = [cand for cand, value in plurality_scores.items() if value == second_lowest_plurality_score]
677
+
678
+ cands_to_remove = []
679
+
680
+ for c1 in cands_with_lowest_plurality_score:
681
+ for c2 in cands_with_second_lowest_plurality_score:
682
+ if c1 != c2:
683
+ if profile.margin(c1,c2) <= 0:
684
+ cands_to_remove.append(c1)
685
+ else:
686
+ cands_to_remove.append(c2)
687
+
688
+ if len(set(cands_to_remove)) == len(candidates):
689
+ return candidates
690
+ else:
691
+ return bottom_two_runoff_instant_runoff(profile, [cand for cand in candidates if cand not in set(cands_to_remove)])
692
+
693
+ @vm(name="Bottom-Two-Runoff Instant Runoff PUT",
694
+ input_types=[ElectionTypes.PROFILE])
695
+ def bottom_two_runoff_instant_runoff_put(profile, curr_cands = None):
696
+ """Find the two candidates with the lowest two plurality scores, remove the one who loses head-to-head to the other, and repeat until a single candidate remains. Parallel-universe tiebreaking is used to break ties for lowest or second lowest plurality scores.
697
+
698
+ .. note::
699
+ BTR-IRV is a Condorcet consistent voting method, i.e., if a Condorcet winner exists, then BTR-IRV will elect the Condorcet winner.
700
+
701
+ Args:
702
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
703
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
704
+
705
+ Returns:
706
+ A sorted list of candidates
707
+ """
708
+ candidates = profile.candidates if curr_cands is None else curr_cands
709
+
710
+ if len(candidates) == 1:
711
+ return candidates
712
+
713
+ plurality_scores = profile.plurality_scores(candidates)
714
+ worst_score = min(plurality_scores.values())
715
+ cands_with_lowest_plurality_score = [cand for cand, value in plurality_scores.items() if value == worst_score]
716
+
717
+ if len(cands_with_lowest_plurality_score) > 1:
718
+ cands_with_second_lowest_plurality_score = cands_with_lowest_plurality_score
719
+ else:
720
+ second_lowest_plurality_score = sorted(plurality_scores.values())[1]
721
+ cands_with_second_lowest_plurality_score = [cand for cand, value in plurality_scores.items() if value == second_lowest_plurality_score]
722
+
723
+ winners = []
724
+
725
+ for c1 in cands_with_lowest_plurality_score:
726
+ for c2 in cands_with_second_lowest_plurality_score:
727
+ if c1 != c2:
728
+ if profile.margin(c1,c2) <= 0:
729
+ additional_winners = bottom_two_runoff_instant_runoff_put(profile, curr_cands = [c for c in candidates if not c == c1])
730
+ else:
731
+ additional_winners = bottom_two_runoff_instant_runoff_put(profile, curr_cands = [c for c in candidates if not c == c2])
732
+
733
+ winners = winners + additional_winners
734
+
735
+ return sorted(set(winners))
736
+
737
+
738
+ @vm(name = "Plurality with Runoff PUT",
739
+ input_types=[ElectionTypes.PROFILE])
740
+ def plurality_with_runoff_put(profile, curr_cands = None):
741
+ """If there is a majority winner then that candidate is the Plurality with Runoff winner. Otherwise hold a runoff between the top two candidates: the candidate with the most first place votes and the candidate with the 2nd most first place votes (or perhaps tied for the most first place votes). In the case of multiple candidates tied for the most or 2nd most first place votes, use parallel-universe tiebreaking: a candidate is a Plurality with Runoff winner if it is a winner in some runoff as described. If the candidates are all tied for the most first place votes, then all candidates are winners.
742
+
743
+ Args:
744
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
745
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
746
+
747
+ Returns:
748
+ A sorted list of candidates
749
+
750
+ .. note::
751
+ Plurality with Runoff is the same as Instant Runoff when there are 3 candidates, but they can give different answers with 4 or more candidates.
752
+
753
+ :Example:
754
+
755
+ .. exec_code::
756
+
757
+ from pref_voting.profiles import Profile
758
+ from pref_voting.iterative_methods import instant_runoff, plurality_with_runoff_put
759
+
760
+ prof = Profile([[0, 1, 2, 3], [3, 1, 2, 0], [2, 0, 3, 1], [1, 2, 3, 0], [2, 3, 0, 1], [0, 3, 2, 1]], [2, 1, 2, 2, 1, 2])
761
+ prof.display()
762
+ instant_runoff.display(prof)
763
+ plurality_with_runoff_put.display(prof)
764
+
765
+ """
766
+
767
+ curr_cands = profile.candidates if curr_cands is None else curr_cands
768
+
769
+ if len(curr_cands) == 1:
770
+ return list(curr_cands)
771
+
772
+ plurality_scores = profile.plurality_scores(curr_cands = curr_cands)
773
+
774
+ max_plurality_score = max(plurality_scores.values())
775
+
776
+ first = [c for c in curr_cands if plurality_scores[c] == max_plurality_score]
777
+
778
+ second = list()
779
+ if len(first) == 1:
780
+ second_plurality_score = list(reversed(sorted(plurality_scores.values())))[1]
781
+ second = [c for c in curr_cands if plurality_scores[c] == second_plurality_score]
782
+
783
+ if len(second) > 0:
784
+ all_runoff_pairs = product(first, second)
785
+ else:
786
+ all_runoff_pairs = [(c1,c2) for c1,c2 in product(first, first) if c1 != c2]
787
+
788
+ winners = list()
789
+ for c1, c2 in all_runoff_pairs:
790
+
791
+ if profile.margin(c1,c2) > 0:
792
+ winners.append(c1)
793
+ elif profile.margin(c1,c2) < 0:
794
+ winners.append(c2)
795
+ elif profile.margin(c1,c2) == 0:
796
+ winners.append(c1)
797
+ winners.append(c2)
798
+
799
+ return sorted(list(set(winners)))
800
+
801
+ def plurality_with_runoff_put_with_explanation(profile, curr_cands = None):
802
+ """Plurality with Runoff with an explanation. In addition to the winner(s), return list of the pairs of candidate that move on to runoff round.
803
+
804
+ Args:
805
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
806
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
807
+
808
+ Returns:
809
+ A sorted list of candidates
810
+
811
+ """
812
+
813
+ curr_cands = profile.candidates if curr_cands is None else curr_cands
814
+
815
+ if len(curr_cands) == 1:
816
+ return list(curr_cands)
817
+
818
+ plurality_scores = profile.plurality_scores(curr_cands = curr_cands)
819
+
820
+ max_plurality_score = max(plurality_scores.values())
821
+
822
+ first = [c for c in curr_cands if plurality_scores[c] == max_plurality_score]
823
+ second = list()
824
+ if len(first) == 1:
825
+ second_plurality_score = list(reversed(sorted(plurality_scores.values())))[1]
826
+ second = [c for c in curr_cands if plurality_scores[c] == second_plurality_score]
827
+
828
+ if len(second) > 0:
829
+ all_runoff_pairs = list(product(first, second))
830
+ else:
831
+ all_runoff_pairs = [(c1,c2) for c1,c2 in product(first, first) if c1 != c2]
832
+
833
+ winners = list()
834
+ for c1, c2 in all_runoff_pairs:
835
+
836
+ if profile.margin(c1,c2) > 0:
837
+ winners.append(c1)
838
+ elif profile.margin(c1,c2) < 0:
839
+ winners.append(c2)
840
+ elif profile.margin(c1,c2) == 0:
841
+ winners.append(c1)
842
+ winners.append(c2)
843
+
844
+ return sorted(list(set(winners))), list(all_runoff_pairs)
845
+
846
+ @vm(name = "Coombs",
847
+ input_types=[ElectionTypes.PROFILE])
848
+ def coombs(profile, curr_cands = None):
849
+ """If there is a majority winner then that candidate is the Coombs winner. If there is no majority winner, then remove all candidates that are ranked last by the greatest number of voters. Continue removing candidates with the most last-place votes until there is a candidate with a majority of first place votes.
850
+
851
+ .. important::
852
+ If there is more than one candidate with the largest number of last-place votes, then *all* such candidates are removed from the profile.
853
+
854
+ Args:
855
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
856
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
857
+
858
+ Returns:
859
+ A sorted list of candidates
860
+
861
+ .. seealso::
862
+
863
+ :func:`pref_voting.iterative_methods.coombs_with_tb`, :func:`pref_voting.iterative_methods.coomb_put`, :func:`pref_voting.iterative_methods.coombs_with_explanation`
864
+
865
+ :Example:
866
+
867
+ .. exec_code::
868
+
869
+ from pref_voting.profiles import Profile
870
+ from pref_voting.iterative_methods import instant_runoff, coombs
871
+
872
+ prof = Profile([[2, 1, 0], [0, 2, 1], [1, 2, 0]], [1, 2, 2])
873
+
874
+ prof.display()
875
+ coombs.display(prof)
876
+ instant_runoff.display(prof)
877
+
878
+ """
879
+
880
+ # need the total number of all candidates in a profile to check when all candidates have been removed
881
+ num_cands = profile.num_cands
882
+
883
+ candidates = profile.candidates if curr_cands is None else curr_cands
884
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
885
+
886
+ strict_maj_size = profile.strict_maj_size()
887
+
888
+ rs, rcounts = profile.rankings_counts # get all the ranking data
889
+
890
+ winners = [c for c in candidates
891
+ if _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
892
+
893
+ while len(winners) == 0:
894
+
895
+ last_place_scores = {c: _num_rank_last(rs, rcounts, cands_to_ignore, c) for c in candidates
896
+ if not isin(cands_to_ignore,c)}
897
+ max_last_place_score = max(last_place_scores.values())
898
+ greatest_last_place_votes = np.array([c for c in last_place_scores.keys()
899
+ if last_place_scores[c] == max_last_place_score])
900
+
901
+ # remove candidates ranked last by the greatest number of voters
902
+ cands_to_ignore = np.concatenate((cands_to_ignore, greatest_last_place_votes), axis=None)
903
+
904
+ if len(cands_to_ignore) == num_cands: # removed all candidates
905
+ winners = list(greatest_last_place_votes)
906
+ else:
907
+ winners = [c for c in candidates
908
+ if not isin(cands_to_ignore,c) and _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
909
+
910
+ return sorted(winners)
911
+
912
+ @vm(name = "Coombs TB",
913
+ input_types=[ElectionTypes.PROFILE])
914
+ def coombs_tb(profile, curr_cands = None, tie_breaker=None):
915
+ """
916
+ Coombs with a fixed tie-breaking rule: The tie-breaking rule is any linear order (i.e., list) of the candidates. The default rule is to order the candidates as follows: 0,....,num_cands-1.
917
+
918
+ Args:
919
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
920
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
921
+ tie_breaker (List[int]): A list of the candidates in the profile to be used as a tiebreaker.
922
+
923
+ Returns:
924
+
925
+ A sorted list of candidates
926
+
927
+ :Example:
928
+
929
+ .. exec_code::
930
+
931
+ from pref_voting.profiles import Profile
932
+ from pref_voting.iterative_methods import coombs, coombs_tb
933
+
934
+ prof = Profile([[2, 0, 1], [0, 2, 1], [1, 0, 2], [2, 1, 0], [0, 1, 2]], [1, 1, 1, 2, 1])
935
+ prof.display()
936
+ print("no tiebreaker")
937
+ coombs.display(prof)
938
+ print("tie_breaker = [0, 1, 2]")
939
+ coombs_tb.display(prof)
940
+ print("tie_breaker = [2, 1, 0]")
941
+ coombs_tb.display(prof, tie_breaker=[2, 1, 0])
942
+
943
+ """
944
+
945
+ # the tie_breaker is any linear order (i.e., list) of the candidates
946
+ tb = tie_breaker if tie_breaker is not None else list(range(profile.num_cands))
947
+
948
+ num_cands = profile.num_cands
949
+ candidates = profile.candidates if curr_cands is None else curr_cands
950
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
951
+
952
+ strict_maj_size = profile.strict_maj_size()
953
+
954
+ rs, rcounts = profile.rankings_counts # get all the ranking data
955
+
956
+ winners = [c for c in candidates
957
+ if _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
958
+
959
+ while len(winners) == 0:
960
+
961
+ last_place_scores = {c: _num_rank_last(rs, rcounts, cands_to_ignore, c) for c in candidates
962
+ if not isin(cands_to_ignore,c)}
963
+ max_last_place_score = max(last_place_scores.values())
964
+ greatest_last_place_votes = [c for c in last_place_scores.keys() if last_place_scores[c] == max_last_place_score]
965
+
966
+ # select the candidate to remove using the tie-breaking rule (a linear order over the candidates)
967
+ cand_to_remove = greatest_last_place_votes[0]
968
+ for c in greatest_last_place_votes[1:]:
969
+ if tb.index(c) < tb.index(cand_to_remove):
970
+ cand_to_remove = c
971
+
972
+ # remove candidates ranked last by the greatest number of voters
973
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array([cand_to_remove])), axis=None)
974
+
975
+ if len(cands_to_ignore) == num_cands:
976
+ winners = list(greatest_last_place_votes)
977
+ else:
978
+ winners = [c for c in candidates
979
+ if not isin(cands_to_ignore,c) and _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
980
+
981
+ return sorted(winners)
982
+
983
+ @vm(name = "Coombs PUT",
984
+ input_types=[ElectionTypes.PROFILE])
985
+ def coombs_put(profile, curr_cands = None):
986
+ """Coombs with parallel universe tie-breaking (PUT), defined recursively: if there is a candidate with a strict majority of first-place votes, that candidate is the Coombs-PUT winner; otherwise a candidate x is a Coombs-PUT winner if there is some candidate y with a maximal number of last-place votes such that after removing y from the profile, x is a Coombs-PUT winner.
987
+
988
+ Args:
989
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
990
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
991
+
992
+ Returns:
993
+ A sorted list of candidates
994
+
995
+ .. warning::
996
+ This will take a long time on profiles with many candidates having the same number of last-place votes.
997
+
998
+ :Example:
999
+
1000
+ .. exec_code::
1001
+
1002
+ from pref_voting.profiles import Profile
1003
+ from pref_voting.iterative_methods import coombs, coombs_tb, coombs_put
1004
+
1005
+ prof = Profile([[2, 0, 1], [1, 0, 2], [0, 1, 2]], [2, 1, 1])
1006
+
1007
+ prof.display()
1008
+ print("no tiebreaker")
1009
+ coombs.display(prof)
1010
+ print("tie_breaker = [0, 1, 2]")
1011
+ coombs_tb.display(prof, tie_breaker=[0, 1, 2])
1012
+ print("tie_breaker = [0, 2, 1]")
1013
+ coombs_tb.display(prof, tie_breaker=[0, 2, 1])
1014
+ print("tie_breaker = [1, 0, 2]")
1015
+ coombs_tb.display(prof, tie_breaker=[1, 0, 2])
1016
+ print("tie_breaker = [1, 2, 0]")
1017
+ coombs_tb.display(prof, tie_breaker=[1, 2, 0])
1018
+ print("tie_breaker = [2, 0, 1]")
1019
+ coombs_tb.display(prof, tie_breaker=[2, 0, 1])
1020
+ print("tie_breaker = [2, 1, 0]")
1021
+ coombs_tb.display(prof, tie_breaker=[2, 1, 0])
1022
+ print()
1023
+ coombs_put.display(prof)
1024
+ """
1025
+
1026
+ candidates = profile.candidates if curr_cands is None else curr_cands
1027
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1028
+
1029
+ strict_maj_size = profile.strict_maj_size()
1030
+ majority_winner = [cand for cand, value in profile.plurality_scores(candidates).items() if value >= strict_maj_size]
1031
+
1032
+ if len(majority_winner) > 0:
1033
+ return majority_winner
1034
+
1035
+ rs, rcounts = profile.rankings_counts # get all the ranking data
1036
+
1037
+ last_place_scores = {c: _num_rank_last(rs, rcounts, cands_to_ignore, c) for c in candidates}
1038
+ max_last_place_score = max(last_place_scores.values())
1039
+ cands_to_remove = [c for c in last_place_scores.keys() if last_place_scores[c] == max_last_place_score]
1040
+
1041
+ winners = []
1042
+ for cand_to_remove in cands_to_remove:
1043
+ new_winners = coombs_put(profile, curr_cands = [c for c in candidates if not c == cand_to_remove])
1044
+ winners = winners + new_winners
1045
+
1046
+ return sorted(set(winners))
1047
+
1048
+ def coombs_with_explanation(profile, curr_cands = None):
1049
+ """
1050
+ Coombs with an explanation. In addition to the winner(s), return the order in which the candidates are eliminated as a list of lists.
1051
+
1052
+ Args:
1053
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1054
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1055
+
1056
+ Returns:
1057
+ A sorted list of candidates
1058
+
1059
+ A list describing the order in which candidates are eliminated
1060
+
1061
+ .. exec_code::
1062
+
1063
+ from pref_voting.profiles import Profile
1064
+ from pref_voting.iterative_methods import coombs, coombs_with_explanation
1065
+
1066
+
1067
+ prof = Profile([[2, 1, 0], [0, 2, 1], [1, 2, 0]], [1, 2, 2])
1068
+ prof.display()
1069
+ coombs.display(prof)
1070
+ ws, exp = coombs_with_explanation(prof)
1071
+ print(f"winning set: {ws}")
1072
+ print(f"order of elimination: {exp}")
1073
+
1074
+ prof = Profile([[1, 0, 3, 2], [2, 3, 1, 0], [2, 0, 3, 1], [1, 2, 3, 0]], [1, 1, 1, 1])
1075
+ prof.display()
1076
+ coombs.display(prof)
1077
+ ws, exp = coombs_with_explanation(prof)
1078
+ print(f"winning set: {ws}")
1079
+ print(f"order of elimination: {exp}")
1080
+
1081
+
1082
+ """
1083
+ num_cands = profile.num_cands
1084
+
1085
+ candidates = profile.candidates if curr_cands is None else curr_cands
1086
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1087
+
1088
+ strict_maj_size = profile.strict_maj_size()
1089
+
1090
+ rs, rcounts = profile.rankings_counts # get all the ranking data
1091
+
1092
+ winners = [c for c in candidates
1093
+ if _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
1094
+
1095
+ elims_list = list()
1096
+ while len(winners) == 0:
1097
+
1098
+ last_place_scores = {c: _num_rank_last(rs, rcounts, cands_to_ignore, c) for c in candidates
1099
+ if not isin(cands_to_ignore,c)}
1100
+ max_last_place_score = max(last_place_scores.values())
1101
+ greatest_last_place_votes = np.array([c for c in last_place_scores.keys()
1102
+ if last_place_scores[c] == max_last_place_score])
1103
+
1104
+ elims_list.append(list(greatest_last_place_votes))
1105
+ # remove candidates ranked last by the greatest number of voters
1106
+ cands_to_ignore = np.concatenate((cands_to_ignore, greatest_last_place_votes), axis=None)
1107
+
1108
+ if len(cands_to_ignore) == num_cands:
1109
+ winners = list(greatest_last_place_votes)
1110
+ else:
1111
+ winners = [c for c in candidates
1112
+ if not isin(cands_to_ignore,c) and _num_rank_first(rs, rcounts, cands_to_ignore, c) >= strict_maj_size]
1113
+
1114
+ return sorted(winners), elims_list
1115
+
1116
+ @vm(name = "Baldwin",
1117
+ input_types=[ElectionTypes.PROFILE])
1118
+ def baldwin(profile, curr_cands = None):
1119
+ """Iteratively remove all candidates with the lowest Borda score until a single candidate remains. If, at any stage, all candidates have the same Borda score, then all (remaining) candidates are winners.
1120
+
1121
+ .. note::
1122
+ Baldwin is a Condorcet consistent voting method, i.e., if a Condorcet winner exists, then Baldwin will elect the Condorcet winner.
1123
+
1124
+ Args:
1125
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1126
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1127
+
1128
+ Returns:
1129
+ A sorted list of candidates
1130
+
1131
+ .. seealso::
1132
+
1133
+ :func:`pref_voting.iterative_methods.baldwin_with_tb`, :func:`pref_voting.iterative_methods.baldwin`, :func:`pref_voting.iterative_methods.baldwin_with_explanation`
1134
+
1135
+ :Example:
1136
+
1137
+ .. exec_code::
1138
+
1139
+ from pref_voting.profiles import Profile
1140
+ from pref_voting.iterative_methods import baldwin
1141
+
1142
+ prof = Profile([[1, 0, 2, 3], [3, 1, 0, 2], [2, 0, 3, 1]], [2, 1, 1])
1143
+
1144
+ prof.display()
1145
+ baldwin.display(prof)
1146
+ """
1147
+ all_num_cands = profile.num_cands
1148
+ candidates = profile.candidates if curr_cands is None else curr_cands
1149
+ rcounts = profile._rcounts # get all the ranking data
1150
+ rankings = profile._rankings if curr_cands is None else _find_updated_profile(profile._rankings, np.array([c for c in profile.candidates if c not in candidates]), all_num_cands)
1151
+ num_cands = len(candidates)
1152
+ cands_to_ignore = np.empty(0)
1153
+ borda_scores = {c: _borda_score(rankings, rcounts, len(candidates), c) for c in candidates}
1154
+
1155
+ min_borda_score = min(list(borda_scores.values()))
1156
+ last_place_borda_scores = [c for c in candidates
1157
+ if c in borda_scores.keys() and borda_scores[c] == min_borda_score]
1158
+
1159
+ cands_to_ignore = np.concatenate((cands_to_ignore, last_place_borda_scores), axis=None)
1160
+
1161
+ winners = list()
1162
+ if cands_to_ignore.shape[0] == num_cands: # all candidates have lowest Borda score
1163
+ winners = sorted(last_place_borda_scores)
1164
+ else: # remove the candidates with lowest Borda score
1165
+ num_cands = len(candidates)
1166
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1167
+
1168
+ while len(winners) == 0:
1169
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_cands - cands_to_ignore.shape[0], c) for c in candidates if not isin(cands_to_ignore, c)}
1170
+
1171
+ min_borda_score = min(borda_scores.values())
1172
+ last_place_borda_scores = [c for c in borda_scores.keys() if borda_scores[c] == min_borda_score]
1173
+
1174
+ cands_to_ignore = np.concatenate((cands_to_ignore, last_place_borda_scores), axis=None)
1175
+
1176
+ if cands_to_ignore.shape[0] == num_cands: # removed all remaining candidates
1177
+ winners = sorted(last_place_borda_scores)
1178
+ elif num_cands - cands_to_ignore.shape[0] == 1: # only one candidate remains
1179
+ winners = sorted([c for c in candidates if c not in cands_to_ignore])
1180
+ else:
1181
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1182
+ return sorted(winners)
1183
+
1184
+ @vm(name = "Baldwin TB",
1185
+ input_types=[ElectionTypes.PROFILE])
1186
+ def baldwin_tb(profile, curr_cands = None, tie_breaker=None):
1187
+ """
1188
+ Baldwin with a fixed tie-breaking rule: The tie-breaking rule is any linear order (i.e., list) of the candidates. The default rule is to order the candidates as follows: 0,....,num_cands-1.
1189
+
1190
+ Args:
1191
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1192
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1193
+ tie_breaker (List[int]): A list of the candidates in the profile to be used as a tiebreaker.
1194
+
1195
+ Returns:
1196
+
1197
+ A sorted list of candidates
1198
+
1199
+ :Example:
1200
+
1201
+ .. exec_code::
1202
+
1203
+ from pref_voting.profiles import Profile
1204
+ from pref_voting.iterative_methods import baldwin, baldwin_tb
1205
+
1206
+ prof = Profile([[0, 2, 1, 3], [1, 3, 0, 2], [2, 3, 0, 1]], [1, 1, 1])
1207
+ prof.display()
1208
+ print("no tiebreaker")
1209
+ baldwin.display(prof)
1210
+ print("tie_breaker = [0, 1, 2, 3]")
1211
+ baldwin_tb.display(prof)
1212
+ print("tie_breaker = [2, 1, 0, 3]")
1213
+ baldwin_tb.display(prof, tie_breaker=[2, 1, 0, 3])
1214
+
1215
+ """
1216
+
1217
+ # the tie_breaker is any linear order (i.e., list) of the candidates
1218
+ tb = tie_breaker if tie_breaker is not None else list(range(profile.num_cands))
1219
+
1220
+ if len(profile.candidates) <= 1:
1221
+ return sorted(profile.candidates)
1222
+
1223
+ all_num_cands = profile.num_cands
1224
+ candidates = profile.candidates if curr_cands is None else curr_cands
1225
+ rcounts = profile._rcounts # get all the ranking data
1226
+ rankings = profile._rankings if curr_cands is None else _find_updated_profile(profile._rankings, np.array([c for c in profile.candidates if c not in candidates]), all_num_cands)
1227
+ num_cands = len(candidates)
1228
+ cands_to_ignore = np.empty(0)
1229
+ borda_scores = {c: _borda_score(rankings, rcounts, num_cands, c) for c in candidates}
1230
+
1231
+ min_borda_score = min(list(borda_scores.values()))
1232
+ last_place_borda_scores = [c for c in candidates
1233
+ if c in borda_scores.keys() and borda_scores[c] == min_borda_score]
1234
+
1235
+ cand_to_remove = last_place_borda_scores[0]
1236
+ for c in last_place_borda_scores[1:]:
1237
+ if tb.index(c) < tb.index(cand_to_remove):
1238
+ cand_to_remove = c
1239
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array([cand_to_remove])), axis=None)
1240
+
1241
+ winners = list()
1242
+ if cands_to_ignore.shape[0] == all_num_cands: # all candidates have lowest Borda score
1243
+ winners = sorted(last_place_borda_scores)
1244
+ else: # remove the candidates with lowest Borda score
1245
+ num_cands = len(candidates)
1246
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1247
+
1248
+ while len(winners) == 0:
1249
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_cands - cands_to_ignore.shape[0], c)
1250
+ for c in candidates if not isin(cands_to_ignore, c)}
1251
+
1252
+ min_borda_score = min(borda_scores.values())
1253
+ last_place_borda_scores = [c for c in borda_scores.keys() if borda_scores[c] == min_borda_score]
1254
+
1255
+ # select the candidate to remove using the tie-breaking rule (a linear order over the candidates)
1256
+ cand_to_remove = last_place_borda_scores[0]
1257
+ for c in last_place_borda_scores[1:]:
1258
+ if tb.index(c) < tb.index(cand_to_remove):
1259
+ cand_to_remove = c
1260
+
1261
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array([cand_to_remove])), axis=None)
1262
+
1263
+ if cands_to_ignore.shape[0] == num_cands: # removed all remaining candidates
1264
+ winners = sorted(last_place_borda_scores)
1265
+ elif num_cands - cands_to_ignore.shape[0] == 1: # only one candidate remains
1266
+ winners = sorted([c for c in candidates if c not in cands_to_ignore])
1267
+ else:
1268
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1269
+ return sorted(winners)
1270
+
1271
+ @vm(name = "Baldwin PUT",
1272
+ input_types=[ElectionTypes.PROFILE])
1273
+ def baldwin_put(profile, curr_cands=None):
1274
+ """Baldwin with parallel universe tie-breaking (PUT), defined recursively: if there is a single candidate in the profile, that candidate wins; otherwise a candidate x is a Baldwin-PUT winner if there is some candidate y with a minimal Borda score such that after removing y from the profile, x is a Baldwin-PUT winner.
1275
+
1276
+ Args:
1277
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1278
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1279
+
1280
+ Returns:
1281
+ A sorted list of candidates
1282
+
1283
+ :Example:
1284
+
1285
+ .. exec_code::
1286
+
1287
+ from pref_voting.profiles import Profile
1288
+ from pref_voting.iterative_methods import baldwin, baldwin_tb, baldwin_put
1289
+
1290
+ prof = Profile([[1, 2, 0], [0, 1, 2], [2, 0, 1]], [1, 3, 2])
1291
+
1292
+ prof.display()
1293
+ print("no tiebreaker")
1294
+ baldwin.display(prof)
1295
+ print("tie_breaker = [0, 1, 2]")
1296
+ baldwin_tb.display(prof, tie_breaker=[0, 1, 2])
1297
+ print("tie_breaker = [0, 2, 1]")
1298
+ baldwin_tb.display(prof, tie_breaker=[0, 2, 1])
1299
+ print("tie_breaker = [1, 0, 2]")
1300
+ baldwin_tb.display(prof, tie_breaker=[1, 0, 2])
1301
+ print("tie_breaker = [1, 2, 0]")
1302
+ baldwin_tb.display(prof, tie_breaker=[1, 2, 0])
1303
+ print("tie_breaker = [2, 0, 1]")
1304
+ baldwin_tb.display(prof, tie_breaker=[2, 0, 1])
1305
+ print("tie_breaker = [2, 1, 0]")
1306
+ baldwin_tb.display(prof, tie_breaker=[2, 1, 0])
1307
+ print()
1308
+ baldwin_put.display(prof)
1309
+ """
1310
+
1311
+ num_original_cands = len(profile.candidates)
1312
+ candidates = profile.candidates if curr_cands is None else curr_cands
1313
+
1314
+ if len(candidates) == 1:
1315
+ return candidates
1316
+
1317
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1318
+
1319
+ rankings, rcounts = profile.rankings_counts # get all the ranking data
1320
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_original_cands)
1321
+
1322
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_original_cands - cands_to_ignore.shape[0], c) for c in candidates if not isin(cands_to_ignore, c)}
1323
+ min_borda_score = min(list(borda_scores.values()))
1324
+
1325
+ cands_to_remove = [c for c in candidates if c in borda_scores.keys() and borda_scores[c] == min_borda_score]
1326
+
1327
+ winners = []
1328
+ for cand_to_remove in cands_to_remove:
1329
+ new_winners = baldwin_put(profile, curr_cands = [c for c in candidates if not c == cand_to_remove])
1330
+ winners = winners + new_winners
1331
+
1332
+ return sorted(set(winners))
1333
+
1334
+
1335
+ def baldwin_with_explanation(profile, curr_cands = None):
1336
+ """Baldwin with an explanation. In addition to the winner(s), return the order in which the candidates are eliminated as a list of dictionaries specifying the Borda scores in the profile restricted to the candidates that have not been eliminated.
1337
+
1338
+ Args:
1339
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1340
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1341
+
1342
+ Returns:
1343
+ A sorted list of candidates
1344
+
1345
+ A list describing for each round, the candidates that are eliminated and the Borda scores of the remaining candidates (in the profile restricted to candidates that have not been eliminated)
1346
+
1347
+ .. exec_code::
1348
+
1349
+ from pref_voting.profiles import Profile
1350
+ from pref_voting.iterative_methods import baldwin, baldwin_with_explanation
1351
+
1352
+ prof = Profile([[2, 1, 0], [0, 2, 1], [1, 2, 0]], [1, 2, 2])
1353
+ prof.display()
1354
+ baldwin.display(prof)
1355
+ ws, exp = baldwin_with_explanation(prof)
1356
+ print(f"winning set: {ws}")
1357
+ print(f"order of elimination: {exp}")
1358
+
1359
+ prof = Profile([[1, 0, 3, 2], [2, 3, 1, 0], [2, 0, 3, 1], [1, 2, 3, 0]], [1, 1, 1, 1])
1360
+ prof.display()
1361
+ baldwin.display(prof)
1362
+ ws, exp = baldwin_with_explanation(prof)
1363
+ print(f"winning set: {ws}")
1364
+ print(f"order of elimination: {exp}")
1365
+
1366
+
1367
+ """
1368
+
1369
+ all_num_cands = profile.num_cands
1370
+ candidates = profile.candidates if curr_cands is None else curr_cands
1371
+ elims_list = list()
1372
+
1373
+ rcounts = profile._rcounts # get all the ranking data
1374
+ rankings = profile._rankings if curr_cands is None else _find_updated_profile(profile._rankings, np.array([c for c in profile.candidates if c not in curr_cands]), all_num_cands)
1375
+
1376
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1377
+
1378
+ borda_scores = {c: _borda_score(rankings, rcounts, len(candidates), c) for c in candidates}
1379
+
1380
+ min_borda_score = min(list(borda_scores.values()))
1381
+
1382
+ last_place_borda_scores = [c for c in candidates
1383
+ if c in borda_scores.keys() and borda_scores[c] == min_borda_score]
1384
+ elims_list.append([last_place_borda_scores, borda_scores])
1385
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array(last_place_borda_scores)), axis=None)
1386
+
1387
+ winners = list()
1388
+ if cands_to_ignore.shape[0] == all_num_cands: # all candidates have lowest Borda score
1389
+ winners = sorted(last_place_borda_scores)
1390
+ else: # remove the candidates with lowest Borda score
1391
+ num_cands = len(candidates)
1392
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1393
+
1394
+ while len(winners) == 0:
1395
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_cands - cands_to_ignore.shape[0], c)
1396
+ for c in candidates if not isin(cands_to_ignore, c)}
1397
+
1398
+ min_borda_score = min(borda_scores.values())
1399
+ last_place_borda_scores = [c for c in borda_scores.keys() if borda_scores[c] == min_borda_score]
1400
+ elims_list.append([last_place_borda_scores, borda_scores])
1401
+
1402
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array(last_place_borda_scores)), axis=None)
1403
+
1404
+ if cands_to_ignore.shape[0] == all_num_cands: # removed all remaining candidates
1405
+ winners = sorted(last_place_borda_scores)
1406
+ elif num_cands - cands_to_ignore.shape[0] == 1: # only one candidate remains
1407
+ winners = sorted([c for c in candidates if c not in cands_to_ignore])
1408
+ else:
1409
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1410
+ return sorted(winners), elims_list
1411
+
1412
+ @vm(name = "Strict Nanson",
1413
+ input_types=[ElectionTypes.PROFILE])
1414
+ def strict_nanson(profile, curr_cands = None):
1415
+ """Iteratively remove all candidates with the Borda score strictly below the average Borda score until one candidate remains. If, at any stage, all candidates have the same Borda score, then all (remaining) candidates are winners.
1416
+
1417
+ .. note::
1418
+
1419
+ Strict Nanson is a Condorcet consistent voting method, i.e., if a Condorcet winner exists, then Strict Nanson will elect the Condorcet winner.
1420
+
1421
+ Args:
1422
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1423
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1424
+
1425
+ Returns:
1426
+ A sorted list of candidates
1427
+
1428
+ .. seealso::
1429
+
1430
+ :func:`pref_voting.iterative_methods.strict_nanson_with_explanation`, :func:`pref_voting.iterative_methods.weak_nanson`
1431
+
1432
+ :Example:
1433
+
1434
+ .. exec_code::
1435
+
1436
+ from pref_voting.profiles import Profile
1437
+ from pref_voting.iterative_methods import strict_nanson
1438
+
1439
+ prof = Profile([[2, 1, 0, 3], [0, 2, 1, 3], [1, 3, 0, 2], [0, 3, 2, 1]], [2, 1, 1, 1])
1440
+
1441
+ prof.display()
1442
+ strict_nanson.display(prof)
1443
+ """
1444
+
1445
+ all_num_cands = profile.num_cands
1446
+ candidates = profile.candidates if curr_cands is None else curr_cands
1447
+
1448
+ rcounts = profile._rcounts # get all the ranking data
1449
+ rankings = profile._rankings if curr_cands is None else _find_updated_profile(profile._rankings, np.array([c for c in profile.candidates if c not in curr_cands]), all_num_cands)
1450
+ cands_to_ignore = np.empty(0)
1451
+
1452
+ borda_scores = {c: _borda_score(rankings, rcounts, len(candidates), c) for c in candidates}
1453
+
1454
+ avg_borda_score = np.mean(list(borda_scores.values()))
1455
+ below_borda_avg_candidates = np.array([c for c in borda_scores.keys() if borda_scores[c] < avg_borda_score])
1456
+
1457
+ cands_to_ignore = np.concatenate((cands_to_ignore, below_borda_avg_candidates), axis=None)
1458
+ winners = list()
1459
+ if cands_to_ignore.shape[0] == all_num_cands: # all candidates have same Borda score
1460
+ winners = sorted(candidates)
1461
+ else:
1462
+ num_cands = len(candidates)
1463
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1464
+ while len(winners) == 0:
1465
+
1466
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_cands - cands_to_ignore.shape[0], c)
1467
+ for c in candidates if not isin(cands_to_ignore, c)}
1468
+
1469
+ avg_borda_scores = np.mean(list(borda_scores.values()))
1470
+
1471
+ below_borda_avg_candidates = np.array([c for c in borda_scores.keys()
1472
+ if borda_scores[c] < avg_borda_scores])
1473
+
1474
+ cands_to_ignore = np.concatenate((cands_to_ignore, below_borda_avg_candidates), axis=None)
1475
+
1476
+ if (below_borda_avg_candidates.shape[0] == 0) or ((all_num_cands - cands_to_ignore.shape[0]) == 1):
1477
+ winners = sorted([c for c in candidates if c not in cands_to_ignore])
1478
+ else:
1479
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1480
+
1481
+ return winners
1482
+
1483
+
1484
+ def strict_nanson_with_explanation(profile, curr_cands = None):
1485
+ """Strict Nanson with an explanation. In addition to the winner(s), return the order in which the candidates are eliminated as a list of dictionaries specifying the Borda scores in the profile restricted to the candidates that have not been eliminated and the average Borda score.
1486
+
1487
+ Args:
1488
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1489
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1490
+
1491
+ Returns:
1492
+ A sorted list of candidates
1493
+
1494
+ A list describing for each round, the candidates that are eliminated and the Borda scores of the remaining candidates (in the profile restricted to candidates that have not been eliminated)
1495
+ :Example:
1496
+
1497
+ .. exec_code::
1498
+
1499
+ from pref_voting.profiles import Profile
1500
+ from pref_voting.iterative_methods import strict_nanson_with_explanation
1501
+
1502
+ prof = Profile([[2, 1, 0, 3], [0, 2, 1, 3], [1, 3, 0, 2], [0, 3, 2, 1]], [2, 1, 1, 1])
1503
+
1504
+ prof.display()
1505
+ print(strict_nanson_with_explanation(prof))
1506
+ """
1507
+
1508
+ all_num_cands = profile.num_cands
1509
+ candidates = profile.candidates if curr_cands is None else curr_cands
1510
+
1511
+ rcounts = profile._rcounts # get all the ranking data
1512
+ rankings = profile._rankings if curr_cands is None else _find_updated_profile(profile._rankings, np.array([c for c in profile.candidates if c not in curr_cands]), all_num_cands)
1513
+ cands_to_ignore = np.empty(0)
1514
+ elim_list = list()
1515
+
1516
+ borda_scores = {c: _borda_score(rankings, rcounts, len(candidates), c) for c in candidates}
1517
+
1518
+ avg_borda_score = np.mean(list(borda_scores.values()))
1519
+ below_borda_avg_candidates = [c for c in borda_scores.keys() if borda_scores[c] < avg_borda_score]
1520
+
1521
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array(below_borda_avg_candidates)), axis=None)
1522
+ winners = list()
1523
+ if cands_to_ignore.shape[0] == all_num_cands: # all candidates have same Borda score
1524
+ elim_list.append({"avg_borda_score": avg_borda_score,
1525
+ "elim_cands": below_borda_avg_candidates,
1526
+ "borda_scores": borda_scores})
1527
+ winners = sorted(candidates)
1528
+ else:
1529
+ num_cands = len(candidates)
1530
+ elim_list.append({"avg_borda_score": avg_borda_score,
1531
+ "elim_cands": below_borda_avg_candidates,
1532
+ "borda_scores": borda_scores})
1533
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1534
+ while len(winners) == 0:
1535
+
1536
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_cands - cands_to_ignore.shape[0], c)
1537
+ for c in candidates if not isin(cands_to_ignore, c)}
1538
+
1539
+ avg_borda_score = np.mean(list(borda_scores.values()))
1540
+
1541
+ below_borda_avg_candidates = [c for c in borda_scores.keys()
1542
+ if borda_scores[c] < avg_borda_score]
1543
+
1544
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array(below_borda_avg_candidates)), axis=None)
1545
+ elim_list.append({"avg_borda_score": avg_borda_score,
1546
+ "elim_cands": below_borda_avg_candidates,
1547
+ "borda_scores": borda_scores})
1548
+
1549
+ if (len(below_borda_avg_candidates) == 0) or ((all_num_cands - cands_to_ignore.shape[0]) == 1):
1550
+ winners = sorted([c for c in candidates if c not in cands_to_ignore])
1551
+ else:
1552
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1553
+
1554
+ return winners, elim_list
1555
+
1556
+ @vm(name = "Weak Nanson",
1557
+ input_types=[ElectionTypes.PROFILE])
1558
+ def weak_nanson(profile, curr_cands = None):
1559
+ """Iteratively remove all candidates with Borda score less than or equal the average Borda score until one candidate remains. If, at any stage, all candidates have the same Borda score, then all (remaining) candidates are winners.
1560
+
1561
+ .. note::
1562
+
1563
+ Weak Nanson is a Condorcet consistent voting method, i.e., if a Condorcet winner exists, then Weak Nanson will elect the Condorcet winner.
1564
+
1565
+ Args:
1566
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1567
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1568
+
1569
+ Returns:
1570
+ A sorted list of candidates
1571
+
1572
+ .. seealso::
1573
+
1574
+ :func:`pref_voting.iterative_methods.weak_nanson_with_explanation`, :func:`pref_voting.iterative_methods.strict_nanson`
1575
+
1576
+ :Example:
1577
+
1578
+ .. exec_code::
1579
+
1580
+ from pref_voting.profiles import Profile
1581
+ from pref_voting.iterative_methods import weak_nanson
1582
+
1583
+ prof = Profile([[2, 1, 0, 3], [0, 2, 1, 3], [1, 3, 0, 2], [0, 3, 2, 1]], [2, 1, 1, 1])
1584
+
1585
+ prof.display()
1586
+ weak_nanson.display(prof)
1587
+
1588
+ """
1589
+
1590
+ all_num_cands = profile.num_cands
1591
+ candidates = profile.candidates if curr_cands is None else curr_cands
1592
+
1593
+ rcounts = profile._rcounts # get all the ranking data
1594
+ rankings = profile._rankings if curr_cands is None else _find_updated_profile(profile._rankings, np.array([c for c in profile.candidates if c not in curr_cands]), all_num_cands)
1595
+
1596
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1597
+
1598
+ borda_scores = {c: _borda_score(rankings, rcounts, len(candidates), c) for c in candidates}
1599
+
1600
+ avg_borda_score = np.mean(list(borda_scores.values()))
1601
+
1602
+ below_borda_avg_candidates = np.array([c for c in borda_scores.keys() if borda_scores[c] <= avg_borda_score])
1603
+
1604
+ cands_to_ignore = np.concatenate((cands_to_ignore, below_borda_avg_candidates), axis=None)
1605
+
1606
+ winners = list()
1607
+ if cands_to_ignore.shape[0] == all_num_cands: # all candidates have same Borda score
1608
+ winners = sorted(candidates)
1609
+ elif all_num_cands - cands_to_ignore.shape[0] == 1: # one candidate remains
1610
+ winners = [c for c in candidates if not isin(cands_to_ignore, c)]
1611
+ else:
1612
+ num_cands = len(candidates)
1613
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1614
+
1615
+ while len(winners) == 0:
1616
+
1617
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_cands - cands_to_ignore.shape[0], c)
1618
+ for c in candidates if not isin(cands_to_ignore, c)}
1619
+
1620
+
1621
+ avg_borda_score = np.mean(list(borda_scores.values()))
1622
+
1623
+ below_borda_avg_candidates = np.array([c for c in borda_scores.keys()
1624
+ if borda_scores[c] <= avg_borda_score])
1625
+
1626
+ cands_to_ignore = np.concatenate((cands_to_ignore, below_borda_avg_candidates), axis=None)
1627
+
1628
+ if cands_to_ignore.shape[0] == all_num_cands: # all remaining candidates have been removed
1629
+ winners = sorted(below_borda_avg_candidates)
1630
+ elif all_num_cands - cands_to_ignore.shape[0] == 1: # one candidate remains
1631
+ winners = [c for c in candidates if not isin(cands_to_ignore, c)]
1632
+ else:
1633
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1634
+
1635
+ return winners
1636
+
1637
+
1638
+ def weak_nanson_with_explanation(profile, curr_cands = None):
1639
+ """
1640
+ Weak Nanson with an explanation. In addition to the winner(s), return the order in which the candidates are eliminated as a list of dictionaries specifying the Borda scores in the profile restricted to the candidates that have not been eliminated and the average Borda score.
1641
+
1642
+ Args:
1643
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1644
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1645
+
1646
+ Returns:
1647
+ A sorted list of candidates
1648
+
1649
+ A list describing for each round, the candidates that are eliminated and the Borda scores of the remaining candidates (in the profile restricted to candidates that have not been eliminated)
1650
+ :Example:
1651
+
1652
+ .. exec_code::
1653
+
1654
+ from pref_voting.profiles import Profile
1655
+ from pref_voting.iterative_methods import weak_nanson_with_explanation
1656
+
1657
+ prof = Profile([[2, 1, 0, 3], [0, 2, 1, 3], [1, 3, 0, 2], [0, 3, 2, 1]], [2, 1, 1, 1])
1658
+
1659
+ prof.display()
1660
+ print(weak_nanson_with_explanation(prof))
1661
+
1662
+ """
1663
+ all_num_cands = profile.num_cands
1664
+ candidates = profile.candidates if curr_cands is None else curr_cands
1665
+
1666
+ rcounts = profile._rcounts # get all the ranking data
1667
+ rankings = profile._rankings if curr_cands is None else _find_updated_profile(profile._rankings, np.array([c for c in profile.candidates if c not in curr_cands]), all_num_cands)
1668
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1669
+ elim_list = list()
1670
+
1671
+ borda_scores = {c: _borda_score(rankings, rcounts, len(candidates), c) for c in candidates}
1672
+
1673
+ avg_borda_score = np.mean(list(borda_scores.values()))
1674
+ below_borda_avg_candidates = [c for c in borda_scores.keys()
1675
+ if borda_scores[c] <= avg_borda_score]
1676
+
1677
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array(below_borda_avg_candidates)), axis=None)
1678
+ winners = list()
1679
+ if cands_to_ignore.shape[0] == all_num_cands: # all candidates have same Borda score
1680
+ elim_list.append({"avg_borda_score": avg_borda_score,
1681
+ "elim_cands": below_borda_avg_candidates,
1682
+ "borda_scores": borda_scores})
1683
+ winners = sorted(candidates)
1684
+ elif all_num_cands - cands_to_ignore.shape[0] == 1: # one candidate remains
1685
+ elim_list.append({"avg_borda_score": avg_borda_score,
1686
+ "elim_cands": below_borda_avg_candidates,
1687
+ "borda_scores": borda_scores})
1688
+ winners = [c for c in candidates if not isin(cands_to_ignore, c)]
1689
+ else:
1690
+ num_cands = len(candidates)
1691
+ elim_list.append({"avg_borda_score": avg_borda_score,
1692
+ "elim_cands": below_borda_avg_candidates,
1693
+ "borda_scores": borda_scores})
1694
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1695
+
1696
+
1697
+ while len(winners) == 0:
1698
+
1699
+ borda_scores = {c: _borda_score(updated_rankings, rcounts, num_cands - cands_to_ignore.shape[0], c)
1700
+ for c in candidates if not isin(cands_to_ignore, c)}
1701
+
1702
+ avg_borda_score = np.mean(list(borda_scores.values()))
1703
+
1704
+ below_borda_avg_candidates = [c for c in borda_scores.keys()
1705
+ if borda_scores[c] <= avg_borda_score]
1706
+
1707
+ cands_to_ignore = np.concatenate((cands_to_ignore, np.array(below_borda_avg_candidates)), axis=None)
1708
+ elim_list.append({"avg_borda_score": avg_borda_score,
1709
+ "elim_cands": below_borda_avg_candidates,
1710
+ "borda_scores": borda_scores})
1711
+
1712
+ if cands_to_ignore.shape[0] == all_num_cands: # all remaining candidates have been removed
1713
+ winners = sorted(below_borda_avg_candidates)
1714
+ elif all_num_cands - cands_to_ignore.shape[0] == 1: # one candidate remains
1715
+ winners = [c for c in candidates if not isin(cands_to_ignore, c)]
1716
+ else:
1717
+ updated_rankings = _find_updated_profile(rankings, cands_to_ignore, num_cands)
1718
+
1719
+ return winners, elim_list
1720
+
1721
+
1722
+ @vm(name = "Iterated Removal Condorcet Loser",
1723
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MAJORITY_GRAPH, ElectionTypes.MARGIN_GRAPH])
1724
+ def iterated_removal_cl(edata, curr_cands = None):
1725
+ """
1726
+ Iteratively remove candidates that are Condorcet losers until there are no Condorcet losers. A candidate :math:`c` is a **Condorcet loser** when every other candidate is majority preferred to :math:`c`.
1727
+
1728
+ Args:
1729
+ edata (Profile, ProfileWithTies, MajorityGraph, MarginGraph): Any election data that has a `condorcet_loser` method.
1730
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1731
+
1732
+ Returns:
1733
+ A sorted list of candidates
1734
+
1735
+ .. seealso::
1736
+
1737
+ :meth:`pref_voting.profiles.Profile.condorcet_loser`, :meth:`pref_voting.profiles_with_ties.ProfileWithTies.condorcet_loser`, :meth:`pref_voting.weighted_majority_graphs.MajorityGraph.condorcet_loser`
1738
+
1739
+ :Example:
1740
+
1741
+ .. exec_code::
1742
+
1743
+ from pref_voting.profiles import Profile
1744
+ from pref_voting.profiles_with_ties import ProfileWithTies
1745
+ from pref_voting.iterative_methods import iterated_removal_cl
1746
+
1747
+ prof = Profile([[2, 1, 3, 0], [2, 1, 0, 3], [3, 1, 2, 0], [1, 2, 3, 0]], [1, 1, 1, 1])
1748
+
1749
+ prof.display()
1750
+ iterated_removal_cl.display(prof)
1751
+ iterated_removal_cl.display(prof.majority_graph())
1752
+ iterated_removal_cl.display(prof.margin_graph())
1753
+
1754
+ prof2 = ProfileWithTies([{2:1, 1:1, 3:2, 0:3}, {2:1, 1:2, 0:3, 3:4}, {3:1, 1:2, 2:3, 0:4}, {1:1, 2:2, 3:3, 0:4}], [1, 1, 1, 1])
1755
+
1756
+ prof2.display()
1757
+ iterated_removal_cl.display(prof2)
1758
+
1759
+ """
1760
+
1761
+ condorcet_loser = edata.condorcet_loser(curr_cands = curr_cands)
1762
+
1763
+ remaining_cands = edata.candidates if curr_cands is None else curr_cands
1764
+
1765
+ while len(remaining_cands) > 1 and condorcet_loser is not None:
1766
+ remaining_cands = [c for c in remaining_cands if c not in [condorcet_loser]]
1767
+ condorcet_loser = edata.condorcet_loser(curr_cands = remaining_cands)
1768
+
1769
+ return sorted(remaining_cands)
1770
+
1771
+
1772
+ def iterated_removal_cl_with_explanation(edata, curr_cands = None):
1773
+ """
1774
+ Iterated Removal Condorcet Loser with an explanation. In addition to the winner(s), return the order of elimination, where each candidate in the list is a Condorcet loser in the profile (restricted to the remaining candidates).
1775
+
1776
+ Args:
1777
+ edata (Profile, ProfileWithTies, MajorityGraph, MarginGraph): Any election data that has a `condorcet_loser` method.
1778
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1779
+
1780
+ Returns:
1781
+ A sorted list of candidates
1782
+
1783
+ :Example:
1784
+
1785
+ .. exec_code::
1786
+
1787
+ from pref_voting.profiles import Profile
1788
+ from pref_voting.iterative_methods import iterated_removal_cl_with_explanation
1789
+
1790
+ prof = Profile([[2, 1, 3, 0], [2, 1, 0, 3], [3, 1, 2, 0], [1, 2, 3, 0]], [1, 1, 1, 1])
1791
+
1792
+ prof.display()
1793
+ ws, exp = iterated_removal_cl_with_explanation(prof)
1794
+ print(f"The winning set is {ws}")
1795
+ print(f"The order of elimination is {exp}")
1796
+ """
1797
+
1798
+ elim_list = list()
1799
+ condorcet_loser = edata.condorcet_loser(curr_cands = curr_cands)
1800
+
1801
+ remaining_cands = edata.candidates if curr_cands is None else curr_cands
1802
+
1803
+ while len(remaining_cands) > 1 and condorcet_loser is not None:
1804
+ elim_list.append(condorcet_loser)
1805
+ remaining_cands = [c for c in remaining_cands if c not in [condorcet_loser]]
1806
+ condorcet_loser = edata.condorcet_loser(curr_cands = remaining_cands)
1807
+
1808
+ return sorted(remaining_cands), elim_list
1809
+
1810
+ def _remove_worst_losers(edata,curr_cands,score_method):
1811
+ m_scores = minimax_scores(edata,curr_cands,score_method)
1812
+ worst_m_score = min([m_scores[c] for c in curr_cands])
1813
+ worst_losers = [c for c in curr_cands if m_scores[c] == worst_m_score]
1814
+ if len(worst_losers) == len(curr_cands):
1815
+ return curr_cands
1816
+ else:
1817
+ return [c for c in curr_cands if c not in worst_losers]
1818
+
1819
+ @vm(name = "Raynaud",
1820
+ input_types=[ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MARGIN_GRAPH])
1821
+ def raynaud(edata, curr_cands=None, score_method = "margins"):
1822
+ """Iteratively remove the candidate(s) whose worst loss is biggest, unless all candidates have the same worst loss. See https://electowiki.org/wiki/Raynaud.
1823
+
1824
+ Args:
1825
+ edata (Profile, ProfileWithTies, MarginGraph): Any election data that has a `margin` method.
1826
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``.
1827
+ 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.
1828
+
1829
+ Returns:
1830
+ A sorted list of candidates.
1831
+ """
1832
+ candidates = edata.candidates if curr_cands is None else curr_cands
1833
+ new_cands = _remove_worst_losers(edata,candidates,score_method)
1834
+ while not new_cands == candidates:
1835
+ candidates = new_cands
1836
+ new_cands = _remove_worst_losers(edata,candidates,score_method)
1837
+ return sorted(candidates)
1838
+
1839
+ @vm(name = "Benham",
1840
+ input_types=[ElectionTypes.PROFILE])
1841
+ def benham(profile, curr_cands = None):
1842
+ """
1843
+ As long as the profile has no Condorcet winner, eliminate the candidate with the lowest plurality score.
1844
+
1845
+ .. important::
1846
+ If there is more than one candidate with the fewest number of first-place votes, then *all* such candidates are removed from the profile.
1847
+
1848
+
1849
+ Args:
1850
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1851
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1852
+
1853
+ Returns:
1854
+ A sorted list of candidates
1855
+
1856
+ .. seealso::
1857
+
1858
+ Related functions: :func:`pref_voting.iterative_methods.benham_put`
1859
+
1860
+ """
1861
+
1862
+ # need the total number of all candidates in a profile to check when all candidates have been removed
1863
+ num_cands = profile.num_cands
1864
+
1865
+ candidates = profile.candidates if curr_cands is None else curr_cands
1866
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1867
+
1868
+ cw = profile.condorcet_winner(curr_cands = [c for c in profile.candidates if not isin(cands_to_ignore, c)])
1869
+
1870
+ winners = [cw] if cw is not None else list()
1871
+
1872
+ rs, rcounts = profile.rankings_counts # get all the ranking data
1873
+
1874
+ while len(winners) == 0:
1875
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c) for c in candidates
1876
+ if not isin(cands_to_ignore,c)}
1877
+ min_plurality_score = min(plurality_scores.values())
1878
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
1879
+ if plurality_scores[c] == min_plurality_score])
1880
+
1881
+ # remove cands with lowest plurality score
1882
+ cands_to_ignore = np.concatenate((cands_to_ignore, lowest_first_place_votes), axis=None)
1883
+ if len(cands_to_ignore) == num_cands: # removed all of the candidates
1884
+ winners = sorted(lowest_first_place_votes)
1885
+ else:
1886
+ cw = profile.condorcet_winner([c for c in profile.candidates if not isin(cands_to_ignore, c)])
1887
+ if cw is not None:
1888
+ winners = [cw]
1889
+
1890
+ return sorted(winners)
1891
+
1892
+ @vm(name = "Benham TB",
1893
+ input_types=[ElectionTypes.PROFILE])
1894
+ def benham_tb(profile, curr_cands = None, tie_breaker = None):
1895
+ """Benham (``benham``) with tie breaking: If there is more than one candidate with the fewest number of first-place votes, then remove the candidate with lowest in the tie_breaker ranking from the profile.
1896
+
1897
+ Args:
1898
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1899
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1900
+ tie_breaker (List[int]): A list of the candidates in the profile to be used as a tiebreaker.
1901
+
1902
+ Returns:
1903
+ A sorted list of candidates
1904
+
1905
+ """
1906
+
1907
+ # the tie_breaker is any linear order (i.e., list) of the candidates
1908
+ tb = tie_breaker if tie_breaker is not None else list(range(profile.num_cands))
1909
+
1910
+ # need the total number of all candidates in a profile to check when all candidates have been removed
1911
+ num_cands = profile.num_cands
1912
+
1913
+ candidates = profile.candidates if curr_cands is None else curr_cands
1914
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
1915
+
1916
+ rs, rcounts = profile.rankings_counts # get all the ranking data
1917
+
1918
+ cw = profile.condorcet_winner(curr_cands = [c for c in profile.candidates if not isin(cands_to_ignore, c)])
1919
+
1920
+ winners = [cw] if cw is not None else list()
1921
+
1922
+ while len(winners) == 0:
1923
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c) for c in candidates if not isin(cands_to_ignore,c)}
1924
+ min_plurality_score = min(plurality_scores.values())
1925
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
1926
+ if plurality_scores[c] == min_plurality_score])
1927
+
1928
+ cand_to_remove = lowest_first_place_votes[0]
1929
+ for c in lowest_first_place_votes[1:]:
1930
+ if tb.index(c) < tb.index(cand_to_remove):
1931
+ cand_to_remove = c
1932
+
1933
+ # remove cands with lowest plurality winners
1934
+ cands_to_ignore = np.concatenate((cands_to_ignore, cand_to_remove), axis=None)
1935
+ if len(cands_to_ignore) == num_cands: #all the candidates where removed
1936
+ winners = sorted(lowest_first_place_votes)
1937
+ else:
1938
+ cw = profile.condorcet_winner(curr_cands = [c for c in profile.candidates if not isin(cands_to_ignore, c)])
1939
+ if cw is not None:
1940
+ winners = [cw]
1941
+ return sorted(winners)
1942
+
1943
+
1944
+ @vm(name = "Benham PUT",
1945
+ input_types=[ElectionTypes.PROFILE])
1946
+ def benham_put(profile, curr_cands = None):
1947
+ """Benham (:func:`benham`) with parallel universe tie-breaking (PUT), defined recursively: if there is a Condorcet winner, that candidate is the Benham-PUT winner; otherwise a candidate x is a Benham-PUT winner if there is some candidate y with minimal plurality score such that after removing y from the profile, x is a Benham-PUT winner.
1948
+
1949
+ Args:
1950
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
1951
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
1952
+
1953
+ Returns:
1954
+ A sorted list of candidates
1955
+
1956
+ .. warning::
1957
+ This will take a long time on profiles with many candidates having the same plurality scores.
1958
+
1959
+ """
1960
+ candidates = profile.candidates if curr_cands is None else curr_cands
1961
+
1962
+ cw = profile.condorcet_winner(candidates)
1963
+ if cw is not None:
1964
+ return [cw]
1965
+
1966
+ plurality_scores = profile.plurality_scores(candidates)
1967
+ worst_score = min(plurality_scores.values())
1968
+
1969
+ cands_to_remove = [cand for cand, value in plurality_scores.items() if value == worst_score]
1970
+
1971
+ winners = []
1972
+ for cand_to_remove in cands_to_remove:
1973
+ new_winners = benham_put(profile, curr_cands = [c for c in candidates if not c == cand_to_remove])
1974
+ winners = winners + new_winners
1975
+
1976
+ return sorted(set(winners))
1977
+
1978
+ def iterated(vm):
1979
+ """Iteratively restrict the set of candidates to the vm winners until reaching a fixpoint.
1980
+
1981
+ Args:
1982
+ vm (VotingMethod): A voting method.
1983
+
1984
+ Returns:
1985
+ A voting method that iterates vm.
1986
+
1987
+ """
1988
+
1989
+ def _vm(edata, curr_cands = None):
1990
+
1991
+ candidates = edata.candidates if curr_cands is None else curr_cands
1992
+
1993
+ vm_ws = vm(edata, curr_cands=candidates)
1994
+
1995
+ while not vm_ws == candidates:
1996
+ candidates = vm_ws
1997
+ vm_ws = vm(edata, curr_cands=candidates)
1998
+
1999
+ return vm_ws
2000
+
2001
+ return VotingMethod(_vm, name=f"Iterated {vm.name}")
2002
+
2003
+ def tideman_alternative(vm):
2004
+ """Given a voting method vm, returns a voting method that restricts the profile to the set of vm winners, then eliminates all the candidate with the fewest first-place votes, and then repeats until there is only one vm winner. If at some stage all remaining candidates are tied for the fewest number of first-place votes, then all remaining candidates win.
2005
+
2006
+ Args:
2007
+ vm (VotingMethod): A voting method.
2008
+
2009
+ Returns:
2010
+ The Tideman Alternative PUT version of vm.
2011
+
2012
+ """
2013
+
2014
+ def _ta(profile, curr_cands = None):
2015
+
2016
+ candidates = profile.candidates if curr_cands is None else curr_cands
2017
+
2018
+ vm_ws = vm(profile, curr_cands = candidates)
2019
+
2020
+ plurality_scores = profile.plurality_scores(vm_ws)
2021
+ worst_score = min(plurality_scores.values())
2022
+
2023
+ cands_to_remove = [cand for cand, value in plurality_scores.items() if value == worst_score]
2024
+
2025
+ if len(cands_to_remove) == len(vm_ws):
2026
+ return vm_ws
2027
+
2028
+ else:
2029
+ return _ta(profile, curr_cands = [c for c in candidates if not c in cands_to_remove])
2030
+
2031
+ _ta.__name__ = f"tideman_alternative_{vm.__name__}"
2032
+ return VotingMethod(_ta, name=f"Tideman Alternative {vm.name}")
2033
+
2034
+ tideman_alternative_smith = tideman_alternative(top_cycle)
2035
+ tideman_alternative_smith.load_properties()
2036
+ tideman_alternative_smith.input_types = [ElectionTypes.PROFILE]
2037
+
2038
+ tideman_alternative_gocha = tideman_alternative(gocha)
2039
+ tideman_alternative_gocha.load_properties()
2040
+ tideman_alternative_gocha.input_types = [ElectionTypes.PROFILE]
2041
+
2042
+ def tideman_alternative_put(vm):
2043
+ """Given a voting method vm, returns a voting method that restricts the profile to the set of vm winners, then eliminates the candidate with the fewest first-place votes, and then repeats until there is only one vm winner. Parallel-universe tiebreaking is used when there are multiple candidates with the fewest first-place votes.
2044
+
2045
+ Args:
2046
+ vm (VotingMethod): A voting method.
2047
+
2048
+ Returns:
2049
+ The Tideman Alternative PUT version of vm.
2050
+
2051
+ """
2052
+
2053
+ def _ta(profile, curr_cands = None):
2054
+
2055
+ candidates = profile.candidates if curr_cands is None else curr_cands
2056
+
2057
+ vm_ws = vm(profile, curr_cands = candidates)
2058
+
2059
+ if len(vm_ws) == 1:
2060
+ return vm_ws
2061
+
2062
+ else:
2063
+ plurality_scores = profile.plurality_scores(vm_ws)
2064
+ worst_score = min(plurality_scores.values())
2065
+ cands_to_remove = [cand for cand, value in plurality_scores.items() if value == worst_score]
2066
+
2067
+ winners = []
2068
+ for cand_to_remove in cands_to_remove:
2069
+ additional_winners = _ta(profile, curr_cands = [c for c in candidates if not c == cand_to_remove])
2070
+ winners = winners + additional_winners
2071
+
2072
+ return sorted(set(winners))
2073
+
2074
+ _ta.__name__ = f"tideman_alternative_{vm.__name__}_put"
2075
+ return VotingMethod(_ta, name=f"Tideman Alternative {vm.name} PUT")
2076
+
2077
+
2078
+ tideman_alternative_smith_put = tideman_alternative_put(top_cycle)
2079
+ tideman_alternative_smith_put.load_properties()
2080
+ tideman_alternative_smith_put.input_types = [ElectionTypes.PROFILE]
2081
+
2082
+ tideman_alternative_gocha_put = tideman_alternative_put(gocha)
2083
+ tideman_alternative_gocha_put.load_properties()
2084
+ tideman_alternative_gocha_put.input_types = [ElectionTypes.PROFILE]
2085
+
2086
+
2087
+ @vm(name = "Woodall",
2088
+ input_types=[ElectionTypes.PROFILE])
2089
+ def woodall(profile, curr_cands = None):
2090
+ """
2091
+ If there is a single member of the Smith Set (i.e., a Condorcet winner) then that candidate is the winner. If there the Smith Set contains more than one candidate, then remove all candidates that are ranked first by the fewest number of voters. Continue removing candidates with the fewest number first-place votes until there is a single member of the originally Smith Set remaining.
2092
+
2093
+ .. important::
2094
+ If there is more than one candidate with the fewest number of first-place votes, then *all* such candidates are removed from the profile.
2095
+
2096
+
2097
+ Args:
2098
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
2099
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2100
+
2101
+ Returns:
2102
+ A sorted list of candidates
2103
+
2104
+ .. seealso::
2105
+
2106
+ Related functions: :func:`pref_voting.iterative_methods.instant_runoff`
2107
+
2108
+
2109
+ """
2110
+
2111
+ # need the total number of all candidates in a profile to check when all candidates have been removed
2112
+
2113
+ candidates = profile.candidates if curr_cands is None else curr_cands
2114
+ cands_to_ignore = np.empty(0) if curr_cands is None else np.array([c for c in profile.candidates if c not in curr_cands])
2115
+
2116
+ s_set = top_cycle(profile, curr_cands=candidates)
2117
+
2118
+ if len(s_set) == 1:
2119
+ return s_set
2120
+
2121
+ rs, rcounts = profile.rankings_counts # get all the ranking data
2122
+
2123
+ winners = []
2124
+
2125
+ while len(winners) == 0:
2126
+ plurality_scores = {c: _num_rank_first(rs, rcounts, cands_to_ignore, c)
2127
+ for c in candidates if not isin(cands_to_ignore,c)}
2128
+ min_plurality_score = min(plurality_scores.values())
2129
+ lowest_first_place_votes = np.array([c for c in plurality_scores.keys()
2130
+ if plurality_scores[c] == min_plurality_score])
2131
+
2132
+ remaining_cands_in_smith_set = [c for c in candidates if not isin(cands_to_ignore,c) and isin(np.array(s_set), c)]
2133
+
2134
+ # remove cands with lowest plurality score
2135
+ new_cands_to_ignore = np.concatenate((cands_to_ignore, lowest_first_place_votes), axis=None)
2136
+
2137
+ new_remaining_cands_in_smith_set = [c for c in candidates if not isin(new_cands_to_ignore,c) and isin(np.array(s_set), c)]
2138
+
2139
+ if len(new_remaining_cands_in_smith_set) == 0:
2140
+ winners = remaining_cands_in_smith_set
2141
+
2142
+ if len(new_remaining_cands_in_smith_set) == 1:
2143
+ winners = new_remaining_cands_in_smith_set
2144
+
2145
+ cands_to_ignore = new_cands_to_ignore
2146
+
2147
+ return sorted(winners)
2148
+
2149
+ @vm(name = "Knockout Voting",
2150
+ input_types=[ElectionTypes.PROFILE])
2151
+ def knockout(profile, curr_cands=None):
2152
+ """Find the two candidates in curr_cands with the lowest and second lowest Borda scores among any candidates in curr_cands. Then remove from curr_cands whichever one loses to the other in a head-to-head majority comparison. Repeat this process, always using the original Borda score (i.e., the Borda scores calculated with respect to all candidates in the profile, not with respect to curr_cands as for Baldwin and Nanson) until only one candidate remains in curr_cands. Parallel universe tie-breaking (PUT) is used when there are ties in lowest or second lowest Borda scores.
2153
+
2154
+ .. note::
2155
+ Proposed by Edward B. Foley (with unspecified handling of ties in Borda scores, so PUT is used here as an example).
2156
+
2157
+ Args:
2158
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
2159
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
2160
+
2161
+ Returns:
2162
+ A sorted list of candidates
2163
+
2164
+ """
2165
+ candidates = profile.candidates if curr_cands is None else curr_cands
2166
+
2167
+ if len(candidates) == 1:
2168
+ return candidates
2169
+
2170
+ # Key point: use global Borda score, calculated with respect to the full profile, not just the candidates in curr_cands
2171
+ borda_scores = profile.borda_scores()
2172
+ min_borda_score = min([borda_scores[c] for c in candidates])
2173
+ cands_with_lowest_borda_score = [c for c in candidates if c in borda_scores.keys() and borda_scores[c] == min_borda_score]
2174
+
2175
+ winners = []
2176
+
2177
+ # If multiple candidates tie for lowest Borda score, consider all head-to-head matchups of these candidates
2178
+ if len(cands_with_lowest_borda_score) > 1:
2179
+ for c1 in cands_with_lowest_borda_score:
2180
+ for c2 in cands_with_lowest_borda_score:
2181
+ if c1 != c2:
2182
+ # If c1 has a non-negative margin over c2, then remove c2 from curr_cands and calculate the winning set
2183
+ # Take the union over all such winning sets as the ultimate winning set
2184
+ if profile.margin(c1, c2) >= 0:
2185
+ new_winners = knockout(profile, curr_cands = [c for c in candidates if not c == c2])
2186
+ winners = winners + new_winners
2187
+
2188
+ # If there is a candidate with the uniquely lowest Borda score
2189
+ if len(cands_with_lowest_borda_score) == 1:
2190
+ cand_with_lowest_borda_score = cands_with_lowest_borda_score[0]
2191
+
2192
+ # There may be multiple candidates with the second lowest Borda score
2193
+ second_lowest_borda_score = min([borda_scores[c] for c in candidates if c not in cands_with_lowest_borda_score])
2194
+ cands_with_second_lowest_borda_score = [c for c in candidates if c in borda_scores.keys() and borda_scores[c] == second_lowest_borda_score]
2195
+
2196
+ # Consider all head-to-head matchups between the candidate with the lowest Borda score and the candidates with the second lowest Borda score
2197
+ for c2 in cands_with_second_lowest_borda_score:
2198
+
2199
+ # If a candidate with second lowest Borda score has a non-negative margin over the candidate with the lowest Borda score,
2200
+ # then remove the latter from curr_cands and calculate the winning set
2201
+ if profile.margin(c2, cand_with_lowest_borda_score) >= 0:
2202
+ new_winners = knockout(profile, curr_cands = [c for c in candidates if not c == cand_with_lowest_borda_score])
2203
+ winners = winners + new_winners
2204
+
2205
+ # If the candidate with the lowest Borda score has a positive margin over a candidate with the second lowest Borda score,
2206
+ # then remove the latter from curr_cands and calculate the winning set
2207
+ if profile.margin(cand_with_lowest_borda_score, c2) > 0:
2208
+ new_winners = knockout(profile, curr_cands = [c for c in candidates if not c == c2])
2209
+ winners = winners + new_winners
2210
+
2211
+ return sorted(set(winners))
2212
+
2213
+ @vm(name="Plurality Veto",
2214
+ input_types=[ElectionTypes.PROFILE])
2215
+ def plurality_veto(profile, curr_cands=None, voter_order=None):
2216
+ """Returns the winner using the Plurality Veto method of Kizilkaya and Kempe (https://arxiv.org/abs/2305.19632).
2217
+
2218
+ The method works as follows:
2219
+ 1. Assign initial scores to candidates equal to their plurality scores
2220
+ 2. Process voters one by one in the given order
2221
+ 3. Each voter decrements the score of their bottom choice among non-eliminated candidates
2222
+ 4. A candidate is eliminated when their score reaches zero
2223
+ 5. The winner is the last remaining candidate
2224
+
2225
+ Args:
2226
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
2227
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in curr_cands
2228
+ voter_order (List[int], optional): List of voters in the order to process them. If None, uses range(len(profile.rankings))
2229
+
2230
+ Returns:
2231
+ A sorted list of candidates
2232
+
2233
+ warning::
2234
+ If no voter order is specified, the method uses the default order of voter rankings in the profile. Note that anonymizing a profile changes the order of voter rankings.
2235
+ """
2236
+ candidates = profile.candidates if curr_cands is None else curr_cands
2237
+
2238
+ # Initialize scores as plurality scores
2239
+ scores = profile.plurality_scores(curr_cands=candidates)
2240
+
2241
+ # If no voter order specified, use default order
2242
+ if voter_order is None:
2243
+ voter_order = list(range(profile.num_voters))
2244
+
2245
+ # Track non-eliminated candidates and last remaining
2246
+ active_candidates = set(candidates)
2247
+ last_remaining = None # Track the last remaining candidate
2248
+
2249
+ # Process each voter
2250
+ for voter in voter_order:
2251
+ # Get remaining candidates with positive scores
2252
+ remaining = {c for c in active_candidates if scores[c] > 0}
2253
+ if not remaining:
2254
+ # If all remaining candidates have 0 scores, return the last remaining
2255
+ return [last_remaining] if last_remaining is not None else sorted(active_candidates)
2256
+
2257
+ # If only one candidate remains with positive score, they are the winner
2258
+ if len(remaining) == 1:
2259
+ return sorted(remaining)
2260
+
2261
+ # Get voter's bottom choice among remaining candidates
2262
+ ranking = profile.rankings[voter]
2263
+ # Find the last ranked candidate among remaining ones
2264
+ bottom = next(c for c in reversed(ranking) if c in remaining)
2265
+
2266
+ # Decrement score
2267
+ scores[bottom] -= 1
2268
+ if scores[bottom] == 0:
2269
+ active_candidates.remove(bottom)
2270
+ last_remaining = bottom
2271
+
2272
+ # Return the last remaining candidate if there was one,
2273
+ # otherwise return candidates with highest remaining score
2274
+ if last_remaining is not None:
2275
+ return [last_remaining]
2276
+ else:
2277
+ max_score = max(scores.values())
2278
+ return sorted([c for c in candidates if scores[c] == max_score])
2279
+
2280
+ def plurality_veto_with_explanation(profile, curr_cands=None, voter_order=None):
2281
+ """Returns the winner using the Plurality Veto method, with a detailed explanation of the process.
2282
+
2283
+ Args:
2284
+ profile (Profile): An anonymous profile of linear orders on a set of candidates
2285
+ curr_cands (List[int], optional): If set, then find the winners for the profile restricted to curr_cands
2286
+ voter_order (List[int], optional): List of voters in the order to process them. If None, uses range(len(profile.rankings))
2287
+
2288
+ Returns:
2289
+ tuple: A tuple containing (winner list, explanation string)
2290
+
2291
+ warning::
2292
+ If no voter order is specified, the method uses the default order of voter rankings in the profile. Note that anonymizing a profile changes the order of voter rankings.
2293
+ """
2294
+ curr_cands = profile.candidates if curr_cands is None else curr_cands
2295
+ scores = profile.plurality_scores(curr_cands=curr_cands)
2296
+
2297
+ if voter_order is None:
2298
+ voter_order = list(range(profile.num_voters))
2299
+
2300
+ explanation = [
2301
+ "Initial plurality scores: " + str(dict(scores)),
2302
+ ]
2303
+
2304
+ # Note any candidates eliminated due to zero initial plurality scores
2305
+ zero_initial = [c for c in curr_cands if scores[c] == 0]
2306
+ if zero_initial:
2307
+ explanation.append(f"Candidates eliminated due to zero initial plurality score: {sorted(zero_initial)}")
2308
+ explanation.append("") # Add blank line
2309
+
2310
+ active_candidates = set(curr_cands)
2311
+ last_remaining = None
2312
+
2313
+ # Add initially eliminated candidates
2314
+ for c in zero_initial:
2315
+ active_candidates.remove(c)
2316
+ last_remaining = c
2317
+
2318
+ for step, voter in enumerate(voter_order):
2319
+ remaining = {c for c in active_candidates if scores[c] > 0}
2320
+ if not remaining:
2321
+ explanation.append("All remaining candidates have score 0")
2322
+ if last_remaining is not None:
2323
+ explanation.append(f"Winners are candidates [{last_remaining}] (highest remaining scores)")
2324
+ return [last_remaining], "\\n".join(explanation)
2325
+ else:
2326
+ winners = sorted(active_candidates)
2327
+ explanation.append(f"Winners are candidates {winners} (highest remaining scores)")
2328
+ return winners, "\\n".join(explanation)
2329
+
2330
+ # If only one candidate remains with positive score, they are the winner
2331
+ if len(remaining) == 1:
2332
+ winners = sorted(remaining)
2333
+ explanation.append(f"Only one candidate remains with positive score")
2334
+ explanation.append(f"Winners: {winners} (highest remaining scores)")
2335
+ return winners, "\\n".join(explanation)
2336
+
2337
+ ranking = profile.rankings[voter]
2338
+ # Filter ranking to show only active candidates
2339
+ active_ranking = [c for c in ranking if c in remaining]
2340
+ bottom = next(c for c in reversed(ranking) if c in remaining)
2341
+
2342
+ explanation.append(f"Step {step + 1}:")
2343
+ explanation.append(f"Voter {voter} (active candidates in ranking: {active_ranking}) vetoes {bottom}")
2344
+
2345
+ scores[bottom] -= 1
2346
+ explanation.append(f"Scores after veto: {dict({c: s for c, s in scores.items() if c in remaining})}")
2347
+
2348
+ if scores[bottom] == 0:
2349
+ active_candidates.remove(bottom)
2350
+ last_remaining = bottom
2351
+ explanation.append(f"Candidate {bottom} eliminated!")
2352
+ explanation.append("")
2353
+
2354
+ if last_remaining is not None:
2355
+ explanation.append(f"Winners: [{last_remaining}] (highest remaining scores)")
2356
+ return [last_remaining], "\\n".join(explanation)
2357
+ else:
2358
+ max_score = max(scores.values())
2359
+ winners = sorted([c for c in curr_cands if scores[c] == max_score])
2360
+ explanation.append(f"Winners: {winners} (highest remaining scores)")
2361
+ return winners, "\\n".join(explanation)
2362
+
2363
+ @vm(name="Consensus Builder",
2364
+ input_types=[ElectionTypes.PROFILE])
2365
+ def consensus_builder(profile, curr_cands=None, consensus_building_ranking=None, beta=0.5):
2366
+
2367
+ """Deterministic version of the Random Consensus Builder due to Charikar et al. (https://arxiv.org/abs/2306.17838).
2368
+
2369
+ The method processes candidates in reverse order of the consensus building ranking. When processing
2370
+ candidate i, it eliminates any candidate j above i in the consensus building ranking if a large enough fraction of voters (>= beta) prefer i to j. The winner is the last candidate that gets processed.
2371
+
2372
+ Args:
2373
+ profile (Profile): An anonymous profile of linear orders
2374
+ curr_cands (List[int], optional): Candidates to consider. Defaults to all candidates if not provided.
2375
+ consensus_building_ranking (List[int]): The ranking to use as the consensus builder. If not provided, uses the lexicographically first ranking of curr_cands.
2376
+ beta (float): Threshold for elimination (default 0.5). When processing candidate i, eliminates a candidate j above i in the consensus building ranking if the proportion of voters preferring i to j is >= beta
2377
+
2378
+ Returns:
2379
+ list: List containing the winning candidate
2380
+
2381
+ .. seealso::
2382
+ :meth:`pref_voting.probabilistic_methods.random_consensus_builder`
2383
+ :meth:`pref_voting.stochastic_methods.random_consensus_builder_st`
2384
+ """
2385
+
2386
+ if curr_cands is None:
2387
+ curr_cands = profile.candidates
2388
+
2389
+ if consensus_building_ranking is None:
2390
+ consensus_building_ranking = sorted(curr_cands)
2391
+
2392
+ # all candidates in curr_cands must be in consensus_building_ranking
2393
+ assert len([c for c in curr_cands if c not in consensus_building_ranking]) == 0
2394
+
2395
+ eliminated = set()
2396
+ last_processed = None
2397
+
2398
+ for i in reversed(consensus_building_ranking):
2399
+
2400
+ if i not in curr_cands or i in eliminated:
2401
+ continue
2402
+
2403
+ for j in consensus_building_ranking:
2404
+ if j == i or j not in curr_cands or j in eliminated:
2405
+ continue
2406
+
2407
+ if consensus_building_ranking.index(j) < consensus_building_ranking.index(i):
2408
+ support_ratio = profile.support(i, j) / profile.num_voters
2409
+ if support_ratio >= beta:
2410
+ eliminated.add(j)
2411
+
2412
+ last_processed = i
2413
+
2414
+ return [last_processed]
2415
+
2416
+ iterated_vms_with_explanation = [
2417
+ instant_runoff_with_explanation,
2418
+ coombs_with_explanation,
2419
+ plurality_with_runoff_put_with_explanation,
2420
+ baldwin_with_explanation,
2421
+ strict_nanson_with_explanation,
2422
+ weak_nanson_with_explanation,
2423
+ iterated_removal_cl_with_explanation,
2424
+ plurality_veto_with_explanation
2425
+ ]