From 22b0496399dbb961a25f9d43987a60dbb17716e9 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Wed, 7 Dec 2022 01:30:20 +0200 Subject: [PATCH 01/22] Borda rule and manipulatopn heuristics algorithms- not completley implemented yet. --- py3votecore/borda.py | 12 +++ py3votecore/borda_at_large.py | 92 ++++++++++++++++++ .../borda_manipulation_heurestics_methods.py | 63 ++++++++++++ test_functionality/test_borda.py | 64 +++++++++++++ test_functionality/test_borda_at_large.py | 91 ++++++++++++++++++ ...t_borda_manipulation_heurestics_methods.py | 95 +++++++++++++++++++ 6 files changed, 417 insertions(+) create mode 100644 py3votecore/borda.py create mode 100644 py3votecore/borda_at_large.py create mode 100644 py3votecore/borda_manipulation_heurestics_methods.py create mode 100644 test_functionality/test_borda.py create mode 100644 test_functionality/test_borda_at_large.py create mode 100644 test_functionality/test_borda_manipulation_heurestics_methods.py diff --git a/py3votecore/borda.py b/py3votecore/borda.py new file mode 100644 index 0000000..6abcd0c --- /dev/null +++ b/py3votecore/borda.py @@ -0,0 +1,12 @@ + + +from .abstract_classes import AbstractSingleWinnerVotingSystem +from .borda_at_large import BordaAtLarge + +class Borda(AbstractSingleWinnerVotingSystem): + + def __init__(self, ballots: dict, tie_breaker=None): + super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker) + + + diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py new file mode 100644 index 0000000..5549796 --- /dev/null +++ b/py3votecore/borda_at_large.py @@ -0,0 +1,92 @@ + + +from py3votecore.abstract_classes import MultipleWinnerVotingSystem +from py3votecore.common_functions import matching_keys, unique_permutations +import copy + + +class BordaAtLarge(MultipleWinnerVotingSystem): + + def __init__(self, ballots: dict, tie_breaker=None, required_winners: int=1): + super(BordaAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners) + + def calculate_results(self): + # Standardize the ballot format and extract the candidates + self.candidates = set() + for ballot in self.ballots: + + # Convert a single candidate ballots into ballot lists + if not isinstance(ballot["ballot"], list): + ballot["ballot"] = [ballot["ballot"]] + + # Ensure no ballot has an excess of votes + if len(ballot["ballot"]) < self.required_winners: + raise Exception("A ballot contained too many candidates") + + # Add all candidates on the ballot to the set + self.candidates.update(set(ballot["ballot"])) + + # Sum up all votes for each candidate + self.tallies = dict.fromkeys(self.candidates, 0) + for ballot in self.ballots: + for i in range(len(ballot["ballot"])): + self.tallies[ballot["ballot"][i]] += (ballot["count"]*(len(self.candidates)-1-i)) + tallies = copy.deepcopy(self.tallies) + + # Determine which candidates win + winning_candidates = set() + while len(winning_candidates) < self.required_winners: + + # Find the remaining candidates with the most votes + largest_tally = max(tallies.values()) + top_candidates = matching_keys(tallies, largest_tally) + + # Reduce the found candidates if there are too many + if len(top_candidates | winning_candidates) > self.required_winners: + self.tied_winners = top_candidates.copy() + while len(top_candidates | winning_candidates) > self.required_winners: + top_candidates.remove(self.break_ties(top_candidates, True)) + + # Move the top candidates into the winning pile + winning_candidates |= top_candidates + for candidate in top_candidates: + del tallies[candidate] + + self.winners = winning_candidates + + def as_dict(self): + data = super(BordaAtLarge, self).as_dict() + data["tallies"] = self.tallies + return data + +def main(): + lst = ["a", "b", "c", "d", "e", "f", "g", "h"] + ballots = [] + for p1 in unique_permutations(lst): + # ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}] + # print(ballots) + # output = BordaAtLarge(ballots) + # print(output.as_dict()["tallies"]) + # if output.as_dict()["tallies"] == {'g': 48, 'h': 56, 'f': 40, 'a': 0, 'd': 24, 'b': 8, 'c': 16, 'e': 32}: + # print(ballots) + # print("True") + + + for p2 in unique_permutations(lst): + for p3 in unique_permutations(lst): + for p4 in unique_permutations(lst): + for p5 in unique_permutations(lst): + for p6 in unique_permutations(lst): + for p7 in unique_permutations(lst): + for p8 in unique_permutations(lst): + ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p2}, {"count": 1, "ballot":p3}, {"count": 1, "ballot":p4}, {"count": 1, "ballot":p5}, {"count": 1, "ballot":p6}, {"count": 1, "ballot":p7}, {"count": 1, "ballot":p8}] + # print(ballots) + output = BordaAtLarge(ballots) + if output.as_dict()["tallies"] == {'a': 41, 'b': 34, 'c': 30, 'd': 14, 'e': 27, 'f': 27, 'g': 26, 'h': 25}: + print(ballots) + + +if __name__ == "__main__": + main() + + diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py new file mode 100644 index 0000000..40871ec --- /dev/null +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -0,0 +1,63 @@ +# +# +# +# +# +# + +def AverageFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: + """ + "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), + https://ojs.aaai.org/index.php/AAAI/article/view/7873 + + AverageFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred + candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds + to find such manipulation, and false otherwise. + + Programmer: Leora Schmerler + + >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> AverageFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) + True + + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) + True + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) + False + """ + return 0 + +def LargestFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: + """ + "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), + https://ojs.aaai.org/index.php/AAAI/article/view/7873 + + LargestFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred + candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds + to find such manipulation, and false otherwise. + + Programmer: Leora Schmerler + + >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) + True + + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) + True + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) + True + + """ + + return 0 + diff --git a/test_functionality/test_borda.py b/test_functionality/test_borda.py new file mode 100644 index 0000000..1ba3533 --- /dev/null +++ b/test_functionality/test_borda.py @@ -0,0 +1,64 @@ + + +from py3votecore.borda import Borda +import unittest + + +class TestBorda(unittest.TestCase): + + # Borda, no ties + def test_no_ties(self): + + # Generate data + input = [ + {"count": 26, "ballot": ["c1","c2","c3"]}, + {"count": 22, "ballot": ["c2","c1","c3"]}, + {"count": 23, "ballot": ["c3","c2","c1"]} + ] + output = Borda(input).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3']), + 'tallies': {'c3': 46, 'c2': 93, 'c1': 74}, + 'winner': 'c1' + }) + + # Borda, irrelevant ties + def test_irrelevant_ties(self): + + # Generate data + input = [ + {"count": 26, "ballot": ["c1", "c2", "c3"]}, + {"count": 23, "ballot": ["c2", "c1", "c3"]}, + {"count": 72, "ballot": ["c1", "c3", "c2"]} + ] + output = Borda(input).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3']), + 'tallies': {'c3': 72, 'c2': 72, 'c1': 219}, + 'winner': 'c1' + }) + + # Borda, relevant ties + def test_relevant_ties(self): + + # Generate data + input = [ + {"count": 49, "ballot": ["c1", "c2", "c3"]}, + {"count": 26, "ballot": ["c2", "c1", "c3"]}, + {"count": 23, "ballot": ["c3", "c2", "c1"]} + ] + output = Borda(input).as_dict() + + # Run tests + self.assertEqual(output["tallies"], {'c1': 124, 'c2': 124, 'c3': 46}) + self.assertEqual(output["tied_winners"], set(['c1', 'c2'])) + self.assertTrue(output["winner"] in output["tied_winners"]) + self.assertEqual(len(output["tie_breaker"]), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/test_functionality/test_borda_at_large.py b/test_functionality/test_borda_at_large.py new file mode 100644 index 0000000..59244eb --- /dev/null +++ b/test_functionality/test_borda_at_large.py @@ -0,0 +1,91 @@ + + + +from py3votecore.borda_at_large import BordaAtLarge +import unittest + + +class TestBordaAtLarge(unittest.TestCase): + + # Borda at Large, no ties + def test_borda_at_large_no_ties(self): + + # Generate data + output = BordaAtLarge([ + {"count": 26, "ballot": ["c1", "c2", "c3"]}, + {"count": 22, "ballot": ["c1", "c3", "c2"]}, + {"count": 23, "ballot": ["c2", "c3", "c1"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3']), + 'tallies': {'c3': 45, 'c2': 72, 'c1': 96}, + 'winners': set(['c2', 'c1']) + }) + + # Borda at Large, irrelevant ties + def test_borda_at_large_irrelevant_ties_top(self): + + # Generate data + output = BordaAtLarge([ + {"count": 16, "ballot": ["c1", "c2", "c3", "c4", "c5"]}, + {"count": 25, "ballot": ["c1", "c3", "c2", "c4", "c5"]}, + {"count": 22, "ballot": ["c2", "c3", "c1", "c5", "c4"]}, + {"count": 22, "ballot": ["c4", "c5", "c2", "c1", "c3"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3', 'c4', 'c5']), + 'tallies': {'c3': 173, 'c2': 230, 'c1': 230, 'c5': 88, 'c4': 129}, + 'winners': set(['c2', 'c1']) + }) + self.assertEqual(output["tied_winners"], set(['c2', 'c1'])) + + # Borda at Large, irrelevant ties + def test_borda_at_large_irrelevant_ties_low(self): + + # Generate data + output = BordaAtLarge([ + {"count": 30, "ballot": ["c4", "c1", "c2", "c3"]}, + {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, + {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, + {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, + {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, + {"count": 2, "ballot": ["c3", "c2", "c4", "c1"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output["tallies"], {'c3': 110, 'c2': 110, 'c1': 160, 'c4': 148}) + self.assertEqual(len(output["tie_breaker"]), 4) + self.assertEqual(output['winners'], set(['c4', 'c1'])) + self.assertEqual(len(output), 4) + + # Borda at Large, relevant ties + def test_borda_at_large_relevant_ties(self): + + + # Check these tests!!!!!!!!! + # Generate data + output = BordaAtLarge([ + {"count": 30, "ballot": ["c1", "c4", "c2", "c3"]}, + {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, + {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, + {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, + {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, + {"count": 10, "ballot": ["c1", "c2", "c4", "c3"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output["tallies"], {'c3': 104, 'c2': 126, 'c1': 220, 'c4': 126}) + self.assertEqual(len(output["tie_breaker"]), 4) + self.assertEqual(output["tied_winners"], set(['c2', 'c4'])) + self.assertTrue("c1" in output["winners"] and ("c2" in output["winners"] or "c4" in output["winners"])) + self.assertEqual(len(output), 5) + + +if __name__ == "__main__": + unittest.main() + + diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py new file mode 100644 index 0000000..f4aefbd --- /dev/null +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -0,0 +1,95 @@ + + + + +from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit +import unittest + + +class TestAverageFit(unittest.TestCase): + + # Manipulation fails + def test_4_candidates_manipulation_fails(self): + + input = [ + {"count": 26, "ballot": ["a","b","c","d"]}, + {"count": 22, "ballot": ["a","c","d","b"]}, + {"count": 23, "ballot": ["a","b","c","d"]} + ] + output = AverageFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertFalse(output) + + # Manipulation succeeds + def test_10_candidates_successful_manipulation(self): + + input = [ + {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, + {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, + {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} + ] + output = AverageFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertTrue(output) + + # Borda, relevant ties + def test_4_candidate_manipulation(self): + + # Generate data + input = [ + {"count": 2, "ballot": ["c","a","b","d"]}, + {"count": 2, "ballot": ["b","c","a","d"]}] + output = AverageFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) + + # Run tests + self.assertTrue(output) + +class TestLargestFit(unittest.TestCase): + + # Manipulation fails + def test_4_candidates_manipulation_fails(self): + + input = [ + {"count": 26, "ballot": ["a","b","c","d"]}, + {"count": 22, "ballot": ["a","c","d","b"]}, + {"count": 23, "ballot": ["a","b","c","d"]} + ] + output = LargestFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertFalse(output) + + # Manipulation succeeds + def test_10_candidates_successful_manipulation(self): + + input = [ + {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, + {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, + {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} + ] + output = LargestFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertTrue(output) + + # Borda, relevant ties + def test_4_candidate_manipulation(self): + + # Generate data + input = [ + {"count": 2, "ballot": ["c","a","b","d"]}, + {"count": 2, "ballot": ["b","c","a","d"]}] + output = LargestFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) + + # Run tests + self.assertTrue(output) + + +if __name__ == "__main__": + unittest.main() + + + + From e4b0c2cfadf82d46bca9a816d0700caaed7dfb7a Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Wed, 7 Dec 2022 01:44:37 +0200 Subject: [PATCH 02/22] undo changes --- py3votecore/borda.py | 12 --- py3votecore/borda_at_large.py | 92 ------------------ .../borda_manipulation_heurestics_methods.py | 63 ------------ test_functionality/test_borda.py | 64 ------------- test_functionality/test_borda_at_large.py | 91 ------------------ ...t_borda_manipulation_heurestics_methods.py | 95 ------------------- 6 files changed, 417 deletions(-) delete mode 100644 py3votecore/borda.py delete mode 100644 py3votecore/borda_at_large.py delete mode 100644 py3votecore/borda_manipulation_heurestics_methods.py delete mode 100644 test_functionality/test_borda.py delete mode 100644 test_functionality/test_borda_at_large.py delete mode 100644 test_functionality/test_borda_manipulation_heurestics_methods.py diff --git a/py3votecore/borda.py b/py3votecore/borda.py deleted file mode 100644 index 6abcd0c..0000000 --- a/py3votecore/borda.py +++ /dev/null @@ -1,12 +0,0 @@ - - -from .abstract_classes import AbstractSingleWinnerVotingSystem -from .borda_at_large import BordaAtLarge - -class Borda(AbstractSingleWinnerVotingSystem): - - def __init__(self, ballots: dict, tie_breaker=None): - super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker) - - - diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py deleted file mode 100644 index 5549796..0000000 --- a/py3votecore/borda_at_large.py +++ /dev/null @@ -1,92 +0,0 @@ - - -from py3votecore.abstract_classes import MultipleWinnerVotingSystem -from py3votecore.common_functions import matching_keys, unique_permutations -import copy - - -class BordaAtLarge(MultipleWinnerVotingSystem): - - def __init__(self, ballots: dict, tie_breaker=None, required_winners: int=1): - super(BordaAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners) - - def calculate_results(self): - # Standardize the ballot format and extract the candidates - self.candidates = set() - for ballot in self.ballots: - - # Convert a single candidate ballots into ballot lists - if not isinstance(ballot["ballot"], list): - ballot["ballot"] = [ballot["ballot"]] - - # Ensure no ballot has an excess of votes - if len(ballot["ballot"]) < self.required_winners: - raise Exception("A ballot contained too many candidates") - - # Add all candidates on the ballot to the set - self.candidates.update(set(ballot["ballot"])) - - # Sum up all votes for each candidate - self.tallies = dict.fromkeys(self.candidates, 0) - for ballot in self.ballots: - for i in range(len(ballot["ballot"])): - self.tallies[ballot["ballot"][i]] += (ballot["count"]*(len(self.candidates)-1-i)) - tallies = copy.deepcopy(self.tallies) - - # Determine which candidates win - winning_candidates = set() - while len(winning_candidates) < self.required_winners: - - # Find the remaining candidates with the most votes - largest_tally = max(tallies.values()) - top_candidates = matching_keys(tallies, largest_tally) - - # Reduce the found candidates if there are too many - if len(top_candidates | winning_candidates) > self.required_winners: - self.tied_winners = top_candidates.copy() - while len(top_candidates | winning_candidates) > self.required_winners: - top_candidates.remove(self.break_ties(top_candidates, True)) - - # Move the top candidates into the winning pile - winning_candidates |= top_candidates - for candidate in top_candidates: - del tallies[candidate] - - self.winners = winning_candidates - - def as_dict(self): - data = super(BordaAtLarge, self).as_dict() - data["tallies"] = self.tallies - return data - -def main(): - lst = ["a", "b", "c", "d", "e", "f", "g", "h"] - ballots = [] - for p1 in unique_permutations(lst): - # ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}] - # print(ballots) - # output = BordaAtLarge(ballots) - # print(output.as_dict()["tallies"]) - # if output.as_dict()["tallies"] == {'g': 48, 'h': 56, 'f': 40, 'a': 0, 'd': 24, 'b': 8, 'c': 16, 'e': 32}: - # print(ballots) - # print("True") - - - for p2 in unique_permutations(lst): - for p3 in unique_permutations(lst): - for p4 in unique_permutations(lst): - for p5 in unique_permutations(lst): - for p6 in unique_permutations(lst): - for p7 in unique_permutations(lst): - for p8 in unique_permutations(lst): - ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p2}, {"count": 1, "ballot":p3}, {"count": 1, "ballot":p4}, {"count": 1, "ballot":p5}, {"count": 1, "ballot":p6}, {"count": 1, "ballot":p7}, {"count": 1, "ballot":p8}] - # print(ballots) - output = BordaAtLarge(ballots) - if output.as_dict()["tallies"] == {'a': 41, 'b': 34, 'c': 30, 'd': 14, 'e': 27, 'f': 27, 'g': 26, 'h': 25}: - print(ballots) - - -if __name__ == "__main__": - main() - - diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py deleted file mode 100644 index 40871ec..0000000 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ /dev/null @@ -1,63 +0,0 @@ -# -# -# -# -# -# - -def AverageFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: - """ - "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), - https://ojs.aaai.org/index.php/AAAI/article/view/7873 - - AverageFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred - candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds - to find such manipulation, and false otherwise. - - Programmer: Leora Schmerler - - >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> AverageFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) - True - - >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) - True - >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) - False - """ - return 0 - -def LargestFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: - """ - "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), - https://ojs.aaai.org/index.php/AAAI/article/view/7873 - - LargestFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred - candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds - to find such manipulation, and false otherwise. - - Programmer: Leora Schmerler - - >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) - True - - >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) - True - >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) - True - - """ - - return 0 - diff --git a/test_functionality/test_borda.py b/test_functionality/test_borda.py deleted file mode 100644 index 1ba3533..0000000 --- a/test_functionality/test_borda.py +++ /dev/null @@ -1,64 +0,0 @@ - - -from py3votecore.borda import Borda -import unittest - - -class TestBorda(unittest.TestCase): - - # Borda, no ties - def test_no_ties(self): - - # Generate data - input = [ - {"count": 26, "ballot": ["c1","c2","c3"]}, - {"count": 22, "ballot": ["c2","c1","c3"]}, - {"count": 23, "ballot": ["c3","c2","c1"]} - ] - output = Borda(input).as_dict() - - # Run tests - self.assertEqual(output, { - 'candidates': set(['c1', 'c2', 'c3']), - 'tallies': {'c3': 46, 'c2': 93, 'c1': 74}, - 'winner': 'c1' - }) - - # Borda, irrelevant ties - def test_irrelevant_ties(self): - - # Generate data - input = [ - {"count": 26, "ballot": ["c1", "c2", "c3"]}, - {"count": 23, "ballot": ["c2", "c1", "c3"]}, - {"count": 72, "ballot": ["c1", "c3", "c2"]} - ] - output = Borda(input).as_dict() - - # Run tests - self.assertEqual(output, { - 'candidates': set(['c1', 'c2', 'c3']), - 'tallies': {'c3': 72, 'c2': 72, 'c1': 219}, - 'winner': 'c1' - }) - - # Borda, relevant ties - def test_relevant_ties(self): - - # Generate data - input = [ - {"count": 49, "ballot": ["c1", "c2", "c3"]}, - {"count": 26, "ballot": ["c2", "c1", "c3"]}, - {"count": 23, "ballot": ["c3", "c2", "c1"]} - ] - output = Borda(input).as_dict() - - # Run tests - self.assertEqual(output["tallies"], {'c1': 124, 'c2': 124, 'c3': 46}) - self.assertEqual(output["tied_winners"], set(['c1', 'c2'])) - self.assertTrue(output["winner"] in output["tied_winners"]) - self.assertEqual(len(output["tie_breaker"]), 3) - - -if __name__ == "__main__": - unittest.main() diff --git a/test_functionality/test_borda_at_large.py b/test_functionality/test_borda_at_large.py deleted file mode 100644 index 59244eb..0000000 --- a/test_functionality/test_borda_at_large.py +++ /dev/null @@ -1,91 +0,0 @@ - - - -from py3votecore.borda_at_large import BordaAtLarge -import unittest - - -class TestBordaAtLarge(unittest.TestCase): - - # Borda at Large, no ties - def test_borda_at_large_no_ties(self): - - # Generate data - output = BordaAtLarge([ - {"count": 26, "ballot": ["c1", "c2", "c3"]}, - {"count": 22, "ballot": ["c1", "c3", "c2"]}, - {"count": 23, "ballot": ["c2", "c3", "c1"]} - ], required_winners=2).as_dict() - - # Run tests - self.assertEqual(output, { - 'candidates': set(['c1', 'c2', 'c3']), - 'tallies': {'c3': 45, 'c2': 72, 'c1': 96}, - 'winners': set(['c2', 'c1']) - }) - - # Borda at Large, irrelevant ties - def test_borda_at_large_irrelevant_ties_top(self): - - # Generate data - output = BordaAtLarge([ - {"count": 16, "ballot": ["c1", "c2", "c3", "c4", "c5"]}, - {"count": 25, "ballot": ["c1", "c3", "c2", "c4", "c5"]}, - {"count": 22, "ballot": ["c2", "c3", "c1", "c5", "c4"]}, - {"count": 22, "ballot": ["c4", "c5", "c2", "c1", "c3"]} - ], required_winners=2).as_dict() - - # Run tests - self.assertEqual(output, { - 'candidates': set(['c1', 'c2', 'c3', 'c4', 'c5']), - 'tallies': {'c3': 173, 'c2': 230, 'c1': 230, 'c5': 88, 'c4': 129}, - 'winners': set(['c2', 'c1']) - }) - self.assertEqual(output["tied_winners"], set(['c2', 'c1'])) - - # Borda at Large, irrelevant ties - def test_borda_at_large_irrelevant_ties_low(self): - - # Generate data - output = BordaAtLarge([ - {"count": 30, "ballot": ["c4", "c1", "c2", "c3"]}, - {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, - {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, - {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, - {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, - {"count": 2, "ballot": ["c3", "c2", "c4", "c1"]} - ], required_winners=2).as_dict() - - # Run tests - self.assertEqual(output["tallies"], {'c3': 110, 'c2': 110, 'c1': 160, 'c4': 148}) - self.assertEqual(len(output["tie_breaker"]), 4) - self.assertEqual(output['winners'], set(['c4', 'c1'])) - self.assertEqual(len(output), 4) - - # Borda at Large, relevant ties - def test_borda_at_large_relevant_ties(self): - - - # Check these tests!!!!!!!!! - # Generate data - output = BordaAtLarge([ - {"count": 30, "ballot": ["c1", "c4", "c2", "c3"]}, - {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, - {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, - {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, - {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, - {"count": 10, "ballot": ["c1", "c2", "c4", "c3"]} - ], required_winners=2).as_dict() - - # Run tests - self.assertEqual(output["tallies"], {'c3': 104, 'c2': 126, 'c1': 220, 'c4': 126}) - self.assertEqual(len(output["tie_breaker"]), 4) - self.assertEqual(output["tied_winners"], set(['c2', 'c4'])) - self.assertTrue("c1" in output["winners"] and ("c2" in output["winners"] or "c4" in output["winners"])) - self.assertEqual(len(output), 5) - - -if __name__ == "__main__": - unittest.main() - - diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py deleted file mode 100644 index f4aefbd..0000000 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ /dev/null @@ -1,95 +0,0 @@ - - - - -from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit -import unittest - - -class TestAverageFit(unittest.TestCase): - - # Manipulation fails - def test_4_candidates_manipulation_fails(self): - - input = [ - {"count": 26, "ballot": ["a","b","c","d"]}, - {"count": 22, "ballot": ["a","c","d","b"]}, - {"count": 23, "ballot": ["a","b","c","d"]} - ] - output = AverageFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) - - # Run test - self.assertFalse(output) - - # Manipulation succeeds - def test_10_candidates_successful_manipulation(self): - - input = [ - {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, - {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, - {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} - ] - output = AverageFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) - - # Run test - self.assertTrue(output) - - # Borda, relevant ties - def test_4_candidate_manipulation(self): - - # Generate data - input = [ - {"count": 2, "ballot": ["c","a","b","d"]}, - {"count": 2, "ballot": ["b","c","a","d"]}] - output = AverageFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) - - # Run tests - self.assertTrue(output) - -class TestLargestFit(unittest.TestCase): - - # Manipulation fails - def test_4_candidates_manipulation_fails(self): - - input = [ - {"count": 26, "ballot": ["a","b","c","d"]}, - {"count": 22, "ballot": ["a","c","d","b"]}, - {"count": 23, "ballot": ["a","b","c","d"]} - ] - output = LargestFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) - - # Run test - self.assertFalse(output) - - # Manipulation succeeds - def test_10_candidates_successful_manipulation(self): - - input = [ - {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, - {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, - {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} - ] - output = LargestFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) - - # Run test - self.assertTrue(output) - - # Borda, relevant ties - def test_4_candidate_manipulation(self): - - # Generate data - input = [ - {"count": 2, "ballot": ["c","a","b","d"]}, - {"count": 2, "ballot": ["b","c","a","d"]}] - output = LargestFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) - - # Run tests - self.assertTrue(output) - - -if __name__ == "__main__": - unittest.main() - - - - From 3f35ad5d40a208cb73ee5d4be1e9e554eda3a3c1 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Wed, 7 Dec 2022 01:50:34 +0200 Subject: [PATCH 03/22] Borda rule and manipulatopn heuristics algorithms- not completley implemented yet. --- py3votecore/borda.py | 12 +++ py3votecore/borda_at_large.py | 92 ++++++++++++++++++ .../borda_manipulation_heurestics_methods.py | 63 ++++++++++++ test_functionality/test_borda.py | 64 +++++++++++++ test_functionality/test_borda_at_large.py | 91 ++++++++++++++++++ ...t_borda_manipulation_heurestics_methods.py | 95 +++++++++++++++++++ 6 files changed, 417 insertions(+) create mode 100644 py3votecore/borda.py create mode 100644 py3votecore/borda_at_large.py create mode 100644 py3votecore/borda_manipulation_heurestics_methods.py create mode 100644 test_functionality/test_borda.py create mode 100644 test_functionality/test_borda_at_large.py create mode 100644 test_functionality/test_borda_manipulation_heurestics_methods.py diff --git a/py3votecore/borda.py b/py3votecore/borda.py new file mode 100644 index 0000000..6abcd0c --- /dev/null +++ b/py3votecore/borda.py @@ -0,0 +1,12 @@ + + +from .abstract_classes import AbstractSingleWinnerVotingSystem +from .borda_at_large import BordaAtLarge + +class Borda(AbstractSingleWinnerVotingSystem): + + def __init__(self, ballots: dict, tie_breaker=None): + super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker) + + + diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py new file mode 100644 index 0000000..5549796 --- /dev/null +++ b/py3votecore/borda_at_large.py @@ -0,0 +1,92 @@ + + +from py3votecore.abstract_classes import MultipleWinnerVotingSystem +from py3votecore.common_functions import matching_keys, unique_permutations +import copy + + +class BordaAtLarge(MultipleWinnerVotingSystem): + + def __init__(self, ballots: dict, tie_breaker=None, required_winners: int=1): + super(BordaAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners) + + def calculate_results(self): + # Standardize the ballot format and extract the candidates + self.candidates = set() + for ballot in self.ballots: + + # Convert a single candidate ballots into ballot lists + if not isinstance(ballot["ballot"], list): + ballot["ballot"] = [ballot["ballot"]] + + # Ensure no ballot has an excess of votes + if len(ballot["ballot"]) < self.required_winners: + raise Exception("A ballot contained too many candidates") + + # Add all candidates on the ballot to the set + self.candidates.update(set(ballot["ballot"])) + + # Sum up all votes for each candidate + self.tallies = dict.fromkeys(self.candidates, 0) + for ballot in self.ballots: + for i in range(len(ballot["ballot"])): + self.tallies[ballot["ballot"][i]] += (ballot["count"]*(len(self.candidates)-1-i)) + tallies = copy.deepcopy(self.tallies) + + # Determine which candidates win + winning_candidates = set() + while len(winning_candidates) < self.required_winners: + + # Find the remaining candidates with the most votes + largest_tally = max(tallies.values()) + top_candidates = matching_keys(tallies, largest_tally) + + # Reduce the found candidates if there are too many + if len(top_candidates | winning_candidates) > self.required_winners: + self.tied_winners = top_candidates.copy() + while len(top_candidates | winning_candidates) > self.required_winners: + top_candidates.remove(self.break_ties(top_candidates, True)) + + # Move the top candidates into the winning pile + winning_candidates |= top_candidates + for candidate in top_candidates: + del tallies[candidate] + + self.winners = winning_candidates + + def as_dict(self): + data = super(BordaAtLarge, self).as_dict() + data["tallies"] = self.tallies + return data + +def main(): + lst = ["a", "b", "c", "d", "e", "f", "g", "h"] + ballots = [] + for p1 in unique_permutations(lst): + # ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}] + # print(ballots) + # output = BordaAtLarge(ballots) + # print(output.as_dict()["tallies"]) + # if output.as_dict()["tallies"] == {'g': 48, 'h': 56, 'f': 40, 'a': 0, 'd': 24, 'b': 8, 'c': 16, 'e': 32}: + # print(ballots) + # print("True") + + + for p2 in unique_permutations(lst): + for p3 in unique_permutations(lst): + for p4 in unique_permutations(lst): + for p5 in unique_permutations(lst): + for p6 in unique_permutations(lst): + for p7 in unique_permutations(lst): + for p8 in unique_permutations(lst): + ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p2}, {"count": 1, "ballot":p3}, {"count": 1, "ballot":p4}, {"count": 1, "ballot":p5}, {"count": 1, "ballot":p6}, {"count": 1, "ballot":p7}, {"count": 1, "ballot":p8}] + # print(ballots) + output = BordaAtLarge(ballots) + if output.as_dict()["tallies"] == {'a': 41, 'b': 34, 'c': 30, 'd': 14, 'e': 27, 'f': 27, 'g': 26, 'h': 25}: + print(ballots) + + +if __name__ == "__main__": + main() + + diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py new file mode 100644 index 0000000..40871ec --- /dev/null +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -0,0 +1,63 @@ +# +# +# +# +# +# + +def AverageFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: + """ + "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), + https://ojs.aaai.org/index.php/AAAI/article/view/7873 + + AverageFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred + candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds + to find such manipulation, and false otherwise. + + Programmer: Leora Schmerler + + >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> AverageFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) + True + + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) + True + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) + False + """ + return 0 + +def LargestFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: + """ + "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), + https://ojs.aaai.org/index.php/AAAI/article/view/7873 + + LargestFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred + candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds + to find such manipulation, and false otherwise. + + Programmer: Leora Schmerler + + >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) + True + >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) + True + + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) + True + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, + {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) + True + + """ + + return 0 + diff --git a/test_functionality/test_borda.py b/test_functionality/test_borda.py new file mode 100644 index 0000000..1ba3533 --- /dev/null +++ b/test_functionality/test_borda.py @@ -0,0 +1,64 @@ + + +from py3votecore.borda import Borda +import unittest + + +class TestBorda(unittest.TestCase): + + # Borda, no ties + def test_no_ties(self): + + # Generate data + input = [ + {"count": 26, "ballot": ["c1","c2","c3"]}, + {"count": 22, "ballot": ["c2","c1","c3"]}, + {"count": 23, "ballot": ["c3","c2","c1"]} + ] + output = Borda(input).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3']), + 'tallies': {'c3': 46, 'c2': 93, 'c1': 74}, + 'winner': 'c1' + }) + + # Borda, irrelevant ties + def test_irrelevant_ties(self): + + # Generate data + input = [ + {"count": 26, "ballot": ["c1", "c2", "c3"]}, + {"count": 23, "ballot": ["c2", "c1", "c3"]}, + {"count": 72, "ballot": ["c1", "c3", "c2"]} + ] + output = Borda(input).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3']), + 'tallies': {'c3': 72, 'c2': 72, 'c1': 219}, + 'winner': 'c1' + }) + + # Borda, relevant ties + def test_relevant_ties(self): + + # Generate data + input = [ + {"count": 49, "ballot": ["c1", "c2", "c3"]}, + {"count": 26, "ballot": ["c2", "c1", "c3"]}, + {"count": 23, "ballot": ["c3", "c2", "c1"]} + ] + output = Borda(input).as_dict() + + # Run tests + self.assertEqual(output["tallies"], {'c1': 124, 'c2': 124, 'c3': 46}) + self.assertEqual(output["tied_winners"], set(['c1', 'c2'])) + self.assertTrue(output["winner"] in output["tied_winners"]) + self.assertEqual(len(output["tie_breaker"]), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/test_functionality/test_borda_at_large.py b/test_functionality/test_borda_at_large.py new file mode 100644 index 0000000..59244eb --- /dev/null +++ b/test_functionality/test_borda_at_large.py @@ -0,0 +1,91 @@ + + + +from py3votecore.borda_at_large import BordaAtLarge +import unittest + + +class TestBordaAtLarge(unittest.TestCase): + + # Borda at Large, no ties + def test_borda_at_large_no_ties(self): + + # Generate data + output = BordaAtLarge([ + {"count": 26, "ballot": ["c1", "c2", "c3"]}, + {"count": 22, "ballot": ["c1", "c3", "c2"]}, + {"count": 23, "ballot": ["c2", "c3", "c1"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3']), + 'tallies': {'c3': 45, 'c2': 72, 'c1': 96}, + 'winners': set(['c2', 'c1']) + }) + + # Borda at Large, irrelevant ties + def test_borda_at_large_irrelevant_ties_top(self): + + # Generate data + output = BordaAtLarge([ + {"count": 16, "ballot": ["c1", "c2", "c3", "c4", "c5"]}, + {"count": 25, "ballot": ["c1", "c3", "c2", "c4", "c5"]}, + {"count": 22, "ballot": ["c2", "c3", "c1", "c5", "c4"]}, + {"count": 22, "ballot": ["c4", "c5", "c2", "c1", "c3"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output, { + 'candidates': set(['c1', 'c2', 'c3', 'c4', 'c5']), + 'tallies': {'c3': 173, 'c2': 230, 'c1': 230, 'c5': 88, 'c4': 129}, + 'winners': set(['c2', 'c1']) + }) + self.assertEqual(output["tied_winners"], set(['c2', 'c1'])) + + # Borda at Large, irrelevant ties + def test_borda_at_large_irrelevant_ties_low(self): + + # Generate data + output = BordaAtLarge([ + {"count": 30, "ballot": ["c4", "c1", "c2", "c3"]}, + {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, + {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, + {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, + {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, + {"count": 2, "ballot": ["c3", "c2", "c4", "c1"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output["tallies"], {'c3': 110, 'c2': 110, 'c1': 160, 'c4': 148}) + self.assertEqual(len(output["tie_breaker"]), 4) + self.assertEqual(output['winners'], set(['c4', 'c1'])) + self.assertEqual(len(output), 4) + + # Borda at Large, relevant ties + def test_borda_at_large_relevant_ties(self): + + + # Check these tests!!!!!!!!! + # Generate data + output = BordaAtLarge([ + {"count": 30, "ballot": ["c1", "c4", "c2", "c3"]}, + {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, + {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, + {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, + {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, + {"count": 10, "ballot": ["c1", "c2", "c4", "c3"]} + ], required_winners=2).as_dict() + + # Run tests + self.assertEqual(output["tallies"], {'c3': 104, 'c2': 126, 'c1': 220, 'c4': 126}) + self.assertEqual(len(output["tie_breaker"]), 4) + self.assertEqual(output["tied_winners"], set(['c2', 'c4'])) + self.assertTrue("c1" in output["winners"] and ("c2" in output["winners"] or "c4" in output["winners"])) + self.assertEqual(len(output), 5) + + +if __name__ == "__main__": + unittest.main() + + diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py new file mode 100644 index 0000000..f4aefbd --- /dev/null +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -0,0 +1,95 @@ + + + + +from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit +import unittest + + +class TestAverageFit(unittest.TestCase): + + # Manipulation fails + def test_4_candidates_manipulation_fails(self): + + input = [ + {"count": 26, "ballot": ["a","b","c","d"]}, + {"count": 22, "ballot": ["a","c","d","b"]}, + {"count": 23, "ballot": ["a","b","c","d"]} + ] + output = AverageFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertFalse(output) + + # Manipulation succeeds + def test_10_candidates_successful_manipulation(self): + + input = [ + {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, + {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, + {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} + ] + output = AverageFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertTrue(output) + + # Borda, relevant ties + def test_4_candidate_manipulation(self): + + # Generate data + input = [ + {"count": 2, "ballot": ["c","a","b","d"]}, + {"count": 2, "ballot": ["b","c","a","d"]}] + output = AverageFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) + + # Run tests + self.assertTrue(output) + +class TestLargestFit(unittest.TestCase): + + # Manipulation fails + def test_4_candidates_manipulation_fails(self): + + input = [ + {"count": 26, "ballot": ["a","b","c","d"]}, + {"count": 22, "ballot": ["a","c","d","b"]}, + {"count": 23, "ballot": ["a","b","c","d"]} + ] + output = LargestFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertFalse(output) + + # Manipulation succeeds + def test_10_candidates_successful_manipulation(self): + + input = [ + {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, + {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, + {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} + ] + output = LargestFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) + + # Run test + self.assertTrue(output) + + # Borda, relevant ties + def test_4_candidate_manipulation(self): + + # Generate data + input = [ + {"count": 2, "ballot": ["c","a","b","d"]}, + {"count": 2, "ballot": ["b","c","a","d"]}] + output = LargestFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) + + # Run tests + self.assertTrue(output) + + +if __name__ == "__main__": + unittest.main() + + + + From 1d13531b3244f4983796a3bb57f1c25492b8e35d Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Wed, 7 Dec 2022 09:55:54 +0200 Subject: [PATCH 04/22] modified: py3votecore/borda.py modified: py3votecore/borda_at_large.py --- py3votecore/borda.py | 2 +- py3votecore/borda_at_large.py | 84 ++--------------------------------- 2 files changed, 5 insertions(+), 81 deletions(-) diff --git a/py3votecore/borda.py b/py3votecore/borda.py index 6abcd0c..989ce6d 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -6,7 +6,7 @@ class Borda(AbstractSingleWinnerVotingSystem): def __init__(self, ballots: dict, tie_breaker=None): - super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker) + pass diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py index 5549796..634d79a 100644 --- a/py3votecore/borda_at_large.py +++ b/py3votecore/borda_at_large.py @@ -1,92 +1,16 @@ from py3votecore.abstract_classes import MultipleWinnerVotingSystem -from py3votecore.common_functions import matching_keys, unique_permutations -import copy class BordaAtLarge(MultipleWinnerVotingSystem): def __init__(self, ballots: dict, tie_breaker=None, required_winners: int=1): - super(BordaAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners) - - def calculate_results(self): - # Standardize the ballot format and extract the candidates - self.candidates = set() - for ballot in self.ballots: - - # Convert a single candidate ballots into ballot lists - if not isinstance(ballot["ballot"], list): - ballot["ballot"] = [ballot["ballot"]] - - # Ensure no ballot has an excess of votes - if len(ballot["ballot"]) < self.required_winners: - raise Exception("A ballot contained too many candidates") - - # Add all candidates on the ballot to the set - self.candidates.update(set(ballot["ballot"])) - - # Sum up all votes for each candidate - self.tallies = dict.fromkeys(self.candidates, 0) - for ballot in self.ballots: - for i in range(len(ballot["ballot"])): - self.tallies[ballot["ballot"][i]] += (ballot["count"]*(len(self.candidates)-1-i)) - tallies = copy.deepcopy(self.tallies) - - # Determine which candidates win - winning_candidates = set() - while len(winning_candidates) < self.required_winners: - - # Find the remaining candidates with the most votes - largest_tally = max(tallies.values()) - top_candidates = matching_keys(tallies, largest_tally) - - # Reduce the found candidates if there are too many - if len(top_candidates | winning_candidates) > self.required_winners: - self.tied_winners = top_candidates.copy() - while len(top_candidates | winning_candidates) > self.required_winners: - top_candidates.remove(self.break_ties(top_candidates, True)) + pass - # Move the top candidates into the winning pile - winning_candidates |= top_candidates - for candidate in top_candidates: - del tallies[candidate] - - self.winners = winning_candidates + def calculate_results(self): + return 0 def as_dict(self): - data = super(BordaAtLarge, self).as_dict() - data["tallies"] = self.tallies - return data - -def main(): - lst = ["a", "b", "c", "d", "e", "f", "g", "h"] - ballots = [] - for p1 in unique_permutations(lst): - # ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}, {"count": 1, "ballot":p1}] - # print(ballots) - # output = BordaAtLarge(ballots) - # print(output.as_dict()["tallies"]) - # if output.as_dict()["tallies"] == {'g': 48, 'h': 56, 'f': 40, 'a': 0, 'd': 24, 'b': 8, 'c': 16, 'e': 32}: - # print(ballots) - # print("True") - - - for p2 in unique_permutations(lst): - for p3 in unique_permutations(lst): - for p4 in unique_permutations(lst): - for p5 in unique_permutations(lst): - for p6 in unique_permutations(lst): - for p7 in unique_permutations(lst): - for p8 in unique_permutations(lst): - ballots = [{"count": 1, "ballot":p1}, {"count": 1, "ballot":p2}, {"count": 1, "ballot":p3}, {"count": 1, "ballot":p4}, {"count": 1, "ballot":p5}, {"count": 1, "ballot":p6}, {"count": 1, "ballot":p7}, {"count": 1, "ballot":p8}] - # print(ballots) - output = BordaAtLarge(ballots) - if output.as_dict()["tallies"] == {'a': 41, 'b': 34, 'c': 30, 'd': 14, 'e': 27, 'f': 27, 'g': 26, 'h': 25}: - print(ballots) - - -if __name__ == "__main__": - main() - + return 0 From 141b54cd0cc7cfe3c62e53ea9c9cd7ab15207c16 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Sun, 25 Dec 2022 23:33:25 +0200 Subject: [PATCH 05/22] Borda voting rule implemented and Largest and Average Fit. --- py3votecore/abstract_classes.py | 2 +- py3votecore/borda.py | 2 +- py3votecore/borda_at_large.py | 64 +++++++++++++- .../borda_manipulation_heurestics_methods.py | 86 ++++++++++++++++++- test_functionality/test_borda.py | 2 +- test_functionality/test_borda_at_large.py | 7 +- 6 files changed, 149 insertions(+), 14 deletions(-) diff --git a/py3votecore/abstract_classes.py b/py3votecore/abstract_classes.py index c40e11c..16554cb 100644 --- a/py3votecore/abstract_classes.py +++ b/py3votecore/abstract_classes.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from .tie_breaker import TieBreaker +from py3votecore.tie_breaker import TieBreaker from abc import ABCMeta, abstractmethod from copy import copy, deepcopy import types diff --git a/py3votecore/borda.py b/py3votecore/borda.py index 989ce6d..6abcd0c 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -6,7 +6,7 @@ class Borda(AbstractSingleWinnerVotingSystem): def __init__(self, ballots: dict, tie_breaker=None): - pass + super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker) diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py index 634d79a..88b2f14 100644 --- a/py3votecore/borda_at_large.py +++ b/py3votecore/borda_at_large.py @@ -1,16 +1,72 @@ from py3votecore.abstract_classes import MultipleWinnerVotingSystem - +from py3votecore.common_functions import matching_keys, unique_permutations +import copy class BordaAtLarge(MultipleWinnerVotingSystem): def __init__(self, ballots: dict, tie_breaker=None, required_winners: int=1): - pass + super(BordaAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners) def calculate_results(self): - return 0 + # Standardize the ballot format and extract the candidates + self.candidates = set() + for ballot in self.ballots: + + # Convert a single candidate ballots into ballot lists + if not isinstance(ballot["ballot"], list): + ballot["ballot"] = [ballot["ballot"]] + + # Ensure no ballot has an excess of votes + if len(ballot["ballot"]) < self.required_winners: + raise Exception("A ballot contained too many candidates") + + # Add all candidates on the ballot to the set + self.candidates.update(set(ballot["ballot"])) + + # Sum up all votes for each candidate + self.tallies = dict.fromkeys(self.candidates, 0) + for ballot in self.ballots: + for i in range(len(ballot["ballot"])): + self.tallies[ballot["ballot"][i]] += (ballot["count"]*(len(self.candidates)-1-i)) + tallies = copy.deepcopy(self.tallies) + + # Determine which candidates win + winning_candidates = set() + while len(winning_candidates) < self.required_winners: + + # Find the remaining candidates with the most votes + largest_tally = max(tallies.values()) + top_candidates = matching_keys(tallies, largest_tally) + + # Reduce the found candidates if there are too many + if len(top_candidates | winning_candidates) > self.required_winners: + self.tied_winners = top_candidates.copy() + while len(top_candidates | winning_candidates) > self.required_winners: + top_candidates.remove(self.break_ties(top_candidates, True)) + + # Move the top candidates into the winning pile + winning_candidates |= top_candidates + for candidate in top_candidates: + del tallies[candidate] + + self.winners = winning_candidates def as_dict(self): - return 0 + data = super(BordaAtLarge, self).as_dict() + data["tallies"] = self.tallies + return data + + +# if __name__ == "__main__": +# output = BordaAtLarge([ +# {"count": 30, "ballot": ["c1", "c4", "c2", "c3"]}, +# {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, +# {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, +# {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, +# {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, +# {"count": 10, "ballot": ["c1", "c2", "c4", "c3"]} +# ], required_winners=2).as_dict() +# print(output) \ No newline at end of file diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 40871ec..ea477a1 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -5,7 +5,9 @@ # # -def AverageFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: +from py3votecore.borda import Borda + +def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), https://ojs.aaai.org/index.php/AAAI/article/view/7873 @@ -30,9 +32,60 @@ def AverageFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) False """ - return 0 -def LargestFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: + candidates = {c:0 for c in ballots[0]["ballot"]} + m = len(candidates) + scores_to_give = {i:k for i in range(m-1)} + highest_score_to_give = m-2 + honest_voters = Borda(ballots) + scores = Borda(ballots).as_dict()["tallies"] + scores[candidate] += (k*(m-1)) + for c in scores: + if scores[c] > scores[candidate]: + return False + gap = {} + for c in scores.keys(): + if c != candidate: + gap[c] = scores[candidate] - scores[c] + if gap[c] < 0: + return False + for i in range(k*(m - 2), -1, -1): + # for j in range(k, 0, -1): + given_score = False + sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) + for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): + if candidates[c]= score_to_give + scores[c]: + scores[c] += score_to_give + candidates[c] += 1 + gap[c] = (scores[candidate] - scores[c])/(k - candidates[c]) + scores_to_give[score_to_give] -= 1 + if scores_to_give[score_to_give] == 0: + for k in range(highest_score_to_give, -1, -1): + if scores_to_give[score_to_give] != 0: + highest_score_to_give = k + given_score = True + break + if given_score: + break + return True + + +def LargestFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), https://ojs.aaai.org/index.php/AAAI/article/view/7873 @@ -59,5 +112,30 @@ def LargestFit(ballots: dict, candidate: str, k: int, tie_breaker=None)->bool: """ - return 0 + candidates = {c:0 for c in ballots[0]["ballot"]} + m = len(candidates) + honest_voters = Borda(ballots) + scores = Borda(ballots).as_dict()["tallies"] + scores[candidate] += (k*(m-1)) + for c in scores: + if scores[c] > scores[candidate]: + return False + gap = {} + for c in scores.keys(): + if c != candidate: + gap[c] = scores[candidate] - scores[c] + if gap[c] < 0: + return False + + for i in range(m - 2, -1, -1): + for j in range(k, 0, -1): + for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): + if v - i < 0: + return False + elif candidates[c] Date: Tue, 27 Dec 2022 00:08:54 +0200 Subject: [PATCH 06/22] modified: py3votecore/borda_manipulation_heurestics_methods.py modified: test_functionality/test_borda_at_large.py --- .../borda_manipulation_heurestics_methods.py | 70 +++++++++++-------- test_functionality/test_borda_at_large.py | 4 +- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index ea477a1..ad53f46 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -7,6 +7,30 @@ from py3votecore.borda import Borda +def find_possible_score(scores: dict, scores_to_give: dict, candidate: str, c: str, highest_score_to_give: int): + score_to_give = highest_score_to_give + if scores[candidate] <= score_to_give + scores[c]: + score_to_give = scores[candidate] - scores[c] + if score_to_give < 0: + return False + if scores_to_give[score_to_give] == 0: + no_score_to_give = True + for k in range(score_to_give, -1, -1): + if scores_to_give[k] != 0: + score_to_give = k + no_score_to_give = False + break + if no_score_to_give: + return False + return score_to_give + +def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: int, score_to_give: int): + if highest_score_to_give == score_to_give and scores_to_give[score_to_give] == 0: + for k in range(highest_score_to_give, -1, -1): + if scores_to_give[score_to_give] != 0: + return k + return highest_score_to_give + def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), @@ -37,7 +61,6 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: m = len(candidates) scores_to_give = {i:k for i in range(m-1)} highest_score_to_give = m-2 - honest_voters = Borda(ballots) scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) for c in scores: @@ -50,38 +73,24 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: if gap[c] < 0: return False for i in range(k*(m - 2), -1, -1): - # for j in range(k, 0, -1): - given_score = False + exists_given_score = False sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): if candidates[c]= score_to_give + scores[c]: - scores[c] += score_to_give - candidates[c] += 1 - gap[c] = (scores[candidate] - scores[c])/(k - candidates[c]) - scores_to_give[score_to_give] -= 1 - if scores_to_give[score_to_give] == 0: - for k in range(highest_score_to_give, -1, -1): - if scores_to_give[score_to_give] != 0: - highest_score_to_give = k - given_score = True - break - if given_score: - break + + score_to_give = find_possible_score(scores, scores_to_give, candidate, c, highest_score_to_give) + if not score_to_give: + return False + scores[c] += score_to_give + candidates[c] += 1 + gap[c] = (scores[candidate] - scores[c])/(k - candidates[c]) if candidates[c] != k else 0 + scores_to_give[score_to_give] -= 1 + + highest_score_to_give = update_highest_score_to_give(scores_to_give, highest_score_to_give, score_to_give) + exists_given_score = True + break + if exists_given_score: + break return True @@ -114,7 +123,6 @@ def LargestFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: candidates = {c:0 for c in ballots[0]["ballot"]} m = len(candidates) - honest_voters = Borda(ballots) scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) for c in scores: diff --git a/test_functionality/test_borda_at_large.py b/test_functionality/test_borda_at_large.py index 0c9ce42..7aa9670 100644 --- a/test_functionality/test_borda_at_large.py +++ b/test_functionality/test_borda_at_large.py @@ -41,7 +41,7 @@ def test_borda_at_large_irrelevant_ties_top(self): 'tallies': {'c3': 173, 'c2': 230, 'c1': 230, 'c5': 88, 'c4': 129}, 'winners': set(['c2', 'c1']) }) - # self.assertEqual(output["tied_winners"], set(['c2', 'c1'])) + # Borda at Large, irrelevant ties def test_borda_at_large_irrelevant_ties_low(self): @@ -59,7 +59,6 @@ def test_borda_at_large_irrelevant_ties_low(self): print(output) # Run tests self.assertEqual(output["tallies"], {'c3': 110, 'c2': 110, 'c1': 160, 'c4': 148}) - # self.assertEqual(len(output["tie_breaker"]), 4) self.assertEqual(output['winners'], set(['c4', 'c1'])) self.assertEqual(len(output), 3) @@ -67,7 +66,6 @@ def test_borda_at_large_irrelevant_ties_low(self): def test_borda_at_large_relevant_ties(self): - # Check these tests!!!!!!!!! # Generate data output = BordaAtLarge([ {"count": 30, "ballot": ["c1", "c4", "c2", "c3"]}, From dd6873d123ae244977bfe1ef10ad6d5bd8273db1 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Tue, 27 Dec 2022 00:19:21 +0200 Subject: [PATCH 07/22] modified: py3votecore/borda_manipulation_heurestics_methods.py --- .../borda_manipulation_heurestics_methods.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index ad53f46..d6c046c 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -31,6 +31,15 @@ def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: in return k return highest_score_to_give +def create_gap_dic(scores: dict, candidate: str): + gap = {} + for c in scores.keys(): + if c != candidate: + gap[c] = scores[candidate] - scores[c] + if gap[c] < 0: + return False + return gap + def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), @@ -63,15 +72,9 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: highest_score_to_give = m-2 scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) - for c in scores: - if scores[c] > scores[candidate]: - return False - gap = {} - for c in scores.keys(): - if c != candidate: - gap[c] = scores[candidate] - scores[c] - if gap[c] < 0: - return False + gap = create_gap_dic(scores, candidate) + if not gap: + return False for i in range(k*(m - 2), -1, -1): exists_given_score = False sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) @@ -125,16 +128,9 @@ def LargestFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: m = len(candidates) scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) - for c in scores: - if scores[c] > scores[candidate]: - return False - gap = {} - for c in scores.keys(): - if c != candidate: - gap[c] = scores[candidate] - scores[c] - if gap[c] < 0: - return False - + gap = create_gap_dic(scores, candidate) + if not gap: + return False for i in range(m - 2, -1, -1): for j in range(k, 0, -1): for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): From 5185e442e278d8f1cb7decd7538d28319f8165f4 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Wed, 28 Dec 2022 01:20:01 +0200 Subject: [PATCH 08/22] modified: py3votecore/borda.py modified: py3votecore/borda_manipulation_heurestics_methods.py modified: test_functionality/test_borda_manipulation_heurestics_methods.py --- py3votecore/borda.py | 4 +- .../borda_manipulation_heurestics_methods.py | 145 ++++++++++++------ ...t_borda_manipulation_heurestics_methods.py | 6 +- 3 files changed, 100 insertions(+), 55 deletions(-) diff --git a/py3votecore/borda.py b/py3votecore/borda.py index 6abcd0c..eaaaa44 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -1,7 +1,7 @@ -from .abstract_classes import AbstractSingleWinnerVotingSystem -from .borda_at_large import BordaAtLarge +from py3votecore.abstract_classes import AbstractSingleWinnerVotingSystem +from py3votecore.borda_at_large import BordaAtLarge class Borda(AbstractSingleWinnerVotingSystem): diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index d6c046c..2fb61a7 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -6,39 +6,9 @@ # from py3votecore.borda import Borda +import logging -def find_possible_score(scores: dict, scores_to_give: dict, candidate: str, c: str, highest_score_to_give: int): - score_to_give = highest_score_to_give - if scores[candidate] <= score_to_give + scores[c]: - score_to_give = scores[candidate] - scores[c] - if score_to_give < 0: - return False - if scores_to_give[score_to_give] == 0: - no_score_to_give = True - for k in range(score_to_give, -1, -1): - if scores_to_give[k] != 0: - score_to_give = k - no_score_to_give = False - break - if no_score_to_give: - return False - return score_to_give - -def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: int, score_to_give: int): - if highest_score_to_give == score_to_give and scores_to_give[score_to_give] == 0: - for k in range(highest_score_to_give, -1, -1): - if scores_to_give[score_to_give] != 0: - return k - return highest_score_to_give -def create_gap_dic(scores: dict, candidate: str): - gap = {} - for c in scores.keys(): - if c != candidate: - gap[c] = scores[candidate] - scores[c] - if gap[c] < 0: - return False - return gap def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: """ @@ -65,34 +35,38 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) False """ + # create and configure logger + logging.basicConfig(filename= 'my_logging.log', level= logging.DEBUG) + logger = logging.getLogger() - candidates = {c:0 for c in ballots[0]["ballot"]} - m = len(candidates) - scores_to_give = {i:k for i in range(m-1)} - highest_score_to_give = m-2 + candidates = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. + m = len(candidates) # Number of candidates. + scores_to_give = {i:k for i in range(m-1)} # Dictionary of the scores the manipulators can give. scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) - gap = create_gap_dic(scores, candidate) - if not gap: + gap = create_gap_dic(scores, candidate, k) # Dictionary of the average score gap of the candidate that the manipulators want to win to each other candidate. + highest_score_to_give = m-2 + if not gap: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. return False - for i in range(k*(m - 2), -1, -1): + for i in range(k*(m - 1)): # Number of scores to give to the candidates. + logger.debug((k*(m - 1) - i), 'th time giving score', exc_info=1) exists_given_score = False sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) - for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): + for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): # Sort according to the largest average gap. if candidates[c]bool: """ - candidates = {c:0 for c in ballots[0]["ballot"]} - m = len(candidates) + candidates = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. + m = len(candidates) # Number of candidates. scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) - gap = create_gap_dic(scores, candidate) - if not gap: + gap = create_gap_dic(scores, candidate, 1) # Dictionary of the score gap of the candidate that the manipulators want to win to each other candidate. + if not gap: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. return False - for i in range(m - 2, -1, -1): - for j in range(k, 0, -1): + for i in range(m - 2, -1, -1): # The score given in the current iteration. + for j in range(k, 0, -1): # The relaxed manipulator giving that score. for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): - if v - i < 0: + if v - i < 0: # If we add to c the score of i and it overtakes the score of candidate, LargestFit return False. return False - elif candidates[c]>> find_possible_score({"C": 11, "Carle": 10, "Barak": 7, "Diana": 5, "Rachel":10}, {0: 4, 1: 4, 2:4, 3: 4, 4: 4}, "C", "Diana", 4) + 4 + >>> find_possible_score({"C": 91, "Carle": 80, "Barak": 92, "Diana": 77, "Rachel":88}, {0: 4, 1: 4, 2: 4, 3: 0, 4: 1}, "Barak", "C", 4) + 1 + >>> find_possible_score({"A": 91, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, {0: 0, 1: 0, 2: 1, 3: 0, 4: 1}, "H", "C", 4) + False + """ + score_to_give = highest_score_to_give + if scores[candidate] <= score_to_give + scores[c]: + score_to_give = scores[candidate] - scores[c] # The highest score c can get, since it is smaller that the highest avialable one. + if score_to_give < 0: + return False + if scores_to_give[score_to_give] == 0: + no_score_to_give = True + for k in range(score_to_give, -1, -1): + if scores_to_give[k] != 0: + score_to_give = k + no_score_to_give = False + break + if no_score_to_give: + return False + return score_to_give + +def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: int): + """ + update_highest_score_to_give: accepts a dict of scores still posiible to give and an integer that is the highest possible Borda score to give. + The function outputs the next highest score possible to give. If the highest possible score is not possible anymore, that is, it was currently + given, the function searches for a lower integer that can be given next, from the next highest to 0, and returns it. If all scores were given + or the highest possible Borda score stays the same, then the function returns the highest possible Borda score. + >>> update_highest_score_to_give({0: 4, 1: 4, 2:4, 3: 4, 4: 4}, 4) + 4 + >>> update_highest_score_to_give({0: 4, 1: 4, 2: 4, 3: 0, 4: 0}, 4) + 2 + >>> update_highest_score_to_give({0: 0, 1: 0, 2: 0, 3: 0, 4: 0}, 4) + 4 + """ + if scores_to_give[highest_score_to_give] == 0: # If giving a candidate the highest possible and therefore that score is not possible anymore. + for k in range(highest_score_to_give, -1, -1): + if scores_to_give[highest_score_to_give] != 0: + return k + return highest_score_to_give + +def create_gap_dic(scores: dict, candidate: str, k: int): + """ + create_gap_dic: accepts a dict of the current scores from ballots of voters and part of the manipulators preference profile and the candidate + the manipulators want her to win. The function constructs and outputs a dict of gaps of each candidate c, to the candidate the manipulators + want to win. When the algorithm that calls this function is LargestFit, then the gap is the difference between the scores. Otherwise, the gap in AverageFit, + is an average gap. + >>> create_gap_dic({"A": 91, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "H", 1) + {"A": 8, "B": 19, "C": 1, "D": 22, "E": 11, "F": 12, "G": 10} + >>> create_gap_dic({"A": 100, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "A", 2) + {"B": 10, "C": 1, "D": 11.5, "E": 6, "F": 6.5, "G": 5.5, "H": 0.5} + >>> create_gap_dic({"A": 91, "B": 102, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "B", 5) + {"A": 2.2, "C": 0.8, "D": 5, "E": 2.8, "F": 3, "G": 2.6, "H": 0.6} + """ + gap = {} + for c in scores.keys(): + if c != candidate: + gap[c] = (scores[candidate] - scores[c])/ k # Creates the average gap for each candidate c. When LargestFit calls this function, k=1. + if gap[c] < 0: + return False + return gap + + diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py index f4aefbd..3fe3399 100644 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -32,9 +32,9 @@ def test_10_candidates_successful_manipulation(self): output = AverageFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) # Run test - self.assertTrue(output) + self.assertFalse(output) - # Borda, relevant ties + # Borda def test_4_candidate_manipulation(self): # Generate data @@ -44,7 +44,7 @@ def test_4_candidate_manipulation(self): output = AverageFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) # Run tests - self.assertTrue(output) + self.assertFalse(output) class TestLargestFit(unittest.TestCase): From 865bc682ae71aac9d11b3519a22635f31dc3618d Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Wed, 28 Dec 2022 22:17:47 +0200 Subject: [PATCH 09/22] modified: py3votecore/borda.py modified: py3votecore/borda_at_large.py modified: py3votecore/borda_manipulation_heurestics_methods.py modified: test_functionality/test_borda_manipulation_heurestics_methods.py --- py3votecore/abstract_classes.py | 2 +- py3votecore/borda.py | 4 +- py3votecore/borda_at_large.py | 4 +- .../borda_manipulation_heurestics_methods.py | 46 +++++++++---------- ...t_borda_manipulation_heurestics_methods.py | 4 +- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/py3votecore/abstract_classes.py b/py3votecore/abstract_classes.py index 16554cb..c40e11c 100644 --- a/py3votecore/abstract_classes.py +++ b/py3votecore/abstract_classes.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from py3votecore.tie_breaker import TieBreaker +from .tie_breaker import TieBreaker from abc import ABCMeta, abstractmethod from copy import copy, deepcopy import types diff --git a/py3votecore/borda.py b/py3votecore/borda.py index eaaaa44..6abcd0c 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -1,7 +1,7 @@ -from py3votecore.abstract_classes import AbstractSingleWinnerVotingSystem -from py3votecore.borda_at_large import BordaAtLarge +from .abstract_classes import AbstractSingleWinnerVotingSystem +from .borda_at_large import BordaAtLarge class Borda(AbstractSingleWinnerVotingSystem): diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py index 88b2f14..bbee9b2 100644 --- a/py3votecore/borda_at_large.py +++ b/py3votecore/borda_at_large.py @@ -1,7 +1,7 @@ -from py3votecore.abstract_classes import MultipleWinnerVotingSystem -from py3votecore.common_functions import matching_keys, unique_permutations +from .abstract_classes import MultipleWinnerVotingSystem +from .common_functions import matching_keys, unique_permutations import copy class BordaAtLarge(MultipleWinnerVotingSystem): diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 2fb61a7..6853acd 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -5,7 +5,7 @@ # # -from py3votecore.borda import Borda +from .borda import Borda import logging @@ -28,15 +28,13 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) True - >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) True - >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) False """ # create and configure logger - logging.basicConfig(filename= 'my_logging.log', level= logging.DEBUG) + logging.basicConfig(filename= 'my_logging.log', level= logging.INFO) logger = logging.getLogger() candidates = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. @@ -49,14 +47,14 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: if not gap: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. return False for i in range(k*(m - 1)): # Number of scores to give to the candidates. - logger.debug((k*(m - 1) - i), 'th time giving score', exc_info=1) + logger.debug('th time giving score') exists_given_score = False sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): # Sort according to the largest average gap. if candidates[c]bool: Programmer: Leora Schmerler - >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) + >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) True >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) True >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) True - >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) True - >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, - {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) True """ @@ -129,13 +125,15 @@ def find_possible_score(scores: dict, scores_to_give: dict, candidate: str, c: s >>> find_possible_score({"C": 91, "Carle": 80, "Barak": 92, "Diana": 77, "Rachel":88}, {0: 4, 1: 4, 2: 4, 3: 0, 4: 1}, "Barak", "C", 4) 1 >>> find_possible_score({"A": 91, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, {0: 0, 1: 0, 2: 1, 3: 0, 4: 1}, "H", "C", 4) - False + -1 + >>> find_possible_score({'b': 6, 'c': 6, 'd': 6, 'a': 6}, {0: 2, 1: 0, 2: 0}, 'd', 'b', 0) + 0 """ score_to_give = highest_score_to_give if scores[candidate] <= score_to_give + scores[c]: score_to_give = scores[candidate] - scores[c] # The highest score c can get, since it is smaller that the highest avialable one. if score_to_give < 0: - return False + return -1 if scores_to_give[score_to_give] == 0: no_score_to_give = True for k in range(score_to_give, -1, -1): @@ -144,7 +142,7 @@ def find_possible_score(scores: dict, scores_to_give: dict, candidate: str, c: s no_score_to_give = False break if no_score_to_give: - return False + return -1 return score_to_give def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: int): @@ -159,10 +157,12 @@ def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: in 2 >>> update_highest_score_to_give({0: 0, 1: 0, 2: 0, 3: 0, 4: 0}, 4) 4 + >>> update_highest_score_to_give({0: 2, 1: 0, 2: 0}, 1) + 0 """ if scores_to_give[highest_score_to_give] == 0: # If giving a candidate the highest possible and therefore that score is not possible anymore. for k in range(highest_score_to_give, -1, -1): - if scores_to_give[highest_score_to_give] != 0: + if scores_to_give[k] != 0: return k return highest_score_to_give @@ -172,12 +172,12 @@ def create_gap_dic(scores: dict, candidate: str, k: int): the manipulators want her to win. The function constructs and outputs a dict of gaps of each candidate c, to the candidate the manipulators want to win. When the algorithm that calls this function is LargestFit, then the gap is the difference between the scores. Otherwise, the gap in AverageFit, is an average gap. - >>> create_gap_dic({"A": 91, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "H", 1) - {"A": 8, "B": 19, "C": 1, "D": 22, "E": 11, "F": 12, "G": 10} - >>> create_gap_dic({"A": 100, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "A", 2) - {"B": 10, "C": 1, "D": 11.5, "E": 6, "F": 6.5, "G": 5.5, "H": 0.5} - >>> create_gap_dic({"A": 91, "B": 102, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "B", 5) - {"A": 2.2, "C": 0.8, "D": 5, "E": 2.8, "F": 3, "G": 2.6, "H": 0.6} + >>> create_gap_dic({"A": 91, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "H", 1) #doctest: +NORMALIZE_WHITESPACE + {'A': 8.0, 'B': 19.0, 'C': 1.0, 'D': 22.0, 'E': 11.0, 'F': 12.0, 'G': 10.0} + >>> create_gap_dic({"A": 100, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "A", 2) #doctest: +NORMALIZE_WHITESPACE + {'B': 10.0, 'C': 1.0, 'D': 11.5, 'E': 6.0, 'F': 6.5, 'G': 5.5, 'H': 0.5} + >>> create_gap_dic({"A": 91, "B": 102, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "B", 5) #doctest: +NORMALIZE_WHITESPACE + {'A': 2.2, 'C': 0.8, 'D': 5.0, 'E': 2.8, 'F': 3.0, 'G': 2.6, 'H': 0.6} """ gap = {} for c in scores.keys(): diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py index 3fe3399..8d82207 100644 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -32,7 +32,7 @@ def test_10_candidates_successful_manipulation(self): output = AverageFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) # Run test - self.assertFalse(output) + self.assertTrue(output) # Borda def test_4_candidate_manipulation(self): @@ -44,7 +44,7 @@ def test_4_candidate_manipulation(self): output = AverageFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) # Run tests - self.assertFalse(output) + self.assertTrue(output) class TestLargestFit(unittest.TestCase): From 1ecea1efc45acb5cdb43b8fd9f2dd0cb544a2fc0 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Tue, 3 Jan 2023 00:11:07 +0200 Subject: [PATCH 10/22] Fixed conditions in the algorithm --- py3votecore/borda_manipulation_heurestics_methods.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 6853acd..65af7f3 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -48,11 +48,9 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: return False for i in range(k*(m - 1)): # Number of scores to give to the candidates. logger.debug('th time giving score') - exists_given_score = False sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): # Sort according to the largest average gap. if candidates[c]bool: scores_to_give[score_to_give] -= 1 highest_score_to_give = update_highest_score_to_give(scores_to_give, highest_score_to_give) - exists_given_score = True - break - if exists_given_score: # There was given a score in this iteration, then AverageFit continues to the next iteratin. break return True @@ -104,9 +99,9 @@ def LargestFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: for i in range(m - 2, -1, -1): # The score given in the current iteration. for j in range(k, 0, -1): # The relaxed manipulator giving that score. for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): - if v - i < 0: # If we add to c the score of i and it overtakes the score of candidate, LargestFit return False. - return False - elif candidates[c] Date: Fri, 6 Jan 2023 12:19:01 +0200 Subject: [PATCH 11/22] The heurestics modified to return a successfull manipulation if such exist modified: py3votecore/borda_manipulation_heurestics_methods.py modified: test_functionality/test_borda_manipulation_heurestics_methods.py --- .../borda_manipulation_heurestics_methods.py | 127 +++++++++++++----- ...t_borda_manipulation_heurestics_methods.py | 122 ++++++++++++++--- 2 files changed, 202 insertions(+), 47 deletions(-) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 65af7f3..55c19ed 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -7,10 +7,10 @@ from .borda import Borda import logging +import networkx as nx - -def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: +def AverageFit(ballots: list, candidate: str, k: int)->list or bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), https://ojs.aaai.org/index.php/AAAI/article/view/7873 @@ -21,16 +21,16 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: Programmer: Leora Schmerler - >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> AverageFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) - True + >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE + [['d', 'a', 'c', 'b'], ['d', 'b', 'a', 'c']] + >>> AverageFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE + [['d', 'a', 'c', 'b'], ['d', 'a', 'c', 'b']] + >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE + [['d', 'c', 'a', 'b'], ['d', 'c', 'a', 'b']] - >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) - True - >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5) # doctest:+ELLIPSIS + [...] + >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4) False """ # create and configure logger @@ -39,15 +39,18 @@ def AverageFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: candidates = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. m = len(candidates) # Number of candidates. + manipulators = [["" for j in range(m)] for i in range(k)] # Creating the manipulators preference profile. scores_to_give = {i:k for i in range(m-1)} # Dictionary of the scores the manipulators can give. scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) + for manipulator in manipulators: + manipulator[0] = candidate gap = create_gap_dic(scores, candidate, k) # Dictionary of the average score gap of the candidate that the manipulators want to win to each other candidate. highest_score_to_give = m-2 if not gap: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. return False for i in range(k*(m - 1)): # Number of scores to give to the candidates. - logger.debug('th time giving score') + logger.debug(f'{i}th time giving score') sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): # Sort according to the largest average gap. if candidates[c]bool: candidates[c] += 1 gap[c] = (scores[candidate] - scores[c])/(k - candidates[c]) if candidates[c] != k else 0 scores_to_give[score_to_give] -= 1 - + manipulators[k-1-scores_to_give[score_to_give]][m-1-score_to_give] = c highest_score_to_give = update_highest_score_to_give(scores_to_give, highest_score_to_give) + if candidates[c]==k: + del gap[c] break - return True + if not is_legal(manipulators, candidates): + return legal_manipulation(manipulators, candidates.keys()) + return manipulators + # return True -def LargestFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: +def LargestFit(ballots: list, candidate: str, k: int)->list or bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), https://ojs.aaai.org/index.php/AAAI/article/view/7873 @@ -75,41 +83,50 @@ def LargestFit(ballots: list, candidate: str, k: int, tie_breaker=None)->bool: Programmer: Leora Schmerler - >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2, ["d", "c", "b", "a"]) - True - >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2, ["d", "c", "b", "a"]) - True + >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE + [['d', 'a', 'c', 'b'], ['d', 'b', 'a', 'c']] + >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2) # doctest:+ELLIPSIS + [...] + >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE + [['d', 'c', 'a', 'b'], ['d', 'a', 'c', 'b']] - >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5, ["d", "c", "b", "a"]) - True - >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4, ["d", "c", "b", "a"]) - True + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5) # doctest:+ELLIPSIS + [...] + >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4) # doctest:+ELLIPSIS + [...] """ candidates = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. m = len(candidates) # Number of candidates. + manipulators = [["" for j in range(m)] for i in range(k)] # Creating the manipulators preference profile. scores = Borda(ballots).as_dict()["tallies"] scores[candidate] += (k*(m-1)) + for manipulator in manipulators: + manipulator[0] = candidate gap = create_gap_dic(scores, candidate, 1) # Dictionary of the score gap of the candidate that the manipulators want to win to each other candidate. if not gap: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. return False for i in range(m - 2, -1, -1): # The score given in the current iteration. - for j in range(k, 0, -1): # The relaxed manipulator giving that score. + for j in range(k): # The relaxed manipulator giving that score. for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): if candidates[c]int: """ find_possible_score: accepts a dict of the current scores from ballots of voters and part of the manipulators preference profile, a dict of scores still posiible to give, a string of the candidate the manipulators want her to win, a string c of a candidate the manipulators want to give the next Borda score and an integer of the highest @@ -140,7 +157,7 @@ def find_possible_score(scores: dict, scores_to_give: dict, candidate: str, c: s return -1 return score_to_give -def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: int): +def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: int)->int: """ update_highest_score_to_give: accepts a dict of scores still posiible to give and an integer that is the highest possible Borda score to give. The function outputs the next highest score possible to give. If the highest possible score is not possible anymore, that is, it was currently @@ -161,7 +178,7 @@ def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: in return k return highest_score_to_give -def create_gap_dic(scores: dict, candidate: str, k: int): +def create_gap_dic(scores: dict, candidate: str, k: int)->dict: """ create_gap_dic: accepts a dict of the current scores from ballots of voters and part of the manipulators preference profile and the candidate the manipulators want her to win. The function constructs and outputs a dict of gaps of each candidate c, to the candidate the manipulators @@ -182,4 +199,54 @@ def create_gap_dic(scores: dict, candidate: str, k: int): return False return gap +def is_legal(manipulators: list, candidates: dict)->bool: + """ + is_legal: accepts a list of manipulators, and their preference order list and a dict of candidates, where each candidate was placed k times + The function checks if this manipulation is legal, that is, if there exists a manipulator that places a candidate more than one (and therefore, + doesn't place at least one candidate in her preference order). + >>> is_legal([["A","B","C","D"],["A","B","C","D"],["A","B","C","D"]], {"A":3, "B":3, "C":3, "D":3}) + True + >>> is_legal([["C","D","B","A"],["A","D","C","B"],["D","A","C","B"]], {"A":3, "B":3, "C":3, "D":3}) + True + >>> is_legal([["C","D","B","A"],["A","D","C","B"],["D","A","C","B"],["D","A","A","B"],["D","C","C","B"]], {"A":5, "B":5, "C":5, "D":5}) + False + """ + for i in range(len(manipulators)-1, -1, -1): + for c in manipulators[i]: + candidates[c] = candidates[c] - 1 + if candidates[c] != i: + return False + return True + +def legal_manipulation(manipulators: list, candidates: set)->list: + """ + legal_manipulation: accepts a list of manipulators, and their preference order list that is not a legal manipulation and a set of candidates. + The function return a leagl manipulation. + """ + G = nx.Graph() + G.add_nodes_from([j for j in range(len(manipulators))], bipartite=0) + G.add_nodes_from(candidates, bipartite=1) + E = [(j, manipulators[i][j]) for i in range(len(manipulators)) for j in range(len(manipulators[i]))] + is_more_than_one_component = False + G.add_edges_from(E) + components = [G.subgraph(c).copy() for c in nx.connected_components(G)] + for i in range(len(manipulators)): + if is_more_than_one_component: + components = [G.subgraph(c).copy() for c in nx.connected_components(G)] + is_more_than_one_component = False + match = {} + for k in range(len(components)): + match.update(nx.bipartite.maximum_matching(components[k])) + for j in range(len(manipulators[i])): + manipulators[i][j] = match[j] + E.remove((j, match[j])) + if E.count((j, match[j])) == 0: + G.remove_edge(j, match[j]) + is_more_than_one_component = True + return manipulators + +if __name__ == '__main__': + import doctest + (failures, tests) = doctest.testmod(report=True) + print("{} failures, {} tests".format(failures, tests)) \ No newline at end of file diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py index 8d82207..bf913fe 100644 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -2,8 +2,10 @@ +import random from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit import unittest +import time class TestAverageFit(unittest.TestCase): @@ -15,8 +17,8 @@ def test_4_candidates_manipulation_fails(self): {"count": 26, "ballot": ["a","b","c","d"]}, {"count": 22, "ballot": ["a","c","d","b"]}, {"count": 23, "ballot": ["a","b","c","d"]} - ] - output = AverageFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) + ] + output = AverageFit(ballots=input, candidate="d", k=60) # Run test self.assertFalse(output) @@ -26,13 +28,18 @@ def test_10_candidates_successful_manipulation(self): input = [ {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, - {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, + {"count": 53, "ballot": ["a", "b", "j", "d", "g", "e", "h", "f", "c", "i"]}, {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} - ] - output = AverageFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) - + ] # "a" = 1143, "b" = 1136, "c" = 811, "d" = 1122, "e" = 702, "f" = 426, "g" = 415, "h" = 499, "i" = 170, "j" = 371 + output = AverageFit(ballots=input, candidate="d", k=8) + # [["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"]] + # "a" = 1143, "b" = 1144, "c" = 827, "d" = 1194, "e" = 726, "f" = 466, "g" = 463, "h" = 531, "i" = 234, "j" = 427 # Run test - self.assertTrue(output) + self.assertEqual(len(output), 8) + for manip in output: + self.assertEqual(len(manip), 10) + for i,j in [(0,'d'),(1,'i'),(2,'j'),(3,'g'),(4,'f'),(5,'h'),(6,'e'),(7,'c'),(8,'b'),(9,'a')]: + self.assertEqual(manip[i],j) # Borda def test_4_candidate_manipulation(self): @@ -41,11 +48,58 @@ def test_4_candidate_manipulation(self): input = [ {"count": 2, "ballot": ["c","a","b","d"]}, {"count": 2, "ballot": ["b","c","a","d"]}] - output = AverageFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) - + # "a" = 6, "b" = 8, "c" = 10, "d" = 0 + output = AverageFit(ballots=input, candidate="d", k=5) + # [["d","a","b","c"],["d","a","b","c"],["d","a","b","c"],["d","a","b","c"],["d","b","c","a"]] + # "a" = 14, "b" = 13, "c" = 11, "d" = 15 # Run tests + self.assertEqual(len(output),5) + for manip in output: + self.assertEqual(len(manip), 4) + self.assertEqual(manip[0],'d') + + def test_100_candidate_manipulation(self): + ballots = [] + for i in range(20): + ballots.append({"count": 2, "ballot": create_vote(100)}) + start = time.process_time() + print(AverageFit(ballots, "A", 4)) + end = time.process_time() + duration_in_seconds = end-start + output = duration_in_seconds < 10 + self.assertTrue(output) + + + def test_1000_voters_manipulation(self): + ballots = [] + for i in range(1000): + ballots.append({"count": 1, "ballot": create_vote(50)}) + start = time.process_time() + print(AverageFit(ballots, "A", 100)) + end = time.process_time() + duration_in_seconds = end-start + output = duration_in_seconds < 10 self.assertTrue(output) +def create_alphabet_vote(m): + ABC = ['Z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y'] + preference = [] + for i in range(1, m + 1): + tmp = '' + c = i + while c >= 0: + tmp += ABC[c % 26] + c = c - 26 + if c == 0: + c = -1 + preference.append(tmp) + return preference +def create_vote(m): + preference = create_alphabet_vote(m) + random.shuffle(preference) + return preference + class TestLargestFit(unittest.TestCase): # Manipulation fails @@ -56,7 +110,7 @@ def test_4_candidates_manipulation_fails(self): {"count": 22, "ballot": ["a","c","d","b"]}, {"count": 23, "ballot": ["a","b","c","d"]} ] - output = LargestFit(ballots=input, candidate="d", k=60, tie_breaker=["d","c","b","a"]) + output = LargestFit(ballots=input, candidate="d", k=60) # Run test self.assertFalse(output) @@ -66,13 +120,18 @@ def test_10_candidates_successful_manipulation(self): input = [ {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]}, - {"count": 53, "ballot": ["a", "b", "j", "d", "i", "e", "h", "f", "c", "i"]}, + {"count": 53, "ballot": ["a", "b", "j", "d", "g", "e", "h", "f", "c", "i"]}, {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} - ] - output = LargestFit(ballots=input, candidate="d", k=8, tie_breaker=["d","c","b","a"]) - + ] # "a" = 1143, "b" = 1136, "c" = 811, "d" = 1122, "e" = 702, "f" = 426, "g" = 415, "h" = 499, "i" = 170, "j" = 371 + output = LargestFit(ballots=input, candidate="d", k=8) + # [["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","g","j","f","h","e","c","b","a"]] + # "a" = 1143, "b" = 1144, "c" = 827, "d" = 1194, "e" = 726, "f" = 469, "g" = 461, "h" = 531, "i" = 234, "j" = 426 # Run test - self.assertTrue(output) + self.assertEqual(len(output),8) + for manip in output: + self.assertEqual(len(manip), 10) + for i,j in [(0,'d'),(1,'i'),(5,'h'),(6,'e'),(7,'c')]: + self.assertEqual(manip[i],j) # Borda, relevant ties def test_4_candidate_manipulation(self): @@ -81,12 +140,41 @@ def test_4_candidate_manipulation(self): input = [ {"count": 2, "ballot": ["c","a","b","d"]}, {"count": 2, "ballot": ["b","c","a","d"]}] - output = LargestFit(ballots=input, candidate="d", k=5, tie_breaker=["d", "c", "b", "a"]) - + # "a" = 6, "b" = 8, "c" = 10, "d" = 0 + output = LargestFit(ballots=input, candidate="d", k=5) + # [["d","a","c","b"],["d","a","c","b"],["d","b","a","c"],["d","a","b","c"],["d","b","c","a"]] # Run tests + self.assertEqual(len(output),5) + for manip in output: + self.assertEqual(len(manip), 4) + self.assertEqual(manip[0],'d') + + def test_100_candidate_manipulation(self): + + ballots = [] + for i in range(20): + ballots.append({"count": 2, "ballot": create_vote(100)}) + start = time.process_time() + print(LargestFit(ballots, "A", 4)) + end = time.process_time() + duration_in_seconds = end-start + output = duration_in_seconds < 10 + self.assertTrue(output) + + + def test_1000_voters_manipulation(self): + ballots = [] + for i in range(1000): + ballots.append({"count": 1, "ballot": create_vote(50)}) + start = time.process_time() + print(LargestFit(ballots, "A", 100)) + end = time.process_time() + duration_in_seconds = end-start + output = duration_in_seconds < 10 self.assertTrue(output) + if __name__ == "__main__": unittest.main() From 3cc4925bdcd392a0a711f0f2c68ca6b2351f0572 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:13:36 +0200 Subject: [PATCH 12/22] modified: py3votecore/borda.py modified: py3votecore/borda_manipulation_heurestics_methods.py --- py3votecore/borda.py | 2 +- .../borda_manipulation_heurestics_methods.py | 205 +++++++++--------- 2 files changed, 100 insertions(+), 107 deletions(-) diff --git a/py3votecore/borda.py b/py3votecore/borda.py index 6abcd0c..f974b25 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -5,7 +5,7 @@ class Borda(AbstractSingleWinnerVotingSystem): - def __init__(self, ballots: dict, tie_breaker=None): + def __init__(self, ballots, tie_breaker=None): super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 55c19ed..374e020 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -5,12 +5,11 @@ # # -from .borda import Borda -import logging +from py3votecore.borda import Borda import networkx as nx -def AverageFit(ballots: list, candidate: str, k: int)->list or bool: +def AverageFit(ballots: list, preferred_candidate: str, k: int)->list or bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), https://ojs.aaai.org/index.php/AAAI/article/view/7873 @@ -21,8 +20,8 @@ def AverageFit(ballots: list, candidate: str, k: int)->list or bool: Programmer: Leora Schmerler - >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE - [['d', 'a', 'c', 'b'], ['d', 'b', 'a', 'c']] + >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+ELLIPSIS + [...] >>> AverageFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE [['d', 'a', 'c', 'b'], ['d', 'a', 'c', 'b']] >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE @@ -33,46 +32,39 @@ def AverageFit(ballots: list, candidate: str, k: int)->list or bool: >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4) False """ - # create and configure logger - logging.basicConfig(filename= 'my_logging.log', level= logging.INFO) - logger = logging.getLogger() - - candidates = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. - m = len(candidates) # Number of candidates. + map_candidates_times_placed = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. + m = len(map_candidates_times_placed) # Number of candidates. manipulators = [["" for j in range(m)] for i in range(k)] # Creating the manipulators preference profile. - scores_to_give = {i:k for i in range(m-1)} # Dictionary of the scores the manipulators can give. - scores = Borda(ballots).as_dict()["tallies"] - scores[candidate] += (k*(m-1)) + map_scores_to_give = {i:k for i in range(m-1)} # Dictionary of the scores the manipulators can give. + current_scores = Borda(ballots).as_dict()["tallies"] + current_scores[preferred_candidate] += (k*(m-1)) + map_candidates_times_placed[preferred_candidate] = k for manipulator in manipulators: - manipulator[0] = candidate - gap = create_gap_dic(scores, candidate, k) # Dictionary of the average score gap of the candidate that the manipulators want to win to each other candidate. + manipulator[0] = preferred_candidate + average_gap = create_gap_dic(current_scores, preferred_candidate, k) # Dictionary of the average score gap of the candidate that the manipulators want to win to each other candidate. highest_score_to_give = m-2 - if not gap: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. + if average_gap == False: # If average_gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. return False for i in range(k*(m - 1)): # Number of scores to give to the candidates. - logger.debug(f'{i}th time giving score') - sorted(gap.items(), key=lambda item: candidates[item[0]], reverse = True) - for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): # Sort according to the largest average gap. - if candidates[c]list or bool: + current_c = max(average_gap, key = lambda a: (average_gap[a], map_candidates_times_placed[a])) + current_score_to_give = find_possible_score(current_scores, map_scores_to_give, preferred_candidate, current_c, highest_score_to_give) + if current_score_to_give == -1: # If there is no available score that is possible to give c, AverageFit returns False. + return False + current_scores[current_c] += current_score_to_give + map_candidates_times_placed[current_c] += 1 + average_gap[current_c] = (current_scores[preferred_candidate] - current_scores[current_c])/(k - map_candidates_times_placed[current_c]) if map_candidates_times_placed[current_c] != k else 0 + map_scores_to_give[current_score_to_give] -= 1 + manipulators[k-1-map_scores_to_give[current_score_to_give]][m-1-current_score_to_give] = current_c + highest_score_to_give = update_highest_score_to_give(map_scores_to_give, highest_score_to_give) + if map_candidates_times_placed[current_c]==k: + del average_gap[current_c] + if is_legal(manipulators, map_candidates_times_placed): + return manipulators + else: + return legal_manipulation(manipulators, map_candidates_times_placed.keys()) + + +def LargestFit(ballots: list, preferred_candidate: str, k: int)->list or bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), https://ojs.aaai.org/index.php/AAAI/article/view/7873 @@ -83,12 +75,12 @@ def LargestFit(ballots: list, candidate: str, k: int)->list or bool: Programmer: Leora Schmerler - >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE - [['d', 'a', 'c', 'b'], ['d', 'b', 'a', 'c']] + >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+ELLIPSIS + [...] >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2) # doctest:+ELLIPSIS [...] - >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE - [['d', 'c', 'a', 'b'], ['d', 'a', 'c', 'b']] + >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2) # doctest:+ELLIPSIS + [...] >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5) # doctest:+ELLIPSIS [...] @@ -97,36 +89,35 @@ def LargestFit(ballots: list, candidate: str, k: int)->list or bool: """ - candidates = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. - m = len(candidates) # Number of candidates. + map_candidates_times_placed = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate. + m = len(map_candidates_times_placed) # Number of candidates. manipulators = [["" for j in range(m)] for i in range(k)] # Creating the manipulators preference profile. - scores = Borda(ballots).as_dict()["tallies"] - scores[candidate] += (k*(m-1)) + current_scores = Borda(ballots).as_dict()["tallies"] + current_scores[preferred_candidate] += (k*(m-1)) + map_candidates_times_placed[preferred_candidate] = k for manipulator in manipulators: - manipulator[0] = candidate - gap = create_gap_dic(scores, candidate, 1) # Dictionary of the score gap of the candidate that the manipulators want to win to each other candidate. - if not gap: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. + manipulator[0] = preferred_candidate + gap = create_gap_dic(current_scores, preferred_candidate, 1) # Dictionary of the score gap of the candidate that the manipulators want to win to each other candidate. + if gap == False: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False. return False for i in range(m - 2, -1, -1): # The score given in the current iteration. for j in range(k): # The relaxed manipulator giving that score. - for c,v in sorted(gap.items(), key=lambda item: item[1], reverse = True): - if candidates[c]int: +def find_possible_score(current_scores: dict, map_scores_to_give: dict, preferred_candidate: str, current_c: str, highest_score_to_give: int)->int: """ find_possible_score: accepts a dict of the current scores from ballots of voters and part of the manipulators preference profile, a dict of scores still posiible to give, a string of the candidate the manipulators want her to win, a string c of a candidate the manipulators want to give the next Borda score and an integer of the highest @@ -141,23 +132,17 @@ def find_possible_score(scores: dict, scores_to_give: dict, candidate: str, c: s >>> find_possible_score({'b': 6, 'c': 6, 'd': 6, 'a': 6}, {0: 2, 1: 0, 2: 0}, 'd', 'b', 0) 0 """ - score_to_give = highest_score_to_give - if scores[candidate] <= score_to_give + scores[c]: - score_to_give = scores[candidate] - scores[c] # The highest score c can get, since it is smaller that the highest avialable one. - if score_to_give < 0: + current_score_to_give = highest_score_to_give + if current_scores[preferred_candidate] <= current_score_to_give + current_scores[current_c]: + current_score_to_give = current_scores[preferred_candidate] - current_scores[current_c] # The highest score c can get, since it is smaller that the highest avialable one. + if map_scores_to_give[current_score_to_give] == 0: + for k in range(current_score_to_give, -1, -1): + if map_scores_to_give[k] != 0: + return k return -1 - if scores_to_give[score_to_give] == 0: - no_score_to_give = True - for k in range(score_to_give, -1, -1): - if scores_to_give[k] != 0: - score_to_give = k - no_score_to_give = False - break - if no_score_to_give: - return -1 - return score_to_give - -def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: int)->int: + return current_score_to_give + +def update_highest_score_to_give(map_scores_to_give :dict, highest_score_to_give: int)->int: """ update_highest_score_to_give: accepts a dict of scores still posiible to give and an integer that is the highest possible Borda score to give. The function outputs the next highest score possible to give. If the highest possible score is not possible anymore, that is, it was currently @@ -172,13 +157,13 @@ def update_highest_score_to_give(scores_to_give :dict, highest_score_to_give: in >>> update_highest_score_to_give({0: 2, 1: 0, 2: 0}, 1) 0 """ - if scores_to_give[highest_score_to_give] == 0: # If giving a candidate the highest possible and therefore that score is not possible anymore. - for k in range(highest_score_to_give, -1, -1): - if scores_to_give[k] != 0: + if map_scores_to_give[highest_score_to_give] == 0: # If giving a candidate the highest possible score and therefore that score is not possible anymore. + for k in range(highest_score_to_give - 1, -1, -1): + if map_scores_to_give[k] != 0: return k return highest_score_to_give -def create_gap_dic(scores: dict, candidate: str, k: int)->dict: +def create_gap_dic(current_scores: dict, preferred_candidate: str, k: int)->dict: """ create_gap_dic: accepts a dict of the current scores from ballots of voters and part of the manipulators preference profile and the candidate the manipulators want her to win. The function constructs and outputs a dict of gaps of each candidate c, to the candidate the manipulators @@ -192,14 +177,14 @@ def create_gap_dic(scores: dict, candidate: str, k: int)->dict: {'A': 2.2, 'C': 0.8, 'D': 5.0, 'E': 2.8, 'F': 3.0, 'G': 2.6, 'H': 0.6} """ gap = {} - for c in scores.keys(): - if c != candidate: - gap[c] = (scores[candidate] - scores[c])/ k # Creates the average gap for each candidate c. When LargestFit calls this function, k=1. + for c in current_scores.keys(): + if c != preferred_candidate: + gap[c] = (current_scores[preferred_candidate] - current_scores[c])/ k # Creates the average gap for each candidate c. When LargestFit calls this function, k=1. if gap[c] < 0: return False return gap -def is_legal(manipulators: list, candidates: dict)->bool: +def is_legal(manipulators: list, map_candidates_times_placed: dict)->bool: """ is_legal: accepts a list of manipulators, and their preference order list and a dict of candidates, where each candidate was placed k times The function checks if this manipulation is legal, that is, if there exists a manipulator that places a candidate more than one (and therefore, @@ -210,11 +195,13 @@ def is_legal(manipulators: list, candidates: dict)->bool: True >>> is_legal([["C","D","B","A"],["A","D","C","B"],["D","A","C","B"],["D","A","A","B"],["D","C","C","B"]], {"A":5, "B":5, "C":5, "D":5}) False + >>> is_legal([['d', 'h', 'g', 'c', 'e', 'f', 'b', 'a'], ['d', 'h', 'g', 'c', 'e', 'f', 'b', 'a'], ['d', 'g', 'f', 'e', 'c', 'h', 'b', 'a'], ['d', 'e', 'h', 'f', 'c', 'g', 'b', 'a'], ['d', 'f', 'e', 'c', 'h', 'g', 'b', 'a']], {"a":5, "b":5, "c":5, "d":5, "e":5, "f":5, "g":5, "h":5}) + True """ for i in range(len(manipulators)-1, -1, -1): for c in manipulators[i]: - candidates[c] = candidates[c] - 1 - if candidates[c] != i: + map_candidates_times_placed[c] = map_candidates_times_placed[c] - 1 + if map_candidates_times_placed[c] != i: return False return True @@ -223,30 +210,36 @@ def legal_manipulation(manipulators: list, candidates: set)->list: """ legal_manipulation: accepts a list of manipulators, and their preference order list that is not a legal manipulation and a set of candidates. The function return a leagl manipulation. + >>> legal_manipulation([["C","D","B","A"],["A","D","C","B"],["D","A","C","B"],["D","A","A","B"],["D","C","C","B"]], {"A", "B", "C", "D"})# doctest:+ELLIPSIS + [...] + >>> legal_manipulation([["C","D","B","E","A"],["A","E","D","C","B"],["D","A","E","E","B"],["D","E","A","A","B"],["D","C","C","C","B"]], {"A", "B", "C", "D", "E"})# doctest:+ELLIPSIS + [...] """ G = nx.Graph() G.add_nodes_from([j for j in range(len(manipulators))], bipartite=0) G.add_nodes_from(candidates, bipartite=1) - E = [(j, manipulators[i][j]) for i in range(len(manipulators)) for j in range(len(manipulators[i]))] - is_more_than_one_component = False - G.add_edges_from(E) - components = [G.subgraph(c).copy() for c in nx.connected_components(G)] for i in range(len(manipulators)): - if is_more_than_one_component: - components = [G.subgraph(c).copy() for c in nx.connected_components(G)] - is_more_than_one_component = False - match = {} - for k in range(len(components)): - match.update(nx.bipartite.maximum_matching(components[k])) + for j in range(len(manipulators[i])): + # Each edge's whight is for the amount of time a candidate recieves j points. + if (j, manipulators[i][j]) in G.edges: + G.edges[j, manipulators[i][j]]["weight"] += 1 + else: + G.add_edge(j, manipulators[i][j], weight=1) + match = nx.bipartite.maximum_matching(G, top_nodes=candidates) # Each match is for each manipulaotr's placing. + for i in range(len(manipulators)): for j in range(len(manipulators[i])): manipulators[i][j] = match[j] - E.remove((j, match[j])) - if E.count((j, match[j])) == 0: + G.edges[j, match[j]]["weight"] -= 1 # Removing weight for each match placed. + if G.edges[j, match[j]]["weight"] == 0: G.remove_edge(j, match[j]) - is_more_than_one_component = True + match = nx.bipartite.maximum_matching(G, top_nodes=candidates) # Preparing the next manipulators placing. return manipulators if __name__ == '__main__': import doctest (failures, tests) = doctest.testmod(report=True) - print("{} failures, {} tests".format(failures, tests)) \ No newline at end of file + print("{} failures, {} tests".format(failures, tests)) + + + + From c47cba5e1f90447b3bbb97ca1ec31b57cbdb6041 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:25:47 +0200 Subject: [PATCH 13/22] modified: py3votecore/borda.py modified: py3votecore/borda_manipulation_heurestics_methods.py --- py3votecore/borda.py | 1 + py3votecore/borda_manipulation_heurestics_methods.py | 1 + 2 files changed, 2 insertions(+) diff --git a/py3votecore/borda.py b/py3votecore/borda.py index f974b25..46a42ab 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -1,5 +1,6 @@ + from .abstract_classes import AbstractSingleWinnerVotingSystem from .borda_at_large import BordaAtLarge diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 374e020..3bc95ac 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -4,6 +4,7 @@ # # # +# from py3votecore.borda import Borda import networkx as nx From 50915b1b54c27edeb07422d570f81a18f848fa3b Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Sun, 15 Jan 2023 20:23:27 +0200 Subject: [PATCH 14/22] changes of parameters in test file, and some notes edited. modified: py3votecore/borda_manipulation_heurestics_methods.py modified: test_functionality/test_borda_manipulation_heurestics_methods.py --- py3votecore/borda_manipulation_heurestics_methods.py | 7 +++---- .../test_borda_manipulation_heurestics_methods.py | 12 ++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 3bc95ac..0389534 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -5,8 +5,7 @@ # # # - -from py3votecore.borda import Borda +from .borda import Borda import networkx as nx @@ -16,7 +15,7 @@ def AverageFit(ballots: list, preferred_candidate: str, k: int)->list or bool: https://ojs.aaai.org/index.php/AAAI/article/view/7873 AverageFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred - candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds + candidate will be elected by the Borda voting rule, tie-breaks in faver of the preferred_candidate. The algorithm outputs true if it succeeds to find such manipulation, and false otherwise. Programmer: Leora Schmerler @@ -71,7 +70,7 @@ def LargestFit(ballots: list, preferred_candidate: str, k: int)->list or bool: https://ojs.aaai.org/index.php/AAAI/article/view/7873 LargestFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred - candidate will be elected by the Borda voting rule, with a tie-breaker, if recieved one. The algorithm outputs true if it succeeds + candidate will be elected by the Borda voting rule, tie-breaks in faver of the preferred_candidate. The algorithm outputs true if it succeeds to find such manipulation, and false otherwise. Programmer: Leora Schmerler diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py index bf913fe..29918bb 100644 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -18,7 +18,7 @@ def test_4_candidates_manipulation_fails(self): {"count": 22, "ballot": ["a","c","d","b"]}, {"count": 23, "ballot": ["a","b","c","d"]} ] - output = AverageFit(ballots=input, candidate="d", k=60) + output = AverageFit(ballots=input, preferred_candidate="d", k=60) # Run test self.assertFalse(output) @@ -31,7 +31,7 @@ def test_10_candidates_successful_manipulation(self): {"count": 53, "ballot": ["a", "b", "j", "d", "g", "e", "h", "f", "c", "i"]}, {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} ] # "a" = 1143, "b" = 1136, "c" = 811, "d" = 1122, "e" = 702, "f" = 426, "g" = 415, "h" = 499, "i" = 170, "j" = 371 - output = AverageFit(ballots=input, candidate="d", k=8) + output = AverageFit(ballots=input, preferred_candidate="d", k=8) # [["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"]] # "a" = 1143, "b" = 1144, "c" = 827, "d" = 1194, "e" = 726, "f" = 466, "g" = 463, "h" = 531, "i" = 234, "j" = 427 # Run test @@ -49,7 +49,7 @@ def test_4_candidate_manipulation(self): {"count": 2, "ballot": ["c","a","b","d"]}, {"count": 2, "ballot": ["b","c","a","d"]}] # "a" = 6, "b" = 8, "c" = 10, "d" = 0 - output = AverageFit(ballots=input, candidate="d", k=5) + output = AverageFit(ballots=input, preferred_candidate="d", k=5) # [["d","a","b","c"],["d","a","b","c"],["d","a","b","c"],["d","a","b","c"],["d","b","c","a"]] # "a" = 14, "b" = 13, "c" = 11, "d" = 15 # Run tests @@ -110,7 +110,7 @@ def test_4_candidates_manipulation_fails(self): {"count": 22, "ballot": ["a","c","d","b"]}, {"count": 23, "ballot": ["a","b","c","d"]} ] - output = LargestFit(ballots=input, candidate="d", k=60) + output = LargestFit(ballots=input, preferred_candidate="d", k=60) # Run test self.assertFalse(output) @@ -123,7 +123,7 @@ def test_10_candidates_successful_manipulation(self): {"count": 53, "ballot": ["a", "b", "j", "d", "g", "e", "h", "f", "c", "i"]}, {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]} ] # "a" = 1143, "b" = 1136, "c" = 811, "d" = 1122, "e" = 702, "f" = 426, "g" = 415, "h" = 499, "i" = 170, "j" = 371 - output = LargestFit(ballots=input, candidate="d", k=8) + output = LargestFit(ballots=input, preferred_candidate="d", k=8) # [["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","g","j","f","h","e","c","b","a"]] # "a" = 1143, "b" = 1144, "c" = 827, "d" = 1194, "e" = 726, "f" = 469, "g" = 461, "h" = 531, "i" = 234, "j" = 426 # Run test @@ -141,7 +141,7 @@ def test_4_candidate_manipulation(self): {"count": 2, "ballot": ["c","a","b","d"]}, {"count": 2, "ballot": ["b","c","a","d"]}] # "a" = 6, "b" = 8, "c" = 10, "d" = 0 - output = LargestFit(ballots=input, candidate="d", k=5) + output = LargestFit(ballots=input, preferred_candidate="d", k=5) # [["d","a","c","b"],["d","a","c","b"],["d","b","a","c"],["d","a","b","c"],["d","b","c","a"]] # Run tests self.assertEqual(len(output),5) From 47b8218144f2623ab8e5c48023b525861a515f9e Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Sun, 15 Jan 2023 20:58:05 +0200 Subject: [PATCH 15/22] modified: py3votecore/borda_manipulation_heurestics_methods.py modified: test_functionality/test_borda_manipulation_heurestics_methods.py --- py3votecore/borda_manipulation_heurestics_methods.py | 1 - test_functionality/test_borda_manipulation_heurestics_methods.py | 1 - 2 files changed, 2 deletions(-) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 0389534..0414d7b 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -8,7 +8,6 @@ from .borda import Borda import networkx as nx - def AverageFit(ballots: list, preferred_candidate: str, k: int)->list or bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py index 29918bb..c0ee98c 100644 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -1,7 +1,6 @@ - import random from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit import unittest From f55282c066cb3e7b0280bdff3255e18f8ebdd836 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:28:40 +0200 Subject: [PATCH 16/22] modified: py3votecore/borda_manipulation_heurestics_methods.py modified: test_functionality/test_borda_manipulation_heurestics_methods.py --- py3votecore/borda_manipulation_heurestics_methods.py | 1 + test_functionality/test_borda_manipulation_heurestics_methods.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 0414d7b..0389534 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -8,6 +8,7 @@ from .borda import Borda import networkx as nx + def AverageFit(ballots: list, preferred_candidate: str, k: int)->list or bool: """ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011), diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py index c0ee98c..5aaac71 100644 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -1,6 +1,5 @@ - import random from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit import unittest From 433c06e5dc1ce78d80d92dc0d0f25f41f183e6f8 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Mon, 16 Jan 2023 13:50:08 +0200 Subject: [PATCH 17/22] Added requirements and fixed a test. new file: requirement.txt modified: setup.py modified: test_functionality/test_schulze_method.py --- requirement.txt | Bin 0 -> 88 bytes setup.py | 1 + test_functionality/test_schulze_method.py | 5 ++++- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 requirement.txt diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000000000000000000000000000000000000..59e7338e363d4d1c82223f381a6a87106e39786a GIT binary patch literal 88 zcmezWFOMOWp@gBFA)ld$A)BFs!4?RO8T1$o7= 1.8.0', + 'networkx >= 3.0', ] setup(name='python3-vote-core', diff --git a/test_functionality/test_schulze_method.py b/test_functionality/test_schulze_method.py index 9372712..ac16fd7 100644 --- a/test_functionality/test_schulze_method.py +++ b/test_functionality/test_schulze_method.py @@ -169,7 +169,10 @@ def test_tuple_ballots(self): output_list = SchulzeMethod(input_list, ballot_notation=SchulzeMethod.BALLOT_NOTATION_GROUPING).as_dict() # Run tests - self.assertEqual(output_tuple, output_list) + self.assertEqual(output_tuple['candidates'], output_list['candidates']) + self.assertEqual(output_tuple['tied_winners'], output_list['tied_winners']) + self.assertEqual(output_tuple['pairs'], output_list['pairs']) + self.assertEqual(output_tuple['strong_pairs'], output_list['strong_pairs']) if __name__ == "__main__": unittest.main() From 6af08d48bacf838beeead756c00d24a78a89a8be Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Mon, 16 Jan 2023 15:02:05 +0200 Subject: [PATCH 18/22] Added description of the added Borda votting rule and version update modified: README.rst modified: py3votecore/borda.py modified: py3votecore/borda_at_large.py modified: py3votecore/borda_manipulation_heurestics_methods.py modified: setup.py modified: test_functionality/test_borda.py modified: test_functionality/test_borda_at_large.py modified: test_functionality/test_borda_manipulation_heurestics_methods.py --- README.rst | 7 +++++ py3votecore/borda.py | 16 +++++++++-- py3votecore/borda_at_large.py | 28 +++++++++---------- .../borda_manipulation_heurestics_methods.py | 20 +++++++++---- setup.py | 2 +- test_functionality/test_borda.py | 15 +++++++++- test_functionality/test_borda_at_large.py | 16 +++++++++-- ...t_borda_manipulation_heurestics_methods.py | 15 +++++++++- 8 files changed, 92 insertions(+), 27 deletions(-) diff --git a/README.rst b/README.rst index c542a8b..ad983b5 100644 --- a/README.rst +++ b/README.rst @@ -25,18 +25,25 @@ Methods implemented * Plurality (aka first-past-the-post or fptp) * Instant-Runoff Voting (aka IRV) * Schulze Method (aka Beatpath) + * Borda * Multiple Winner Methods * Plurality at large (aka block voting) * Single Transferable Vote (aka STV) * Schulze STV + * Borda at large * Ordering Methods * Schulze Proportional Representation * Schulze Nonproportional Representation +* Manipulation Algorithms + + * Average Fit (coalition manipulation for Borda votting rule) + * Largest Fit (coalition manipulation for Borda votting rule) + Basic Usage ----------- diff --git a/py3votecore/borda.py b/py3votecore/borda.py index 46a42ab..ca84a58 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -1,5 +1,17 @@ - - +# Copyright (C) 2023, Leora Schmerler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . from .abstract_classes import AbstractSingleWinnerVotingSystem from .borda_at_large import BordaAtLarge diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py index bbee9b2..a12dab5 100644 --- a/py3votecore/borda_at_large.py +++ b/py3votecore/borda_at_large.py @@ -1,4 +1,17 @@ - +# Copyright (C) 2023, Leora Schmerler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . from .abstract_classes import MultipleWinnerVotingSystem from .common_functions import matching_keys, unique_permutations @@ -57,16 +70,3 @@ def as_dict(self): data = super(BordaAtLarge, self).as_dict() data["tallies"] = self.tallies return data - - -# if __name__ == "__main__": -# output = BordaAtLarge([ -# {"count": 30, "ballot": ["c1", "c4", "c2", "c3"]}, -# {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]}, -# {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]}, -# {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]}, -# {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]}, -# {"count": 10, "ballot": ["c1", "c2", "c4", "c3"]} -# ], required_winners=2).as_dict() - -# print(output) \ No newline at end of file diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py index 0389534..be40373 100644 --- a/py3votecore/borda_manipulation_heurestics_methods.py +++ b/py3votecore/borda_manipulation_heurestics_methods.py @@ -1,10 +1,18 @@ -# -# -# -# -# -# +# Copyright (C) 2023, Leora Schmerler # +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + from .borda import Borda import networkx as nx diff --git a/setup.py b/setup.py index e2244e8..31e6f1c 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ ] setup(name='python3-vote-core', - version='20170329.00', + version='20230116.00', description="An implementation of various election methods, most notably the Schulze Method and Schulze STV. -- Python 3 Only", long_description=README + '\n\n' + CHANGES + '\n\n' + LICENSE, classifiers=[ diff --git a/test_functionality/test_borda.py b/test_functionality/test_borda.py index fd80a93..2dade03 100644 --- a/test_functionality/test_borda.py +++ b/test_functionality/test_borda.py @@ -1,4 +1,17 @@ - +# Copyright (C) 2023, Leora Schmerler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . from py3votecore.borda import Borda import unittest diff --git a/test_functionality/test_borda_at_large.py b/test_functionality/test_borda_at_large.py index 7aa9670..7ff83ce 100644 --- a/test_functionality/test_borda_at_large.py +++ b/test_functionality/test_borda_at_large.py @@ -1,5 +1,17 @@ - - +# Copyright (C) 2023, Leora Schmerler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . from py3votecore.borda_at_large import BordaAtLarge import unittest diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py index 5aaac71..5165061 100644 --- a/test_functionality/test_borda_manipulation_heurestics_methods.py +++ b/test_functionality/test_borda_manipulation_heurestics_methods.py @@ -1,4 +1,17 @@ - +# Copyright (C) 2023, Leora Schmerler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . import random from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit From be0253a81597a758eccf858966d7581be7ec7f65 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Mon, 16 Jan 2023 15:29:57 +0200 Subject: [PATCH 19/22] modified: README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index ad983b5..bf830df 100644 --- a/README.rst +++ b/README.rst @@ -41,8 +41,8 @@ Methods implemented * Manipulation Algorithms - * Average Fit (coalition manipulation for Borda votting rule) - * Largest Fit (coalition manipulation for Borda votting rule) + * Average Fit (coalition manipulation heurestic for Borda votting rule) + * Largest Fit (coalition manipulation heurestic for Borda votting rule) Basic Usage ----------- From 6faa9c14bea17026d73df75a2b188481786d20f2 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:21:58 +0200 Subject: [PATCH 20/22] Create requirements.txt --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..21a9cd7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +networkx >= 3.0 +python-graph-core >= 1.8.2 From e2d3e623d01de2339c8c1f96a586e23201899312 Mon Sep 17 00:00:00 2001 From: leorasc <31726486+leorasc@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:22:48 +0200 Subject: [PATCH 21/22] Delete requirement.txt --- requirement.txt | Bin 88 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 requirement.txt diff --git a/requirement.txt b/requirement.txt deleted file mode 100644 index 59e7338e363d4d1c82223f381a6a87106e39786a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88 zcmezWFOMOWp@gBFA)ld$A)BFs!4?RO8T1$o7 Date: Wed, 18 Jan 2023 10:17:42 +0200 Subject: [PATCH 22/22] Class documentation and doctest added. modified: py3votecore/borda.py modified: py3votecore/borda_at_large.py --- py3votecore/borda.py | 11 +++++++++++ py3votecore/borda_at_large.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/py3votecore/borda.py b/py3votecore/borda.py index ca84a58..395db34 100644 --- a/py3votecore/borda.py +++ b/py3votecore/borda.py @@ -19,7 +19,18 @@ class Borda(AbstractSingleWinnerVotingSystem): def __init__(self, ballots, tie_breaker=None): + """ + The constructer accepts ballots of voters and a tie breaker if given. + >>> Borda([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}]) # doctest:+ELLIPSIS + <...> + >>> Borda([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"]) # doctest:+ELLIPSIS + <...> + """ super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker) +if __name__ == '__main__': + import doctest + (failures, tests) = doctest.testmod(report=True) + print("{} failures, {} tests".format(failures, tests)) diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py index a12dab5..f82d6d3 100644 --- a/py3votecore/borda_at_large.py +++ b/py3votecore/borda_at_large.py @@ -19,10 +19,25 @@ class BordaAtLarge(MultipleWinnerVotingSystem): - def __init__(self, ballots: dict, tie_breaker=None, required_winners: int=1): + def __init__(self, ballots: list, tie_breaker=None, required_winners: int=1): + """ + The constructer accepts ballots of voters, a tie breaker if given and the number of required winners. + >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], required_winners = 2) # doctest:+ELLIPSIS + <...> + >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"] ,required_winners = 2) # doctest:+ELLIPSIS + <...> + """ super(BordaAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners) def calculate_results(self): + """ + calculate_results accepts an instance of Borda, and is called from the constructor. + >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"] ,required_winners = 2).calculate_results() # doctest:+ELLIPSIS + ... + >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], required_winners = 2).calculate_results() # doctest:+ELLIPSIS + ... + """ + # Standardize the ballot format and extract the candidates self.candidates = set() for ballot in self.ballots: @@ -67,6 +82,19 @@ def calculate_results(self): self.winners = winning_candidates def as_dict(self): + """ + as_dict accepts an instance of Borda, and returns a dict of the BordaAtLarge. + >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"] ,required_winners = 2).as_dict() # doctest:+ELLIPSIS + {...} + >>> BordaAtLarge([{ "count":3, "ballot":["A", "C", "B", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], required_winners = 2).as_dict() # doctest:+ELLIPSIS + {...} + """ data = super(BordaAtLarge, self).as_dict() data["tallies"] = self.tallies return data + +if __name__ == '__main__': + import doctest + (failures, tests) = doctest.testmod(report=True) + print("{} failures, {} tests".format(failures, tests)) +