diff --git a/elect.py b/elect.py index 0341a47df5985793732e42a42f83d4d6154857f6..fcf15484b822b3ced42e9227333f92b0425d54f7 100755 --- a/elect.py +++ b/elect.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse import json +import networkx as nx import numpy as np # N[a,b] is the number of voters who prefer candidate a to candidate b @@ -78,6 +79,101 @@ def schulze_winners(n, candidates, link_strength_method='margin'): winners.append(candidates[i]) return winners +# from https://en.wikipedia.org/wiki/Schwartz_set (2023-07-14) +# In voting systems, the Schwartz set is the union of all Schwartz set components. +# A Schwartz set component is any non-empty set S of candidates such that +# Every candidate inside the set S is pairwise unbeaten by every candidate outside S; and +# No non-empty proper subset of S fulfills the first property. +def schwartz_set(graph, candidates): + nb_nodes = 0 + candidate_to_beat_index = dict() + beat_index_to_candidate = dict() + for node in graph.nodes(): + candidate_to_beat_index[node] = nb_nodes + beat_index_to_candidate[nb_nodes] = node + nb_nodes += 1 + + beats = np.zeros((nb_nodes, nb_nodes), dtype=bool) + for (i, neighbors) in graph.adjacency(): + for j in neighbors: + beats[candidate_to_beat_index[i]][candidate_to_beat_index[j]] = True + + for k in range(nb_nodes): + for i in range(nb_nodes): + for j in range(nb_nodes): + beats[i][j] = beats[i][j] or (beats[i][k] and beats[k][j]) + + schwartz_components = [] + for i in range(nb_nodes): + schwartz_component = {i} + is_schwartz = True + for j in range(nb_nodes): + if beats[j][i]: + if beats[i][j]: + schwartz_component.add(j) + else: + is_schwartz = False + if is_schwartz: + schwartz_components.append(schwartz_component) + + schwartz_set_index = set.union(*schwartz_components) + return [beat_index_to_candidate[x] for x in schwartz_set_index] + +# As defined in Appendix 4 of https://citizensassembly.arts.ubc.ca/resources/submissions/csharman-10_0409201706-143.pdf +def schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin'): + nb_candidates = len(candidates) + graph = nx.DiGraph() + graph.add_nodes_from(candidates) + + for i in range(nb_candidates): + for j in range(i, nb_candidates): + margin = n[i][j] - n[j][i] + if margin != 0: + (x, y) = (i, j) if margin > 0 else (j, i) + if link_strength_method == 'margin': + graph.add_edge(candidates[x], candidates[y], weight=abs(margin)) + elif link_strength_method == 'ratio': + graph.add_edge(candidates[x], candidates[y], weight=np.Inf if n[y][x] == 0 else n[x][y]/n[y][x]) + elif link_strength_method == 'winning_votes': + graph.add_edge(candidates[x], candidates[y], weight=n[x][y]) + elif link_strength_method == 'losing_votes': + graph.add_edge(candidates[x], candidates[y], weight=-n[y][x]) + else: + raise ValueError(f"link_strength_method='{link_strength_method}' is not implemented") + + while len(graph.nodes()) > 1: + s_set = schwartz_set(graph, candidates) + #print(f'Schwartz set: {s_set}') + + # eliminate all candidates that are not in the Schwartz set + nodes_to_remove = [] + for candidate in graph.nodes(): + if candidate not in s_set: + nodes_to_remove.append(candidate) + if len(nodes_to_remove) > 0: + #print(f'Removing nodes: {nodes_to_remove}') + graph.remove_nodes_from(nodes_to_remove) + + if len(graph.nodes()) > 1 and len(graph.edges()) > 1: + # remove all the edges that have the current minimal weight + minimum_weight = np.Inf + edges_per_weight = dict() + for (i, neighbors) in graph.adjacency(): + for (neighbor, data) in neighbors.items(): + weight = data['weight'] + minimum_weight = min(minimum_weight, weight) + if weight in edges_per_weight: + edges_per_weight[weight].append((i, neighbor)) + else: + edges_per_weight[weight] = [(i, neighbor)] + + #print(f'Removing edges of weight={minimum_weight}: {edges_per_weight[minimum_weight]}') + for (i, j) in edges_per_weight[minimum_weight]: + graph.remove_edge(i, j) + continue + break + + return [x for x in graph.nodes()] if __name__ == '__main__': parser = argparse.ArgumentParser(description='Computes winners of the election') @@ -107,6 +203,13 @@ if __name__ == '__main__': print(f'Weak Condorcet winners: {weak_c_winners}') # Schulze winners (win every head-to-head contest, directly or indirectly) - schulze_winners = schulze_winners(n, candidates) - assert(len(schulze_winners) > 0) - print(f'Schulze winners: {schulze_winners}') + for method in ['margin', 'ratio']: + s_winners = schulze_winners(n, candidates, link_strength_method=method) + assert(len(s_winners) > 0) + print(f'Schulze winners (beatpath, {method}): {s_winners}') + + # Schulze winners, computed via the Schwartz sequential dropping algorithm + for method in ['margin', 'ratio', 'winning_votes', 'losing_votes']: + s_winners = schwartz_sequential_dropping_winners(n, candidates, link_strength_method=method) + assert(len(s_winners) > 0) + print(f'Schulze winners (ssd, {method}): {s_winners}') diff --git a/test_schulze_examples.py b/test_schulze_examples.py index 13b7aedc564875e57707edef419efed35662f1bf..8ff367df648f1c688b2b7224b1fe4de645ff09b9 100644 --- a/test_schulze_examples.py +++ b/test_schulze_examples.py @@ -29,6 +29,10 @@ def test_example1(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['d'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['d'] def test_example2(): @@ -53,6 +57,10 @@ def test_example2(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['c'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['c'] def test_example3(): @@ -81,6 +89,10 @@ def test_example3(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['e'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['e'] def test_example4(): @@ -104,6 +116,10 @@ def test_example4(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['b', 'd'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['b', 'd'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['b', 'd'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['b', 'd'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['b', 'd'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['b', 'd'] def test_example5(): @@ -128,6 +144,10 @@ def test_example5(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['d'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['d'] def test_example6(): @@ -154,6 +174,10 @@ def test_example6(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['a', 'c'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['a', 'c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['a', 'c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['a', 'c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['a', 'c'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['a', 'c'] def test_example7(): # situation 1 @@ -182,6 +206,10 @@ def test_example7(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['a'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['a'] # situation 2 candidates = ['a', 'b', 'c', 'd', 'e', 'f'] @@ -203,6 +231,10 @@ def test_example7(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['d'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['d'] def test_example8(): @@ -232,6 +264,10 @@ def test_example8(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['a'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['a'] # situation 2 candidates = ['a', 'b', 'c', 'd', 'e'] @@ -260,6 +296,10 @@ def test_example8(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['b'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['b'] def test_example9(): @@ -284,6 +324,10 @@ def test_example9(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['a'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['a'] # situation 2 candidates = ['a', 'b', 'c', 'd', 'e'] @@ -307,6 +351,10 @@ def test_example9(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['b'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['b'] def test_example10(): candidates = ['a', 'b', 'c', 'd'] @@ -337,9 +385,10 @@ def test_example10(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['a'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['b'] - - #assert False, "TODO: implement other methods to compute strength of link" - + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['a'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['d'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['c'] def test_example11(): candidates = ['a', 'b', 'c', 'd', 'e'] @@ -367,6 +416,10 @@ def test_example11(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['b'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['b'] def test_example12(): @@ -395,6 +448,10 @@ def test_example12(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['e'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['e'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['e'] def test_example13(): @@ -416,3 +473,7 @@ def test_example13(): assert len(elect.weak_condorcet_winners(n, candidates)) == 0 assert elect.schulze_winners(n, candidates, link_strength_method='margin') == ['a', 'b'] assert elect.schulze_winners(n, candidates, link_strength_method='ratio') == ['a', 'b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='margin') == ['a', 'b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='ratio') == ['a', 'b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='winning_votes') == ['a', 'b'] + assert elect.schwartz_sequential_dropping_winners(n, candidates, link_strength_method='losing_votes') == ['a', 'b']