Source code for pref_voting.io.writers

"""
    File: writers.py
    Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
    Date: March 17, 2024

    Functions to write election data to a file.
"""

from pref_voting.rankings import Ranking
from pref_voting.profiles import Profile
from pref_voting.profiles_with_ties import ProfileWithTies
from preflibtools.instances import OrdinalInstance
import csv
import json
import numpy as np

[docs] def to_preflib_instance(profile): """ Returns an instance of the ``OrdinalInstance`` class from the ``preflibtools`` package (see https://preflib.github.io/preflibtools/usage.html#ordinal-preferences). Args: profile: A Profile or ProfileWithTies object. Returns: An instance of the ``OrdinalInstance`` class from preflibtools. """ assert type(profile) in [Profile, ProfileWithTies], "Must be a Profile or ProfileWithTies object to convert to a preflib OrdinalInstance." instance = OrdinalInstance() vote_map = dict() cand_to_cidx = {c: i for i, c in enumerate(profile.candidates)} cmap = {i: profile.cmap[c] for c, i in cand_to_cidx.items()} for r,c in zip(*profile.rankings_counts): 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]) if ranking in vote_map.keys(): vote_map[ranking] += c else: vote_map[ranking] = c instance.append_vote_map(vote_map) instance.alternatives_name = cmap return instance
[docs] def write_preflib(profile, filename): """ Write a profile to a file in the PrefLib format. Args: profile: A Profile or ProfileWithTies object. filename: The name of the file to write the profile to. Returns: The name of the file the profile was written to. """ assert type(profile) in [Profile, ProfileWithTies], "Must be a Profile or ProfileWithTies object to write in the preflib format." instance = to_preflib_instance(profile) preflib_type = instance.infer_type() instance.write(filename) if not filename.endswith(preflib_type): filename += f".{preflib_type}" print(f"Election written to {filename}.") return f"{filename}"
[docs] def write_csv(profile, filename, csv_format="candidate_columns"): """ Write a profile to a file in CSV format. Args: profile: A Profile or ProfileWithTies object. filename: The name of the file to write the profile to. csv_format: The format to write the profile in. Defaults to "candidate_columns". The other option is "rank_columns". """ assert type(profile) in [Profile, ProfileWithTies], "Must be a Profile or ProfileWithTies object to write in the csv format." candidates = profile.candidates if not filename.endswith(".csv"): filename += ".csv" if csv_format == "rank_columns": assert profile.is_truncated_linear, "The profile must be truncated linear to use the rank_columns csv format." ranks = range(1, len(candidates) + 1) with open(filename, mode='w') as file: writer = csv.writer(file) writer.writerow([f"Rank{_r}" for _r in ranks]) for indiff_list in profile.rankings_as_indifference_list: ranking = [str(profile.cmap[cs[0]]) for cs in indiff_list] writer.writerow(ranking if len(ranking) == len(candidates) else ranking + ["skipped"] * (len(candidates) - len(ranking))) print(f"Election written to {filename}.") return filename elif csv_format == "candidate_columns": prof = profile.to_profile_with_ties() if type(profile) == Profile else profile rs, cs = prof.rankings_counts anon_rankings = [] for r, count in zip(rs, cs): r.normalize_ranks() found_it = False for r_c in anon_rankings: if r_c[0] == r: found_it = True r_c[1] += count if not found_it: anon_rankings.append([r, count]) with open(filename, mode='w') as file: writer = csv.writer(file) writer.writerow([profile.cmap[c] for c in candidates] + ["#"]) for r,count in anon_rankings: writer.writerow([r.rmap[c] if r.is_ranked(c) else "" for c in candidates] + [count]) print(f"Election written to {filename}.") return filename
[docs] def write_json(profile, filename): """ Write a profile to a file in JSON format. Args: profile: A Profile or ProfileWithTies object. filename: The name of the file to write the profile to. Returns: The name of the file the profile was written to. """ assert type(profile) in [Profile, ProfileWithTies], "Cannot write to the abif format." if not filename.endswith(".json"): filename += ".json" prof = profile.to_profile_with_ties() if type(profile) == Profile else profile prof_as_dict = { "candidates": profile.candidates, "rankings": [{"ranking": { int(cand) if isinstance(cand, np.int64) else cand: int(rank) for cand,rank in r.rmap.items()}, "count": int(c)} for r,c in zip(*prof.rankings_counts)], "cmap": profile.cmap } with open(filename, "w") as f: json.dump(prof_as_dict, f) print(f"Election written to {filename}.") return filename
[docs] def write_abif(profile, filename): """ Write a profile to a file in ABIF format. The ABIF format is explained here: https://electowiki.org/wiki/ABIF. Args: profile: A Profile or ProfileWithTies object. filename: The name of the file to write the profile to. Returns: The name of the file the profile was written to. """ assert type(profile) in [Profile, ProfileWithTies], "Cannot write to the abif format." if not filename.endswith(".abif"): filename += ".abif" with open(filename, mode='w') as file: file.write(f"# {len(profile.candidates)} candidates\n") for c in profile.candidates: file.write(f"={c} : [{profile.cmap[c]}]\n") for r, c in zip(*profile.rankings_counts): indiff_list = r.to_indiff_list() if type(r) == Ranking else [(c,) for c in r] file.write(f"{c}:{'>'.join(['='.join([str(c) for c in cs]) for cs in indiff_list])}\n") print(f"Election written to {filename}.") return filename
def write_grade_profile_to_abif(profile): """ Write a profile to a file in ABIF format. Args: profile: A Profile object. """ pass
[docs] def write_spatial_profile_to_json(spatial_profile, filename): """ Write a spatial profile to a file in JSON format. Args: spatial_profile: A SpatialProfile object. Returns: The name of the file the spatial profile was written to. """ if not filename.endswith(".json"): filename += ".json" with open(filename, "w") as f: spatial_profile_dict = { "cand_names": spatial_profile.candidates, "voter_names": spatial_profile.voters, "candidates": {c: list(spatial_profile.candidate_position(c)) for c in spatial_profile.candidates}, "voters": {v: list(spatial_profile.voter_position(v)) for v in spatial_profile.voters} } json.dump(spatial_profile_dict, f) print(f"Spatial profile written to {filename}.") return filename
[docs] def write( edata, filename, file_format='preflib', csv_format="candidate_columns"): """ Write election data to ``filename`` in the format specified in ``file_format``. Args: edata: Election data to write. filename: The name of the file to write the election data to. file_format: The format to write the election data in. Defaults to "preflib". The other options are "csv", "json", and "abif". 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``. Returns: The name of the file the election data was written to. Note: 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). """ if file_format == 'preflib': return write_preflib(edata, filename) elif file_format == 'csv': return write_csv(edata, filename, csv_format=csv_format) elif file_format == 'json': return write_json(edata, filename) elif file_format == 'abif': return write_abif(edata, filename) else: raise ValueError(f"File format {file_format} not recognized.")