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
pref_voting/helper.py
ADDED
@@ -0,0 +1,370 @@
|
|
1
|
+
from pref_voting.profiles import Profile
|
2
|
+
from pref_voting.profiles_with_ties import ProfileWithTies
|
3
|
+
from pref_voting.weighted_majority_graphs import MajorityGraph
|
4
|
+
from pref_voting.rankings import Ranking
|
5
|
+
from pref_voting.social_welfare_function import *
|
6
|
+
from pref_voting.voting_method import *
|
7
|
+
from itertools import combinations, chain
|
8
|
+
import random
|
9
|
+
|
10
|
+
import networkx as nx
|
11
|
+
|
12
|
+
def get_mg(edata, curr_cands = None):
|
13
|
+
|
14
|
+
if curr_cands == None:
|
15
|
+
if type(edata) == Profile or type(edata) == ProfileWithTies:
|
16
|
+
mg = MajorityGraph.from_profile(edata).mg
|
17
|
+
else:
|
18
|
+
mg = edata.mg
|
19
|
+
else:
|
20
|
+
if type(edata) == Profile or type(edata) == ProfileWithTies:
|
21
|
+
mg = nx.DiGraph()
|
22
|
+
mg.add_nodes_from(curr_cands)
|
23
|
+
mg.add_edges_from([(c1,c2) for c1 in curr_cands for c2 in curr_cands if edata.majority_prefers(c1, c2)])
|
24
|
+
else:
|
25
|
+
mg = edata.mg.copy()
|
26
|
+
mg.remove_nodes_from([c for c in edata.candidates if c not in curr_cands])
|
27
|
+
return mg
|
28
|
+
|
29
|
+
|
30
|
+
def get_weak_mg(edata, curr_cands = None):
|
31
|
+
|
32
|
+
if curr_cands == None:
|
33
|
+
if type(edata) == Profile or type(edata) == ProfileWithTies:
|
34
|
+
wmg = MajorityGraph.from_profile(edata).mg
|
35
|
+
else:
|
36
|
+
wmg = edata.mg
|
37
|
+
wmg.add_edges_from([(c1, c2) for c1 in edata.candidates for c2 in edata.candidates if c1 != c2 and edata.is_tied(c1, c2)])
|
38
|
+
else:
|
39
|
+
if type(edata) == Profile or type(edata) == ProfileWithTies:
|
40
|
+
wmg = nx.DiGraph()
|
41
|
+
wmg.add_nodes_from(curr_cands)
|
42
|
+
wmg.add_edges_from([(c1,c2) for c1 in curr_cands for c2 in curr_cands if c1 != c2 and (edata.majority_prefers(c1, c2) or edata.is_tied(c1, c2))])
|
43
|
+
else:
|
44
|
+
wmg = edata.mg.copy()
|
45
|
+
wmg.remove_nodes_from([c for c in edata.candidates if c not in curr_cands])
|
46
|
+
wmg.add_edges_from([(c1, c2) for c1 in curr_cands for c2 in curr_cands if c1 != c2 and edata.is_tied(c1, c2)])
|
47
|
+
return wmg
|
48
|
+
|
49
|
+
|
50
|
+
def swf_from_vm(vm, tie_breaker = None):
|
51
|
+
"""
|
52
|
+
Given a voting method, returns a social welfare function that uses the voting method to rank the candidates (winners are ranked first; then they are excluded from curr_cands and the new winners are ranked second; etc.).
|
53
|
+
|
54
|
+
Args:
|
55
|
+
vm (function): A voting method.
|
56
|
+
tie_breaker (str): The tie-breaking method to use. Options are "alphabetic", "random", and None. Default is None.
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
function: A social welfare function that uses the voting method to rank the candidates.
|
60
|
+
"""
|
61
|
+
|
62
|
+
def f(prof, curr_cands = None):
|
63
|
+
|
64
|
+
cands = prof.candidates if curr_cands == None else curr_cands
|
65
|
+
|
66
|
+
ranked_cands = list()
|
67
|
+
ranking_dict = dict()
|
68
|
+
|
69
|
+
n=0
|
70
|
+
|
71
|
+
while n < len(cands):
|
72
|
+
|
73
|
+
if len(ranked_cands) == len(cands):
|
74
|
+
break
|
75
|
+
|
76
|
+
ws = vm(prof, curr_cands = [c for c in cands if c not in ranked_cands])
|
77
|
+
ranked_cands = ranked_cands + ws
|
78
|
+
|
79
|
+
if tie_breaker is None:
|
80
|
+
for c in ws:
|
81
|
+
ranking_dict[c] = n
|
82
|
+
n += 1
|
83
|
+
|
84
|
+
if tie_breaker == "alphabetic":
|
85
|
+
sorted_ws = sorted(ws)
|
86
|
+
for c in sorted_ws:
|
87
|
+
ranking_dict[c] = n
|
88
|
+
n += 1
|
89
|
+
|
90
|
+
if tie_breaker == "random":
|
91
|
+
random.shuffle(ws)
|
92
|
+
for c in ws:
|
93
|
+
ranking_dict[c] = n
|
94
|
+
n += 1
|
95
|
+
|
96
|
+
return Ranking(ranking_dict)
|
97
|
+
|
98
|
+
return SocialWelfareFunction(f, name = f"SWF from {vm.name}")
|
99
|
+
|
100
|
+
|
101
|
+
def vm_from_swf(swf):
|
102
|
+
"""
|
103
|
+
Given a social welfare function, returns a voting method that selects all the candidates ranked first according to the swf.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
swf (function): A social welfare function.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
function: A voting method that uses the swf to find the winning set.
|
110
|
+
"""
|
111
|
+
|
112
|
+
def f(edata, curr_cands = None):
|
113
|
+
return sorted(swf(edata, curr_cands = curr_cands).first())
|
114
|
+
|
115
|
+
return VotingMethod(f, name = f"VM from {swf.name}")
|
116
|
+
|
117
|
+
|
118
|
+
def create_election(ranking_list,
|
119
|
+
rcounts = None,
|
120
|
+
using_extended_strict_preference=None,
|
121
|
+
candidates=None):
|
122
|
+
"""Creates an election from a list of rankings.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
ranking_list (list): A list of rankings, which may be a list of tuples of candidates, a list of dictionaries or a list of Ranking objects.
|
126
|
+
using_extended_strict_preference (bool, optional): Whether to use extended strict preference after creating a ProfileWithTies. Defaults to None.
|
127
|
+
candidates (list, optional): A list of candidates. Only used for creating a ProfileWithTies. Defaults to None (by default the candidates are all the candidates that are ranked by at least on voter).
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Profile or ProfileWithTies: The election profile.
|
131
|
+
"""
|
132
|
+
|
133
|
+
if len(ranking_list) > 0 and (type(ranking_list[0]) == tuple or type(ranking_list[0]) == list):
|
134
|
+
return Profile(ranking_list, rcounts=rcounts)
|
135
|
+
elif len(ranking_list) > 0 and (type(ranking_list[0]) == dict or type(ranking_list[0]) == Ranking):
|
136
|
+
if candidates is not None:
|
137
|
+
prof = ProfileWithTies(ranking_list, candidates=candidates, rcounts=rcounts)
|
138
|
+
else:
|
139
|
+
prof = ProfileWithTies(ranking_list, rcounts=rcounts)
|
140
|
+
if using_extended_strict_preference:
|
141
|
+
prof.use_extended_strict_preference()
|
142
|
+
return prof
|
143
|
+
else: # ranking_list is empty
|
144
|
+
print("Warning: list of rankings is empty.")
|
145
|
+
return Profile(ranking_list)
|
146
|
+
|
147
|
+
|
148
|
+
class SPO(object):
|
149
|
+
"""A strict partial order class due to Jobst Heitzig.
|
150
|
+
|
151
|
+
The strict partial order P as a binary relation is encoded as a 2d numpy array. The predecessors and successors of each object are precomputed. The add method adds a new pair to the relation and computes the transitive closure.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
n (int): The number of objects.
|
155
|
+
|
156
|
+
"""
|
157
|
+
|
158
|
+
n = None
|
159
|
+
"""The number of objects"""
|
160
|
+
objects = None
|
161
|
+
"""The list of objects"""
|
162
|
+
P = None
|
163
|
+
"""The strict partial ordering P as a binary relation encoded as a 2d numpy array"""
|
164
|
+
preds = None
|
165
|
+
"""The list of predecessors of each object"""
|
166
|
+
succs = None
|
167
|
+
"""The list of successors of each object"""
|
168
|
+
|
169
|
+
def __init__(self, n):
|
170
|
+
self.n = n
|
171
|
+
self.objects = list(range(n))
|
172
|
+
self.P = np.zeros((n, n), dtype=bool)
|
173
|
+
self.preds = [[] for _ in range(n)]
|
174
|
+
self.succs = [[] for _ in range(n)]
|
175
|
+
|
176
|
+
def add(self, a, b):
|
177
|
+
"""add a P b and all transitive consequences"""
|
178
|
+
if not self.P[a][b]:
|
179
|
+
self.P[a][b] = True
|
180
|
+
self.preds[b].append(a)
|
181
|
+
self.succs[a].append(b)
|
182
|
+
for c in self.preds[a]:
|
183
|
+
self._register(c, b)
|
184
|
+
for d in self.succs[b]:
|
185
|
+
self._register(c, d)
|
186
|
+
for d in self.succs[b]:
|
187
|
+
self._register(a, d)
|
188
|
+
|
189
|
+
def initial_elements(self):
|
190
|
+
"""return the initial elements of P (those without predecessors))"""
|
191
|
+
return [i for i in self.objects if len(self.preds[i]) == 0]
|
192
|
+
|
193
|
+
def _register(self, a, b):
|
194
|
+
"""register that a P b, without forming the transitive closure"""
|
195
|
+
if not self.P[a][b]:
|
196
|
+
self.P[a][b] = True
|
197
|
+
self.preds[b].append(a)
|
198
|
+
self.succs[a].append(b)
|
199
|
+
|
200
|
+
def to_numpy(self):
|
201
|
+
"""Return the partial order matrix P as a numpy array."""
|
202
|
+
return self.P
|
203
|
+
|
204
|
+
def to_networkx(self, cmap=None):
|
205
|
+
"""Convert the SPO to a networkx DiGraph.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
cmap (dict): A dictionary mapping each number to a candidate name. If None, the identity map is used.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
nx.DiGraph: The resulting directed graph with nodes labeled according to cmap if provided.
|
212
|
+
"""
|
213
|
+
G = nx.DiGraph()
|
214
|
+
|
215
|
+
# Determine node labels based on cmap
|
216
|
+
if cmap is not None:
|
217
|
+
node_labels = {i: cmap[i] for i in self.objects}
|
218
|
+
else:
|
219
|
+
node_labels = {i: i for i in self.objects}
|
220
|
+
|
221
|
+
# Add nodes with labels
|
222
|
+
G.add_nodes_from(node_labels.values())
|
223
|
+
|
224
|
+
# Add edges based on the partial order matrix
|
225
|
+
for a in range(self.n):
|
226
|
+
for b in range(self.n):
|
227
|
+
if self.P[a][b]:
|
228
|
+
G.add_edge(node_labels[a], node_labels[b])
|
229
|
+
|
230
|
+
return G
|
231
|
+
|
232
|
+
def to_list(self, cmap=None):
|
233
|
+
"""If the SPO is a linear order, return a list representing the order.
|
234
|
+
|
235
|
+
The list will contain candidate names based on the cmap if provided; otherwise, it will
|
236
|
+
contain the numbers.
|
237
|
+
|
238
|
+
Returns None if the SPO is not a linear order.
|
239
|
+
|
240
|
+
Args:
|
241
|
+
cmap (dict): A dictionary mapping each number to a candidate name. If None, the identity map is used.
|
242
|
+
"""
|
243
|
+
# Check if the SPO is a strict linear order
|
244
|
+
for i in range(self.n):
|
245
|
+
for j in range(i + 1, self.n):
|
246
|
+
if not (self.P[i][j] or self.P[j][i]):
|
247
|
+
return None # i and j are not comparable
|
248
|
+
|
249
|
+
# Create the linear order list by topologically sorting the nodes
|
250
|
+
linear_order = []
|
251
|
+
visited = [False] * self.n
|
252
|
+
|
253
|
+
def visit(node):
|
254
|
+
if not visited[node]:
|
255
|
+
visited[node] = True
|
256
|
+
for successor in self.succs[node]:
|
257
|
+
visit(successor)
|
258
|
+
linear_order.append(node)
|
259
|
+
|
260
|
+
for node in self.objects:
|
261
|
+
visit(node)
|
262
|
+
|
263
|
+
# Reverse to get the correct order
|
264
|
+
linear_order = linear_order[::-1]
|
265
|
+
|
266
|
+
# If cmap is provided, map the numbers to candidate names
|
267
|
+
if cmap is not None:
|
268
|
+
linear_order = [cmap[node] for node in linear_order]
|
269
|
+
|
270
|
+
return linear_order
|
271
|
+
|
272
|
+
def weak_orders(A):
|
273
|
+
"""A generator for all weak orders on A"""
|
274
|
+
if not A:
|
275
|
+
yield {}
|
276
|
+
return
|
277
|
+
for k in range(1, len(A) + 1):
|
278
|
+
for B in combinations(A, k):
|
279
|
+
for order in weak_orders(set(A) - set(B)):
|
280
|
+
new_order = {cand: rank + 1 for cand, rank in order.items()}
|
281
|
+
yield {**new_order, **{cand: 0 for cand in B}}
|
282
|
+
|
283
|
+
|
284
|
+
def weak_compositions(n, k):
|
285
|
+
"""A generator for all weak compositions of n into k parts"""
|
286
|
+
|
287
|
+
if k == 1:
|
288
|
+
yield [n]
|
289
|
+
else:
|
290
|
+
for i in range(n + 1):
|
291
|
+
for comp in weak_compositions(n - i, k - 1):
|
292
|
+
yield [i] + comp
|
293
|
+
|
294
|
+
def compositions(n):
|
295
|
+
"""Generates all compositions of the integer n. Adapted from https://stackoverflow.com/questions/10244180/python-generating-integer-partitions."""
|
296
|
+
|
297
|
+
a = [0 for i in range(n + 1)]
|
298
|
+
k = 1
|
299
|
+
a[0] = 0
|
300
|
+
a[1] = n
|
301
|
+
while k != 0:
|
302
|
+
x = a[k - 1] + 1
|
303
|
+
y = a[k] - 1
|
304
|
+
k -= 1
|
305
|
+
while 1 <= y:
|
306
|
+
a[k] = x
|
307
|
+
x = 1
|
308
|
+
y -= x
|
309
|
+
k += 1
|
310
|
+
a[k] = x + y
|
311
|
+
yield a[:k + 1]
|
312
|
+
|
313
|
+
def enumerate_compositions(int_list):
|
314
|
+
"""Given a list of integers, enumerate all the compositions of the integers."""
|
315
|
+
|
316
|
+
first_int = int_list[0]
|
317
|
+
|
318
|
+
if len(int_list) == 1:
|
319
|
+
for composition in compositions(first_int):
|
320
|
+
yield [composition]
|
321
|
+
|
322
|
+
else:
|
323
|
+
for composition in compositions(first_int):
|
324
|
+
for comps in enumerate_compositions(int_list[1:]):
|
325
|
+
yield [composition] + comps
|
326
|
+
|
327
|
+
def sublists(lst, length, x = None, partial_sublist = None):
|
328
|
+
"""Generate all sublists of lst of a specified length."""
|
329
|
+
|
330
|
+
x = length if x is None else x
|
331
|
+
|
332
|
+
partial_sublist = list() if partial_sublist is None else partial_sublist
|
333
|
+
|
334
|
+
if len(partial_sublist) == length:
|
335
|
+
yield partial_sublist
|
336
|
+
|
337
|
+
for i,el in enumerate(lst):
|
338
|
+
|
339
|
+
if i < x:
|
340
|
+
|
341
|
+
extended_partial_sublist = partial_sublist + [el]
|
342
|
+
x += 1
|
343
|
+
yield from sublists(lst[i+1::], length, x, extended_partial_sublist)
|
344
|
+
|
345
|
+
def convex_lexicographic_sublists(l):
|
346
|
+
"""Given a list l, return all convex sublists S such that S is already sorted lexicographically."""
|
347
|
+
|
348
|
+
cl_sublists = []
|
349
|
+
current_list = []
|
350
|
+
|
351
|
+
for idx, p in enumerate(l):
|
352
|
+
if current_list + [p] == sorted(current_list + [p]):
|
353
|
+
current_list = current_list + [p]
|
354
|
+
|
355
|
+
if idx == len(l)-1:
|
356
|
+
cl_sublists.append(current_list)
|
357
|
+
|
358
|
+
else:
|
359
|
+
cl_sublists.append(current_list)
|
360
|
+
current_list = [p]
|
361
|
+
|
362
|
+
if idx == len(l) - 1:
|
363
|
+
cl_sublists.append(current_list)
|
364
|
+
|
365
|
+
return cl_sublists
|
366
|
+
|
367
|
+
def powerset(iterable):
|
368
|
+
"""powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"""
|
369
|
+
s = list(iterable)
|
370
|
+
return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
|