Subversion Repositories Local Hare Voting

Rev

Rev 75 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
75 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
#
20
# To download the complete source distribution, use Subversion:
21
#
22
#      % svn co http://www.bikmort.com/hare
23
#
24
# $Id: tally.py 78 2016-05-11 19:18:03Z jtkorb $
15 jtkorb 25
 
26
import sys
27
import math 
75 jtkorb 28
import copy
15 jtkorb 29
import random
30
 
45 jtkorb 31
fdOut = None
15 jtkorb 32
 
75 jtkorb 33
traceLevel = 0	# generate trace output if trace >= trace parameter
34
fParallel = 0	# track parallel universes instead of randomization if possible
35
 
36
class Universe:
37
    id = 0                              # id of last created Universe
38
    pendingUniverses = []
39
    results = {}
40
    dropped = 0
41
 
42
    def __reset(self):
78 jtkorb 43
        Universe.id = 0
44
        Universe.pendingUniverses = []
45
        Universe.results = {}
46
        Universe.dropped = 0
75 jtkorb 47
 
78 jtkorb 48
 
75 jtkorb 49
    def __init__(self, p, nWinners, nBallots, quota):
78 jtkorb 50
        if p == 1.0:
51
            self.__reset()
75 jtkorb 52
        Universe.id += 1
53
        self.id = Universe.id
54
 
55
        self.p = p
78 jtkorb 56
        self.nWinners = nWinners
75 jtkorb 57
        self.nBallots = nBallots
58
        self.quota = quota
59
 
60
        self.winners = []
61
        self.losers = []
62
        self.tally = {}
63
 
78 jtkorb 64
        self.pendingItem = None
65
        self.pendingType = None
66
        self.pendingData = None
75 jtkorb 67
 
68
    def forkPendingWinners(self, lWin):
78 jtkorb 69
        return self.__forkPendingList(lWin, "winner")
75 jtkorb 70
 
71
    def forkPendingLosers(self, lMin):
78 jtkorb 72
        return self.__forkPendingList(lMin, "loser")
75 jtkorb 73
 
74
    def forkPendingRedist(self, lRedist, winner):
78 jtkorb 75
        return self.__forkPendingList(lRedist, "redist", winner)
75 jtkorb 76
 
77
    def __forkPendingList(self, lItems, type, data=None):
78 jtkorb 78
        global fParallel
75 jtkorb 79
 
80
        if len(lItems) == 1:
78 jtkorb 81
            return lItems[0]
82
        if not fParallel:
83
            trace(1, "\tmaking random choice among %d alternatives" %
84
                len(lItems))
85
            return random.choice(lItems)
75 jtkorb 86
 
78 jtkorb 87
        # Split into parallel universes unless too many with small probability
88
        #
89
        if Universe.id > 10000 and self.p/len(lItems) < 1.0/10000:
90
            trace(1, "\tpassing on %d universes (p = %f)" %
91
                (len(lItems)-1, self.p/len(lItems)))
92
            trace(1, "\tmaking random choice among %d alternatives" %
93
                len(lItems))
94
            Universe.dropped += len(lItems)-1
95
            return random.choice(lItems)
75 jtkorb 96
 
78 jtkorb 97
        self.p /= len(lItems)
98
        trace(1, "\tcreating %d universes (p = %f)" % (len(lItems)-1, self.p))
75 jtkorb 99
 
78 jtkorb 100
        for item in lItems[1:]:
101
            u = Universe(self.p, self.nWinners, self.nBallots, self.quota)
102
            u.winners = self.winners[:]
103
            u.losers = self.losers[:]
104
            u.tally = copy.deepcopy(self.tally)
105
            u.pendingItem = item
106
            u.pendingType = type
107
            u.pendingData = data
108
            Universe.pendingUniverses.append(u)
109
        return lItems[0]
75 jtkorb 110
 
111
def combinations(l, n):
112
    if n == 0:
78 jtkorb 113
        return [[]]
75 jtkorb 114
    else:
115
        lResult = []
78 jtkorb 116
        for i in range(len(l)):
117
            for lsub in combinations(l[i+1:], n-1):
118
                lResult.append([l[i]] + lsub)
75 jtkorb 119
        return lResult
120
 
121
def trace(t, s):
122
    global traceLevel, fdOut
123
    if traceLevel >= t: print >>fdOut, s
15 jtkorb 124
    return
125
 
75 jtkorb 126
def findNextWinner(u):
127
    cWin = u.quota	# number of votes for highest winner
128
    lWin = []		# list of candidates with highest votes
129
    for c in u.tally.keys():
130
        if c not in u.losers and c not in u.winners and len(u.tally[c]) >= cWin:
131
            if len(u.tally[c]) == cWin:
39 jtkorb 132
                lWin.append(c)
133
            else:
134
                lWin = [c]
75 jtkorb 135
                cWin = len(u.tally[c])
136
    if len(lWin) > 0:
137
        return u.forkPendingWinners(lWin)
15 jtkorb 138
 
139
    # Check to see if only enough candidates remain
75 jtkorb 140
    # TODO: sort by len(tally[c]) to choose larger winners first
141
    # create multiple universes if some have equal votes
15 jtkorb 142
    n = 0
143
    last = ""
75 jtkorb 144
    for c in u.tally.keys():
145
        if c not in u.winners and c not in u.losers:
40 jtkorb 146
            last = c
15 jtkorb 147
            n = n + 1
75 jtkorb 148
    if u.nWinners - len(u.winners) >= n:
149
        trace(1, "\tremaining winner(s) have fewer than quota (%d) ballots" %
150
            u.quota)
15 jtkorb 151
        return last
152
    return
153
 
75 jtkorb 154
def redistributeWinner(winner, u):
155
    excess = len(u.tally[winner]) - u.quota
15 jtkorb 156
    if excess <= 0:
75 jtkorb 157
        trace(1, "\tno excess ballots to redistribute")
15 jtkorb 158
    else:
75 jtkorb 159
        lEffective = gatherEffectiveBallots(winner, u)
160
        nRedist = min(excess, len(lEffective))
78 jtkorb 161
        trace(1, "\tredistributing %d effective ballots, %d at a time" %
162
            (len(lEffective), nRedist))
163
        for ballot in u.forkPendingRedist(combinations(lEffective, nRedist), winner):
75 jtkorb 164
            u.tally[winner].remove(ballot)
165
            redistributeBallot(ballot, u)
166
    traceTally(1, u)
15 jtkorb 167
 
75 jtkorb 168
def gatherEffectiveBallots(winner, u):
44 jtkorb 169
    lEffective = []
75 jtkorb 170
    for ballot in u.tally[winner]:
44 jtkorb 171
        for candidateTo in ballot:
75 jtkorb 172
           if candidateTo not in u.winners and candidateTo not in u.losers:
44 jtkorb 173
                lEffective.append(ballot)
174
                break
175
    return lEffective
176
 
75 jtkorb 177
def redistributeBallot(ballot, u):
15 jtkorb 178
    for candidateTo in ballot:
75 jtkorb 179
        if candidateTo not in u.winners and candidateTo not in u.losers:
180
            trace(1, "\tto %s: %s" % (candidateTo, ballot))
181
            if not u.tally.has_key(candidateTo): u.tally[candidateTo] = []
182
            u.tally[candidateTo].append(ballot)
21 jtkorb 183
            ballot = ""
184
            break
15 jtkorb 185
    if ballot:
75 jtkorb 186
        trace(1, "\tineffective ballot dropped: %s" % ballot)
15 jtkorb 187
 
75 jtkorb 188
def findNextLoser(u):
15 jtkorb 189
    cMin = sys.maxint  # least number of votes for candidate loser
190
    lMin = []          # list of candidates with least votes
75 jtkorb 191
    for c in u.tally.keys():
192
        if c not in u.losers and c not in u.winners and len(u.tally[c]) <= cMin:
193
            if len(u.tally[c]) == cMin:
21 jtkorb 194
                lMin.append(c)
195
            else:
196
                lMin = [c]
75 jtkorb 197
                cMin = len(u.tally[c])
15 jtkorb 198
    if len(lMin) == 0:
199
        return None
75 jtkorb 200
    else: 
201
        return u.forkPendingLosers(lMin)
15 jtkorb 202
 
75 jtkorb 203
def redistributeLoser(loser, u):
204
    excess = len(u.tally[loser])
15 jtkorb 205
    if excess <= 0:
75 jtkorb 206
        trace(1, "\tno ballots to redistribute")
15 jtkorb 207
    else:
75 jtkorb 208
        trace(1, "\tredistributing %d ballot(s) from %s" % (excess, loser))
209
        while len(u.tally[loser]) > 0:
210
            ballot = u.tally[loser][0]
211
            u.tally[loser] = u.tally[loser][1:]
212
            redistributeBallot(ballot, u)
213
    traceTally(1, u)
15 jtkorb 214
    return
215
 
75 jtkorb 216
def traceTally(t, u):
217
    global traceLevel
218
    if traceLevel < t: return
219
    trace(t, "\nCURRENT ASSIGNMENT OF BALLOTS (%d needed to win)" % u.quota)
220
    for candidate in u.tally.keys():
221
        trace(t, "\t%s:" % candidate)
222
        for ballot in u.tally[candidate]:
223
            trace(t, "\t\t%s" % ballot)
15 jtkorb 224
    return
225
 
226
# The basic Single Transferable Vote algorithm with Hare quota
227
#
228
# while winners < nWinners:
229
#   if a candidate has more than quota votes:
230
#       redistribute excess votes to next priority candidate
231
#   else:
232
#       eliminate lowest ranking candidate
233
#       redistribute wasted votes to next priority candidate
234
#
235
def dotally(nWinners, ballots):
75 jtkorb 236
    global fParallel
15 jtkorb 237
 
75 jtkorb 238
    nBallots=len(ballots)
239
    quota=int(math.ceil((nBallots + 1.0)/(nWinners + 1)))
15 jtkorb 240
 
75 jtkorb 241
    u = Universe(1.0, nWinners, nBallots, quota)
242
 
243
    trace(0, "INPUT SUMMARY")
244
    trace(0, "\t%d ballots" % u.nBallots)
245
    trace(0, "\tChoosing %s winners" % u.nWinners)
246
    trace(0, "\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %
247
          (u.nBallots, u.nWinners, u.quota))
248
 
45 jtkorb 249
    # Create initial tally and statistics
15 jtkorb 250
    #
251
    for ballot in ballots:
252
        candidate = ballot[0]
75 jtkorb 253
        if not u.tally.has_key(candidate):
254
            u.tally[candidate] = []
255
        u.tally[candidate].append(ballot)
45 jtkorb 256
 
257
    total1st = {}
258
    total2nd = {}
259
    totalMrk = {}
260
 
261
    for ballot in ballots:
262
        for c in ballot:
263
            total1st[c] = 0
264
            total2nd[c] = 0
265
            totalMrk[c] = 0
266
 
267
    for ballot in ballots:
268
        total1st[ballot[0]] += 1
269
        if len(ballot) > 1:
270
            total2nd[ballot[1]] += 1
271
        for c in ballot:
272
            totalMrk[c] += 1
273
 
75 jtkorb 274
    trace(0, "\n\tBALLOT MARKS\t1ST\t2ND\tNONE")
45 jtkorb 275
    candidates = totalMrk.keys()
276
    candidates.sort()
277
    for c in candidates:
75 jtkorb 278
        trace(0, "\t%-16s%3d\t%3d\t%4d" % 
78 jtkorb 279
            (c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))
75 jtkorb 280
    traceTally(0, u)
15 jtkorb 281
 
75 jtkorb 282
    Universe.pendingUniverses.append(u)
283
 
284
    while len(Universe.pendingUniverses) > 0:
78 jtkorb 285
        u = Universe.pendingUniverses.pop()
286
        trace(1, "POPPED UNIVERSE %d (p = %f, %d pending):" %
287
            (u.id, u.p, len(Universe.pendingUniverses)))
75 jtkorb 288
 
78 jtkorb 289
        if u.pendingType == "redist":
290
            trace(1, "\tredistributing %d ballots" % len(u.pendingItem))
291
            winner = u.pendingData
292
            assert(winner in u.winners)
293
            for ballot in u.pendingItem:
75 jtkorb 294
                u.tally[winner].remove(ballot)
295
                redistributeBallot(ballot, u)
296
        elif u.pendingType == "winner":
78 jtkorb 297
            trace(1, "\tfinishing pending winner %s" % u.pendingItem)
298
            winner = u.pendingItem
299
            assert(winner not in u.winners)
75 jtkorb 300
            u.winners.append(winner)
301
            trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))
302
            redistributeWinner(winner, u)
78 jtkorb 303
        elif u.pendingType == "loser":
304
            trace(1, "\tfinishing pending loser %s" % u.pendingItem)
305
            loser = u.pendingItem
75 jtkorb 306
            u.losers.append(loser)
307
            trace(1, "\nELIMINATED: %s" % loser)
308
            redistributeLoser(loser, u)
309
 
310
        u.pendingType = None
311
 
312
        while len(u.winners) < u.nWinners:
313
            winner = findNextWinner(u)
314
            if winner:
315
                u.winners.append(winner)
316
                trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))
317
                redistributeWinner(winner, u)
15 jtkorb 318
            else:
78 jtkorb 319
                loser = findNextLoser(u)
75 jtkorb 320
                if loser:
321
                    u.losers.append(loser)
322
                    trace(1, "\nELIMINATED: %s" % loser)
323
                    redistributeLoser(loser, u)
324
                else:
325
                    trace(1, "Not enough chosen candidates to fill all positions")
326
                    break
15 jtkorb 327
 
78 jtkorb 328
        winners = str(u.winners)
329
        if fParallel:
330
            trace(1, "RESULTS FOR UNIVERSE %d (p = %f): %s" % (u.id, u.p, winners))
331
        if winners not in Universe.results: Universe.results[winners] = 0.0
332
        Universe.results[winners] += u.p
15 jtkorb 333
 
75 jtkorb 334
    if fParallel:
78 jtkorb 335
        trace(0, "\nTracked %d alternative decisions (skipped %d with low probability)" %
336
            (Universe.id, Universe.dropped))
75 jtkorb 337
 
338
    trace(0, "\nFINAL RESULTS")
339
 
340
    lWinners = []
341
    for winner in Universe.results.keys():
78 jtkorb 342
        if fParallel:
343
            lWinners.append("%f: %s" % (Universe.results[winner], winner))
344
        else:
345
            lWinners.append("%s" % winner)
75 jtkorb 346
    lWinners.sort()
347
    lWinners.reverse()
348
 
349
    for winner in lWinners:
78 jtkorb 350
        trace(0, winner)
75 jtkorb 351
 
352
    return lWinners