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,481 @@
|
|
1
|
+
'''
|
2
|
+
File: scoring_rules.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: January 6, 2022
|
5
|
+
|
6
|
+
Implementations of scoring rules.
|
7
|
+
'''
|
8
|
+
from pref_voting.voting_method import *
|
9
|
+
from pref_voting.social_welfare_function import *
|
10
|
+
from pref_voting.profiles import Profile
|
11
|
+
from pref_voting.rankings import Ranking, break_ties_alphabetically
|
12
|
+
from pref_voting.voting_method import _num_rank_last
|
13
|
+
from pref_voting.profiles import _find_updated_profile, _num_rank
|
14
|
+
from pref_voting.weighted_majority_graphs import MarginGraph
|
15
|
+
from pref_voting.voting_method_properties import ElectionTypes
|
16
|
+
|
17
|
+
@vm(name = "Plurality",
|
18
|
+
input_types=[ElectionTypes.PROFILE,
|
19
|
+
ElectionTypes.TRUNCATED_LINEAR_PROFILE])
|
20
|
+
def plurality(profile, curr_cands = None):
|
21
|
+
"""The **Plurality score** of a candidate :math:`c` is the number of voters that rank :math:`c` in first place. The Plurality winners are the candidates with the largest Plurality score in the ``profile`` restricted to ``curr_cands``.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
25
|
+
curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
A sorted list of candidates
|
29
|
+
|
30
|
+
.. seealso::
|
31
|
+
|
32
|
+
The method :meth:`pref_voting.profiles.Profile.plurality_scores` returns a dictionary assigning the Plurality scores of each candidate.
|
33
|
+
|
34
|
+
:Example:
|
35
|
+
|
36
|
+
.. exec_code::
|
37
|
+
|
38
|
+
from pref_voting.profiles import Profile
|
39
|
+
from pref_voting.scoring_methods import plurality
|
40
|
+
|
41
|
+
prof1 = Profile([[0, 1, 2], [1, 0, 2], [2, 1, 0]], [3, 1, 2])
|
42
|
+
prof1.display()
|
43
|
+
print(plurality(prof1)) # [2]
|
44
|
+
plurality.display(prof1)
|
45
|
+
|
46
|
+
prof2 = Profile([[0, 1, 2], [1, 0, 2], [1, 2, 0]], [3, 1, 2])
|
47
|
+
prof2.display()
|
48
|
+
print(plurality(prof2)) # [0, 1]
|
49
|
+
plurality.display(prof2)
|
50
|
+
|
51
|
+
"""
|
52
|
+
|
53
|
+
curr_cands = profile.candidates if curr_cands is None else curr_cands
|
54
|
+
|
55
|
+
# get the Plurality scores for all the candidates in curr_cands
|
56
|
+
plurality_scores = profile.plurality_scores(curr_cands = curr_cands)
|
57
|
+
|
58
|
+
assert plurality_scores != {}, "Cannot calculate plurality scores."
|
59
|
+
|
60
|
+
max_plurality_score = max(plurality_scores.values())
|
61
|
+
|
62
|
+
return sorted([c for c in curr_cands if plurality_scores[c] == max_plurality_score])
|
63
|
+
|
64
|
+
|
65
|
+
@swf(name="Plurality ranking")
|
66
|
+
def plurality_ranking(profile, curr_cands=None, local=True, tie_breaking=None):
|
67
|
+
"""The SWF that ranks the candidates in curr_cands according to their plurality scores. If local is True, then the plurality scores are computed with respect to the profile restricted to curr_cands. Otherwise, the plurality scores are computed with respect to the entire profile.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
71
|
+
curr_cands (List[int], optional): The candidates to rank. If None, then all candidates in profile are ranked
|
72
|
+
local (bool, optional): If True, then the plurality scores are computed with respect to the profile restricted to curr_cands. Otherwise, the plurality scores are computed with respect to the entire profile.
|
73
|
+
tie_breaking (str, optional): The tie-breaking method to use. If None, then no tie-breaking is used. If "alphabetic", then the tie-breaking is done alphabetically.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
A Ranking object
|
77
|
+
"""
|
78
|
+
|
79
|
+
cands = profile.candidates if curr_cands is None else curr_cands
|
80
|
+
|
81
|
+
if local:
|
82
|
+
plurality_scores_dict = profile.plurality_scores(curr_cands = cands)
|
83
|
+
|
84
|
+
else:
|
85
|
+
plurality_scores_dict = profile.plurality_scores()
|
86
|
+
plurality_scores_dict = {k: v for k, v in plurality_scores_dict.items() if k in cands}
|
87
|
+
|
88
|
+
assert plurality_scores_dict != {}, "Cannot calculate plurality scores."
|
89
|
+
|
90
|
+
for cand in cands:
|
91
|
+
plurality_scores_dict[cand] = -plurality_scores_dict[cand]
|
92
|
+
|
93
|
+
p_ranking = Ranking(plurality_scores_dict)
|
94
|
+
p_ranking.normalize_ranks()
|
95
|
+
|
96
|
+
if tie_breaking == "alphabetic":
|
97
|
+
p_ranking = break_ties_alphabetically(p_ranking)
|
98
|
+
|
99
|
+
return p_ranking
|
100
|
+
|
101
|
+
@vm(name = "Borda",
|
102
|
+
input_types=[ElectionTypes.PROFILE, ElectionTypes.MARGIN_GRAPH])
|
103
|
+
def borda(edata, curr_cands = None, algorithm = "positional"):
|
104
|
+
"""The **Borda score** of a candidate is calculated as follows: If there are :math:`m` candidates, then the Borda score of candidate :math:`c` is :math:`\sum_{r=1}^{m} (m - r) * Rank(c,r)` where :math:`Rank(c,r)` is the number of voters that rank candidate :math:`c` in position :math:`r`. The Borda winners are the candidates with the largest Borda score in the ``profile`` restricted to ``curr_cands``.
|
105
|
+
Args:
|
106
|
+
edata (Profile, MarginGraph): An anonymous profile of linear orders or a MarginGraph.
|
107
|
+
curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
|
108
|
+
algorithm (String): if "positional", then the Borda score of a candidate is calculated from each voter's ranking as described above. If "marginal", then the Borda score of a candidate is calculated as the sum of the margins of the candidate vs. all other candidates. The positional scores and marginal scores are affinely equivalent.
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
A sorted list of candidates
|
112
|
+
|
113
|
+
.. note:
|
114
|
+
If edata is a MarginGraph, then the "marginal" algorithm is used.
|
115
|
+
|
116
|
+
.. seealso::
|
117
|
+
|
118
|
+
The method :meth:`pref_voting.profiles.Profile.borda_scores` returns a dictionary assigning the Borda score to each candidate.
|
119
|
+
|
120
|
+
:Example:
|
121
|
+
|
122
|
+
.. exec_code::
|
123
|
+
|
124
|
+
from pref_voting.profiles import Profile
|
125
|
+
from pref_voting.scoring_methods import borda
|
126
|
+
|
127
|
+
prof1 = Profile([[0, 1, 2], [1, 0, 2], [2, 1, 0]], [3, 1, 2])
|
128
|
+
prof1.display()
|
129
|
+
print(borda(prof1)) # [0,1]
|
130
|
+
borda.display(prof1)
|
131
|
+
|
132
|
+
prof2 = Profile([[0, 1, 2], [1, 0, 2], [1, 2, 0]], [3, 1, 2])
|
133
|
+
prof2.display()
|
134
|
+
print(borda(prof2)) # [1]
|
135
|
+
borda.display(prof2)
|
136
|
+
|
137
|
+
"""
|
138
|
+
|
139
|
+
curr_cands = edata.candidates if curr_cands is None else curr_cands
|
140
|
+
|
141
|
+
if isinstance(edata,MarginGraph):
|
142
|
+
algorithm = "marginal"
|
143
|
+
|
144
|
+
if algorithm == "positional":
|
145
|
+
# get the Borda scores for all the candidates in curr_cands
|
146
|
+
borda_scores = edata.borda_scores(curr_cands = curr_cands)
|
147
|
+
|
148
|
+
if algorithm == "marginal":
|
149
|
+
borda_scores = {c: sum([edata.margin(c,d) for d in curr_cands]) for c in curr_cands}
|
150
|
+
|
151
|
+
max_borda_score = max(borda_scores.values())
|
152
|
+
|
153
|
+
return sorted([c for c in curr_cands if borda_scores[c] == max_borda_score])
|
154
|
+
|
155
|
+
@swf(name="Borda ranking")
|
156
|
+
def borda_ranking(profile, curr_cands=None, local=True, tie_breaking=None):
|
157
|
+
"""The SWF that ranks the candidates in curr_cands according to their Borda scores. If local is True, then the Borda scores are computed with respect to the profile restricted to curr_cands. Otherwise, the Borda scores are computed with respect to the entire profile.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
161
|
+
curr_cands (List[int], optional): The candidates to rank. If None, then all candidates in profile are ranked
|
162
|
+
local (bool, optional): If True, then the Borda scores are computed with respect to the profile restricted to curr_cands. Otherwise, the Borda scores are computed with respect to the entire profile.
|
163
|
+
tie_breaking (str, optional): The tie-breaking method to use. If None, then no tie-breaking is used. If "alphabetic", then the tie-breaking is done alphabetically.
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
A Ranking object
|
167
|
+
"""
|
168
|
+
|
169
|
+
cands = profile.candidates if curr_cands is None else curr_cands
|
170
|
+
|
171
|
+
if local:
|
172
|
+
borda_scores_dict = profile.borda_scores(curr_cands = cands)
|
173
|
+
|
174
|
+
else:
|
175
|
+
borda_scores_dict = profile.borda_scores()
|
176
|
+
borda_scores_dict = {k: v for k, v in borda_scores_dict.items() if k in cands}
|
177
|
+
|
178
|
+
for cand in cands:
|
179
|
+
borda_scores_dict[cand] = -borda_scores_dict[cand]
|
180
|
+
|
181
|
+
b_ranking = Ranking(borda_scores_dict)
|
182
|
+
b_ranking.normalize_ranks()
|
183
|
+
|
184
|
+
if tie_breaking == "alphabetic":
|
185
|
+
b_ranking = break_ties_alphabetically(b_ranking)
|
186
|
+
|
187
|
+
return b_ranking
|
188
|
+
|
189
|
+
@vm(name = "Anti-Plurality",
|
190
|
+
input_types=[ElectionTypes.PROFILE])
|
191
|
+
def anti_plurality(profile, curr_cands = None):
|
192
|
+
"""The **Anti-Plurality score** of a candidate $c$ is the number of voters that rank $c$ in last place. The Anti-Plurality winners are the candidates with the smallest Anti-Plurality score in the ``profile`` restricted to ``curr_cands``.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
196
|
+
curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
A sorted list of candidates
|
200
|
+
|
201
|
+
:Example:
|
202
|
+
|
203
|
+
.. exec_code::
|
204
|
+
|
205
|
+
from pref_voting.profiles import Profile
|
206
|
+
from pref_voting.scoring_methods import anti_plurality
|
207
|
+
|
208
|
+
prof1 = Profile([[2, 1, 0], [2, 0, 1], [0, 1, 2]], [3, 1, 2])
|
209
|
+
prof1.display()
|
210
|
+
print(anti_plurality(prof1)) # [1]
|
211
|
+
anti_plurality.display(prof1)
|
212
|
+
|
213
|
+
prof2 = Profile([[2, 1, 0], [2, 0, 1], [0, 2, 1]], [3, 1, 2])
|
214
|
+
prof2.display()
|
215
|
+
print(anti_plurality(prof2)) # [2]
|
216
|
+
anti_plurality.display(prof2)
|
217
|
+
|
218
|
+
"""
|
219
|
+
|
220
|
+
# get ranking data
|
221
|
+
rankings, rcounts = profile.rankings_counts
|
222
|
+
|
223
|
+
curr_cands = profile.candidates if curr_cands is None else curr_cands
|
224
|
+
cands_to_ignore = np.array([c for c in profile.candidates if c not in curr_cands])
|
225
|
+
|
226
|
+
last_place_scores = {c: _num_rank_last(rankings, rcounts, cands_to_ignore, c) for c in curr_cands}
|
227
|
+
min_last_place_score = min(list(last_place_scores.values()))
|
228
|
+
|
229
|
+
return sorted([c for c in curr_cands if last_place_scores[c] == min_last_place_score])
|
230
|
+
|
231
|
+
@swf(name="Anti-Plurality ranking")
|
232
|
+
def anti_plurality_ranking(profile, curr_cands=None, local=True, tie_breaking=None):
|
233
|
+
"""The SWF that ranks the candidates in curr_cands according to their Anti-Plurality scores. If local is True, then the Anti-Plurality scores are computed with respect to the profile restricted to curr_cands. Otherwise, the Anti-Plurality scores are computed with respect to the entire profile.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
237
|
+
curr_cands (List[int], optional): The candidates to rank. If None, then all candidates in profile are ranked
|
238
|
+
local (bool, optional): If True, then the Anti-Plurality scores are computed with respect to the profile restricted to curr_cands. Otherwise, the Anti-Plurality scores are computed with respect to the entire profile.
|
239
|
+
tie_breaking (str, optional): The tie-breaking method to use. If None, then no tie-breaking is used. If "alphabetic", then the tie-breaking is done alphabetically.
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
A Ranking object
|
243
|
+
"""
|
244
|
+
|
245
|
+
cands = profile.candidates if curr_cands is None else curr_cands
|
246
|
+
|
247
|
+
rankings, rcounts = profile.rankings_counts
|
248
|
+
|
249
|
+
if local:
|
250
|
+
cands_to_ignore = np.array([c for c in profile.candidates if c not in cands])
|
251
|
+
|
252
|
+
else:
|
253
|
+
cands_to_ignore = np.array([])
|
254
|
+
|
255
|
+
anti_plurality_scores_dict = {c: _num_rank_last(rankings, rcounts, cands_to_ignore, c) for c in cands}
|
256
|
+
|
257
|
+
ap_ranking = Ranking(anti_plurality_scores_dict)
|
258
|
+
ap_ranking.normalize_ranks()
|
259
|
+
|
260
|
+
if tie_breaking == "alphabetic":
|
261
|
+
ap_ranking = break_ties_alphabetically(ap_ranking)
|
262
|
+
|
263
|
+
return ap_ranking
|
264
|
+
|
265
|
+
|
266
|
+
@vm(name = "Scoring Rule",
|
267
|
+
skip_registration=True,)
|
268
|
+
def scoring_rule(profile, curr_cands = None, score = lambda num_cands, rank : 1 if rank == 1 else 0):
|
269
|
+
"""A general scoring rule. Each voter assign a score to each candidate using the ``score`` function based on their submitted ranking (restricted to candidates in ``curr_cands``). Returns that candidates with the greatest overall score in the profile restricted to ``curr_cands``.
|
270
|
+
|
271
|
+
Args:
|
272
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
273
|
+
curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
|
274
|
+
score (function): A function that accepts two parameters ``num_cands`` (the number of candidates) and ``rank`` (a rank of a candidate) used to calculate the score of a candidate. The default ``score`` function assigns 1 to a candidate ranked in first place, otherwise it assigns 0 to the candidate.
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
A sorted list of candidates
|
278
|
+
|
279
|
+
.. important::
|
280
|
+
The signature of the ``score`` function is::
|
281
|
+
|
282
|
+
def score(num_cands, rank):
|
283
|
+
# return an int or float
|
284
|
+
|
285
|
+
:Example:
|
286
|
+
|
287
|
+
.. exec_code::
|
288
|
+
|
289
|
+
from pref_voting.profiles import Profile
|
290
|
+
from pref_voting.scoring_methods import scoring_rule, plurality, borda, anti_plurality
|
291
|
+
|
292
|
+
prof = Profile([[0, 1, 2], [1, 0, 2], [2, 1, 0]], [3, 1, 2])
|
293
|
+
prof.display()
|
294
|
+
scoring_rule.display(prof) # Uses default scoring function, same a Plurality
|
295
|
+
plurality.display(prof)
|
296
|
+
|
297
|
+
scoring_rule.display(prof, score=lambda num_cands, rank: num_cands - rank) # same a Borda
|
298
|
+
borda.display(prof)
|
299
|
+
|
300
|
+
scoring_rule.display(prof, score=lambda num_cands, rank: -1 if rank == num_cands else 0) # same as Anti-Plurality
|
301
|
+
anti_plurality.display(prof)
|
302
|
+
|
303
|
+
"""
|
304
|
+
|
305
|
+
# get ranking data
|
306
|
+
_rankings, rcounts = profile.rankings_counts
|
307
|
+
|
308
|
+
# get (restricted) rankings
|
309
|
+
cands_to_ignore = np.array([c for c in profile.candidates if c not in curr_cands]) if curr_cands is not None else np.array([])
|
310
|
+
rankings = _rankings if curr_cands is None else _find_updated_profile(np.array(_rankings), cands_to_ignore, len(profile.candidates))
|
311
|
+
candidates = profile.candidates if curr_cands is None else curr_cands
|
312
|
+
|
313
|
+
# find the candidate scores using the score function
|
314
|
+
cand_scores = {c: sum(_num_rank(rankings, rcounts, c, level) * score(len(candidates), level) for level in range(1, len(candidates) + 1)) for c in candidates}
|
315
|
+
|
316
|
+
# find maximum score
|
317
|
+
max_score = max(cand_scores.values())
|
318
|
+
|
319
|
+
return sorted([c for c in candidates if cand_scores[c] == max_score])
|
320
|
+
|
321
|
+
def create_scoring_method(score, name):
|
322
|
+
"""Create a scoring method using a given score function and name."""
|
323
|
+
|
324
|
+
def _vm(profile, curr_cands = None):
|
325
|
+
return scoring_rule(profile, curr_cands = curr_cands, score = score)
|
326
|
+
|
327
|
+
return VotingMethod(_vm, name = name)
|
328
|
+
|
329
|
+
@vm(name = "Dowdall",
|
330
|
+
input_types=[ElectionTypes.PROFILE])
|
331
|
+
def dowdall(profile, curr_cands = None):
|
332
|
+
"""The first-ranked candidate gets 1 point, the second-ranked candidate gets 1/2 point, the third-ranked candidate gets 1/3 point, and so on. The Dowdall winners are the candidates with the greatest overall score in the profile restricted to ``curr_cands``.
|
333
|
+
|
334
|
+
Args:
|
335
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
336
|
+
curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``.
|
337
|
+
|
338
|
+
Returns:
|
339
|
+
A sorted list of candidates
|
340
|
+
|
341
|
+
.. note::
|
342
|
+
This system is used in Nauru. See, e.g., Jon Fraenkel & Bernard Grofman (2014), "The Borda Count and its real-world alternatives: Comparing scoring rules in Nauru and Slovenia," Australian Journal of Political Science, 49:2, 186-205, DOI: 10.1080/10361146.2014.900530.
|
343
|
+
|
344
|
+
"""
|
345
|
+
|
346
|
+
return scoring_rule(profile, curr_cands = curr_cands, score = lambda num_cands, rank: 1 / rank)
|
347
|
+
|
348
|
+
@vm(name="Positive-Negative Voting",
|
349
|
+
input_types=[ElectionTypes.PROFILE])
|
350
|
+
def positive_negative_voting(profile, curr_cands = None):
|
351
|
+
"""The **Positive-Negative Voting** method is a scoring rule where each voter assigns a score of 1 to their top-ranked candidate and a score of -1 to their bottom-ranked candidate. See https://onlinelibrary.wiley.com/doi/10.1111/ecin.12929 for more information.
|
352
|
+
|
353
|
+
Args:
|
354
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
355
|
+
curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
|
356
|
+
|
357
|
+
Returns:
|
358
|
+
A sorted list of candidates
|
359
|
+
|
360
|
+
"""
|
361
|
+
|
362
|
+
return scoring_rule(profile,
|
363
|
+
curr_cands = curr_cands,
|
364
|
+
score = lambda num_cands, rank : 1 if rank == 1 else (-1 if rank == num_cands else 0))
|
365
|
+
|
366
|
+
|
367
|
+
@swf(name = "Score Ranking")
|
368
|
+
def score_ranking(profile, curr_cands = None, score = lambda num_cands, rank : 1 if rank == 1 else 0):
|
369
|
+
"""A general swf that ranks the candidates according to the given score function. Each voter assign a score to each candidate using the ``score`` function based on their submitted ranking (restricted to candidates in ``curr_cands``). Returns the ranking of the candidates according to the scores.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
profile (Profile): An anonymous profile of linear orders on a set of candidates
|
373
|
+
curr_cands (List[int], optional): If set, then find the winners for the profile restricted to the candidates in ``curr_cands``
|
374
|
+
score (function): A function that accepts two parameters ``num_cands`` (the number of candidates) and ``rank`` (a rank of a candidate) used to calculate the score of a candidate. The default ``score`` function assigns 1 to a candidate ranked in first place, otherwise it assigns 0 to the candidate.
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
A ranking of the candidates
|
378
|
+
|
379
|
+
.. important::
|
380
|
+
The signature of the ``score`` function is::
|
381
|
+
|
382
|
+
def score(num_cands, rank):
|
383
|
+
# return an int or float
|
384
|
+
"""
|
385
|
+
|
386
|
+
# get ranking data
|
387
|
+
_rankings, rcounts = profile.rankings_counts
|
388
|
+
|
389
|
+
# get (restricted) rankings
|
390
|
+
cands_to_ignore = np.array([c for c in profile.candidates if c not in curr_cands]) if curr_cands is not None else np.array([])
|
391
|
+
rankings = _rankings if curr_cands is None else _find_updated_profile(np.array(_rankings), cands_to_ignore, len(profile.candidates))
|
392
|
+
candidates = profile.candidates if curr_cands is None else curr_cands
|
393
|
+
|
394
|
+
# find the candidate scores using the score function
|
395
|
+
cand_scores = {c: -1 * sum(_num_rank(rankings, rcounts, c, level) * score(len(candidates), level) for level in range(1, len(candidates) + 1)) for c in candidates}
|
396
|
+
|
397
|
+
ranking = Ranking(cand_scores)
|
398
|
+
ranking.normalize_ranks()
|
399
|
+
return ranking
|
400
|
+
|
401
|
+
## Borda for ProfilesWithTies
|
402
|
+
|
403
|
+
def symmetric_borda_scores(profile):
|
404
|
+
"""
|
405
|
+
The symmetric Borda score of a candidate c for a ranking r is the number of candidates ranked strictly below c according to r
|
406
|
+
minus the number of candidates ranked strictly above c according to r.
|
407
|
+
|
408
|
+
See http://www.illc.uva.nl/~ulle/pubs/files/TerzopoulouEndrissJME2021.pdf for a discussion.
|
409
|
+
"""
|
410
|
+
|
411
|
+
return {cand: sum([len([_cand for _cand in profile.candidates if r.extended_strict_pref(cand, _cand)]) * c
|
412
|
+
for r,c in zip(*profile.rankings_counts)]) - sum([len([_cand for _cand in profile.candidates if r.extended_strict_pref(_cand, cand)]) * c
|
413
|
+
for r,c in zip(*profile.rankings_counts)]) for cand in profile.candidates}
|
414
|
+
|
415
|
+
def domination_borda_scores(profile):
|
416
|
+
"""
|
417
|
+
The domination Borda score of a candidate c for a ranking r is the number of candidates ranked strictly below c according to r.
|
418
|
+
|
419
|
+
See http://www.illc.uva.nl/~ulle/pubs/files/TerzopoulouEndrissJME2021.pdf for a discussion.
|
420
|
+
|
421
|
+
"""
|
422
|
+
|
423
|
+
return {cand: sum([len([_cand for _cand in profile.candidates if r.extended_strict_pref(cand, _cand)]) * c
|
424
|
+
for r,c in zip(*profile.rankings_counts)]) for cand in profile.candidates}
|
425
|
+
|
426
|
+
|
427
|
+
def weak_domination_borda_scores(profile):
|
428
|
+
"""
|
429
|
+
The weak domination Borda score of a candidate c for a ranking r is the number of candidates ranked weakly below c according to r.
|
430
|
+
|
431
|
+
See http://www.illc.uva.nl/~ulle/pubs/files/TerzopoulouEndrissJME2021.pdf for a discussion.
|
432
|
+
|
433
|
+
"""
|
434
|
+
|
435
|
+
return {cand: sum([len([_cand for _cand in profile.candidates if r.extended_weak_pref(cand, _cand)]) * c
|
436
|
+
for r,c in zip(*profile.rankings_counts)]) for cand in profile.candidates}
|
437
|
+
|
438
|
+
def non_domination_borda_scores(profile):
|
439
|
+
"""
|
440
|
+
The non-domination Borda score of a candidate c for a ranking r is -1 times the number of candidates ranked strictly above c according to r.
|
441
|
+
|
442
|
+
See http://www.illc.uva.nl/~ulle/pubs/files/TerzopoulouEndrissJME2021.pdf for a discussion.
|
443
|
+
|
444
|
+
"""
|
445
|
+
|
446
|
+
return {cand: -sum([len([_cand for _cand in profile.candidates if r.extended_strict_pref(_cand, cand)]) * c
|
447
|
+
for r,c in zip(*profile.rankings_counts)]) for cand in profile.candidates}
|
448
|
+
|
449
|
+
|
450
|
+
@vm(name="Borda (for Truncated Profiles)",
|
451
|
+
input_types=[ElectionTypes.TRUNCATED_LINEAR_PROFILE])
|
452
|
+
def borda_for_profile_with_ties(
|
453
|
+
profile,
|
454
|
+
curr_cands=None,
|
455
|
+
borda_scores=symmetric_borda_scores):
|
456
|
+
"""
|
457
|
+
Borda score for truncated linear orders using different ways of defining the Borda score for truncated linear
|
458
|
+
orders.
|
459
|
+
"""
|
460
|
+
# profile must be a ProfileWithTies object
|
461
|
+
if isinstance(profile, Profile):
|
462
|
+
return borda(profile, curr_cands = curr_cands)
|
463
|
+
|
464
|
+
curr_cands = curr_cands if curr_cands is not None else profile.candidates
|
465
|
+
|
466
|
+
restricted_prof = profile.remove_candidates([c for c in profile.candidates if c not in curr_cands])
|
467
|
+
|
468
|
+
b_scores = borda_scores(restricted_prof)
|
469
|
+
|
470
|
+
max_borda_score = max(b_scores.values())
|
471
|
+
|
472
|
+
return sorted([c for c in restricted_prof.candidates if b_scores[c] == max_borda_score])
|
473
|
+
|
474
|
+
|
475
|
+
scoring_swfs = [
|
476
|
+
plurality_ranking,
|
477
|
+
borda_ranking,
|
478
|
+
anti_plurality_ranking,
|
479
|
+
score_ranking
|
480
|
+
]
|
481
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
'''
|
2
|
+
File: social_welfare_function.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: February 6, 2024
|
5
|
+
|
6
|
+
The SWF class and helper functions for social welfare functions
|
7
|
+
'''
|
8
|
+
|
9
|
+
import functools
|
10
|
+
|
11
|
+
class SocialWelfareFunction(object):
|
12
|
+
"""
|
13
|
+
A class to add functionality to social welfare functions
|
14
|
+
|
15
|
+
Args:
|
16
|
+
swf (function): An implementation of a voting method. The function should accept any type of profile, and a keyword parameter ``curr_cands`` to find the winner after restricting to ``curr_cands``.
|
17
|
+
name (string): The Human-readable name of the social welfare function.
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
A ranking (Ranking) of the candidates.
|
21
|
+
"""
|
22
|
+
def __init__(self, swf, name = None):
|
23
|
+
|
24
|
+
self.swf = swf
|
25
|
+
self.name = name
|
26
|
+
functools.update_wrapper(self, swf)
|
27
|
+
|
28
|
+
def __call__(self, edata, curr_cands = None, **kwargs):
|
29
|
+
|
30
|
+
if (curr_cands is not None and len(curr_cands) == 0) or len(edata.candidates) == 0:
|
31
|
+
return []
|
32
|
+
return self.swf(edata, curr_cands = curr_cands, **kwargs)
|
33
|
+
|
34
|
+
def winners(self, edata, curr_cands = None, **kwargs):
|
35
|
+
"""Return a sorted list of the first place candidates."""
|
36
|
+
|
37
|
+
return sorted(self.swf(edata, curr_cands = curr_cands, **kwargs).first())
|
38
|
+
|
39
|
+
def display(self, edata, curr_cands = None, **kwargs):
|
40
|
+
"""Display the result of the social welfare function."""
|
41
|
+
|
42
|
+
ranking = self.swf(edata, curr_cands = curr_cands, **kwargs)
|
43
|
+
print(f"{self.name} ranking is {ranking}")
|
44
|
+
|
45
|
+
def set_name(self, new_name):
|
46
|
+
"""Set the name of the social welfare function."""
|
47
|
+
|
48
|
+
self.name = new_name
|
49
|
+
|
50
|
+
def __str__(self):
|
51
|
+
return f"{self.name}"
|
52
|
+
|
53
|
+
def swf(name = None):
|
54
|
+
"""
|
55
|
+
A decorator used when creating a social welfare function.
|
56
|
+
"""
|
57
|
+
def wrapper(f):
|
58
|
+
return SocialWelfareFunction(f, name=name)
|
59
|
+
return wrapper
|
@@ -0,0 +1,7 @@
|
|
1
|
+
from pref_voting.utility_methods import sum_utilitarian_ranking, relative_utilitarian_ranking, maximin_ranking, lexicographic_maximin_ranking, nash_ranking, utilitarian_swfs
|
2
|
+
|
3
|
+
from pref_voting.scoring_methods import plurality_ranking, borda_ranking, scoring_swfs
|
4
|
+
|
5
|
+
# List of all social welfare functions
|
6
|
+
social_welfare_functions = utilitarian_swfs + scoring_swfs
|
7
|
+
|