Skip to content

Feat/remove ballots with cand - #389

Open
graceg571 wants to merge 4 commits into
mainfrom
feat/remove-ballots-with-cand
Open

Feat/remove ballots with cand#389
graceg571 wants to merge 4 commits into
mainfrom
feat/remove-ballots-with-cand

Conversation

@graceg571

@graceg571 graceg571 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes #381

Summary

remove_ballots_with_cand_rank_profile is added as a cleaning function to remove ballots containing a certain candidate or one from a set of candidates. Currently, specific candidate(s) can be removed from a ballot, but this supports workflows where ballots with an invalid write-in marker like "overvote" or "undervote" can be removed entirely.

Changes

  • remove_ballots_with_cand_rank_profile is added to rank_profiles_cleaning.py. The ballots are removed using the candidates' integer IDs within internal df. The removed ballots are considered empty and recorded in no_rank_altr_idxs of the returned CleanedRankProfile.
  • no_rank_altr_idxs is modified to include ballots where all rank slots are frozsenset() or frozenset("~"). This accounts for ballots that are now empty after cleaning by removing candidates and replacing them with empty frozensets.
  • unaltr_idxs are removed from no_rank_altr_idxs to correctly distinguish which ballots were already empty and are empty after cleaning
  • is_equiv_to_condensed updated to return True if no frozenset() in ranking or only trailing ones alongside any frozenset("~").
  • is_equiv_for_remove_and_condense updated to use is_equiv_to_condensed

Testing

  • test_remove_ballots_with_cand_rank_profile.py added with tests for ballots with removed candidates with and without ties
  • test_clean_ranked_profile.py and test_remove_cand_ranked_profile.py updated to account for the changes to no_rank_altr_idxs and unaltr_idxs now including originally empty ballots
  • test_condense_ranked_profile updated to account for changes to is_equiv_to_condensed to include originally empty ballots

@graceg571 graceg571 added this to the 3.6.0 milestone Aug 17, 2026
@graceg571
graceg571 requested a review from peterrrock2 August 17, 2026 12:30
@graceg571 graceg571 self-assigned this Aug 17, 2026
@graceg571 graceg571 removed this from the 3.6.0 milestone Aug 17, 2026

@peterrrock2 peterrrock2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good so far!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixtures
Test all attrs for CleanProfile independently
Test chaining
Test idempotent

Comment on lines +47 to +50
no_rank_altr_idxs = {
idx for idx, c in zip(idxs, cleaned_rows) if all(x == tilde or x == empty for x in c)
}
no_rank_altr_idxs = no_rank_altr_idxs - unaltr_idxs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document change in PR description, and update the doc string in the CleanProfile class. Please add examples in that doc string so we can more easily determine the meaning of these.

TODO:

Add issue where we use a dataclass to descriptively partition the possible state space for these alterations -- users should make the decision on what they car about

example

@dataclass(slots = true)
class CleaningIndexDeltas:
    dropped: set or list or tuple --> entire row removed index does not exist in child
    remove_empty_ranking: TypedDict { trailing_only: <object>, internal_only: <object>, both_trailing_and_internal: <object>}
    subset_previous_ballot: TypedDict {strict_subset_no_gaps ([A, B, {C, D}, E] -> [B, {C,D}]), strict_subset_gaps, weak_subset ([A, B, {C,D}, E] -> [B, C]) 
... and so on

This is a loose sketch

Comment on lines +534 to +535
A removed ballot's ranking is considered empty after cleaning and recorded in the
``no_rank_altr_idxs`` of the returned ``CleanedRankProfile``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current paradigm (which is not great) records dropped ballots as gaps in the index (compared to the parent profile)

with that integer candidate.
"""
if not isinstance(profile, RankProfile):
raise ProfileError("Profile must be a RankProfile.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise ProfileError("Profile must be a RankProfile.")
raise TypeError("Profile must be a RankProfile.")

Comment on lines +561 to +567
if isinstance(removed, Candidate) and not isinstance(removed, bool):
removed = [removed]
elif isinstance(removed, list):
if any(not isinstance(cand, (str, int)) or isinstance(cand, bool) for cand in removed):
raise TypeError("Candidates must be strings or integers within removed.")
else:
raise TypeError("removed must be a str/int candidate or a list of candidates.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we have a function that checks if something is a valid candidate that we should use here

ranking_cols = [f"Ranking_{i}" for i in range(1, profile.max_ranking_length + 1)]
ballots_to_remove = profile._df[ranking_cols].isin(cand_ids).any(axis=1)
cleaned_df = profile.df[~ballots_to_remove]
removed_ballot_idxs = set(profile.df[ballots_to_remove].index)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
removed_ballot_idxs = set(profile.df[ballots_to_remove].index)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove ranked ballots containing specified candidates

2 participants