Skip to content

Commit

Permalink
add tiebreaking compensation
Browse files Browse the repository at this point in the history
  • Loading branch information
epacuit committed Jan 13, 2025
1 parent 44e8c5b commit d0a32a1
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 9 deletions.
222 changes: 219 additions & 3 deletions _pref_eq.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -11,11 +11,227 @@
"from pref_voting.voting_methods import *\n",
"from pref_voting.rankings import Ranking\n",
"from tqdm.notebook import tqdm\n",
"from pref_voting.invariance_axioms import preferential_equality\n",
"from pref_voting.invariance_axioms import preferential_equality, tiebreaking_compensation\n",
"from pref_voting.generate_profiles import strict_weak_orders\n",
"from pref_voting.variable_voter_axioms import nonlinear_neutral_reversal, neutral_indifference"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"((0,), (1, 2, 3), (4, 5))\n",
"(2, 1, 3)\n",
"True\n",
"True\n",
"False\n",
"False\n",
"True\n",
"0 2 1 3 ( 4 5 ) \n"
]
}
],
"source": [
"lin_order = (2, 1, 3)\n",
"\n",
"r = Ranking({0:1, 1:2, 2:2, 3:2, 4:3, 5:3})\n",
"\n",
"\n",
"print(r.to_indiff_list())\n",
"\n",
"new_indiff_list = []\n",
"for cs in r.to_indiff_list():\n",
" if set(cs) == set(lin_order):\n",
" for c in lin_order: \n",
" new_indiff_list.append((c,))\n",
" else:\n",
" new_indiff_list.append(cs)\n",
"\n",
"print(lin_order)\n",
"print(r.is_tied(lin_order))\n",
"print(r.is_tied([4, 5]))\n",
"print(r.is_tied([0, 1]))\n",
"print(r.is_tied([1, 2, 3, 4, 5]))\n",
"print(r.is_tied([0]))\n",
"print(Ranking.from_indiff_list(new_indiff_list))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'linear_orders_with_reverse' is not defined",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[0;32mIn[5], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mlinear_orders_with_reverse\u001b[49m([\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m]))\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m r1, reverse_r1 \u001b[38;5;129;01min\u001b[39;00m linear_orders_with_reverse([\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m]): \n\u001b[1;32m 4\u001b[0m \u001b[38;5;28mprint\u001b[39m(r1, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m: \u001b[39m\u001b[38;5;124m\"\u001b[39m, reverse_r1)\n",
"\u001b[0;31mNameError\u001b[0m: name 'linear_orders_with_reverse' is not defined"
]
}
],
"source": [
"\n",
"print(linear_orders_with_reverse([1, 2, 3]))\n",
"\n",
"for r1, reverse_r1 in linear_orders_with_reverse([1, 2, 3]): \n",
" print(r1, \": \", reverse_r1)\n",
"\n",
"for r1, r2 in combinations([0, 0, 1, 2], 2):\n",
" print(r1, r2)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"+-----+-------+\n",
"| 2 | 2 |\n",
"+-----+-------+\n",
"| 0 1 | 0 1 2 |\n",
"| 3 | 3 |\n",
"| 2 | |\n",
"| | |\n",
"+-----+-------+\n",
"\n",
"[]\n"
]
}
],
"source": [
"from itertools import chain, combinations\n",
"# generate all subsets of a set, use combinations\n",
"def powerset(lst):\n",
" s = list(lst)\n",
" return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))\n",
"\n",
"def linear_orders_with_reverse(cands): \n",
"\n",
" lin_orders = list(permutations(cands))\n",
" lin_orders_with_reverse = []\n",
" for lin_order in lin_orders:\n",
" lin_orders_with_reverse.append((lin_order, lin_order[::-1]))\n",
" return lin_orders_with_reverse\n",
"\n",
"def remove_first_occurrences(rankings, r1, r2):\n",
" removed_r1 = False\n",
" removed_r2 = False\n",
" result = []\n",
"\n",
" for r in rankings:\n",
" if r == r1 and not removed_r1:\n",
" removed_r1 = True # Skip the first r1\n",
" elif r == r2 and not removed_r2:\n",
" removed_r2 = True # Skip the first r2\n",
" else:\n",
" result.append(r) # Keep all other elements\n",
"\n",
" return result\n",
"\n",
"\n",
"prof = generate_profile(3, 10).to_profile_with_ties()\n",
"\n",
"\n",
"def has_tiebreaking_compensation_violation(prof, vm, verbose=False):\n",
" \"\"\"\n",
" Return True if the profile prof has a tiebreaking compensation violation for the voting method vm.\n",
" \"\"\"\n",
" for cands in powerset(prof.candidates): \n",
" if len(cands) > 1: \n",
"\n",
" rankings_with_tie = [r for r in prof.rankings if r.is_tied(cands)]\n",
"\n",
" checked_rankings = []\n",
" for r1, r2 in combinations(rankings_with_tie, 2):\n",
" if set([r1, r2]) in checked_rankings:\n",
" continue\n",
" checked_rankings.append(set([r1, r2]))\n",
" for lin_order, reverse_lin_order in linear_orders_with_reverse(cands): \n",
"\n",
" other_rankings = [r for r in prof.rankings if not r in rankings_with_tie]\n",
" \n",
" # rankings_with_tie without r1 and r2\n",
" other_rankings_with_tie = remove_first_occurrences(rankings_with_tie, r1, r2)\n",
"\n",
" new_rankings = [r1.break_tie(lin_order),r2.break_tie(reverse_lin_order)] + other_rankings_with_tie + other_rankings \n",
"\n",
" new_prof = ProfileWithTies(new_rankings, candidates=prof.candidates)\n",
" if vm(prof) != vm(new_prof): \n",
" if verbose: \n",
" print(f\"\\nAfter adding breaking the tie between {cands} with {lin_order} and {reverse_lin_order} in {r1} and {r2}, respectively: \\n\")\n",
" new_prof.anonymize().display()\n",
" vm.display(new_prof)\n",
" return True\n",
" return False\n",
"\n",
"\n",
"def find_all_tiebreaking_compensation_violations(prof, vm, verbose=False):\n",
" \"\"\"\n",
" Find all the violations of tiebreaking compensation for prof with respect to the voting method vm. Returns a list of tuples consisting of the rankings and the rankings with the ties broken. If there are no violations, return an empty list.\n",
" \"\"\"\n",
"\n",
" violations = []\n",
" for cands in powerset(prof.candidates): \n",
" if len(cands) > 1: \n",
"\n",
" rankings_with_tie = [r for r in prof.rankings if r.is_tied(cands)]\n",
"\n",
" checked_rankings = []\n",
" for r1, r2 in combinations(rankings_with_tie, 2):\n",
" if set([r1, r2]) in checked_rankings:\n",
" continue\n",
" checked_rankings.append(set([r1, r2]))\n",
" for lin_order, reverse_lin_order in linear_orders_with_reverse(cands): \n",
"\n",
" other_rankings = [r for r in prof.rankings if not r in rankings_with_tie]\n",
" \n",
" # rankings_with_tie without r1 and r2\n",
" other_rankings_with_tie = remove_first_occurrences(rankings_with_tie, r1, r2)\n",
"\n",
" new_rankings = [r1.break_tie(lin_order),r2.break_tie(reverse_lin_order)] + other_rankings_with_tie + other_rankings \n",
"\n",
" new_prof = ProfileWithTies(new_rankings, candidates=prof.candidates)\n",
" if vm(prof) != vm(new_prof): \n",
" if verbose: \n",
" print(f\"\\nAfter adding breaking the tie between {cands} with {lin_order} and {reverse_lin_order} in {r1} and {r2}, respectively: \\n\")\n",
" new_prof.anonymize().display()\n",
" vm.display(new_prof)\n",
" violations.append((r1, r1.break_tie(lin_order)), (r2, r2.break_tie(reverse_lin_order)))\n",
" return violations\n",
"\n",
"prof = ProfileWithTies([\n",
" {0:1, 1:1, 2:3, 3:2},\n",
" {0:1, 1:1, 2:3, 3:2},\n",
" {0:1, 1:1, 2:1, 3:2},\n",
" {0:1, 1:1, 2:1, 3:2},\n",
"])\n",
"\n",
"prof.anonymize().display() \n",
"print()\n",
"print(tiebreaking_compensation.find_all_violations(prof, pareto))\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 2,
Expand Down Expand Up @@ -1861,7 +2077,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
author = 'Wes Holliday and Eric Pacuit'

# The full version, including alpha/beta/rc tags
release = '1.14.28'
release = '1.14.29'


# -- General configuration ---------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pref_voting/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.14.28'
__version__ = '1.14.29'
33 changes: 31 additions & 2 deletions pref_voting/axiom_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pref_voting.profiles import Profile
from pref_voting.profiles_with_ties import ProfileWithTies
from pref_voting.rankings import Ranking
from itertools import combinations
from itertools import combinations, chain, permutations
import copy

def display_mg(edata):
Expand Down Expand Up @@ -97,4 +97,33 @@ def get_rank(ranking, c):
elif isinstance(ranking, (list, tuple)):
return ranking.index(c)
else:
raise ValueError("Invalid input type")
raise ValueError("Invalid input type")


# generate all subsets of a set, use combinations
def powerset(lst):
s = list(lst)
return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

def linear_orders_with_reverse(cands):

lin_orders = list(permutations(cands))
lin_orders_with_reverse = []
for lin_order in lin_orders:
lin_orders_with_reverse.append((lin_order, lin_order[::-1]))
return lin_orders_with_reverse

def remove_first_occurrences(rankings, r1, r2):
removed_r1 = False
removed_r2 = False
result = []

for r in rankings:
if r == r1 and not removed_r1:
removed_r1 = True # Skip the first r1
elif r == r2 and not removed_r2:
removed_r2 = True # Skip the first r2
else:
result.append(r) # Keep all other elements

return result
76 changes: 75 additions & 1 deletion pref_voting/invariance_axioms.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,12 +477,86 @@ def find_all_preferential_equality_violations(prof, vm, verbose=False):
find_all_violations = find_all_preferential_equality_violations,
)


def has_tiebreaking_compensation_violation(prof, vm, verbose=False):
"""
Return True if the profile prof has a tiebreaking compensation violation for the voting method vm.
"""
for cands in powerset(prof.candidates):
if len(cands) > 1:

rankings_with_tie = [r for r in prof.rankings if r.is_tied(cands)]

checked_rankings = []
for r1, r2 in combinations(rankings_with_tie, 2):
if set([r1, r2]) in checked_rankings:
continue
checked_rankings.append(set([r1, r2]))
for lin_order, reverse_lin_order in linear_orders_with_reverse(cands):

other_rankings = [r for r in prof.rankings if not r in rankings_with_tie]

# rankings_with_tie without r1 and r2
other_rankings_with_tie = remove_first_occurrences(rankings_with_tie, r1, r2)

new_rankings = [r1.break_tie(lin_order),r2.break_tie(reverse_lin_order)] + other_rankings_with_tie + other_rankings

new_prof = ProfileWithTies(new_rankings, candidates=prof.candidates)
if vm(prof) != vm(new_prof):
if verbose:
print(f"\nAfter adding breaking the tie between {cands} with {lin_order} and {reverse_lin_order} in {r1} and {r2}, respectively: \n")
new_prof.anonymize().display()
vm.display(new_prof)
return True
return False


def find_all_tiebreaking_compensation_violations(prof, vm, verbose=False):
"""
Find all the violations of tiebreaking compensation for prof with respect to the voting method vm. Returns a list of tuples consisting of the rankings and the rankings with the ties broken. If there are no violations, return an empty list.
"""

violations = []
for cands in powerset(prof.candidates):
if len(cands) > 1:

rankings_with_tie = [r for r in prof.rankings if r.is_tied(cands)]

checked_rankings = []
for r1, r2 in combinations(rankings_with_tie, 2):
if set([r1, r2]) in checked_rankings:
continue
checked_rankings.append(set([r1, r2]))
for lin_order, reverse_lin_order in linear_orders_with_reverse(cands):

other_rankings = [r for r in prof.rankings if not r in rankings_with_tie]

# rankings_with_tie without r1 and r2
other_rankings_with_tie = remove_first_occurrences(rankings_with_tie, r1, r2)

new_rankings = [r1.break_tie(lin_order),r2.break_tie(reverse_lin_order)] + other_rankings_with_tie + other_rankings

new_prof = ProfileWithTies(new_rankings, candidates=prof.candidates)
if vm(prof) != vm(new_prof):
if verbose:
print(f"\nAfter adding breaking the tie between {cands} with {lin_order} and {reverse_lin_order} in {r1} and {r2}, respectively: \n")
new_prof.anonymize().display()
vm.display(new_prof)
violations.append((r1, r1.break_tie(lin_order)), (r2, r2.break_tie(reverse_lin_order)))
return violations

tiebreaking_compensation = Axiom(
"Tiebreaking Compensation",
has_violation = has_tiebreaking_compensation_violation,
find_all_violations = find_all_tiebreaking_compensation_violations,
)
invariance_axioms = [
block_invariance,
upward_block_preservation,
downward_block_preservation,
homogeneity,
upward_homogeneity,
downward_homogeneity,
preferential_equality
preferential_equality,
tiebreaking_compensation,
]
Loading

0 comments on commit d0a32a1

Please sign in to comment.