Skip to content

v3.4.0

Latest

Choose a tag to compare

@peterrrock2 peterrrock2 released this 09 Apr 01:40
· 4 commits to main since this release

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"] = "viable",
    nicknames: Optional[dict[str, str]] = None,
    candidate_colors: Optional[Mapping[str, ParsableManimColor]] = None,
    color_palette: ColorPalette = DARK_PALETTE,
    font: str = "",
)
anim.render()
anim.save(save_path)  # renders first if not already rendered

New Contributors

Full Changelog: v3.3.1...v3.4.0