Subversion Repositories Remote Hare Voting

Rev

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