pref_voting 1.16.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. pref_voting/__init__.py +1 -0
  2. pref_voting/analysis.py +496 -0
  3. pref_voting/axiom.py +38 -0
  4. pref_voting/axiom_helpers.py +129 -0
  5. pref_voting/axioms.py +10 -0
  6. pref_voting/c1_methods.py +963 -0
  7. pref_voting/combined_methods.py +514 -0
  8. pref_voting/create_methods.py +128 -0
  9. pref_voting/data/examples/condorcet_winner/minimal_Anti-Plurality.soc +16 -0
  10. pref_voting/data/examples/condorcet_winner/minimal_Borda.soc +17 -0
  11. pref_voting/data/examples/condorcet_winner/minimal_Bracket_Voting.soc +20 -0
  12. pref_voting/data/examples/condorcet_winner/minimal_Bucklin.soc +19 -0
  13. pref_voting/data/examples/condorcet_winner/minimal_Coombs.soc +20 -0
  14. pref_voting/data/examples/condorcet_winner/minimal_Coombs_PUT.soc +20 -0
  15. pref_voting/data/examples/condorcet_winner/minimal_Coombs_TB.soc +20 -0
  16. pref_voting/data/examples/condorcet_winner/minimal_Dowdall.soc +19 -0
  17. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff.soc +18 -0
  18. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_PUT.soc +18 -0
  19. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_TB.soc +18 -0
  20. pref_voting/data/examples/condorcet_winner/minimal_Iterated_Removal_Condorcet_Loser.soc +17 -0
  21. pref_voting/data/examples/condorcet_winner/minimal_Pareto.soc +17 -0
  22. pref_voting/data/examples/condorcet_winner/minimal_Plurality.soc +18 -0
  23. pref_voting/data/examples/condorcet_winner/minimal_PluralityWRunoff_PUT.soc +18 -0
  24. pref_voting/data/examples/condorcet_winner/minimal_Positive-Negative_Voting.soc +17 -0
  25. pref_voting/data/examples/condorcet_winner/minimal_Simplified_Bucklin.soc +18 -0
  26. pref_voting/data/examples/condorcet_winner/minimal_Superior_Voting.soc +19 -0
  27. pref_voting/data/examples/condorcet_winner/minimal_Weighted_Bucklin.soc +19 -0
  28. pref_voting/data/examples/condorcet_winner/minimal_resolute_Anti-Plurality.soc +17 -0
  29. pref_voting/data/examples/condorcet_winner/minimal_resolute_Borda.soc +17 -0
  30. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bracket_Voting.soc +20 -0
  31. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bucklin.soc +19 -0
  32. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs.soc +21 -0
  33. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_PUT.soc +21 -0
  34. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_TB.soc +20 -0
  35. pref_voting/data/examples/condorcet_winner/minimal_resolute_Dowdall.soc +18 -0
  36. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff.soc +18 -0
  37. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_PUT.soc +18 -0
  38. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_TB.soc +18 -0
  39. pref_voting/data/examples/condorcet_winner/minimal_resolute_Plurality.soc +18 -0
  40. pref_voting/data/examples/condorcet_winner/minimal_resolute_PluralityWRunoff_PUT.soc +18 -0
  41. pref_voting/data/examples/condorcet_winner/minimal_resolute_Positive-Negative_Voting.soc +17 -0
  42. pref_voting/data/examples/condorcet_winner/minimal_resolute_Simplified_Bucklin.soc +20 -0
  43. pref_voting/data/examples/condorcet_winner/minimal_resolute_Weighted_Bucklin.soc +19 -0
  44. pref_voting/data/voting_methods_properties.json +414 -0
  45. pref_voting/data/voting_methods_properties.json.lock +0 -0
  46. pref_voting/dominance_axioms.py +387 -0
  47. pref_voting/generate_profiles.py +801 -0
  48. pref_voting/generate_spatial_profiles.py +198 -0
  49. pref_voting/generate_utility_profiles.py +160 -0
  50. pref_voting/generate_weighted_majority_graphs.py +506 -0
  51. pref_voting/grade_methods.py +184 -0
  52. pref_voting/grade_profiles.py +357 -0
  53. pref_voting/helper.py +370 -0
  54. pref_voting/invariance_axioms.py +671 -0
  55. pref_voting/io/__init__.py +0 -0
  56. pref_voting/io/readers.py +432 -0
  57. pref_voting/io/writers.py +256 -0
  58. pref_voting/iterative_methods.py +2425 -0
  59. pref_voting/maj_graph_ex1.png +0 -0
  60. pref_voting/mappings.py +577 -0
  61. pref_voting/margin_based_methods.py +2345 -0
  62. pref_voting/monotonicity_axioms.py +872 -0
  63. pref_voting/num_evaluation_method.py +77 -0
  64. pref_voting/other_axioms.py +161 -0
  65. pref_voting/other_methods.py +939 -0
  66. pref_voting/pairwise_profiles.py +547 -0
  67. pref_voting/prob_voting_method.py +105 -0
  68. pref_voting/probabilistic_methods.py +287 -0
  69. pref_voting/profiles.py +856 -0
  70. pref_voting/profiles_with_ties.py +1069 -0
  71. pref_voting/rankings.py +466 -0
  72. pref_voting/scoring_methods.py +481 -0
  73. pref_voting/social_welfare_function.py +59 -0
  74. pref_voting/social_welfare_functions.py +7 -0
  75. pref_voting/spatial_profiles.py +448 -0
  76. pref_voting/stochastic_methods.py +99 -0
  77. pref_voting/strategic_axioms.py +1394 -0
  78. pref_voting/swf_axioms.py +173 -0
  79. pref_voting/utility_functions.py +102 -0
  80. pref_voting/utility_methods.py +178 -0
  81. pref_voting/utility_profiles.py +333 -0
  82. pref_voting/variable_candidate_axioms.py +640 -0
  83. pref_voting/variable_voter_axioms.py +3747 -0
  84. pref_voting/voting_method.py +355 -0
  85. pref_voting/voting_method_properties.py +92 -0
  86. pref_voting/voting_methods.py +8 -0
  87. pref_voting/voting_methods_registry.py +136 -0
  88. pref_voting/weighted_majority_graphs.py +1539 -0
  89. pref_voting-1.16.31.dist-info/METADATA +208 -0
  90. pref_voting-1.16.31.dist-info/RECORD +92 -0
  91. pref_voting-1.16.31.dist-info/WHEEL +4 -0
  92. pref_voting-1.16.31.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,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
+