Subversion Repositories Remote Hare Voting

Rev

Rev 63 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
15 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
##
24 jtkorb 20
## To download the complete source distribution, use Subversion:
21
##
51 jtkorb 22
##      % svn co http://www.bikmort.com/hare
24 jtkorb 23
##
55 jtkorb 24
## $Id: tally.py 64 2007-10-15 15:48:23Z jtkorb $
15 jtkorb 25
 
26
import sys
27
import math 
61 jtkorb 28
import copy
15 jtkorb 29
import random
30
 
45 jtkorb 31
fdOut = None
15 jtkorb 32
 
63 jtkorb 33
traceLevel = 0	# generate trace output if trace >= trace parameter
34
fParallel = 0	# track parallel universes instead of randomization if possible
35
 
53 jtkorb 36
class Universe:
57 jtkorb 37
    id = 0                              # id of last created Universe
38
    pendingUniverses = []
61 jtkorb 39
    results = {}
40
    dropped = 0
53 jtkorb 41
 
61 jtkorb 42
    def __reset(self):
43
	Universe.id = 0
44
 	Universe.pendingUniverses = []
45
	Universe.results = {}
46
	Universe.dropped = 0
47
 
48
 
54 jtkorb 49
    def __init__(self, p, nWinners, nBallots, quota):
61 jtkorb 50
	if p == 1.0:
51
	    self.__reset()
54 jtkorb 52
        Universe.id += 1
53
        self.id = Universe.id
61 jtkorb 54
 
54 jtkorb 55
        self.p = p
61 jtkorb 56
	self.nWinners = nWinners
54 jtkorb 57
        self.nBallots = nBallots
58
        self.quota = quota
61 jtkorb 59
 
54 jtkorb 60
        self.winners = []
61
        self.losers = []
62
        self.tally = {}
63
 
61 jtkorb 64
	self.pendingItem = None
65
	self.pendingType = None
66
	self.pendingData = None
67
 
57 jtkorb 68
    def forkPendingWinners(self, lWin):
69
	return self.__forkPendingList(lWin, "winner")
53 jtkorb 70
 
57 jtkorb 71
    def forkPendingLosers(self, lMin):
72
	return self.__forkPendingList(lMin, "loser")
73
 
61 jtkorb 74
    def forkPendingRedist(self, lRedist, winner):
75
	return self.__forkPendingList(lRedist, "redist", winner)
57 jtkorb 76
 
61 jtkorb 77
    def __forkPendingList(self, lItems, type, data=None):
63 jtkorb 78
	global fParallel
79
 
61 jtkorb 80
        if len(lItems) == 1:
81
	    return lItems[0]
63 jtkorb 82
	if not fParallel:
83
	    trace(1, "\tmaking random choice among %d alternatives" % 
84
		len(lItems))
85
	    return random.choice(lItems)
86
 
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))
61 jtkorb 94
	    Universe.dropped += len(lItems)-1
95
	    return random.choice(lItems)
63 jtkorb 96
 
57 jtkorb 97
	self.p /= len(lItems)
63 jtkorb 98
	trace(1, "\tcreating %d universes (p = %f)" % (len(lItems)-1, self.p))
99
 
57 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[:]
61 jtkorb 104
	    u.tally = copy.deepcopy(self.tally)
105
	    u.pendingItem = item
106
	    u.pendingType = type
107
	    u.pendingData = data
57 jtkorb 108
	    Universe.pendingUniverses.append(u)
109
	return lItems[0]
110
 
111
def combinations(l, n):
112
    if n == 0:
61 jtkorb 113
	return [[]]
57 jtkorb 114
    else:
61 jtkorb 115
        lResult = []
57 jtkorb 116
	for i in range(len(l)):
117
	    for lsub in combinations(l[i+1:], n-1):
61 jtkorb 118
		lResult.append([l[i]] + lsub)
119
        return lResult
57 jtkorb 120
 
63 jtkorb 121
def trace(t, s):
122
    global traceLevel, fdOut
123
    if traceLevel >= t: print >>fdOut, s
15 jtkorb 124
    return
125
 
57 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]
57 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
40 jtkorb 140
    ## TODO: sort by len(tally[c]) to choose larger winners first
61 jtkorb 141
    ## create multiple universes if some have equal votes
15 jtkorb 142
    n = 0
143
    last = ""
57 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
57 jtkorb 148
    if u.nWinners - len(u.winners) >= n:
63 jtkorb 149
        trace(1, "\tremaining winner(s) have fewer than quota (%d) ballots" %
57 jtkorb 150
            u.quota)
15 jtkorb 151
        return last
152
    return
153
 
57 jtkorb 154
def redistributeWinner(winner, u):
155
    excess = len(u.tally[winner]) - u.quota
15 jtkorb 156
    if excess <= 0:
63 jtkorb 157
        trace(1, "\tno excess ballots to redistribute")
15 jtkorb 158
    else:
57 jtkorb 159
        lEffective = gatherEffectiveBallots(winner, u)
61 jtkorb 160
        nRedist = min(excess, len(lEffective))
63 jtkorb 161
	trace(1, "\tredistributing %d effective ballots, %d at a time" % 
162
	    (len(lEffective), nRedist))
61 jtkorb 163
	for ballot in u.forkPendingRedist(combinations(lEffective, nRedist), winner):
57 jtkorb 164
            u.tally[winner].remove(ballot)
165
            redistributeBallot(ballot, u)
63 jtkorb 166
    traceTally(1, u)
15 jtkorb 167
 
57 jtkorb 168
def gatherEffectiveBallots(winner, u):
44 jtkorb 169
    lEffective = []
57 jtkorb 170
    for ballot in u.tally[winner]:
44 jtkorb 171
        for candidateTo in ballot:
57 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
 
57 jtkorb 177
def redistributeBallot(ballot, u):
15 jtkorb 178
    for candidateTo in ballot:
57 jtkorb 179
        if candidateTo not in u.winners and candidateTo not in u.losers:
63 jtkorb 180
            trace(1, "\tto %s: %s" % (candidateTo, ballot))
57 jtkorb 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:
63 jtkorb 186
        trace(1, "\tineffective ballot dropped: %s" % ballot)
15 jtkorb 187
 
57 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
57 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]
57 jtkorb 197
                cMin = len(u.tally[c])
15 jtkorb 198
    if len(lMin) == 0:
199
        return None
57 jtkorb 200
    else: 
201
        return u.forkPendingLosers(lMin)
15 jtkorb 202
 
57 jtkorb 203
def redistributeLoser(loser, u):
204
    excess = len(u.tally[loser])
15 jtkorb 205
    if excess <= 0:
63 jtkorb 206
        trace(1, "\tno ballots to redistribute")
15 jtkorb 207
    else:
63 jtkorb 208
        trace(1, "\tredistributing %d ballot(s) from %s" % (excess, loser))
57 jtkorb 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)
63 jtkorb 213
    traceTally(1, u)
15 jtkorb 214
    return
215
 
63 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)
57 jtkorb 220
    for candidate in u.tally.keys():
63 jtkorb 221
        trace(t, "\t%s:" % candidate)
57 jtkorb 222
        for ballot in u.tally[candidate]:
63 jtkorb 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):
63 jtkorb 236
    global fParallel
237
 
54 jtkorb 238
    nBallots=len(ballots)
239
    quota=int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
240
 
57 jtkorb 241
    u = Universe(1.0, nWinners, nBallots, quota)
54 jtkorb 242
 
63 jtkorb 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" %
57 jtkorb 247
          (u.nBallots, u.nWinners, u.quota))
15 jtkorb 248
 
45 jtkorb 249
    # Create initial tally and statistics
15 jtkorb 250
    #
251
    for ballot in ballots:
252
        candidate = ballot[0]
57 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
 
63 jtkorb 274
    trace(0, "\n\tBALLOT MARKS\t1ST\t2ND\tNONE")
45 jtkorb 275
    candidates = totalMrk.keys()
276
    candidates.sort()
277
    for c in candidates:
63 jtkorb 278
        trace(0, "\t%-16s%3d\t%3d\t%4d" % 
279
	    (c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))
280
    traceTally(0, u)
15 jtkorb 281
 
61 jtkorb 282
    Universe.pendingUniverses.append(u)
54 jtkorb 283
 
61 jtkorb 284
    while len(Universe.pendingUniverses) > 0:
285
    	u = Universe.pendingUniverses.pop()
63 jtkorb 286
	trace(1, "POPPED UNIVERSE %d (p = %f, %d pending):" % 
61 jtkorb 287
	    (u.id, u.p, len(Universe.pendingUniverses)))
288
 
289
	if u.pendingType == "redist":
63 jtkorb 290
	    trace(1, "\tredistributing %d ballots" % len(u.pendingItem))
61 jtkorb 291
	    winner = u.pendingData
292
	    assert(winner in u.winners)
293
	    for ballot in u.pendingItem:
294
                u.tally[winner].remove(ballot)
295
                redistributeBallot(ballot, u)
296
        elif u.pendingType == "winner":
63 jtkorb 297
	    trace(1, "\tfinishing pending winner %s" % u.pendingItem)
61 jtkorb 298
	    winner = u.pendingItem
299
	    assert(winner not in u.winners)
57 jtkorb 300
            u.winners.append(winner)
63 jtkorb 301
            trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))
57 jtkorb 302
            redistributeWinner(winner, u)
61 jtkorb 303
	elif u.pendingType == "loser":
63 jtkorb 304
	    trace(1, "\tfinishing pending loser %s" % u.pendingItem)
61 jtkorb 305
	    loser = u.pendingItem
306
            u.losers.append(loser)
63 jtkorb 307
            trace(1, "\nELIMINATED: %s" % loser)
61 jtkorb 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)
63 jtkorb 316
                trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))
61 jtkorb 317
                redistributeWinner(winner, u)
15 jtkorb 318
            else:
61 jtkorb 319
	        loser = findNextLoser(u)
320
                if loser:
321
                    u.losers.append(loser)
63 jtkorb 322
                    trace(1, "\nELIMINATED: %s" % loser)
61 jtkorb 323
                    redistributeLoser(loser, u)
324
                else:
63 jtkorb 325
                    trace(1, "Not enough chosen candidates to fill all positions")
61 jtkorb 326
                    break
327
 
328
	winners = str(u.winners)
64 jtkorb 329
	if fParallel:
330
	    trace(1, "RESULTS FOR UNIVERSE %d (p = %f): %s" % (u.id, u.p, winners))
61 jtkorb 331
	if winners not in Universe.results: Universe.results[winners] = 0.0
332
	Universe.results[winners] += u.p
333
 
63 jtkorb 334
    if fParallel:
64 jtkorb 335
	trace(0, "\nTracked %d alternative decisions (skipped %d with low probability)" %
63 jtkorb 336
	    (Universe.id, Universe.dropped))
337
 
338
    trace(0, "\nFINAL RESULTS")
339
 
61 jtkorb 340
    lWinners = []
341
    for winner in Universe.results.keys():
64 jtkorb 342
	if fParallel:
343
	    lWinners.append("%f: %s" % (Universe.results[winner], winner))
344
	else:
345
	    lWinners.append("%s" % winner)
61 jtkorb 346
    lWinners.sort()
347
    lWinners.reverse()
63 jtkorb 348
 
349
    for winner in lWinners:
350
	trace(0, winner)
351
 
61 jtkorb 352
    return lWinners