Skip to content

Releases: mggg/VoteKit

v3.4.0

09 Apr 01:40

Choose a tag to compare

Added

  • STVAnimation module (optional manim dependency) for visualizing STV elections (PR #249):
    • ColorPalette dataclass for customizing candidate colors
    • Light/dark mode, user-specified fonts, and candidate nicknames
    • Automatic focus on elected candidates; condensed reporting for offscreen events
    • Inline rendering in interactive notebooks; explicit .save() method for writing to file
  • SimultaneousVeto election method with support for harmonic Borda scores (PR #333)
  • SerialVeto election class; PluralityVeto and SerialVeto are now subclasses of a shared SequentialVeto base class (PR #325)
  • Schulze election method (PR #320, closes #318)
  • strict parameter to get_condorcet_cycles for detecting strict Condorcet cycles (PR #330, closes #327)
  • FastIRV and FastSequentialRCV election methods built on NumpyInnerSTV
  • AlbanySTV election method (closes #281)
  • allow_zero_support_candidates parameter to BlocSlateConfig, allowing candidates with zero support in preference intervals (PR #338, closes #298)
  • Missing docstrings for spatial ballot generator models (closes #328)
  • go-task task runner and updated contributing guide (PR #342, closes #277)

Changed

  • Migrated package management to uv and task running to go-task (PR #342, #352)
  • Replaced black/isort/mypy with ruff/ty for formatting and type checking (PR #352)
  • Election class __init__ parameter names made more descriptive — see API Updates below (PR #355)
  • Major overhaul of PluralityVeto (PR #325):
    • tiebreak=None is no longer permitted; default is now 'first_place' with 'lex' as backup
    • New tiebreak_order attribute for inspecting the tiebreak order
    • ~50-60x faster for deterministic tiebreaks by using profile.df directly
  • BlocSlateConfig refactored to reduce nesting; error and warning strings are now dynamically formatted (PR #338, closes #299)
  • get_preference_interval_for_bloc_and_slate now only validates the target bloc/slate rather than the full config (PR #338)
  • STV restructured as a subpackage; NumpyElection refactored into the abstract base class NumpyInnerSTV
  • Removed ElectionCore, STVCore, and NumpyElection classes
  • PreferenceProfile.to_csv now encodes candidate names with integer IDs (e.g. (Aleine:0),(Alex:1)) instead of the previous shortened prefix strings (e.g. (Aleine:Alei),(Alex:Alex)). This avoids ambiguity when candidates share long common prefixes. Old prefix-format CSVs are still fully readable by from_csv (PR #361)
  • BlockPlurality moved out of the approval submodule and now accepts both RankProfile and ScoreProfile inputs, dispatching to the appropriate ranked or score-based implementation (PR #360, closes #351)
  • RankingElection base class now validates that the profile is non-empty, contains at least one ranked candidate, and has enough candidates who received votes to fill the requested seats (PR #360, closes #356)

Fixed

  • Fixed index_to_lexicographic_ballot to correctly handle short ballots (PR #348)
  • Fixed scoring functions for profiles where a ballot ranked more candidates than max_ranking_length via ties (PR #334)
  • Fixed a deletion desync in BlocSlateConfig (PR #338)
  • Fixed PluralityVeto to treat unranked candidates as tied for last place and thus eligible to be vetoed (PR #325)
  • Fixed pairwise_dict to use candidates_cast instead of candidates, preventing errors in elections like RankedPairs when a candidate received no votes (PR #361, closes #309)
  • Fixed flaky BoostedRandomDictator and RandomDictator tests (PR #361, closes #339)
  • Fixed broken link in readthedocs (PR #360, closes #350)
  • Pre-commit hooks now only run on staged files (PR #360)
  • Tests now run in parallel via pytest-xdist (-n auto). Bijection tests write to tmp_path
    instead of fixed data directories, and random seed usage is properly isolated.

API Updates

Deprecated: m renamed to n_seats across all election classes (PR #355)

The m keyword argument has been renamed to n_seats in every election class.
Using m still works but emits a DeprecationWarning and will be removed in a future version.

Affected classes: Approval, BlockPlurality, Borda, CondoBorda, SNTV, Plurality,
Alaska, BoostedRandomDictator, RandomDictator, RankedPairs, PluralityVeto,
SerialVeto, SimultaneousVeto, Schulze, STV, IRV, SequentialRCV, FastSTV,
AlbanySTV, FastIRV, FastSequentialRCV, GeneralRating, Rating, Limited, Cumulative

Also affects r_representation_score(m, ...)r_representation_score(n_seats, ...).

# Deprecated (still works, emits warning)
STV(profile, m=3)

# Recommended
STV(profile, n_seats=3)

Deprecated: GeneralRating parameter renames (PR #355)

Old names still accepted with a DeprecationWarning.

Old parameter New parameter Notes
m n_seats Number of seats
k per_candidate_limit Per-candidate score cap
(new) budget Total per-voter budget (was unnamed)

Deprecated: Cumulative and Limited parameter rename (PR #355)

Old names still accepted with a DeprecationWarning.

Old parameter New parameter
m n_seats
k budget

Breaking: BlocPlurality renamed to BlockPlurality (PR #355)

BlocPlurality still works but now raises a DeprecationWarning. Use BlockPlurality instead.

# Deprecated
from votekit.elections import BlocPlurality

# Use instead
from votekit.elections import BlockPlurality

New election classes

SimultaneousVeto (votekit.elections)

SimultaneousVeto(
    profile: RankProfile,
    n_seats: int = 1,
    candidate_weights: Literal["first_place", "uniform", "borda", "harmonic"]
                       | dict[str, float] | int = "first_place",
    tiebreak: Optional[str] = None,
    scoring_tie_convention: Literal["average", "high", "low"] = "low",
    allow_bullet_veto: bool = False,
)

SerialVeto (votekit.elections) — variant of PluralityVeto where a candidate with zero
score is only eliminated when a voter attempts to veto them (rather than being immediately removed).

Schulze (votekit.elections, closes #318)

Schulze(profile: RankProfile, n_seats: int = 1)

Changed: PluralityVeto signature and behavior (PR #325)

PluralityVeto(
    profile: RankProfile,
    n_seats: int = 1,
    tiebreak: Literal["first_place", "borda", "random", "lex"] = "first_place",
)
  • tiebreak=None is no longer accepted; 'first_place' is the default with 'lex' as automatic backup
    when first-place votes are tied.
  • New attribute tiebreak_order: Optional[tuple[frozenset[str]]] — the pre-computed ordering used to
    resolve last-place ties. Is None when tiebreak='random'.
  • Unranked candidates are now treated as tied for last place and are eligible to be vetoed (bug fix).
  • ~50–60x faster for deterministic tiebreaks on large profiles.

Changed: get_condorcet_cycles new strict parameter (PR #330, closes #327)

PairwiseComparisonGraph.get_condorcet_cycles(strict: bool = False) -> list[list[str]]

When strict=True, only strict wins (edge weight > 0) are considered; ties are excluded.
Defaults to False for backward compatibility.

New: BlocSlateConfig.allow_zero_support_candidates (PR #338, closes #298)

BlocSlateConfig(
    ...,
    allow_zero_support_candidates: bool = False,
)

When True, candidates may have zero support in a PreferenceInterval without raising an error.
Defaults to False, preserving existing behavior.

Changed: BlockPlurality now supports ranked profiles (PR #360, closes #351)

BlockPlurality has been moved out of the approval submodule. It now accepts either a
RankProfile or a ScoreProfile and dispatches to the appropriate implementation.

# Ranked profile — top `budget` candidates each receive 1 point
BlockPlurality(profile: RankProfile, n_seats=1, budget=None, tiebreak=None)

# Score profile — voters give at most 1 point to each of `budget` candidates
BlockPlurality(profile: ScoreProfile, n_seats=1, budget=None, tiebreak=None)

Changed: PreferenceProfile.to_csv candidate encoding and in-memory support (PR #361)

CSV files now use integer IDs for candidate labels instead of shortened name prefixes.
Old-format CSVs (with prefix labels) are still readable by from_csv.

# Old format
(Aleine:Alei),(Alex:Alex),(C:C)

# New format
(Aleine:0),(Alex:1),(C:2)

to_csv now defaults fpath to None. When no path is given, it returns the CSV content
as a string instead of writing to disk. This is useful for writing profiles into zip files
or other in-memory workflows without intermediate files.

# Write to disk (unchanged)
profile.to_csv("output.csv")

# Get CSV as a string
csv_str = profile.to_csv()

# Write multiple profiles into a zip
import zipfile, io
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
    for name, profile in profiles.items():
        zf.writestr(f"{name}.csv", profile.to_csv())

New: STVAnimation module (PR #249)

Requires the optional manim dependency (pip install votekit[manim]).

from votekit.animations import STVAnimation, ColorPalette, DARK_PALETTE, LIGHT_PALETTE

anim = STVAnimation(
    election: STV,
    title: Optional[str] = None,
    focus: set[str] | list[str] | Literal["winners", "viable", "all"] = ...
Read more

v3.3.1

25 Nov 13:35
c4d2e9b

Choose a tag to compare

[3.3.1] - 2025-11-24

Added

Changed

  • implemented speed improvements to sBT, sPL, and CS
  • refactored ballot generator tests

Fixed

  • fixed a floating point error in STV transfers (#311)
  • fixed a cohesion issue in Cambridge sampler (#313)

v3.3.0

02 Oct 17:59
66b72dc

Choose a tag to compare

[3.3.0] - 2025-10-02

Added

  • Created a BlocSlateConfig class for ballot generators. This validates that all of the inputs to a ballot generator are valid.
  • FastSTV, a new implementation of the STV class that relies more heavily on numpy and should be faster.
  • RankBallot and RankProfile, and ScoreBallot and ScoreProfile, now allowing us to distinguish between the two types of input. The old
    PreferenceProfile and Ballot constructors still work and create instances of Rank and Score for you.
  • RankedPairs, StarVoting and OpenList, all new election methods.

Changed

  • refactored the codebase to use the new updates.
  • deprecated the ballot generator classes in favor of functions.
  • PreferenceProfile and cvr imports can now use urls.

Fixed

  • Issues 252, 229, 258, 248, 164, 205, 269, 233, 169, 285, 282, 238, 303, 302

Huge thanks to @peterrrock2 , @WillithG , @ppeaung , @EdouardHeitzmann, @cjg266, @mvbbharath for their work!

v3.2.2

02 Jul 18:34
7c131bb

Choose a tag to compare

This minor patch fixes an error with the STV class. In edge cases where not enough candidates receive votes to pass threshold, the class was not properly counting the number of remaining candidates.

v3.2.1

18 Jun 12:49
51fe1da

Choose a tag to compare

What's Changed

  • removed the PreferenceProfile warning raised when max_ranking_length>0 but no rankings provided.
    We have decided an empty profile can still have positive max ranking length.
  • fixed an error raised by an empty ranking of ~ symbols being passed to utils.ballots_by_first_cand .

v3.2.0

13 Jun 18:17
dac379e

Choose a tag to compare

[3.2.0] - 2025-06-13

Added

  • created a PreferenceProfile.df attribute that is a pandas DataFrame representation of the profile. The df is in bijection with the profile, and using the df allows for great speed improvements throughout the codebase.
  • a new tutorial notebook replicating the Portland, OR election case study.
  • added a to_pickle and from_pickle method to PreferenceProfile.

Changed

  • separated the computation of the pairwise comparison dictionary from the pairwise comparison graph.
  • renamed dominating_tiers method of pairwise comparison graph to get_dominating_tiers.
  • renamed cleaning.deduplicate_profile to cleaning.remove_repeated_candidates.
  • moved remove_cand from utils to cleaning, and removed the older function remove_noncands.
  • renamed PreferenceProfile.condense_ballots() to PreferenceProfile.group_ballots().
  • removed Ballot.id attribute.
  • rewrote many cleaning functions and utilities to use the underlying dataframe of PreferenceProfile for speed improvements.
  • removed the use of Fraction and use floats instead for speed improvement.
  • altered the various plotting functions for profiles, now in the plots.profiles module.

Fixed

  • PreferenceProfile.group_ballots() now also groups the voter_set attribute of ballots.

v3.1.0

05 Mar 15:24
160e41e

Choose a tag to compare

This is a minor update to VoteKit that includes some new election analysis tools, some refactored visualization code for bar plots, and a minor tweak to how scores are computed from ranked ballots to allow for different averaging conventions for ties.

Added

  • added support for three types of averaging conventions within score_profile_from_rankings: average,
    low, and high.
  • r-representation scores: compute how "satisfied" voters are with a given winners set.
  • matrices: create three kinds of matrices based on profiles: boost, mentions, and average distance. Accompanying heatmap code that plots them.
  • support for Python 3.12, 3.13
  • contributing guidelines and community resources to our docs.

Changed

  • changed the default Borda scoring to use low averaging, where tied rankings receive the lowest possible
    points
  • changed the sampling method for boosted_random_dictator and random_dictator to more clearly
    use the first place votes distribution
  • changed the structure of plot_summary_stats. Is now split into many functions, all called
    profile_STAT_plot or multi_profile_STAT_plot, where STAT can be fpv, borda, mentions,
    and ballot_lengths. Built on top of more general bar_plot and multi_bar_plot functions.
  • removed support for Python 3.9.

New Contributors

Full Changelog: v3.0.0...v3.1.0

v3.0.0

15 Aug 16:07
addb0bf

Choose a tag to compare

This is a major update to VoteKit that includes some breaking changes for the API and a major refactoring of the code base to make it more organized, maintainable, and (hopefully) easier to contribute to in the future.

Added

  • The election methods thanks to @kevin-q2. The newest election methods are PluralityVeto,
    RandomDictator, and BoostedRandomDictator.
  • More comprehensive tests for the Ballot class.
  • More comprehensive tests for all of the election types.
  • More comprehensive tests for the PreferenceProfile class.
  • Tests for potential errors in the BallotGenerator classes.

Changed

  • Moved the following election methods to sorted folders in src/votekit/elections/election_types:

    • approval
      • Approval
      • BlockPlurality
    • ranking
      • RankingElection
      • Alaska
      • BoostedRandomDictator
      • Borda
      • CondoBorda
      • DominatingSets
      • PluralityVeto
      • Plurality
      • SNTV
      • RandomDictator
    • scores
      • GeneralRating
      • Limited
      • Rating
      • Cumulative
  • Updated / added the following methods to src/votekit/utils.py:

    • ballots_by_first_cand
    • remove_cand
    • add_missing_cands
    • validate_score_vector
    • score_profile
    • first_place_votes
    • mentions
    • borda_scores
    • tie_broken_ranking
    • score_dict_to_ranking
    • elect_cands_from_set_ranking
    • expand_tied_ballot
    • resolve_profile_ties
  • Changed the way that the ElectionState class operates. It now operates as a dataclass
    storing the following data:

    • round_number: The round number of the election.
    • remaining: The remaining candidates in the election that have not been elected.
    • elected: The set of candidates that have been elected.
    • eliminated: The set of candidates that have been eliminated.
    • tiebreak_winners: The set of candidates that were elected due to a tiebreak within a round.
    • scores: The scores for each candidate in the election.
  • The PreferenceProfile class is now a frozen dataclass with the idea being that, once the
    ballots and candidates for an election have been set, they should not be changed.

    • Several validators have also been added to the PreferenceProfile class to ensure that
      the ballots and candidates are valid.
  • Updated all documentation to reflect major changes in the API of the package.

New Contributors

  • @kevin-q2 made their first contribution in #130
  • @peterrrock2 made their first contribution in #151

Full Changelog: v2.0.1...v3.0.0

v2.0.1

11 Jul 17:10

Choose a tag to compare

This is a minor patch of votekit. It fixes minor bugs in the slate methods, adapts to the new csv format in the scot-elex repo, and updates some plotting functions.

Added

  • Created a read the docs page.
  • Add scale parameter to ballot_graph.draw() to allow for easier reading of text labels.
  • Allow users to choose which bloc is W/C in historical Cambridge data for CambridgeSampler.

Changed

  • Updated tutorial notebooks; larger focus on slate models, updated notebooks to match current codebase.
  • Removed the seq-RCV transfer rule since it is a dummy function, replaced with lambda function.
  • Update plot MDS to have aspect ratio 1, remove axes labels since they are meaningless in MDS.
  • Update all BLT files in scot-elex repo to be true CSV files, updated load_scottish accordingly. #123 #129

Fixed

  • Fixed bug by which slate-PlackettLuce could not generate ballots when some candidate had 0 support. #131
  • Updated various functions in the ballot generator module to only generate ballots for non-zero candidates.
  • Fixed one bloc s-BT pdf, which was incorrectly giving 0 weight to all ballot types.

Full Changelog: v2.0.0...v2.0.1

v2.0.0

01 Mar 23:39

Choose a tag to compare

This update will break some of the backwards compatibility, so we have incremented the major version number.

Added

  • A PreferenceInterval class.
  • MCMC sampling for both BradleyTerry ballot generators.
  • Add print statement to BallotGraph so that when you draw the graph without labels, it prints a dictionary of candidate labels for you.
  • Add an IRV election class, which is just a wrapper for STV with 1 seat.
  • Add default option to Borda election class, so users do not have to input a score vector if they want to use the traditional Borda vector.
  • Add several methods to PairwiseComparisonGraph. Added two boolean methods that return True if there is a condorcet winner or if there is a condorcet cycle. Added two get methods that return the winner or the cycles. Cached the results of dominating_tiers and get_condorcet_cycles.
  • Added optional to_float method to first_place_votes if users want to see them as floats instead of Fractions.
    -Added a by_bloc parameter to generate_profile. If True, this returns a tuple, the first entry of which is a dictionary of PreferenceProfiles by bloc. The second entry is the aggregated profile. This is very helpful for analyzing the behavior of a single bloc of voters. Defaults to False for backwards compatibility.
  • Created a Cumulative ballot generator class. The Cumulative class works like PL, but samples with replacement instead of without. The ranking order does not matter here, simply that candidates are listed on the ballot with multiplicity.
  • Created a HighestScore election class. This takes in a profile and a score vector, and returns the candidates with highest scores. There is a lot of flexibility in the score vector, so this class can run things like Borda, cumulative, etc.
  • Created a Cumulative election class which is just a subclass of HighestScore with the score vector set to all 1s. Thus anyone appearing on the ballot gets one point for each time they appear.
  • Wrote an __add__ method for PreferenceProfile that combines the ballot lists of two profiles.
  • Created utility functions to compute the winners of a profile given a score vector, as well as to validate a score vector (non-negative and non-increasing).
  • Created a shortPlackettLuce class which allows you to generate ballots of arbitrary length in the style of PL.
  • Added tests for __add__ method of PreferenceProfile.
  • Added SlatePreference model and tests.

Changed

  • Change the way the condense_ballots() method works in profiles. Rather than altering the original profile, it returns a new profile. This gives users the option to preserve the original profile.

  • Alter STV class so that the remaining candidates are always listed in order of current first place votes.

  • Made PlackettLuce a subclass of shortPlackettLuce.

  • Change PlackettLuce, BradleyTerry, and Cumulative ballot generators to have name_ prefix. This is in contrast to the slate_ models we have introduced.

  • Speed improvements for various ballot generators.

  • pref_interval_by_bloc is now pref_intervals_by_bloc in all ballot generators. This is now a dictionary of dictionaries, where the both sets of keys are the blocs, and the values of the sub-dictionaries are PreferenceInterval objects. The slate models require that we sample from the uncombined PreferenceInterval objects, while the name models require that we combine the PreferenceInterval objects using cohesion parameters.

  • MDS plot functionality, splitting it into compute_MDS which computes the coordinates, and plot_MDS which plots them. Made because the computation is the most time intensive.

Fixed

  • Fixed an error in the PreferenceProfile tail method.
  • Errors in bloc labeling in CambridgeSampler.

Deprecations

  • Deprecated PL and BT to name_PL and name_BT

Full Changelog: v1.1.1...v2.0.0