Rev 46 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
# Compute Hare Ballot## Tim Korb (jtk@cs.purdue.edu), May 2007## A few definitions and notes...## tally: dictionary in which the keys are candidate names and the# values are lists of ballots currently assigned to that candidate## ballot: list of candidates in the order determined by the voter## winners: list of candidates that have reached the quota of ballots# or have remained in the running long enough to be declared a winner## losers: list of candidates that have been eliminated from the running## Note that plurals are generally used to indicate lists of other# items, e.g., ballots is a list of ballot items.## To download the complete source distribution, use Subversion:## % svn co http://www.bikmort.com/hare## $Id: tally.py 75 2009-01-08 23:29:13Z jtkorb $import sysimport mathimport copyimport randomfdOut = NonetraceLevel = 0 # generate trace output if trace >= trace parameterfParallel = 0 # track parallel universes instead of randomization if possibleclass Universe:id = 0 # id of last created UniversependingUniverses = []results = {}dropped = 0def __reset(self):Universe.id = 0Universe.pendingUniverses = []Universe.results = {}Universe.dropped = 0def __init__(self, p, nWinners, nBallots, quota):if p == 1.0:self.__reset()Universe.id += 1self.id = Universe.idself.p = pself.nWinners = nWinnersself.nBallots = nBallotsself.quota = quotaself.winners = []self.losers = []self.tally = {}self.pendingItem = Noneself.pendingType = Noneself.pendingData = Nonedef forkPendingWinners(self, lWin):return self.__forkPendingList(lWin, "winner")def forkPendingLosers(self, lMin):return self.__forkPendingList(lMin, "loser")def forkPendingRedist(self, lRedist, winner):return self.__forkPendingList(lRedist, "redist", winner)def __forkPendingList(self, lItems, type, data=None):global fParallelif len(lItems) == 1:return lItems[0]if not fParallel:trace(1, "\tmaking random choice among %d alternatives" %len(lItems))return random.choice(lItems)# Split into parallel universes unless too many with small probability#if Universe.id > 10000 and self.p/len(lItems) < 1.0/10000:trace(1, "\tpassing on %d universes (p = %f)" %(len(lItems)-1, self.p/len(lItems)))trace(1, "\tmaking random choice among %d alternatives" %len(lItems))Universe.dropped += len(lItems)-1return random.choice(lItems)self.p /= len(lItems)trace(1, "\tcreating %d universes (p = %f)" % (len(lItems)-1, self.p))for item in lItems[1:]:u = Universe(self.p, self.nWinners, self.nBallots, self.quota)u.winners = self.winners[:]u.losers = self.losers[:]u.tally = copy.deepcopy(self.tally)u.pendingItem = itemu.pendingType = typeu.pendingData = dataUniverse.pendingUniverses.append(u)return lItems[0]def combinations(l, n):if n == 0:return [[]]else:lResult = []for i in range(len(l)):for lsub in combinations(l[i+1:], n-1):lResult.append([l[i]] + lsub)return lResultdef trace(t, s):global traceLevel, fdOutif traceLevel >= t: print >>fdOut, sreturndef findNextWinner(u):cWin = u.quota # number of votes for highest winnerlWin = [] # list of candidates with highest votesfor c in u.tally.keys():if c not in u.losers and c not in u.winners and len(u.tally[c]) >= cWin:if len(u.tally[c]) == cWin:lWin.append(c)else:lWin = [c]cWin = len(u.tally[c])if len(lWin) > 0:return u.forkPendingWinners(lWin)# Check to see if only enough candidates remain# TODO: sort by len(tally[c]) to choose larger winners first# create multiple universes if some have equal votesn = 0last = ""for c in u.tally.keys():if c not in u.winners and c not in u.losers:last = cn = n + 1if u.nWinners - len(u.winners) >= n:trace(1, "\tremaining winner(s) have fewer than quota (%d) ballots" %u.quota)return lastreturndef redistributeWinner(winner, u):excess = len(u.tally[winner]) - u.quotaif excess <= 0:trace(1, "\tno excess ballots to redistribute")else:lEffective = gatherEffectiveBallots(winner, u)nRedist = min(excess, len(lEffective))trace(1, "\tredistributing %d effective ballots, %d at a time" %(len(lEffective), nRedist))for ballot in u.forkPendingRedist(combinations(lEffective, nRedist), winner):u.tally[winner].remove(ballot)redistributeBallot(ballot, u)traceTally(1, u)def gatherEffectiveBallots(winner, u):lEffective = []for ballot in u.tally[winner]:for candidateTo in ballot:if candidateTo not in u.winners and candidateTo not in u.losers:lEffective.append(ballot)breakreturn lEffectivedef redistributeBallot(ballot, u):for candidateTo in ballot:if candidateTo not in u.winners and candidateTo not in u.losers:trace(1, "\tto %s: %s" % (candidateTo, ballot))if not u.tally.has_key(candidateTo): u.tally[candidateTo] = []u.tally[candidateTo].append(ballot)ballot = ""breakif ballot:trace(1, "\tineffective ballot dropped: %s" % ballot)def findNextLoser(u):cMin = sys.maxint # least number of votes for candidate loserlMin = [] # list of candidates with least votesfor c in u.tally.keys():if c not in u.losers and c not in u.winners and len(u.tally[c]) <= cMin:if len(u.tally[c]) == cMin:lMin.append(c)else:lMin = [c]cMin = len(u.tally[c])if len(lMin) == 0:return Noneelse:return u.forkPendingLosers(lMin)def redistributeLoser(loser, u):excess = len(u.tally[loser])if excess <= 0:trace(1, "\tno ballots to redistribute")else:trace(1, "\tredistributing %d ballot(s) from %s" % (excess, loser))while len(u.tally[loser]) > 0:ballot = u.tally[loser][0]u.tally[loser] = u.tally[loser][1:]redistributeBallot(ballot, u)traceTally(1, u)returndef traceTally(t, u):global traceLevelif traceLevel < t: returntrace(t, "\nCURRENT ASSIGNMENT OF BALLOTS (%d needed to win)" % u.quota)for candidate in u.tally.keys():trace(t, "\t%s:" % candidate)for ballot in u.tally[candidate]:trace(t, "\t\t%s" % ballot)return# The basic Single Transferable Vote algorithm with Hare quota## while winners < nWinners:# if a candidate has more than quota votes:# redistribute excess votes to next priority candidate# else:# eliminate lowest ranking candidate# redistribute wasted votes to next priority candidate#def dotally(nWinners, ballots):global fParallelnBallots=len(ballots)quota=int(math.ceil((nBallots + 1.0)/(nWinners + 1)))u = Universe(1.0, nWinners, nBallots, quota)trace(0, "INPUT SUMMARY")trace(0, "\t%d ballots" % u.nBallots)trace(0, "\tChoosing %s winners" % u.nWinners)trace(0, "\tNeed ceil((%d + 1)/(%d + 1)) = %d ballots to win" %(u.nBallots, u.nWinners, u.quota))# Create initial tally and statistics#for ballot in ballots:candidate = ballot[0]if not u.tally.has_key(candidate):u.tally[candidate] = []u.tally[candidate].append(ballot)total1st = {}total2nd = {}totalMrk = {}for ballot in ballots:for c in ballot:total1st[c] = 0total2nd[c] = 0totalMrk[c] = 0for ballot in ballots:total1st[ballot[0]] += 1if len(ballot) > 1:total2nd[ballot[1]] += 1for c in ballot:totalMrk[c] += 1trace(0, "\n\tBALLOT MARKS\t1ST\t2ND\tNONE")candidates = totalMrk.keys()candidates.sort()for c in candidates:trace(0, "\t%-16s%3d\t%3d\t%4d" %(c, total1st[c], total2nd[c], len(ballots)-totalMrk[c]))traceTally(0, u)Universe.pendingUniverses.append(u)while len(Universe.pendingUniverses) > 0:u = Universe.pendingUniverses.pop()trace(1, "POPPED UNIVERSE %d (p = %f, %d pending):" %(u.id, u.p, len(Universe.pendingUniverses)))if u.pendingType == "redist":trace(1, "\tredistributing %d ballots" % len(u.pendingItem))winner = u.pendingDataassert(winner in u.winners)for ballot in u.pendingItem:u.tally[winner].remove(ballot)redistributeBallot(ballot, u)elif u.pendingType == "winner":trace(1, "\tfinishing pending winner %s" % u.pendingItem)winner = u.pendingItemassert(winner not in u.winners)u.winners.append(winner)trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))redistributeWinner(winner, u)elif u.pendingType == "loser":trace(1, "\tfinishing pending loser %s" % u.pendingItem)loser = u.pendingItemu.losers.append(loser)trace(1, "\nELIMINATED: %s" % loser)redistributeLoser(loser, u)u.pendingType = Nonewhile len(u.winners) < u.nWinners:winner = findNextWinner(u)if winner:u.winners.append(winner)trace(1, "\nSELECTION #%d: %s" % (len(u.winners), winner))redistributeWinner(winner, u)else:loser = findNextLoser(u)if loser:u.losers.append(loser)trace(1, "\nELIMINATED: %s" % loser)redistributeLoser(loser, u)else:trace(1, "Not enough chosen candidates to fill all positions")breakwinners = str(u.winners)if fParallel:trace(1, "RESULTS FOR UNIVERSE %d (p = %f): %s" % (u.id, u.p, winners))if winners not in Universe.results: Universe.results[winners] = 0.0Universe.results[winners] += u.pif fParallel:trace(0, "\nTracked %d alternative decisions (skipped %d with low probability)" %(Universe.id, Universe.dropped))trace(0, "\nFINAL RESULTS")lWinners = []for winner in Universe.results.keys():if fParallel:lWinners.append("%f: %s" % (Universe.results[winner], winner))else:lWinners.append("%s" % winner)lWinners.sort()lWinners.reverse()for winner in lWinners:trace(0, winner)return lWinners