Subversion Repositories Remote Hare Voting

Rev

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