From e82bdac44ca655d3481e98379cfeabdc420bd36e Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Wed, 3 Mar 2021 14:00:23 +0100 Subject: [PATCH 1/8] schulze-condorcet: move definition of strength functions in own file --- schulze_condorcet/strength.py | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 schulze_condorcet/strength.py diff --git a/schulze_condorcet/strength.py b/schulze_condorcet/strength.py new file mode 100644 index 0000000..50a764b --- /dev/null +++ b/schulze_condorcet/strength.py @@ -0,0 +1,49 @@ +""" Example implementations to define the strength of a link in the Schulze problem. + +We view the candidates as vertices of a graph and determining the result as the +strongest path in the graph. To determine the strength of a path, we have to define a +metric which takes the voting of the voters into account. + +Therefore, we transform every vote string into a number of supports and a number of +oppositions per path in the graph of candidates (for details, see at `schulze_evaluate`. +Now, we use this and the total number of votes to define the strength of a given path +in the graph of all candidates. + +How to asses the strength of a path is one thing not specified by the Schulze method and +indeed there are several possibilities. We offer some sample implementations of such a +strength function, together with an abstract function which can be used as a blue print +to implement your own strength function. +""" + + +def strength_example(support: int, opposition: int, totalvotes: int) -> int: + """A generic example of a strength function.""" + raise NotImplementedError + + +def winning_votes(support: int, opposition: int, totalvotes: int) -> int: + """This strategy is also advised by the paper of Markus Schulze. + + If two two links have more support than opposition, then the link with more + supporters is stronger, if supporters tie then less opposition is used as secondary + criterion. + """ + if support > opposition: + return totalvotes * support - opposition + elif support == opposition: + return 0 + else: + return -1 + + +def margin(support: int, opposition: int, totalvotes: int) -> int: + """This strategy which seems to have a more intuitive appeal. + + It sets the difference between support and opposition as strength of a link. However + the discrepancy between this strategy and `winning_votes` is rather small, to wit + all cases in the test suite give the same result for both of them. + + Moreover if the votes contain no ties both strategies (and several more) are totally + equivalent. + """ + return support - opposition From e6460f680ddc55d5b4e52e61d4b00268f4e32eb5 Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Wed, 3 Mar 2021 14:01:05 +0100 Subject: [PATCH 2/8] schulze-evaluate: use callback strength function this allows to simply interchange the strength function which shall be used as a metric for the graph of candidates. closes #3 --- schulze_condorcet/schulze_condorcet.py | 43 +++++++------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/schulze_condorcet/schulze_condorcet.py b/schulze_condorcet/schulze_condorcet.py index 0f0c887..340751b 100644 --- a/schulze_condorcet/schulze_condorcet.py +++ b/schulze_condorcet/schulze_condorcet.py @@ -1,5 +1,7 @@ from gettext import gettext as _ -from typing import Collection, Container, Dict, List, Mapping, Tuple, Union +from typing import Callable, Collection, Container, Dict, List, Mapping, Tuple, Union + +from .strength import winning_votes def _schulze_winners(d: Mapping[Tuple[str, str], int], @@ -32,7 +34,9 @@ def _schulze_winners(d: Mapping[Tuple[str, str], int], return winners -def schulze_evaluate(votes: Collection[str], candidates: Collection[str] +def schulze_evaluate(votes: Collection[str], + candidates: Collection[str], + strength: Callable[[int, int, int], int] = winning_votes ) -> Tuple[str, List[Dict[str, Union[int, List[str]]]]]: """Use the Schulze method to cumulate preference list into one list. @@ -50,6 +54,8 @@ def schulze_evaluate(votes: Collection[str], candidates: Collection[str] preference. One vote has the form ``3>0>1=2>4``. :param candidates: We require that the candidates be explicitly passed. This allows for more flexibility (like returning a useful result for zero votes). + :param strength: A function which will be used as the metric on the graph of all + candidates. See `strength.py` for more detailed information. :returns: The first Element is the aggregated result, the second is an more extended list, containing every level (descending) as dict with some extended information. """ @@ -74,36 +80,9 @@ def _subindex(alist: Collection[Container[str]], element: str) -> int: if _subindex(vote, x) < _subindex(vote, y): counts[(x, y)] += 1 - # Second we calculate a numeric link strength abstracting the problem - # into the realm of graphs with one vertex per candidate - def _strength(support: int, opposition: int, totalvotes: int) -> int: - """One thing not specified by the Schulze method is how to asses the - strength of a link and indeed there are several possibilities. We - use the strategy called 'winning votes' as advised by the paper of - Markus Schulze. - - If two two links have more support than opposition, then the link - with more supporters is stronger, if supporters tie then less - opposition is used as secondary criterion. - - Another strategy which seems to have a more intuitive appeal is - called 'margin' and sets the difference between support and - opposition as strength of a link. However the discrepancy - between the strategies is rather small, to wit all cases in the - test suite give the same result for both of them. Moreover if - the votes contain no ties both strategies (and several more) are - totally equivalent. - """ - # the margin strategy would be given by the following line - # return support - opposition - if support > opposition: - return totalvotes * support - opposition - elif support == opposition: - return 0 - else: - return -1 - - d = {(x, y): _strength(counts[(x, y)], counts[(y, x)], len(votes)) + # Second we calculate a numeric link strength abstracting the problem into the realm + # of graphs with one vertex per candidate + d = {(x, y): strength(counts[(x, y)], counts[(y, x)], len(votes)) for x in candidates for y in candidates} # Third we execute the Schulze method by iteratively determining winners From 2d0621c8d6df1930d97553b9cf31c0d361da7206 Mon Sep 17 00:00:00 2001 From: Tobias Udtke Date: Wed, 3 Mar 2021 14:22:42 +0100 Subject: [PATCH 3/8] strength: implement Protocol to specify signature of strength callback function. --- schulze_condorcet/schulze_condorcet.py | 4 ++-- schulze_condorcet/strength.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/schulze_condorcet/schulze_condorcet.py b/schulze_condorcet/schulze_condorcet.py index 340751b..aa5581d 100644 --- a/schulze_condorcet/schulze_condorcet.py +++ b/schulze_condorcet/schulze_condorcet.py @@ -1,7 +1,7 @@ from gettext import gettext as _ from typing import Callable, Collection, Container, Dict, List, Mapping, Tuple, Union -from .strength import winning_votes +from .strength import StrengthCallback, winning_votes def _schulze_winners(d: Mapping[Tuple[str, str], int], @@ -36,7 +36,7 @@ def _schulze_winners(d: Mapping[Tuple[str, str], int], def schulze_evaluate(votes: Collection[str], candidates: Collection[str], - strength: Callable[[int, int, int], int] = winning_votes + strength: StrengthCallback = winning_votes ) -> Tuple[str, List[Dict[str, Union[int, List[str]]]]]: """Use the Schulze method to cumulate preference list into one list. diff --git a/schulze_condorcet/strength.py b/schulze_condorcet/strength.py index 50a764b..8d25795 100644 --- a/schulze_condorcet/strength.py +++ b/schulze_condorcet/strength.py @@ -15,10 +15,11 @@ to implement your own strength function. """ +from typing import Protocol -def strength_example(support: int, opposition: int, totalvotes: int) -> int: - """A generic example of a strength function.""" - raise NotImplementedError + +class StrengthCallback(Protocol): + def __call__(self, support: int, opposition: int, totalvotes: int) -> int: ... def winning_votes(support: int, opposition: int, totalvotes: int) -> int: From 9aa93e25a33a79dd701e7a70aa903c8ed0c1dbce Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Wed, 3 Mar 2021 18:28:38 +0100 Subject: [PATCH 4/8] fix typos and add a docstring to StrengthCallback Protocoll --- schulze_condorcet/schulze_condorcet.py | 2 +- schulze_condorcet/strength.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/schulze_condorcet/schulze_condorcet.py b/schulze_condorcet/schulze_condorcet.py index aa5581d..6fc1e81 100644 --- a/schulze_condorcet/schulze_condorcet.py +++ b/schulze_condorcet/schulze_condorcet.py @@ -1,5 +1,5 @@ from gettext import gettext as _ -from typing import Callable, Collection, Container, Dict, List, Mapping, Tuple, Union +from typing import Collection, Container, Dict, List, Mapping, Tuple, Union from .strength import StrengthCallback, winning_votes diff --git a/schulze_condorcet/strength.py b/schulze_condorcet/strength.py index 8d25795..73ad76a 100644 --- a/schulze_condorcet/strength.py +++ b/schulze_condorcet/strength.py @@ -1,4 +1,4 @@ -""" Example implementations to define the strength of a link in the Schulze problem. +"""Example implementations to define the strength of a link in the Schulze problem. We view the candidates as vertices of a graph and determining the result as the strongest path in the graph. To determine the strength of a path, we have to define a @@ -19,6 +19,7 @@ class StrengthCallback(Protocol): + """The interface every strength function has to implement.""" def __call__(self, support: int, opposition: int, totalvotes: int) -> int: ... @@ -38,7 +39,7 @@ def winning_votes(support: int, opposition: int, totalvotes: int) -> int: def margin(support: int, opposition: int, totalvotes: int) -> int: - """This strategy which seems to have a more intuitive appeal. + """This strategy seems to have a more intuitive appeal. It sets the difference between support and opposition as strength of a link. However the discrepancy between this strategy and `winning_votes` is rather small, to wit From 652bed9389f6ec7547be9408ffa81dc1050e2623 Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Wed, 3 Mar 2021 18:29:10 +0100 Subject: [PATCH 5/8] tests: use different metrics in the testsuite --- tests/test_schulze.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/test_schulze.py b/tests/test_schulze.py index 1e6a328..3adf4ed 100644 --- a/tests/test_schulze.py +++ b/tests/test_schulze.py @@ -4,6 +4,7 @@ import unittest from schulze_condorcet import schulze_evaluate +from schulze_condorcet.strength import margin, winning_votes class MyTest(unittest.TestCase): @@ -36,11 +37,12 @@ def _ordinary_votes(spec: Dict[Optional[Tuple[str, ...]], int], ("1=2=3>0>4=5", {('1', '2', '3'): 2, ('1', '2',): 3, ('3',): 3, ('1', '3'): 1, ('2',): 1}), ) - for expectation, spec in tests: - with self.subTest(spec=spec): - condensed, detailed = schulze_evaluate( - _ordinary_votes(spec, candidates), candidates) - self.assertEqual(expectation, condensed) + for metric in {margin, winning_votes}: + for expectation, spec in tests: + with self.subTest(spec=spec, metric=metric): + condensed, _ = schulze_evaluate(_ordinary_votes(spec, candidates), + candidates, strength=metric) + self.assertEqual(expectation, condensed) def test_schulze(self) -> None: candidates = ('0', '1', '2', '3', '4') @@ -82,10 +84,12 @@ def test_schulze(self) -> None: ("0=3=4>2>1", advanced + ("0=2=3=4>1",)), ("1=3=4>2>0", advanced + ("1=2=3=4>0",)), ) - for expectation, addons in tests: - with self.subTest(addons=addons): - condensed, detailed = schulze_evaluate(base + addons, candidates) - self.assertEqual(expectation, condensed) + for metric in {margin, winning_votes}: + for expectation, addons in tests: + with self.subTest(addons=addons, metric=metric): + condensed, _ = schulze_evaluate(base + addons, candidates, + strength=metric) + self.assertEqual(expectation, condensed) def test_schulze_runtime(self) -> None: # silly test, since I just realized, that the algorithm runtime is From c74337aeea003c489ebc74568ce0691c4ce060e1 Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Sat, 6 Mar 2021 18:02:16 +0100 Subject: [PATCH 6/8] strength: require passing of StrengthCallback arguments as keyword --- schulze_condorcet/schulze_condorcet.py | 4 +++- schulze_condorcet/strength.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/schulze_condorcet/schulze_condorcet.py b/schulze_condorcet/schulze_condorcet.py index 6fc1e81..6812063 100644 --- a/schulze_condorcet/schulze_condorcet.py +++ b/schulze_condorcet/schulze_condorcet.py @@ -82,7 +82,9 @@ def _subindex(alist: Collection[Container[str]], element: str) -> int: # Second we calculate a numeric link strength abstracting the problem into the realm # of graphs with one vertex per candidate - d = {(x, y): strength(counts[(x, y)], counts[(y, x)], len(votes)) + d = {(x, y): strength(support=counts[(x, y)], + opposition=counts[(y, x)], + totalvotes=len(votes)) for x in candidates for y in candidates} # Third we execute the Schulze method by iteratively determining winners diff --git a/schulze_condorcet/strength.py b/schulze_condorcet/strength.py index 73ad76a..0b6b1ad 100644 --- a/schulze_condorcet/strength.py +++ b/schulze_condorcet/strength.py @@ -20,10 +20,10 @@ class StrengthCallback(Protocol): """The interface every strength function has to implement.""" - def __call__(self, support: int, opposition: int, totalvotes: int) -> int: ... + def __call__(self, *, support: int, opposition: int, totalvotes: int) -> int: ... -def winning_votes(support: int, opposition: int, totalvotes: int) -> int: +def winning_votes(*, support: int, opposition: int, totalvotes: int) -> int: """This strategy is also advised by the paper of Markus Schulze. If two two links have more support than opposition, then the link with more @@ -38,7 +38,7 @@ def winning_votes(support: int, opposition: int, totalvotes: int) -> int: return -1 -def margin(support: int, opposition: int, totalvotes: int) -> int: +def margin(*, support: int, opposition: int, totalvotes: int) -> int: """This strategy seems to have a more intuitive appeal. It sets the difference between support and opposition as strength of a link. However From ce99f17158f0bc06e4d577581a23ef6cb2a735a7 Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Sat, 6 Mar 2021 18:03:21 +0100 Subject: [PATCH 7/8] strength: fix docstring of module --- schulze_condorcet/strength.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schulze_condorcet/strength.py b/schulze_condorcet/strength.py index 0b6b1ad..5bcd083 100644 --- a/schulze_condorcet/strength.py +++ b/schulze_condorcet/strength.py @@ -5,14 +5,14 @@ metric which takes the voting of the voters into account. Therefore, we transform every vote string into a number of supports and a number of -oppositions per path in the graph of candidates (for details, see at `schulze_evaluate`. +oppositions per path in the graph of candidates (for details, see at `schulze_evaluate`). Now, we use this and the total number of votes to define the strength of a given path in the graph of all candidates. How to asses the strength of a path is one thing not specified by the Schulze method and indeed there are several possibilities. We offer some sample implementations of such a -strength function, together with an abstract function which can be used as a blue print -to implement your own strength function. +strength function, together with an protocol defining the interface of a generic +strength function . You can use it to implement your own strength function. """ from typing import Protocol From 5782062b812374c1728ba6b1b1e57ec527c5297d Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Tue, 9 Mar 2021 11:42:16 +0100 Subject: [PATCH 8/8] schulze-condorcet: use absolute imports --- schulze_condorcet/schulze_condorcet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schulze_condorcet/schulze_condorcet.py b/schulze_condorcet/schulze_condorcet.py index 6812063..120ec84 100644 --- a/schulze_condorcet/schulze_condorcet.py +++ b/schulze_condorcet/schulze_condorcet.py @@ -1,7 +1,7 @@ from gettext import gettext as _ from typing import Collection, Container, Dict, List, Mapping, Tuple, Union -from .strength import StrengthCallback, winning_votes +from schulze_condorcet.strength import StrengthCallback, winning_votes def _schulze_winners(d: Mapping[Tuple[str, str], int],