Subversion Repositories Remote Hare Voting

Rev

Rev 14 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 14 Rev 15
Line 1... Line 1...
1
#!/p/python/python
1
#!/p/python/python
2
##
-
 
3
## Compute Hare Ballot
-
 
4
##
-
 
5
## Tim Korb (jtk@cs.purdue.edu), May 2007
-
 
6
##
2
 
7
## A few definitions and notes...
-
 
8
##
-
 
9
## tally: dictionary in which the keys are candidate names and the
-
 
10
## values are lists of ballots currently assigned to that candidate
-
 
11
##
-
 
12
## ballot: list of candidates in the order determined by the voter
-
 
13
##
-
 
14
## winners: list of candidates that have reached the quota of ballots
-
 
15
## or have remained in the running long enough to be declared a winner
-
 
16
##
-
 
17
## losers: list of candidates that have been eliminated from the running
-
 
18
##
-
 
19
## Note that plurals are generally used to indicate lists of other
-
 
20
## objects, e.g., ballots is a list of ballots.
-
 
21
##
-
 
22
import cgi
3
import cgi
23
import string
4
import string
24
import sys
5
import sys
25
import math 
-
 
26
import random
-
 
27
 
-
 
28
from revision import revision
-
 
29
 
6
 
-
 
7
import revision
30
formaction = string.split(sys.argv[0],"/")[-1]
8
import parseinput
-
 
9
import tally
31
 
10
 
32
fTrace = 1
-
 
33
fHTML = 1
11
fHTML = 1
-
 
12
formaction = string.split(sys.argv[0],"/")[-1]
34
 
13
 
35
def emitPreHTML(formaction):
14
def emitPreHTML(formaction):
36
    emitHTML("""Content-type: text/html\n
15
    emitHTML("""Content-type: text/html\n
37
<html>
16
<html>
38
<body>
17
<body>
Line 54... Line 33...
54
The source code is available <a href="%s?fetch=yes">here</a>.
33
The source code is available <a href="%s?fetch=yes">here</a>.
55
<p>
34
<p>
56
<small>%s</small>
35
<small>%s</small>
57
<hr>
36
<hr>
58
<pre>
37
<pre>
59
""" % (formaction, formaction, revision))
38
""" % (formaction, formaction, revision.revision))
60
    return
39
    return
61
 
40
 
62
def emitPostHTML():
41
def emitPostHTML():
63
    emitHTML("""</pre>
42
    emitHTML("""</pre>
64
</body>
43
</body>
Line 69... Line 48...
69
def emitHTML(s):
48
def emitHTML(s):
70
    global fHTML
49
    global fHTML
71
    if fHTML: print s
50
    if fHTML: print s
72
    return
51
    return
73
 
52
 
74
def trace(s):
-
 
75
    global fTrace
-
 
76
    if fTrace: print s
-
 
77
    return
-
 
78
 
-
 
79
def findWinner(winners, losers, tally, quota, nWinners):
-
 
80
    for candidate in tally.keys():
-
 
81
        if candidate not in winners and len(tally[candidate]) >= quota:
-
 
82
            return candidate
-
 
83
 
-
 
84
    # Check to see if only enough candidates remain
-
 
85
    n = 0
-
 
86
    last = ""
-
 
87
    for candidate in tally.keys():
-
 
88
        if candidate not in winners and candidate not in losers:
-
 
89
            last = candidate
-
 
90
            n = n + 1
-
 
91
    if nWinners - len(winners) >= n:
-
 
92
        return last
-
 
93
    return
-
 
94
 
-
 
95
def redistributeWinner(winner, winners, losers, tally, quota):
-
 
96
    excess = len(tally[winner]) - quota
-
 
97
    if excess <= 0:
-
 
98
	trace("\tno excess ballots to redistribute")
-
 
99
    else:
-
 
100
	trace("\tredistributing %d excess ballot(s) at random from %s" %
-
 
101
	      (excess, winner))
-
 
102
	while len(tally[winner]) > quota:
-
 
103
	    i = int(random.uniform(0, len(tally[winner])))
-
 
104
	    trace("\trandom choice = ballot %d" % (i+1))
-
 
105
	    ballot = tally[winner][i]
-
 
106
	    tally[winner] = tally[winner][0:i] + tally[winner][i+1:]
-
 
107
	    redistributeBallot(ballot, winner, winners, losers, tally)
-
 
108
    traceTally(quota, tally)
-
 
109
 
-
 
110
def redistributeBallot(ballot, candidateFrom, winners, losers, tally):
-
 
111
    for candidateTo in ballot:
-
 
112
	if candidateTo not in winners and candidateTo not in losers:
-
 
113
	    trace("\tto %s: %s" % (candidateTo, ballot))
-
 
114
	    if not tally.has_key(candidateTo): tally[candidateTo] = []
-
 
115
	    tally[candidateTo].append(ballot)
-
 
116
	    ballot = ""
-
 
117
	    break
-
 
118
    if ballot:
-
 
119
	trace("\tineffective ballot dropped: %s" % ballot)
-
 
120
 
-
 
121
def findLoser(losers, winners, tally):
-
 
122
    cMin = sys.maxint  # least number of votes for candidate loser
-
 
123
    lMin = []          # list of candidates with least votes
-
 
124
    for c in tally.keys():
-
 
125
        if c not in losers and c not in winners and len(tally[c]) <= cMin:
-
 
126
	    if len(tally[c]) == cMin:
-
 
127
		lMin.append(c)
-
 
128
	    else:
-
 
129
		lMin = [c]
-
 
130
		cMin = len(tally[c])
-
 
131
    trace("\nELIMINATING LOW CANDIDATE RANDOMLY FROM %s" % lMin)
-
 
132
    if len(lMin) == 0:
-
 
133
        return None
-
 
134
    else:
-
 
135
        return random.choice(lMin)
-
 
136
 
-
 
137
def redistributeLoser(loser, losers, winners, tally, quota):
-
 
138
    excess = len(tally[loser])
-
 
139
    if excess <= 0:
-
 
140
	trace("\tno ballots to redistribute")
-
 
141
    else:
-
 
142
	trace("\tredistributing %d ballot(s) from %s" % (excess, loser))
-
 
143
	while len(tally[loser]) > 0:
-
 
144
	    ballot = tally[loser][0]
-
 
145
	    tally[loser] = tally[loser][1:]
-
 
146
	    redistributeBallot(ballot, loser, winners, losers, tally)
-
 
147
    traceTally(quota, tally)
-
 
148
    return
-
 
149
 
-
 
150
def traceTally(quota, tally):
-
 
151
    global fTrace
-
 
152
    if not fTrace: return
-
 
153
    trace("\nCURRENT ASSIGNMENT OF BALLOTS (%d needed to win)" % quota)
-
 
154
    for candidate in tally.keys():
-
 
155
        trace("\t%s:" % candidate)
-
 
156
        for ballot in tally[candidate]:
-
 
157
            trace("\t\t%s" % ballot)
-
 
158
    return
-
 
159
 
-
 
160
# The basic Single Transferable Vote algorithm with Hare quota
-
 
161
#
-
 
162
# while winners < nWinners:
-
 
163
#   if a candidate has more than quota votes:
-
 
164
#       redistribute excess votes to next priority candidate
-
 
165
#   else:
-
 
166
#       eliminate lowest ranking candidate
-
 
167
#       redistribute wasted votes to next priority candidate
-
 
168
#
-
 
169
def dotally(nWinners, ballots):
-
 
170
    nBallots = len(ballots)
-
 
171
    quota = int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
-
 
172
 
-
 
173
    trace("INPUT SUMMARY")
-
 
174
    trace("\t%d ballots" % nBallots)
-
 
175
    trace("\tChoosing %s winners" % nWinners)
-
 
176
    trace("\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %
-
 
177
	  (nBallots, nWinners, quota))
-
 
178
 
-
 
179
    # Create initial tally
-
 
180
    #
-
 
181
    tally = {}
-
 
182
    for ballot in ballots:
-
 
183
        candidate = ballot[0]
-
 
184
        if not tally.has_key(candidate):
-
 
185
            tally[candidate] = []
-
 
186
        tally[candidate].append(ballot)
-
 
187
    traceTally(quota, tally)
-
 
188
    
-
 
189
    winners = []
-
 
190
    losers = []
-
 
191
 
-
 
192
    while len(winners) < nWinners:
-
 
193
        winner = findWinner(winners, losers, tally, quota, nWinners)
-
 
194
        if winner:
-
 
195
            winners.append(winner)
-
 
196
            trace("\nSELECTION #%d: %s" % (len(winners), winner))
-
 
197
            redistributeWinner(winner, winners, losers, tally, quota)
-
 
198
        else:
-
 
199
            loser = findLoser(losers, winners, tally)
-
 
200
            if loser:
-
 
201
                losers.append(loser)
-
 
202
                trace("\nELIMINATED: %s" % loser)
-
 
203
                redistributeLoser(loser, losers, winners, tally, quota)
-
 
204
            else:
-
 
205
                trace("Not enough chosen candidates to fill all positions")
-
 
206
                break
-
 
207
 
-
 
208
    return winners
-
 
209
 
-
 
210
# Parse input file
-
 
211
#
-
 
212
# input: lines of comma-separated values
-
 
213
#	nWinners, ...ignored fields...
-
 
214
#	candidate1, ballot1rank, ballot2rank, ...
-
 
215
#	candidate2, ballot1rank, ballot2rank, ...
-
 
216
#
-
 
217
# output: list of lists of ballots
-
 
218
#	[[1stcandbal1, 2ndcandbal1, ...], [1stcandbal2, 2ndcandbal2, ...], ...]
-
 
219
 
-
 
220
def parsecontents(contents):
-
 
221
    lines = string.split(contents, "\r\n")
-
 
222
    fields = string.split(lines[0], ",")
-
 
223
    nWinners = int(fields[0])
-
 
224
 
-
 
225
    votes = []
-
 
226
    for line in lines[1:]:
-
 
227
        fields = string.split(line, ",")
-
 
228
        candidate = fields[0]
-
 
229
	positions = fields[1:]
-
 
230
        ballotnumber = 0
-
 
231
	for position in positions:
-
 
232
            if position:
-
 
233
                votes.append([ballotnumber, int(position), candidate])
-
 
234
            ballotnumber = ballotnumber + 1
-
 
235
 
-
 
236
    votes.sort()
-
 
237
 
-
 
238
    ballots = []
-
 
239
    ballotnumber = ""
-
 
240
    ballot = ""
-
 
241
    for triple in votes:
-
 
242
        if ballotnumber == triple[0]:
-
 
243
	    ballot.append(triple[2])
-
 
244
        else:
-
 
245
            if ballot:
-
 
246
                ballots.append(ballot)
-
 
247
            ballot = [triple[2]]
-
 
248
            ballotnumber = triple[0]
-
 
249
 
-
 
250
    ballots.append(ballot)
-
 
251
    return nWinners, ballots
-
 
252
 
-
 
253
def main():
53
def main():
254
    global fTrace
-
 
255
    emitPreHTML(formaction)
54
    emitPreHTML(formaction)
256
 
55
 
257
    form = cgi.FieldStorage()
56
    form = cgi.FieldStorage()
258
 
57
 
259
    if not form:
58
    if not form:
Line 267... Line 66...
267
        emitHTML("Error: file name missing")
66
        emitHTML("Error: file name missing")
268
    else:
67
    else:
269
        name = form["name"].filename
68
        name = form["name"].filename
270
        contents = form["name"].value
69
        contents = form["name"].value
271
 
70
 
272
	trace("INPUT FILE")
71
	tally.trace("INPUT FILE")
273
	trace(contents)
72
	tally.trace(contents)
274
 
73
 
275
        nWinners, ballots = parsecontents(contents)
74
        nWinners, ballots = parseinput.parsestring(contents)
276
	if form.has_key("run1"):
75
	if form.has_key("run1"):
277
	    results = dotally(nWinners, ballots)
76
	    results = tally.dotally(nWinners, ballots)
278
	    print "\nFINAL RESULTS"
77
	    print "\nFINAL RESULTS"
279
	    for winner in results:
78
	    for winner in results:
280
		print "\t%s" % winner
79
		print "\t%s" % winner
281
	elif form.has_key("run1000"):
80
	elif form.has_key("run1000"):
282
	    print "RUNNING 1000 TIMES"
81
	    print "RUNNING 1000 TIMES"
283
	    fTrace = 0
82
	    tally.fTrace = 0
284
	    summary = {}
83
	    summary = {}
285
	    for i in range(1000):
84
	    for i in range(1000):
286
		results = str(dotally(nWinners, ballots))
85
		results = str(tally.dotally(nWinners, ballots))
287
		# print "Run %2d: %s" % (i+1, results)
86
		# print "Run %2d: %s" % (i+1, results)
288
		if not summary.has_key(results):
87
		if not summary.has_key(results):
289
		    summary[results] = 0
88
		    summary[results] = 0
290
		summary[results] = summary[results] + 1
89
		summary[results] = summary[results] + 1
291
	    print "\nSUMMARY"
90
	    print "\nSUMMARY"
Line 297... Line 96...
297
	    print "UNEXPECTED SUBMIT BUTTON: %s" % form
96
	    print "UNEXPECTED SUBMIT BUTTON: %s" % form
298
		
97
		
299
    emitPostHTML()
98
    emitPostHTML()
300
    return
99
    return
301
 
100
 
-
 
101
if __name__=='__main__':
302
main()
102
    main()