#!/usr/bin/env python """ LICENSE Attribution Non-Commercial Share Alike cc by-nc-sa http://creativecommons.org/licenses/by-nc-sa/3.0 Attribution: Scott Hendrickson http://drskippy.net """ import sys POS_IDX = 0 INF_IDX = 1 SAL_IDX = 2 SI_IDX = 3 SIP_IDX = 4 class MultiPlayerGame: """ Calculate weighted average as a prediction of the final position of a multi-player game. Also calculates the Balance of Power prediction of the final position of the same game. Calculate is based on initial Position, Influence and Salience for each party. Positions must be along a single dimension. Influence will be normalized to give only 100% Influence for any game. Salience is a proxy for perceived payoff (by each party) for achieving an outcome at their Position. """ def __init__(self, d, maxPos = None): """ Input data (d) is a map of {party: [Pos, Infl, Sal],...} """ self.maxPos = maxPos self.data = {} self.balanceOfPower = None self.weightedAverage = None self.normalizeInfluence(d) self.createPartyByPositionMap() self.getAveragePosition() def createPartyByPositionMap(self): """ Map of parties accessible by position. """ self.partyByPositionMap = {} for party in self.data: pos = self.data[party][POS_IDX] if pos in self.partyByPositionMap: self.partyByPositionMap[pos].append(party) else: self.partyByPositionMap[pos] = [party] def normalizeInfluence(self, data): """ Normalize influence to total 100%. Option to normalize positions to scale of 100 (set maxPos in constructor). """ inf = 0. maxPos = -sys.maxint for i in data: inf += float(data[i][INF_IDX]) pos = float(data[i][POS_IDX]) if pos > maxPos: maxPos = pos if self.maxPos is None: # if no posMax passed to constructor, then normalize to 0-100 self.maxPos = maxPos self.scale = 100. else: # if posMax passed to constructor, then assume items are # already normalized to the provided scale maxPos = self.maxPos self.scale = self.maxPos for i in data: self.data[i] = data[i] self.data[i][INF_IDX] = data[i][INF_IDX] / inf self.data[i][POS_IDX] = data[i][POS_IDX] * self.scale / self.maxPos def getNormalizedData(self): return self.data def getParties(self): return self.data.keys() def getAveragePosition(self): """ Calculate the weighted average of the parties positions. The output will be on positions normalized from 0 - 100 and influence normalized to add up to 100%. Calculate the point where the influences of the parties in opposition is balanced. """ if self.balanceOfPower is None or self.weightedAverage is None: sums = [0,0,0,0,0] # [P, I, S, SI, SIP] self.cumlativePower = {} for party in self.data: partyPos = float(self.data[party][POS_IDX]) partyInf = float(self.data[party][INF_IDX]) sums[POS_IDX] += partyPos if partyPos in self.cumlativePower: self.cumlativePower[partyPos] += partyInf else: self.cumlativePower[partyPos] = partyInf sums[INF_IDX] += partyInf sums[SAL_IDX] += float(self.data[party][SAL_IDX]) tmp = float(self.data[party][SAL_IDX]) * self.data[party][INF_IDX] sums[SI_IDX] += tmp sums[SIP_IDX] += tmp * float(self.data[party][POS_IDX]) self.positions = self.cumlativePower.keys() self.positions.sort() power = 0.0 lastPos = 0.0 for pos in self.positions: if power + self.cumlativePower[pos] >= 0.5: # linearly interpolate accumulation of next influence value p = lastPos + (0.5 - power)*(pos - lastPos)/self.cumlativePower[pos] break else: power += self.cumlativePower[pos] lastPos = pos self.weightedAverage = sums[SIP_IDX]/sums[SI_IDX] self.balanceOfPower = p return self.weightedAverage, self.balanceOfPower def __str__(self): partyLen = 0 for party in self.getParties(): if len(party) > partyLen: partyLen = len(party) result = ''.join([ 'party', ' '*(partyLen - len('party')), '\t:\tPos\t\tInf(Norm)\tSal\n', '-'*60, '\n']) for pos in self.positions: for party in self.partyByPositionMap[pos]: result += ''.join([ party, ' '*(partyLen - len(party)), ("\t:\t%d\t\t%2.5f\t\t%d"%tuple(self.data[party])), "\n"]) result += '-'*60 + '\n' result += 'Position (weighted avg):\t%2.5f\nPosition (balance of power):\t%2.5f'%(self.getAveragePosition()) result += '\n' return result if __name__ == '__main__': # [P, I, S] data = { 'd2': [ 15, 20, 80], 'ctcust': [ 35, 50, 5], 'ctsal': [ 35, 20, 5], 'er': [ 50, 10, 20], 'eng': [ 60, 40, 99], 'd1': [ 75, 80, 99], 'me': [ 75, 80, 99], 'adv': [100, 80, 20], 'inv': [100, 80, 5], 'legal': [100, 70, 95] } p = MultiPlayerGame(data) print p