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,506 @@
|
|
1
|
+
'''
|
2
|
+
File: generate_margin_graphs.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: July 14, 2022
|
5
|
+
Updated: December 19, 2022
|
6
|
+
|
7
|
+
Functions to generate a margin graph
|
8
|
+
|
9
|
+
'''
|
10
|
+
|
11
|
+
|
12
|
+
import networkx as nx
|
13
|
+
from itertools import combinations
|
14
|
+
from pref_voting.helper import sublists, compositions, enumerate_compositions, convex_lexicographic_sublists
|
15
|
+
from pref_voting.weighted_majority_graphs import MarginGraph
|
16
|
+
import numpy as np
|
17
|
+
from scipy.stats import multivariate_normal
|
18
|
+
|
19
|
+
def generate_edge_ordered_tournament(num_cands, parity="even"):
|
20
|
+
"""Generate a random uniquely weighted MarginGraph for ``num_cands`` candidates.
|
21
|
+
|
22
|
+
:param num_cands: the number of candidates
|
23
|
+
:type num_cands: int
|
24
|
+
:returns: a uniquely weighted margin graph
|
25
|
+
:rtype: MarginGraph
|
26
|
+
|
27
|
+
.. note:: This function randomly generates a tournament with a linear order over the edges. A **tournament** is an asymmetric directed graph with an edge between every two nodes. The linear order of the edges is represented by assigning to each edge a number :math:`2, \ldots, 2*n`, where :math:`n` is the number of the edges.
|
28
|
+
"""
|
29
|
+
|
30
|
+
assert parity in ["even", "odd"], "The parity should be either 'even' or 'odd'."
|
31
|
+
|
32
|
+
mg = nx.DiGraph()
|
33
|
+
mg.add_nodes_from(range(num_cands))
|
34
|
+
_edges = list()
|
35
|
+
for c1 in range(num_cands):
|
36
|
+
for c2 in range(c1+1, num_cands):
|
37
|
+
if np.random.choice([True, False]):
|
38
|
+
_edges.append((c1, c2))
|
39
|
+
else:
|
40
|
+
_edges.append((c2, c1))
|
41
|
+
|
42
|
+
edges = list()
|
43
|
+
edge_indices = list(range(len(_edges)))
|
44
|
+
np.random.shuffle(edge_indices)
|
45
|
+
|
46
|
+
for i, e_idx in enumerate(edge_indices):
|
47
|
+
edges.append((_edges[e_idx][0], _edges[e_idx][1], 2 * (i+1) if parity == 'even' else 2 * i+1))
|
48
|
+
|
49
|
+
return MarginGraph(range(num_cands), edges)
|
50
|
+
|
51
|
+
def generate_margin_graph(num_cands, weight_domain = None, parity = 'even'):
|
52
|
+
"""Generate a random MarginGraph (allowing for ties in the margins) for ``num_cands`` candidates.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
num_cands (int): the number of candidates
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
MarginGraph
|
59
|
+
|
60
|
+
"""
|
61
|
+
|
62
|
+
assert parity in ['even', 'odd'], "Parity must be 'even' or 'odd'."
|
63
|
+
assert weight_domain is None or isinstance(weight_domain, list) and len(weight_domain) > 0, "The weight_domain must be a list with at least one element."
|
64
|
+
|
65
|
+
candidates = list(range(num_cands))
|
66
|
+
edges = list()
|
67
|
+
pairs_of_cands = list(combinations(candidates, 2))
|
68
|
+
|
69
|
+
if weight_domain is None and parity == 'even':
|
70
|
+
weight_domain = [2 * pidx for pidx in range(len(pairs_of_cands) + 1)]
|
71
|
+
elif weight_domain is None and parity == 'odd':
|
72
|
+
weight_domain = [2 * pidx + 1 for pidx in range(len(pairs_of_cands) + 1)]
|
73
|
+
#
|
74
|
+
|
75
|
+
for c1, c2 in pairs_of_cands:
|
76
|
+
|
77
|
+
margin = np.random.choice(weight_domain)
|
78
|
+
|
79
|
+
if margin != 0:
|
80
|
+
if np.random.choice([True, False]):
|
81
|
+
edges.append((c1, c2, margin))
|
82
|
+
else:
|
83
|
+
edges.append((c2, c1, margin))
|
84
|
+
|
85
|
+
return MarginGraph(candidates, edges)
|
86
|
+
|
87
|
+
def generate_margin_graph_bradley_terry(num_cands, num_voters, score_prob_mod = lambda c: np.random.uniform(0,1)):
|
88
|
+
"""Generates a margin graph for num_cands candidates by first sampling candidate scores from score_prob_mod and then sampling votes from the Bradley-Terry model using the sampled scores.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
num_cands (int): Number of candidates
|
92
|
+
num_voters (int): Number of voters
|
93
|
+
score_prob_mod (function, optional): A function that takes a candidate and returns a score. Defaults to lambda c: np.random.uniform(0,1).
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
MarginGraph: A margin graph
|
97
|
+
"""
|
98
|
+
|
99
|
+
candidates = list(range(num_cands))
|
100
|
+
pairs_of_cands = list(combinations(candidates, 2))
|
101
|
+
|
102
|
+
cand_score = dict()
|
103
|
+
for c in candidates:
|
104
|
+
cand_score[c] = score_prob_mod(c)
|
105
|
+
|
106
|
+
edges = list()
|
107
|
+
|
108
|
+
for c1, c2 in pairs_of_cands:
|
109
|
+
|
110
|
+
support_c1_c2 = 0
|
111
|
+
support_c2_c1 = 0
|
112
|
+
|
113
|
+
for n in range(num_voters):
|
114
|
+
vote = np.random.choice([1,0], p = [cand_score[c1] / (cand_score[c1] + cand_score[c2]), cand_score[c2] / (cand_score[c1] + cand_score[c2])])
|
115
|
+
|
116
|
+
if vote == 1:
|
117
|
+
support_c1_c2 += 1
|
118
|
+
else:
|
119
|
+
support_c2_c1 += 1
|
120
|
+
|
121
|
+
if support_c1_c2 > support_c2_c1:
|
122
|
+
edges.append((c1,c2, support_c1_c2 - support_c2_c1))
|
123
|
+
|
124
|
+
if support_c2_c1 > support_c1_c2:
|
125
|
+
edges.append((c2,c1, support_c2_c1 - support_c1_c2))
|
126
|
+
|
127
|
+
return MarginGraph(candidates, edges)
|
128
|
+
|
129
|
+
###
|
130
|
+
|
131
|
+
# Turn a code into a pair
|
132
|
+
def depair(pair_vector, k):
|
133
|
+
return pair_vector[k]
|
134
|
+
|
135
|
+
# This function defines the i,jth entry of the covariance matrix
|
136
|
+
def entries(pair_vector, i,j):
|
137
|
+
x = depair(pair_vector, i)
|
138
|
+
y = depair(pair_vector, j)
|
139
|
+
if x[0] == y[0] and x[1] == y[1]:
|
140
|
+
return 1
|
141
|
+
if x[1] == y[0]:
|
142
|
+
return -1/3
|
143
|
+
if x[1] == y[1]:
|
144
|
+
return 1/3
|
145
|
+
if x[0] == y[0]:
|
146
|
+
return 1/3
|
147
|
+
if x[0] == y[1]:
|
148
|
+
return -1/3
|
149
|
+
return 0
|
150
|
+
|
151
|
+
def generate_covariance_matrix(num_candidates):
|
152
|
+
|
153
|
+
num_pairs = num_candidates *(num_candidates -1)//2
|
154
|
+
|
155
|
+
# Store the vector mapping codes to pairs
|
156
|
+
pair_vector = [0]*num_pairs
|
157
|
+
|
158
|
+
# Populate the vector of pairs
|
159
|
+
k=0
|
160
|
+
for i in range(num_candidates):
|
161
|
+
for j in range(i+1,num_candidates):
|
162
|
+
pair_vector[k] = [i,j]
|
163
|
+
k = k+1
|
164
|
+
|
165
|
+
# Populate the covariance matrix
|
166
|
+
cov = np.empty((num_pairs,num_pairs))
|
167
|
+
for i in range(num_pairs):
|
168
|
+
for j in range(num_pairs):
|
169
|
+
cov[i,j] = entries(pair_vector, i,j)
|
170
|
+
|
171
|
+
return cov
|
172
|
+
|
173
|
+
|
174
|
+
def generate_edge_ordered_tournament_infinite_limit(num_candidates, cov_matrix = None):
|
175
|
+
"""
|
176
|
+
Using the ideas from Section 9 of the paper
|
177
|
+
*An Analysis of Random Elections with Large Numbers of Voters* by Matthew Harrison-Trainor
|
178
|
+
(https://arxiv.org/abs/2009.02979) and the code provided at
|
179
|
+
https://github.com/MatthewHT/RandomMarginGraphs/, generate a qualitative margin graph for
|
180
|
+
``num_candidates`` candidates.
|
181
|
+
|
182
|
+
.. important::
|
183
|
+
|
184
|
+
The weights of the generated margin graphs are real numbers, representing a linear ordering of the edges.
|
185
|
+
Only qualitative margin graph invariant voting methods, such as Split Cycle, Beat Path, Minimax,
|
186
|
+
Ranked Pairs, etc., should be used on the generated graphs.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
|
190
|
+
num_candidates (int): the number of candidates
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
|
194
|
+
MarginGraph
|
195
|
+
|
196
|
+
"""
|
197
|
+
|
198
|
+
candidates = range(num_candidates)
|
199
|
+
cov_matrix = cov_matrix if cov_matrix is not None else generate_covariance_matrix(num_candidates)
|
200
|
+
# random_var is a random variable with the multivariate normal distribution of margin graphs
|
201
|
+
random_var = multivariate_normal(None, cov_matrix)
|
202
|
+
rv = random_var.rvs()
|
203
|
+
|
204
|
+
def pair(p):
|
205
|
+
return p[1]-2*p[0]-1 + (num_candidates)*(num_candidates+1)//2 - (num_candidates-p[0])*(num_candidates-p[0]+1)//2
|
206
|
+
|
207
|
+
mg = [[-np.inf for _ in candidates] for _ in candidates]
|
208
|
+
|
209
|
+
for c1 in candidates:
|
210
|
+
for c2 in candidates:
|
211
|
+
if (c1 < c2 and rv[pair([c1,c2])] > 0):
|
212
|
+
mg[c1][c2] = rv[pair([c1,c2])]
|
213
|
+
if (c1 > c2 and rv[pair([c2,c1])] < 0):
|
214
|
+
mg[c1][c2] = -rv[pair([c2,c1])]
|
215
|
+
if (c1 == c2):
|
216
|
+
mg[c1][c2] = 0
|
217
|
+
|
218
|
+
w_edges = [(c1, c2, mg[c1][c2])
|
219
|
+
for c1 in candidates
|
220
|
+
for c2 in candidates if c1 != c2 if mg[c1][c2] > 0]
|
221
|
+
|
222
|
+
return MarginGraph(candidates, w_edges)
|
223
|
+
|
224
|
+
## Generating Canonical MarginGraphs without Tied Margins
|
225
|
+
|
226
|
+
def _enumerate_ceots(num_cands, num_edges, partial_ceot, used_nodes, next_node):
|
227
|
+
|
228
|
+
# Given a partial ceot P, we can extend it with any new edge (A,B) satisfying one of the following conditions:
|
229
|
+
|
230
|
+
# 1. A and B have both already been used in edges in P, but neither (A,B) nor (B,A) is in P;
|
231
|
+
# 2. A has already been used in an edge in P, and B is the next integer after the largest integer in an edge in P.
|
232
|
+
# 3. A is the next integer after the largest integer in an edge in P, and B has already been used in an edge in P;
|
233
|
+
# 4. A is the next integer after the largest integer in an edge in P, and B is the next integer after A.
|
234
|
+
|
235
|
+
if len(partial_ceot) == num_edges:
|
236
|
+
yield partial_ceot
|
237
|
+
|
238
|
+
if len(partial_ceot) < num_edges:
|
239
|
+
|
240
|
+
if next_node == num_cands:
|
241
|
+
available_nodes = used_nodes
|
242
|
+
|
243
|
+
if next_node < num_cands:
|
244
|
+
available_nodes = used_nodes + [next_node]
|
245
|
+
|
246
|
+
for n in available_nodes:
|
247
|
+
|
248
|
+
if n == next_node and next_node < num_cands - 1: # If n == next_node, we are in Case 3 or Case 4 above
|
249
|
+
|
250
|
+
available_nodes = used_nodes + [next_node + 1]
|
251
|
+
|
252
|
+
for m in available_nodes:
|
253
|
+
|
254
|
+
if not n==m and not (n,m) in partial_ceot and not (m,n) in partial_ceot:
|
255
|
+
|
256
|
+
new_ceot = [edge for edge in partial_ceot] + [(n,m)]
|
257
|
+
|
258
|
+
if not (n == next_node or m == next_node): # Then we are in Case 1 above
|
259
|
+
|
260
|
+
yield from _enumerate_ceots(num_cands,num_edges,new_ceot,used_nodes,next_node)
|
261
|
+
|
262
|
+
if (n == next_node or m == next_node) and not m == next_node + 1: # Then we are in Case 2 or 3 above
|
263
|
+
|
264
|
+
new_used_nodes = list(set(used_nodes + [n,m]))
|
265
|
+
new_next_node = next_node + 1
|
266
|
+
|
267
|
+
yield from _enumerate_ceots(num_cands,num_edges,new_ceot,new_used_nodes,new_next_node)
|
268
|
+
|
269
|
+
if m == next_node + 1: # Then we are in Case 4 above
|
270
|
+
|
271
|
+
new_used_nodes = list(set(used_nodes + [n,m]))
|
272
|
+
new_next_node = next_node + 2
|
273
|
+
|
274
|
+
yield from _enumerate_ceots(num_cands,num_edges,new_ceot,new_used_nodes,new_next_node)
|
275
|
+
|
276
|
+
def _enumerate_ceots_as_edgelist(num_cands):
|
277
|
+
|
278
|
+
num_edges = (num_cands * (num_cands -1))//2
|
279
|
+
|
280
|
+
partial_ceot = [(0,1)]
|
281
|
+
|
282
|
+
used_nodes = [0,1]
|
283
|
+
|
284
|
+
next_node = 2
|
285
|
+
|
286
|
+
yield from _enumerate_ceots(num_cands,num_edges,partial_ceot,used_nodes,next_node)
|
287
|
+
|
288
|
+
|
289
|
+
def enumerate_canonical_edge_ordered_tournaments(num_cands, parity = "even"):
|
290
|
+
"""
|
291
|
+
A *canonical* edge-ordered tournament (ceot) is a representative from an isomorphism class of
|
292
|
+
linearly edge-ordered tournaments. Enumerate all ceots for ``num_cands`` candidates, representing
|
293
|
+
a ceot as a ``MarginGraph`` where the margins represent the linear order of the edges.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
num_cands (int): the number of candidates
|
297
|
+
parity (str, optional): The parity of the margins, either 'even' or 'odd'.
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
A generator of ``MarginGraph`` for ``num_candidates``
|
301
|
+
|
302
|
+
.. warning:: It is only feasible to finish the enumeration for up to 5 candidates.
|
303
|
+
|
304
|
+
"""
|
305
|
+
|
306
|
+
assert parity in ["odd", "even"], "parity must be either 'odd' or 'even'"
|
307
|
+
|
308
|
+
for ceot in _enumerate_ceots_as_edgelist(num_cands):
|
309
|
+
yield MarginGraph(list(range(num_cands)),
|
310
|
+
[(e[0], e[1], 2 * (eidx + 1) if parity == "even" else 2 * eidx + 1)
|
311
|
+
for eidx, e in enumerate(reversed(ceot))])
|
312
|
+
|
313
|
+
|
314
|
+
def enumerate_uniquely_weighted_margin_graphs(num_cands, weight_domain):
|
315
|
+
"""
|
316
|
+
Enumerate all representatives from isomorphism classes of uniquely-weighted margin graphs with weights drawn from ``weight_domain``.
|
317
|
+
|
318
|
+
Args:
|
319
|
+
num_cands (int): the number of candidates
|
320
|
+
weight_domain (List[int]): The list of weights in the margin graph.
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
A generator of ``MarginGraph`` for ``num_candidates``
|
324
|
+
|
325
|
+
|
326
|
+
.. warning:: It is only feasible to finish the enumeration for up to 5 candidates.
|
327
|
+
|
328
|
+
"""
|
329
|
+
|
330
|
+
weight_domain = sorted(weight_domain)
|
331
|
+
|
332
|
+
num_edges = (num_cands * (num_cands - 1)) // 2
|
333
|
+
|
334
|
+
for ceot in _enumerate_ceots_as_edgelist(num_cands):
|
335
|
+
|
336
|
+
for weight_list in sublists(weight_domain, num_edges):
|
337
|
+
yield MarginGraph(list(range(num_cands)),
|
338
|
+
[(e[0], e[1], weight_list[eidx]) for eidx, e in enumerate(reversed(ceot))])
|
339
|
+
|
340
|
+
|
341
|
+
## Generating Canonical MarginGraphs with Tied Margins
|
342
|
+
|
343
|
+
def _enumerate_cweots_as_edgelist(num_cands, include_weak_tournaments=True):
|
344
|
+
|
345
|
+
#Enumerate each canonical weakly edge ordered tournament as a list of lists of tied edges.
|
346
|
+
#If include_weak_tournaments = True, then allow weak tournaments in which two nodes may have no edge between them.
|
347
|
+
|
348
|
+
def edge_match(e1, e2):
|
349
|
+
return e1['weight'] == e2['weight']
|
350
|
+
|
351
|
+
cweots = dict() # For isomorphism checking, keep track of the cweots generated so far.
|
352
|
+
|
353
|
+
if include_weak_tournaments:
|
354
|
+
cweots_with_absent_edges = dict() # For isomorphism checking, keep track of the cweots for which some edges are absent.
|
355
|
+
|
356
|
+
for ceot in tqdm(list(_enumerate_ceots_as_edgelist(num_cands))):
|
357
|
+
|
358
|
+
# The sorted list of number of wins by each candidate will be a useful invariant for isomorphism checking below.
|
359
|
+
win_vector = tuple(sorted([len([edge for edge in ceot if edge[0] == i]) for i in range(num_cands)]))
|
360
|
+
|
361
|
+
# Given ceot, we will generate many cweots as follows:
|
362
|
+
|
363
|
+
# 1. Collect all the convex lexicographic sublists of ceot in order as [L1,...,Ln].
|
364
|
+
# It suffices to only consider convex lexicographic sublists because for any cweot,
|
365
|
+
# we can obtain a ceot by breaking all ties between edges in a tied group lexicographically.
|
366
|
+
|
367
|
+
l_sublists = convex_lexicographic_sublists(ceot)
|
368
|
+
|
369
|
+
# 2. Within each L_i, we want to consider all ways of making consecutive edges tied.
|
370
|
+
# Such a way is given by a composition of the integer len(L_i).
|
371
|
+
# Thus, we first iterate over all compositions of len(L_i),...,len(L_n).
|
372
|
+
|
373
|
+
int_list = [len(s) for s in l_sublists]
|
374
|
+
|
375
|
+
# 3. Since the above approach overgenerates cweots, we will check for isomorphism before adding a cweot to our list.
|
376
|
+
|
377
|
+
for compositions in enumerate_compositions(int_list):
|
378
|
+
|
379
|
+
cases = [False, True] if include_weak_tournaments else [False]
|
380
|
+
|
381
|
+
for consider_weak_tourns in cases:
|
382
|
+
|
383
|
+
cweot = []
|
384
|
+
|
385
|
+
for idx, s in enumerate(l_sublists):
|
386
|
+
|
387
|
+
composition = compositions[idx]
|
388
|
+
|
389
|
+
for n in composition:
|
390
|
+
cweot.append(s[:n])
|
391
|
+
s=s[n:]
|
392
|
+
|
393
|
+
# If we are considering weak tournaments in this case, we remove the last tied group of edges and compute the sorted win-loss vector.
|
394
|
+
if consider_weak_tourns:
|
395
|
+
win_loss_vector = tuple(sorted([(len([edge for edge in ceot if edge[0] == i and edge not in cweot[-1]]),len([edge for edge in ceot if edge[1] == i and edge not in cweot[-1]])) for i in range(num_cands)]))
|
396
|
+
cweot = cweot[:-1]
|
397
|
+
|
398
|
+
G = nx.DiGraph()
|
399
|
+
weight = len(ceot)
|
400
|
+
for group in cweot:
|
401
|
+
for edge in group:
|
402
|
+
G.add_edge(edge[0], edge[1], weight=weight)
|
403
|
+
weight = weight-1
|
404
|
+
|
405
|
+
add_graph = True
|
406
|
+
|
407
|
+
# Next we check whether G is isomorphic to a cweot G2 already generated.
|
408
|
+
# We only need to check those cweots G2 that have (i) the same sorted Copeland scores and
|
409
|
+
# (ii) the same list of numbers of edges in each tied group as G,
|
410
|
+
# since these are necessary conditions for isomorphism.
|
411
|
+
|
412
|
+
tie_sizes = tuple([len(s) for s in cweot])
|
413
|
+
|
414
|
+
if not consider_weak_tourns:
|
415
|
+
|
416
|
+
invariant = (win_vector, tie_sizes)
|
417
|
+
|
418
|
+
if invariant not in cweots.keys():
|
419
|
+
cweots[invariant] = []
|
420
|
+
|
421
|
+
for idx, G2 in enumerate(cweots[invariant]):
|
422
|
+
if nx.is_isomorphic(G, G2, edge_match=edge_match):
|
423
|
+
add_graph = False
|
424
|
+
break
|
425
|
+
|
426
|
+
if add_graph:
|
427
|
+
cweots[invariant].append(G)
|
428
|
+
yield cweot
|
429
|
+
|
430
|
+
if consider_weak_tourns:
|
431
|
+
|
432
|
+
invariant = (win_loss_vector, tie_sizes)
|
433
|
+
|
434
|
+
if invariant not in cweots_with_absent_edges.keys():
|
435
|
+
cweots_with_absent_edges[invariant] = []
|
436
|
+
|
437
|
+
for idx, G2 in enumerate(cweots_with_absent_edges[invariant]):
|
438
|
+
if nx.is_isomorphic(G, G2, edge_match=edge_match):
|
439
|
+
add_graph = False
|
440
|
+
break
|
441
|
+
|
442
|
+
if add_graph:
|
443
|
+
cweots_with_absent_edges[invariant].append(G)
|
444
|
+
yield cweot
|
445
|
+
|
446
|
+
def enumerate_canonical_weakly_edge_ordered_tournaments(num_cands, parity = "even", include_weak_tournaments = True):
|
447
|
+
"""
|
448
|
+
A *canonical* weakly edge-ordered tournament (cweot) is a representative from an isomorphism class of
|
449
|
+
weakly edge-ordered tournaments. Enumerate all cweots for ``num_cands`` candidates, representing
|
450
|
+
a cweot as a ``MarginGraph`` where the margins represent the order of the edges.
|
451
|
+
|
452
|
+
If include_weak_tournaments = True, then allow weak tournaments in which two nodes may have no edge between them.
|
453
|
+
|
454
|
+
Args:
|
455
|
+
num_cands (int): the number of candidates
|
456
|
+
parity (str, optional): The parity of the margins, either 'even' or 'odd'.
|
457
|
+
|
458
|
+
Returns:
|
459
|
+
A generator of ``MarginGraph`` for ``num_candidates``
|
460
|
+
|
461
|
+
.. warning:: It is only feasible to finish the enumeration for up to 4 candidates.
|
462
|
+
|
463
|
+
"""
|
464
|
+
|
465
|
+
assert parity in ["odd", "even"], "parity must be either 'odd' or 'even'"
|
466
|
+
|
467
|
+
for cweot in _enumerate_cweots_as_edgelist(num_cands, include_weak_tournaments=include_weak_tournaments):
|
468
|
+
|
469
|
+
weighted_edges = list()
|
470
|
+
|
471
|
+
for idx, group in enumerate(reversed(cweot)):
|
472
|
+
for e in group:
|
473
|
+
weighted_edge = (e[0], e[1], 2 * (idx + 1) if parity == "even" else 2 * idx + 1)
|
474
|
+
weighted_edges.append(weighted_edge)
|
475
|
+
|
476
|
+
yield MarginGraph(list(range(num_cands)), weighted_edges)
|
477
|
+
|
478
|
+
def enumerate_margin_graphs(num_cands, weight_domain, include_weak_tournaments = True):
|
479
|
+
"""
|
480
|
+
Enumerate all representatives from isomorphism classes of margin graphs with weights drawn from ``weight_domain``.
|
481
|
+
|
482
|
+
Args:
|
483
|
+
num_cands (int): the number of candidates
|
484
|
+
weight_domain (List[int]): The list of weights in the margin graph.
|
485
|
+
|
486
|
+
Returns:
|
487
|
+
A generator of ``MarginGraph`` for ``num_candidates``
|
488
|
+
|
489
|
+
.. warning:: It is only feasible to finish the enumeration for up to 4 candidates.
|
490
|
+
|
491
|
+
"""
|
492
|
+
|
493
|
+
weight_domain = sorted(weight_domain)
|
494
|
+
|
495
|
+
for cweot in _enumerate_cweots_as_edgelist(num_cands, include_weak_tournaments = include_weak_tournaments):
|
496
|
+
|
497
|
+
for weight_list in sublists(weight_domain, len(cweot)):
|
498
|
+
|
499
|
+
weighted_edges = list()
|
500
|
+
|
501
|
+
for idx, group in enumerate(reversed(cweot)):
|
502
|
+
for e in group:
|
503
|
+
weighted_edge = (e[0], e[1], weight_list[idx])
|
504
|
+
weighted_edges.append(weighted_edge)
|
505
|
+
|
506
|
+
yield MarginGraph(list(range(num_cands)), weighted_edges)
|
@@ -0,0 +1,184 @@
|
|
1
|
+
'''
|
2
|
+
File: grade_methods.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: September 24, 2023
|
5
|
+
|
6
|
+
Implementations of grading methods (also called evaluative methods).
|
7
|
+
'''
|
8
|
+
from pref_voting.voting_method import *
|
9
|
+
from itertools import product
|
10
|
+
|
11
|
+
@vm(name="Score Voting")
|
12
|
+
def score_voting(gprofile, curr_cands=None, evaluation_method="sum"):
|
13
|
+
"""Return the candidates with the largest scores, where scores are evaluated using the ``evaluation_method``, where the default is summing the scores of the candidates. If ``curr_cands`` is provided, then the score vote is restricted to the candidates in ``curr_cands``.
|
14
|
+
"""
|
15
|
+
|
16
|
+
curr_cands = gprofile.candidates if curr_cands is None else curr_cands
|
17
|
+
if evaluation_method == "sum":
|
18
|
+
evaluation_method_func = gprofile.sum
|
19
|
+
elif evaluation_method == "mean" or evaluation_method == "average":
|
20
|
+
evaluation_method_func = gprofile.avg
|
21
|
+
elif evaluation_method == "median": # returns lower median
|
22
|
+
evaluation_method_func = gprofile.median
|
23
|
+
|
24
|
+
scores = {
|
25
|
+
c: evaluation_method_func(c)
|
26
|
+
for c in curr_cands if gprofile.has_grade(c)
|
27
|
+
}
|
28
|
+
|
29
|
+
max_score = max(scores.values())
|
30
|
+
|
31
|
+
return sorted([c for c in scores.keys() if scores[c] == max_score])
|
32
|
+
|
33
|
+
@vm(name="Approval")
|
34
|
+
def approval(gprofile, curr_cands=None):
|
35
|
+
"""Return the approval vote of the grade profile ``gprofile``. If ``curr_cands`` is provided, then the approval vote is restricted to the candidates in ``curr_cands``.
|
36
|
+
|
37
|
+
.. warning::
|
38
|
+
Approval Vote only works on Grade Profiles that are based on 2 grades: 0 and 1.
|
39
|
+
|
40
|
+
"""
|
41
|
+
assert sorted(gprofile.grades) == [0, 1], "The grades in the profile must be {0, 1}."
|
42
|
+
|
43
|
+
return score_voting(gprofile, curr_cands=curr_cands, evaluation_method="sum")
|
44
|
+
|
45
|
+
@vm(name="Dis&approval")
|
46
|
+
def dis_and_approval(gprofile, curr_cands=None):
|
47
|
+
"""Return the Dis&approval vote of the grade profile ``gprofile``. If ``curr_cands`` is provided, then the dis&approval vote is restricted to the candidates in ``curr_cands``. See https://link.springer.com/article/10.1007/s00355-013-0766-7 for more information.
|
48
|
+
|
49
|
+
.. warning::
|
50
|
+
Dis&approval only works on Grade Profiles that are based on 2 grades: -1 and 1.
|
51
|
+
|
52
|
+
"""
|
53
|
+
assert sorted(gprofile.grades) == [-1, 0, 1], "The grades in the profile must be {-1, 0, 1}."
|
54
|
+
|
55
|
+
return score_voting(gprofile, curr_cands=curr_cands, evaluation_method="sum")
|
56
|
+
|
57
|
+
@vm(name="Cumulative Voting")
|
58
|
+
def cumulative_voting(gprofile, curr_cands=None, max_total_grades=5):
|
59
|
+
"""Return the cumulative vote winner of the grade profile ``gprofile``. This is the candidates with the largest sum of the grades where each voter submits a ballot of scores that sum to ``max_total_grades``. If ``curr_cands`` is provided, then the cumulative vote is restricted to the candidates in ``curr_cands``."""
|
60
|
+
assert sorted(gprofile.grades) == list(range(max_total_grades + 1)) and np.sum(gprofile.grades) == max_total_grades , f"For cumulative voting, the sum the grades must be {max_total_grades}."
|
61
|
+
|
62
|
+
return score_voting(gprofile, curr_cands=curr_cands, evaluation_method="sum")
|
63
|
+
|
64
|
+
@vm(name="STAR")
|
65
|
+
def star(gprofile, curr_cands=None):
|
66
|
+
""" Identify the top two candidates according to the sum of the grades for each candidate. Then hold a runoff between the top two candidates where the candidate that is ranked above the other by the most voters is the winner. The candidates that move to the runoff round are: the candidate(s) with the largest sum of the grades and the candidate(s) with the 2nd largest sum of the grades (or perhaps tied for the largest sum of the grades). In the case of multiple candidates tied for the largest or 2nd largest sum of the grades, use parallel-universe tiebreaking: a candidate is a Star Vote winner if it is a winner in some head-to-head runoff as described. If the candidates are all tied for the largest sum of the grades, then all candidates are winners.
|
67
|
+
|
68
|
+
See https://starvoting.us for more information.
|
69
|
+
|
70
|
+
If ``curr_cands`` is provided, then the winners is restricted to the candidates in ``curr_cands``.
|
71
|
+
|
72
|
+
.. warning::
|
73
|
+
Star Vote only works on Grade Profiles that are based on 6 grades: 0, 1, 2, 3, 4, and 5.
|
74
|
+
"""
|
75
|
+
|
76
|
+
assert sorted(gprofile.grades) == [0, 1, 2, 3, 4, 5], "The grades in the profile must be {0, 1, 2, 3, 4, 5}."
|
77
|
+
|
78
|
+
curr_cands = gprofile.candidates if curr_cands is None else curr_cands
|
79
|
+
|
80
|
+
if len(curr_cands) == 1:
|
81
|
+
return list(curr_cands)
|
82
|
+
|
83
|
+
cand_to_scores = {
|
84
|
+
c: gprofile.sum(c)
|
85
|
+
for c in curr_cands if gprofile.has_grade(c)
|
86
|
+
}
|
87
|
+
|
88
|
+
scores = sorted(list(set(cand_to_scores.values())), reverse=True)
|
89
|
+
|
90
|
+
max_score = scores[0]
|
91
|
+
first = [c for c in cand_to_scores.keys() if cand_to_scores[c] == max_score]
|
92
|
+
|
93
|
+
second = list()
|
94
|
+
if len(first) == 1:
|
95
|
+
second_score = scores[1]
|
96
|
+
second = [c for c in cand_to_scores.keys() if cand_to_scores[c] == second_score]
|
97
|
+
|
98
|
+
if len(second) > 0:
|
99
|
+
all_runoff_pairs = product(first, second)
|
100
|
+
else:
|
101
|
+
all_runoff_pairs = [(c1,c2) for c1,c2 in product(first, first) if c1 != c2]
|
102
|
+
|
103
|
+
winners = list()
|
104
|
+
for c1, c2 in all_runoff_pairs:
|
105
|
+
|
106
|
+
if gprofile.margin(c1,c2) > 0:
|
107
|
+
winners.append(c1)
|
108
|
+
elif gprofile.margin(c1,c2) < 0:
|
109
|
+
winners.append(c2)
|
110
|
+
elif gprofile.margin(c1,c2) == 0:
|
111
|
+
winners.append(c1)
|
112
|
+
winners.append(c2)
|
113
|
+
|
114
|
+
return sorted(list(set(winners)))
|
115
|
+
|
116
|
+
|
117
|
+
def tiebreaker_diff(gprofile, cand, median_grade):
|
118
|
+
"""
|
119
|
+
Tiebreaker when the there are multiple candidates with the largest median grade.
|
120
|
+
The tiebreaker is the difference between the proportion of voters who grade the candidate higher than the median grade and the proportion of voters who grade the candidate lower than the median grade.
|
121
|
+
"""
|
122
|
+
prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
|
123
|
+
prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
|
124
|
+
|
125
|
+
return prop_proponents - prop_opponents
|
126
|
+
|
127
|
+
def tiebreaker_relative_shares(gprofile, cand, median_grade):
|
128
|
+
"""
|
129
|
+
Tiebreaker when the there are multiple candidates with the largest median grade.
|
130
|
+
Returns the *relative shares* of the proponents and opponents of the candidate.
|
131
|
+
"""
|
132
|
+
|
133
|
+
prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
|
134
|
+
prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
|
135
|
+
|
136
|
+
return (prop_proponents - prop_opponents) / (2 * (prop_proponents + prop_opponents))
|
137
|
+
|
138
|
+
def tiebreaker_normalized_difference(gprofile, cand, median_grade):
|
139
|
+
"""
|
140
|
+
Tiebreaker when the there are multiple candidates with the largest median grade.
|
141
|
+
Returns the *normalized difference* of the proponents and opponents of the candidate.
|
142
|
+
"""
|
143
|
+
|
144
|
+
prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
|
145
|
+
prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
|
146
|
+
|
147
|
+
return (prop_proponents - prop_opponents) /(2 * (1 - prop_proponents - prop_opponents))
|
148
|
+
|
149
|
+
|
150
|
+
def tiebreaker_majority_judgement(gprofile, cand, median_grade):
|
151
|
+
"""
|
152
|
+
Tiebreaker when the there are multiple candidates with the largest median grade.
|
153
|
+
Returns the proportion of voters assigning a higher grade than the median to cand if it is greater than the proportion of voters assigning a lower grade than the median to cand, otherwise return -1 * the proportion of voters assigning a lower grade than the median to cand.
|
154
|
+
"""
|
155
|
+
|
156
|
+
prop_proponents = gprofile.proportion_with_higher_grade(cand, median_grade)
|
157
|
+
prop_opponents = gprofile.proportion_with_lower_grade(cand, median_grade)
|
158
|
+
|
159
|
+
if prop_proponents > prop_opponents:
|
160
|
+
return prop_proponents
|
161
|
+
elif prop_opponents >= prop_proponents:
|
162
|
+
return -prop_opponents
|
163
|
+
|
164
|
+
|
165
|
+
def greatest_median(gprofile, curr_cands=None, tb_func = tiebreaker_majority_judgement):
|
166
|
+
|
167
|
+
"""
|
168
|
+
Returns the candidate(s) with the greatest median grade. If there is a tie, the tie is broken by the tiebreaker function.
|
169
|
+
|
170
|
+
"""
|
171
|
+
median_winners = score_voting(gprofile, curr_cands=curr_cands, evaluation_method="median")
|
172
|
+
|
173
|
+
if len(median_winners) == 1:
|
174
|
+
return median_winners
|
175
|
+
else:
|
176
|
+
tb_scores = {c: tb_func(gprofile, c, gprofile.median(c)) for c in median_winners}
|
177
|
+
return sorted([c for c in tb_scores if tb_scores[c] == max(tb_scores.values())])
|
178
|
+
|
179
|
+
@vm(name="Majority Judgement")
|
180
|
+
def majority_judgement(gprofile, curr_cands=None):
|
181
|
+
"""
|
182
|
+
The Majority Judgement voting method as describe in Balinski and Laraki (https://mitpress.mit.edu/9780262545716/majority-judgment/).
|
183
|
+
"""
|
184
|
+
return greatest_median(gprofile, curr_cands=curr_cands, tb_func = tiebreaker_majority_judgement)
|