Subversion Repositories Remote Hare Voting

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
5 jtkorb 1
#!/p/python/python
2
##
3
## Compute Hare Ballot
4
##
5
## Tim Korb (jtk@cs.purdue.edu), May 2007
6
##
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
23
import string
24
import sys
25
import math 
26
import random
27
 
11 jtkorb 28
from revision import revision
7 jtkorb 29
 
5 jtkorb 30
formaction = string.split(sys.argv[0],"/")[-1]
31
 
32
fTrace = 1
33
fHTML = 1
34
 
35
def emitPreHTML(formaction):
36
    emitHTML("""Content-type: text/html\n
37
<html>
38
<body>
39
<title>Hare Ballot Calculation</title>
40
    <h1>Hare Ballot Calculation</h1>
41
    <form enctype="multipart/form-data" method=POST action=%s>
42
        <table>
43
        <tr><td>CSV file of ballots:
44
                <td><input type=file size=70 name=name>
45
        </table>
46
        <p>
47
        <input type=submit name="run1" value="Run 1 Time">
48
        <input type=submit name="run1000" value="Run 1000 Times">
49
    </form>
50
    <p>
51
Input file format is given
52
<a href="http://www.bikmort.com/wiki/index.php?title=Hare_Voting_Procedure">here</a>.
53
Almost no error checking is done, so be sure to review the trace output.
54
The source code is available <a href="%s?fetch=yes">here</a>.
55
<p>
56
<small>%s</small>
57
<hr>
58
<pre>
11 jtkorb 59
""" % (formaction, formaction, revision))
5 jtkorb 60
    return
61
 
62
def emitPostHTML():
63
    emitHTML("""</pre>
64
</body>
65
</html>
66
""")
67
    return
68
 
69
def emitHTML(s):
70
    global fHTML
71
    if fHTML: print s
72
    return
73
 
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():
254
    global fTrace
255
    emitPreHTML(formaction)
256
 
257
    form = cgi.FieldStorage()
258
 
259
    if not form:
260
        emitHTML("Enter file name and press a button to see results.")
261
    elif form.has_key("fetch"):
262
	source = open(formaction).read()
263
	print cgi.escape(source)
264
    elif not form.has_key("name"):
265
        emitHTML("Error: no file received")
266
    elif not form["name"].filename:
267
        emitHTML("Error: file name missing")
268
    else:
269
        name = form["name"].filename
270
        contents = form["name"].value
271
 
272
	trace("INPUT FILE")
273
	trace(contents)
274
 
275
        nWinners, ballots = parsecontents(contents)
276
	if form.has_key("run1"):
277
	    results = dotally(nWinners, ballots)
278
	    print "\nFINAL RESULTS"
279
	    for winner in results:
280
		print "\t%s" % winner
281
	elif form.has_key("run1000"):
282
	    print "RUNNING 1000 TIMES"
283
	    fTrace = 0
284
	    summary = {}
285
	    for i in range(1000):
286
		results = str(dotally(nWinners, ballots))
287
		# print "Run %2d: %s" % (i+1, results)
288
		if not summary.has_key(results):
289
		    summary[results] = 0
290
		summary[results] = summary[results] + 1
291
	    print "\nSUMMARY"
292
	    l = summary.items()
293
	    l.sort(lambda x,y: cmp(y[1], x[1]))
294
	    for pair in l:
295
		print "%2d %s" % (pair[1], pair[0])
296
	else:
297
	    print "UNEXPECTED SUBMIT BUTTON: %s" % form
298
 
299
    emitPostHTML()
300
    return
301
 
302
main()