Subversion Repositories Remote Hare Voting

Rev

Rev 62 | Rev 64 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

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