Subversion Repositories Remote Hare Voting

Rev

Rev 59 | Rev 61 | 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 60 2007-09-22 13:58:59Z jtkorb $
15 jtkorb 25
 
26
import sys
27
import math 
28
import random
29
 
45 jtkorb 30
fdOut = None
15 jtkorb 31
fTrace = 1
39 jtkorb 32
randcalls = 0
15 jtkorb 33
 
53 jtkorb 34
class Universe:
57 jtkorb 35
    id = 0                              # id of last created Universe
36
    pendingUniverses = []
37
    completedUniverses = []
53 jtkorb 38
 
54 jtkorb 39
    def __init__(self, p, nWinners, nBallots, quota):
40
        Universe.id += 1
41
        self.id = Universe.id
42
        self.p = p
43
        self.nBallots = nBallots
44
	self.nWinners = nWinners
45
        self.quota = quota
46
        self.winners = []
47
        self.losers = []
48
        self.tally = {}
49
 
57 jtkorb 50
    def forkPendingWinners(self, lWin):
51
	return self.__forkPendingList(lWin, "winner")
53 jtkorb 52
 
57 jtkorb 53
    def forkPendingLosers(self, lMin):
54
	return self.__forkPendingList(lMin, "loser")
55
 
56
    def forkPendingRedist(self, lRedist):
57
	return self.__forkPendingList(lRedist, "redist")
58
 
59
    def __forkPendingList(self, lItems, pendingType):
60
	self.p /= len(lItems)
61
	for item in lItems[1:]:
62
	    u = Universe(self.p, self.nWinners, self.nBallots, self.quota)
63
	    u.winners = self.winners[:]
64
	    u.losers = self.losers[:]
65
	    u.tally = self.tally.copy()
66
	    u.pending = item
67
	    u.pendingType = pendingType
68
	    Universe.pendingUniverses.append(u)
69
	return lItems[0]
70
 
71
def combinations(l, n):
72
    assert(n >= 0)
73
    if n == 0:
74
	yield []
75
    else:
76
	for i in range(len(l)):
77
	    for lsub in combinations(l[i+1:], n-1):
78
		yield [l[i]] + lsub
79
 
15 jtkorb 80
def trace(s):
45 jtkorb 81
    global fTrace, fdOut
82
    if fTrace: print >>fdOut, s
15 jtkorb 83
    return
84
 
57 jtkorb 85
def findNextWinner(u):
39 jtkorb 86
    global randcalls
57 jtkorb 87
    cWin = u.quota	# number of votes for highest winner
88
    lWin = []		# list of candidates with highest votes
89
    for c in u.tally.keys():
90
        if c not in u.losers and c not in u.winners and len(u.tally[c]) >= cWin:
91
            if len(u.tally[c]) == cWin:
39 jtkorb 92
                lWin.append(c)
93
            else:
94
                lWin = [c]
57 jtkorb 95
                cWin = len(u.tally[c])
96
    if len(lWin) > 0:
97
        return u.forkPendingWinners(lWin)
15 jtkorb 98
 
99
    # Check to see if only enough candidates remain
40 jtkorb 100
    ## TODO: sort by len(tally[c]) to choose larger winners first
101
    ## randomize and count if some with equal votes
15 jtkorb 102
    n = 0
103
    last = ""
57 jtkorb 104
    for c in u.tally.keys():
105
        if c not in u.winners and c not in u.losers:
40 jtkorb 106
            last = c
15 jtkorb 107
            n = n + 1
57 jtkorb 108
    if u.nWinners - len(u.winners) >= n:
40 jtkorb 109
        trace("\tremaining winner(s) have fewer than quota (%d) ballots" %
57 jtkorb 110
            u.quota)
15 jtkorb 111
        return last
112
    return
113
 
57 jtkorb 114
def redistributeWinner(winner, u):
39 jtkorb 115
    global randcalls
57 jtkorb 116
    excess = len(u.tally[winner]) - u.quota
15 jtkorb 117
    if excess <= 0:
21 jtkorb 118
        trace("\tno excess ballots to redistribute")
15 jtkorb 119
    else:
57 jtkorb 120
        lEffective = gatherEffectiveBallots(winner, u)
44 jtkorb 121
        nRedistribute = min(excess, len(lEffective))
122
        trace("\tredistributing %d effective of %d excess ballot(s) at random from %s" %
123
              (nRedistribute, excess, winner))
57 jtkorb 124
 
125
## LEFT OFF HERE...
126
## 	Need to create (len(lEffective) choose nRedistribute)-1 universes.
127
##  	One solution is to set self.nRedistribute, then forkPendingRedist the lEffective ballots.
128
## 	Must wind up with nRedistribute ballots redistributed in current universe.
129
## 	u.forkPendingRedist(lEffective)
130
 
44 jtkorb 131
        for ballot in random.sample(lEffective, nRedistribute):
39 jtkorb 132
            randcalls += 1
44 jtkorb 133
            trace("\trandom choice = %s" % ballot)
57 jtkorb 134
            u.tally[winner].remove(ballot)
135
            redistributeBallot(ballot, u)
44 jtkorb 136
            nRedistribute -= 1
57 jtkorb 137
    traceTally(u)
15 jtkorb 138
 
57 jtkorb 139
def gatherEffectiveBallots(winner, u):
44 jtkorb 140
    lEffective = []
57 jtkorb 141
    for ballot in u.tally[winner]:
44 jtkorb 142
        for candidateTo in ballot:
57 jtkorb 143
           if candidateTo not in u.winners and candidateTo not in u.losers:
44 jtkorb 144
                lEffective.append(ballot)
145
                break
146
    return lEffective
147
 
57 jtkorb 148
def redistributeBallot(ballot, u):
15 jtkorb 149
    for candidateTo in ballot:
57 jtkorb 150
        if candidateTo not in u.winners and candidateTo not in u.losers:
21 jtkorb 151
            trace("\tto %s: %s" % (candidateTo, ballot))
57 jtkorb 152
            if not u.tally.has_key(candidateTo): u.tally[candidateTo] = []
153
            u.tally[candidateTo].append(ballot)
21 jtkorb 154
            ballot = ""
155
            break
15 jtkorb 156
    if ballot:
21 jtkorb 157
        trace("\tineffective ballot dropped: %s" % ballot)
15 jtkorb 158
 
57 jtkorb 159
def findNextLoser(u):
39 jtkorb 160
    global randcalls
15 jtkorb 161
    cMin = sys.maxint  # least number of votes for candidate loser
162
    lMin = []          # list of candidates with least votes
57 jtkorb 163
    for c in u.tally.keys():
164
        if c not in u.losers and c not in u.winners and len(u.tally[c]) <= cMin:
165
            if len(u.tally[c]) == cMin:
21 jtkorb 166
                lMin.append(c)
167
            else:
168
                lMin = [c]
57 jtkorb 169
                cMin = len(u.tally[c])
15 jtkorb 170
    if len(lMin) == 0:
171
        return None
57 jtkorb 172
    else: 
173
        return u.forkPendingLosers(lMin)
15 jtkorb 174
 
57 jtkorb 175
def redistributeLoser(loser, u):
176
    excess = len(u.tally[loser])
15 jtkorb 177
    if excess <= 0:
21 jtkorb 178
        trace("\tno ballots to redistribute")
15 jtkorb 179
    else:
21 jtkorb 180
        trace("\tredistributing %d ballot(s) from %s" % (excess, loser))
57 jtkorb 181
        while len(u.tally[loser]) > 0:
182
            ballot = u.tally[loser][0]
183
            u.tally[loser] = u.tally[loser][1:]
184
            redistributeBallot(ballot, u)
185
    traceTally(u)
15 jtkorb 186
    return
187
 
57 jtkorb 188
def traceTally(u):
15 jtkorb 189
    global fTrace
190
    if not fTrace: return
57 jtkorb 191
    trace("\nCURRENT ASSIGNMENT OF BALLOTS (%d needed to win)" % u.quota)
192
    for candidate in u.tally.keys():
15 jtkorb 193
        trace("\t%s:" % candidate)
57 jtkorb 194
        for ballot in u.tally[candidate]:
15 jtkorb 195
            trace("\t\t%s" % ballot)
196
    return
197
 
198
# The basic Single Transferable Vote algorithm with Hare quota
199
#
200
# while winners < nWinners:
201
#   if a candidate has more than quota votes:
202
#       redistribute excess votes to next priority candidate
203
#   else:
204
#       eliminate lowest ranking candidate
205
#       redistribute wasted votes to next priority candidate
206
#
207
def dotally(nWinners, ballots):
39 jtkorb 208
    global randcalls
46 jtkorb 209
    randcalls = 0
15 jtkorb 210
 
54 jtkorb 211
    nBallots=len(ballots)
212
    quota=int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
213
 
57 jtkorb 214
    u = Universe(1.0, nWinners, nBallots, quota)
54 jtkorb 215
 
216
    trace("INPUT SUMMARY AND RUN TRACE")
57 jtkorb 217
    trace("\t%d ballots" % u.nBallots)
218
    trace("\tChoosing %s winners" % u.nWinners)
15 jtkorb 219
    trace("\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %
57 jtkorb 220
          (u.nBallots, u.nWinners, u.quota))
15 jtkorb 221
 
45 jtkorb 222
    # Create initial tally and statistics
15 jtkorb 223
    #
224
    for ballot in ballots:
225
        candidate = ballot[0]
57 jtkorb 226
        if not u.tally.has_key(candidate):
227
            u.tally[candidate] = []
228
        u.tally[candidate].append(ballot)
45 jtkorb 229
 
230
    total1st = {}
231
    total2nd = {}
232
    totalMrk = {}
233
 
234
    for ballot in ballots:
235
        for c in ballot:
236
            total1st[c] = 0
237
            total2nd[c] = 0
238
            totalMrk[c] = 0
239
 
240
    for ballot in ballots:
241
        total1st[ballot[0]] += 1
242
        if len(ballot) > 1:
243
            total2nd[ballot[1]] += 1
244
        for c in ballot:
245
            totalMrk[c] += 1
246
 
247
    trace("\n\tBALLOT MARKS\t1ST\t2ND\tNONE")
248
    candidates = totalMrk.keys()
249
    candidates.sort()
250
    for c in candidates:
251
        trace("\t%-16s%3d\t%3d\t%4d" % (c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))
57 jtkorb 252
    traceTally(u)
15 jtkorb 253
 
57 jtkorb 254
    continueTally(u)
255
    return u.winners
54 jtkorb 256
 
57 jtkorb 257
def continueTally(u):
258
    while len(u.winners) < u.nWinners:
259
        winner = findNextWinner(u)
15 jtkorb 260
        if winner:
57 jtkorb 261
            u.winners.append(winner)
262
            trace("\nSELECTION #%d: %s" % (len(u.winners), winner))
263
            redistributeWinner(winner, u)
15 jtkorb 264
        else:
57 jtkorb 265
            loser = findNextLoser(u)
15 jtkorb 266
            if loser:
57 jtkorb 267
                u.losers.append(loser)
15 jtkorb 268
                trace("\nELIMINATED: %s" % loser)
57 jtkorb 269
                redistributeLoser(loser, u)
15 jtkorb 270
            else:
271
                trace("Not enough chosen candidates to fill all positions")
272
                break
39 jtkorb 273
    trace("\nNumber of random choices made: %d" % randcalls)