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,1069 @@
|
|
1
|
+
"""
|
2
|
+
File: profiles_with_ties.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: January 5, 2022
|
5
|
+
|
6
|
+
A class that represents profiles of (truncated) strict weak orders.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from math import ceil
|
10
|
+
import copy
|
11
|
+
import numpy as np
|
12
|
+
from tabulate import tabulate
|
13
|
+
from pref_voting.profiles import Profile
|
14
|
+
from pref_voting.rankings import Ranking
|
15
|
+
from pref_voting.scoring_methods import symmetric_borda_scores
|
16
|
+
from pref_voting.weighted_majority_graphs import (
|
17
|
+
MajorityGraph,
|
18
|
+
MarginGraph,
|
19
|
+
SupportGraph,
|
20
|
+
)
|
21
|
+
import os
|
22
|
+
import pandas as pd
|
23
|
+
|
24
|
+
def _num_rank_profile_with_ties(rankings, rcounts, cand, level):
|
25
|
+
"""
|
26
|
+
Counts the number of voters that rank candidate `cand` at rank `level` (1-based)
|
27
|
+
in a ProfileWithTies object.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
rankings: list of Ranking objects
|
31
|
+
rcounts: list of counts for each ranking
|
32
|
+
cand: candidate
|
33
|
+
level: rank to check (1-based)
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Total number of voters ranking candidate `cand` at rank `level`
|
37
|
+
"""
|
38
|
+
total = 0
|
39
|
+
for ranking, count in zip(rankings, rcounts):
|
40
|
+
if cand in ranking.rmap and ranking.rmap[cand] == level - 1: # Convert 1-based rank to 0-based
|
41
|
+
total += count
|
42
|
+
return total
|
43
|
+
|
44
|
+
def same_ranking_extended_strict_pref(ranking1, ranking2, candidates):
|
45
|
+
# check if ranking1 and ranking2 have the same ranking of candidates
|
46
|
+
for c1 in candidates:
|
47
|
+
for c2 in candidates:
|
48
|
+
if (not ranking1.extended_strict_pref(c1, c2) and ranking2.extended_strict_pref(c1, c2)) or (not ranking2.extended_strict_pref(c1, c2) and ranking1.extended_strict_pref(c1, c2)):
|
49
|
+
return False
|
50
|
+
return True
|
51
|
+
|
52
|
+
class ProfileWithTies(object):
|
53
|
+
"""An anonymous profile of (truncated) strict weak orders of :math:`n` candidates.
|
54
|
+
|
55
|
+
:param rankings: List of rankings in the profile, where a ranking is either a :class:`Ranking` object or a dictionary.
|
56
|
+
:type rankings: list[dict[int or str: int]] or list[Ranking]
|
57
|
+
: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``.
|
58
|
+
:type rcounts: list[int], optional
|
59
|
+
:param candidates: List of candidates in the profile. If not provided, this is the list that is ranked by at least on voter.
|
60
|
+
:type candidates: list[int] or list[str], optional
|
61
|
+
:param cmap: Dictionary mapping candidates (integers) to candidate names (strings). If not provided, each candidate name is mapped to itself.
|
62
|
+
:type cmap: dict[int: str], optional
|
63
|
+
|
64
|
+
:Example:
|
65
|
+
|
66
|
+
The following code creates a profile in which
|
67
|
+
2 voters submitted the ranking 0 ranked first, 1 ranked second, and 2 ranked third; 3 voters submitted the ranking 1 and 2 are tied for first place and 0 is ranked second; and 1 voter submitted the ranking in which 2 is ranked first and 0 is ranked second:
|
68
|
+
|
69
|
+
.. code-block:: python
|
70
|
+
|
71
|
+
prof = ProfileWithTies([{0: 1, 1: 2, 2: 3}, {1:1, 2:1, 0:2}, {2:1, 0:2}], [2, 3, 1])
|
72
|
+
"""
|
73
|
+
|
74
|
+
def __init__(self, rankings, rcounts=None, candidates=None, cmap=None):
|
75
|
+
"""constructor method"""
|
76
|
+
|
77
|
+
assert rcounts is None or len(rankings) == len(
|
78
|
+
rcounts
|
79
|
+
), "The number of rankings much be the same as the number of rcounts"
|
80
|
+
|
81
|
+
|
82
|
+
get_cands = lambda r: list(r.keys()) if type(r) == dict else r.cands
|
83
|
+
self.candidates = (
|
84
|
+
sorted(candidates)
|
85
|
+
if candidates is not None
|
86
|
+
else sorted(list(set([c for r in rankings for c in get_cands(r)])))
|
87
|
+
)
|
88
|
+
"""The candidates in the profile. """
|
89
|
+
|
90
|
+
self.num_cands = len(self.candidates)
|
91
|
+
"""The number of candidates in the profile."""
|
92
|
+
|
93
|
+
self.cmap = cmap if cmap is not None else {c: str(c) for c in self.candidates}
|
94
|
+
"""The candidate map is a dictionary associating a candidate with the name used when displaying a candidate."""
|
95
|
+
|
96
|
+
self._rankings = [
|
97
|
+
Ranking(r, cmap=self.cmap)
|
98
|
+
if type(r) == dict
|
99
|
+
else Ranking(r.rmap, cmap=self.cmap)
|
100
|
+
for r in rankings
|
101
|
+
]
|
102
|
+
"""The list of rankings in the Profile (each ranking is a :class:`Ranking` object).
|
103
|
+
"""
|
104
|
+
|
105
|
+
self.ranks = list(range(1, self.num_cands + 1))
|
106
|
+
"""The ranks that are possible in the profile. """
|
107
|
+
|
108
|
+
self.cindices = list(range(self.num_cands))
|
109
|
+
self._cand_to_cindex = {c: i for i, c in enumerate(self.candidates)}
|
110
|
+
self.cand_to_cindex = lambda c: self._cand_to_cindex[c]
|
111
|
+
self._cindex_to_cand = {i: c for i, c in enumerate(self.candidates)}
|
112
|
+
self.cindex_to_cand = lambda i: self._cindex_to_cand[i]
|
113
|
+
"""Maps candidates to their index in the list of candidates and vice versa. """
|
114
|
+
|
115
|
+
self.rcounts = [1] * len(rankings) if rcounts is None else list(rcounts)
|
116
|
+
|
117
|
+
self.num_voters = np.sum(self.rcounts)
|
118
|
+
"""The number of voters in the profile. """
|
119
|
+
|
120
|
+
self.using_extended_strict_preference = False
|
121
|
+
"""A flag indicating whether the profile is using extended strict preferences when calculating supports, margins, etc."""
|
122
|
+
|
123
|
+
# memoize the supports
|
124
|
+
self._supports = {
|
125
|
+
c1: {
|
126
|
+
c2: sum(
|
127
|
+
n
|
128
|
+
for r, n in zip(self._rankings, self.rcounts)
|
129
|
+
if r.strict_pref(c1, c2)
|
130
|
+
)
|
131
|
+
for c2 in self.candidates
|
132
|
+
}
|
133
|
+
for c1 in self.candidates
|
134
|
+
}
|
135
|
+
|
136
|
+
def use_extended_strict_preference(self):
|
137
|
+
"""
|
138
|
+
Redefine the supports so that *extended strict preferences* are used. Using extended strict preference may change the margins between candidates.
|
139
|
+
"""
|
140
|
+
|
141
|
+
self.using_extended_strict_preference = True
|
142
|
+
self._supports = {
|
143
|
+
c1: {
|
144
|
+
c2: sum(
|
145
|
+
n
|
146
|
+
for r, n in zip(self._rankings, self.rcounts)
|
147
|
+
if r.extended_strict_pref(c1, c2)
|
148
|
+
)
|
149
|
+
for c2 in self.candidates
|
150
|
+
}
|
151
|
+
for c1 in self.candidates
|
152
|
+
}
|
153
|
+
|
154
|
+
def use_strict_preference(self):
|
155
|
+
"""
|
156
|
+
Redefine the supports so that strict preferences are used. Using strict preference may change the margins between candidates.
|
157
|
+
"""
|
158
|
+
|
159
|
+
self.using_extended_strict_preference = False
|
160
|
+
self._supports = {
|
161
|
+
c1: {
|
162
|
+
c2: sum(
|
163
|
+
n
|
164
|
+
for r, n in zip(self._rankings, self.rcounts)
|
165
|
+
if r.strict_pref(c1, c2)
|
166
|
+
)
|
167
|
+
for c2 in self.candidates
|
168
|
+
}
|
169
|
+
for c1 in self.candidates
|
170
|
+
}
|
171
|
+
@property
|
172
|
+
def rankings(self):
|
173
|
+
"""
|
174
|
+
Return a list of all individual rankings in the profile.
|
175
|
+
"""
|
176
|
+
|
177
|
+
return [r for ridx,r in enumerate(self._rankings)
|
178
|
+
for _ in range(self.rcounts[ridx])]
|
179
|
+
|
180
|
+
@property
|
181
|
+
def rankings_as_indifference_list(self):
|
182
|
+
"""
|
183
|
+
Return a list of all individual rankings as indifference lists in the profile.
|
184
|
+
"""
|
185
|
+
|
186
|
+
return [r.to_indiff_list() for ridx,r in enumerate(self._rankings)
|
187
|
+
for _ in range(self.rcounts[ridx])]
|
188
|
+
|
189
|
+
@property
|
190
|
+
def ranking_types(self):
|
191
|
+
"""
|
192
|
+
Return a list of the types of rankings in the profile.
|
193
|
+
"""
|
194
|
+
|
195
|
+
unique_rankings = []
|
196
|
+
for r in self._rankings:
|
197
|
+
if r not in unique_rankings:
|
198
|
+
unique_rankings.append(r)
|
199
|
+
return unique_rankings
|
200
|
+
|
201
|
+
@property
|
202
|
+
def rankings_counts(self):
|
203
|
+
"""
|
204
|
+
Returns the rankings and the counts of each ranking.
|
205
|
+
"""
|
206
|
+
|
207
|
+
return self._rankings, self.rcounts
|
208
|
+
|
209
|
+
@property
|
210
|
+
def rankings_as_dicts_counts(self):
|
211
|
+
"""
|
212
|
+
Returns the rankings represented as dictionaries and the counts of each ranking.
|
213
|
+
"""
|
214
|
+
|
215
|
+
return [r.rmap for r in self._rankings], self.rcounts
|
216
|
+
|
217
|
+
def support(self, c1, c2):
|
218
|
+
"""
|
219
|
+
Returns the support of candidate ``c1`` over candidate ``c2``, where the support is the number of voters that rank ``c1`` strictly above ``c2``.
|
220
|
+
"""
|
221
|
+
|
222
|
+
return self._supports[c1][c2]
|
223
|
+
|
224
|
+
def margin(self, c1, c2):
|
225
|
+
"""
|
226
|
+
Returns the margin of candidate ``c1`` over candidate ``c2``, where the margin is the number of voters that rank ``c1`` strictly above ``c2`` minus the number of voters that rank ``c2`` strictly above ``c1``.
|
227
|
+
"""
|
228
|
+
|
229
|
+
return self._supports[c1][c2] - self._supports[c2][c1]
|
230
|
+
|
231
|
+
@property
|
232
|
+
def margin_matrix(self):
|
233
|
+
"""Returns the margin matrix of the profile, where the entry at row ``i`` and column ``j`` is the margin of candidate ``i`` over candidate ``j``."""
|
234
|
+
|
235
|
+
return np.array(
|
236
|
+
[[self.margin(self.cindex_to_cand(c1_idx), self.cindex_to_cand(c2_idx)) for c2_idx in self.cindices] for c1_idx in self.cindices]
|
237
|
+
)
|
238
|
+
|
239
|
+
def is_tied(self, c1, c2):
|
240
|
+
"""Returns True if ``c1`` and ``c2`` are tied (i.e., the margin of ``c1`` over ``c2`` is 0)."""
|
241
|
+
|
242
|
+
return self.margin(c1, c2) == 0
|
243
|
+
|
244
|
+
def dominators(self, cand, curr_cands=None):
|
245
|
+
"""
|
246
|
+
Returns the list of candidates that are majority preferred to ``cand`` in the profile restricted to the candidates in ``curr_cands``.
|
247
|
+
"""
|
248
|
+
candidates = self.candidates if curr_cands is None else curr_cands
|
249
|
+
|
250
|
+
return [c for c in candidates if self.majority_prefers(c, cand)]
|
251
|
+
|
252
|
+
def dominates(self, cand, curr_cands=None):
|
253
|
+
"""
|
254
|
+
Returns the list of candidates that ``cand`` is majority preferred to in the majority graph restricted to ``curr_cands``.
|
255
|
+
"""
|
256
|
+
candidates = self.candidates if curr_cands is None else curr_cands
|
257
|
+
|
258
|
+
return [c for c in candidates if self.majority_prefers(cand, c)]
|
259
|
+
|
260
|
+
def ratio(self, c1, c2):
|
261
|
+
"""
|
262
|
+
Returns the ratio of the support of ``c1`` over ``c2`` to the support ``c2`` over ``c1``.
|
263
|
+
"""
|
264
|
+
|
265
|
+
if self.support(c1, c2) > 0 and self.support(c2, c1) > 0:
|
266
|
+
return self.support(c1, c2) / self.support(c2, c1)
|
267
|
+
elif self.support(c1, c2) > 0 and self.support(c2, c1) == 0:
|
268
|
+
return float(self.num_voters + self.support(c1, c2))
|
269
|
+
elif self.support(c1, c2) == 0 and self.support(c2, c1) > 0:
|
270
|
+
return 1 / (self.num_voters + self.support(c2, c1))
|
271
|
+
elif self.support(c1, c2) == 0 and self.support(c2, c1) == 0:
|
272
|
+
return 1
|
273
|
+
|
274
|
+
def majority_prefers(self, c1, c2):
|
275
|
+
"""Returns True if ``c1`` is majority preferred to ``c2``."""
|
276
|
+
|
277
|
+
return self.margin(c1, c2) > 0
|
278
|
+
|
279
|
+
def strength_matrix(self, curr_cands = None, strength_function = None):
|
280
|
+
"""
|
281
|
+
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."""
|
282
|
+
|
283
|
+
if curr_cands is not None:
|
284
|
+
cindices = [cidx for cidx, _ in enumerate(curr_cands)]
|
285
|
+
cindex_to_cand = lambda cidx: curr_cands[cidx]
|
286
|
+
cand_to_cindex = lambda c: cindices[curr_cands.index(c)]
|
287
|
+
strength_function = self.margin if strength_function is None else strength_function
|
288
|
+
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])
|
289
|
+
else:
|
290
|
+
cindices = self.cindices
|
291
|
+
cindex_to_cand = self.cindex_to_cand
|
292
|
+
cand_to_cindex = self.cand_to_cindex
|
293
|
+
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])
|
294
|
+
|
295
|
+
return strength_matrix, cand_to_cindex
|
296
|
+
|
297
|
+
def condorcet_winner(self, curr_cands=None):
|
298
|
+
"""Returns the Condorcet winner in the profile restricted to ``curr_cands`` if one exists, otherwise return None.
|
299
|
+
|
300
|
+
The **Condorcet winner** is the candidate that is majority preferred to every other candidate.
|
301
|
+
"""
|
302
|
+
curr_cands = curr_cands if curr_cands is not None else self.candidates
|
303
|
+
|
304
|
+
cw = None
|
305
|
+
for c in curr_cands:
|
306
|
+
|
307
|
+
if all([self.majority_prefers(c, c1) for c1 in curr_cands if c1 != c]):
|
308
|
+
cw = c
|
309
|
+
break
|
310
|
+
return cw
|
311
|
+
|
312
|
+
def condorcet_loser(self, curr_cands=None):
|
313
|
+
"""Returns the Condorcet loser in the profile restricted to ``curr_cands`` if one exists, otherwise return None.
|
314
|
+
|
315
|
+
A candidate :math:`c` is a **Condorcet loser** if every other candidate is majority preferred to :math:`c`.
|
316
|
+
"""
|
317
|
+
|
318
|
+
curr_cands = curr_cands if curr_cands is not None else self.candidates
|
319
|
+
|
320
|
+
cl = None
|
321
|
+
for c1 in curr_cands:
|
322
|
+
if all([self.majority_prefers(c2, c1) for c2 in curr_cands if c1 != c2]):
|
323
|
+
cl = c1
|
324
|
+
break # if a Condorcet loser exists, then it is unique
|
325
|
+
return cl
|
326
|
+
|
327
|
+
def weak_condorcet_winner(self, curr_cands=None):
|
328
|
+
"""Returns a list of the weak Condorcet winners in the profile restricted to ``curr_cands`` (which may be empty).
|
329
|
+
|
330
|
+
A candidate :math:`c` is a **weak Condorcet winner** if there is no other candidate that is majority preferred to :math:`c`.
|
331
|
+
|
332
|
+
.. note:: While the Condorcet winner is unique if it exists, there may be multiple weak Condorcet winners.
|
333
|
+
"""
|
334
|
+
|
335
|
+
curr_cands = curr_cands if curr_cands is not None else self.candidates
|
336
|
+
|
337
|
+
weak_cw = list()
|
338
|
+
for c1 in curr_cands:
|
339
|
+
if not any(
|
340
|
+
[self.majority_prefers(c2, c1) for c2 in curr_cands if c1 != c2]
|
341
|
+
):
|
342
|
+
weak_cw.append(c1)
|
343
|
+
return sorted(weak_cw) if len(weak_cw) > 0 else None
|
344
|
+
|
345
|
+
def copeland_scores(self, curr_cands = None, scores = (1,0,-1)):
|
346
|
+
"""The Copeland scores in the profile restricted to the candidates in ``curr_cands``.
|
347
|
+
|
348
|
+
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)``.
|
349
|
+
|
350
|
+
|
351
|
+
:param curr_cands: restrict attention to candidates in this list. Defaults to all candidates in the profile if not provided.
|
352
|
+
:type curr_cands: list[int], optional
|
353
|
+
: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)``
|
354
|
+
:type scores: tuple[int], optional
|
355
|
+
:returns: a dictionary associating each candidate in ``curr_cands`` with its Copeland score.
|
356
|
+
|
357
|
+
"""
|
358
|
+
|
359
|
+
wscore, tscore, lscore = scores
|
360
|
+
candidates = self.candidates if curr_cands is None else curr_cands
|
361
|
+
c_scores = {c: 0.0 for c in candidates}
|
362
|
+
for c1 in candidates:
|
363
|
+
for c2 in candidates:
|
364
|
+
if self.majority_prefers(c1, c2):
|
365
|
+
c_scores[c1] += wscore
|
366
|
+
elif self.majority_prefers(c2, c1):
|
367
|
+
c_scores[c1] += lscore
|
368
|
+
elif c1 != c2:
|
369
|
+
c_scores[c1] += tscore
|
370
|
+
return c_scores
|
371
|
+
|
372
|
+
|
373
|
+
def strict_maj_size(self):
|
374
|
+
"""Returns the strict majority of the number of voters."""
|
375
|
+
|
376
|
+
return int(
|
377
|
+
self.num_voters / 2 + 1
|
378
|
+
if self.num_voters % 2 == 0
|
379
|
+
else int(ceil(float(self.num_voters) / 2))
|
380
|
+
)
|
381
|
+
|
382
|
+
def plurality_scores(self, curr_cands=None):
|
383
|
+
"""
|
384
|
+
Return the Plurality Scores of the candidates, assuming that each voter ranks a single candidate in first place.
|
385
|
+
|
386
|
+
Parameters:
|
387
|
+
- curr_cands: List of current candidates to consider. If None, use all candidates.
|
388
|
+
|
389
|
+
Returns:
|
390
|
+
- Dictionary with candidates as keys and their plurality scores as values.
|
391
|
+
|
392
|
+
Raises:
|
393
|
+
- ValueError: If any voter ranks multiple candidates in first place.
|
394
|
+
"""
|
395
|
+
|
396
|
+
if curr_cands is None:
|
397
|
+
curr_cands = self.candidates
|
398
|
+
|
399
|
+
# Check if any voter ranks multiple candidates in first place
|
400
|
+
if any(len(r.first(cs=curr_cands)) > 1 for r in self._rankings):
|
401
|
+
raise ValueError("Cannot find the plurality scores unless all voters rank a unique candidate in first place.")
|
402
|
+
|
403
|
+
rankings, rcounts = self.rankings_counts
|
404
|
+
|
405
|
+
plurality_scores = {cand: 0 for cand in curr_cands}
|
406
|
+
|
407
|
+
for ranking, count in zip(rankings, rcounts):
|
408
|
+
first_place_candidates = ranking.first(cs=curr_cands)
|
409
|
+
if len(first_place_candidates) == 1:
|
410
|
+
cand = first_place_candidates[0]
|
411
|
+
plurality_scores[cand] += count
|
412
|
+
|
413
|
+
return plurality_scores
|
414
|
+
|
415
|
+
def plurality_scores_ignoring_overvotes(self, curr_cands=None):
|
416
|
+
"""
|
417
|
+
Return the Plurality scores ignoring empty rankings and overvotes.
|
418
|
+
"""
|
419
|
+
|
420
|
+
curr_cands = curr_cands if curr_cands is not None else self.candidates
|
421
|
+
|
422
|
+
rankings, rcounts = self.rankings_counts
|
423
|
+
|
424
|
+
return {cand: sum([c for r, c in zip(rankings, rcounts) if len(r.cands) > 0 and [cand] == r.first(cs=curr_cands)]) for cand in curr_cands}
|
425
|
+
|
426
|
+
def borda_scores(self,
|
427
|
+
curr_cands=None,
|
428
|
+
borda_score_fnc=symmetric_borda_scores):
|
429
|
+
|
430
|
+
curr_cands = self.candidates if curr_cands is None else curr_cands
|
431
|
+
restricted_prof = self.remove_candidates([c for c in self.candidates if c not in curr_cands])
|
432
|
+
return borda_score_fnc(restricted_prof)
|
433
|
+
|
434
|
+
def tops_scores(
|
435
|
+
self,
|
436
|
+
curr_cands=None,
|
437
|
+
score_type='approval'):
|
438
|
+
"""
|
439
|
+
Return the tops scores of the candidates.
|
440
|
+
|
441
|
+
Parameters:
|
442
|
+
- curr_cands: List of current candidates to consider. If None, use all candidates.
|
443
|
+
- score_type: Type of tops score to compute. Options are 'approval' or 'split'.
|
444
|
+
|
445
|
+
Returns:
|
446
|
+
- Dictionary with candidates as keys and their tops scores as values.
|
447
|
+
"""
|
448
|
+
|
449
|
+
if curr_cands is None:
|
450
|
+
curr_cands = self.candidates
|
451
|
+
|
452
|
+
rankings, rcounts = self.rankings_counts
|
453
|
+
|
454
|
+
if score_type not in {'approval', 'split'}:
|
455
|
+
raise ValueError("Invalid score_type specified. Use 'approval' or 'split'.")
|
456
|
+
|
457
|
+
tops_scores = {cand: 0 for cand in curr_cands}
|
458
|
+
|
459
|
+
if score_type == 'approval':
|
460
|
+
for ranking, count in zip(rankings, rcounts):
|
461
|
+
for cand in curr_cands:
|
462
|
+
if cand in ranking.first(cs=curr_cands):
|
463
|
+
tops_scores[cand] += count
|
464
|
+
|
465
|
+
elif score_type == 'split':
|
466
|
+
for ranking, count in zip(rankings, rcounts):
|
467
|
+
for cand in curr_cands:
|
468
|
+
if cand in ranking.first(cs=curr_cands):
|
469
|
+
tops_scores[cand] += count * 1/len(ranking.first(cs=curr_cands))
|
470
|
+
|
471
|
+
return tops_scores
|
472
|
+
|
473
|
+
def remove_empty_rankings(self):
|
474
|
+
"""
|
475
|
+
Remove the empty rankings from the profile.
|
476
|
+
"""
|
477
|
+
new_rankings = list()
|
478
|
+
new_rcounts = list()
|
479
|
+
|
480
|
+
for r,c in zip(*(self.rankings_counts)):
|
481
|
+
|
482
|
+
if len(r.cands) != 0:
|
483
|
+
new_rankings.append(r)
|
484
|
+
new_rcounts.append(c)
|
485
|
+
|
486
|
+
self._rankings = new_rankings
|
487
|
+
self.rcounts = new_rcounts
|
488
|
+
|
489
|
+
# update the number of voters
|
490
|
+
self.num_voters = np.sum(self.rcounts)
|
491
|
+
|
492
|
+
if self.using_extended_strict_preference:
|
493
|
+
self.use_extended_strict_preference()
|
494
|
+
else:
|
495
|
+
self.use_strict_preference()
|
496
|
+
|
497
|
+
def truncate_overvotes(self):
|
498
|
+
"""Return a new profile in which all rankings with overvotes are truncated. """
|
499
|
+
|
500
|
+
new_profile = copy.deepcopy(self)
|
501
|
+
rankings, rcounts = new_profile.rankings_counts
|
502
|
+
|
503
|
+
report = []
|
504
|
+
for r,c in zip(rankings, rcounts):
|
505
|
+
old_ranking = copy.deepcopy(r)
|
506
|
+
if r.has_overvote():
|
507
|
+
r.truncate_overvote()
|
508
|
+
report.append((old_ranking, r, c))
|
509
|
+
|
510
|
+
if self.using_extended_strict_preference:
|
511
|
+
new_profile.use_extended_strict_preference()
|
512
|
+
else:
|
513
|
+
new_profile.use_strict_preference()
|
514
|
+
|
515
|
+
return new_profile, report
|
516
|
+
|
517
|
+
def add_unranked_candidates(self):
|
518
|
+
"""
|
519
|
+
Return a profile in which for each voter, any unranked candidate is added to the bottom of their ranking.
|
520
|
+
"""
|
521
|
+
cands = self.candidates
|
522
|
+
ranks = list()
|
523
|
+
rcounts = list()
|
524
|
+
|
525
|
+
for r in self._rankings:
|
526
|
+
min_rank = max(r.ranks) if len(r.ranks) > 0 else 1
|
527
|
+
new_r ={c:r for c, r in r.rmap.items()}
|
528
|
+
for c in cands:
|
529
|
+
if c not in new_r.keys():
|
530
|
+
new_r[c] = min_rank+1
|
531
|
+
new_ranking = Ranking(new_r)
|
532
|
+
|
533
|
+
found_it = False
|
534
|
+
for _ridx, _r in enumerate(ranks):
|
535
|
+
if new_ranking == _r:
|
536
|
+
rcounts[_ridx] += 1
|
537
|
+
found_it = True
|
538
|
+
if not found_it:
|
539
|
+
ranks.append(new_ranking)
|
540
|
+
rcounts.append(1)
|
541
|
+
|
542
|
+
return ProfileWithTies([r.rmap for r in ranks], rcounts=rcounts, cmap=self.cmap)
|
543
|
+
|
544
|
+
@property
|
545
|
+
def is_truncated_linear(self):
|
546
|
+
"""
|
547
|
+
Return True if the profile only contains (truncated) linear orders.
|
548
|
+
"""
|
549
|
+
return all([r.is_truncated_linear(len(self.candidates)) or r.is_linear(len(self.candidates)) for r in self._rankings])
|
550
|
+
|
551
|
+
def to_linear_profile(self):
|
552
|
+
"""Return a linear profile from the profile with ties. If the profile is not a linear profile, then return None.
|
553
|
+
|
554
|
+
Note that the candidates in a Profile must be integers, so the candidates in the linear profile will be the indices of the candidates in the original profile.
|
555
|
+
|
556
|
+
"""
|
557
|
+
rankings, rcounts = self.rankings_counts
|
558
|
+
_new_rankings = [r.to_linear() for r in rankings]
|
559
|
+
cand_to_cindx = {c:i for i,c in enumerate(sorted(self.candidates))}
|
560
|
+
new_cmap = {cand_to_cindx[c]: self.cmap[c] for c in sorted(self.candidates)}
|
561
|
+
if any([r is None or len(r) != len(self.candidates) for r in _new_rankings]):
|
562
|
+
print("Error: Cannot convert to linear profile.")
|
563
|
+
return None
|
564
|
+
new_rankings = [tuple([cand_to_cindx[c] for c in r]) for r in _new_rankings]
|
565
|
+
return Profile(new_rankings, rcounts=rcounts, cmap=new_cmap)
|
566
|
+
|
567
|
+
def replace_rankings(
|
568
|
+
self,
|
569
|
+
old_ranking,
|
570
|
+
new_ranking,
|
571
|
+
num,
|
572
|
+
use_extended_strict_preference_for_comparison = False):
|
573
|
+
"""
|
574
|
+
|
575
|
+
Create a new profile by replacing num ballots matching old_ranking with new_ranking.
|
576
|
+
|
577
|
+
If num is greater than the number of ballots matching old_ranking, then all ballots matching old_ranking are replaced with new_ranking.
|
578
|
+
|
579
|
+
|
580
|
+
"""
|
581
|
+
using_extended_strict_pref = self.using_extended_strict_preference
|
582
|
+
|
583
|
+
ranking_types, ranking_counts = self.rankings_counts
|
584
|
+
|
585
|
+
if not isinstance(old_ranking, Ranking) or not isinstance(new_ranking, Ranking):
|
586
|
+
raise ValueError("rankings must be of type Ranking")
|
587
|
+
|
588
|
+
if use_extended_strict_preference_for_comparison:
|
589
|
+
same_ranking = lambda r1, r2: same_ranking_extended_strict_pref(r1, r2, self.candidates)
|
590
|
+
else:
|
591
|
+
same_ranking = lambda r1, r2: r1 == r2
|
592
|
+
|
593
|
+
new_ranking_types = []
|
594
|
+
new_ranking_counts = []
|
595
|
+
|
596
|
+
current_num = 0
|
597
|
+
for r, c in zip(ranking_types, ranking_counts):
|
598
|
+
|
599
|
+
if current_num < num and same_ranking(r, old_ranking):
|
600
|
+
if c > num - current_num:
|
601
|
+
new_ranking_types.append(new_ranking)
|
602
|
+
new_ranking_counts.append(num - current_num)
|
603
|
+
new_ranking_types.append(old_ranking)
|
604
|
+
new_ranking_counts.append(c - (num - current_num))
|
605
|
+
current_num = num
|
606
|
+
elif c == num - current_num and same_ranking(r, old_ranking):
|
607
|
+
new_ranking_types.append(new_ranking)
|
608
|
+
new_ranking_counts.append(num - current_num)
|
609
|
+
current_num = num
|
610
|
+
elif c < num - current_num:
|
611
|
+
new_ranking_types.append(new_ranking)
|
612
|
+
new_ranking_counts.append(c)
|
613
|
+
current_num += c
|
614
|
+
else:
|
615
|
+
new_ranking_types.append(r)
|
616
|
+
new_ranking_counts.append(c)
|
617
|
+
|
618
|
+
new_prof = ProfileWithTies(new_ranking_types, new_ranking_counts, self.candidates, cmap=self.cmap)
|
619
|
+
|
620
|
+
if using_extended_strict_pref:
|
621
|
+
new_prof.use_extended_strict_preference()
|
622
|
+
|
623
|
+
assert self.num_voters == new_prof.num_voters, "Problem: the number of voters is not the same in the new profile!"
|
624
|
+
|
625
|
+
return new_prof
|
626
|
+
|
627
|
+
def num_bullet_votes(self):
|
628
|
+
"""
|
629
|
+
Return the number of bullet votes in the profile.
|
630
|
+
"""
|
631
|
+
|
632
|
+
return sum([c for r,c in zip(*self.rankings_counts) if r.is_bullet_vote()])
|
633
|
+
|
634
|
+
def num_empty_rankings(self):
|
635
|
+
"""
|
636
|
+
Return the number of empty rankings in the profile.
|
637
|
+
"""
|
638
|
+
|
639
|
+
return sum([c for r,c in zip(*self.rankings_counts) if r.is_empty()])
|
640
|
+
|
641
|
+
def num_linear_orders(self):
|
642
|
+
"""
|
643
|
+
Return the number of linear orders in the profile.
|
644
|
+
"""
|
645
|
+
|
646
|
+
return sum([c for r,c in zip(*self.rankings_counts) if r.is_linear(len(self.candidates))])
|
647
|
+
|
648
|
+
def num_truncated_linear_orders(self):
|
649
|
+
"""
|
650
|
+
Return the number of truncated linear orders in the profile.
|
651
|
+
"""
|
652
|
+
|
653
|
+
return sum([c for r,c in zip(*self.rankings_counts) if r.is_truncated_linear(len(self.candidates))])
|
654
|
+
|
655
|
+
def num_rankings_with_ties(self):
|
656
|
+
"""
|
657
|
+
Return the number of rankings with ties in the profile.
|
658
|
+
"""
|
659
|
+
|
660
|
+
return sum([c for r,c in zip(*self.rankings_counts) if r.has_tie()])
|
661
|
+
|
662
|
+
def num_ranked_all_candidates(self):
|
663
|
+
"""
|
664
|
+
Return the number of rankings that rank all candidates in the profile.
|
665
|
+
"""
|
666
|
+
|
667
|
+
return sum([c for r,c in zip(*self.rankings_counts) if all([r.is_ranked(cand) for cand in self.candidates])])
|
668
|
+
|
669
|
+
def num_ranking_each_candidate(self):
|
670
|
+
"""Return a dictionary mapping each candidate to the number of voters that rank the candidate. """
|
671
|
+
|
672
|
+
return {
|
673
|
+
cand: sum([c for r,c in zip(*self.rankings_counts) if r.is_ranked(cand)])
|
674
|
+
for cand in self.candidates
|
675
|
+
}
|
676
|
+
def margin_graph(self):
|
677
|
+
"""Returns the margin graph of the profile. See :class:`.MarginGraph`.
|
678
|
+
|
679
|
+
:Example:
|
680
|
+
|
681
|
+
.. exec_code:: python
|
682
|
+
|
683
|
+
from pref_voting.profiles_with_ties import ProfileWithTies
|
684
|
+
prof = ProfileWithTies([{0: 1, 1: 2, 2: 3}, {1:1, 2:1, 0:2}, {2:1, 0:2}], [2, 3, 1])
|
685
|
+
|
686
|
+
mg = prof.margin_graph()
|
687
|
+
print(mg.edges)
|
688
|
+
print(mg.margin_matrix)
|
689
|
+
"""
|
690
|
+
|
691
|
+
return MarginGraph.from_profile(self)
|
692
|
+
|
693
|
+
def support_graph(self):
|
694
|
+
"""Returns the support graph of the profile. See :class:`.SupportGraph`.
|
695
|
+
|
696
|
+
:Example:
|
697
|
+
|
698
|
+
.. exec_code:: python
|
699
|
+
|
700
|
+
from pref_voting.profiles_with_ties import ProfileWithTies
|
701
|
+
prof = ProfileWithTies([{0: 1, 1: 2, 2: 3}, {1:1, 2:1, 0:2}, {2:1, 0:2}], [2, 3, 1])
|
702
|
+
|
703
|
+
sg = prof.support_graph()
|
704
|
+
print(sg.edges)
|
705
|
+
print(sg.s_matrix)
|
706
|
+
|
707
|
+
"""
|
708
|
+
|
709
|
+
return SupportGraph.from_profile(self)
|
710
|
+
|
711
|
+
def majority_graph(self):
|
712
|
+
"""Returns the majority graph of the profile. See :class:`.MarginGraph`.
|
713
|
+
|
714
|
+
:Example:
|
715
|
+
|
716
|
+
.. exec_code:: python
|
717
|
+
|
718
|
+
from pref_voting.profiles_with_ties import ProfileWithTies
|
719
|
+
prof = ProfileWithTies([{0: 1, 1: 2, 2: 3}, {1:1, 2:1, 0:2}, {2:1, 0:2}], [2, 3, 1])
|
720
|
+
|
721
|
+
mg = prof.majority_graph()
|
722
|
+
print(mg.edges)
|
723
|
+
|
724
|
+
"""
|
725
|
+
|
726
|
+
return MajorityGraph.from_profile(self)
|
727
|
+
|
728
|
+
def cycles(self):
|
729
|
+
"""Return a list of the cycles in the profile."""
|
730
|
+
|
731
|
+
return self.margin_graph().cycles()
|
732
|
+
|
733
|
+
def is_uniquely_weighted(self):
|
734
|
+
"""Returns True if the profile is uniquely weighted.
|
735
|
+
|
736
|
+
A profile is **uniquely weighted** when there are no 0 margins and all the margins between any two candidates are unique.
|
737
|
+
"""
|
738
|
+
|
739
|
+
return MarginGraph.from_profile(self).is_uniquely_weighted()
|
740
|
+
|
741
|
+
def remove_candidates(self, cands_to_ignore):
|
742
|
+
"""Remove all candidates from ``cands_to_ignore`` from the profile.
|
743
|
+
|
744
|
+
:param cands_to_ignore: list of candidates to remove from the profile
|
745
|
+
:type cands_to_ignore: list[int]
|
746
|
+
:returns: a profile with candidates from ``cands_to_ignore`` removed.
|
747
|
+
|
748
|
+
:Example:
|
749
|
+
|
750
|
+
.. exec_code::
|
751
|
+
|
752
|
+
from pref_voting.profiles_with_ties import ProfileWithTies
|
753
|
+
prof = ProfileWithTies([{0: 1, 1: 2, 2: 3}, {1:1, 2:1, 0:2}, {2:1, 0:2}], [2, 3, 1])
|
754
|
+
prof.display()
|
755
|
+
new_prof = prof.remove_candidates([1])
|
756
|
+
new_prof.display()
|
757
|
+
print(new_prof.ranks)
|
758
|
+
"""
|
759
|
+
|
760
|
+
updated_rankings = [
|
761
|
+
{c: r for c, r in rank.rmap.items() if c not in cands_to_ignore}
|
762
|
+
for rank in self._rankings
|
763
|
+
]
|
764
|
+
new_candidates = [c for c in self.candidates if c not in cands_to_ignore]
|
765
|
+
restricted_prof = ProfileWithTies(
|
766
|
+
updated_rankings,
|
767
|
+
rcounts=self.rcounts,
|
768
|
+
candidates=new_candidates,
|
769
|
+
cmap=self.cmap,
|
770
|
+
)
|
771
|
+
|
772
|
+
if self.using_extended_strict_preference:
|
773
|
+
restricted_prof.use_extended_strict_preference()
|
774
|
+
|
775
|
+
return restricted_prof
|
776
|
+
|
777
|
+
def report(self):
|
778
|
+
"""
|
779
|
+
Display a report of the types of rankings in the profile.
|
780
|
+
"""
|
781
|
+
num_ties = 0
|
782
|
+
num_empty_rankings = 0
|
783
|
+
num_with_skipped_ranks = 0
|
784
|
+
num_trucated_linear_orders = 0
|
785
|
+
num_linear_orders = 0
|
786
|
+
|
787
|
+
rankings, rcounts = self.rankings_counts
|
788
|
+
|
789
|
+
for r, c in zip(rankings, rcounts):
|
790
|
+
|
791
|
+
if r.has_tie():
|
792
|
+
num_ties += c
|
793
|
+
if r.is_empty():
|
794
|
+
num_empty_rankings += c
|
795
|
+
elif r.is_linear(len(self.candidates)):
|
796
|
+
num_linear_orders += c
|
797
|
+
elif r.is_truncated_linear(len(self.candidates)):
|
798
|
+
num_trucated_linear_orders += c
|
799
|
+
|
800
|
+
if r.has_skipped_rank():
|
801
|
+
num_with_skipped_ranks += c
|
802
|
+
print(f'''There are {len(self.candidates)} candidates and {str(sum(rcounts))} {'ranking: ' if sum(rcounts) == 1 else 'rankings: '}
|
803
|
+
The number of empty rankings: {num_empty_rankings}
|
804
|
+
The number of rankings with ties: {num_ties}
|
805
|
+
The number of linear orders: {num_linear_orders}
|
806
|
+
The number of truncated linear orders: {num_trucated_linear_orders}
|
807
|
+
|
808
|
+
The number of rankings with skipped ranks: {num_with_skipped_ranks}
|
809
|
+
|
810
|
+
''')
|
811
|
+
|
812
|
+
def display_rankings(self):
|
813
|
+
"""
|
814
|
+
Display a list of the rankings in the profile.
|
815
|
+
"""
|
816
|
+
rankings, rcounts = self.rankings_counts
|
817
|
+
|
818
|
+
rs = dict()
|
819
|
+
for r, c in zip(rankings, rcounts):
|
820
|
+
if str(r) in rs.keys():
|
821
|
+
rs[str(r)] += c
|
822
|
+
else:
|
823
|
+
rs[str(r)] = c
|
824
|
+
|
825
|
+
for r,c in rs.items():
|
826
|
+
print(f"{r}: {c}")
|
827
|
+
|
828
|
+
|
829
|
+
def anonymize(self):
|
830
|
+
"""
|
831
|
+
Return a profile which is the anonymized version of this profile.
|
832
|
+
"""
|
833
|
+
|
834
|
+
rankings = list()
|
835
|
+
rcounts = list()
|
836
|
+
for r in self.rankings:
|
837
|
+
found_it = False
|
838
|
+
for _ridx, _r in enumerate(rankings):
|
839
|
+
if r == _r:
|
840
|
+
rcounts[_ridx] += 1
|
841
|
+
found_it = True
|
842
|
+
break
|
843
|
+
if not found_it:
|
844
|
+
rankings.append(r)
|
845
|
+
rcounts.append(1)
|
846
|
+
|
847
|
+
prof = ProfileWithTies(rankings, rcounts=rcounts, cmap=self.cmap)
|
848
|
+
|
849
|
+
if self.using_extended_strict_preference:
|
850
|
+
prof.use_extended_strict_preference()
|
851
|
+
|
852
|
+
return prof
|
853
|
+
|
854
|
+
def description(self):
|
855
|
+
"""
|
856
|
+
Return the Python code needed to create the profile.
|
857
|
+
"""
|
858
|
+
return f"ProfileWithTies({[r.rmap for r in self._rankings]}, rcounts={[int(c) for c in self.rcounts]}, cmap={self.cmap})"
|
859
|
+
|
860
|
+
def display(
|
861
|
+
self,
|
862
|
+
cmap=None,
|
863
|
+
style="pretty",
|
864
|
+
curr_cands=None,
|
865
|
+
order_by_counts=False,
|
866
|
+
):
|
867
|
+
"""Display a profile (restricted to ``curr_cands``) as an ascii table (using tabulate).
|
868
|
+
|
869
|
+
:param cmap: the candidate map (overrides the cmap associated with this profile)
|
870
|
+
:type cmap: dict[int,str], optional
|
871
|
+
:param style: the candidate map to use (overrides the cmap associated with this profile)
|
872
|
+
:type style: str --- "pretty" or "fancy_grid" (or any other style option for tabulate)
|
873
|
+
:param curr_cands: list of candidates
|
874
|
+
:type curr_cands: list[int], optional
|
875
|
+
:rtype: None
|
876
|
+
|
877
|
+
:Example:
|
878
|
+
|
879
|
+
.. exec_code::
|
880
|
+
|
881
|
+
from pref_voting.profiles_with_ties import ProfileWithTies
|
882
|
+
prof = ProfileWithTies([{0: 1, 1: 2, 2: 3}, {1:1, 2:1, 0:2}, {2:1, 0:2}], [2, 3, 1])
|
883
|
+
prof.display()
|
884
|
+
prof.display(cmap={0:"a", 1:"b", 2:"c"})
|
885
|
+
|
886
|
+
"""
|
887
|
+
|
888
|
+
_rankings = copy.deepcopy(self._rankings)
|
889
|
+
_rankings = [r.normalize_ranks() or r for r in _rankings]
|
890
|
+
curr_cands = curr_cands if curr_cands is not None else self.candidates
|
891
|
+
cmap = cmap if cmap is not None else self.cmap
|
892
|
+
|
893
|
+
existing_ranks = list(range(min(min(r.ranks) for r in _rankings), max(max(r.ranks) for r in _rankings) + 1)) if len(_rankings) > 0 else []
|
894
|
+
if order_by_counts:
|
895
|
+
_rankings, rcounts = zip(*sorted(zip(_rankings, self.rcounts), key=lambda x: x[1], reverse=True))
|
896
|
+
else:
|
897
|
+
rcounts = self.rcounts
|
898
|
+
print(
|
899
|
+
tabulate(
|
900
|
+
[
|
901
|
+
[
|
902
|
+
" ".join(
|
903
|
+
[
|
904
|
+
str(cmap[c])
|
905
|
+
for c in r.cands_at_rank(rank)
|
906
|
+
if c in curr_cands
|
907
|
+
]
|
908
|
+
)
|
909
|
+
for r in _rankings
|
910
|
+
]
|
911
|
+
for rank in existing_ranks
|
912
|
+
],
|
913
|
+
rcounts,
|
914
|
+
tablefmt=style,
|
915
|
+
)
|
916
|
+
)
|
917
|
+
|
918
|
+
def display_margin_graph(self, cmap=None, curr_cands=None):
|
919
|
+
"""
|
920
|
+
Display the margin graph of the profile (restricted to ``curr_cands``) using the ``cmap``. See :class:`.MarginGraph`.
|
921
|
+
"""
|
922
|
+
|
923
|
+
cmap = cmap if cmap is not None else self.cmap
|
924
|
+
MarginGraph.from_profile(self, cmap=cmap).display(curr_cands=curr_cands)
|
925
|
+
|
926
|
+
def display_support_graph(self, cmap=None, curr_cands=None):
|
927
|
+
"""
|
928
|
+
Display the support graph of the profile (restricted to ``curr_cands``) using the ``cmap``. See :class:`.SupportGraph`.
|
929
|
+
"""
|
930
|
+
|
931
|
+
cmap = cmap if cmap is not None else self.cmap
|
932
|
+
SupportGraph.from_profile(self, cmap=cmap).display(curr_cands=curr_cands)
|
933
|
+
|
934
|
+
def to_preflib_instance(self):
|
935
|
+
"""
|
936
|
+
Returns an instance of the ``OrdinalInstance`` class from the ``preflibtools`` package. See ``pref_voting.io.writers.to_preflib_instance``.
|
937
|
+
|
938
|
+
"""
|
939
|
+
from pref_voting.io.writers import to_preflib_instance
|
940
|
+
|
941
|
+
return to_preflib_instance(self)
|
942
|
+
|
943
|
+
@classmethod
|
944
|
+
def from_preflib(
|
945
|
+
cls,
|
946
|
+
instance_or_preflib_file,
|
947
|
+
include_cmap=False):
|
948
|
+
"""
|
949
|
+
Convert an preflib OrdinalInstance or file to a Profile. See ``pref_voting.io.readers.from_preflib``.
|
950
|
+
|
951
|
+
"""
|
952
|
+
from pref_voting.io.readers import preflib_to_profile
|
953
|
+
|
954
|
+
return preflib_to_profile(
|
955
|
+
instance_or_preflib_file,
|
956
|
+
include_cmap=include_cmap,
|
957
|
+
as_linear_profile=False)
|
958
|
+
|
959
|
+
def write(
|
960
|
+
self,
|
961
|
+
filename,
|
962
|
+
file_format="preflib",
|
963
|
+
csv_format="candidate_columns"):
|
964
|
+
"""
|
965
|
+
Write a profile to a file. See ``pref_voting.io.writers.write``.
|
966
|
+
"""
|
967
|
+
from pref_voting.io.writers import write
|
968
|
+
|
969
|
+
return write(
|
970
|
+
self,
|
971
|
+
filename,
|
972
|
+
file_format=file_format,
|
973
|
+
csv_format=csv_format)
|
974
|
+
|
975
|
+
@classmethod
|
976
|
+
def read(
|
977
|
+
cls,
|
978
|
+
filename,
|
979
|
+
file_format="preflib",
|
980
|
+
csv_format="candidate_columns",
|
981
|
+
cand_type=None,
|
982
|
+
items_to_skip=None):
|
983
|
+
"""
|
984
|
+
Read a profile from a file. See ``pref_voting.io.readers.read``.
|
985
|
+
|
986
|
+
"""
|
987
|
+
from pref_voting.io.readers import read
|
988
|
+
|
989
|
+
return read(
|
990
|
+
filename,
|
991
|
+
file_format=file_format,
|
992
|
+
csv_format=csv_format,
|
993
|
+
cand_type=cand_type,
|
994
|
+
items_to_skip=items_to_skip,
|
995
|
+
as_linear_profile=False,
|
996
|
+
)
|
997
|
+
|
998
|
+
def to_latex(self, cmap=None, curr_cands=None):
|
999
|
+
"""
|
1000
|
+
Returns a LaTeX table representation of the profile with ties.
|
1001
|
+
|
1002
|
+
:param cmap: Dictionary mapping candidates to their names/labels. If None, use self.cmap.
|
1003
|
+
:param curr_cands: List of candidates to include in the table. If None, use all candidates.
|
1004
|
+
:return: A string containing the LaTeX table.
|
1005
|
+
"""
|
1006
|
+
cmap = self.cmap if cmap is None else cmap
|
1007
|
+
curr_cands = self.candidates if curr_cands is None else curr_cands
|
1008
|
+
|
1009
|
+
prof = copy.deepcopy(self)
|
1010
|
+
prof.remove_empty_rankings()
|
1011
|
+
|
1012
|
+
_rankings = copy.deepcopy(prof._rankings)
|
1013
|
+
|
1014
|
+
if len(_rankings) == 0: # if there are no rankings, return an empty string
|
1015
|
+
return ""
|
1016
|
+
|
1017
|
+
_rankings = [r.normalize_ranks() or r for r in _rankings ]
|
1018
|
+
|
1019
|
+
latex = "\\begin{tabular}{" + "c" * len(_rankings) + "}\n"
|
1020
|
+
latex += " & ".join(["$" + str(count) + "$" for count in self.rcounts]) + "\\\\\n"
|
1021
|
+
latex += "\\hline\n"
|
1022
|
+
|
1023
|
+
max_rank = max(max(ranking.rmap.values()) for ranking in _rankings)
|
1024
|
+
|
1025
|
+
for rank in range(1, max_rank + 1):
|
1026
|
+
row = []
|
1027
|
+
for ranking in _rankings:
|
1028
|
+
tied_cands = sorted([cmap[c] for c in curr_cands if c in ranking.rmap and ranking.rmap[c] == rank])
|
1029
|
+
if tied_cands:
|
1030
|
+
row.append("$" + ",".join(tied_cands) + "$")
|
1031
|
+
else:
|
1032
|
+
prev_cands = [c for c in curr_cands if c in ranking.rmap and ranking.rmap[c] < rank]
|
1033
|
+
if prev_cands:
|
1034
|
+
row.append(" ")
|
1035
|
+
else:
|
1036
|
+
row.append("$\\cdots$")
|
1037
|
+
latex += " & ".join(row) + "\\\\\n"
|
1038
|
+
|
1039
|
+
latex += "\\end{tabular}"
|
1040
|
+
return latex
|
1041
|
+
|
1042
|
+
def __eq__(self, other_prof):
|
1043
|
+
"""
|
1044
|
+
Returns true if two profiles are equal. Two profiles are equal if they have the same rankings. Note that we ignore the cmaps.
|
1045
|
+
"""
|
1046
|
+
|
1047
|
+
rankings = self.rankings
|
1048
|
+
other_rankings = other_prof.rankings[:] # make a copy
|
1049
|
+
for r1 in rankings:
|
1050
|
+
for i, r2 in enumerate(other_rankings):
|
1051
|
+
if r1 == r2:
|
1052
|
+
# Remove the matched item to handle duplicates
|
1053
|
+
del other_rankings[i]
|
1054
|
+
break
|
1055
|
+
else:
|
1056
|
+
# If we didn't find a match for r1, the profiles are not identical
|
1057
|
+
return False
|
1058
|
+
|
1059
|
+
return not other_rankings
|
1060
|
+
|
1061
|
+
|
1062
|
+
def __add__(self, other_prof):
|
1063
|
+
"""
|
1064
|
+
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.
|
1065
|
+
|
1066
|
+
Note: the cmaps of the profiles are ignored.
|
1067
|
+
"""
|
1068
|
+
|
1069
|
+
return ProfileWithTies(self._rankings + other_prof._rankings, rcounts=self.rcounts + other_prof.rcounts, candidates = sorted(list(set(self.candidates +other_prof.candidates))))
|