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