Subversion Repositories Local Hare Voting

Rev

Rev 45 | Rev 75 | 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
##
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
46 jtkorb 163
    randcalls = 0
15 jtkorb 164
    nBallots = len(ballots)
165
    quota = int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
166
 
45 jtkorb 167
    trace("INPUT SUMMARY AND SINGLE RUN TRACE")
15 jtkorb 168
    trace("\t%d ballots" % nBallots)
169
    trace("\tChoosing %s winners" % nWinners)
170
    trace("\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %
21 jtkorb 171
          (nBallots, nWinners, quota))
15 jtkorb 172
 
45 jtkorb 173
    # Create initial tally and statistics
15 jtkorb 174
    #
175
    tally = {}
176
    for ballot in ballots:
177
        candidate = ballot[0]
178
        if not tally.has_key(candidate):
179
            tally[candidate] = []
180
        tally[candidate].append(ballot)
45 jtkorb 181
 
182
    total1st = {}
183
    total2nd = {}
184
    totalMrk = {}
185
 
186
    for ballot in ballots:
187
        for c in ballot:
188
            total1st[c] = 0
189
            total2nd[c] = 0
190
            totalMrk[c] = 0
191
 
192
    for ballot in ballots:
193
        total1st[ballot[0]] += 1
194
        if len(ballot) > 1:
195
            total2nd[ballot[1]] += 1
196
        for c in ballot:
197
            totalMrk[c] += 1
198
 
199
    trace("\n\tBALLOT MARKS\t1ST\t2ND\tNONE")
200
    candidates = totalMrk.keys()
201
    candidates.sort()
202
    for c in candidates:
203
        trace("\t%-16s%3d\t%3d\t%4d" % (c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))
15 jtkorb 204
    traceTally(quota, tally)
205
 
206
    winners = []
207
    losers = []
208
 
209
    while len(winners) < nWinners:
210
        winner = findWinner(winners, losers, tally, quota, nWinners)
211
        if winner:
212
            winners.append(winner)
213
            trace("\nSELECTION #%d: %s" % (len(winners), winner))
214
            redistributeWinner(winner, winners, losers, tally, quota)
215
        else:
216
            loser = findLoser(losers, winners, tally)
217
            if loser:
218
                losers.append(loser)
219
                trace("\nELIMINATED: %s" % loser)
220
                redistributeLoser(loser, losers, winners, tally, quota)
221
            else:
222
                trace("Not enough chosen candidates to fill all positions")
223
                break
39 jtkorb 224
    trace("\nNumber of random choices made: %d" % randcalls)
15 jtkorb 225
 
226
    return winners
227