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,856 @@
1
+ '''
2
+ File: profiles.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: December 7, 2020
5
+ Updated: January 5, 2022
6
+ Updated: July 9, 2022
7
+
8
+ Functions to reason about profiles of linear orders.
9
+ '''
10
+
11
+
12
+ from math import ceil
13
+ import numpy as np
14
+ from numba import jit
15
+ import networkx as nx
16
+ from tabulate import tabulate
17
+ import matplotlib.pyplot as plt
18
+ from pref_voting.weighted_majority_graphs import MajorityGraph, MarginGraph, SupportGraph
19
+ from pref_voting.voting_method import _num_rank_first
20
+ import os
21
+
22
+ # turn off future warnings.
23
+ # getting the following warning when calling tabulate to display a profile:
24
+ # /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/tabulate.py:1027: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
25
+ # if headers == "keys" and not rows:
26
+ # see https://stackoverflow.com/questions/40659212/futurewarning-elementwise-comparison-failed-returning-scalar-but-in-the-futur
27
+ #
28
+ import warnings
29
+ warnings.simplefilter(action='ignore', category=FutureWarning)
30
+
31
+ # #######
32
+ # Internal compiled functions to optimize reasoning with profiles
33
+ # #######
34
+
35
+ @jit(nopython=True, fastmath=True)
36
+ def isin(arr, val):
37
+ """optimized function testing if the value val is in the array arr
38
+ """
39
+ for i in range(arr.shape[0]):
40
+ if (arr[i]==val):
41
+ return True
42
+ return False
43
+
44
+ @jit(nopython=True, fastmath=True)
45
+ def _support(ranks, rcounts, c1, c2):
46
+ """The number of voters that rank candidate c1 over candidate c2
47
+
48
+ Parameters
49
+ ----------
50
+ rankings: 2d numpy array
51
+ list of linear orderings of the candidates
52
+ rcounts: 1d numpy array
53
+ list of number of voters for each ranking
54
+ c1: int
55
+ a candidate
56
+ c2: int
57
+ a candidate.
58
+
59
+ Key assumptions:
60
+ * the candidates are named 0...num_cands - 1, and c1 and c2 are
61
+ numbers between 0 and num_cands - 1
62
+ * voters submit linear orders over the candidate
63
+ """
64
+
65
+ diffs = ranks[0:,c1] - ranks[0:,c2] # for each voter, the difference of the ranks of c1 and c2
66
+ diffs[diffs > 0] = 0 # c1 is ranked below c2
67
+ diffs[diffs < 0] = 1 # c1 is ranked above c2
68
+ num_rank_c1_over_c2 = np.multiply(diffs, rcounts) # mutliply by the number of each ranking
69
+ return np.sum(num_rank_c1_over_c2)
70
+
71
+ @jit(nopython=True, fastmath=True)
72
+ def _margin(tally, c1, c2):
73
+ """The margin of c1 over c2: the number of voters that rank c1 over c2 minus
74
+ the number of voters that rank c2 over c1
75
+
76
+ Parameters
77
+ ----------
78
+ tally: 2d numpy array
79
+ the support for each pair of candidates
80
+ """
81
+ return tally[c1][c2] - tally[c2][c1]
82
+
83
+
84
+ @jit(nopython=True, fastmath=True)
85
+ def _num_rank(rankings, rcounts, cand, level):
86
+ """The number of voters that rank cand at level
87
+
88
+ Parameters
89
+ ----------
90
+ rankings: 2d numpy array
91
+ list of linear orderings of the candidates
92
+ rcounts: 1d numpy array
93
+ list of number of voters for each ranking
94
+ """
95
+ cands_at_level = rankings[0:,level-1] # get all the candidates at level
96
+ is_cand = cands_at_level == cand # set to 0 each candidate not equal to cand
97
+ return np.sum(is_cand * rcounts)
98
+
99
+ @jit(nopython=True, fastmath=True)
100
+ def _borda_score(rankings, rcounts, num_cands, cand):
101
+ """The Borda score for cand
102
+
103
+ Parameters
104
+ ----------
105
+ rankings: 2d numpy array
106
+ list of linear orderings of the candidates
107
+ rcounts: 1d numpy array
108
+ list of number of voters for each ranking
109
+ """
110
+
111
+ bscores = np.arange(num_cands)[::-1]
112
+ levels = np.arange(1,num_cands+1)
113
+ num_ranks = np.array([_num_rank(rankings, rcounts, cand, level) for level in levels])
114
+ return np.sum(num_ranks * bscores)
115
+
116
+ @jit(nopython=True, fastmath=True)
117
+ def _find_updated_profile(rankings, cands_to_ignore, num_cands):
118
+ """Optimized method to remove all candidates from cands_to_ignore
119
+ from a list of rankings.
120
+
121
+ Parameters
122
+ ----------
123
+ rankings: 2d numpy array
124
+ list of linear orderings of the candidates
125
+ cands_to_ignore: 1d numpy array
126
+ list of candidates to ignore
127
+ num_cands: int
128
+ the number of candidates in the original profile
129
+ """
130
+ updated_cand_num = num_cands - cands_to_ignore.shape[0]
131
+ updated_prof_ranks = np.empty(shape=(rankings.shape[0],updated_cand_num), dtype=np.int32)
132
+
133
+ for vidx in range(rankings.shape[0]):
134
+ levels_idx = np.empty(num_cands - cands_to_ignore.shape[0], dtype=np.int32)
135
+ _r = rankings[vidx]
136
+ _r_level = 0
137
+ for lidx in range(0, levels_idx.shape[0]):
138
+ for _r_idx in range(_r_level, len(_r)):
139
+ if not isin(cands_to_ignore, _r[_r_idx]):
140
+ levels_idx[lidx]=_r_idx
141
+ _r_level = _r_idx + 1
142
+ break
143
+ updated_prof_ranks[vidx] = np.array([_r[l] for l in levels_idx])
144
+ return updated_prof_ranks
145
+
146
+ # #######
147
+ # Profiles
148
+ # #######
149
+
150
+ class Profile(object):
151
+ r"""An anonymous profile of linear rankings of :math:`n` candidates. It is assumed that the candidates are named :math:`0, 1, \ldots, n-1` and a ranking of the candidates is a list of candidate names. For instance, the list ``[0, 2, 1]`` represents the ranking in which :math:`0` is ranked above :math:`2`, :math:`2` is ranked above :math:`1`, and :math:`0` is ranked above :math:`1`.
152
+
153
+ :param rankings: List of rankings in the profile, where a ranking is a list of candidates.
154
+ :type rankings: list[list[int]]
155
+ :param rcounts: List of the number of voters associated with each ranking. Should be the same length as rankings. If not provided, it is assumed that 1 voters submitted each element of ``rankings``.
156
+ :type rcounts: list[int], optional
157
+ :param cmap: Dictionary mapping candidates (integers) to candidate names (strings). If not provided, each candidate name is mapped to itself.
158
+ :type cmap: dict[int: str], optional
159
+
160
+ :Example:
161
+
162
+ The following code creates a profile in which
163
+ 2 voters submitted the ranking ``[0, 1, 2]``, 3 voters submitted the ranking ``[1, 2, 0]``, and 1 voter submitted the ranking ``[2, 0, 1]``:
164
+
165
+ .. code-block:: python
166
+
167
+ prof = Profile([[0, 1, 2], [1, 2, 0], [2, 0, 1]], [2, 3, 1])
168
+
169
+ .. warning:: In profiles with :math:`n` candidates, the candidates must be named using the integers :math:`0, 1, 2, \ldots, n`. So, the following will produce an error: ``Profile([[0, 1, 3]])``.
170
+ """
171
+
172
+ def __init__(self, rankings, rcounts=None, cmap=None):
173
+ """constructor method"""
174
+
175
+ self.num_cands = len(rankings[0]) if len(rankings) > 0 else 0
176
+ """The number of candidates"""
177
+
178
+ self.candidates = list(range(0, self.num_cands))
179
+
180
+ # needed for uniformity with ProfileWithTies and MarginGraph
181
+ self.cindices = self.candidates
182
+ self.cand_to_cindex = lambda c: c
183
+ self.cindex_to_cand = lambda i: i
184
+
185
+ # linear ordering of the candidates for each voter
186
+ self._rankings = np.array(rankings)
187
+
188
+ assert all([all([c in self.candidates for c in r]) for r in rankings]), f"The candidates must be from the set {self.candidates}."
189
+
190
+ # for number of each ranking
191
+ self._rcounts = np.array([1]*len(rankings)) if rcounts is None else np.array(rcounts)
192
+
193
+ # for each voter, the ranks of each candidate
194
+ self._ranks = np.array([[np.where(_r == c)[0][0] + 1
195
+ for c in self.candidates]
196
+ for _r in self._rankings])
197
+
198
+ # 2d array where the c,d entry is the support of c over d
199
+ self._tally = np.array([[_support(self._ranks, self._rcounts, c1, c2)
200
+ for c2 in self.candidates]
201
+ for c1 in self.candidates ])
202
+
203
+ # mapping candidates to candidate names
204
+ self.cmap = cmap if cmap is not None else {c:str(c) for c in self.candidates}
205
+
206
+ # total number of voters
207
+ self.num_voters = np.sum(self._rcounts)
208
+ """The number of voters in the election."""
209
+
210
+ self.is_truncated_linear = True
211
+ """The profile is a (truncated) linear order profile. This is needed for compatability with the ProfileWithTies class. """
212
+
213
+ @property
214
+ def rankings_counts(self):
215
+ """
216
+ Returns the submitted rankings and the list of counts.
217
+ """
218
+
219
+ return self._rankings, self._rcounts
220
+
221
+ @property
222
+ def ranking_types(self):
223
+ """
224
+ Returns a list of all the type of rankings in the profile as a list of tuples.
225
+ """
226
+ return list(set([tuple(r) for r in self._rankings]))
227
+
228
+ @property
229
+ def rankings(self):
230
+ """
231
+ Return a list of all individual rankings in the profile. The type is a list of tuples of integers.
232
+ """
233
+
234
+ return [tuple(r) for ridx,r in enumerate(self._rankings) for n in range(self._rcounts[ridx])]
235
+
236
+ @property
237
+ def rankings_as_indifference_list(self):
238
+ """
239
+ Return a list of all individual rankings as indifference lists in the profile. An indifference list of a ranking is a tuple of tuples. Since the rankings are linear orders, an indifference list is a tuple of tuples consisting of a single candidate. The return type is a list of indifference lists.
240
+ """
241
+
242
+ return [tuple([(c,) for c in r]) for ridx,r in enumerate(self._rankings) for n in range(self._rcounts[ridx])]
243
+
244
+ @property
245
+ def counts(self):
246
+ """
247
+ Returns a list of the counts of the rankings in the profile. The type is a list of integers.
248
+ """
249
+
250
+ return list(self._rcounts)
251
+
252
+ def support(self, c1, c2):
253
+ """The number of voters that rank :math:`c1` above :math:`c2`
254
+
255
+ :param c1: the first candidate
256
+ :type c1: int
257
+ :param c2: the second candidate
258
+ :type c2: int
259
+ :rtype: int
260
+
261
+ """
262
+
263
+ return self._tally[c1][c2]
264
+
265
+ def margin(self, c1, c2):
266
+ """The number of voters that rank :math:`c1` above :math:`c2` minus the number of voters that rank :math:`c2` above :math:`c1`.
267
+
268
+ :param c1: the first candidate
269
+ :type c1: int
270
+ :param c2: the second candidate
271
+ :type c2: int
272
+ :rtype: int
273
+
274
+ """
275
+ return _margin(self._tally, c1, c2)
276
+
277
+ def majority_prefers(self, c1, c2):
278
+ """Returns true if more voters rank :math:`c1` over :math:`c2` than :math:`c2` over :math:`c1`; otherwise false.
279
+
280
+ :param c1: the first candidate
281
+ :type c1: int
282
+ :param c2: the second candidate
283
+ :type c2: int
284
+ :rtype: bool
285
+
286
+ """
287
+
288
+ return _margin(self._tally, c1, c2) > 0
289
+
290
+ def is_tied(self, c1, c2):
291
+ """Returns True if ``c1`` tied with ``c2``. That is, the same number of voters rank ``c1`` over ``c2`` as ``c2`` over ``c1``.
292
+ """
293
+
294
+ return _margin(self._tally, c1, c2) == 0
295
+
296
+ def strength_matrix(self, curr_cands = None, strength_function = None):
297
+ """
298
+ Return the strength matrix of the profile. The strength matrix is a matrix where the entry in row :math:`i` and column :math:`j` is the number of voters that rank the candidate with index :math:`i` over the candidate with index :math:`j`. If ``curr_cands`` is provided, then the strength matrix is restricted to the candidates in ``curr_cands``. If ``strength_function`` is provided, then the strength matrix is computed using the strength function."""
299
+
300
+ if curr_cands is not None:
301
+ cindices = [cidx for cidx, _ in enumerate(curr_cands)]
302
+ cindex_to_cand = lambda cidx: curr_cands[cidx]
303
+ cand_to_cindex = lambda c: cindices[curr_cands.index(c)]
304
+ strength_function = self.margin if strength_function is None else strength_function
305
+ strength_matrix = np.array([[strength_function(cindex_to_cand(a_idx), cindex_to_cand(b_idx)) for b_idx in cindices] for a_idx in cindices])
306
+ else:
307
+ cindices = self.cindices
308
+ cindex_to_cand = self.cindex_to_cand
309
+ cand_to_cindex = self.cand_to_cindex
310
+ strength_matrix = np.array(self.margin_matrix) if strength_function is None else np.array([[strength_function(cindex_to_cand(a_idx), cindex_to_cand(b_idx)) for b_idx in cindices] for a_idx in cindices])
311
+
312
+ return strength_matrix, cand_to_cindex
313
+
314
+
315
+ def cycles(self):
316
+ """Return a list of the cycles in the profile."""
317
+
318
+ return self.margin_graph().cycles()
319
+
320
+ def num_rank(self, c, level):
321
+ """The number of voters that rank candidate ``c`` at position ``level``
322
+
323
+ :param c: the candidate
324
+ :type c: int
325
+ :param level: the position of the candidate in the rankings
326
+ :type level: int
327
+
328
+ """
329
+
330
+ return _num_rank(self._rankings, self._rcounts, c, level=level)
331
+
332
+ def plurality_scores(self, curr_cands = None):
333
+ """The plurality scores in the profile restricted to the candidates in ``curr_cands``.
334
+
335
+ The **plurality score** for candidate :math:`c` is the number of voters that rank :math:`c` in first place.
336
+
337
+ :param curr_cands: restrict attention to candidates in this list. Defaults to all candidates in the profile if not provided.
338
+ :type curr_cands: list[int], optional
339
+ :returns: a dictionary associating each candidate in ``curr_cands`` with its plurality score.
340
+ """
341
+ rankings, rcounts = self.rankings_counts
342
+
343
+ curr_cands = self.candidates if curr_cands is None else curr_cands
344
+ cands_to_ignore = np.array([c for c in self.candidates if c not in curr_cands])
345
+
346
+ return {c: _num_rank_first(rankings, rcounts, cands_to_ignore, c) for c in curr_cands}
347
+
348
+ def borda_scores(self, curr_cands = None):
349
+ """The Borda scores in the profile restricted to the candidates in ``curr_cands``.
350
+
351
+ The **Borda score** for candidate :math:`c` is calculate as follows: the score assigned to :math:`c` by a ranking is the number of candidates ranked below :math:`c`. The Borda score is the sum of the score assigned to :math:`c` by each ranking in the ballot.
352
+
353
+ :param curr_cands: restrict attention to candidates in this list. Defaults to all candidates in the profile if not provided.
354
+ :type curr_cands: list[int], optional
355
+ :returns: a dictionary associating each candidate in ``curr_cands`` with its Borda score.
356
+ """
357
+
358
+ rankings = self._rankings if curr_cands is None else _find_updated_profile(self._rankings, np.array([c for c in self.candidates if c not in curr_cands]), len(self.candidates))
359
+ curr_cands = self.candidates if curr_cands is None else curr_cands
360
+
361
+ num_cands = len(curr_cands)
362
+ return {c: _borda_score(rankings, self._rcounts, num_cands, c) for c in curr_cands}
363
+
364
+ def dominators(self, cand, curr_cands = None):
365
+ """Returns the list of candidates that are majority preferred to ``cand`` in the profile restricted to the candidates in ``curr_cands``.
366
+ """
367
+ candidates = self.candidates if curr_cands is None else curr_cands
368
+
369
+ return [c for c in candidates if self.majority_prefers(c, cand)]
370
+
371
+ def dominates(self, cand, curr_cands = None):
372
+ """Returns the list of candidates that ``cand`` is majority preferred to in the profiles restricted to ``curr_cands``.
373
+ """
374
+ candidates = self.candidates if curr_cands is None else curr_cands
375
+
376
+ return [c for c in candidates if self.majority_prefers(cand, c)]
377
+
378
+ def copeland_scores(self, curr_cands = None, scores = (1,0,-1)):
379
+ """The Copeland scores in the profile restricted to the candidates in ``curr_cands``.
380
+
381
+ The **Copeland score** for candidate :math:`c` is calculated as follows: :math:`c` receives ``scores[0]`` points for every candidate that :math:`c` is majority preferred to, ``scores[1]`` points for every candidate that is tied with :math:`c`, and ``scores[2]`` points for every candidate that is majority preferred to :math:`c`. The default ``scores`` is ``(1, 0, -1)``.
382
+
383
+
384
+ :param curr_cands: restrict attention to candidates in this list. Defaults to all candidates in the profile if not provided.
385
+ :type curr_cands: list[int], optional
386
+ :param scores: the scores used to calculate the Copeland score of a candidate :math:`c`: ``scores[0]`` is for the candidates that :math:`c` is majority preferred to; ``scores[1]`` is the number of candidates tied with :math:`c`; and ``scores[2]`` is the number of candidate majority preferred to :math:`c`. The default value is ``scores = (1, 0, -1)``
387
+ :type scores: tuple[int], optional
388
+ :returns: a dictionary associating each candidate in ``curr_cands`` with its Copeland score.
389
+
390
+ """
391
+
392
+ wscore, tscore, lscore = scores
393
+ candidates = self.candidates if curr_cands is None else curr_cands
394
+ c_scores = {c: 0.0 for c in candidates}
395
+ for c1 in candidates:
396
+ for c2 in candidates:
397
+ if self.majority_prefers(c1, c2):
398
+ c_scores[c1] += wscore
399
+ elif self.majority_prefers(c2, c1):
400
+ c_scores[c1] += lscore
401
+ elif c1 != c2:
402
+ c_scores[c1] += tscore
403
+ return c_scores
404
+
405
+ def condorcet_winner(self, curr_cands = None):
406
+ """Returns the Condorcet winner in the profile restricted to ``curr_cands`` if one exists, otherwise return None.
407
+
408
+ The **Condorcet winner** is the candidate that is majority preferred to every other candidate.
409
+ """
410
+
411
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
412
+
413
+ cw = None
414
+ for c1 in curr_cands:
415
+ if all([self.majority_prefers(c1,c2) for c2 in curr_cands if c1 != c2]):
416
+ cw = c1
417
+ break # if a Condorcet winner exists, then it is unique
418
+ return cw
419
+
420
+ def weak_condorcet_winner(self, curr_cands = None):
421
+ """Returns a list of the weak Condorcet winners in the profile restricted to ``curr_cands`` (which may be empty).
422
+
423
+ A candidate :math:`c` is a **weak Condorcet winner** if there is no other candidate that is majority preferred to :math:`c`.
424
+
425
+ .. note:: While the Condorcet winner is unique if it exists, there may be multiple weak Condorcet winners.
426
+ """
427
+
428
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
429
+
430
+ weak_cw = list()
431
+ for c1 in curr_cands:
432
+ if not any([self.majority_prefers(c2,c1) for c2 in curr_cands if c1 != c2]):
433
+ weak_cw.append(c1)
434
+ return sorted(weak_cw) if len(weak_cw) > 0 else None
435
+
436
+ def condorcet_loser(self, curr_cands = None):
437
+ """Returns the Condorcet loser in the profile restricted to ``curr_cands`` if one exists, otherwise return None.
438
+
439
+ A candidate :math:`c` is a **Condorcet loser** if every other candidate is majority preferred to :math:`c`.
440
+ """
441
+
442
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
443
+
444
+ cl = None
445
+ for c1 in curr_cands:
446
+ if all([self.majority_prefers(c2,c1) for c2 in curr_cands if c1 != c2]):
447
+ cl = c1
448
+ break # if a Condorcet loser exists, then it is unique
449
+ return cl
450
+
451
+ def strict_maj_size(self):
452
+ """Returns the strict majority of the number of voters.
453
+ """
454
+
455
+ # return the size of strictly more than 50% of the voters
456
+
457
+ return int(self.num_voters/2 + 1 if self.num_voters % 2 == 0 else int(ceil(float(self.num_voters)/2)))
458
+
459
+ def margin_graph(self):
460
+ """Returns the margin graph of the profile. See :class:`.MarginGraph`.
461
+ """
462
+
463
+ return MarginGraph.from_profile(self)
464
+
465
+ def support_graph(self):
466
+ """Returns the margin graph of the profile. See :class:`.SupportGraph`.
467
+ """
468
+
469
+ return SupportGraph.from_profile(self)
470
+
471
+ def majority_graph(self):
472
+ """Returns the majority graph of the profile. See :class:`.MarginGraph`.
473
+ """
474
+
475
+ return MajorityGraph.from_profile(self)
476
+
477
+ @property
478
+ def margin_matrix(self):
479
+ """Returns the margin matrix of the profile: A matrix where the :math:`i, j` entry is the margin of candidate :math:`i` over candidate :math:`j`.
480
+ """
481
+
482
+ return [[self.margin(c1,c2) for c2 in self.candidates] for c1 in self.candidates]
483
+
484
+ def is_uniquely_weighted(self):
485
+ """Returns True if the profile is uniquely weighted.
486
+
487
+ A profile is **uniquely weighted** when there are no 0 margins and all the margins between any two candidates are unique.
488
+ """
489
+
490
+ return MarginGraph.from_profile(self).is_uniquely_weighted()
491
+
492
+ def remove_candidates(self, cands_to_ignore):
493
+ r"""Remove all candidates from ``cands_to_ignore`` from the profile.
494
+
495
+ :param cands_to_ignore: list of candidates to remove from the profile
496
+ :type cands_to_ignore: list[int]
497
+ :returns: a profile with candidates from ``cands_to_ignore`` removed and a dictionary mapping the candidates from the new profile to the original candidate names.
498
+
499
+ .. warning:: Since the candidates in a Profile must be named :math:`0, 1, \ldots, n-1` (where :math:`n` is the number of candidates), you must use the candidate map returned to by the function to recover the original candidate names.
500
+
501
+ :Example:
502
+
503
+ .. exec_code::
504
+
505
+ from pref_voting.profiles import Profile
506
+ prof = Profile([[0,1,2], [1,2,0], [2,0,1]])
507
+ prof.display()
508
+ new_prof, orig_cnames = prof.remove_candidates([1])
509
+ new_prof.display() # displaying new candidates names
510
+ new_prof.display(cmap=orig_cnames) # use the original candidate names
511
+ """
512
+ updated_rankings = _find_updated_profile(self._rankings, np.array(cands_to_ignore), self.num_cands)
513
+ new_names = {c:cidx for cidx, c in enumerate(sorted(updated_rankings[0]))}
514
+ orig_names = {v:k for k,v in new_names.items()}
515
+ return Profile([[new_names[c] for c in r] for r in updated_rankings], rcounts=self._rcounts, cmap=self.cmap), orig_names
516
+
517
+ def apply_cand_permutation(self, perm):
518
+ r"""Apply a permutation of the candidates to all rankings in the profile.
519
+ :param perm: Dictionary mapping candidates to candidates, which must be a bijection
520
+ :type perm: dict[int: int]
521
+ :returns: A new Profile with rankings transformed according to the permutation
522
+ :rtype: Profile
523
+ :Example:
524
+ .. code-block:: python
525
+ prof = Profile([[2, 0, 1]], rcounts = [1])
526
+ new_prof = prof.apply_cand_permutation({0:1, 1:2, 2:0})
527
+ # new_prof has ranking [0, 1, 2]
528
+ .. note:: The permutation must be a bijection over all candidates in the profile.
529
+ """
530
+ # Validate permutation is bijective and uses valid candidates
531
+ assert all(c in self.candidates for c in perm.keys()), "All keys must be valid candidates"
532
+ assert all(c in self.candidates for c in perm.values()), "All values must be valid candidates"
533
+ assert len(set(perm.keys())) == len(set(perm.values())) == len(self.candidates), \
534
+ "Permutation must be a bijection on the set all candidates"
535
+
536
+ # Create new rankings by applying permutation
537
+ new_rankings = [[perm[c] for c in ranking] for ranking in self._rankings]
538
+
539
+ # Create new profile with transformed rankings
540
+ return Profile(new_rankings, rcounts=self._rcounts, cmap=self.cmap)
541
+
542
+ def anonymize(self):
543
+ """
544
+ Return a profile which is the anonymized version of this profile.
545
+ """
546
+
547
+ rankings = list()
548
+ rcounts = list()
549
+ for r in self.rankings:
550
+ found_it = False
551
+ for _ridx, _r in enumerate(rankings):
552
+ if r == _r:
553
+ rcounts[_ridx] += 1
554
+ found_it = True
555
+ break
556
+ if not found_it:
557
+ rankings.append(r)
558
+ rcounts.append(1)
559
+ return Profile(rankings, rcounts=rcounts, cmap=self.cmap)
560
+
561
+ def to_profile_with_ties(self):
562
+ """Returns the profile as a ProfileWithTies
563
+ """
564
+ from pref_voting.profiles_with_ties import ProfileWithTies
565
+
566
+ ranks,rcounts=self.rankings_counts
567
+
568
+ return ProfileWithTies(
569
+ [{c:cidx for cidx,c in enumerate(list(r))}
570
+ for r in ranks],
571
+ rcounts=list(rcounts),
572
+ candidates = self.candidates,
573
+ cmap=self.cmap)
574
+
575
+ def randomly_truncate(self, truncation_prob_list = None):
576
+ """Given a truncation_prob_list that determines the probability that a ballot will be truncated at each position,
577
+ return the randomly truncated profile.
578
+
579
+ If truncation_prob_list is None, then the truncation probability distribution is uniform."""
580
+
581
+ if truncation_prob_list is None:
582
+ truncation_prob_list = [1/self.num_cands]*self.num_cands
583
+
584
+ truncated_ballots = []
585
+
586
+ for ranking, count in zip(*self.rankings_counts):
587
+ for ranking_instance in range(count):
588
+ random_number_of_cands_ranked = np.random.choice(range(1,self.num_cands+1), p=truncation_prob_list)
589
+ truncated_ranking = ranking[:random_number_of_cands_ranked]
590
+ new_ballot = {cand: ranking[cand] for cand in truncated_ranking}
591
+ truncated_ballots.append(new_ballot)
592
+
593
+ return ProfileWithTies(truncated_ballots)
594
+
595
+ def to_utility_profile(self, seed=None):
596
+ """Returns the profile as a UtilityProfile using the function Utility.from_linear_profile to generate the utility function.
597
+ So, it assigns a random utility that represents the ranking.
598
+ """
599
+
600
+ from pref_voting.mappings import Utility
601
+ from pref_voting.utility_profiles import UtilityProfile
602
+
603
+ return UtilityProfile(
604
+ [Utility.from_linear_ranking(r, seed=(seed + idx if seed is not None else None)) for idx,r in enumerate(self.rankings)]
605
+ )
606
+
607
+ def replace_rankings(
608
+ self,
609
+ old_ranking,
610
+ new_ranking,
611
+ num,
612
+ use_extended_strict_preference_for_comparison = False):
613
+ """
614
+
615
+ Create a new profile by replacing num ballots matching old_ranking with new_ranking.
616
+
617
+ If num is greater than the number of ballots matching old_ranking, then all ballots matching old_ranking are replaced with new_ranking.
618
+
619
+ """
620
+
621
+ ranking_types, ranking_counts = self.rankings_counts
622
+
623
+ same_ranking = lambda r1, r2: list(r1) == list(r2)
624
+
625
+ new_ranking_types = []
626
+ new_ranking_counts = []
627
+
628
+ current_num = 0
629
+ for r, c in zip(ranking_types, ranking_counts):
630
+
631
+ if current_num < num and same_ranking(r, old_ranking):
632
+ if c > num - current_num:
633
+ new_ranking_types.append(new_ranking)
634
+ new_ranking_counts.append(num - current_num)
635
+ new_ranking_types.append(old_ranking)
636
+ new_ranking_counts.append(c - (num - current_num))
637
+ current_num = num
638
+ elif c == num - current_num and same_ranking(r, old_ranking):
639
+ new_ranking_types.append(new_ranking)
640
+ new_ranking_counts.append(num - current_num)
641
+ current_num = num
642
+ elif c < num - current_num:
643
+ new_ranking_types.append(new_ranking)
644
+ new_ranking_counts.append(c)
645
+ current_num += c
646
+ else:
647
+ new_ranking_types.append(r)
648
+ new_ranking_counts.append(c)
649
+
650
+ new_prof = Profile(new_ranking_types, new_ranking_counts, cmap=self.cmap)
651
+
652
+ assert self.num_voters == new_prof.num_voters, "Problem: the number of voters is not the same in the new profile!"
653
+
654
+ return new_prof
655
+
656
+ def to_latex(self, cmap = None, curr_cands = None):
657
+ """Returns a string describing the profile (restricted to ``curr_cands``) as a LaTeX table (use the provided ``cmap`` or the ``cmap`` associated with the profile).
658
+
659
+ :Example:
660
+
661
+ .. exec_code::
662
+
663
+ from pref_voting.profiles import Profile
664
+ prof = Profile([[0,1,2], [1,2,0], [2,0,1]], [2, 3, 1])
665
+ print(prof.to_latex())
666
+ print()
667
+ print(prof.to_latex(cmap={0:"a", 1:"b", 2:"c"}))
668
+ """
669
+
670
+ cmap = cmap if cmap is not None else self.cmap
671
+ rankings = self._rankings if curr_cands is None else _find_updated_profile(self._rankings, np.array([c for c in self.candidates if c not in curr_cands]), len(self.candidates))
672
+
673
+ cs = 'c' * len(self._rcounts)
674
+
675
+ latex_str = "\\begin{tabular}{" + str(cs) + "}\n"
676
+ latex_str += " & ".join([f"${rc}$" for rc in self._rcounts]) + "\\\\\hline \n"
677
+ latex_str += "\\\\ \n".join([" & ".join([f"${cmap[c]}$" for c in cs]) for cs in rankings.transpose()])
678
+ latex_str += "\n\\end{tabular}"
679
+
680
+ return latex_str
681
+
682
+ def display_margin_matrix(self):
683
+ """Display the margin matrix using tabulate.
684
+ """
685
+
686
+ print(tabulate(self.margin_matrix, tablefmt="grid"))
687
+
688
+ def display_margin_graph(self, cmap=None, curr_cands = None):
689
+ """
690
+ Display the margin graph of the profile (restricted to ``curr_cands``) using the ``cmap``. See :class:`.MarginGraph`.
691
+ """
692
+
693
+ cmap = cmap if cmap is not None else self.cmap
694
+ MarginGraph.from_profile(self, cmap=cmap).display(curr_cands = curr_cands)
695
+
696
+ def display_margin_graph_with_defeat(self, defeat, curr_cands=None, show_undefeated=True, cmap=None):
697
+ """
698
+ Display the margin graph of the profile (restricted to ``curr_cands``) with the defeat edges highlighted using the ``cmap``. See :class:`.MarginGraph`.
699
+ """
700
+
701
+ MarginGraph.from_profile(self).display_with_defeat(defeat, curr_cands = curr_cands, show_undefeated = show_undefeated, cmap = cmap)
702
+
703
+ def description(self):
704
+ """
705
+ Returns a string describing the profile.
706
+ """
707
+ rs, cs = self.rankings_counts
708
+ return f"Profile({[list([int(c) for c in r]) for r in rs]}, rcounts={[int(c) for c in cs]}, cmap={self.cmap})"
709
+
710
+ def display(
711
+ self,
712
+ cmap=None,
713
+ style="pretty",
714
+ curr_cands=None,
715
+ order_by_counts=False):
716
+ """Display a profile (restricted to ``curr_cands``) as an ascii table (using tabulate).
717
+
718
+ :param cmap: the candidate map to use (overrides the cmap associated with this profile)
719
+ :type cmap: dict[int,str], optional
720
+ :param style: the candidate map to use (overrides the cmap associated with this profile)
721
+ :type style: str --- "pretty" or "fancy_grid" (or any other style option for tabulate)
722
+ :param curr_cands: list of candidates
723
+ :type curr_cands: list[int], optional
724
+ :rtype: None
725
+
726
+ :Example:
727
+
728
+ .. exec_code::
729
+
730
+ from pref_voting.profiles import Profile
731
+ prof = Profile([[0,1,2], [1,2,0], [2,0,1]], [2, 3, 1])
732
+ prof.display()
733
+ prof.display(cmap={0:"a", 1:"b", 2:"c"})
734
+
735
+ """
736
+ cmap = cmap if cmap is not None else self.cmap
737
+
738
+ rankings = self._rankings if curr_cands is None else _find_updated_profile(self._rankings, np.array([c for c in self.candidates if c not in curr_cands]), len(self.candidates))
739
+
740
+ if order_by_counts:
741
+ rankings, rcounts = zip(*sorted(zip(rankings, self._rcounts), key=lambda x: x[1], reverse=True))
742
+ rankings = np.array(rankings)
743
+ else:
744
+ rcounts = self._rcounts
745
+
746
+ print(tabulate([[cmap[c] for c in cs] for cs in rankings.transpose()], rcounts, tablefmt=style))
747
+
748
+ def to_preflib_instance(self):
749
+ """
750
+ Returns an instance of the ``OrdinalInstance`` class from the ``preflibtools`` package. See ``pref_voting.io.writers.to_preflib_instance``.
751
+
752
+ """
753
+ from pref_voting.io.writers import to_preflib_instance
754
+
755
+ return to_preflib_instance(self)
756
+
757
+ @classmethod
758
+ def from_preflib(
759
+ cls,
760
+ instance_or_preflib_file,
761
+ include_cmap=False):
762
+ """
763
+ Convert an preflib OrdinalInstance or file to a Profile. See ``pref_voting.io.readers.from_preflib``.
764
+
765
+ """
766
+ from pref_voting.io.readers import preflib_to_profile
767
+
768
+ return preflib_to_profile(
769
+ instance_or_preflib_file,
770
+ include_cmap=include_cmap,
771
+ as_linear_profile=True)
772
+
773
+ def write(
774
+ self,
775
+ filename,
776
+ file_format="preflib",
777
+ csv_format="candidate_columns"):
778
+ """
779
+ Write a profile to a file. See ``pref_voting.io.writers.write``.
780
+ """
781
+ from pref_voting.io.writers import write
782
+
783
+ return write(
784
+ self,
785
+ filename,
786
+ file_format=file_format,
787
+ csv_format=csv_format)
788
+
789
+ @classmethod
790
+ def read(
791
+ cls,
792
+ filename,
793
+ file_format="preflib",
794
+ csv_format="candidate_columns",
795
+ items_to_skip=None):
796
+ """
797
+ Read a profile from a file. See ``pref_voting.io.readers.read``.
798
+
799
+ """
800
+ from pref_voting.io.readers import read
801
+
802
+ return read(
803
+ filename,
804
+ file_format=file_format,
805
+ csv_format=csv_format,
806
+ as_linear_profile=True,
807
+ items_to_skip=items_to_skip
808
+ )
809
+
810
+ def __add__(self, other_prof):
811
+ """
812
+ Returns the sum of two profiles. The sum of two profiles is the profile that contains all the rankings from the first in addition to all the rankings from the second profile.
813
+
814
+ It is required that the two profiles have the same candidates.
815
+
816
+ Note: the cmaps of the profiles are ignored.
817
+ """
818
+
819
+ assert self.candidates == other_prof.candidates, "The two profiles must have the same candidates"
820
+
821
+ return Profile(np.concatenate([self._rankings, other_prof._rankings]), rcounts=np.concatenate([self._rcounts, other_prof._rcounts]))
822
+
823
+ def __eq__(self, other_prof):
824
+ """
825
+ Returns true if two profiles are equal. Two profiles are equal if they have the same rankings. Note that we ignore the cmaps.
826
+ """
827
+
828
+ return sorted(self.rankings) == sorted(other_prof.rankings)
829
+
830
+ def __str__(self):
831
+ """print the profile as a table using tabulate."""
832
+
833
+ return tabulate([[self.cmap[c] for c in cs] for cs in self._rankings.transpose()], self._rcounts, tablefmt="pretty")
834
+
835
+ def __getstate__(self):
836
+ """Return the state of the object for pickling."""
837
+ state = self.__dict__.copy()
838
+ # Remove derived attributes that can be recomputed
839
+ del state['_ranks']
840
+ del state['_tally']
841
+ del state['cand_to_cindex']
842
+ del state['cindex_to_cand']
843
+ return state
844
+
845
+ def __setstate__(self, state):
846
+ """Restore the state of the object from pickling."""
847
+ self.__dict__.update(state)
848
+ # Recompute derived attributes
849
+ self._ranks = np.array([[np.where(_r == c)[0][0] + 1
850
+ for c in self.candidates]
851
+ for _r in self._rankings])
852
+ self._tally = np.array([[_support(self._ranks, self._rcounts, c1, c2)
853
+ for c2 in self.candidates]
854
+ for c1 in self.candidates ])
855
+ self.cand_to_cindex = lambda c: c
856
+ self.cindex_to_cand = lambda i: i