Skip to content

feat: add profile subtraction utility - #387

Draft
aryansk wants to merge 1 commit into
mggg:mainfrom
aryansk:codex/issue-384-profile-subtraction
Draft

feat: add profile subtraction utility#387
aryansk wants to merge 1 commit into
mggg:mainfrom
aryansk:codex/issue-384-profile-subtraction

Conversation

@aryansk

@aryansk aryansk commented Aug 11, 2026

Copy link
Copy Markdown

Problem

Profiles can be combined with + / sum_profiles, but there is no inverse operation. With search_profile_for_rank_pattern available, queries like "ballots where a candidate pair is separated by at least x ranks" need profile subtraction.

Change

  • Add subtract_profiles(minuend, subtrahend) in pref_profile/utils.py.
  • Add __sub__ to RankProfile and ScoreProfile.
  • Ballot weights are subtracted for identical rankings (or scores); the minuend's voter sets are retained.
  • A ValueError is raised if any weight would become negative.

Testing

  • New test file tests/pref_profile/utils/test_subtract_profiles.py (9 tests) covering weight subtraction, voter-set retention, negative-weight errors, mixed-length rankings, mixed types, and the standalone function.
  • Full tests/pref_profile/ suite passes (170 tests).

Closes #384

Add subtract_profiles() and the __sub__ operator to RankProfile and
ScoreProfile. Ballot weights are subtracted for identical rankings (or
scores), the minuend's voter sets are retained, and a ValueError is
raised if any weight would become negative. This enables inverse
queries to search_profile_for_rank_pattern.

Closes mggg#384
@peterrrock2

peterrrock2 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

(copy of comment on #383)

Hey @aryansk!

Thank you so much for contributing to VoteKit! It's wonderful to see our work being used by people outside of our community. If you happen to have a moment, I would love to hear more about how you found the package and what you are using it for so that we can better fit the needs of our users.

You may have noticed that we made our latest release of VoteKit around two weeks ago. Since then, I have been preoccupied with some other projects and missed the latest set of PRs that came in. This looks like a great start on this issue, and one of the other maintainers will be adding a review with some additional feedback shortly.

I also saw you mention that you were assisted by Codex when working on this issue. Thank you for including that information. As an academic lab, we value transparency of process, and knowing this sort of thing is genuinely helpful. I think that, until this point, we have not been clear about our preferences for how contributors use AI tools when submitting work for review on this project.

I hope the following makes this clear, but I want to state it explicitly anyway: the recent updates to this policy are not an indictment of any of the work that you have done here. As a maintainer of a small OSS package, it's always heartening to see more people trying to become a part of the community, and I am truly grateful for the work that you have put in.


Additional notes (this section is a form that I am including on several recent PRs)

Since there have been so many new contributors in the last couple of weeks (a wonderful problem to have), I thought it important to update the "Use of AI Tools" section of the Contributing.md on our main branch to clarify what we currently consider to be best-practices for our lab.

To be clear, this this is not an indictment of the use of any AI tools or a statement about the work presented here (indeed, it would be a bit illogical to find anyone at issue with this policy before it was explicitly stated!). I include it here only because this is 1. a recent update in a document people infrequently visit and 2. an effort to clearly communicate our expectations around contributing to this project.

I'll include the exact snippet here so you don't have to go through the trouble of finding it yourself:

Use of AI tools

Researchers rely on VoteKit and its results, so contributors have a heightened responsibility for
the correctness and clarity of every change.

  • Core implementation and business logic in src/votekit/ must be written by humans. AI tools may
    provide inline assistance (e.g., an editor copilot) and may be used to navigate and deepen
    understanding of the codebase and domain material (e.g., with chat agents). Contributors should
    not delegate the generation or rewriting of core implementation code to agentic coding tools.
    For example, Codex GUI/CLI, Claude GUI/CLI, Cursor v3+, and similar tools should not be asked to
    generate or substantially rewrite that code.
  • Tests may be developed jointly by humans and AI. AI is particularly useful for adversarial
    testing, such as searching for user inputs that break an implementation, but the person
    responsible for the pull request must review every AI-generated or AI-assisted test and confirm
    that it meaningfully tests the intended behavior.
  • Docstrings may be developed jointly by humans and AI. The person responsible for the pull
    request must read, edit, and review every AI-assisted docstring for accuracy, thoroughness, and
    compliance with the documentation and formatting guidelines below.

The person responsible for a pull request remains accountable for all of its contents, regardless
of which tools assisted in preparing it.


Thank you again for your contribution, and I hope that we'll see more from you soon!

-Peter
(Lead Maintainer of VoteKit)

@graceg571 graceg571 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for contributing this PR! Subtracting profiles can take many different forms:

  • set difference: subtract only rankings and ignore its weigh. If there's a ranking match between the base profile and the subtracted profile(s), the ranking is removed by default.
  • reduce by weight: subtract ballots by weight. The ranking's weight will be reduced by the weight value of the matching ranking in the subtracted profile. An error would be thrown when a ballot does not exist or goes negative weight because weight cannot be reduced from a missing ballot and a negative weight is not a valid weight reduction.
  • "normal" ballot subtraction: the base profile is the bag of ballots to be removed/subtracted by the subtracted profile(s). A ballot in the subtracted profile found in the base profile is subtracted (or removed from the bag) till the weight of the subtracted ranking's weight is consumed or the base ranking's weight is exhausted (i.e. zero weight). So, zero weight rankings are removed from the profile by default.

We would like to start with the last ballot subtraction type, and my review comments reflect that decision. Let me know if you want to keep working on this!



def subtract_profiles(
minuend: PreferenceProfile, subtrahend: PreferenceProfile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minuend and subtrahend are not intuitive to those without a math background. base_profile and subtracted_profiles would be clearer.



def subtract_profiles(
minuend: PreferenceProfile, subtrahend: PreferenceProfile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar to sum_profiles, the subtrahend could be a list of profiles or a single profile, and the profiles can be summed together prior to subtraction


from votekit.pref_profile.pref_profile import RankProfile

candidates = list(set().union(*[set(profile.candidates) for profile in [minuend, subtrahend]]))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A ballot can only be subtracted if it exists in the minuend profile. Therefore, the total list of candidates is always the same as minuend's candidates.

Likewise, we would want to define what we do if a ballot to be subtracted does not exist in the minuend profile.

from votekit.pref_profile.pref_profile import RankProfile

candidates = list(set().union(*[set(profile.candidates) for profile in [minuend, subtrahend]]))
max_ranking_length = max(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Building off the candidates comment, the max_ranking_length should be the minuend profile as that's the "ground truth" we remove ballots from.

Comment on lines +635 to +646
# Pad both profiles to the same ranking length
padded_dfs = []
for profile in [minuend, subtrahend]:
assert profile.max_ranking_length is not None
curr_df = profile.df.copy()
for i in range(profile.max_ranking_length, max_ranking_length):
curr_df.insert(
len(curr_df.columns),
f"Ranking_{i + 1}",
pd.Series([frozenset("~")] * len(curr_df), dtype=object, index=curr_df.index),
)
padded_dfs.append(curr_df)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No need to pad the profiles. We only care if ballots from subtrahend profile match ballots from minuend profile. Ballots with the same rankings will be equivalent with/without padding.

Comment on lines +652 to +653
subtrahend_weights = subtrahend_df.groupby(ranking_cols, dropna=False)["Weight"].sum()
minuend_grouped = minuend_df.groupby(ranking_cols, dropna=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's a group_ballots instance method for rank and score profiles.

Comment on lines +658 to +661
if weight < 0:
raise ValueError(
f"Cannot subtract profiles: ballot weight would become negative for ranking {key}."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A negative weight could be an error if the user wants to enforce the subtrahend profile being a subset of the minuend profile. For instance, if we were subtracting by weight, a negative weight would indicate subtraction has gone beyond the weight allocated to that ballot. For this function, the subtrahend does not need to be a subset of the minuend, and instead the subtrahend is the hand that removes ballots from the minuend bag. Based on this analogy, if the hand goes to remove a ballot that no longer exists in the bag, then nothing is removed. Therefore, a ballot's weight should be constrained to no less than 0 and removed from the profile. We can have a parameter to state whether we remove those zero weight ballots and have the default be to remove them.

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.

Utility method for subtracting profiles

4 participants