pref_voting 1.16.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. pref_voting/__init__.py +1 -0
  2. pref_voting/analysis.py +496 -0
  3. pref_voting/axiom.py +38 -0
  4. pref_voting/axiom_helpers.py +129 -0
  5. pref_voting/axioms.py +10 -0
  6. pref_voting/c1_methods.py +963 -0
  7. pref_voting/combined_methods.py +514 -0
  8. pref_voting/create_methods.py +128 -0
  9. pref_voting/data/examples/condorcet_winner/minimal_Anti-Plurality.soc +16 -0
  10. pref_voting/data/examples/condorcet_winner/minimal_Borda.soc +17 -0
  11. pref_voting/data/examples/condorcet_winner/minimal_Bracket_Voting.soc +20 -0
  12. pref_voting/data/examples/condorcet_winner/minimal_Bucklin.soc +19 -0
  13. pref_voting/data/examples/condorcet_winner/minimal_Coombs.soc +20 -0
  14. pref_voting/data/examples/condorcet_winner/minimal_Coombs_PUT.soc +20 -0
  15. pref_voting/data/examples/condorcet_winner/minimal_Coombs_TB.soc +20 -0
  16. pref_voting/data/examples/condorcet_winner/minimal_Dowdall.soc +19 -0
  17. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff.soc +18 -0
  18. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_PUT.soc +18 -0
  19. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_TB.soc +18 -0
  20. pref_voting/data/examples/condorcet_winner/minimal_Iterated_Removal_Condorcet_Loser.soc +17 -0
  21. pref_voting/data/examples/condorcet_winner/minimal_Pareto.soc +17 -0
  22. pref_voting/data/examples/condorcet_winner/minimal_Plurality.soc +18 -0
  23. pref_voting/data/examples/condorcet_winner/minimal_PluralityWRunoff_PUT.soc +18 -0
  24. pref_voting/data/examples/condorcet_winner/minimal_Positive-Negative_Voting.soc +17 -0
  25. pref_voting/data/examples/condorcet_winner/minimal_Simplified_Bucklin.soc +18 -0
  26. pref_voting/data/examples/condorcet_winner/minimal_Superior_Voting.soc +19 -0
  27. pref_voting/data/examples/condorcet_winner/minimal_Weighted_Bucklin.soc +19 -0
  28. pref_voting/data/examples/condorcet_winner/minimal_resolute_Anti-Plurality.soc +17 -0
  29. pref_voting/data/examples/condorcet_winner/minimal_resolute_Borda.soc +17 -0
  30. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bracket_Voting.soc +20 -0
  31. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bucklin.soc +19 -0
  32. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs.soc +21 -0
  33. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_PUT.soc +21 -0
  34. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_TB.soc +20 -0
  35. pref_voting/data/examples/condorcet_winner/minimal_resolute_Dowdall.soc +18 -0
  36. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff.soc +18 -0
  37. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_PUT.soc +18 -0
  38. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_TB.soc +18 -0
  39. pref_voting/data/examples/condorcet_winner/minimal_resolute_Plurality.soc +18 -0
  40. pref_voting/data/examples/condorcet_winner/minimal_resolute_PluralityWRunoff_PUT.soc +18 -0
  41. pref_voting/data/examples/condorcet_winner/minimal_resolute_Positive-Negative_Voting.soc +17 -0
  42. pref_voting/data/examples/condorcet_winner/minimal_resolute_Simplified_Bucklin.soc +20 -0
  43. pref_voting/data/examples/condorcet_winner/minimal_resolute_Weighted_Bucklin.soc +19 -0
  44. pref_voting/data/voting_methods_properties.json +414 -0
  45. pref_voting/data/voting_methods_properties.json.lock +0 -0
  46. pref_voting/dominance_axioms.py +387 -0
  47. pref_voting/generate_profiles.py +801 -0
  48. pref_voting/generate_spatial_profiles.py +198 -0
  49. pref_voting/generate_utility_profiles.py +160 -0
  50. pref_voting/generate_weighted_majority_graphs.py +506 -0
  51. pref_voting/grade_methods.py +184 -0
  52. pref_voting/grade_profiles.py +357 -0
  53. pref_voting/helper.py +370 -0
  54. pref_voting/invariance_axioms.py +671 -0
  55. pref_voting/io/__init__.py +0 -0
  56. pref_voting/io/readers.py +432 -0
  57. pref_voting/io/writers.py +256 -0
  58. pref_voting/iterative_methods.py +2425 -0
  59. pref_voting/maj_graph_ex1.png +0 -0
  60. pref_voting/mappings.py +577 -0
  61. pref_voting/margin_based_methods.py +2345 -0
  62. pref_voting/monotonicity_axioms.py +872 -0
  63. pref_voting/num_evaluation_method.py +77 -0
  64. pref_voting/other_axioms.py +161 -0
  65. pref_voting/other_methods.py +939 -0
  66. pref_voting/pairwise_profiles.py +547 -0
  67. pref_voting/prob_voting_method.py +105 -0
  68. pref_voting/probabilistic_methods.py +287 -0
  69. pref_voting/profiles.py +856 -0
  70. pref_voting/profiles_with_ties.py +1069 -0
  71. pref_voting/rankings.py +466 -0
  72. pref_voting/scoring_methods.py +481 -0
  73. pref_voting/social_welfare_function.py +59 -0
  74. pref_voting/social_welfare_functions.py +7 -0
  75. pref_voting/spatial_profiles.py +448 -0
  76. pref_voting/stochastic_methods.py +99 -0
  77. pref_voting/strategic_axioms.py +1394 -0
  78. pref_voting/swf_axioms.py +173 -0
  79. pref_voting/utility_functions.py +102 -0
  80. pref_voting/utility_methods.py +178 -0
  81. pref_voting/utility_profiles.py +333 -0
  82. pref_voting/variable_candidate_axioms.py +640 -0
  83. pref_voting/variable_voter_axioms.py +3747 -0
  84. pref_voting/voting_method.py +355 -0
  85. pref_voting/voting_method_properties.py +92 -0
  86. pref_voting/voting_methods.py +8 -0
  87. pref_voting/voting_methods_registry.py +136 -0
  88. pref_voting/weighted_majority_graphs.py +1539 -0
  89. pref_voting-1.16.31.dist-info/METADATA +208 -0
  90. pref_voting-1.16.31.dist-info/RECORD +92 -0
  91. pref_voting-1.16.31.dist-info/WHEEL +4 -0
  92. pref_voting-1.16.31.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,357 @@
1
+
2
+ '''
3
+ File: grade_profiles.py
4
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
5
+ Date: September 24, 2023
6
+
7
+ Functions to reason about profiles of grades.
8
+ '''
9
+
10
+ import matplotlib.pyplot as plt
11
+ import numpy as np
12
+ import pandas as pd
13
+ from tabulate import tabulate
14
+ from pref_voting.mappings import Grade, _Mapping
15
+ from pref_voting.profiles_with_ties import ProfileWithTies
16
+
17
+ class GradeProfile(object):
18
+ """An anonymous profile of (truncated) grades.
19
+
20
+ :param grade_maps: List of grades in the profile, where a grade is either a :class:`Grade` object or a dictionary.
21
+ :type grade_maps: list[dict[int or str: int or str]] or list[Grade]
22
+ :param grades: List of grades.
23
+ :type gcounts: list[int or str]
24
+ :param gcounts: List of the number of voters associated with each grade. Should be the same length as grade_maps. If not provided, it is assumed that 1 voter submitted each element of ``grade_maps``.
25
+ :type gcounts: list[int], optional
26
+ :param candidates: List of candidates in the profile. If not provided, it is the candidates that are assigned a grade by least on voter.
27
+ :type candidates: list[int] or list[str], optional
28
+ :param cmap: Dictionary mapping candidates to candidate names (strings). If not provided, each candidate name is mapped to itself.
29
+ :type cmap: dict[int or str: str], optional
30
+ :param gmap: Dictionary mapping grades to grade names (strings). If not provided, each grade is mapped to itself.
31
+ :type gmap: dict[int or str: str], optional
32
+ :param grade_order: A list of the grades representing the order of the grades. It is assumed the grades are listed from largest to smallest. If not provided, the grades are assumed to be numbers and compared using the greater-than relation.
33
+ :type gmap: list[int or str], optional
34
+
35
+ :Example:
36
+
37
+ The following code creates a profile in which
38
+ 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:
39
+
40
+ .. code-block:: python
41
+
42
+ gprof = GradeProfile([{"x":1, "y":3, "z":1}, {"x":0, "y":-1, "z":3}, {"x":0, "y":-1}, {"x":0, "y":1, "z":2}], [-1, 0, 1, 2, 3], gcounts=[2, 3, 1, 1], candidates=["x", "y", "z"])
43
+
44
+ gprof.display()
45
+
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ grade_maps,
51
+ grades,
52
+ gcounts=None,
53
+ candidates=None,
54
+ cmap=None,
55
+ gmap=None,
56
+ grade_order=None):
57
+ """Constructor method"""
58
+
59
+ assert gcounts is None or len(grade_maps) == len(
60
+ gcounts
61
+ ), "The number of grades much be the same as the number of gcounts"
62
+ self.candidates = (
63
+ sorted(candidates)
64
+ if candidates is not None
65
+ else sorted(list(set([x for g in grade_maps for x in g.keys()])))
66
+ )
67
+ """The domain of the profile. """
68
+ self.cmap = cmap if cmap is not None else {c: str(c) for c in self.candidates}
69
+ """The candidate map is a dictionary associating an alternative with the name used when displaying a alternative."""
70
+
71
+ self.grades=grades
72
+ """The grades in the profile. """
73
+
74
+ self.can_sum_grades = all([isinstance(g, (float, int)) for g in self.grades])
75
+
76
+ self.grade_order = grade_order if grade_order is not None else sorted(self.grades, reverse = True)
77
+ """The order of the grades. If None, then order from largest to smallest"""
78
+
79
+ self.use_grade_order = grade_order is not None
80
+
81
+ self.compare_function = lambda v1, v2: (v1 > v2) - (v2 > v1) if grade_order is None else lambda v1, v2: (grade_order.index(v1) < grade_order.index(v2)) - (grade_order.index(v2) < grade_order.index(v1))
82
+
83
+ self.gmap = gmap if gmap is not None else {g: str(g) for g in self.grades}
84
+ """The candidate map is a dictionary associating an alternative with the name used when displaying a alternative."""
85
+
86
+ self._grades = [
87
+ Grade(g_map, self.grades, candidates=self.candidates, cmap=self.cmap, gmap=self.gmap, compare_function=self.compare_function)
88
+ if type(g_map) == dict
89
+ else Grade(g_map.as_dict(), self.grades, candidates=self.candidates, cmap=self.cmap, gmap=self.gmap, compare_function=self.compare_function)
90
+ for g_map in grade_maps
91
+ ]
92
+ """The list of grades in the Profile (each utility is a :class:`Utility` object).
93
+ """
94
+ self.gcounts = [1] * len(grade_maps) if gcounts is None else list(gcounts)
95
+
96
+ self.num_voters = np.sum(self.gcounts)
97
+ """The number of voters in the profile. """
98
+
99
+ @property
100
+ def grades_counts(self):
101
+ """Returns the grade and the counts of each grade."""
102
+
103
+ return self._grades, self.gcounts
104
+
105
+ @property
106
+ def grade_functions(self):
107
+ """Return all of the grade functions in the profile."""
108
+
109
+ gs = list()
110
+ for g,c in zip(self._grades, self.gcounts):
111
+ gs += [g] * c
112
+ return gs
113
+
114
+ def has_grade(self, c):
115
+ """Return True if ``c`` is assigned a grade by at least one voter."""
116
+
117
+ return any([g.has_grade(c) for g in self._grades])
118
+
119
+ @property
120
+ def num_cands(self):
121
+ """Return the number of candidates in the profile."""
122
+ return len(self.candidates)
123
+
124
+ def margin(self, c1, c2, use_extended=False):
125
+ """
126
+ Return the margin of ``c1`` over ``c2``. If ``c1`` is not assigned a grade by any voter, return None.
127
+ """
128
+ if use_extended:
129
+ return np.sum([num for g,num in zip(*self.grades_counts) if g.extended_strict_pref(c1, c2)]) - np.sum([num for g,num in zip(*self.grades_counts) if g.extended_strict_pref(c2, c1)])
130
+ else:
131
+ return np.sum([num for g,num in zip(*self.grades_counts) if g.strict_pref(c1, c2)]) - np.sum([num for g,num in zip(*self.grades_counts) if g.strict_pref(c2, c1)])
132
+
133
+ def proportion(self, cand, grade):
134
+ """
135
+ Return the proportion of voters that assign ``cand`` the grade ``grade``.
136
+
137
+ Note that ``grade`` could be None, in which case the proportion of voters that do not assign ``cand`` a grade is returned.
138
+ """
139
+ return np.sum([num for g,num in zip(*self.grades_counts) if g(cand) == grade]) / self.num_voters
140
+
141
+ def sum(self, c):
142
+ """Return the sum of the grades of ``c``. If ``c`` is not assigned a grade by any voter, return None."""
143
+
144
+ assert self.can_sum_grades, "The grades in the profile cannot be summed."
145
+
146
+ return np.sum([g(c) * num for g,num in zip(*self.grades_counts) if g.has_grade(c)]) if self.has_grade(c) else None
147
+
148
+ def avg(self, c):
149
+ """Return the average of the grades of ``c``. If ``c`` is not assigned a grade by any voter, return None."""
150
+
151
+ assert self.can_sum_grades, "The grades in the profile cannot be summed."
152
+
153
+ return np.mean([g(c) for g in self.grade_functions if g.has_grade(c)]) if self.has_grade(c) else None
154
+
155
+ def max(self, c):
156
+ """Return the maximum of the grade of ``c``. If ``c`` is not assigned a grade by any voter, return None."""
157
+
158
+ grades_for_c = [-1 * self.grade_order.index(g(c)) for g in self._grades if g.has_grade(c)] if self.use_grade_order else [g(c) for g in self._grades if g.has_grade(c)]
159
+
160
+ return (self.grade_order[-1 * max(grades_for_c)] if self.use_grade_order else max(grades_for_c)) if self.has_grade(c) else None
161
+
162
+ def min(self, c):
163
+ """Return the minimum of the grades of ``c``. If ``c`` is not assigned a grade by any voter, return None."""
164
+
165
+ grades_for_c = [-1 * self.grade_order.index(g(c)) for g in self._grades if g.has_grade(c)] if self.use_grade_order else [g(c) for g in self._grades if g.has_grade(c)]
166
+
167
+ return (self.grade_order[-1 * min(grades_for_c)] if self.use_grade_order else min(grades_for_c)) if self.has_grade(c) else None
168
+
169
+ def median(self, c, use_lower=True, use_average=False):
170
+ """Return the median of the grades of ``c``. If ``c`` is not assigned a grade by any voter, return None."""
171
+
172
+ grades_for_c = [-1 * self.grade_order.index(g(c)) for g in self.grade_functions if g.has_grade(c)] if self.use_grade_order else [g(c) for g in self.grade_functions if g.has_grade(c)]
173
+
174
+ sorted_grades_for_c = sorted(grades_for_c)
175
+ num_grades = len(sorted_grades_for_c)
176
+ median_idx = num_grades // 2
177
+ if num_grades % 2 == 0:
178
+ median_grades = sorted_grades_for_c[median_idx - 1:median_idx + 1]
179
+ else:
180
+ median_grades = [sorted_grades_for_c[median_idx]]
181
+
182
+ if use_lower:
183
+ return (self.grade_order[-1 * median_grades[0]] if self.use_grade_order else median_grades[0]) if self.has_grade(c) else None
184
+ elif use_average:
185
+ return (np.average([self.grade_order[-1 * m] for m in median_grades]) if self.use_grade_order else np.average(median_grades)) if self.has_grade(c) else None
186
+ else:
187
+ return ([self.grade_order[-1 * m] for m in median_grades] if self.use_grade_order else median_grades) if self.has_grade(c) else None
188
+
189
+ def sum_grade_function(self):
190
+ """Return the sum grade function of the profile."""
191
+
192
+ assert self.can_sum_grades, "The grades in the profile cannot be summed."
193
+
194
+ return _Mapping(
195
+ {
196
+ c: self.sum(c)
197
+ for c in self.candidates if self.has_grade(c)
198
+ },
199
+ domain=self.candidates,
200
+ item_map=self.cmap,
201
+ compare_function=self.compare_function
202
+ )
203
+ def avg_grade_function(self):
204
+ """Return the average grade function of the profile."""
205
+
206
+ assert self.can_sum_grades, "The grades in the profile cannot be summed."
207
+
208
+ return _Mapping(
209
+ {
210
+ c: self.avg(c)
211
+ for c in self.candidates if self.has_grade(c)
212
+ },
213
+ domain=self.candidates,
214
+ item_map=self.cmap,
215
+ compare_function=self.compare_function
216
+ )
217
+
218
+
219
+ def proportion_with_grade(self, cand, grade):
220
+ """
221
+ Return the proportion of voters that assign a ``grade`` to ``cand`` .
222
+ """
223
+
224
+ assert cand in self.candidates, f"{cand} is not a candidate in the profile."
225
+ assert grade in self.grades, f"{grade} is not a grade in the profile."
226
+
227
+ num_with_higher_grade = 0
228
+ for g,num in zip(*self.grades_counts):
229
+ if self.compare_function(g(cand),grade) == 0:
230
+ num_with_higher_grade += num
231
+ return num_with_higher_grade / self.num_voters
232
+
233
+
234
+ def proportion_with_higher_grade(self, cand, grade):
235
+ """
236
+ Return the proportion of voters that assign a strictly higher grade to ``cand`` than ``grade``.
237
+ """
238
+
239
+ assert cand in self.candidates, f"{cand} is not a candidate in the profile."
240
+ assert grade in self.grades, f"{grade} is not a grade in the profile."
241
+
242
+ num_with_higher_grade = 0
243
+ for g,num in zip(*self.grades_counts):
244
+ if self.compare_function(g(cand),grade) == 1:
245
+ num_with_higher_grade += num
246
+ return num_with_higher_grade / self.num_voters
247
+
248
+
249
+ def proportion_with_lower_grade(self, cand, grade):
250
+ """
251
+ Return the proportion of voters that assign a strictly lower grade to ``cand`` than ``grade``.
252
+ """
253
+
254
+ assert cand in self.candidates, f"{cand} is not a candidate in the profile."
255
+ assert grade in self.grades, f"{grade} is not a grade in the profile."
256
+
257
+
258
+ num_with_lower_grade = 0
259
+ for g,num in zip(*self.grades_counts):
260
+ if self.compare_function(g(cand),grade) == -1:
261
+ num_with_lower_grade += num
262
+ return num_with_lower_grade / self.num_voters
263
+
264
+ def approval_scores(self):
265
+ """
266
+ Return a dictionary representing the approval scores of the candidates in the profile.
267
+ """
268
+
269
+ assert self.can_sum_grades, "The grades in the profile cannot be summed."
270
+ assert sorted(self.grades) == [0,1], "The grades in the profile must be 0 and 1."
271
+
272
+ return {c: self.sum(c) for c in self.candidates}
273
+
274
+ def to_ranking_profile(self):
275
+ """Return a ranking profile (a :class:ProfileWithTies) corresponding to the profile."""
276
+
277
+ return ProfileWithTies(
278
+ [g.ranking() for g in self._grades],
279
+ rcounts = self.gcounts,
280
+ candidates = self.candidates,
281
+ cmap = self.cmap
282
+ )
283
+
284
+ def write(self):
285
+ """Write the profile to a string."""
286
+
287
+ gprof_str = f"{len(self.candidates)};{self.num_voters};{self.grades}"
288
+ for g in self.grade_functions:
289
+ g_str = ''
290
+ for c in g.graded_candidates:
291
+ g_str += f"{c}:{g(c)},"
292
+ gprof_str += f";{g_str[0:-1]}"
293
+ return str(gprof_str)
294
+
295
+ @classmethod
296
+ def from_string(cls, gprof_str):
297
+ """
298
+ Returns a profile of utilities described by ``gprof_str``.
299
+
300
+ ``gprof_str`` must be in the format produced by the :meth:`pref_voting.GradeProfile.write` function.
301
+ """
302
+ gprof_data = gprof_str.split(";")
303
+
304
+ num_alternatives,num_voters,grades = int(gprof_data[0]),int(gprof_data[1]),gprof_data[2:]
305
+
306
+ grade_maps = [{int(cg.split(":")[0]): float(cg.split(":")[1]) for cg in gs.split(",")} if gs != '' else {} for gs in grades]
307
+
308
+ if len(grade_maps) != num_voters:
309
+ raise Exception("Number of voters does not match the number of utilities.")
310
+
311
+ return cls(grade_maps, candidates=range(num_alternatives))
312
+
313
+ def display(self, show_totals=False, average_median_ties=False):
314
+ """Display a grade profile as an ascii table (using tabulate). If ``show_totals`` is true then the sum, min, and max of the grades are displayed.
315
+
316
+ """
317
+
318
+ if show_totals:
319
+ sum_grade_fnc = self.sum_grade_function()
320
+ headers = [''] + self.gcounts + ["Sum", "Median"]
321
+ tbl = [[self.cmap[c]] + [self.gmap[g(c)] if g.has_grade(c) else "" for g in self._grades] + [sum_grade_fnc(c), self.median(c)] for c in self.candidates]
322
+
323
+ else:
324
+ headers = [''] + self.gcounts
325
+ tbl = [[self.cmap[c]] + [self.gmap[g(c)] if g.has_grade(c) else "" for g in self._grades] for c in self.candidates]
326
+ print(tabulate(tbl, headers=headers))
327
+
328
+ def visualize(self):
329
+ """Visualize a grade profile as a stacked bar plot."""
330
+ data_for_df = {
331
+ 'Candidate': [],
332
+ 'Grade': [],
333
+ 'Proportion': []
334
+ }
335
+
336
+ for c in self.candidates:
337
+ for g in [None] + self.grades:
338
+ data_for_df['Candidate'].append(self.cmap[c])
339
+ data_for_df['Grade'].append(self.gmap[g] if g is not None else "None")
340
+ data_for_df['Proportion'].append(self.proportion(c, g))
341
+ df = pd.DataFrame(data_for_df)
342
+
343
+ # Pivot the DataFrame to organize it for the stacked bar plot
344
+ df_pivot = df.pivot(index='Candidate', columns='Grade', values='Proportion')
345
+
346
+ df_pivot
347
+ # # Create the stacked bar plot
348
+ ax = df_pivot.plot(kind='barh', stacked=True, figsize=(10, 6), rot=0)
349
+ ax.set_ylabel('Candidate')
350
+ ax.set_xlabel('Proportion')
351
+ for spine in ['top', 'right']:
352
+ ax.spines[spine].set_visible(False)
353
+ # Show the plot
354
+
355
+ ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.1), ncol=len(self.grades) + 1, title="Grades")
356
+
357
+ plt.show()