Subversion Repositories Remote Hare Voting

Rev

Rev 61 | 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 62 2007-09-24 13:57:27Z 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
fTrace = 1
33
 
53 jtkorb 34
class Universe:
57 jtkorb 35
    id = 0                              # id of last created Universe
36
    pendingUniverses = []
61 jtkorb 37
    results = {}
38
    dropped = 0
53 jtkorb 39
 
61 jtkorb 40
    def __reset(self):
41
	Universe.id = 0
42
 	Universe.pendingUniverses = []
43
	Universe.results = {}
44
	Universe.dropped = 0
45
 
46
 
54 jtkorb 47
    def __init__(self, p, nWinners, nBallots, quota):
61 jtkorb 48
	if p == 1.0:
49
	    self.__reset()
54 jtkorb 50
        Universe.id += 1
51
        self.id = Universe.id
61 jtkorb 52
 
54 jtkorb 53
        self.p = p
61 jtkorb 54
	self.nWinners = nWinners
54 jtkorb 55
        self.nBallots = nBallots
56
        self.quota = quota
61 jtkorb 57
 
54 jtkorb 58
        self.winners = []
59
        self.losers = []
60
        self.tally = {}
61
 
61 jtkorb 62
	self.pendingItem = None
63
	self.pendingType = None
64
	self.pendingData = None
65
 
57 jtkorb 66
    def forkPendingWinners(self, lWin):
67
	return self.__forkPendingList(lWin, "winner")
53 jtkorb 68
 
57 jtkorb 69
    def forkPendingLosers(self, lMin):
70
	return self.__forkPendingList(lMin, "loser")
71
 
61 jtkorb 72
    def forkPendingRedist(self, lRedist, winner):
73
	return self.__forkPendingList(lRedist, "redist", winner)
57 jtkorb 74
 
61 jtkorb 75
    def __forkPendingList(self, lItems, type, data=None):
76
        if len(lItems) == 1:
77
	    return lItems[0]
62 jtkorb 78
	if self.p/len(lItems) < 1.0/1000000:
61 jtkorb 79
	    trace("\tpassing on %d universes (p = %f)" % (len(lItems)-1, self.p/len(lItems)))
80
	    Universe.dropped += len(lItems)-1
81
	    return random.choice(lItems)
57 jtkorb 82
	self.p /= len(lItems)
61 jtkorb 83
	trace("\tcreating %d universes (p = %f)" % (len(lItems)-1, self.p))
57 jtkorb 84
	for item in lItems[1:]:
85
	    u = Universe(self.p, self.nWinners, self.nBallots, self.quota)
86
	    u.winners = self.winners[:]
87
	    u.losers = self.losers[:]
61 jtkorb 88
	    u.tally = copy.deepcopy(self.tally)
89
	    u.pendingItem = item
90
	    u.pendingType = type
91
	    u.pendingData = data
57 jtkorb 92
	    Universe.pendingUniverses.append(u)
93
	return lItems[0]
94
 
95
def combinations(l, n):
96
    if n == 0:
61 jtkorb 97
	return [[]]
57 jtkorb 98
    else:
61 jtkorb 99
        lResult = []
57 jtkorb 100
	for i in range(len(l)):
101
	    for lsub in combinations(l[i+1:], n-1):
61 jtkorb 102
		lResult.append([l[i]] + lsub)
103
        return lResult
57 jtkorb 104
 
15 jtkorb 105
def trace(s):
45 jtkorb 106
    global fTrace, fdOut
107
    if fTrace: print >>fdOut, s
15 jtkorb 108
    return
109
 
57 jtkorb 110
def findNextWinner(u):
111
    cWin = u.quota	# number of votes for highest winner
112
    lWin = []		# list of candidates with highest votes
113
    for c in u.tally.keys():
114
        if c not in u.losers and c not in u.winners and len(u.tally[c]) >= cWin:
115
            if len(u.tally[c]) == cWin:
39 jtkorb 116
                lWin.append(c)
117
            else:
118
                lWin = [c]
57 jtkorb 119
                cWin = len(u.tally[c])
120
    if len(lWin) > 0:
121
        return u.forkPendingWinners(lWin)
15 jtkorb 122
 
123
    # Check to see if only enough candidates remain
40 jtkorb 124
    ## TODO: sort by len(tally[c]) to choose larger winners first
61 jtkorb 125
    ## create multiple universes if some have equal votes
15 jtkorb 126
    n = 0
127
    last = ""
57 jtkorb 128
    for c in u.tally.keys():
129
        if c not in u.winners and c not in u.losers:
40 jtkorb 130
            last = c
15 jtkorb 131
            n = n + 1
57 jtkorb 132
    if u.nWinners - len(u.winners) >= n:
40 jtkorb 133
        trace("\tremaining winner(s) have fewer than quota (%d) ballots" %
57 jtkorb 134
            u.quota)
15 jtkorb 135
        return last
136
    return
137
 
57 jtkorb 138
def redistributeWinner(winner, u):
139
    excess = len(u.tally[winner]) - u.quota
15 jtkorb 140
    if excess <= 0:
21 jtkorb 141
        trace("\tno excess ballots to redistribute")
15 jtkorb 142
    else:
57 jtkorb 143
        lEffective = gatherEffectiveBallots(winner, u)
61 jtkorb 144
        nRedist = min(excess, len(lEffective))
145
	trace("\tredistributing %d effective ballots, %d at a time" % (len(lEffective), nRedist))
146
	for ballot in u.forkPendingRedist(combinations(lEffective, nRedist), winner):
57 jtkorb 147
            u.tally[winner].remove(ballot)
148
            redistributeBallot(ballot, u)
149
    traceTally(u)
15 jtkorb 150
 
57 jtkorb 151
def gatherEffectiveBallots(winner, u):
44 jtkorb 152
    lEffective = []
57 jtkorb 153
    for ballot in u.tally[winner]:
44 jtkorb 154
        for candidateTo in ballot:
57 jtkorb 155
           if candidateTo not in u.winners and candidateTo not in u.losers:
44 jtkorb 156
                lEffective.append(ballot)
157
                break
158
    return lEffective
159
 
57 jtkorb 160
def redistributeBallot(ballot, u):
15 jtkorb 161
    for candidateTo in ballot:
57 jtkorb 162
        if candidateTo not in u.winners and candidateTo not in u.losers:
21 jtkorb 163
            trace("\tto %s: %s" % (candidateTo, ballot))
57 jtkorb 164
            if not u.tally.has_key(candidateTo): u.tally[candidateTo] = []
165
            u.tally[candidateTo].append(ballot)
21 jtkorb 166
            ballot = ""
167
            break
15 jtkorb 168
    if ballot:
21 jtkorb 169
        trace("\tineffective ballot dropped: %s" % ballot)
15 jtkorb 170
 
57 jtkorb 171
def findNextLoser(u):
15 jtkorb 172
    cMin = sys.maxint  # least number of votes for candidate loser
173
    lMin = []          # list of candidates with least votes
57 jtkorb 174
    for c in u.tally.keys():
175
        if c not in u.losers and c not in u.winners and len(u.tally[c]) <= cMin:
176
            if len(u.tally[c]) == cMin:
21 jtkorb 177
                lMin.append(c)
178
            else:
179
                lMin = [c]
57 jtkorb 180
                cMin = len(u.tally[c])
15 jtkorb 181
    if len(lMin) == 0:
182
        return None
57 jtkorb 183
    else: 
184
        return u.forkPendingLosers(lMin)
15 jtkorb 185
 
57 jtkorb 186
def redistributeLoser(loser, u):
187
    excess = len(u.tally[loser])
15 jtkorb 188
    if excess <= 0:
21 jtkorb 189
        trace("\tno ballots to redistribute")
15 jtkorb 190
    else:
21 jtkorb 191
        trace("\tredistributing %d ballot(s) from %s" % (excess, loser))
57 jtkorb 192
        while len(u.tally[loser]) > 0:
193
            ballot = u.tally[loser][0]
194
            u.tally[loser] = u.tally[loser][1:]
195
            redistributeBallot(ballot, u)
196
    traceTally(u)
15 jtkorb 197
    return
198
 
57 jtkorb 199
def traceTally(u):
15 jtkorb 200
    global fTrace
201
    if not fTrace: return
57 jtkorb 202
    trace("\nCURRENT ASSIGNMENT OF BALLOTS (%d needed to win)" % u.quota)
203
    for candidate in u.tally.keys():
15 jtkorb 204
        trace("\t%s:" % candidate)
57 jtkorb 205
        for ballot in u.tally[candidate]:
15 jtkorb 206
            trace("\t\t%s" % ballot)
207
    return
208
 
209
# The basic Single Transferable Vote algorithm with Hare quota
210
#
211
# while winners < nWinners:
212
#   if a candidate has more than quota votes:
213
#       redistribute excess votes to next priority candidate
214
#   else:
215
#       eliminate lowest ranking candidate
216
#       redistribute wasted votes to next priority candidate
217
#
218
def dotally(nWinners, ballots):
54 jtkorb 219
    nBallots=len(ballots)
220
    quota=int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
221
 
57 jtkorb 222
    u = Universe(1.0, nWinners, nBallots, quota)
54 jtkorb 223
 
224
    trace("INPUT SUMMARY AND RUN TRACE")
57 jtkorb 225
    trace("\t%d ballots" % u.nBallots)
226
    trace("\tChoosing %s winners" % u.nWinners)
15 jtkorb 227
    trace("\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %
57 jtkorb 228
          (u.nBallots, u.nWinners, u.quota))
15 jtkorb 229
 
45 jtkorb 230
    # Create initial tally and statistics
15 jtkorb 231
    #
232
    for ballot in ballots:
233
        candidate = ballot[0]
57 jtkorb 234
        if not u.tally.has_key(candidate):
235
            u.tally[candidate] = []
236
        u.tally[candidate].append(ballot)
45 jtkorb 237
 
238
    total1st = {}
239
    total2nd = {}
240
    totalMrk = {}
241
 
242
    for ballot in ballots:
243
        for c in ballot:
244
            total1st[c] = 0
245
            total2nd[c] = 0
246
            totalMrk[c] = 0
247
 
248
    for ballot in ballots:
249
        total1st[ballot[0]] += 1
250
        if len(ballot) > 1:
251
            total2nd[ballot[1]] += 1
252
        for c in ballot:
253
            totalMrk[c] += 1
254
 
255
    trace("\n\tBALLOT MARKS\t1ST\t2ND\tNONE")
256
    candidates = totalMrk.keys()
257
    candidates.sort()
258
    for c in candidates:
259
        trace("\t%-16s%3d\t%3d\t%4d" % (c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))
57 jtkorb 260
    traceTally(u)
15 jtkorb 261
 
61 jtkorb 262
    Universe.pendingUniverses.append(u)
54 jtkorb 263
 
61 jtkorb 264
    while len(Universe.pendingUniverses) > 0:
265
    	u = Universe.pendingUniverses.pop()
266
	trace("POPPED UNIVERSE %d (p = %f, %d pending):" % 
267
	    (u.id, u.p, len(Universe.pendingUniverses)))
268
 
269
	if u.pendingType == "redist":
270
	    trace("\tredistributing %d ballots" % len(u.pendingItem))
271
	    winner = u.pendingData
272
	    assert(winner in u.winners)
273
	    for ballot in u.pendingItem:
274
                u.tally[winner].remove(ballot)
275
                redistributeBallot(ballot, u)
276
        elif u.pendingType == "winner":
277
	    trace("\tfinishing pending winner %s" % u.pendingItem)
278
	    winner = u.pendingItem
279
	    assert(winner not in u.winners)
57 jtkorb 280
            u.winners.append(winner)
281
            trace("\nSELECTION #%d: %s" % (len(u.winners), winner))
282
            redistributeWinner(winner, u)
61 jtkorb 283
	elif u.pendingType == "loser":
284
	    trace("\tfinishing pending loser %s" % u.pendingItem)
285
	    loser = u.pendingItem
286
            u.losers.append(loser)
287
            trace("\nELIMINATED: %s" % loser)
288
            redistributeLoser(loser, u)
289
 
290
        u.pendingType = None
291
 
292
        while len(u.winners) < u.nWinners:
293
            winner = findNextWinner(u)
294
            if winner:
295
                u.winners.append(winner)
296
                trace("\nSELECTION #%d: %s" % (len(u.winners), winner))
297
                redistributeWinner(winner, u)
15 jtkorb 298
            else:
61 jtkorb 299
	        loser = findNextLoser(u)
300
                if loser:
301
                    u.losers.append(loser)
302
                    trace("\nELIMINATED: %s" % loser)
303
                    redistributeLoser(loser, u)
304
                else:
305
                    trace("Not enough chosen candidates to fill all positions")
306
                    break
307
 
308
	winners = str(u.winners)
309
	trace("RESULTS FOR UNIVERSE %d (p = %f): %s" % (u.id, u.p, winners))
310
	if winners not in Universe.results: Universe.results[winners] = 0.0
311
	Universe.results[winners] += u.p
312
 
62 jtkorb 313
    trace("\nUsed %d parallel universes (skipped %d with low probability)" %
314
	(Universe.id, Universe.dropped))
61 jtkorb 315
    lWinners = []
316
    for winner in Universe.results.keys():
317
	lWinners.append("%f: %s" % (Universe.results[winner], winner))
318
    lWinners.sort()
319
    lWinners.reverse()
320
    return lWinners