Source code for pref_voting.interfaces.abcvoting_interface
"""
File: abcvoting_interface.py
Author: Eric Pacuit (epacuit@umd.edu) and Wes Holliday (wesholliday@berkeley.edu)
Functions to convert between pref_voting grade profiles and abcvoting profiles
(https://abcvoting.readthedocs.io).
abcvoting is an optional dependency of pref_voting: it is only imported when one
of the functions in this module is called. Install it with
``uv pip install "pref_voting[abcvoting]"``.
abcvoting profiles consist of approval ballots, i.e., dichotomous ballots. The
corresponding pref_voting object is a :class:`~pref_voting.grade_profiles.GradeProfile`
with grades 0 and 1 (as produced, e.g., by
:meth:`~pref_voting.utility_profiles.UtilityProfile.to_approval_profile`).
A candidate is approved by a voter if the voter assigns the candidate the grade 1.
abcvoting identifies candidates with the integers ``0, ..., num_cand - 1``; the
conversion functions preserve candidate names via the ``cmap`` of the grade
profile and the ``cand_names`` of the abcvoting profile. abcvoting voters carry
weights: the ``gcounts`` of a grade profile become voter weights, and integer
voter weights become ``gcounts`` (fractional weights raise a ``ValueError``
unless ``scale_weights=True`` is passed).
"""
from pref_voting.grade_profiles import GradeProfile
from pref_voting.interfaces._utils import weights_to_counts
ABCVOTING_IMPORT_ERROR = (
"abcvoting is required for this function. Install it with: "
'uv pip install "pref_voting[abcvoting]" (or pip install "pref_voting[abcvoting]").'
)
def _abcvoting_classes():
try:
from abcvoting.preferences import Profile as AbcvotingProfile, Voter
except ImportError as err:
raise ImportError(ABCVOTING_IMPORT_ERROR) from err
return AbcvotingProfile, Voter
[docs]
def to_abcvoting_profile(gprofile):
"""Convert a :class:`GradeProfile` with grades 0 and 1 to an abcvoting
``Profile``.
The candidates ``0, ..., num_cand - 1`` of the abcvoting profile are the
indices of the (sorted) candidates of the grade profile, and the candidate
names of the abcvoting profile are given by the grade profile's ``cmap``.
Each ballot type contributes one abcvoting voter, whose approval set is the
set of candidates assigned the grade 1 and whose weight is the ballot count.
Use ``abcvoting.preferences.Profile.convert_to_unit_weights()`` on the
result if unit weights are needed.
:param gprofile: A grade profile with grades 0 and 1.
:type gprofile: GradeProfile
:returns: An abcvoting ``Profile``.
"""
AbcvotingProfile, Voter = _abcvoting_classes()
if sorted(gprofile.grades) != [0, 1]:
raise ValueError(
"Only grade profiles with grades 0 and 1 (approval ballots) can be "
"converted to an abcvoting profile. See, e.g., "
"UtilityProfile.to_approval_profile and "
"UtilityProfile.to_k_approval_profile for ways to produce such a "
"profile."
)
cand_to_index = {c: i for i, c in enumerate(gprofile.candidates)}
abc_profile = AbcvotingProfile(
gprofile.num_cands,
cand_names=[str(gprofile.cmap[c]) for c in gprofile.candidates],
)
for grade_fnc, count in zip(*gprofile.grades_counts):
approved = [
cand_to_index[c] for c in grade_fnc.graded_candidates if grade_fnc(c) == 1
]
abc_profile.add_voter(Voter(approved, weight=int(count), num_cand=gprofile.num_cands))
return abc_profile
[docs]
def abcvoting_to_grade_profile(abc_profile, scale_weights=False):
"""Convert an abcvoting ``Profile`` to a :class:`GradeProfile` with grades
0 and 1.
The candidates of the grade profile are the integers
``0, ..., num_cand - 1``, with the ``cand_names`` of the abcvoting profile
as the ``cmap``. Each voter contributes one ballot assigning the grade 1
to each approved candidate and the grade 0 to every other candidate, with
the voter's weight as the ballot count.
:param abc_profile: An abcvoting ``Profile``.
:param scale_weights: If True, fractional voter weights are scaled to integers by clearing denominators; otherwise fractional weights raise a ``ValueError``.
:type scale_weights: bool, optional
:returns: A :class:`GradeProfile` with grades 0 and 1.
"""
candidates = list(range(abc_profile.num_cand))
grade_maps = []
weights = []
for voter in abc_profile:
grade_maps.append(
{c: 1 if c in voter.approved else 0 for c in candidates}
)
weights.append(voter.weight)
gcounts = weights_to_counts(weights, scale_weights=scale_weights, what="voter")
return GradeProfile(
grade_maps,
[0, 1],
gcounts=gcounts,
candidates=candidates,
cmap={c: str(name) for c, name in enumerate(abc_profile.cand_names)},
)