pref_voting 1.16.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pref_voting/__init__.py +1 -0
- pref_voting/analysis.py +496 -0
- pref_voting/axiom.py +38 -0
- pref_voting/axiom_helpers.py +129 -0
- pref_voting/axioms.py +10 -0
- pref_voting/c1_methods.py +963 -0
- pref_voting/combined_methods.py +514 -0
- pref_voting/create_methods.py +128 -0
- pref_voting/data/examples/condorcet_winner/minimal_Anti-Plurality.soc +16 -0
- pref_voting/data/examples/condorcet_winner/minimal_Borda.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Bracket_Voting.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs_PUT.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs_TB.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Dowdall.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_TB.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Iterated_Removal_Condorcet_Loser.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Pareto.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Plurality.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_PluralityWRunoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Positive-Negative_Voting.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Simplified_Bucklin.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Superior_Voting.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Weighted_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Anti-Plurality.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Borda.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Bracket_Voting.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs.soc +21 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_PUT.soc +21 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_TB.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Dowdall.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_TB.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Plurality.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_PluralityWRunoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Positive-Negative_Voting.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Simplified_Bucklin.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Weighted_Bucklin.soc +19 -0
- pref_voting/data/voting_methods_properties.json +414 -0
- pref_voting/data/voting_methods_properties.json.lock +0 -0
- pref_voting/dominance_axioms.py +387 -0
- pref_voting/generate_profiles.py +801 -0
- pref_voting/generate_spatial_profiles.py +198 -0
- pref_voting/generate_utility_profiles.py +160 -0
- pref_voting/generate_weighted_majority_graphs.py +506 -0
- pref_voting/grade_methods.py +184 -0
- pref_voting/grade_profiles.py +357 -0
- pref_voting/helper.py +370 -0
- pref_voting/invariance_axioms.py +671 -0
- pref_voting/io/__init__.py +0 -0
- pref_voting/io/readers.py +432 -0
- pref_voting/io/writers.py +256 -0
- pref_voting/iterative_methods.py +2425 -0
- pref_voting/maj_graph_ex1.png +0 -0
- pref_voting/mappings.py +577 -0
- pref_voting/margin_based_methods.py +2345 -0
- pref_voting/monotonicity_axioms.py +872 -0
- pref_voting/num_evaluation_method.py +77 -0
- pref_voting/other_axioms.py +161 -0
- pref_voting/other_methods.py +939 -0
- pref_voting/pairwise_profiles.py +547 -0
- pref_voting/prob_voting_method.py +105 -0
- pref_voting/probabilistic_methods.py +287 -0
- pref_voting/profiles.py +856 -0
- pref_voting/profiles_with_ties.py +1069 -0
- pref_voting/rankings.py +466 -0
- pref_voting/scoring_methods.py +481 -0
- pref_voting/social_welfare_function.py +59 -0
- pref_voting/social_welfare_functions.py +7 -0
- pref_voting/spatial_profiles.py +448 -0
- pref_voting/stochastic_methods.py +99 -0
- pref_voting/strategic_axioms.py +1394 -0
- pref_voting/swf_axioms.py +173 -0
- pref_voting/utility_functions.py +102 -0
- pref_voting/utility_methods.py +178 -0
- pref_voting/utility_profiles.py +333 -0
- pref_voting/variable_candidate_axioms.py +640 -0
- pref_voting/variable_voter_axioms.py +3747 -0
- pref_voting/voting_method.py +355 -0
- pref_voting/voting_method_properties.py +92 -0
- pref_voting/voting_methods.py +8 -0
- pref_voting/voting_methods_registry.py +136 -0
- pref_voting/weighted_majority_graphs.py +1539 -0
- pref_voting-1.16.31.dist-info/METADATA +208 -0
- pref_voting-1.16.31.dist-info/RECORD +92 -0
- pref_voting-1.16.31.dist-info/WHEEL +4 -0
- pref_voting-1.16.31.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,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
|
+
]
|