| 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 |
##
|
|
|
22 |
## % svn co http://www.bikmort.com/hare
|
|
|
23 |
##
|
| 15 |
jtkorb |
24 |
|
|
|
25 |
import sys
|
|
|
26 |
import math
|
|
|
27 |
import random
|
|
|
28 |
|
| 45 |
jtkorb |
29 |
fdOut = None
|
| 15 |
jtkorb |
30 |
fTrace = 1
|
| 39 |
jtkorb |
31 |
randcalls = 0
|
| 15 |
jtkorb |
32 |
|
|
|
33 |
def trace(s):
|
| 45 |
jtkorb |
34 |
global fTrace, fdOut
|
|
|
35 |
if fTrace: print >>fdOut, s
|
| 15 |
jtkorb |
36 |
return
|
|
|
37 |
|
|
|
38 |
def findWinner(winners, losers, tally, quota, nWinners):
|
| 39 |
jtkorb |
39 |
global randcalls
|
|
|
40 |
cWin = quota # number of votes for highest winner
|
|
|
41 |
lWin = [] # list of candidates with highest votes
|
|
|
42 |
for c in tally.keys():
|
|
|
43 |
if c not in losers and c not in winners and len(tally[c]) >= cWin:
|
|
|
44 |
if len(tally[c]) == cWin:
|
|
|
45 |
lWin.append(c)
|
|
|
46 |
else:
|
|
|
47 |
lWin = [c]
|
|
|
48 |
cWin = len(tally[c])
|
|
|
49 |
if len(lWin) == 1:
|
|
|
50 |
return lWin[0]
|
|
|
51 |
elif len(lWin) > 1:
|
|
|
52 |
trace("\tselecting winning candidate randomly from %s" % lWin)
|
|
|
53 |
randcalls += 1
|
|
|
54 |
return random.choice(lWin)
|
| 15 |
jtkorb |
55 |
|
|
|
56 |
# Check to see if only enough candidates remain
|
| 40 |
jtkorb |
57 |
## TODO: sort by len(tally[c]) to choose larger winners first
|
|
|
58 |
## randomize and count if some with equal votes
|
| 15 |
jtkorb |
59 |
n = 0
|
|
|
60 |
last = ""
|
| 40 |
jtkorb |
61 |
for c in tally.keys():
|
|
|
62 |
if c not in winners and c not in losers:
|
|
|
63 |
last = c
|
| 15 |
jtkorb |
64 |
n = n + 1
|
|
|
65 |
if nWinners - len(winners) >= n:
|
| 40 |
jtkorb |
66 |
trace("\tremaining winner(s) have fewer than quota (%d) ballots" %
|
|
|
67 |
quota)
|
| 15 |
jtkorb |
68 |
return last
|
|
|
69 |
return
|
|
|
70 |
|
|
|
71 |
def redistributeWinner(winner, winners, losers, tally, quota):
|
| 39 |
jtkorb |
72 |
global randcalls
|
| 15 |
jtkorb |
73 |
excess = len(tally[winner]) - quota
|
|
|
74 |
if excess <= 0:
|
| 21 |
jtkorb |
75 |
trace("\tno excess ballots to redistribute")
|
| 15 |
jtkorb |
76 |
else:
|
| 44 |
jtkorb |
77 |
lEffective = gatherEffectiveBallots(winner, winners, losers, tally)
|
|
|
78 |
nRedistribute = min(excess, len(lEffective))
|
|
|
79 |
trace("\tredistributing %d effective of %d excess ballot(s) at random from %s" %
|
|
|
80 |
(nRedistribute, excess, winner))
|
|
|
81 |
for ballot in random.sample(lEffective, nRedistribute):
|
| 39 |
jtkorb |
82 |
randcalls += 1
|
| 44 |
jtkorb |
83 |
trace("\trandom choice = %s" % ballot)
|
|
|
84 |
tally[winner].remove(ballot)
|
|
|
85 |
redistributeBallot(ballot, winners, losers, tally)
|
|
|
86 |
nRedistribute -= 1
|
| 15 |
jtkorb |
87 |
traceTally(quota, tally)
|
|
|
88 |
|
| 44 |
jtkorb |
89 |
def gatherEffectiveBallots(winner, winners, losers, tally):
|
|
|
90 |
lEffective = []
|
|
|
91 |
for ballot in tally[winner]:
|
|
|
92 |
for candidateTo in ballot:
|
|
|
93 |
if candidateTo not in winners and candidateTo not in losers:
|
|
|
94 |
lEffective.append(ballot)
|
|
|
95 |
break
|
|
|
96 |
return lEffective
|
|
|
97 |
|
|
|
98 |
def redistributeBallot(ballot, winners, losers, tally):
|
| 15 |
jtkorb |
99 |
for candidateTo in ballot:
|
| 21 |
jtkorb |
100 |
if candidateTo not in winners and candidateTo not in losers:
|
|
|
101 |
trace("\tto %s: %s" % (candidateTo, ballot))
|
|
|
102 |
if not tally.has_key(candidateTo): tally[candidateTo] = []
|
|
|
103 |
tally[candidateTo].append(ballot)
|
|
|
104 |
ballot = ""
|
|
|
105 |
break
|
| 15 |
jtkorb |
106 |
if ballot:
|
| 21 |
jtkorb |
107 |
trace("\tineffective ballot dropped: %s" % ballot)
|
| 15 |
jtkorb |
108 |
|
|
|
109 |
def findLoser(losers, winners, tally):
|
| 39 |
jtkorb |
110 |
global randcalls
|
| 15 |
jtkorb |
111 |
cMin = sys.maxint # least number of votes for candidate loser
|
|
|
112 |
lMin = [] # list of candidates with least votes
|
|
|
113 |
for c in tally.keys():
|
|
|
114 |
if c not in losers and c not in winners and len(tally[c]) <= cMin:
|
| 21 |
jtkorb |
115 |
if len(tally[c]) == cMin:
|
|
|
116 |
lMin.append(c)
|
|
|
117 |
else:
|
|
|
118 |
lMin = [c]
|
|
|
119 |
cMin = len(tally[c])
|
| 15 |
jtkorb |
120 |
if len(lMin) == 0:
|
|
|
121 |
return None
|
| 39 |
jtkorb |
122 |
elif len(lMin) == 1:
|
|
|
123 |
return lMin[0]
|
| 15 |
jtkorb |
124 |
else:
|
| 39 |
jtkorb |
125 |
trace("\teliminating low candidate randomly from %s" % lMin)
|
|
|
126 |
randcalls += 1
|
| 15 |
jtkorb |
127 |
return random.choice(lMin)
|
|
|
128 |
|
|
|
129 |
def redistributeLoser(loser, losers, winners, tally, quota):
|
|
|
130 |
excess = len(tally[loser])
|
|
|
131 |
if excess <= 0:
|
| 21 |
jtkorb |
132 |
trace("\tno ballots to redistribute")
|
| 15 |
jtkorb |
133 |
else:
|
| 21 |
jtkorb |
134 |
trace("\tredistributing %d ballot(s) from %s" % (excess, loser))
|
|
|
135 |
while len(tally[loser]) > 0:
|
|
|
136 |
ballot = tally[loser][0]
|
|
|
137 |
tally[loser] = tally[loser][1:]
|
| 44 |
jtkorb |
138 |
redistributeBallot(ballot, winners, losers, tally)
|
| 15 |
jtkorb |
139 |
traceTally(quota, tally)
|
|
|
140 |
return
|
|
|
141 |
|
|
|
142 |
def traceTally(quota, tally):
|
|
|
143 |
global fTrace
|
|
|
144 |
if not fTrace: return
|
|
|
145 |
trace("\nCURRENT ASSIGNMENT OF BALLOTS (%d needed to win)" % quota)
|
|
|
146 |
for candidate in tally.keys():
|
|
|
147 |
trace("\t%s:" % candidate)
|
|
|
148 |
for ballot in tally[candidate]:
|
|
|
149 |
trace("\t\t%s" % ballot)
|
|
|
150 |
return
|
|
|
151 |
|
|
|
152 |
# The basic Single Transferable Vote algorithm with Hare quota
|
|
|
153 |
#
|
|
|
154 |
# while winners < nWinners:
|
|
|
155 |
# if a candidate has more than quota votes:
|
|
|
156 |
# redistribute excess votes to next priority candidate
|
|
|
157 |
# else:
|
|
|
158 |
# eliminate lowest ranking candidate
|
|
|
159 |
# redistribute wasted votes to next priority candidate
|
|
|
160 |
#
|
|
|
161 |
def dotally(nWinners, ballots):
|
| 39 |
jtkorb |
162 |
global randcalls
|
| 15 |
jtkorb |
163 |
nBallots = len(ballots)
|
|
|
164 |
quota = int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
|
|
|
165 |
|
| 45 |
jtkorb |
166 |
trace("INPUT SUMMARY AND SINGLE RUN TRACE")
|
| 15 |
jtkorb |
167 |
trace("\t%d ballots" % nBallots)
|
|
|
168 |
trace("\tChoosing %s winners" % nWinners)
|
|
|
169 |
trace("\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %
|
| 21 |
jtkorb |
170 |
(nBallots, nWinners, quota))
|
| 15 |
jtkorb |
171 |
|
| 45 |
jtkorb |
172 |
# Create initial tally and statistics
|
| 15 |
jtkorb |
173 |
#
|
|
|
174 |
tally = {}
|
|
|
175 |
for ballot in ballots:
|
|
|
176 |
candidate = ballot[0]
|
|
|
177 |
if not tally.has_key(candidate):
|
|
|
178 |
tally[candidate] = []
|
|
|
179 |
tally[candidate].append(ballot)
|
| 45 |
jtkorb |
180 |
|
|
|
181 |
total1st = {}
|
|
|
182 |
total2nd = {}
|
|
|
183 |
totalMrk = {}
|
|
|
184 |
|
|
|
185 |
for ballot in ballots:
|
|
|
186 |
for c in ballot:
|
|
|
187 |
total1st[c] = 0
|
|
|
188 |
total2nd[c] = 0
|
|
|
189 |
totalMrk[c] = 0
|
|
|
190 |
|
|
|
191 |
for ballot in ballots:
|
|
|
192 |
total1st[ballot[0]] += 1
|
|
|
193 |
if len(ballot) > 1:
|
|
|
194 |
total2nd[ballot[1]] += 1
|
|
|
195 |
for c in ballot:
|
|
|
196 |
totalMrk[c] += 1
|
|
|
197 |
|
|
|
198 |
trace("\n\tBALLOT MARKS\t1ST\t2ND\tNONE")
|
|
|
199 |
candidates = totalMrk.keys()
|
|
|
200 |
candidates.sort()
|
|
|
201 |
for c in candidates:
|
|
|
202 |
trace("\t%-16s%3d\t%3d\t%4d" % (c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))
|
| 15 |
jtkorb |
203 |
traceTally(quota, tally)
|
|
|
204 |
|
|
|
205 |
winners = []
|
|
|
206 |
losers = []
|
|
|
207 |
|
|
|
208 |
while len(winners) < nWinners:
|
|
|
209 |
winner = findWinner(winners, losers, tally, quota, nWinners)
|
|
|
210 |
if winner:
|
|
|
211 |
winners.append(winner)
|
|
|
212 |
trace("\nSELECTION #%d: %s" % (len(winners), winner))
|
|
|
213 |
redistributeWinner(winner, winners, losers, tally, quota)
|
|
|
214 |
else:
|
|
|
215 |
loser = findLoser(losers, winners, tally)
|
|
|
216 |
if loser:
|
|
|
217 |
losers.append(loser)
|
|
|
218 |
trace("\nELIMINATED: %s" % loser)
|
|
|
219 |
redistributeLoser(loser, losers, winners, tally, quota)
|
|
|
220 |
else:
|
|
|
221 |
trace("Not enough chosen candidates to fill all positions")
|
|
|
222 |
break
|
| 39 |
jtkorb |
223 |
trace("\nNumber of random choices made: %d" % randcalls)
|
| 15 |
jtkorb |
224 |
|
|
|
225 |
return winners
|
|
|
226 |
|