Releases: mggg/VoteKit
v3.4.0
Added
STVAnimationmodule (optionalmanimdependency) for visualizing STV elections (PR #249):ColorPalettedataclass 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
SimultaneousVetoelection method with support for harmonic Borda scores (PR #333)SerialVetoelection class;PluralityVetoandSerialVetoare now subclasses of a sharedSequentialVetobase class (PR #325)- Schulze election method (PR #320, closes #318)
strictparameter toget_condorcet_cyclesfor detecting strict Condorcet cycles (PR #330, closes #327)FastIRVandFastSequentialRCVelection methods built onNumpyInnerSTVAlbanySTVelection method (closes #281)allow_zero_support_candidatesparameter toBlocSlateConfig, allowing candidates with zero support in preference intervals (PR #338, closes #298)- Missing docstrings for spatial ballot generator models (closes #328)
go-tasktask runner and updated contributing guide (PR #342, closes #277)
Changed
- Migrated package management to
uvand task running togo-task(PR #342, #352) - Replaced
black/isort/mypywithruff/tyfor 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=Noneis no longer permitted; default is now'first_place'with'lex'as backup- New
tiebreak_orderattribute for inspecting the tiebreak order - ~50-60x faster for deterministic tiebreaks by using
profile.dfdirectly
BlocSlateConfigrefactored to reduce nesting; error and warning strings are now dynamically formatted (PR #338, closes #299)get_preference_interval_for_bloc_and_slatenow only validates the target bloc/slate rather than the full config (PR #338)- STV restructured as a subpackage;
NumpyElectionrefactored into the abstract base classNumpyInnerSTV - Removed
ElectionCore,STVCore, andNumpyElectionclasses PreferenceProfile.to_csvnow 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 byfrom_csv(PR #361)BlockPluralitymoved out of theapprovalsubmodule and now accepts bothRankProfileandScoreProfileinputs, dispatching to the appropriate ranked or score-based implementation (PR #360, closes #351)RankingElectionbase 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_ballotto correctly handle short ballots (PR #348) - Fixed scoring functions for profiles where a ballot ranked more candidates than
max_ranking_lengthvia ties (PR #334) - Fixed a deletion desync in
BlocSlateConfig(PR #338) - Fixed
PluralityVetoto treat unranked candidates as tied for last place and thus eligible to be vetoed (PR #325) - Fixed
pairwise_dictto usecandidates_castinstead ofcandidates, preventing errors in elections likeRankedPairswhen a candidate received no votes (PR #361, closes #309) - Fixed flaky
BoostedRandomDictatorandRandomDictatortests (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 totmp_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 BlockPluralityNew 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=Noneis 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. IsNonewhentiebreak='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"] = ...v3.3.1
v3.3.0
[3.3.0] - 2025-10-02
Added
- Created a
BlocSlateConfigclass for ballot generators. This validates that all of the inputs to a ballot generator are valid. FastSTV, a new implementation of theSTVclass that relies more heavily on numpy and should be faster.RankBallotandRankProfile, andScoreBallotandScoreProfile, now allowing us to distinguish between the two types of input. The old
PreferenceProfileandBallotconstructors still work and create instances ofRankandScorefor you.RankedPairs,StarVotingandOpenList, all new election methods.
Changed
- refactored the codebase to use the new updates.
- deprecated the ballot generator classes in favor of functions.
PreferenceProfileand 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
v3.2.1
What's Changed
- removed the PreferenceProfile warning raised when
max_ranking_length>0but 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
[3.2.0] - 2025-06-13
Added
- created a
PreferenceProfile.dfattribute that is a pandasDataFramerepresentation 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_pickleandfrom_picklemethod toPreferenceProfile.
Changed
- separated the computation of the pairwise comparison dictionary from the pairwise comparison graph.
- renamed
dominating_tiersmethod of pairwise comparison graph toget_dominating_tiers. - renamed
cleaning.deduplicate_profiletocleaning.remove_repeated_candidates. - moved
remove_candfromutilstocleaning, and removed the older functionremove_noncands. - renamed
PreferenceProfile.condense_ballots()toPreferenceProfile.group_ballots(). - removed
Ballot.idattribute. - rewrote many cleaning functions and utilities to use the underlying dataframe of
PreferenceProfilefor speed improvements. - removed the use of
Fractionand use floats instead for speed improvement. - altered the various plotting functions for profiles, now in the
plots.profilesmodule.
Fixed
PreferenceProfile.group_ballots()now also groups thevoter_setattribute of ballots.
v3.1.0
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_dictatorandrandom_dictatorto 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_plotormulti_profile_STAT_plot, whereSTATcan befpv,borda,mentions,
andballot_lengths. Built on top of more generalbar_plotandmulti_bar_plotfunctions. - removed support for Python 3.9.
New Contributors
Full Changelog: v3.0.0...v3.1.0
v3.0.0
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, andBoostedRandomDictator. - More comprehensive tests for the
Ballotclass. - More comprehensive tests for all of the election types.
- More comprehensive tests for the
PreferenceProfileclass. - Tests for potential errors in the
BallotGeneratorclasses.
Changed
-
Moved the following election methods to sorted folders in
src/votekit/elections/election_types:approvalApprovalBlockPlurality
rankingRankingElectionAlaskaBoostedRandomDictatorBordaCondoBordaDominatingSetsPluralityVetoPluralitySNTVRandomDictator
scoresGeneralRatingLimitedRatingCumulative
-
Updated / added the following methods to
src/votekit/utils.py:ballots_by_first_candremove_candadd_missing_candsvalidate_score_vectorscore_profilefirst_place_votesmentionsborda_scorestie_broken_rankingscore_dict_to_rankingelect_cands_from_set_rankingexpand_tied_ballotresolve_profile_ties
-
Changed the way that the
ElectionStateclass 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
PreferenceProfileclass 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
PreferenceProfileclass to ensure that
the ballots and candidates are valid.
- Several validators have also been added to the
-
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
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
scaleparameter toballot_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_scottishaccordingly. #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
This update will break some of the backwards compatibility, so we have incremented the major version number.
Added
- A
PreferenceIntervalclass. - MCMC sampling for both
BradleyTerryballot generators. - Add print statement to
BallotGraphso 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 ofdominating_tiersandget_condorcet_cycles. - Added optional to_float method to
first_place_votesif users want to see them as floats instead of Fractions.
-Added aby_blocparameter togenerate_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
Cumulativeballot generator class. TheCumulativeclass 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
HighestScoreelection 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
Cumulativeelection class which is just a subclass ofHighestScorewith 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 forPreferenceProfilethat 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
shortPlackettLuceclass which allows you to generate ballots of arbitrary length in the style of PL. - Added tests for
__add__method ofPreferenceProfile. - Added
SlatePreferencemodel 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
STVclass so that the remaining candidates are always listed in order of current first place votes. -
Made
PlackettLucea subclass ofshortPlackettLuce. -
Change
PlackettLuce,BradleyTerry, andCumulativeballot generators to havename_prefix. This is in contrast to theslate_models we have introduced. -
Speed improvements for various ballot generators.
-
pref_interval_by_blocis nowpref_intervals_by_blocin 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_MDSwhich computes the coordinates, andplot_MDSwhich plots them. Made because the computation is the most time intensive.
Fixed
- Fixed an error in the
PreferenceProfiletail 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