Skip to content

Commit

Permalink
Merge pull request 'schulze-evaluate: give strength as callback funct…
Browse files Browse the repository at this point in the history
…ion' (#6) from feature/callback-strength into master

Reviewed-on: https://tracker.cde-ev.de/gitea/cdedb/schulze-condorcet/pulls/6
  • Loading branch information
houseofsuns committed Mar 11, 2021
2 parents e5bf033 + 5782062 commit 5e7e587
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 40 deletions.
43 changes: 12 additions & 31 deletions schulze_condorcet/schulze_condorcet.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from gettext import gettext as _
from typing import Collection, Container, Dict, List, Mapping, Tuple, Union

from schulze_condorcet.strength import StrengthCallback, winning_votes


def _schulze_winners(d: Mapping[Tuple[str, str], int],
candidates: Collection[str]) -> List[str]:
Expand Down Expand Up @@ -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: StrengthCallback = winning_votes
) -> Tuple[str, List[Dict[str, Union[int, List[str]]]]]:
"""Use the Schulze method to cumulate preference list into one list.
Expand All @@ -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.
"""
Expand All @@ -74,36 +80,11 @@ 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(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
Expand Down
51 changes: 51 additions & 0 deletions schulze_condorcet/strength.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""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 protocol defining the interface of a generic
strength function . You can use it to implement your own strength function.
"""

from typing import Protocol


class StrengthCallback(Protocol):
"""The interface every strength function has to implement."""
def __call__(self, *, 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
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 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
22 changes: 13 additions & 9 deletions tests/test_schulze.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import unittest

from schulze_condorcet import schulze_evaluate
from schulze_condorcet.strength import margin, winning_votes


class MyTest(unittest.TestCase):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 5e7e587

Please sign in to comment.