from .ballot import BallotSet
from .candidate import Candidate
from .values_dict import ValuesDict
[docs]class PreferenceSchedule:
"""A reduced preference schedule.
The :meth:`from_ballots` method can be useful for creating a preference
schedule from raw preference orderings:
>>> PreferenceSchedule.from_ballots([
... ("Amy", "Elizabeth", "Kirsten"),
... ("Amy", "Elizabeth", "Kirsten"),
... ("Amy", "Elizabeth", "Kirsten"),
... ("Kirsten", "Amy"),
... ("Elizabeth", "Kamala", "Kirsten"),
... ("Kamala", "Elizabeth"),
... ("Kamala", "Elizabeth"),
... ("Kamala", "Amy"),
... ("Kamala", "Amy"),
... ])
<PreferenceSchedule total_votes=9>
You can also create the :class:`~rcv.BallotSet` beforehand and pass it
directly to the constructor:
>>> ballots = BallotSet({
... (("Amy", "Elizabeth", "Kirsten"), 10),
... (("Kirsten", "Amy"), 20),
... (("Elizabeth", "Kamala", "Kirsten"), 15),
... (("Kamala", "Elizabeth"), 16),
... (("Kamala", "Amy"), 14),
... })
>>> PreferenceSchedule(ballots)
<PreferenceSchedule total_votes=75>
"""
def __init__(self, ballots, candidates=None):
"""
:param ballots: the :class:`~rcv.BallotSet` holding all of the valid
ballots cast in the election
:param candidates: optionally, the :class:`Candidates <~rcv.Candidate>`
up for election. If not provided, the candidates will be inferred
from the names on the ballots.
"""
if candidates is None:
names = {name for ballot, weight in ballots for name in ballot}
candidates = {Candidate(name) for name in names}
self.candidates = ValuesDict(
{str(candidate): candidate for candidate in candidates}
)
self.distribute_ballots(ballots)
self.total_votes = sum(candidate.total_votes for candidate in self.candidates)
def __repr__(self):
return "<{} total_votes={}>".format(
self.__class__.__name__, int(self.total_votes)
)
def __iter__(self):
return self.ballots
@property
def ballots(self):
for candidate in self.candidates:
for ballot in candidate.votes:
yield ballot
def remove_candidate(self, removed):
for candidate in self.candidates:
if candidate is not removed:
candidate.votes = candidate.votes.eliminate(removed)
removed.votes = BallotSet()
del self.candidates[str(removed)]
def distribute_ballots(self, ballots):
for ballot, weight in ballots:
if not ballot.is_empty:
candidate = self.candidates[ballot.top_choice]
candidate.votes.add(ballot, weight)
def eliminate(self, eliminated):
self.distribute_ballots(eliminated.votes.eliminate(eliminated))
self.remove_candidate(eliminated)
@classmethod
def from_ballots(cls, items):
return cls(BallotSet.from_items(items))
[docs] @classmethod
def from_dataframe(cls, df):
"""Create a preference schedule from a dataframe whose rows are ballots.
That is, the first column is the first-ranked candidate for each ballot,
the second is the second-ranked candidate, and so on.
The preference orders are cleaned using :func:`normalize_preferences`.
"""
if df.isna().any().any():
df = df.fillna(False)
grouped_ballots = df.groupby(list(df.columns)).size().items()
return cls(
BallotSet(
(
(normalize_preferences(ballot), count)
for ballot, count in grouped_ballots
)
)
)
def normalize_preferences(choices):
"""Removes duplicates and drops falsy values from a single preference order."""
new_choices = []
for choice in choices:
if choice and (choice not in new_choices):
new_choices.append(choice)
return new_choices