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,872 @@
|
|
1
|
+
"""
|
2
|
+
File: monotonicity_axioms.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: November 4, 2023
|
5
|
+
|
6
|
+
Monotonicity axioms
|
7
|
+
"""
|
8
|
+
|
9
|
+
from pref_voting.axiom import Axiom
|
10
|
+
from pref_voting.axiom_helpers import *
|
11
|
+
from pref_voting.rankings import Ranking
|
12
|
+
import numpy as np
|
13
|
+
from itertools import product
|
14
|
+
import copy
|
15
|
+
import math
|
16
|
+
|
17
|
+
def ranks_above(ranking,c):
|
18
|
+
"""
|
19
|
+
Returns the number of positions above candidate ``c`` in ``ranking``, taking into account ties in Ranking objects.
|
20
|
+
"""
|
21
|
+
if isinstance(ranking, tuple):
|
22
|
+
|
23
|
+
return ranking.index(c)
|
24
|
+
|
25
|
+
if isinstance(ranking, Ranking):
|
26
|
+
|
27
|
+
ranking.normalize_ranks()
|
28
|
+
|
29
|
+
if any([ranking.rmap[d] == ranking.rmap[c] for d in ranking.cands if c!=d]):
|
30
|
+
return 2 * (ranking.rmap[c] - 1) + 1
|
31
|
+
else:
|
32
|
+
return 2 * (ranking.rmap[c] - 1)
|
33
|
+
|
34
|
+
def ranks_below(ranking,c):
|
35
|
+
"""
|
36
|
+
Returns the number of positions below candidate ``c`` in ``ranking``, taking into account ties in Ranking objects.
|
37
|
+
"""
|
38
|
+
if isinstance(ranking, tuple):
|
39
|
+
|
40
|
+
return len(ranking) - ranking.index(c) - 1
|
41
|
+
|
42
|
+
if isinstance(ranking, Ranking):
|
43
|
+
|
44
|
+
ranking.normalize_ranks()
|
45
|
+
|
46
|
+
if any([ranking.rmap[d] == ranking.rmap[c] for d in ranking.cands if c!=d]):
|
47
|
+
return 2 * (len(ranking.cands) - ranking.rmap[c]) + 1
|
48
|
+
else:
|
49
|
+
return 2 * (len(ranking.cands) - ranking.rmap[c])
|
50
|
+
|
51
|
+
def n_rank_lift(ranking, c, n):
|
52
|
+
"""
|
53
|
+
Return a ranking in which ``c`` is moved up n positions in ``ranking``.
|
54
|
+
"""
|
55
|
+
if isinstance(ranking, tuple):
|
56
|
+
assert c not in ranking[:n], f"there are not enough ranks above {c} to lift {c} {n} ranks"
|
57
|
+
_new_ranking = copy.deepcopy(ranking)
|
58
|
+
c_idx = _new_ranking.index(c)
|
59
|
+
new_ranking = _new_ranking[:c_idx-n] + (_new_ranking[c_idx],) + _new_ranking[c_idx-n:c_idx] + _new_ranking[c_idx+1:]
|
60
|
+
|
61
|
+
if isinstance(ranking, Ranking):
|
62
|
+
|
63
|
+
ranking.normalize_ranks()
|
64
|
+
|
65
|
+
assert ranks_above(ranking,c) >= n, f"there are not enough ranks above {c} to lift {c} {n} ranks"
|
66
|
+
|
67
|
+
new_ranking_dict = dict()
|
68
|
+
|
69
|
+
if any([ranking.rmap[d] == ranking.rmap[c] for d in ranking.cands if c!=d]): # if c is tied with another candidate
|
70
|
+
|
71
|
+
if n%2 == 0:
|
72
|
+
for d in ranking.cands:
|
73
|
+
if d == c:
|
74
|
+
new_ranking_dict[d] = ranking.rmap[d] - (n/2)
|
75
|
+
else:
|
76
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
77
|
+
|
78
|
+
else:
|
79
|
+
for d in ranking.cands:
|
80
|
+
if d == c:
|
81
|
+
new_ranking_dict[d] = ranking.rmap[d] - (math.floor(n/2) + math.ceil(n/2))/2
|
82
|
+
else:
|
83
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
84
|
+
|
85
|
+
new_ranking = Ranking(new_ranking_dict)
|
86
|
+
|
87
|
+
else:
|
88
|
+
if n%2 == 0:
|
89
|
+
for d in ranking.cands:
|
90
|
+
if d == c:
|
91
|
+
new_ranking_dict[d] = ranking.rmap[d] - ((n//2) + (n//2 + 1))/2
|
92
|
+
else:
|
93
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
94
|
+
else:
|
95
|
+
for d in ranking.cands:
|
96
|
+
if d == c:
|
97
|
+
new_ranking_dict[d] = ranking.rmap[d] - math.ceil(n/2)
|
98
|
+
else:
|
99
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
100
|
+
|
101
|
+
new_ranking = Ranking(new_ranking_dict)
|
102
|
+
|
103
|
+
new_ranking.normalize_ranks()
|
104
|
+
|
105
|
+
return new_ranking
|
106
|
+
|
107
|
+
def n_rank_drop(ranking, c, n):
|
108
|
+
"""
|
109
|
+
Return a ranking in which ``c`` is moved down n positions in ``ranking``.
|
110
|
+
"""
|
111
|
+
if isinstance(ranking, tuple):
|
112
|
+
# assert that there are n ranks below c
|
113
|
+
assert len(ranking) - ranking.index(c) - 1 >= n, f"there are not enough ranks below {c} to drop {c} {n} ranks"
|
114
|
+
_new_ranking = copy.deepcopy(ranking)
|
115
|
+
c_idx = _new_ranking.index(c)
|
116
|
+
new_ranking = _new_ranking[:c_idx] + _new_ranking[c_idx+1:c_idx+n+1] + (_new_ranking[c_idx],) + _new_ranking[c_idx+n+1:]
|
117
|
+
|
118
|
+
if isinstance(ranking, Ranking):
|
119
|
+
|
120
|
+
ranking.normalize_ranks()
|
121
|
+
|
122
|
+
assert ranks_below(ranking,c) >= n, f"there are not enough ranks below {c} to drop {c} {n} ranks"
|
123
|
+
|
124
|
+
new_ranking_dict = dict()
|
125
|
+
|
126
|
+
if any([ranking.rmap[d] == ranking.rmap[c] for d in ranking.cands if c!=d]): # if c is tied with another candidate
|
127
|
+
|
128
|
+
if n%2 == 0:
|
129
|
+
for d in ranking.cands:
|
130
|
+
if d == c:
|
131
|
+
new_ranking_dict[d] = ranking.rmap[d] + (n/2)
|
132
|
+
else:
|
133
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
134
|
+
|
135
|
+
else:
|
136
|
+
for d in ranking.cands:
|
137
|
+
if d == c:
|
138
|
+
new_ranking_dict[d] = ranking.rmap[d] + (math.floor(n/2) + math.ceil(n/2))/2
|
139
|
+
else:
|
140
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
141
|
+
|
142
|
+
new_ranking = Ranking(new_ranking_dict)
|
143
|
+
|
144
|
+
else:
|
145
|
+
if n%2 == 0:
|
146
|
+
for d in ranking.cands:
|
147
|
+
if d == c:
|
148
|
+
new_ranking_dict[d] = ranking.rmap[d] + ((n//2) + (n//2 + 1))/2
|
149
|
+
else:
|
150
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
151
|
+
else:
|
152
|
+
for d in ranking.cands:
|
153
|
+
if d == c:
|
154
|
+
new_ranking_dict[d] = ranking.rmap[d] + math.ceil(n/2)
|
155
|
+
else:
|
156
|
+
new_ranking_dict[d] = ranking.rmap[d]
|
157
|
+
|
158
|
+
new_ranking = Ranking(new_ranking_dict)
|
159
|
+
|
160
|
+
new_ranking.normalize_ranks()
|
161
|
+
|
162
|
+
return new_ranking
|
163
|
+
|
164
|
+
def has_monotonicity_violation(profile, vm, verbose = False, violation_type = "Lift", check_probabilities = False, one_rank_monotonicity = False):
|
165
|
+
"""
|
166
|
+
If violation_type = "Lift", returns True if there is some winning candidate A and some voter v such that lifting A up some number of positions in v's ranking causes A to lose.
|
167
|
+
|
168
|
+
If violation_type = "Drop", returns True if there is some losing candidate A and some voter v such that dropping A down some number of positions in v's ranking causes A to win.
|
169
|
+
|
170
|
+
If checking_probabilities = True, returns True if there is some candidate whose probability of winning decreases after a lifting or increases after a dropping.
|
171
|
+
|
172
|
+
If one_rank_monotonicity = True, then the function will check lifts/drops of one rank only.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
profile: a Profile object.
|
176
|
+
vm (VotingMethod): A voting method to test.
|
177
|
+
verbose (bool, default=False): If a violation is found, display the violation.
|
178
|
+
violation_type: default is "Lift"
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
Result of the test (bool): Returns True if there is a violation and False otherwise.
|
182
|
+
|
183
|
+
.. note::
|
184
|
+
If a voting method violates monotonicity, then it violates one-rank monotonicity, so setting one_rank_monotonicity = True is sufficient for testing whether a method violates monotonicity (though not for testing the frequency of monotonicity violations).
|
185
|
+
|
186
|
+
"""
|
187
|
+
|
188
|
+
_rankings, _rcounts = profile.rankings_counts
|
189
|
+
|
190
|
+
if isinstance(profile, Profile):
|
191
|
+
rankings = [tuple(r) for r in list(_rankings)]
|
192
|
+
|
193
|
+
if isinstance(profile, ProfileWithTies):
|
194
|
+
rankings = _rankings
|
195
|
+
|
196
|
+
rcounts = list(_rcounts)
|
197
|
+
old_rankings = copy.deepcopy(rankings)
|
198
|
+
|
199
|
+
ws = vm(profile)
|
200
|
+
|
201
|
+
if violation_type == "Lift":
|
202
|
+
for w in ws:
|
203
|
+
for r_idx, r in enumerate(rankings):
|
204
|
+
|
205
|
+
if rcounts[r_idx] == 0:
|
206
|
+
continue
|
207
|
+
|
208
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
209
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
210
|
+
|
211
|
+
if r[0] != w:
|
212
|
+
old_ranking = copy.deepcopy(r)
|
213
|
+
|
214
|
+
if one_rank_monotonicity:
|
215
|
+
ranks_above_w = 1
|
216
|
+
else:
|
217
|
+
ranks_above_w = ranks_above(r, w)
|
218
|
+
|
219
|
+
for n in range(1, ranks_above_w+1):
|
220
|
+
|
221
|
+
new_ranking = n_rank_lift(r, w, n)
|
222
|
+
new_rankings = old_rankings + [new_ranking]
|
223
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
224
|
+
new_rcounts[r_idx] -= 1
|
225
|
+
|
226
|
+
if isinstance(profile, Profile):
|
227
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
228
|
+
if isinstance(profile, ProfileWithTies):
|
229
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
230
|
+
if profile.using_extended_strict_preference:
|
231
|
+
new_prof.use_extended_strict_preference()
|
232
|
+
|
233
|
+
new_ws = vm(new_prof)
|
234
|
+
|
235
|
+
if w not in new_ws:
|
236
|
+
if verbose:
|
237
|
+
if n==1:
|
238
|
+
print(f"Monotonicity violation for {vm.name} by lifting {w} one rank:")
|
239
|
+
else:
|
240
|
+
print(f"Monotonicity violation for {vm.name} by lifting {w} by {n} ranks:")
|
241
|
+
profile.display()
|
242
|
+
print(profile.description())
|
243
|
+
profile.display_margin_graph()
|
244
|
+
print(f"{vm.name} winners: ", ws)
|
245
|
+
print("Original ranking: ", old_ranking)
|
246
|
+
print(f"New ranking: {new_ranking}")
|
247
|
+
new_prof.display()
|
248
|
+
print(new_prof.description())
|
249
|
+
new_prof.display_margin_graph()
|
250
|
+
print(f"{vm.name} winners in updated profile:", new_ws)
|
251
|
+
return True
|
252
|
+
|
253
|
+
if w in new_ws and check_probabilities == True and len(new_ws) > len(ws):
|
254
|
+
if verbose:
|
255
|
+
if n==1:
|
256
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by lifting {w} one rank:")
|
257
|
+
else:
|
258
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by lifting {w} by {n} ranks:")
|
259
|
+
profile.display()
|
260
|
+
print(profile.description())
|
261
|
+
profile.display_margin_graph()
|
262
|
+
print(f"{vm.name} winners: ", ws)
|
263
|
+
print("Original ranking: ", old_ranking)
|
264
|
+
print(f"New ranking: {new_ranking}")
|
265
|
+
new_prof.display()
|
266
|
+
print(new_prof.description())
|
267
|
+
new_prof.display_margin_graph()
|
268
|
+
print(f"{vm.name} winners in updated profile:", new_ws)
|
269
|
+
return True
|
270
|
+
|
271
|
+
elif violation_type == "Drop":
|
272
|
+
for l in profile.candidates:
|
273
|
+
if l not in ws:
|
274
|
+
for r_idx, r in enumerate(rankings):
|
275
|
+
|
276
|
+
if rcounts[r_idx] == 0:
|
277
|
+
continue
|
278
|
+
|
279
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
280
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
281
|
+
|
282
|
+
if r[-1] != l:
|
283
|
+
old_ranking = copy.deepcopy(r)
|
284
|
+
|
285
|
+
if one_rank_monotonicity:
|
286
|
+
ranks_below_l = 1
|
287
|
+
else:
|
288
|
+
ranks_below_l = ranks_below(r, l)
|
289
|
+
|
290
|
+
for n in range(1, ranks_below_l+1):
|
291
|
+
|
292
|
+
new_ranking = n_rank_drop(r, l, n)
|
293
|
+
new_rankings = old_rankings + [new_ranking]
|
294
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
295
|
+
new_rcounts[r_idx] -= 1
|
296
|
+
|
297
|
+
if isinstance(profile, Profile):
|
298
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
299
|
+
if isinstance(profile, ProfileWithTies):
|
300
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
301
|
+
if profile.using_extended_strict_preference:
|
302
|
+
new_prof.use_extended_strict_preference()
|
303
|
+
|
304
|
+
new_ws = vm(new_prof)
|
305
|
+
|
306
|
+
if l in new_ws:
|
307
|
+
if verbose:
|
308
|
+
if n==1:
|
309
|
+
print(f"Monotonicity violation for {vm.name} by dropping {l} one rank:")
|
310
|
+
else:
|
311
|
+
print(f"Monotonicity violation for {vm.name} by dropping {l} by {n} ranks:")
|
312
|
+
profile.display()
|
313
|
+
print(profile.description())
|
314
|
+
profile.display_margin_graph()
|
315
|
+
print(f"{vm.name} winners: ", ws)
|
316
|
+
print("Original ranking: ", old_ranking)
|
317
|
+
print(f"New ranking: {new_ranking}")
|
318
|
+
new_prof.display()
|
319
|
+
print(new_prof.description())
|
320
|
+
new_prof.display_margin_graph()
|
321
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
322
|
+
return True
|
323
|
+
|
324
|
+
if check_probabilities and l in ws:
|
325
|
+
for r_idx, r in enumerate(rankings):
|
326
|
+
|
327
|
+
if rcounts[r_idx] == 0:
|
328
|
+
continue
|
329
|
+
|
330
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
331
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
332
|
+
|
333
|
+
if r[-1] != l:
|
334
|
+
old_ranking = copy.deepcopy(r)
|
335
|
+
|
336
|
+
if one_rank_monotonicity:
|
337
|
+
ranks_below_l = 1
|
338
|
+
else:
|
339
|
+
ranks_below_l = ranks_below(r, l)
|
340
|
+
|
341
|
+
new_ranking = one_rank_drop(r, l)
|
342
|
+
new_rankings = old_rankings + [new_ranking]
|
343
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
344
|
+
new_rcounts[r_idx] -= 1
|
345
|
+
|
346
|
+
if isinstance(profile, Profile):
|
347
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
348
|
+
if isinstance(profile, ProfileWithTies):
|
349
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
350
|
+
if profile.using_extended_strict_preference:
|
351
|
+
new_prof.use_extended_strict_preference()
|
352
|
+
|
353
|
+
new_ws = vm(new_prof)
|
354
|
+
if l in new_ws and len(new_ws) < len(ws):
|
355
|
+
if verbose:
|
356
|
+
if n==1:
|
357
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by dropping {l} one rank:")
|
358
|
+
else:
|
359
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by dropping {l} by {n} ranks:")
|
360
|
+
profile.display()
|
361
|
+
print(profile.description())
|
362
|
+
profile.display_margin_graph()
|
363
|
+
print(f"{vm.name} winners: ", ws)
|
364
|
+
print("Original ranking: ", old_ranking)
|
365
|
+
print(f"New ranking: {new_ranking}")
|
366
|
+
new_prof.display()
|
367
|
+
print(new_prof.description())
|
368
|
+
new_prof.display_margin_graph()
|
369
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
370
|
+
return True
|
371
|
+
|
372
|
+
return False
|
373
|
+
|
374
|
+
def find_all_monotonicity_violations(profile, vm, verbose = False, violation_type = "Lift", check_probabilities = False, one_rank_monotonicity = False):
|
375
|
+
"""
|
376
|
+
If violation_type = "Lift", returns all tuples (candidate, ranking, "Lift", n) such that the candidate wins in the original profile but loses after lifting the candidate up n positions in the ranking.
|
377
|
+
|
378
|
+
If violation_type = "Drop", returns all tuples (candidate, ranking, "Drop", n) such that the candidate loses in the original profile but wins after dropping the candidate down n positions in the ranking.
|
379
|
+
|
380
|
+
If checking_probabilities = True, returns all tuples (candidate, ranking, violation_type, n) such that the candidate's probability of winning decreases after a lifting or increases after a dropping.
|
381
|
+
|
382
|
+
If one_rank_monotonicity = True, then the function will check lifts/drops of one rank only.
|
383
|
+
|
384
|
+
Args:
|
385
|
+
profile: a Profile object.
|
386
|
+
vm (VotingMethod): A voting method to test.
|
387
|
+
verbose (bool, default=False): If a violation is found, display the violation.
|
388
|
+
violation_type: default is "Lift"
|
389
|
+
|
390
|
+
Returns:
|
391
|
+
A list of tuples (candidate, ranking, violation_type, positions lifted/dropped) witnessing violations of monotonicity.
|
392
|
+
|
393
|
+
.. note::
|
394
|
+
If a voting method violates monotonicity, then it violates one-rank monotonicity, so setting one_rank_monotonicity = True is sufficient for testing whether a method violates monotonicity (though not for testing the frequency of monotonicity violations).
|
395
|
+
"""
|
396
|
+
|
397
|
+
_rankings, _rcounts = profile.rankings_counts
|
398
|
+
|
399
|
+
if isinstance(profile, Profile):
|
400
|
+
rankings = [tuple(r) for r in list(_rankings)]
|
401
|
+
|
402
|
+
if isinstance(profile, ProfileWithTies):
|
403
|
+
rankings = _rankings
|
404
|
+
|
405
|
+
rcounts = list(_rcounts)
|
406
|
+
old_rankings = copy.deepcopy(rankings)
|
407
|
+
|
408
|
+
ws = vm(profile)
|
409
|
+
witnesses = list()
|
410
|
+
|
411
|
+
if violation_type == "Lift":
|
412
|
+
for w in ws:
|
413
|
+
for r_idx, r in enumerate(rankings):
|
414
|
+
|
415
|
+
if rcounts[r_idx] == 0:
|
416
|
+
continue
|
417
|
+
|
418
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
419
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
420
|
+
|
421
|
+
if r[0] != w:
|
422
|
+
old_ranking = copy.deepcopy(r)
|
423
|
+
|
424
|
+
if one_rank_monotonicity:
|
425
|
+
ranks_above_w = 1
|
426
|
+
else:
|
427
|
+
ranks_above_w = ranks_above(r, w)
|
428
|
+
|
429
|
+
for n in range(1, ranks_above_w+1):
|
430
|
+
new_ranking = n_rank_lift(r, w, n)
|
431
|
+
new_rankings = old_rankings + [new_ranking]
|
432
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
433
|
+
new_rcounts[r_idx] -= 1
|
434
|
+
|
435
|
+
if isinstance(profile, Profile):
|
436
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
437
|
+
if isinstance(profile, ProfileWithTies):
|
438
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
439
|
+
if profile.using_extended_strict_preference:
|
440
|
+
new_prof.use_extended_strict_preference()
|
441
|
+
|
442
|
+
new_ws = vm(new_prof)
|
443
|
+
|
444
|
+
if w not in new_ws:
|
445
|
+
witnesses.append((w, old_ranking, "Lift", n))
|
446
|
+
if verbose:
|
447
|
+
if n==1:
|
448
|
+
print(f"Monotonicity violation for {vm.name} by lifting {w} one rank:")
|
449
|
+
else:
|
450
|
+
print(f"Monotonicity violation for {vm.name} by lifting {w} by {n} ranks:")
|
451
|
+
profile.display()
|
452
|
+
print(profile.description())
|
453
|
+
profile.display_margin_graph()
|
454
|
+
print(f"{vm.name} winners: ", ws)
|
455
|
+
print("Original ranking ", old_ranking)
|
456
|
+
print(f"New ranking: {new_ranking}")
|
457
|
+
new_prof.display()
|
458
|
+
print(new_prof.description())
|
459
|
+
new_prof.display_margin_graph()
|
460
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
461
|
+
print("")
|
462
|
+
|
463
|
+
if w in new_ws and check_probabilities == True and len(new_ws) > len(ws):
|
464
|
+
witnesses.append((w, old_ranking, "Lift", n))
|
465
|
+
if verbose:
|
466
|
+
if n==1:
|
467
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by lifting {w} one rank:")
|
468
|
+
else:
|
469
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by lifting {w} by {n} ranks:")
|
470
|
+
profile.display()
|
471
|
+
print(profile.description())
|
472
|
+
profile.display_margin_graph()
|
473
|
+
print(f"{vm.name} winners: ", ws)
|
474
|
+
print("Original ranking: ", old_ranking)
|
475
|
+
print(f"New ranking: {new_ranking}")
|
476
|
+
new_prof.display()
|
477
|
+
print(new_prof.description())
|
478
|
+
new_prof.display_margin_graph()
|
479
|
+
print(f"{vm.name} winners in updated profile:", new_ws)
|
480
|
+
print("")
|
481
|
+
|
482
|
+
elif violation_type == "Drop":
|
483
|
+
for l in profile.candidates:
|
484
|
+
if l not in ws:
|
485
|
+
for r_idx, r in enumerate(rankings):
|
486
|
+
|
487
|
+
if rcounts[r_idx] == 0:
|
488
|
+
continue
|
489
|
+
|
490
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
491
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
492
|
+
|
493
|
+
if r[-1] != l:
|
494
|
+
old_ranking = copy.deepcopy(r)
|
495
|
+
|
496
|
+
if one_rank_monotonicity:
|
497
|
+
ranks_below_l = 1
|
498
|
+
else:
|
499
|
+
ranks_below_l = ranks_below(r, l)
|
500
|
+
|
501
|
+
for n in range(1, ranks_below_l+1):
|
502
|
+
new_ranking = n_rank_drop(r, l, n)
|
503
|
+
new_rankings = old_rankings + [new_ranking]
|
504
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
505
|
+
new_rcounts[r_idx] -= 1
|
506
|
+
|
507
|
+
if isinstance(profile, Profile):
|
508
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
509
|
+
if isinstance(profile, ProfileWithTies):
|
510
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
511
|
+
if profile.using_extended_strict_preference:
|
512
|
+
new_prof.use_extended_strict_preference()
|
513
|
+
|
514
|
+
new_ws = vm(new_prof)
|
515
|
+
|
516
|
+
if l in new_ws:
|
517
|
+
witnesses.append((l, old_ranking, "Drop", n))
|
518
|
+
if verbose:
|
519
|
+
if n==1:
|
520
|
+
print(f"Monotonicity violation for {vm.name} by dropping {l} one rank:")
|
521
|
+
else:
|
522
|
+
print(f"Monotonicity violation for {vm.name} by dropping {l} by {n} ranks:")
|
523
|
+
profile.display()
|
524
|
+
print(profile.description())
|
525
|
+
profile.display_margin_graph()
|
526
|
+
print(f"{vm.name} winners: ", ws)
|
527
|
+
print("Original ranking: ", old_ranking)
|
528
|
+
print(f"New ranking: {new_ranking}")
|
529
|
+
new_prof.display()
|
530
|
+
print(new_prof.description())
|
531
|
+
new_prof.display_margin_graph()
|
532
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
533
|
+
print("")
|
534
|
+
|
535
|
+
if check_probabilities and l in ws:
|
536
|
+
for r_idx, r in enumerate(rankings):
|
537
|
+
|
538
|
+
if rcounts[r_idx] == 0:
|
539
|
+
continue
|
540
|
+
|
541
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
542
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
543
|
+
|
544
|
+
if r[-1] != l:
|
545
|
+
old_ranking = copy.deepcopy(r)
|
546
|
+
|
547
|
+
if one_rank_monotonicity:
|
548
|
+
ranks_below_l = 1
|
549
|
+
else:
|
550
|
+
ranks_below_l = ranks_below(r, l)
|
551
|
+
|
552
|
+
for n in range(1, ranks_below_l+1):
|
553
|
+
new_ranking = n_rank_drop(r, l, n)
|
554
|
+
new_rankings = old_rankings + [new_ranking]
|
555
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
556
|
+
new_rcounts[r_idx] -= 1
|
557
|
+
|
558
|
+
if isinstance(profile, Profile):
|
559
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
560
|
+
if isinstance(profile, ProfileWithTies):
|
561
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
562
|
+
if profile.using_extended_strict_preference:
|
563
|
+
new_prof.use_extended_strict_preference()
|
564
|
+
|
565
|
+
new_ws = vm(new_prof)
|
566
|
+
|
567
|
+
if l in new_ws and len(new_ws) < len(ws):
|
568
|
+
witnesses.append((l, old_ranking, "Drop", n))
|
569
|
+
if verbose:
|
570
|
+
if n==1:
|
571
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by dropping {l} one rank:")
|
572
|
+
else:
|
573
|
+
print(f"Probabilistic monotonicity violation for {vm.name} by dropping {l} by {n} ranks:")
|
574
|
+
profile.display()
|
575
|
+
print(profile.description())
|
576
|
+
profile.display_margin_graph()
|
577
|
+
print(f"{vm.name} winners: ", ws)
|
578
|
+
print("Original ranking: ", old_ranking)
|
579
|
+
print(f"New ranking: {new_ranking}")
|
580
|
+
new_prof.display()
|
581
|
+
print(new_prof.description())
|
582
|
+
new_prof.display_margin_graph()
|
583
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
584
|
+
print("")
|
585
|
+
|
586
|
+
return witnesses
|
587
|
+
|
588
|
+
monotonicity = Axiom(
|
589
|
+
"Monotonicity",
|
590
|
+
has_violation = has_monotonicity_violation,
|
591
|
+
find_all_violations = find_all_monotonicity_violations,
|
592
|
+
)
|
593
|
+
|
594
|
+
def lift_to_first(ranking, c):
|
595
|
+
"""
|
596
|
+
Return a ranking in which ``c`` is moved to first position in ``ranking``.
|
597
|
+
"""
|
598
|
+
|
599
|
+
if isinstance(ranking, tuple):
|
600
|
+
assert c != ranking[0], "can't lift a candidate already in first place"
|
601
|
+
new_ranking = copy.deepcopy(ranking)
|
602
|
+
c_idx = new_ranking.index(c)
|
603
|
+
new_ranking = (c,) + new_ranking[:c_idx] + new_ranking[c_idx+1:]
|
604
|
+
|
605
|
+
if isinstance(ranking, Ranking):
|
606
|
+
assert not ranking.first() == [c], "can't lift a candidate already uniquely in first place"
|
607
|
+
new_ranking = Ranking({a: ranking.rmap[a] if a !=c else min(ranking.ranks) - 1 for a in ranking.cands})
|
608
|
+
new_ranking.normalize_ranks()
|
609
|
+
|
610
|
+
return new_ranking
|
611
|
+
|
612
|
+
def drop_to_last(ranking, c):
|
613
|
+
"""
|
614
|
+
Return a ranking in which ``c`` is moved to last position in ``ranking``.
|
615
|
+
"""
|
616
|
+
|
617
|
+
if isinstance(ranking, tuple):
|
618
|
+
assert c != ranking[-1], "can't drop a candidate already in last place"
|
619
|
+
new_ranking = copy.deepcopy(ranking)
|
620
|
+
c_idx = new_ranking.index(c)
|
621
|
+
new_ranking = new_ranking[:c_idx] + new_ranking[c_idx+1:] + (c,)
|
622
|
+
|
623
|
+
if isinstance(ranking, Ranking):
|
624
|
+
assert not ranking.last() == [c], "can't drop a candidate already uniquely in last place"
|
625
|
+
new_ranking = Ranking({a: ranking.rmap[a] if a !=c else max(ranking.ranks) + 1 for a in ranking.cands})
|
626
|
+
new_ranking.normalize_ranks()
|
627
|
+
|
628
|
+
return new_ranking
|
629
|
+
|
630
|
+
def has_weak_positive_responsiveness_violation(profile, vm, verbose = False, violation_type="Lift"):
|
631
|
+
"""
|
632
|
+
If violation_type = "Lift", returns True if there is some winning candidate A and some voter v who ranks A last such that v moving A into first place does not make A the unique winner
|
633
|
+
|
634
|
+
If violation_type = "Drop", returns True if there is some candidate A who is either a loser or a non-unique winner and some voter v who ranks A first such that v moving A into last place does not make A a loser.
|
635
|
+
|
636
|
+
Args:
|
637
|
+
profile: a Profile object.
|
638
|
+
vm (VotingMethod): A voting method to test.
|
639
|
+
verbose (bool, default=False): If a violation is found, display the violation.
|
640
|
+
violation_type: default is "Lift"
|
641
|
+
|
642
|
+
Returns:
|
643
|
+
Result of the test (bool): Returns True if there is a violation and False otherwise.
|
644
|
+
|
645
|
+
"""
|
646
|
+
|
647
|
+
_rankings, _rcounts = profile.rankings_counts
|
648
|
+
|
649
|
+
if isinstance(profile, Profile):
|
650
|
+
rankings = [tuple(r) for r in list(_rankings)]
|
651
|
+
|
652
|
+
if isinstance(profile, ProfileWithTies):
|
653
|
+
rankings = _rankings
|
654
|
+
|
655
|
+
rcounts = list(_rcounts)
|
656
|
+
old_rankings = copy.deepcopy(rankings)
|
657
|
+
|
658
|
+
ws = vm(profile)
|
659
|
+
|
660
|
+
if violation_type == "Lift":
|
661
|
+
for w in ws:
|
662
|
+
for r_idx, r in enumerate(rankings):
|
663
|
+
|
664
|
+
if rcounts[r_idx] == 0:
|
665
|
+
continue
|
666
|
+
|
667
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
668
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
669
|
+
|
670
|
+
if r[-1] == w and not r[0] == w:
|
671
|
+
old_ranking = copy.deepcopy(r)
|
672
|
+
new_ranking = lift_to_first(r, w)
|
673
|
+
new_rankings = old_rankings + [new_ranking]
|
674
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
675
|
+
new_rcounts[r_idx] -= 1
|
676
|
+
|
677
|
+
if isinstance(profile, Profile):
|
678
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
679
|
+
|
680
|
+
if isinstance(profile, ProfileWithTies):
|
681
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
682
|
+
if profile.using_extended_strict_preference:
|
683
|
+
new_prof.use_extended_strict_preference()
|
684
|
+
|
685
|
+
new_ws = vm(new_prof)
|
686
|
+
|
687
|
+
if len(new_ws) > 1 or (len(new_ws) == 1 and new_ws[0] != w):
|
688
|
+
if verbose:
|
689
|
+
print(f"Weak positive responsiveness violation for {vm.name} by lifting {w}:")
|
690
|
+
profile.display()
|
691
|
+
print(profile.description())
|
692
|
+
profile.display_margin_graph()
|
693
|
+
print(f"{vm.name} winners: ", ws)
|
694
|
+
print("Original ranking: ", old_ranking)
|
695
|
+
print(f"New ranking: {new_ranking}")
|
696
|
+
new_prof.display()
|
697
|
+
print(new_prof.description())
|
698
|
+
new_prof.display_margin_graph()
|
699
|
+
print(f"{vm.name} winners in updated profile:", new_ws)
|
700
|
+
return True
|
701
|
+
|
702
|
+
elif violation_type == "Drop":
|
703
|
+
for l in profile.candidates:
|
704
|
+
if l not in ws or (l in ws and len(ws) > 1):
|
705
|
+
for r_idx, r in enumerate(rankings):
|
706
|
+
|
707
|
+
if rcounts[r_idx] == 0:
|
708
|
+
continue
|
709
|
+
|
710
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
711
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
712
|
+
|
713
|
+
if r[0] == l and not r[-1] == l:
|
714
|
+
old_ranking = copy.deepcopy(r)
|
715
|
+
new_ranking = drop_to_last(r, l)
|
716
|
+
new_rankings = old_rankings + [new_ranking]
|
717
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
718
|
+
new_rcounts[r_idx] -= 1
|
719
|
+
|
720
|
+
if isinstance(profile, Profile):
|
721
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
722
|
+
if isinstance(profile, ProfileWithTies):
|
723
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
724
|
+
if profile.using_extended_strict_preference:
|
725
|
+
new_prof.use_extended_strict_preference()
|
726
|
+
|
727
|
+
new_ws = vm(new_prof)
|
728
|
+
|
729
|
+
if l in new_ws:
|
730
|
+
if verbose:
|
731
|
+
print(f"Weak positive responsiveness violation for {vm.name} by dropping {l}:")
|
732
|
+
profile.display()
|
733
|
+
print(profile.description())
|
734
|
+
profile.display_margin_graph()
|
735
|
+
print(f"{vm.name} winners: ", ws)
|
736
|
+
print("Original ranking: ", old_ranking)
|
737
|
+
print(f"New ranking: {new_ranking}")
|
738
|
+
new_prof.display()
|
739
|
+
print(new_prof.description())
|
740
|
+
new_prof.display_margin_graph()
|
741
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
742
|
+
return True
|
743
|
+
|
744
|
+
return False
|
745
|
+
|
746
|
+
def find_all_weak_positive_responsiveness_violations(profile, vm, verbose = False, violation_type="Lift"):
|
747
|
+
"""
|
748
|
+
If violation_type = "Lift", returns all pairs (candidate, ranking) such that the candidate is a unique winner in the original profile but is not a unique winner after the voter moves the candidate from last to first place in the ranking.
|
749
|
+
|
750
|
+
If violation_type = "Drop", returns all pairs (candidate, ranking) such that the candidate is either a loser or a non-unique winner in the original profile but is a winner after the voter moves the candidate from first to last place in the ranking.
|
751
|
+
|
752
|
+
Args:
|
753
|
+
profile: a Profile object.
|
754
|
+
vm (VotingMethod): A voting method to test.
|
755
|
+
verbose (bool, default=False): If a violation is found, display the violation.
|
756
|
+
violation_type: default is "Lift"
|
757
|
+
|
758
|
+
Returns:
|
759
|
+
A list of pairs (candidate, ranking) witnessing violations of weak positive responsiveness.
|
760
|
+
|
761
|
+
"""
|
762
|
+
|
763
|
+
_rankings, _rcounts = profile.rankings_counts
|
764
|
+
|
765
|
+
if isinstance(profile, Profile):
|
766
|
+
rankings = [tuple(r) for r in list(_rankings)]
|
767
|
+
|
768
|
+
if isinstance(profile, ProfileWithTies):
|
769
|
+
rankings = _rankings
|
770
|
+
|
771
|
+
rcounts = list(_rcounts)
|
772
|
+
old_rankings = copy.deepcopy(rankings)
|
773
|
+
|
774
|
+
ws = vm(profile)
|
775
|
+
witnesses = list()
|
776
|
+
|
777
|
+
if violation_type == "Lift":
|
778
|
+
for w in ws:
|
779
|
+
for r_idx, r in enumerate(rankings):
|
780
|
+
|
781
|
+
if rcounts[r_idx] == 0:
|
782
|
+
continue
|
783
|
+
|
784
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
785
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
786
|
+
|
787
|
+
if r[-1] == w and not r[0] == w:
|
788
|
+
old_ranking = copy.deepcopy(r)
|
789
|
+
new_ranking = lift_to_first(r, w)
|
790
|
+
new_rankings = old_rankings + [new_ranking]
|
791
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
792
|
+
new_rcounts[r_idx] -= 1
|
793
|
+
|
794
|
+
if isinstance(profile, Profile):
|
795
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
796
|
+
if isinstance(profile, ProfileWithTies):
|
797
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
798
|
+
if profile.using_extended_strict_preference:
|
799
|
+
new_prof.use_extended_strict_preference()
|
800
|
+
|
801
|
+
new_ws = vm(new_prof)
|
802
|
+
|
803
|
+
if len(new_ws) > 1 or (len(new_ws) == 1 and new_ws[0] != w):
|
804
|
+
witnesses.append((w, old_ranking, "Lift"))
|
805
|
+
if verbose:
|
806
|
+
print(f"Weak positive responsiveness violation for {vm.name} by lifting {w}:")
|
807
|
+
profile.display()
|
808
|
+
print(profile.description())
|
809
|
+
profile.display_margin_graph()
|
810
|
+
print(f"{vm.name} winners: ", ws)
|
811
|
+
print("Original ranking ", old_ranking)
|
812
|
+
print(f"New ranking: {new_ranking}")
|
813
|
+
new_prof.display()
|
814
|
+
print(new_prof.description())
|
815
|
+
new_prof.display_margin_graph()
|
816
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
817
|
+
|
818
|
+
elif violation_type == "Drop":
|
819
|
+
for l in profile.candidates:
|
820
|
+
if l not in ws or (l in ws and len(ws) > 1):
|
821
|
+
for r_idx, r in enumerate(rankings):
|
822
|
+
|
823
|
+
if rcounts[r_idx] == 0:
|
824
|
+
continue
|
825
|
+
|
826
|
+
if isinstance(r, Ranking): # Make sure all candidates are ranked in r
|
827
|
+
r = Ranking({a: r.rmap[a] if a in r.cands else max(r.ranks)+1 for a in profile.candidates})
|
828
|
+
|
829
|
+
if r[0] == l and not r[-1] == l:
|
830
|
+
old_ranking = copy.deepcopy(r)
|
831
|
+
new_ranking = drop_to_last(r, l)
|
832
|
+
new_rankings = old_rankings + [new_ranking]
|
833
|
+
new_rcounts = copy.deepcopy(rcounts + [1])
|
834
|
+
new_rcounts[r_idx] -= 1
|
835
|
+
|
836
|
+
if isinstance(profile, Profile):
|
837
|
+
new_prof = Profile(new_rankings, new_rcounts)
|
838
|
+
|
839
|
+
if isinstance(profile, ProfileWithTies):
|
840
|
+
new_prof = ProfileWithTies(new_rankings, new_rcounts)
|
841
|
+
if profile.using_extended_strict_preference:
|
842
|
+
new_prof.use_extended_strict_preference()
|
843
|
+
|
844
|
+
new_ws = vm(new_prof)
|
845
|
+
|
846
|
+
if l in new_ws:
|
847
|
+
witnesses.append((l, old_ranking, "Drop"))
|
848
|
+
if verbose:
|
849
|
+
print(f"Weak positive responsiveness violation for {vm.name} by dropping {l}:")
|
850
|
+
profile.display()
|
851
|
+
print(profile.description())
|
852
|
+
profile.display_margin_graph()
|
853
|
+
print(f"{vm.name} winners: ", ws)
|
854
|
+
print("Original ranking: ", old_ranking)
|
855
|
+
print(f"New ranking: {new_ranking}")
|
856
|
+
new_prof.display()
|
857
|
+
print(new_prof.description())
|
858
|
+
new_prof.display_margin_graph()
|
859
|
+
print(f"{vm.name} winners in updated profile: ", new_ws)
|
860
|
+
|
861
|
+
return witnesses
|
862
|
+
|
863
|
+
weak_positive_responsiveness = Axiom(
|
864
|
+
"Weak Positive Responsiveness",
|
865
|
+
has_violation = has_weak_positive_responsiveness_violation,
|
866
|
+
find_all_violations = find_all_weak_positive_responsiveness_violations,
|
867
|
+
)
|
868
|
+
|
869
|
+
monotonicity_axioms = [
|
870
|
+
monotonicity,
|
871
|
+
weak_positive_responsiveness
|
872
|
+
]
|