| 75 |
jtkorb |
1 |
# Compute Hare Ballot
|
|
|
2 |
#
|
|
|
3 |
# Tim Korb (jtk@cs.purdue.edu), May 2007
|
|
|
4 |
#
|
|
|
5 |
# A few definitions and notes...
|
|
|
6 |
#
|
|
|
7 |
# tally: dictionary in which the keys are candidate names and the
|
|
|
8 |
# values are lists of ballots currently assigned to that candidate
|
|
|
9 |
#
|
|
|
10 |
# ballot: list of candidates in the order determined by the voter
|
|
|
11 |
#
|
|
|
12 |
# winners: list of candidates that have reached the quota of ballots
|
|
|
13 |
# or have remained in the running long enough to be declared a winner
|
|
|
14 |
#
|
|
|
15 |
# losers: list of candidates that have been eliminated from the running
|
|
|
16 |
#
|
|
|
17 |
# Note that plurals are generally used to indicate lists of other
|
|
|
18 |
# items, e.g., ballots is a list of ballot items.
|
|
|
19 |
#
|
|
|
20 |
# To download the complete source distribution, use Subversion:
|
|
|
21 |
#
|
|
|
22 |
# % svn co http://www.bikmort.com/hare
|
|
|
23 |
#
|
|
|
24 |
# $Id: tally.py 78 2016-05-11 19:18:03Z jtkorb $
|
| 15 |
jtkorb |
25 |
|
|
|
26 |
import sys
|
|
|
27 |
import math
|
| 75 |
jtkorb |
28 |
import copy
|
| 15 |
jtkorb |
29 |
import random
|
|
|
30 |
|
| 45 |
jtkorb |
31 |
fdOut = None
|
| 15 |
jtkorb |
32 |
|
| 75 |
jtkorb |
33 |
traceLevel = 0 # generate trace output if trace >= trace parameter
|
|
|
34 |
fParallel = 0 # track parallel universes instead of randomization if possible
|
|
|
35 |
|
|
|
36 |
class Universe:
|
|
|
37 |
id = 0 # id of last created Universe
|
|
|
38 |
pendingUniverses = []
|
|
|
39 |
results = {}
|
|
|
40 |
dropped = 0
|
|
|
41 |
|
|
|
42 |
def __reset(self):
|
| 78 |
jtkorb |
43 |
Universe.id = 0
|
|
|
44 |
Universe.pendingUniverses = []
|
|
|
45 |
Universe.results = {}
|
|
|
46 |
Universe.dropped = 0
|
| 75 |
jtkorb |
47 |
|
| 78 |
jtkorb |
48 |
|
| 75 |
jtkorb |
49 |
def __init__(self, p, nWinners, nBallots, quota):
|
| 78 |
jtkorb |
50 |
if p == 1.0:
|
|
|
51 |
self.__reset()
|
| 75 |
jtkorb |
52 |
Universe.id += 1
|
|
|
53 |
self.id = Universe.id
|
|
|
54 |
|
|
|
55 |
self.p = p
|
| 78 |
jtkorb |
56 |
self.nWinners = nWinners
|
| 75 |
jtkorb |
57 |
self.nBallots = nBallots
|
|
|
58 |
self.quota = quota
|
|
|
59 |
|
|
|
60 |
self.winners = []
|
|
|
61 |
self.losers = []
|
|
|
62 |
self.tally = {}
|
|
|
63 |
|
| 78 |
jtkorb |
64 |
self.pendingItem = None
|
|
|
65 |
self.pendingType = None
|
|
|
66 |
self.pendingData = None
|
| 75 |
jtkorb |
67 |
|
|
|
68 |
def forkPendingWinners(self, lWin):
|
| 78 |
jtkorb |
69 |
return self.__forkPendingList(lWin, "winner")
|
| 75 |
jtkorb |
70 |
|
|
|
71 |
def forkPendingLosers(self, lMin):
|
| 78 |
jtkorb |
72 |
return self.__forkPendingList(lMin, "loser")
|
| 75 |
jtkorb |
73 |
|
|
|
74 |
def forkPendingRedist(self, lRedist, winner):
|
| 78 |
jtkorb |
75 |
return self.__forkPendingList(lRedist, "redist", winner)
|
| 75 |
jtkorb |
76 |
|
|
|
77 |
def __forkPendingList(self, lItems, type, data=None):
|
| 78 |
jtkorb |
78 |
global fParallel
|
| 75 |
jtkorb |
79 |
|
|
|
80 |
if len(lItems) == 1:
|
| 78 |
jtkorb |
81 |
return lItems[0]
|
|
|
82 |
if not fParallel:
|
|
|
83 |
trace(1, "\tmaking random choice among %d alternatives" %
|
|
|
84 |
len(lItems))
|
|
|
85 |
return random.choice(lItems)
|
| 75 |
jtkorb |
86 |
|
| 78 |
jtkorb |
87 |
# Split into parallel universes unless too many with small probability
|
|
|
88 |
#
|
|
|
89 |
if Universe.id > 10000 and self.p/len(lItems) < 1.0/10000:
|
|
|
90 |
trace(1, "\tpassing on %d universes (p = %f)" %
|
|
|
91 |
(len(lItems)-1, self.p/len(lItems)))
|
|
|
92 |
trace(1, "\tmaking random choice among %d alternatives" %
|
|
|
93 |
len(lItems))
|
|
|
94 |
Universe.dropped += len(lItems)-1
|
|
|
95 |
return random.choice(lItems)
|
| 75 |
jtkorb |
96 |
|
| 78 |
jtkorb |
97 |
self.p /= len(lItems)
|
|
|
98 |
trace(1, "\tcreating %d universes (p = %f)" % (len(lItems)-1, self.p))
|
| 75 |
jtkorb |
99 |
|
| 78 |
jtkorb |
100 |
for item in lItems[1:]:
|
|
|
101 |
u = Universe(self.p, self.nWinners, self.nBallots, self.quota)
|
|
|
102 |
u.winners = self.winners[:]
|
|
|
103 |
u.losers = self.losers[:]
|
|
|
104 |
u.tally = copy.deepcopy(self.tally)
|
|
|
105 |
u.pendingItem = item
|
|
|
106 |
u.pendingType = type
|
|
|
107 |
u.pendingData = data
|
|
|
108 |
Universe.pendingUniverses.append(u)
|
|
|
109 |
return lItems[0]
|
| 75 |
jtkorb |
110 |
|
|
|
111 |
def combinations(l, n):
|
|
|
112 |
if n == 0:
|
| 78 |
jtkorb |
113 |
return [[]]
|
| 75 |
jtkorb |
114 |
else:
|
|
|
115 |
lResult = []
|
| 78 |
jtkorb |
116 |
for i in range(len(l)):
|
|
|
117 |
for lsub in combinations(l[i+1:], n-1):
|
|
|
118 |
lResult.append([l[i]] + lsub)
|
| 75 |
jtkorb |
119 |
return lResult
|
|
|
120 |
|
|
|
121 |
def trace(t, s):
|
|
|
122 |
global traceLevel, fdOut
|
|
|
123 |
if traceLevel >= t: print >>fdOut, s
|
| 15 |
jtkorb |
124 |
return
|
|
|
125 |
|
| 75 |
jtkorb |
126 |
def findNextWinner(u):
|
|
|
127 |
cWin = u.quota # number of votes for highest winner
|
|
|
128 |
lWin = [] # list of candidates with highest votes
|
|
|
129 |
for c in u.tally.keys():
|
|
|
130 |
if c not in u.losers and c not in u.winners and len(u.tally[c]) >= cWin:
|
|
|
131 |
if len(u.tally[c]) == cWin:
|
| 39 |
jtkorb |
132 |
lWin.append(c)
|
|
|
133 |
else:
|
|
|
134 |
lWin = [c]
|
| 75 |
jtkorb |
135 |
cWin = len(u.tally[c])
|
|
|
136 |
if len(lWin) > 0:
|
|
|
137 |
return u.forkPendingWinners(lWin)
|
| 15 |
jtkorb |
138 |
|
|
|
139 |
# Check to see if only enough candidates remain
|
| 75 |
jtkorb |
140 |
# TODO: sort by len(tally[c]) to choose larger winners first
|
|
|
141 |
# create multiple universes if some have equal votes
|
| 15 |
jtkorb |
142 |
n = 0
|
|
|
143 |
last = ""
|
| 75 |
jtkorb |
144 |
for c in u.tally.keys():
|
|
|
145 |
if c not in u.winners and c not in u.losers:
|
| 40 |
jtkorb |
146 |
last = c
|
| 15 |
jtkorb |
147 |
n = n + 1
|
| 75 |
jtkorb |
148 |
if u.nWinners - len(u.winners) >= n:
|
|
|
149 |
trace(1, "\tremaining winner(s) have fewer than quota (%d) ballots" %
|
|
|
150 |
u.quota)
|
| 15 |
jtkorb |
151 |
return last
|
|
|
152 |
return
|
|
|
153 |
|
| 75 |
jtkorb |
154 |
def redistributeWinner(winner, u):
|
|
|
155 |
excess = len(u.tally[winner]) - u.quota
|
| 15 |
jtkorb |
156 |
if excess <= 0:
|
| 75 |
jtkorb |
157 |
trace(1, "\tno excess ballots to redistribute")
|
| 15 |
jtkorb |
158 |
else:
|
| 75 |
jtkorb |
159 |
lEffective = gatherEffectiveBallots(winner, u)
|
|
|
160 |
nRedist = min(excess, len(lEffective))
|
| 78 |
jtkorb |
161 |
trace(1, "\tredistributing %d effective ballots, %d at a time" %
|
|
|
162 |
(len(lEffective), nRedist))
|
|
|
163 |
for ballot in u.forkPendingRedist(combinations(lEffective, nRedist), winner):
|
| 75 |
jtkorb |
164 |
u.tally[winner].remove(ballot)
|
|
|
165 |
redistributeBallot(ballot, u)
|
|
|
166 |
traceTally(1, u)
|
| 15 |
jtkorb |
167 |
|
| 75 |
jtkorb |
168 |
def gatherEffectiveBallots(winner, u):
|
| 44 |
jtkorb |
169 |
lEffective = []
|
| 75 |
jtkorb |
170 |
for ballot in u.tally[winner]:
|
| 44 |
jtkorb |
171 |
for candidateTo in ballot:
|
| 75 |
jtkorb |
172 |
if candidateTo not in u.winners and candidateTo not in u.losers:
|
| 44 |
jtkorb |
173 |
lEffective.append(ballot)
|
|
|
174 |
break
|
|
|
175 |
return lEffective
|
|
|
176 |
|
| 75 |
jtkorb |
177 |
def redistributeBallot(ballot, u):
|
| 15 |
jtkorb |
178 |
for candidateTo in ballot:
|
| 75 |
jtkorb |
179 |
if candidateTo not in u.winners and candidateTo not in u.losers:
|
|
|
180 |
trace(1, "\tto %s: %s" % (candidateTo, ballot))
|
|
|
181 |
if not u.tally.has_key(candidateTo): u.tally[candidateTo] = []
|
|
|
182 |
u.tally[candidateTo].append(ballot)
|
| 21 |
jtkorb |
183 |
ballot = ""
|
|
|
184 |
break
|
| 15 |
jtkorb |
185 |
if ballot:
|
| 75 |
jtkorb |
186 |
trace(1, "\tineffective ballot dropped: %s" % ballot)
|
| 15 |
jtkorb |
187 |
|
| 75 |
jtkorb |
188 |
def findNextLoser(u):
|
| 15 |
jtkorb |
189 |
cMin = sys.maxint # least number of votes for candidate loser
|
|
|
190 |
lMin = [] # list of candidates with least votes
|
| 75 |
jtkorb |
191 |
for c in u.tally.keys():
|
|
|
192 |
if c not in u.losers and c not in u.winners and len(u.tally[c]) <= cMin:
|
|
|
193 |
if len(u.tally[c]) == cMin:
|
| 21 |
jtkorb |
194 |
lMin.append(c)
|
|
|
195 |
else:
|
|
|
196 |
lMin = [c]
|
| 75 |
jtkorb |
197 |
cMin = len(u.tally[c])
|
| 15 |
jtkorb |
198 |
if len(lMin) == 0:
|
|
|
199 |
return None
|
| 75 |
jtkorb |
200 |
else:
|
|
|
201 |
return u.forkPendingLosers(lMin)
|
| 15 |
jtkorb |
202 |
|
| 75 |
jtkorb |
203 |
def redistributeLoser(loser, u):
|
|
|
204 |
excess = len(u.tally[loser])
|
| 15 |
jtkorb |
205 |
if excess <= 0:
|
| 75 |
jtkorb |
206 |
trace(1, "\tno ballots to redistribute")
|
| 15 |
jtkorb |
207 |
else:
|
| 75 |
jtkorb |
208 |
trace(1, "\tredistributing %d ballot(s) from %s" % (excess, loser))
|
|
|
209 |
while len(u.tally[loser]) > 0:
|
|
|
210 |
ballot = u.tally[loser][0]
|
|
|
211 |
u.tally[loser] = u.tally[loser][1:]
|
|
|
212 |
redistributeBallot(ballot, u)
|
|
|
213 |
traceTally(1, u)
|
| 15 |
jtkorb |
214 |
return
|
|
|
215 |
|
| 75 |
jtkorb |
216 |
def traceTally(t, u):
|
|
|
217 |
global traceLevel
|
|
|
218 |
if traceLevel < t: return
|
|
|
219 |
trace(t, "\nCURRENT ASSIGNMENT OF BALLOTS (%d needed to win)" % u.quota)
|
|
|
220 |
for candidate in u.tally.keys():
|
|
|
221 |
trace(t, "\t%s:" % candidate)
|
|
|
222 |
for ballot in u.tally[candidate]:
|
|
|
223 |
trace(t, "\t\t%s" % ballot)
|
| 15 |
jtkorb |
224 |
return
|
|
|
225 |
|
|
|
226 |
# The basic Single Transferable Vote algorithm with Hare quota
|
|
|
227 |
#
|
|
|
228 |
# while winners < nWinners:
|
|
|
229 |
# if a candidate has more than quota votes:
|
|
|
230 |
# redistribute excess votes to next priority candidate
|
|
|
231 |
# else:
|
|
|
232 |
# eliminate lowest ranking candidate
|
|
|
233 |
# redistribute wasted votes to next priority candidate
|
|
|
234 |
#
|
|
|
235 |
def dotally(nWinners, ballots):
|
| 75 |
jtkorb |
236 |
global fParallel
|
| 15 |
jtkorb |
237 |
|
| 75 |
jtkorb |
238 |
nBallots=len(ballots)
|
|
|
239 |
quota=int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
|
| 15 |
jtkorb |
240 |
|
| 75 |
jtkorb |
241 |
u = Universe(1.0, nWinners, nBallots, quota)
|
|
|
242 |
|
|
|
243 |
trace(0, "INPUT SUMMARY")
|
|
|
244 |
trace(0, "\t%d ballots" % u.nBallots)
|
|
|
245 |
trace(0, "\tChoosing %s winners" % u.nWinners)
|
|
|
246 |
trace(0, "\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %
|
|
|
247 |
(u.nBallots, u.nWinners, u.quota))
|
|
|
248 |
|
| 45 |
jtkorb |
249 |
# Create initial tally and statistics
|
| 15 |
jtkorb |
250 |
#
|
|
|
251 |
for ballot in ballots:
|
|
|
252 |
candidate = ballot[0]
|
| 75 |
jtkorb |
253 |
if not u.tally.has_key(candidate):
|
|
|
254 |
u.tally[candidate] = []
|
|
|
255 |
u.tally[candidate].append(ballot)
|
| 45 |
jtkorb |
256 |
|
|
|
257 |
total1st = {}
|
|
|
258 |
total2nd = {}
|
|
|
259 |
totalMrk = {}
|
|
|
260 |
|
|
|
261 |
for ballot in ballots:
|
|
|
262 |
for c in ballot:
|
|
|
263 |
total1st[c] = 0
|
|
|
264 |
total2nd[c] = 0
|
|
|
265 |
totalMrk[c] = 0
|
|
|
266 |
|
|
|
267 |
for ballot in ballots:
|
|
|
268 |
total1st[ballot[0]] += 1
|
|
|
269 |
if len(ballot) > 1:
|
|
|
270 |
total2nd[ballot[1]] += 1
|
|
|
271 |
for c in ballot:
|
|
|
272 |
totalMrk[c] += 1
|
|
|
273 |
|
| 75 |
jtkorb |
274 |
trace(0, "\n\tBALLOT MARKS\t1ST\t2ND\tNONE")
|
| 45 |
jtkorb |
275 |
candidates = totalMrk.keys()
|
|
|
276 |
candidates.sort()
|
|
|
277 |
for c in candidates:
|
| 75 |
jtkorb |
278 |
trace(0, "\t%-16s%3d\t%3d\t%4d" %
|
| 78 |
jtkorb |
279 |
(c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))
|
| 75 |
jtkorb |
280 |
traceTally(0, u)
|
| 15 |
jtkorb |
281 |
|
| 75 |
jtkorb |
282 |
Universe.pendingUniverses.append(u)
|
|
|
283 |
|
|
|
284 |
while len(Universe.pendingUniverses) > 0:
|
| 78 |
jtkorb |
285 |
u = Universe.pendingUniverses.pop()
|
|
|
286 |
trace(1, "POPPED UNIVERSE %d (p = %f, %d pending):" %
|
|
|
287 |
(u.id, u.p, len(Universe.pendingUniverses)))
|
| 75 |
jtkorb |
288 |
|
| 78 |
jtkorb |
289 |
if u.pendingType == "redist":
|
|
|
290 |
trace(1, "\tredistributing %d ballots" % len(u.pendingItem))
|
|
|
291 |
winner = u.pendingData
|
|
|
292 |
assert(winner in u.winners)
|
|
|
293 |
for ballot in u.pendingItem:
|
| 75 |
jtkorb |
294 |
u.tally[winner].remove(ballot)
|
|
|
295 |
redistributeBallot(ballot, u)
|
|
|
296 |
elif u.pendingType == "winner":
|
| 78 |
jtkorb |
297 |
trace(1, "\tfinishing pending winner %s" % u.pendingItem)
|
|
|
298 |
winner = u.pendingItem
|
|
|
299 |
assert(winner not in u.winners)
|
| 75 |
jtkorb |
300 |
u.winners.append(winner)
|
|
|
301 |
trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))
|
|
|
302 |
redistributeWinner(winner, u)
|
| 78 |
jtkorb |
303 |
elif u.pendingType == "loser":
|
|
|
304 |
trace(1, "\tfinishing pending loser %s" % u.pendingItem)
|
|
|
305 |
loser = u.pendingItem
|
| 75 |
jtkorb |
306 |
u.losers.append(loser)
|
|
|
307 |
trace(1, "\nELIMINATED: %s" % loser)
|
|
|
308 |
redistributeLoser(loser, u)
|
|
|
309 |
|
|
|
310 |
u.pendingType = None
|
|
|
311 |
|
|
|
312 |
while len(u.winners) < u.nWinners:
|
|
|
313 |
winner = findNextWinner(u)
|
|
|
314 |
if winner:
|
|
|
315 |
u.winners.append(winner)
|
|
|
316 |
trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))
|
|
|
317 |
redistributeWinner(winner, u)
|
| 15 |
jtkorb |
318 |
else:
|
| 78 |
jtkorb |
319 |
loser = findNextLoser(u)
|
| 75 |
jtkorb |
320 |
if loser:
|
|
|
321 |
u.losers.append(loser)
|
|
|
322 |
trace(1, "\nELIMINATED: %s" % loser)
|
|
|
323 |
redistributeLoser(loser, u)
|
|
|
324 |
else:
|
|
|
325 |
trace(1, "Not enough chosen candidates to fill all positions")
|
|
|
326 |
break
|
| 15 |
jtkorb |
327 |
|
| 78 |
jtkorb |
328 |
winners = str(u.winners)
|
|
|
329 |
if fParallel:
|
|
|
330 |
trace(1, "RESULTS FOR UNIVERSE %d (p = %f): %s" % (u.id, u.p, winners))
|
|
|
331 |
if winners not in Universe.results: Universe.results[winners] = 0.0
|
|
|
332 |
Universe.results[winners] += u.p
|
| 15 |
jtkorb |
333 |
|
| 75 |
jtkorb |
334 |
if fParallel:
|
| 78 |
jtkorb |
335 |
trace(0, "\nTracked %d alternative decisions (skipped %d with low probability)" %
|
|
|
336 |
(Universe.id, Universe.dropped))
|
| 75 |
jtkorb |
337 |
|
|
|
338 |
trace(0, "\nFINAL RESULTS")
|
|
|
339 |
|
|
|
340 |
lWinners = []
|
|
|
341 |
for winner in Universe.results.keys():
|
| 78 |
jtkorb |
342 |
if fParallel:
|
|
|
343 |
lWinners.append("%f: %s" % (Universe.results[winner], winner))
|
|
|
344 |
else:
|
|
|
345 |
lWinners.append("%s" % winner)
|
| 75 |
jtkorb |
346 |
lWinners.sort()
|
|
|
347 |
lWinners.reverse()
|
|
|
348 |
|
|
|
349 |
for winner in lWinners:
|
| 78 |
jtkorb |
350 |
trace(0, winner)
|
| 75 |
jtkorb |
351 |
|
|
|
352 |
return lWinners
|