Skip to content

Commit a981ded

Browse files
authored
Merge pull request #163 from wesholliday/main
Added "marginal clone" option to independence of clones
2 parents b9c3617 + ad56a90 commit a981ded

File tree

3 files changed

+54
-8
lines changed

3 files changed

+54
-8
lines changed

pref_voting/c1_methods.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def copeland_ranking(edata, curr_cands=None, local=True, tie_breaking=None):
179179
@vm(name = "Llull",
180180
input_types = [ElectionTypes.PROFILE, ElectionTypes.PROFILE_WITH_TIES, ElectionTypes.MAJORITY_GRAPH, ElectionTypes.MARGIN_GRAPH])
181181
def llull(edata, curr_cands = None):
182-
"""The Llull score for a candidate :math:`c` is the number of candidates that :math:`c` is weakly majority preferred to. This is equivalent to calculating the Copeland scores for a candidate :math:`c` with 1 point for each candidate that :math:`c` is majority preferred to, 1/2 point for each candidate that :math:`c` is tied with, and 0 points for each candidate that is majority preferred to :math:`c`. The Llull winners are the candidates with the maximum Llull score in the profile restricted to ``curr_cands``.
182+
"""The Llull score for a candidate :math:`c` is the number of candidates that :math:`c` is weakly majority preferred to. This is equivalent to calculating the Copeland scores for a candidate :math:`c` with 1 point for each candidate that :math:`c` is majority preferred to, 1 point for each candidate that :math:`c` is tied with (instead of 1/2 a point as for Copeland), and 0 points for each candidate that is majority preferred to :math:`c`. The Llull winners are the candidates with the maximum Llull score in the profile restricted to ``curr_cands``.
183183
184184
Args:
185185
edata (Profile, ProfileWithTies, MajorityGraph, MarginGraph): Any election data that has a `copeland_scores` method.

pref_voting/helper.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
21
from pref_voting.profiles import Profile
32
from pref_voting.profiles_with_ties import ProfileWithTies
43
from pref_voting.weighted_majority_graphs import MajorityGraph
54
from pref_voting.rankings import Ranking
65
from pref_voting.social_welfare_function import *
76
from pref_voting.voting_method import *
8-
from itertools import combinations
7+
from itertools import combinations, chain
98
import random
109

1110
import networkx as nx
@@ -363,4 +362,9 @@ def convex_lexicographic_sublists(l):
363362
if idx == len(l) - 1:
364363
cl_sublists.append(current_list)
365364

366-
return cl_sublists
365+
return cl_sublists
366+
367+
def powerset(iterable):
368+
"""powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"""
369+
s = list(iterable)
370+
return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

pref_voting/variable_candidate_axioms.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
File: variable_candidate_axioms.py
33
Author: Wes Holliday ([email protected]) and Eric Pacuit ([email protected])
44
Date: July 7, 2024
5+
Updated: August 20, 2025
56
67
Variable candidate axioms
78
"""
@@ -10,6 +11,7 @@
1011
from pref_voting.axiom_helpers import *
1112
from pref_voting.c1_methods import top_cycle
1213
import numpy as np
14+
from itertools import combinations
1315

1416
def has_stability_for_winners_violation(edata, vm, verbose=False, strong_stability=False):
1517
"""
@@ -425,13 +427,46 @@ def tideman_clone_sets(prof):
425427

426428
return clone_sets
427429

428-
def has_independence_of_clones_violation(prof, vm, clone_def = "Tideman", conditions_to_check = "all", verbose = False):
430+
def marginal_clone_sets(edata, epsilon=0):
431+
"""Given a profile or margin graph, returns all sets of "marginal clones": a set C of candidates is a set of marginal clones if for any c,d in C and x not in C, |margin(c,x) - margin(d,x)| <= epsilon."""
432+
433+
mc_sets = []
434+
435+
cands = edata.candidates
436+
437+
for subset in powerset(cands):
438+
439+
if len(subset) <=1 or len(subset) == len(cands):
440+
continue
441+
442+
is_marginal_clone_set = True
443+
444+
for c, d in combinations(subset, 2):
445+
are_clones = True
446+
447+
for x in cands:
448+
if not x in subset:
449+
if abs(edata.margin(c,x) - edata.margin(d,x)) > epsilon:
450+
are_clones = False
451+
break
452+
453+
if not are_clones:
454+
is_marginal_clone_set = False
455+
break
456+
457+
if is_marginal_clone_set:
458+
mc_sets.append(subset)
459+
460+
return mc_sets
461+
462+
def has_independence_of_clones_violation(prof, vm, clone_def = "Tideman", epsilon = 0, conditions_to_check = "all", verbose = False):
429463
"""Independence of Clones: returns True if there is a set C of clones and a clone c in C such that removing c either (i) changes which non-clones (candidates not in C) win or (ii) changes whether any clone in C wins. We call (i) a violation of "non-clone choice is independent of clones" (NCIC) and call (ii) a violation of "clone choice is independent of clones" (CIC).
430464
431465
Args:
432466
prof (Profile): the election data.
433467
vm (VotingMethod): A voting method to test.
434-
clone_def (str, default="Tideman"): The definition of clones. Currently only "Tideman" is supported.
468+
clone_def (str, default="Tideman"): The definition of clones. Options are "Tideman" and "Marginal".
469+
epsilon (float, default=0): If clone_def is "Marginal", then for C to be a marginal clone set, it must but that for any c,c' in C and x not in C, |margin(c,x) - margin(c',x)| <= epsilon.
435470
conditions_to_check (str, default="all"): The conditions to check. If "all", then both NCIC and CIC are checked. If "NCIC", then only NCIC is checked. If "CIC", then only CIC is checked.
436471
verbose (bool, default=False): If a violation is found, display the violation.
437472
@@ -440,6 +475,9 @@ def has_independence_of_clones_violation(prof, vm, clone_def = "Tideman", condit
440475
if clone_def == "Tideman":
441476
clone_sets = tideman_clone_sets(prof)
442477

478+
if clone_def == "Marginal":
479+
clone_sets = marginal_clone_sets(prof, epsilon)
480+
443481
for clone_set in clone_sets:
444482

445483
non_clones = [n for n in prof.candidates if n not in clone_set]
@@ -500,13 +538,14 @@ def has_independence_of_clones_violation(prof, vm, clone_def = "Tideman", condit
500538

501539
return False
502540

503-
def find_all_independence_of_clones_violations(prof, vm, clone_def = "Tideman", conditions_to_check = "all", verbose = False):
541+
def find_all_independence_of_clones_violations(prof, vm, clone_def = "Tideman", epsilon = 0, conditions_to_check = "all", verbose = False):
504542
"""Returns all violations of Independence of Clones for the given election data and voting method.
505543
506544
Args:
507545
prof (Profile): the election data.
508546
vm (VotingMethod): A voting method to test.
509-
clone_def (str, default="Tideman"): The definition of clones. Currently only "Tideman" is supported.
547+
clone_def (str, default="Tideman"): The definition of clones. Options are "Tideman" and "Marginal".
548+
epsilon (float, default=0): If clone_def is "Marginal", then for C to be a marginal clone set, it must but that for any c,c' in C and x not in C, |margin(c,x) - margin(c',x)| <= epsilon.
510549
conditions_to_check (str, default="all"): The conditions to check. If "all", then both NCIC and CIC are checked. If "NCIC", then only NCIC is checked. If "CIC", then only CIC is checked.
511550
verbose (bool, default=False): If a violation is found, display the violation.
512551
@@ -516,6 +555,9 @@ def find_all_independence_of_clones_violations(prof, vm, clone_def = "Tideman",
516555

517556
if clone_def == "Tideman":
518557
clone_sets = tideman_clone_sets(prof)
558+
559+
if clone_def == "Marginal":
560+
clone_sets = marginal_clone_sets(prof, epsilon)
519561

520562
violations = list()
521563

0 commit comments

Comments
 (0)