Skip to content

Commit 3ba0f9f

Browse files
committed
Stripping out unnecessary code
Improving performance by setting a vote management threshold Cleaning things up, largely
1 parent 2e9e398 commit 3ba0f9f

File tree

2 files changed

+83
-164
lines changed

2 files changed

+83
-164
lines changed

common_functions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def unique_permutations(xs):
2+
if len(xs) < 2:
3+
yield xs
4+
else:
5+
h = []
6+
for x in xs:
7+
h.append(x)
8+
if x in h[:-1]:
9+
continue
10+
ts = xs[:]
11+
ts.remove(x)
12+
for ps in unique_permutations(ts):
13+
yield [x]+ps

schulze_stv.py

Lines changed: 70 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,57 @@
1717
from schulze_method import SchulzeMethod
1818
from pygraph.classes.digraph import digraph
1919
from pygraph.algorithms.minmax import maximum_flow
20+
#from multiprocessing import Pool
2021
import itertools
22+
from common_functions import unique_permutations
2123

22-
class SchulzeSTV(SchulzeMethod):
23-
24-
compute_vote_management_with_pygraph = False
24+
PREFERRED_LESS = 1
25+
PREFERRED_SAME = 2
26+
PREFERRED_MORE = 3
27+
STRENGTH_TOLERANCE = 0.0000000001
28+
STRENGTH_THRESHOLD = 0.1
2529

30+
class SchulzeSTV(SchulzeMethod):
31+
2632
def __init__(self, ballots, required_winners, notation = None):
2733
self.required_winners = required_winners
2834
SchulzeMethod.__init__(self, ballots, notation)
29-
35+
3036
def calculate_results(self):
31-
37+
3238
# Generate the list of patterns we need to complete
33-
self.__generate_completion_patterns__()
3439
self.__generate_completed_patterns__()
3540
self.__generate_vote_management_graph__()
36-
41+
3742
# Build the graph of possible winners
3843
self.graph = digraph()
3944
for candidate_set in itertools.combinations(self.candidates, self.required_winners):
4045
self.graph.add_nodes([tuple(sorted(list(candidate_set)))])
41-
46+
4247
# Generate the edges between nodes
4348
for candidate_set in itertools.combinations(self.candidates, self.required_winners + 1):
4449
for candidate in candidate_set:
45-
other_candidates = sorted(list(set(candidate_set) - set([candidate])))
50+
other_candidates = sorted(set(candidate_set) - set([candidate]))
4651
completed = self.__proportional_completion__(candidate, other_candidates)
4752
weight = self.__strength_of_vote_management__(completed)
4853
if weight > 0:
4954
for subset in itertools.combinations(other_candidates, len(other_candidates) - 1):
5055
self.graph.add_edge((tuple(other_candidates), tuple(sorted(list(subset) + [candidate]))), weight)
51-
56+
5257
# Determine the winner through the Schwartz set heuristic
5358
self.schwartz_set_heuristic()
54-
59+
5560
# Split the "winner" into its candidate components
5661
self.winners = set([item for innerlist in self.winners for item in innerlist])
57-
62+
5863
def results(self):
5964
results = SchulzeMethod.results(self)
6065
return results
61-
66+
6267
def __generate_vote_management_graph__(self):
63-
if self.compute_vote_management_with_pygraph == False: return
6468
self.vote_management_graph = digraph()
6569
self.vote_management_graph.add_nodes(self.completed_patterns)
66-
self.vote_management_graph.del_node(tuple([3]*self.required_winners))
70+
self.vote_management_graph.del_node(tuple([PREFERRED_MORE]*self.required_winners))
6771
self.pattern_nodes = self.vote_management_graph.nodes()
6872
self.vote_management_graph.add_nodes(["source","sink"])
6973
for pattern_node in self.pattern_nodes:
@@ -76,86 +80,70 @@ def __generate_vote_management_graph__(self):
7680
self.vote_management_graph.add_edge((pattern_node, i))
7781
for i in range(self.required_winners):
7882
self.vote_management_graph.add_edge((i, "sink"))
79-
80-
def __generate_completion_patterns__(self):
81-
self.completion_patterns = []
82-
for i in range(0,self.required_winners):
83-
for j in range(0, i+1):
84-
for pattern in self.__unique_permutations__([2]*(self.required_winners-i)+[1]*(j)+[3]*(i-j)):
85-
self.completion_patterns.append(tuple(pattern))
86-
83+
84+
# Generates a list of all patterns that do not contain indifference
8785
def __generate_completed_patterns__(self):
8886
self.completed_patterns = []
89-
for i in range(0,self.required_winners + 1):
90-
for pattern in self.__unique_permutations__([1]*(self.required_winners-i)+[3]*(i)):
87+
for i in range(0, self.required_winners + 1):
88+
for pattern in unique_permutations(
89+
[PREFERRED_LESS]*(self.required_winners-i)
90+
+ [PREFERRED_MORE]*(i)
91+
):
9192
self.completed_patterns.append(tuple(pattern))
92-
93-
def __unique_permutations__(self, xs):
94-
if len(xs)<2:
95-
yield xs
96-
else:
97-
h = []
98-
for x in xs:
99-
h.append(x)
100-
if x in h[:-1]:
101-
continue
102-
ts = xs[:]; ts.remove(x)
103-
for ps in self.__unique_permutations__(ts):
104-
yield [x]+ps
105-
93+
10694
def __proportional_completion__(self, candidate, other_candidates):
10795
profile = dict(zip(self.completed_patterns, [0]*len(self.completed_patterns)))
108-
96+
10997
# Obtain an initial tally from the ballots
11098
for ballot in self.ballots:
11199
pattern = []
112100
for other_candidate in other_candidates:
113101
if ballot["ballot"][candidate] < ballot["ballot"][other_candidate]:
114-
pattern.append(1)
102+
pattern.append(PREFERRED_LESS)
115103
elif ballot["ballot"][candidate] == ballot["ballot"][other_candidate]:
116-
pattern.append(2)
104+
pattern.append(PREFERRED_SAME)
117105
else:
118-
pattern.append(3)
106+
pattern.append(PREFERRED_MORE)
119107
pattern = tuple(pattern)
120108
if pattern not in profile:
121109
profile[pattern] = 0.0
122110
profile[pattern] += ballot["count"]
123-
124-
# Complete each pattern in order
125-
for pattern in self.completion_patterns:
126-
if pattern in profile:
127-
profile = self.__proportional_completion_round__(pattern, profile)
128-
111+
112+
# Peel off patterns with indifference (from the most to the least) and apply proportional completion to them
113+
for pattern in sorted(profile.keys(), key = lambda pattern: pattern.count(PREFERRED_SAME), reverse = True):
114+
if pattern.count(PREFERRED_SAME) == 0: break
115+
self.__proportional_completion_round__(pattern, profile)
116+
129117
return profile
130-
118+
131119
def __proportional_completion_round__(self, completion_pattern, profile):
132-
120+
133121
# Remove pattern that contains indifference
134122
completion_pattern_weight = profile[completion_pattern]
135123
del profile[completion_pattern]
136-
124+
137125
patterns_to_consider = {}
138126
for pattern in profile.keys():
139127
append = False
140128
append_target = []
141129
for i in range(len(completion_pattern)):
142-
if completion_pattern[i] != 2:
130+
if completion_pattern[i] != PREFERRED_SAME:
143131
append_target.append(completion_pattern[i])
144132
else:
145133
append_target.append(pattern[i])
146-
if completion_pattern[i] == 2 and pattern[i] != 2:
134+
if completion_pattern[i] == PREFERRED_SAME and pattern[i] != PREFERRED_SAME:
147135
append = True
148-
append_target = tuple(append_target)
149136
if append == True:
137+
append_target = tuple(append_target)
150138
if append_target not in patterns_to_consider:
151139
patterns_to_consider[append_target] = set()
152140
patterns_to_consider[append_target].add(pattern)
153-
141+
154142
denominator = 0
155143
for (append_target, patterns) in patterns_to_consider.items():
156144
for pattern in patterns:
157145
denominator += profile[pattern]
158-
146+
159147
# Reweight the remaining items
160148
for pattern in patterns_to_consider.keys():
161149
if denominator == 0:
@@ -164,117 +152,35 @@ def __proportional_completion_round__(self, completion_pattern, profile):
164152
if pattern not in profile:
165153
profile[pattern] = 0
166154
profile[pattern] += sum(profile[considered_pattern] for considered_pattern in patterns_to_consider[pattern]) * completion_pattern_weight / denominator
167-
155+
168156
return profile
169-
157+
170158
# This method converts the voter profile into a capacity graph and iterates
171159
# on the maximum flow using the Edmonds Karp algorithm. The end result is
172160
# the limit of the strength of the voter management as per Markus Schulze's
173161
# Calcul02.pdf (draft, 28 March 2008, abstract: "In this paper we illustrate
174162
# the calculation of the strengths of the vote managements.").
175163
def __strength_of_vote_management__(self, voter_profile):
176-
177-
# This implementation uses Python Graph Core and requires less code, but
178-
# runs roughly 2-3x slower than our internal implementation
179-
if self.compute_vote_management_with_pygraph:
180-
181-
# Initialize the graph weights
182-
for pattern in self.pattern_nodes:
183-
self.vote_management_graph.set_edge_weight(("source", pattern), voter_profile[pattern])
184-
for i in range(self.required_winners):
185-
if pattern[i] == 1:
186-
self.vote_management_graph.set_edge_weight((pattern, i), voter_profile[pattern])
187-
188-
# Iterate towards the limit
189-
r = [(float(sum(voter_profile.values())) - voter_profile[tuple([3]*self.required_winners)]) / self.required_winners]
190-
while len(r) < 2 or r[-2] - r[-1] > 0.0000000001:
191-
for i in range(self.required_winners):
192-
self.vote_management_graph.set_edge_weight((i, "sink"), r[-1])
193-
max_flow = maximum_flow(self.vote_management_graph, "source", "sink")
194-
sink_sum = sum(v for k,v in max_flow[0].iteritems() if k[1] == "sink")
195-
r.append(sink_sum/self.required_winners)
196-
197-
# Return the final max flow
198-
return round(r[-1],9)
199-
200-
# This implementation generates a capacity matrix and iterates on it
201-
# using a custom-written Edmonds-Karp implementation. It'd be nice to
202-
# speed up the Python Graph Core implementation and scrap this section,
203-
# along with the two accompanying methods.
204-
else:
205-
number_of_candidates = len(voter_profile.keys()[0])
206-
number_of_patterns = len(voter_profile) - 1
207-
number_of_nodes = 1 + number_of_patterns + number_of_candidates + 1
208-
ordered_patterns = sorted(voter_profile.keys())
209-
ordered_patterns.remove(tuple([3]*number_of_candidates))
210-
211-
r = [(float(sum(voter_profile.values())) - voter_profile[tuple([3]*number_of_candidates)]) / number_of_candidates]
212-
213-
# Generate a number_of_nodes x number_of_nodes matrix of zeroes
214-
C = []
215-
for i in range(number_of_nodes):
216-
C.append([0] * number_of_nodes)
217-
218-
# Source to voters
219-
vertex = 0
220-
for pattern in ordered_patterns:
221-
C[0][vertex+1] = voter_profile[pattern]
222-
vertex += 1
223-
224-
# Voters to candidates
225-
vertex = 0
226-
for pattern in ordered_patterns:
227-
for i in range(1,number_of_candidates + 1):
228-
if pattern[i-1] == 1:
229-
C[vertex+1][1 + number_of_patterns + i - 1] = voter_profile[pattern]
230-
vertex += 1
231-
232-
# Iterate towards the limit
233-
while len(r) < 2 or r[-2] - r[-1] > 0.0000000001:
234-
for i in range(number_of_candidates):
235-
C[1 + number_of_patterns + i][number_of_nodes - 1] = r[-1]
236-
r.append(SchulzeSTV.__edmonds_karp__(C,0,number_of_nodes-1)/number_of_candidates)
237-
return round(r[-1],9)
238-
239-
# The Edmonds-Karp algorithm is an implementation of the Ford-Fulkerson
240-
# method for computing the maximum flow in a flow network in O(VE^2).
241-
#
242-
# Sourced from http://semanticweb.org/wiki/Python_implementation_of_Edmonds-Karp_algorithm
243-
@staticmethod
244-
def __edmonds_karp__(C, source, sink):
245-
n = len(C) # C is the capacity matrix
246-
F = [[0] * n for i in xrange(n)]
247-
# residual capacity from u to v is C[u][v] - F[u][v]
248-
249-
while True:
250-
path = SchulzeSTV.__bfs__(C, F, source, sink)
251-
if not path:
252-
break
253-
# traverse path to find smallest capacity
254-
flow = min(C[u][v] - F[u][v] for u,v in path)
255-
# traverse path to update flow
256-
for u,v in path:
257-
F[u][v] += flow
258-
F[v][u] -= flow
259-
260-
return sum(F[source][i] for i in xrange(n))
261-
262-
# In graph theory, breadth-first search (BFS) is a graph search algorithm
263-
# that begins at the root node and explores all the neighboring nodes. Then
264-
# for each of those nearest nodes, it explores their unexplored neighbor
265-
# nodes, and so on, until it finds the goal.
266-
#
267-
# Sourced from http://semanticweb.org/wiki/Python_implementation_of_Edmonds-Karp_algorithm
268-
@staticmethod
269-
def __bfs__(C, F, source, sink):
270-
queue = [source]
271-
paths = {source: []}
272-
while queue:
273-
u = queue.pop(0)
274-
for v in xrange(len(C)):
275-
if C[u][v] - F[u][v] > 0 and v not in paths:
276-
paths[v] = paths[u] + [(u,v)]
277-
if v == sink:
278-
return paths[v]
279-
queue.append(v)
280-
return None
164+
165+
# Initialize the graph weights
166+
for pattern in self.pattern_nodes:
167+
self.vote_management_graph.set_edge_weight(("source", pattern), voter_profile[pattern])
168+
for i in range(self.required_winners):
169+
if pattern[i] == 1:
170+
self.vote_management_graph.set_edge_weight((pattern, i), voter_profile[pattern])
171+
172+
# Iterate towards the limit
173+
r = [(float(sum(voter_profile.values())) - voter_profile[tuple([PREFERRED_MORE]*self.required_winners)]) / self.required_winners]
174+
while len(r) < 2 or r[-2] - r[-1] > STRENGTH_TOLERANCE:
175+
for i in range(self.required_winners):
176+
self.vote_management_graph.set_edge_weight((i, "sink"), r[-1])
177+
max_flow = maximum_flow(self.vote_management_graph, "source", "sink")
178+
sink_sum = sum(v for k,v in max_flow[0].iteritems() if k[1] == "sink")
179+
r.append(sink_sum/self.required_winners)
180+
181+
# We expect strengths to be above a specified threshold
182+
if sink_sum < STRENGTH_THRESHOLD:
183+
return 0
184+
185+
# Return the final max flow
186+
return round(r[-1],9)

0 commit comments

Comments
 (0)