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,432 @@
1
+ """
2
+ File: readers.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: March 17, 2024
5
+
6
+ Functions to write election data to a file.
7
+ """
8
+
9
+ from pref_voting.profiles import Profile
10
+ from pref_voting.profiles_with_ties import ProfileWithTies
11
+ from pref_voting.spatial_profiles import SpatialProfile
12
+ from preflibtools.instances import OrdinalInstance
13
+ import os
14
+ import csv
15
+ import pandas as pd
16
+ import json
17
+
18
+ def abif_to_profile(filename):
19
+ """
20
+ Open filename in the abif format and return a Profile object.
21
+
22
+ Args:
23
+ filename: The name of the file to read the profile from.
24
+
25
+ Returns:
26
+ A Profile object.
27
+
28
+ """
29
+
30
+ with open(filename, mode='r') as file:
31
+
32
+ lines = list(file.readlines())
33
+
34
+ cmap = {}
35
+ cand_to_indices = {}
36
+ cindx = 0
37
+ # create a candidate map
38
+ for line in lines:
39
+ if line.startswith("="):
40
+ _, cname = line[1:].strip().split(":")
41
+ cmap[cindx] = cname.strip().strip("[]")
42
+ cand_to_indices[cname.strip().strip("[]")] = cindx
43
+ cindx += 1
44
+
45
+ rankings = []
46
+ rcounts = []
47
+ for line in lines:
48
+ if line.startswith("#"):
49
+ # comment
50
+ continue
51
+ elif line.startswith("="):
52
+ # candidate line
53
+ continue
54
+ elif line.startswith("{"):
55
+ # metadata
56
+ continue
57
+ else:
58
+ # ranking line
59
+ count, ranking = line.strip().split(":")
60
+ count = int(count)
61
+ ranking = ranking.split(">")
62
+
63
+ assert not any(["=" in cs or "," in cs for cs in ranking]), "The election must contain linear orders on the candidates to create a Profile."
64
+
65
+ if len(cmap) == 0:
66
+ # no candidate map provided, so need to create one from the rankings
67
+ cmap = {cidx: str(sorted(ranking)[cidx].strip()) for cidx in range(len(sorted(ranking)))}
68
+ cand_to_indices = {c: i for i, c in cmap.items()}
69
+
70
+ r = list()
71
+ assert len(cmap) > 0 and len(ranking) == len(cmap), "The election must contain linear orders on the candidates to create a Profile."
72
+ for c in ranking:
73
+ assert len(cmap) > 0 and c in cand_to_indices.keys(), "Candidate found that is not in the candidate map."
74
+ r.append(cand_to_indices[c.strip()])
75
+ rankings.append(r)
76
+ rcounts.append(count)
77
+
78
+ return Profile(
79
+ rankings,
80
+ rcounts=rcounts,
81
+ cmap=cmap)
82
+
83
+
84
+ def abif_to_profile_with_ties(filename, cand_type=None):
85
+ """
86
+ Open filename in the abif format and return a ProfileWithTies object.
87
+
88
+ Args:
89
+ filename: The name of the file to read the profile from.
90
+
91
+ Returns:
92
+ A ProfileWithTies object.
93
+
94
+ """
95
+
96
+ import re
97
+
98
+ with open(filename, mode='r') as file:
99
+ lines = list(file.readlines())
100
+ rankings = []
101
+ rcounts = []
102
+ cmap = {}
103
+ for line in lines:
104
+ if line.startswith("#"):
105
+ # comment
106
+ continue
107
+ elif line.startswith("="):
108
+ # candidate line
109
+ cidx, cname = line[1:].strip().split(":")
110
+ cmap[cand_type(cidx.strip())
111
+ if cand_type is not None else cidx.strip()] = cname.strip().strip("[]")
112
+ elif line.startswith("{"):
113
+ # metadata
114
+ continue
115
+ else:
116
+ # ranking line
117
+ count, ranking = line.strip().split(":")
118
+ count = int(count)
119
+ ranking = ranking.split(">")
120
+ r = dict()
121
+ for ridx, cs in enumerate(ranking):
122
+ cands = re.split(r'[=,]', cs)
123
+ for c in cands:
124
+ if cand_type is not None:
125
+ r[cand_type(c.strip())] = ridx + 1
126
+ else:
127
+ r[c.strip()] = ridx + 1
128
+ rankings.append(r)
129
+ rcounts.append(count)
130
+
131
+ if len(cmap) == 0:
132
+ return ProfileWithTies(
133
+ rankings,
134
+ rcounts=rcounts)
135
+ else:
136
+ return ProfileWithTies(
137
+ rankings,
138
+ rcounts=rcounts,
139
+ candidates = sorted(list(cmap.keys())),
140
+ cmap = cmap)
141
+
142
+ def preflib_to_profile(
143
+ instance_or_preflib_file,
144
+ include_cmap=False,
145
+ use_cand_names=False,
146
+ as_linear_profile=False):
147
+
148
+ """
149
+ Read a profile from an OrdinalInstance or a .soc, .soi, .toc, or .toi file used by PrefLib (https://www.preflib.org/format#types).
150
+
151
+ This function uses the ``OrdinalInstance`` class from the ``preflibtools`` package to read the profile from the file (see https://preflib.github.io/preflibtools/usage.html#ordinal-preferences).
152
+
153
+ Args:
154
+ preflib_file (str): the path to the file
155
+ include_cmap (bool): if True, then include the candidate map. Defaults to False.
156
+ use_cand_names (bool): if True, then use the candidate map as the candidate names. Defaults to False.
157
+ as_linear_profile (bool): if True, then return a Profile object. Defaults to False. If False, then return a ProfileWithTies object.
158
+
159
+ Returns:
160
+ Profile or ProfileWithTies: the profile read from the file
161
+
162
+ """
163
+
164
+ assert type(instance_or_preflib_file) == OrdinalInstance or type(instance_or_preflib_file) == str, "The argument must be an instance of OrdinalInstance or a string."
165
+
166
+ if type(instance_or_preflib_file) == str:
167
+ preflib_file = instance_or_preflib_file
168
+
169
+ assert preflib_file.endswith(".soc") or preflib_file.endswith(".soi") or preflib_file.endswith(".toc") or preflib_file.endswith(".toi"), f"The file must be one of the file types from preflib: https://www.preflib.org/format#types, not {preflib_file}."
170
+
171
+ assert os.path.exists(preflib_file), f"The file {preflib_file} does not exist."
172
+
173
+ instance = OrdinalInstance()
174
+ instance.parse_file(preflib_file)
175
+
176
+ else:
177
+ instance = instance_or_preflib_file
178
+
179
+ rankings = []
180
+ rcounts = []
181
+ cmap = {c:str(c) for c in instance.alternatives_name.keys()}
182
+
183
+ if not as_linear_profile:
184
+
185
+ for order in instance.orders:
186
+ rank = dict()
187
+ for r,cs in enumerate(order):
188
+ for c in cs:
189
+ if not use_cand_names:
190
+ rank[c] = r + 1
191
+ else:
192
+ rank[instance.alternatives_name[c]] = r + 1
193
+ if include_cmap:
194
+ if use_cand_names:
195
+ cmap[instance.alternatives_name[c]] = instance.alternatives_name[c]
196
+ else:
197
+ cmap[c] = instance.alternatives_name[c]
198
+
199
+ rankings.append(rank)
200
+ rcounts.append(instance.multiplicity[order])
201
+
202
+ return ProfileWithTies(rankings,
203
+ rcounts=rcounts,
204
+ cmap=cmap)
205
+
206
+ elif as_linear_profile:
207
+
208
+ cand_to_cidx = {c:cidx
209
+ for cidx,c in enumerate(sorted(list(instance.alternatives_name.keys())))}
210
+
211
+ for order in instance.orders:
212
+ rank = list()
213
+ cmap = {c:str(c) for c in range(instance.num_alternatives)}
214
+ for _,cs in enumerate(order):
215
+ for c in cs:
216
+ rank.append(cand_to_cidx[c])
217
+ if include_cmap:
218
+ cmap[cand_to_cidx[c]] = instance.alternatives_name[c]
219
+ rankings.append(rank)
220
+ rcounts.append(instance.multiplicity[order])
221
+
222
+ return Profile(rankings,
223
+ rcounts=rcounts,
224
+ cmap=cmap)
225
+
226
+ def csv_to_profile(
227
+ filename,
228
+ csv_format="candidate_columns",
229
+ as_linear_profile=False,
230
+ items_to_skip=None,
231
+ cand_type=None):
232
+ """
233
+ Read a profile from a csv file.
234
+
235
+ Args:
236
+ filename (str): the path to the file
237
+ csv_format (str): the format of the csv file. Defaults to "candidate_columns". The other option is "rank_columns".
238
+ as_linear_profile (bool): if True, then return a Profile object. Defaults to False. If False, then return a ProfileWithTies object.
239
+ items_to_skip (list[str]): a list of items to skip. Defaults to None. Items in this list are not included in the profile. Only relevant for "rank_columns" csv format.
240
+
241
+ Returns:
242
+ Profile or ProfileWithTies: the profile read from the file
243
+
244
+ Note:
245
+ There are two formats for the csv file: "rank_columns" and "candidate_columns". The "rank_columns" format is used when the csv file contains a column for each rank and the rows are the candidates at that rank (or "skipped" if the ranked is skipped). The "candidate_columns" format is used when the csv file contains a column for each candidate and the rows are the rank of the candidates (or the empty string if the candidate is not ranked).
246
+ """
247
+
248
+ if csv_format == "rank_columns":
249
+ df = pd.read_csv(filename, low_memory=False)
250
+ items_to_skip = items_to_skip if items_to_skip is not None else ["skipped"]
251
+ ranks = []
252
+ rank_columns = [col for col in df.columns if col.startswith('rank') or col.startswith('Rank')]
253
+
254
+ # Get unique values from these columns, excluding 'skipped'
255
+ cand_names = pd.unique(df[rank_columns].values.ravel('K'))
256
+ cand_names = [str(value) for value in cand_names if value not in items_to_skip]
257
+
258
+ if 'writein' in cand_names:
259
+ cands = list(set([c for c in sorted(cand_names) if c != 'writein'])) + ['writein']
260
+ else:
261
+ cands = sorted(list(set(cand_names)))
262
+ if len(cands) == 0:
263
+ print("No candidates found in file", filename)
264
+ cmap = {cidx: c for cidx,c in enumerate(cands)}
265
+ cand_to_cidx = {c:cidx for cidx,c in enumerate(cands)}
266
+
267
+ rank_str_to_rank = lambda rank_str: int(rank_str[4:].strip())
268
+ for _, row in df.iterrows():
269
+ ballot_dict = {}
270
+ for rank in rank_columns:
271
+ candidate = str(row[rank])
272
+ if candidate not in items_to_skip:
273
+ ballot_dict[cand_to_cidx[candidate]] = rank_str_to_rank(rank)
274
+
275
+ ballot_dict = {cand_type(c) if cand_type is not None else c:r
276
+ for c,r in ballot_dict.items()}
277
+ ranks.append(ballot_dict)
278
+ cmap = {cand_to_cidx[c]:str(c) for c in cands}
279
+ prof = ProfileWithTies(ranks, cmap=cmap)
280
+ if as_linear_profile:
281
+ prof = prof.to_linear_profile()
282
+ assert prof is not None, "The profile could not be converted to a Profile."
283
+ return prof
284
+
285
+ elif csv_format == "candidate_columns":
286
+ with open(filename, mode='r') as file:
287
+ reader = csv.reader(file)
288
+ header = next(reader)
289
+ candidates = header[:-1]
290
+ rankings = list()
291
+ rcounts = list()
292
+ for row in reader:
293
+ ranks = [int(r) if r != "" else None for r in row[:-1]]
294
+ count = int(row[-1])
295
+ ranking = {cand_type(c)
296
+ if cand_type is not None else c:r
297
+ for c,r in zip(candidates, ranks)
298
+ if r is not None}
299
+ rankings.append(ranking)
300
+ rcounts.append(count)
301
+
302
+ prof = ProfileWithTies(rankings,
303
+ rcounts=rcounts,
304
+ cmap={cand_type(c)
305
+ if cand_type is not None else str(c):str(c)
306
+ for c in candidates})
307
+ if as_linear_profile:
308
+ prof = prof.to_linear_profile()
309
+ assert prof is not None, "The profile could not be converted to a Profile."
310
+ return prof
311
+
312
+
313
+ # helper function for json_to_profile
314
+ def _convert_key_type(key, lst):
315
+ for c in lst:
316
+ try:
317
+ # Attempt to convert the key to the same type as the candidate
318
+ if type(c)(key) == c:
319
+ return type(c)(key)
320
+ except ValueError:
321
+ continue
322
+ # Return the original key if no conversion is successful
323
+ return key
324
+
325
+
326
+ def json_to_profile(filename, cand_type=None, as_linear_profile=False):
327
+ """
328
+ Read a profile from a json file.
329
+
330
+ Args:
331
+ filename (str): the path to the file
332
+ cand_type (type): the type of the candidates. Defaults to None. If not None, then the candidates are converted to this type.
333
+ as_linear_profile (bool): if True, then return a Profile object. Defaults to False. If False, then return a ProfileWithTies object.
334
+
335
+ Returns:
336
+ Profile or ProfileWithTies: the profile read from the file
337
+ """
338
+ with open(filename, mode='r') as file:
339
+ data = json.load(file)
340
+ candidates = data["candidates"]
341
+ cmap = {_convert_key_type(c, candidates): c_str for c, c_str in data["cmap"].items()}
342
+
343
+ if cand_type is not None:
344
+ cmap = {cand_type(c):str(c_str) for c,c_str in cmap.items()}
345
+ candidates = [cand_type(c) for c in candidates]
346
+
347
+ rankings = []
348
+ rcounts = []
349
+ for r_data in data["rankings"]:
350
+ rank = {cand_type(c) if cand_type is not None else _convert_key_type(c, candidates):int(r) for c,r in r_data["ranking"].items()}
351
+ rankings.append(rank)
352
+ rcounts.append(int(r_data["count"]))
353
+
354
+ if as_linear_profile:
355
+ prof = ProfileWithTies(rankings,
356
+ rcounts=rcounts,
357
+ candidates=candidates,
358
+ cmap=cmap)
359
+
360
+ prof = prof.to_linear_profile()
361
+ assert prof is not None, "The profile could not be converted to a Profile."
362
+ else:
363
+ prof = ProfileWithTies(rankings,
364
+ rcounts=rcounts,
365
+ candidates=candidates,
366
+ cmap=cmap)
367
+ return prof
368
+
369
+ def read(filename,
370
+ file_format,
371
+ as_linear_profile=False,
372
+ cand_type=None,
373
+ csv_format="candidate_columns",
374
+ items_to_skip=None):
375
+ """
376
+ Read election data from ``filename`` in the format ``file_format``.
377
+
378
+ Args:
379
+ filename (str): the path to the file
380
+ file_format (str): the format of the file. The options are "preflib", "json", "csv", and "abif".
381
+ as_linear_profile (bool): if True, then return a Profile object. Defaults to False. If False, then return a ProfileWithTies object.
382
+ cand_type (type): the type of the candidates. Defaults to None. If not None, then the candidates are converted to this type.
383
+ csv_format (str): the format of the csv file. Defaults to "candidate_columns". The other option is "rank_columns".
384
+ items_to_skip (list[str]): a list of items to skip. Defaults to None. Items in this list are not included in the profile. Only relevant for "rank_columns" csv format.
385
+
386
+ Returns:
387
+ Profile or ProfileWithTies: the profile read from the file
388
+ """
389
+ if file_format == "abif":
390
+ if as_linear_profile:
391
+ return abif_to_profile(
392
+ filename)
393
+ else:
394
+ return abif_to_profile_with_ties(
395
+ filename,
396
+ cand_type=cand_type)
397
+ elif file_format == "json":
398
+ return json_to_profile(
399
+ filename,
400
+ cand_type=cand_type,
401
+ as_linear_profile=as_linear_profile)
402
+ elif file_format == "csv":
403
+ return csv_to_profile(
404
+ filename,
405
+ as_linear_profile=as_linear_profile,
406
+ cand_type=cand_type,
407
+ csv_format=csv_format,
408
+ items_to_skip=items_to_skip)
409
+ elif file_format == "preflib":
410
+ return preflib_to_profile(filename, as_linear_profile=as_linear_profile)
411
+ else:
412
+ raise ValueError(f"File format {file_format} not recognized.")
413
+
414
+ def json_to_spatial_profile(filename):
415
+ """
416
+ Load a spatial profile from a JSON file.
417
+
418
+ Args:
419
+ filename (str): the path to the file
420
+
421
+ Returns:
422
+ SpatialProfile: the spatial profile read from the file
423
+ """
424
+
425
+ with open(filename, "r") as f:
426
+ spatial_profile_dict = json.load(f)
427
+ candidates = spatial_profile_dict["cand_names"]
428
+ voters = spatial_profile_dict["voter_names"]
429
+ return SpatialProfile(
430
+ {_convert_key_type(c, candidates):c_pos for c,c_pos in spatial_profile_dict["candidates"].items()},
431
+ {_convert_key_type(v, voters):v_pos for v,v_pos in spatial_profile_dict["voters"].items()}
432
+ )
@@ -0,0 +1,256 @@
1
+ """
2
+ File: writers.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: March 17, 2024
5
+
6
+ Functions to write election data to a file.
7
+ """
8
+
9
+ from pref_voting.rankings import Ranking
10
+ from pref_voting.profiles import Profile
11
+ from pref_voting.profiles_with_ties import ProfileWithTies
12
+ from preflibtools.instances import OrdinalInstance
13
+ import csv
14
+ import json
15
+ import numpy as np
16
+
17
+ def to_preflib_instance(profile):
18
+ """
19
+ Returns an instance of the ``OrdinalInstance`` class from the ``preflibtools`` package (see https://preflib.github.io/preflibtools/usage.html#ordinal-preferences).
20
+
21
+ Args:
22
+ profile: A Profile or ProfileWithTies object.
23
+
24
+ Returns:
25
+ An instance of the ``OrdinalInstance`` class from preflibtools.
26
+
27
+ """
28
+ assert type(profile) in [Profile, ProfileWithTies], "Must be a Profile or ProfileWithTies object to convert to a preflib OrdinalInstance."
29
+
30
+ instance = OrdinalInstance()
31
+ vote_map = dict()
32
+ cand_to_cidx = {c: i for i, c in enumerate(profile.candidates)}
33
+ cmap = {i: profile.cmap[c] for c, i in cand_to_cidx.items()}
34
+ for r,c in zip(*profile.rankings_counts):
35
+ ranking = tuple([tuple([cand_to_cidx[_c] for _c in indiff]) for indiff in r.to_indiff_list()]) if type(r) == Ranking else tuple([(c,) for c in r])
36
+ if ranking in vote_map.keys():
37
+ vote_map[ranking] += c
38
+ else:
39
+ vote_map[ranking] = c
40
+ instance.append_vote_map(vote_map)
41
+ instance.alternatives_name = cmap
42
+ return instance
43
+
44
+ def write_preflib(profile, filename):
45
+ """
46
+ Write a profile to a file in the PrefLib format.
47
+
48
+ Args:
49
+ profile: A Profile or ProfileWithTies object.
50
+ filename: The name of the file to write the profile to.
51
+
52
+ Returns:
53
+ The name of the file the profile was written to.
54
+ """
55
+ assert type(profile) in [Profile, ProfileWithTies], "Must be a Profile or ProfileWithTies object to write in the preflib format."
56
+
57
+ instance = to_preflib_instance(profile)
58
+ preflib_type = instance.infer_type()
59
+ instance.write(filename)
60
+
61
+ if not filename.endswith(preflib_type):
62
+ filename += f".{preflib_type}"
63
+
64
+ print(f"Election written to {filename}.")
65
+ return f"{filename}"
66
+
67
+ def write_csv(profile, filename, csv_format="candidate_columns"):
68
+ """
69
+ Write a profile to a file in CSV format.
70
+
71
+ Args:
72
+ profile: A Profile or ProfileWithTies object.
73
+ filename: The name of the file to write the profile to.
74
+ csv_format: The format to write the profile in. Defaults to "candidate_columns". The other option is "rank_columns".
75
+ """
76
+ assert type(profile) in [Profile, ProfileWithTies], "Must be a Profile or ProfileWithTies object to write in the csv format."
77
+
78
+ candidates = profile.candidates
79
+
80
+ if not filename.endswith(".csv"):
81
+ filename += ".csv"
82
+
83
+ if csv_format == "rank_columns":
84
+
85
+ assert profile.is_truncated_linear, "The profile must be truncated linear to use the rank_columns csv format."
86
+
87
+ ranks = range(1, len(candidates) + 1)
88
+ with open(filename, mode='w') as file:
89
+ writer = csv.writer(file)
90
+ writer.writerow([f"Rank{_r}" for _r in ranks])
91
+ for indiff_list in profile.rankings_as_indifference_list:
92
+ ranking = [str(profile.cmap[cs[0]]) for cs in indiff_list]
93
+ writer.writerow(ranking if len(ranking) == len(candidates) else ranking + ["skipped"] * (len(candidates) - len(ranking)))
94
+
95
+ print(f"Election written to {filename}.")
96
+
97
+ return filename
98
+
99
+ elif csv_format == "candidate_columns":
100
+
101
+ prof = profile.to_profile_with_ties() if type(profile) == Profile else profile
102
+
103
+ rs, cs = prof.rankings_counts
104
+ anon_rankings = []
105
+ for r, count in zip(rs, cs):
106
+ r.normalize_ranks()
107
+ found_it = False
108
+ for r_c in anon_rankings:
109
+ if r_c[0] == r:
110
+ found_it = True
111
+ r_c[1] += count
112
+ if not found_it:
113
+ anon_rankings.append([r, count])
114
+
115
+ with open(filename, mode='w') as file:
116
+ writer = csv.writer(file)
117
+ writer.writerow([profile.cmap[c] for c in candidates] + ["#"])
118
+ for r,count in anon_rankings:
119
+ writer.writerow([r.rmap[c] if r.is_ranked(c) else "" for c in candidates] + [count])
120
+
121
+ print(f"Election written to {filename}.")
122
+
123
+ return filename
124
+
125
+ def write_json(profile, filename):
126
+ """
127
+ Write a profile to a file in JSON format.
128
+
129
+ Args:
130
+ profile: A Profile or ProfileWithTies object.
131
+ filename: The name of the file to write the profile to.
132
+
133
+ Returns:
134
+ The name of the file the profile was written to.
135
+ """
136
+ assert type(profile) in [Profile, ProfileWithTies], "Cannot write to the abif format."
137
+
138
+ if not filename.endswith(".json"):
139
+ filename += ".json"
140
+
141
+ prof = profile.to_profile_with_ties() if type(profile) == Profile else profile
142
+
143
+ prof_as_dict = {
144
+ "candidates": profile.candidates,
145
+ "rankings": [{"ranking": {
146
+ int(cand) if isinstance(cand, np.int64) else cand: int(rank)
147
+ for cand,rank in r.rmap.items()},
148
+ "count": int(c)}
149
+ for r,c in zip(*prof.rankings_counts)],
150
+ "cmap": profile.cmap
151
+ }
152
+ with open(filename, "w") as f:
153
+ json.dump(prof_as_dict, f)
154
+
155
+ print(f"Election written to {filename}.")
156
+ return filename
157
+
158
+ def write_abif(profile, filename):
159
+ """
160
+ Write a profile to a file in ABIF format.
161
+
162
+ The ABIF format is explained here: https://electowiki.org/wiki/ABIF.
163
+
164
+ Args:
165
+ profile: A Profile or ProfileWithTies object.
166
+ filename: The name of the file to write the profile to.
167
+
168
+ Returns:
169
+ The name of the file the profile was written to.
170
+ """
171
+
172
+ assert type(profile) in [Profile, ProfileWithTies], "Cannot write to the abif format."
173
+
174
+ if not filename.endswith(".abif"):
175
+ filename += ".abif"
176
+
177
+ with open(filename, mode='w') as file:
178
+ file.write(f"# {len(profile.candidates)} candidates\n")
179
+ for c in profile.candidates:
180
+ file.write(f"={c} : [{profile.cmap[c]}]\n")
181
+ for r, c in zip(*profile.rankings_counts):
182
+ indiff_list = r.to_indiff_list() if type(r) == Ranking else [(c,) for c in r]
183
+ file.write(f"{c}:{'>'.join(['='.join([str(c) for c in cs]) for cs in indiff_list])}\n")
184
+
185
+ print(f"Election written to {filename}.")
186
+
187
+ return filename
188
+
189
+ def write_grade_profile_to_abif(profile):
190
+ """
191
+ Write a profile to a file in ABIF format.
192
+
193
+ Args:
194
+ profile: A Profile object.
195
+ """
196
+ pass
197
+
198
+ def write_spatial_profile_to_json(spatial_profile, filename):
199
+ """
200
+ Write a spatial profile to a file in JSON format.
201
+
202
+ Args:
203
+ spatial_profile: A SpatialProfile object.
204
+
205
+ Returns:
206
+ The name of the file the spatial profile was written to.
207
+ """
208
+
209
+ if not filename.endswith(".json"):
210
+ filename += ".json"
211
+
212
+ with open(filename, "w") as f:
213
+ spatial_profile_dict = {
214
+ "cand_names": spatial_profile.candidates,
215
+ "voter_names": spatial_profile.voters,
216
+ "candidates": {c: list(spatial_profile.candidate_position(c)) for c in spatial_profile.candidates},
217
+ "voters": {v: list(spatial_profile.voter_position(v)) for v in spatial_profile.voters}
218
+ }
219
+ json.dump(spatial_profile_dict, f)
220
+
221
+ print(f"Spatial profile written to {filename}.")
222
+
223
+ return filename
224
+
225
+ def write(
226
+ edata,
227
+ filename,
228
+ file_format='preflib',
229
+ csv_format="candidate_columns"):
230
+ """
231
+ Write election data to ``filename`` in the format specified in ``file_format``.
232
+
233
+ Args:
234
+ edata: Election data to write.
235
+ filename: The name of the file to write the election data to.
236
+ file_format: The format to write the election data in. Defaults to "preflib". The other options are "csv", "json", and "abif".
237
+ csv_format: The format to write the election data in if the file format is "csv". Defaults to 'candidate_columns'. The other option is ``rank_columns``.
238
+
239
+ Returns:
240
+ The name of the file the election data was written to.
241
+
242
+ Note:
243
+ There are two formats for the csv file: "rank_columns" and "candidate_columns". The "rank_columns" format is used when the csv file contains a column for each rank and the rows are the candidates at that rank (or "skipped" if the ranked is skipped). The "candidate_columns" format is used when the csv file contains a column for each candidate and the rows are the rank of the candidates (or the empty string if the candidate is not ranked).
244
+
245
+ """
246
+
247
+ if file_format == 'preflib':
248
+ return write_preflib(edata, filename)
249
+ elif file_format == 'csv':
250
+ return write_csv(edata, filename, csv_format=csv_format)
251
+ elif file_format == 'json':
252
+ return write_json(edata, filename)
253
+ elif file_format == 'abif':
254
+ return write_abif(edata, filename)
255
+ else:
256
+ raise ValueError(f"File format {file_format} not recognized.")