#!/usr/bin/env python3
"""
Python 3 implementation of voting digraphs
Refactored from revision 1.549 of the digraphs module
Copyright (C) 2011-2023 Raymond Bisdorff
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
#######################
__version__ = "$Revision: Python 3.10"
from digraphsTools import *
from digraphs import *
#--------- Decimal precision --------------
from decimal import Decimal
#---------- voting profiles
[docs]
class VotingProfile(object):
"""
A generic class for storing voting profiles.
General structure::
candidates = OrderedDict([('a', ...),('b', ...),('c', ...), ( ... ) ])
voters = OrderedDict([
('1', {'weight':1.0}),
('2', {'weight':1.0}),
...,
])
ballot = { # voters x candidates x candidates
'1': { # bipolar characteristic {-1,0,1} of each voter's
'a': { 'a':0,'b':-1,'c':0, ...}, # pairwise preferences
'b': { 'a':1,'b':0, 'c':1, ...},
'c': { 'a':0,'b':-1,'c':0, ...},
...,
},
'2': { 'a': { 'a':0, 'b':0, 'c':1, ...},
'b': { 'a':0, 'b':0, 'c':1, ...},
'c': { 'a':-1,'b':-1,'c':0, ...},
...,
},
...,
}
"""
def __repr__(self):
"""
Default description for VotingProfile instances.
"""
reprString = '*------- VotingProfile instance description ------*\n'
reprString += 'Instance class : %s\n' % self.__class__.__name__
reprString += 'Instance name : %s\n' % self.name
reprString += '# Candidates : %d\n' % len(self.candidates)
reprString += '# Voters : %d\n' % len(self.voters)
reprString += 'Attributes : %s\n' % list(self.__dict__.keys())
return reprString
def __init__(self,fileVotingProfile=None,seed=None):
if fileVotingProfile is not None:
fileName = fileVotingProfile+'.py'
argDict = {}
fi = open(fileName,'r')
exec(compile(fi.read(), fileName, 'exec'),argDict)
fi.close()
self.name = str(fileVotingProfile)
self.candidates = argDict['candidates']
self.voters = argDict['voters']
self.ballot = argDict['ballot']
else:
print('Error: a stored voting profile is required!')
return
[docs]
def showAll(self, WithBallots=True):
"""
Show method for <VotingProfile> instances.
"""
print('*------ VotingProfile instance: %s ------*' % self.name)
voters = [x for x in self.voters]
voters.sort()
print('Voters : %s' % str(voters))
candidates = [x for x in self.candidates]
candidates.sort()
print('Candidates : %s' % str(candidates))
if WithBallots:
print('Ballots')
for v in voters:
print('voting of voter %s (weight = %.1f)' \
% (v,self.voters[v]['weight']))
self.showVoterBallot(v)
print('----------------------')
[docs]
def showVoterBallot(self, voter, hasIntegerValuation=False):
"""
Show the actual voting of a voter.
"""
candidates = [x for x in self.candidates]
print(' > |', end=' ')
for x in candidates:
print("'"+x+"', ", end=' ')
print('\n-----|------------------------------------------------------------')
for x in candidates:
print("'"+x+"' | ", end=' ')
for y in candidates:
if hasIntegerValuation:
print('%d ' % (self.ballot[voter][x][y]), end=' ')
else:
print('%.2f ' % (self.ballot[voter][x][y]), end=' ')
print()
print('\n')
[docs]
def save(self,fileName='tempVotingProfile'):
"""
Persistant storage of an approval voting profile.
"""
print('*--- Saving voting profile in file: <' + str(fileName) + '.py> ---*')
candidates = self.candidates
voters = self.voters
ballot = self.ballot
saveFileName = str(fileName)+str('.py')
fo = open(saveFileName, 'w')
fo.write('# Saved voting profile: \n')
fo.write('from collections import OrderedDict \n')
fo.write('candidates = OrderedDict([\n')
for x in candidates:
try:
candidateName = candidates[x]['name']
except:
candidateName = x
fo.write('(\'%s\', {\'name\', \'%s\'}),\n' % (x,candidateName) )
fo.write('])\n')
fo.write('voters = OrderedDict([\n')
for v in voters:
fo.write('(\'' +str(v)+'\', {\n')
fo.write('\'weight\':'+str(voters[v]['weight'])+'}),\n')
fo.write('])\n')
fo.write('ballot = {\n')
for v in ballot:
fo.write('\'' +str(v)+'\': {\n')
for x in candidates:
fo.write('\'' + str(x) + '\':{' +'\n')
for y in candidates:
fo.write('\'' + str(y) + '\':' +str(ballot[v][x][y])+',\n')
fo.write('},\n')
fo.write('},\n')
fo.write( '}\n')
fo.close()
[docs]
def computePrerankedBallot(self,preranking,Debug=False):
"""
Renders the bipolar-valued ballot obtained from
given preorderings of the candidates per voter
"""
Max = Decimal('1')
Med = Decimal('0')
Min = Decimal('-1')
candidates = [c for c in self.candidates]
currentCandidates = set(candidates)
prerankedBallot = {}
for x in candidates:
prerankedBallot[x] = {}
for y in candidates:
prerankedBallot[x][y] = Med
for eqcl in preranking:
currRest = currentCandidates - set(eqcl)
if Debug:
print(currentCandidates, eqcl, currRest)
for x in eqcl:
for y in eqcl:
if x != y:
prerankedBallot[x][y] = Max
prerankedBallot[y][x] = Max
for x in eqcl:
for y in currRest:
prerankedBallot[x][y] = Max
prerankedBallot[y][x] = Min
currentCandidates = currentCandidates - set(eqcl)
return prerankedBallot
[docs]
class RandomVotingProfile(VotingProfile):
"""
A subclass for generating random voting profiles.
When *IntraGroup* is set to *True*, the candidates are the voters themselves
"""
def __init__(self,numberOfVoters=9,
votersIdPrefix='v',
IntraGroup=False,
numberOfCandidates=5,
candidatesIdPrefix='c',
hasRandomWeights=False,maxWeight=10,
seed=None,Debug=False):
"""
Random profile creation parameters:
| numberOfVoters=9,
| numberOfCandidates=5
"""
from collections import OrderedDict
import random
random.seed(seed)
self.seed = seed
votersList = [x for x in range(1,numberOfVoters + 1)]
voters = OrderedDict()
nd = len(str(numberOfVoters))
for v in votersList:
voterID = ('%s%%0%dd' % (votersIdPrefix,nd)) % (v)
if hasRandomWeights:
voters[voterID] = {'weight':random.randint(1,maxWeight)}
else:
voters[voterID] = {'weight':1}
if IntraGroup:
candidates = voters
else:
candidatesList = [x for x in range(1,numberOfCandidates + 1)]
candidates = OrderedDict()
na = len(str(numberOfCandidates))
for c in candidatesList:
candidateID =('%s%%0%dd' % (candidatesIdPrefix,na)) % (c)
candidates[candidateID] = {'name': candidateID}
self.IntraGroup = IntraGroup
self.name = str('randonVotingProfile')
self.candidates = candidates
self.voters = voters
self.ballot = self.generateRandomBallot(seed=seed,Debug=Debug)
if Debug:
self.showAll()
[docs]
def generateRandomBallot(self,seed,Debug=False):
"""
Renders a randomly generated approval ballot
from a list of candidates for each voter.
"""
from decimal import Decimal
import random
if seed is not None:
random.seed(seed)
ballot = {}
voters = [x for x in self.voters]
#candidatesList = [x for x in self.candidates]
#nc = len(candidatesList)
for v in voters:
ballot[v] = {}
candidatesList = [x for x in self.candidates]
nc = len(candidatesList)
for i in range(nc):
ballot[v][candidatesList[i]] = {}
for j in range(nc):
ballot[v][candidatesList[i]][candidatesList[j]] = Decimal('0.0')
if self.IntraGroup:
candidatesList.remove(v)
random.shuffle(candidatesList)
if Debug:
print(v, candidatesList)
for i in range(nc):
for j in range(i+1,nc):
ballot[v][candidatesList[i]][candidatesList[j]] = Decimal('1.0')
ballot[v][candidatesList[j]][candidatesList[i]] = Decimal('-1.0')
return ballot
#---------
[docs]
class PrerankedVotingProfile(VotingProfile):
"""
A specialised class for preranked voting profiles
Structure::
candidates = OrderedDict([('a', ...) ,('b', ...),('c', ...), ...])
voters = OrderedDict([('1',{'weight':1.0}), ('2',{'weight':1.0}), ...])
## each specifies a a ranked list of lists of candidates
## from the best to the worst
preorderBallot = {
'1' : [['b','c'],['a'], ...],
'2' : [['a'],['b','c'], ...],
...
}
"""
def __init__(self,fileVotingProfile=None):
if fileVotingProfile is not None:
fileName = fileVotingProfile+'.py'
argDict = {}
fi = open(fileName,'r')
exec(compile(fi.read(), fileName, 'exec'),argDict)
fi.close()
self.name = str(fileVotingProfile)
self.candidates = argDict['candidates']
self.voters = argDict['voters']
self.prerankedBallot = argDict['prerankedBallot']
self.ballot = self.computeBallot()
else:
print('Error: a stored preranked voting profile is required!')
return
self.sumWeights = 0.0
for v in self.voters:
self.sumWeights += self.voters[v]['weight']
[docs]
def save(self,fileName='tempPreRankedProfile'):
"""
Persistant storage of a preranked voting profile.
Parameter:
name of file (without <.py> extension!).
"""
print('*--- Saving preorder profile in file: <' + str(fileName) + '.py> ---*')
candidates = self.candidates
voters = self.voters
prerankedBallot = self.prerankedBallot
saveFileName = str(fileName)+str('.py')
fo = open(saveFileName, 'w')
fo.write('# Saved preranked voting profile: \n')
fo.write('from collections import OrderedDict \n')
fo.write('candidates = OrderedDict([\n')
for x in candidates:
try:
candidateName = candidates[x]['name']
except:
candidateName = x
#fo.write('\'' + str(x) + '\': {\'name\': \'' + str(x)+ '\'},\n')
fo.write('(\'%s\', {\'name\': \'%s\'}),\n' % (x,candidateName) )
fo.write('])\n')
fo.write('voters = OrderedDict([\n')
for v in voters:
fo.write('(\'' +str(v)+'\', {\n')
fo.write('\'weight\':'+str(voters[v]['weight'])+'}),\n')
fo.write('])\n')
fo.write('prerankedBallot = {\n')
for v in prerankedBallot:
fo.write('\'' +str(v)+'\': [\n')
for x in prerankedBallot[v]:
fo.write(str(x) + ',\n')
fo.write('],\n')
fo.write( '}\n')
fo.close()
[docs]
def showPrerankedBallots(self,IntegerWeights=True):
"""
show the preranked ballots
"""
## sumWeights = Decimal('0')
if IntegerWeights:
formStr = ' %s(%.0f):\t %s'
else:
formStr = ' %s(%.f):\t %s'
print(' voters \t marginal ')
print('(weight)\t candidates rankings')
for v in self.voters:
## sumWeights += self.voters[v]['weight']
print(formStr \
% (str(v),self.voters[v]['weight'],
str(self.prerankedBallot[v])))
print('# voters: ',str(self.sumWeights))
[docs]
def computeBallot(self,Debug=False):
"""
compute ballots from the prerankeds
"""
from copy import deepcopy
ballot = {}
for v in self.voters:
preranked = self.prerankedBallot[v]
ballot[v] = self.computePrerankedBallot(preranked)
if Debug:
print(preranked)
print(ballot[v])
return ballot
#-----------
[docs]
class RandomPrerankedVotingProfile(PrerankedVotingProfile):
"""
A specialized class for generating random preranked voting profiles.
*Parameters*
* The *votersWeights* parameter may be a list of positive integers in order to
deterministically attribute weights to the voters
Is ignored when *RandomWeights* is True
* When *voterWeights* are None and *RandomWeights* is False, each voter
obtains a single vote (default setting)
* the *lengthProbability* influences the expected cardinalities of the equivalence classes
"""
def __init__(self,numberOfVoters=10,
numberOfCandidates=5,
votersIdPrefix='v',
candidatesIdPrefix='c',
votersWeights=None,
RandomWeights=False,
lengthProbability=0.4,
seed=None,Debug=False):
"""
"""
from collections import OrderedDict
import random
if seed is None:
seed = random.random()
random.seed(seed)
votersList = [x for x in range(1,numberOfVoters + 1)]
voters = OrderedDict()
nd = len(str(numberOfVoters))
for v in votersList:
voterID = ('%s%%0%dd' % (votersIdPrefix,nd)) % (v)
if votersWeights is not None:
try:
weight = votersWeights[v-1]
except:
weight = 1
else:
if RandomWeights:
weight = random.randint(1,numberOfVoters)
else:
weight = 1
voters[voterID] = {'weight':Decimal('%d' % weight)}
candidatesList = [x for x in range(1,numberOfCandidates + 1)]
candidates = OrderedDict()
na = len(str(numberOfCandidates))
for c in candidatesList:
candidateID =('%s%%0%dd' % (candidatesIdPrefix,na)) % (c)
candidates[candidateID] = {'name': 'Candidate '+ candidateID}
# store instance attributes
self.name = str('randprerankedProfile')
self.seed = seed
self.candidates = candidates
self.voters = voters
self.RandomWeights = RandomWeights
self.sumWeights = Decimal('0')
for v in self.voters:
self.sumWeights += self.voters[v]['weight']
self.lengthProbability = lengthProbability
self.prerankedBallot = self.generateRandomPrerankedBallot(
seed=seed)
self.ballot = self.computeBallot()
[docs]
def generateRandomPrerankedBallot(self,
seed=None,Debug=False):
"""
Renders a randomly generated preranked ballot.
"""
import random
random.seed(seed)
prerankedBallot = {}
voters = self.voters
candidatesList = [x for x in self.candidates]
n = len(candidatesList)
if Debug:
print(candidatesList)
for v in voters:
random.shuffle(candidatesList)
if Debug:
print(candidatesList)
from randomNumbers import\
BinomialRandomVariable
currIndex = 0
prerankedBallotv = []
length = BinomialRandomVariable(n,self.lengthProbability)
while currIndex < n:
endIndex = currIndex + length.random()
if Debug:
print(v,n,currIndex,endIndex)
if endIndex > currIndex:
if endIndex < n:
prerankedBallotv.append(candidatesList[currIndex:endIndex])
currIndex = endIndex
else:
prerankedBallotv.append(candidatesList[currIndex:n])
currIndex = n
if Debug:
print(v,prerankedBallotv)
prerankedBallot[v] = prerankedBallotv
return prerankedBallot
#-------------
[docs]
class LinearVotingProfile(VotingProfile):
"""
A specialised class for linear voting profiles
Structure::
candidates = OrderedDict([('a', ...) ,('b', ...),('c', ...), ...])
voters = OrderedDict([('1',{'weight':1.0}), ('2',{'weight':1.0}), ...])
## each specifies a a ranked list of candidates
## from the best to the worst
linearBallot = {
'1' : ['b','c','a', ...],
'2' : ['a','b','c', ...],
...
}
Sample Python3 session
>>> from votingProfiles import *
>>> v = RandomLinearVotingProfile(numberOfVoters=5,numberOfCandidates=3)
>>> v.showLinearBallots()
voters marginal
(weight) candidates rankings
v1(1): ['c1', 'c3', 'c2']
v2(1): ['c2', 'c1', 'c3']
v3(1): ['c1', 'c3', 'c2']
v4(1): ['c2', 'c1', 'c3']
v5(1): ['c3', 'c1', 'c2']
# voters: 5
>>> v.computeRankAnalysis()
{'c1': [Decimal('2'), Decimal('3'), 0],
'c2': [Decimal('2'), 0, Decimal('3')],
'c3': [Decimal('1'), Decimal('2'), Decimal('2')]}
>>> v.showRankAnalysisTable()
*---- Borda rank analysis tableau -----*
candi- | alternative-to-rank | Borda
dates | 1 2 3 | score average
-------|-------------------------------------
'c1' | 2 3 0 | 8 1.60
'c2' | 2 0 3 | 11 2.20
'c3' | 1 2 2 | 11 2.20
>>> v.computeUninominalVotes()
{'c1': Decimal('2'), 'c2': Decimal('2'), 'c3': Decimal('1')}
>>> v.computeSimpleMajorityWinner()
['c1', 'c2']
>>> v.computeBordaScores()
OrderedDict([
('c1', {'BordaScore': Decimal('8'), 'averageBordaScore': Decimal('1.6')}),
('c2', {'BordaScore': Decimal('11'), 'averageBordaScore': Decimal('2.2')}),
('c3', {'BordaScore': Decimal('11'), 'averageBordaScore': Decimal('2.2')})])
>>> v.computeBordaWinners()
['a1']
>>> v.computeInstantRunoffWinner()
['c1']
"""
def __init__(self,fileVotingProfile=None,numberOfCandidates=5,
numberOfVoters=9):
if fileVotingProfile is not None:
fileName = fileVotingProfile + '.py'
## else:
## fileName = 'testapprovalvotingprofile.py'
argDict = {}
fi = open(fileName,'r')
exec(compile(fi.read(), fileName, 'exec'),argDict)
fi.close()
self.name = str(fileVotingProfile)
self.candidates = argDict['candidates']
self.voters = argDict['voters']
try:
self.seed = argDict('seed')
except:
pass
try:
self.withPolls = argDict('WithPolls')
except:
self.WithPolls = False
try:
self.RandomWeights = argDict('RandomWeights')
except:
pass
try:
self.sumWeights = argDict('sumWeights')
except:
pass
try:
self.poll1 = argDict('poll1')
except:
pass
try:
self.poll2 = argDict('poll2')
except:
pass
try:
self.other = argDict('other')
except:
pass
try:
self.partyRepartition = argDict('partyRepartition')
except:
pass
self.linearBallot = argDict['linearBallot']
self.ballot = self.computeBallot()
else:
print('!!! Error: The name of a stored linear voting profile is required !!!')
return
self.sumWeights = 0.0
for v in self.voters:
self.sumWeights += self.voters[v]['weight']
[docs]
def computeBallot(self):
"""
Computes a complete ballot from the linear Ballot.
"""
candidates = [x for x in self.candidates]
linearBallot = self.linearBallot
ballot = {}
for v in linearBallot:
ballot[v] = {}
for x in candidates:
ballot[v][x] = {}
for y in candidates:
ballot[v][x][y] = Decimal("0.0")
n = len(linearBallot[v])
for i in range(n):
for j in range(i+1,n):
x = linearBallot[v][i]
y = linearBallot[v][j]
ballot[v][x][y] = Decimal("1.0")
ballot[v][y][x] = Decimal("-1.0")
self.ballot = ballot
return ballot
[docs]
def save(self,fileName='templinearprofile'):
"""
Persistant storage of a linear voting profile.
Parameter:
name of file (without <.py> extension!).
"""
print('*--- Saving linear profile in file: <' + str(fileName) + '.py> ---*')
candidates = self.candidates
voters = self.voters
linearBallot = self.linearBallot
saveFileName = str(fileName)+str('.py')
fo = open(saveFileName, 'w')
fo.write('# Saved linear voting profile: \n')
fo.write('from collections import OrderedDict \n')
fo.write('candidates = OrderedDict([\n')
for x in candidates:
try:
candidateName = candidates[x]['name']
except:
candidateName = x
#fo.write('\'' + str(x) + '\': {\'name\': \'' + str(x)+ '\'},\n')
fo.write('(\'%s\', {\'name\': \'%s\'}),\n' % (x,candidateName) )
fo.write('])\n')
fo.write('voters = OrderedDict([\n')
for v in voters:
fo.write('(\'' +str(v)+'\', {\n')
fo.write('\'weight\':'+str(voters[v]['weight'])+'}),\n')
fo.write('])\n')
fo.write('linearBallot = {\n')
for v in linearBallot:
fo.write('\'' +str(v)+'\': [\n')
for x in linearBallot[v]:
fo.write('\'' + str(x) + '\'' +',\n')
fo.write('],\n')
fo.write( '}\n')
fo.close()
[docs]
def showLinearBallots(self,IntegerWeights=True):
"""
show the linear ballots
"""
## sumWeights = Decimal('0')
if IntegerWeights:
formStr = ' %s(%.0f):\t %s'
else:
formStr = ' %s(%.f):\t %s'
print(' voters \t marginal ')
print('(weight)\t candidates rankings')
for v in self.voters:
## sumWeights += self.voters[v]['weight']
print(formStr \
% (str(v),self.voters[v]['weight'],
str(self.linearBallot[v])))
print('# voters: ',str(self.sumWeights))
[docs]
def computeRankAnalysis(self):
"""
compute the number of ranks each candidate obtains
"""
ranks = {}
candidatesList = [x for x in self.candidates]
n = len(candidatesList)
for x in candidatesList:
ranks[x] = [0 for i in range(n)]
for v in self.voters:
for i in range(n):
#print(v,i)
try:
x = self.linearBallot[v][i]
#ranks[x][i] += 1
ranks[x][i] += self.voters[v]['weight']
except:
pass
#print ranks
return ranks
[docs]
def showRankAnalysisTable(self,Sorted=True,ndigits=0,Debug=False):
"""
Print the rank analysis tableau.
If Sorted (True by default), the candidates
are ordered by increasing Borda Scores.
In case of decimal voters weights, ndigits allows
to format the decimal precision of the numerical output.
"""
print('*---- Borda rank analysis tableau -----*')
bordaScores = self.computeBordaScores()
candidatesList = [(bordaScores[x]['BordaScore'],x) for x in self.candidates]
if Sorted:
candidatesList.sort()
if Debug:
print(candidatesList)
nc = len(candidatesList)
rankIndex = [i+1 for i in range(nc)]
if Debug:
print(nc,rankIndex)
ranks = self.computeRankAnalysis()
if Debug:
print(ranks)
if Debug:
print(bordaScores)
## print table
print(' candi- | alternative-to-rank | Borda')
print(' dates | ', end=' ')
for x in rankIndex:
print( str(x) + ' ', end=' ')
print('| score average')
print('-------|-------------------------------------------------')
for c in candidatesList:
print(' \''+str(c[1])+'\' |', end=' ')
for i in rankIndex:
formatString = '%% .%df ' % ndigits
print(formatString % (ranks[c[1]][(i-1)]), end=' ')
formatString = ' | %% .%df %%.2f' % ndigits
print(formatString % (bordaScores[c[1]]['BordaScore'],\
bordaScores[c[1]]['averageBordaScore']))
[docs]
def computeBordaScores(self):
"""
compute Borda scores from the rank analysis
"""
from collections import defaultdict, OrderedDict
from operator import itemgetter
ranks = self.computeRankAnalysis()
scores = []
candidatesList = [x for x in self.candidates]
n = len(candidatesList)
for x in candidatesList:
BordaScore_x = 0
for i in range(n):
BordaScore_x += (i+1)*ranks[x][i]
averageBordaScore_x = BordaScore_x/self.sumWeights
scores.append((BordaScore_x,x,averageBordaScore_x))
scores.sort(key=itemgetter(0))
BordaScores = OrderedDict([(x[1],
{'BordaScore':x[0],
'averageBordaScore':x[2]}) for x in scores])
return BordaScores
[docs]
def showBordaRanking(self):
"""
show Borda ranking in increasing order of the Borda score
"""
BordaScores = self.computeBordaScores()
print('Borda ranking of the candidates')
for x in BordaScores:
print(x,': ',BordaScores[x])
[docs]
def computeBordaWinners(self):
"""
compute the Borda winner from the Borda scores, ie the list of
candidates with the minimal Borda score.
"""
BordaScores = self.computeBordaScores()
n = len(self.candidates)
m = 0
for v in self.voters:
m += self.voters[v]['weight']
BordaMinimum = n * m
candidatesList = [x for x in self.candidates]
for x in candidatesList:
if BordaMinimum > BordaScores[x]['BordaScore']:
BordaMinimum = BordaScores[x]['BordaScore']
winners = [x for x in BordaScores \
if BordaScores[x]['BordaScore'] == BordaMinimum]
return winners
[docs]
def computeInstantRunoffWinner(self,Comments=False):
"""
compute the instant runoff winner from a linear voting ballot
"""
from copy import deepcopy,copy
from decimal import Decimal
voters = copy(self.voters)
votersList = [x for x in self.voters]
totalWeight = Decimal("0.0")
for v in votersList:
totalWeight += Decimal('%.3f' % (voters[v]['weight']) )
halfWeight = totalWeight/Decimal("2.0")
if Comments:
print('Total number of votes = ', totalWeight)
print('Half of the Votes = ', halfWeight)
candidates = copy(self.candidates)
candidatesList = [x for x in candidates]
remainingCandidates = copy(candidatesList)
remainingLinearBallot = deepcopy(self.linearBallot)
stage = 1
while len(remainingCandidates) > 1:
uninominalVotes = self.computeUninominalVotes(
remainingCandidates,remainingLinearBallot)
if Comments:
print(' ==> stage = ', stage)
print(' remaining candidates', remainingCandidates)
print(' uninominal votes', uninominalVotes)
minVotes = totalWeight
maxVotes = Decimal("0.0")
for x in uninominalVotes:
if uninominalVotes[x] < minVotes:
minVotes = uninominalVotes[x]
if uninominalVotes[x] > maxVotes:
maxVotes = uninominalVotes[x]
if Comments:
print(' minimal number of votes = ', minVotes)
print(' maximal number of votes = ', maxVotes)
if maxVotes <= halfWeight:
currentCandidates = set(remainingCandidates)
for x in currentCandidates:
if uninominalVotes[x] == minVotes:
if Comments:
print(' candidate to remove = ', x)
remainingCandidates.remove(x)
for v in voters:
try:
remainingLinearBallot[v].remove(x)
except:
pass
if Comments:
print(' remaining candidates = ', remainingCandidates)
#print ' remaining ballots = ', remainingLinearBallot
stage += 1
else:
for x in remainingCandidates:
if uninominalVotes[x] == maxVotes:
if Comments:
print(' candidate %s obtains an absolute majority' % x)
return [x]
return remainingCandidates
[docs]
def save2PerfTab(self,fileName='votingPerfTab',
isDecimal=True,NA=-9999,
valueDigits=2,NegativeWeights=True,
Comments=False):
"""
Persistant storage of a linear voting profile in the format of a rank performance Tableau.
For each voter *v*, the rank performance of candidate *x* corresponds to:
number of candidates - linearProfile[v].index(x)
"""
from copy import deepcopy
if Comments:
print('*--- Saving as performance tableau in file: <' \
+ str(fileName) + '.py> ---*')
objectives = {}
fileNameExt = str(fileName)+str('.py')
fo = open(fileNameExt, 'w')
fo.write('# Saved performance Tableau: \n')
fo.write('from decimal import Decimal\n')
fo.write('from collections import OrderedDict\n')
# actions
nc = len(self.candidates)
fo.write('actions = OrderedDict([\n')
for x in self.candidates:
fo.write('(\'%s\', {\n' % str(x))
for it in self.candidates[x].keys():
fo.write('\'%s\': %s,\n' \
% (it,repr(self.candidates[x][it])) )
fo.write('}),\n')
fo.write('])\n')
# writing objectives if WithPolls
if self.WithPolls:
fo.write('objectives = OrderedDict([\n')
p0 = [v for v in self.voters if self.voters[v]['party'] == 0]
p1 = [v for v in self.voters if self.voters[v]['party'] == 1]
p2 = [v for v in self.voters if self.voters[v]['party'] == 2]
n0 = len(p0)
n1 = len(p1)
n2 = len(p2)
fo.write('(\'party0\',{\'name\':\'other\',\n')
fo.write('\'weight\': %.2f,\n' % Decimal(str(n0)) )
fo.write('\'criteria\': %s }),\n' % p0)
fo.write('(\'party1\',{\'name\':\'party 1\',\n')
fo.write('\'weight\': %.2f,\n' % Decimal(str(n1)) )
fo.write('\'criteria\': %s }),\n' % p1)
fo.write('(\'party2\',{\'name\':\'party 2\',\n')
fo.write('\'weight\': %.2f,\n' % Decimal(str(n2)) )
fo.write('\'criteria\': %s }),\n' % p2)
fo.write('])\n')
else:
fo.write('objectives = OrderedDict()\n')
# criteria
if self.WithPolls:
NegativeWeights = False
fo.write('criteria = OrderedDict([\n')
for g in self.voters:
fo.write('(\'%s\', {\'name\': \'%s\',\n' % (str(g),str(g)) )
for it in self.voters[g].keys():
if it == 'weight':
if NegativeWeights:
fo.write('\'%s\': %s,\n' \
% (it,repr(-self.voters[g][it])))
else:
fo.write('\'%s\': %s,\n' \
% (it,repr(self.voters[g][it])))
else:
fo.write('\'%s\': %s,\n' % (it,repr(self.voters[g][it])))
fo.write("\'scale\':(Decimal(1),Decimal(%d)),\n" % nc)
fo.write("\'preferenceDirection\': \'%s\'" % 'min')
fo.write('}),\n')
fo.write('])\n')
# evaluation
fo.write('evaluation = {\n')
for g in self.voters:
fo.write('\'' +str(g)+'\': {\n')
for x in self.candidates:
if Decimal:
#fo.write('\'' + str(x) + '\':Decimal("' + str(evaluation[g][x]) + '"),\n')
evaluationString = '\'%%s\':Decimal("%%.%df"),\n' \
% (valueDigits)
try:
xval = (self.linearBallot[g].index(x) + 1)
except:
xval = NA
if NegativeWeights:
fo.write(evaluationString % (x,Decimal(str(xval))))
else:
fo.write(evaluationString % (x,Decimal(str(-xval))))
else:
fo.write('\'' + str(x) + '\':' \
+ str(-evaluation[g][x]) + ',\n')
fo.write('},\n')
fo.write('}\n')
fo.write("NA = Decimal('%d')\n" % NA )
fo.close()
[docs]
def showHTMLVotingHeatmap(self,criteriaList=None,
actionsList=None,
fromIndex=None,
toIndex=None,
Transposed=True,
SparseModel=False,
minimalComponentSize=1,
rankingRule='Copeland',
quantiles=None,
strategy='average',
ndigits=0,
colorLevels=None,
pageTitle='Voting Heatmap',
Correlations=True,
Threading=False,
nbrOfCPUs=1,
Debug=False):
"""
Show the linear voting profile as a rank performance heatmap.
The linear voting profile is previously saved to a stored Performance Tableau.
(see perfTabs.PerformanceTableau.showHTMLPerformanceHeatmap() )
"""
from tempfile import mkdtemp
tempDir = mkdtemp()
perfTabFileName = '%s/votingPerfTab' % tempDir
if SparseModel:
self.save2PerfTab(perfTabFileName,_NegativeWeights=False)
else:
self.save2PerfTab(perfTabFileName)
t = PerformanceTableau(perfTabFileName)
t.showHTMLPerformanceHeatmap(criteriaList=criteriaList,
actionsList=actionsList,
fromIndex=fromIndex,toIndex=toIndex,
Transposed=Transposed,
SparseModel=SparseModel,
minimalComponentSize=minimalComponentSize,
rankingRule=rankingRule,
quantiles=quantiles, strategy=strategy,
ndigits=ndigits, colorLevels=colorLevels,
pageTitle=pageTitle, Correlations=Correlations,
Threading=Threading, nbrOfCPUs=nbrOfCPUs,
Debug=Debug)
[docs]
def computeUninominalVotes(self,candidates=None,linearBallot=None):
"""
compute uninominal votes for each candidate in candidates sublist
and restricted linear ballots
"""
if candidates==None:
candidates = self.candidates
if linearBallot is None:
linearBallot = self.linearBallot
uninominalVotes = {}
for x in candidates:
uninominalVotes[x] = 0
for v in self.voters:
if linearBallot[v][0] == x:
uninominalVotes[x] += self.voters[v]['weight']
return uninominalVotes
[docs]
def computeSimpleMajorityWinner(self,Comments=False):
"""
compute the winner in a uninominal Election from a linear ballot
"""
uv = self.computeUninominalVotes(self.candidates,self.linearBallot)
if Comments:
print('uninominal votes ', uv)
maxVotes = 0
for x in self.candidates:
if uv[x] > maxVotes:
maxVotes = uv[x]
if Comments:
print('maxVotes ', maxVotes)
simpleMajorityWinner = []
for x in self.candidates:
if uv[x] == maxVotes:
simpleMajorityWinner.append(x)
if Comments:
print('simple majority winner(s) ', simpleMajorityWinner)
return simpleMajorityWinner
[docs]
class RandomLinearVotingProfile(LinearVotingProfile):
"""
A specialized class for generating random liwear voting profiles.
*Parameters*
* When *WithPolls* is True, each party supporting voter's linear ballot is randomly oriented
by one of two random exponential poll results. The corresponding polls are stored
in self.poll1, respectively self.poll2.
* The *partyRepartition* sets the theoretical distribution of the two polls over the
set of voters. If set to 0.0 or 1.0, only self.poll2, resp. self.poll1, will orient
the respective party supporters.
* The *other* paraemter sets the theoretical proportion of non party supporters.
* The *DivisivePolitics* flag provides, if True, two reversed polls for
generating the random linear ballots.
* The *votersWeights* parameter may be a list of positive integers in order to
deterministically attribute weights to the voters.
Is ignored when *RandomWeights* is True.
* When *voterWeights* are None and *RandomWeights* is False, each voter
obtains a single vote (default setting).
* With *PartialLnearBallots* set to *True*, the linear voting ballots will be
randomly shortened with the *lengthProbability* parameter.
"""
def __init__(self,numberOfVoters=10,
numberOfCandidates=5,
IntraGroup=False,
votersIdPrefix='v',
candidatesIdPrefix='c',
PartialLinearBallots=False,
lengthProbability=0.5,
WithPolls=False,
partyRepartition=0.5,
other=0.05,
DivisivePolitics=False,
votersWeights=None,
RandomWeights=False,
seed=None,Debug=False):
"""
"""
from collections import OrderedDict
import random
if seed is None:
seed = random.random()
random.seed(seed)
votersList = [x for x in range(1,numberOfVoters + 1)]
voters = OrderedDict()
nd = len(str(numberOfVoters))
for v in votersList:
voterID = ('%s%%0%dd' % (votersIdPrefix,nd)) % (v)
if votersWeights is not None:
try:
weight = votersWeights[v-1]
except:
weight = 1
else:
if RandomWeights:
weight = random.randint(1,numberOfVoters)
else:
weight = 1
voters[voterID] = {'weight':Decimal('%d' % weight)}
if IntraGroup:
candidates = voters
else:
candidatesList = [x for x in range(1,numberOfCandidates + 1)]
candidates = OrderedDict()
na = len(str(numberOfCandidates))
for c in candidatesList:
candidateID =('%s%%0%dd' % (candidatesIdPrefix,na)) % (c)
candidates[candidateID] = {'name': 'Candidate '+ candidateID}
# store instance attributes
self.name = str('randLinearProfile')
self.seed = seed
self.candidates = candidates
self.voters = voters
self.IntraGroup = IntraGroup
self.WithPolls = WithPolls
self.RandomWeights = RandomWeights
self.sumWeights = Decimal('0')
for v in self.voters:
self.sumWeights += self.voters[v]['weight']
if WithPolls:
self.linearBallot \
= self.generateRandomLinearBallotWithPoll(
partyRepartition,other,DivisivePolitics,
seed=seed,Debug=Debug)
else:
self.linearBallot = self.generateRandomLinearBallot(
PartialLinearBallots=PartialLinearBallots,
lengthProbability=lengthProbability,
seed=seed)
self.ballot = self.computeBallot()
[docs]
def generateRandomLinearBallot(self,
PartialLinearBallots=False,
lengthProbability=0.5,
seed=None,Debug=False):
"""
Renders a randomly generated linear ballot.
"""
from random import shuffle
## random.seed(seed)
linearBallot = {}
voters = self.voters
if Debug:
print(candidatesList)
for v in voters:
candidatesList = [x for x in self.candidates]
if self.IntraGroup:
candidatesList.remove(v)
n = len(candidatesList)
shuffle(candidatesList)
if Debug:
print(v,candidatesList)
if PartialLinearBallots:
from randomNumbers import\
BinomialRandomVariable
length = BinomialRandomVariable(n,lengthProbability)
linearBallot[v] = candidatesList[:length.random()]
else:
linearBallot[v] = candidatesList.copy()
return linearBallot
[docs]
def generateRandomLinearBallotWithPoll(self,partyRepartition,
other,DivisivePolitics,
seed=None,Debug=False):
"""
Renders a random linear ballot in accordance with the given polls:
self.poll1 and self.poll2.
Polls are distributed in the *bipartisan* proportion.
"""
import random
random.seed(seed)
from randomNumbers import DiscreteRandomVariable
voters = self.voters
candidatesList = [x for x in self.candidates]
if Debug:
print(candidatesList)
nc = len(candidatesList)
# divisive or independent polls
poll1 = {}
sumPoll = 0.0
for c in candidatesList:
poll1[c] = random.expovariate(1)
sumPoll += poll1[c]
for c in poll1:
poll1[c] /= sumPoll
self.poll1 = poll1
poll2 = {}
sumPoll = 0.0
if DivisivePolitics:
p1 = [(poll1[c],c) for c in candidatesList]
p1 = list(sorted(p1))
p2 = list(reversed(p1))
if Debug:
print(p1,p2)
nc = len(candidatesList)
for i in range(nc):
#for c in candidatesList:
#c = candidatesList[i]
#poll2[c] = random.expovariate(1)
c = p1[i][1]
poll2[c] = p2[i][0]
sumPoll += poll2[c]
else:
for c in candidatesList:
poll2[c] = random.expovariate(1)
sumPoll += poll2[c]
for c in poll2:
poll2[c] /= sumPoll
# storing polls
self.poll1 = poll1
self.poll2 = poll2
self.other = other
self.partyRepartition = partyRepartition
if Debug:
print(poll1,poll2,other,partyRepartition)
# generating random linear ballots
linearBallot = {}
j = 1
for v in voters:
# each voter is attached to one of the polls
u = random.random()
if u < other: # random voting
otherCandidatesList = list(candidatesList)
random.shuffle(otherCandidatesList)
voters[v]['party'] = 0
linearBallot[v] = otherCandidatesList
else: # poll driven random
if partyRepartition < random.random():
pollv = poll1
voters[v]['party'] = 1
else:
pollv = poll2
voters[v]['party'] = 2
# generating a random linear ranking
shuffledCandidatesList = []
for i in range(nc-1):
NotShuffled = True
currPoll = pollv.copy()
rdv = DiscreteRandomVariable(currPoll,seed=j)
while NotShuffled:
xc = rdv.random()
if xc not in shuffledCandidatesList:
NotShuffled = False
shuffledCandidatesList.append(xc)
currPoll.pop(xc)
## if Debug:
## print(i,shuffledCandidatesList)
shc = set(shuffledCandidatesList)
sc = set(candidatesList)
xc = (sc-shc).pop()
shuffledCandidatesList.append(xc)
## if Debug:
## print('==>>', v,shuffledCandidatesList)
j += 1
linearBallot[v] = shuffledCandidatesList
return linearBallot
[docs]
def showRandomPolls(self,Debug=False):
"""
Shows the random polls, the case given.
"""
from operator import itemgetter
try:
poll1 = [(self.poll1[x],x) for x in self.poll1]
except:
poll1 = []
nc = len(poll1)
if nc > 1:
voters = self.voters
nv = len(voters)
supportersParty1 = [x for x in voters if voters[x]['party'] == 1]
supportersParty2 = [x for x in voters if voters[x]['party'] == 2]
otherSupporters = [x for x in voters if voters[x]['party'] == 0]
n0 = len(otherSupporters)
p0 = float(n0)/float(nv)
n1 = len(supportersParty1)
p1 = float(n1)/float(nv)
n2 = len(supportersParty2)
p2 = float(n2)/float(nv)
if Debug:
print(n1,p1,n2,p2,n0,p0)
poll1.sort(reverse=True,key=itemgetter(0))
poll2 = [(self.poll2[x],x) for x in self.poll2]
poll2.sort(reverse=True,key=itemgetter(0))
poll = []
for x in self.candidates:
res = p1 * self.poll1[x] +\
p2 * self.poll2[x]
poll.append( (res,x) )
poll.sort(reverse=True,key=itemgetter(0))
# print polls
print('Random repartition of voters')
print(' Party-1 supporters : %d (%05.2f%%)' % (n1,p1*100))
print(' Party-2 supporters : %d (%05.2f%%)' % (n2,p2*100))
print(' Other voters : %d (%05.2f%%)' % (n0,p0*100))
print('*---------------- random polls ---------------')
print(' Party-1(%04.1f%%) | Party-2(%04.1f%%)| expected '\
% (p1*100, p2*100) )
print('-----------------------------------------------')
for i in range(nc):
print(" %s : %05.2f%% | %s : %05.2f%% | %s : %05.2f%%" %\
(poll1[i][1],poll1[i][0]*100.0,
poll2[i][1],poll2[i][0]*100.0,
poll[i][1], poll[i][0]*100.0) )
else:
print('No polls defined !')
[docs]
class BipolarApprovalVotingProfile(VotingProfile):
"""
A specialised class for approval-disapproval voting profiles.
Structure::
candidates = OrderedDict([('a', {'name': ...}),
('b', {'name': ...}),
..., ... ])
voters = OrderedDict([('v1',{'weight':1.0}),('v2':{'weight':1.0}), ...])
## each voter characterises the candidates
## he approves (+1), disapproves (-1) or ignores (0).
approvalBallot = {
'v1' : {'a': Decima('1'), 'b': Decimal('0'), ...},
'v2' : {'a': Decima('0'), 'b': Decimal('-1'), ...},
...
}
...
"""
def __init__(self,fileVotingProfile=None,seed=None):
if fileVotingProfile is not None:
fileName = fileVotingProfile + '.py'
## else:
## fileName = 'testapprovalvotingprofile.py'
argDict = {}
fi = open(fileName,'r')
exec(compile(fi.read(), fileName, 'exec'), argDict)
fi.close()
self.name = str(fileVotingProfile)
self.candidates = argDict['candidates']
self.voters = argDict['voters']
self.approvalBallot = argDict['approvalBallot']
#self.disapprovalBallot = argDict['disapprovalBallot']
self.netApprovalScores = self.computeNetApprovalScores()
self.IntraGroup = argDict['IntraGroup']
self.ballot = self.computeBallot()
else:
print('Error: a valid stored bipolar approval voting profile is required !')
return
[docs]
def showBipolarApprovals(self):
"""
Renders the approval and disapprovals per voter
"""
print('Bipolar approval ballots')
print('------------------------')
for v in self.voters:
app = [b for b in self.approvalBallot[v]\
if self.approvalBallot[v][b] == Decimal('1')]
disapp = [b for b in self.approvalBallot[v]\
if self.approvalBallot[v][b] == Decimal('-1')]
print(v, ':')
print('Approvals :',app)
print('Disapprovals:',disapp)
[docs]
def showApprovalResults(self):
"""
Renders the number of approvals obtained by each candidate.
"""
from operator import itemgetter
print('Approval results')
candidates = [x for x in self.candidates]
voters = self.voters
Max = Decimal('1')
Min = Decimal('-1')
Med = Decimal('0')
#candidates.sort()
votesPerCandidate = {}
for c in candidates:
votesPerCandidate[c] = Decimal('0.0')
ballot = self.approvalBallot
sumApp = Decimal('0')
for v in ballot:
for c in ballot[v]:
if ballot[v][c] > Med:
votesPerCandidate[c] += voters[v]['weight']
sumApp += voters[v]['weight']
results = []
for c in candidates:
results.append((int(votesPerCandidate[c]),c))
results.sort(reverse=True,key=itemgetter(0))
for c in results:
print(' Candidate: %s obtains %d votes' % (c[1],c[0] ))
print('Total approval votes: %d' % (int(sumApp)) )
tot = len(candidates) * len(voters)
print('Approval proportion: %d/%d = %.2f' \
% ( int(sumApp),tot,sumApp/Decimal(str(tot)) ) )
[docs]
def showDisapprovalResults(self):
"""
Renders the number of disapprovals obtained by each candidate.
"""
from operator import itemgetter
print('Disapproval results')
candidates = [x for x in self.candidates]
voters = self.voters
Max = Decimal('1')
Min = Decimal('-1')
Med = Decimal('0')
#candidates.sort()
votesPerCandidate = {}
for c in candidates:
votesPerCandidate[c] = Decimal('0.0')
ballot = self.approvalBallot
sumDisapp = Decimal('0')
for v in ballot:
for c in ballot[v]:
if ballot[v][c] < Med:
votesPerCandidate[c] += voters[v]['weight']
sumDisapp += voters[v]['weight']
results = []
for c in candidates:
results.append((int(votesPerCandidate[c]),c))
results.sort(key=itemgetter(0))
for c in results:
print(' Candidate: %s obtains %d votes' % (c[1],c[0] ))
print('Total disapproval votes: %d' % (int(sumDisapp)) )
tot = len(candidates) * len(voters)
print('Disapproval proportion: %d/%d = %.2f' \
% ( int(sumDisapp),tot,sumDisapp/Decimal(str(tot)) ) )
[docs]
def showHTMLVotingHeatmap(self,criteriaList=None,
actionsList=None,
fromIndex=None,
toIndex=None,
Transposed=True,
rankingRule='NetFlows',
quantiles=None,
strategy='average',
ndigits=0,
colorLevels=3,
pageTitle='Voting Heatmap',
Correlations=True,
Threading=False,
nbrOfCPUs=1,
Debug=False):
"""
Show the linear voting profile as a rank performance heatmap.
The linear voting profile is previously saved to a stored Performance Tableau.
(see perfTabs.PerformanceTableau.showHTMLPerformanceHeatmap() )
"""
from tempfile import mkdtemp
tempDir = mkdtemp()
perfTabFileName = '%s/votingPerfTab' % tempDir
self.save2PerfTab(perfTabFileName)
t = PerformanceTableau(perfTabFileName)
t.showHTMLPerformanceHeatmap(criteriaList=criteriaList,
actionsList=actionsList,
fromIndex=fromIndex, toIndex=toIndex,
Transposed=Transposed,
rankingRule=rankingRule,
quantiles=quantiles, strategy=strategy,
ndigits=ndigits, colorLevels=colorLevels,
pageTitle=pageTitle, Correlations=Correlations,
Threading=Threading, nbrOfCPUs=nbrOfCPUs,
Debug=Debug)
[docs]
def computeNetApprovalScores(self,Debug=False):
"""
Computes the approvals - disapprovals score obtained by each candidate.
"""
from operator import itemgetter
candidates = [x for x in self.candidates]
voters = self.voters
Max = Decimal('1')
Min = Decimal('-1')
Med = Decimal('0')
#candidates.sort()
votesPerCandidate = {}
for c in candidates:
votesPerCandidate[c] = Decimal('0.0')
approved = self.approvalBallot
#disapproved = self.disapprovalBallot
for v in voters:
for c in candidates:
votesPerCandidate[c] += approved[v][c]*voters[v]['weight']
results = []
for c in candidates:
results.append((int(votesPerCandidate[c]),c))
results.sort(reverse=True,key=itemgetter(0))
if Debug:
print('Net Approval Scores')
for c in results:
print('candidate: %s obtains %d net approvals' \
% (c[1],c[0]) )
return votesPerCandidate
[docs]
def showNetApprovalScores(self):
"""
Prints the approval - disapproval scores obtained by each candidate.
"""
from operator import itemgetter
candidates = self.candidates
scores = self.netApprovalScores
results = []
for c in candidates:
results.append((int(scores[c]),c))
results.sort(reverse=True,key=itemgetter(0))
print('Net Approval Scores')
for c in results:
print(' Candidate: %s obtains %d net approvals' \
% (c[1],c[0]) )
[docs]
def computeBallot(self,Debug=True):
"""
Computes a complete ballot from the approval Ballot.
"""
candidates = [x for x in self.candidates]
n = len(candidates)
app = self.approvalBallot
Max = Decimal('1')
Med = Decimal('0')
Min = Decimal('-1')
#DAVBallot = self.disapprovalBallot
ballot = {}
for v in app:
av = app[v]
ballot[v] = {}
for x in candidates:
ballot[v][x] = {}
for y in candidates:
ballot[v][x][y] = Decimal("0.0")
for i in range(n):
x = candidates[i]
for j in range(i+1,n):
y = candidates[j]
## if Debug:
## print(x,av[x],y,av[y])
if av[x] > av[y]:
ballot[v][x][y] = Max
ballot[v][y][x] = Min
elif av[x] < av[y]:
ballot[v][x][y] = Min
ballot[v][y][x] = Max
else:
ballot[v][x][y] = Med
self.ballot = ballot
return ballot
[docs]
def save(self,fileName='tempAVprofile'):
"""
Persistant storage of a bipolar approval voting profile.
Parameter:
name of file (without <.py> extension!).
"""
print('*--- Saving AV profile in file: <' + str(fileName) + '.py> ---*')
candidates = self.candidates
voters = self.voters
approvalBallot = self.approvalBallot
#disapprovalBallot = self.disapprovalBallot
saveFileName = str(fileName)+str('.py')
fo = open(saveFileName, 'w')
fo.write('# Saved approval voting profile: \n')
fo.write('from collections import OrderedDict\n')
fo.write('from decimal import Decimal\n')
fo.write('candidates = OrderedDict([\n')
for x in candidates:
try:
candidateName = candidates[x]['name']
except:
candidateName = x
#fo.write('\'' + str(x) + '\': {\'name\': \'' + str(x)+ '\'},\n')
fo.write('(\'%s\', {\'name\': \'%s\'}),\n' % (x,candidateName) )
fo.write('])\n')
fo.write('voters = OrderedDict([\n')
for v in voters:
fo.write('(\'' +str(v)+'\', {\n')
fo.write('\'weight\':'+str(voters[v]['weight'])+'}),\n')
fo.write('])\n')
try:
fo.write('IntraGroup = %s' % self.IntraGroup)
except:
fo.write('IntraGroup = %s' % False)
fo.write('approvalProbability = %.2f\n' \
% self.approvalProbability)
fo.write('disapprovalProbability = %.2f\n' \
% self.disapprovalProbability)
fo.write('approvalBallot = {\n')
for v in approvalBallot:
fo.write('\'' +str(v)+'\': {\n')
for x in approvalBallot[v]:
fo.write('\'%s\': Decimal(\'%.f\'),\n' \
% (str(x),approvalBallot[v][x]) )
fo.write('},\n')
fo.write( '}\n')
## fo.write('disapprovalBallot = {\n')
## for v in disapprovalBallot:
## fo.write('\'' +str(v)+'\': [\n')
## for x in disapprovalBallot[v]:
## fo.write('\'' + str(x) + '\'' +',\n')
## fo.write('],\n')
## fo.write( '}\n')
fo.write('netVotingScores = {\n')
for c in candidates:
fo.write('\'%s\': Decimal(\'%.f\'),\n' \
% (str(c), self.netApprovalScores[c]))
fo.write( '}\n')
fo.close()
[docs]
def save2PerfTab(self,fileName='votingPerfTab',valueDigits=2):
"""
Persistant storage of an approval voting profile in the format of a standard performance tableau.
For each voter *v*, the performance of candidate *x* corresponds to:
1, if approved;
-1, if disapproved;
NA, missing evaluation otherwise,
"""
from copy import deepcopy
print('*--- Saving as performance tableau in file: <' \
+ str(fileName) + '.py> ---*')
objectives = {}
fileNameExt = str(fileName)+str('.py')
fo = open(fileNameExt, 'w')
fo.write('# Saved performance Tableau: \n')
fo.write('from decimal import Decimal\n')
fo.write('from collections import OrderedDict\n')
# actions
fo.write('actions = OrderedDict([\n')
for x in self.candidates:
fo.write('(\'%s\', {\n' % str(x))
for it in self.candidates[x].keys():
fo.write('\'%s\': %s,\n' \
% (it,repr(self.candidates[x][it])) )
fo.write('}),\n')
fo.write('])\n')
# no objectives
fo.write('objectives = OrderedDict()\n')
# criteria
minScale = -1
maxScale = 1
medScale = 0
NA = 999
fo.write('criteria = OrderedDict([\n')
for g in self.voters:
fo.write('(\'%s\', {\n' % str(g))
for it in self.voters[g].keys():
fo.write('\'%s\': %s,\n' % (it,repr(self.voters[g][it])))
fo.write("\'scale\':(Decimal(\'%d\'),Decimal(\'%d\')),\n" \
% (minScale,maxScale) )
fo.write('}),\n')
fo.write('])\n')
# evaluation
AVballot = self.approvalBallot
Max = Decimal('1')
Min = Decimal('-1')
#DAVBallot = self.disapprovalBallot
fo.write('NA = Decimal("%d")\n' % NA)
fo.write('evaluation = {\n')
for g in self.voters:
fo.write('\'' +str(g)+'\': {\n')
approved = AVballot[g]
for x in self.candidates:
evaluationString = '\'%%s\':Decimal("%%.%df"),\n' \
% (valueDigits)
if approved[x] == Max:
xval = maxScale
elif approved[x] == Min:
xval = minScale
else: # ignored
xval = medScale
fo.write(evaluationString % (x,Decimal(str(xval))))
fo.write('},\n')
fo.write( '}\n')
fo.close()
# ------------------------
[docs]
class RandomBipolarApprovalVotingProfile(BipolarApprovalVotingProfile,
RandomLinearVotingProfile):
"""
A specialized class for generating random approval-disapproval voting profiles
with the help of random linear voting profiles.
*approvalProbability* determines the number of first-ranked candidates approved
*disapprovalProbability* determines the number of last-ranked candidates disapproved
"""
def __init__(self,
numberOfVoters=100,
votersIdPrefix= 'v',
IntraGroup=False,
numberOfCandidates=15,
candidatesIdPrefix='c',
approvalProbability=0.25,
disapprovalProbability=0.3,
WithPolls=False,
partyRepartition=0.5,
other=0.05,
DivisivePolitics=False,
votersWeights=None,
RandomWeights=False,
seed=None,Debug=False):
"""
Random profile creation parameters:
| numberOfVoters=9, numberOfCandidates=5,
| minSizeOfBallot=1, maxSizeOfBallot=2.
"""
from collections import OrderedDict
from votingProfiles import RandomLinearVotingProfile
rlv = RandomLinearVotingProfile(
numberOfVoters=numberOfVoters,
votersIdPrefix=votersIdPrefix,
IntraGroup=IntraGroup,
numberOfCandidates=numberOfCandidates,
candidatesIdPrefix=candidatesIdPrefix,
WithPolls=WithPolls,
partyRepartition=partyRepartition,
other=other,
DivisivePolitics=DivisivePolitics,
votersWeights=votersWeights,
RandomWeights=RandomWeights,
seed=seed)
if Debug:
print(rlv)
rlv.showRandomPolls()
self.name = str('bipolarApprovalProfile')
self.IntraGroup=IntraGroup
self.candidates = rlv.candidates
self.voters = rlv.voters
self.seed = rlv.seed
self.sumWeights = rlv.sumWeights
self.WithPolls = rlv.WithPolls
if WithPolls:
self.poll1 = rlv.poll1
self.poll2 = rlv.poll2
self.other = rlv.other
self.partyRepartition = rlv.partyRepartition
self.linearBallot = rlv.linearBallot
if approvalProbability <= 1.0 and approvalProbability >= 0.0:
self.approvalProbability = approvalProbability
else:
print('!!! Invalid approval probability: %.3f' % approvalProbability)
return
if disapprovalProbability <= 1.0 and disapprovalProbability >= 0.0:
self.disapprovalProbability = disapprovalProbability
else:
print('!!! Invalid disapproval probability: %.3f' % disapprovalProbability)
return
self.approvalBallot =\
self._generateRandomApprovalBallot(rlv,
approvalProbability=approvalProbability,
disapprovalProbability=disapprovalProbability,
seed=seed,Debug=Debug)
self.netApprovalScores = self.computeNetApprovalScores()
self.ballot = self.computeBallot()
def _generateRandomApprovalBallot(self,rlv,
approvalProbability=0.25,
disapprovalProbability=0.35,
seed=None,Debug=True):
"""
Renders a randomly generated approval ballot.
"""
from randomNumbers import BinomialRandomVariable
approvalBallot = {}
#disapprovalBallot = {}
voters = self.voters
candidates = self.candidates
nc = len(candidates)
approvalNbr = BinomialRandomVariable(nc,approvalProbability)
disapprovalNbr = BinomialRandomVariable(nc,disapprovalProbability)
for v in voters:
ac = approvalNbr.random()
dc = disapprovalNbr.random()
# !!! -0 == 0 so that [-0:] does not select [] as expected
# !!! but instead [0:] i.e. the complete list of n elements
# !!! just like [-n:]
if Debug:
print(v,nc,ac,dc)
candidatesList = rlv.linearBallot[v]
vApprovals = candidatesList[:ac]
if dc > 0:
vDisapprovals = candidatesList[-dc:]
else:
vDisapprovals = []
for c in vApprovals:
if c in vDisapprovals:
vDisapprovals.remove(c)
if Debug:
print(vApprovals)
print(vDisapprovals)
approvalBallot[v] = {}
for c in candidates:
if c in vApprovals and c not in vDisapprovals:
approvalBallot[v][c] = Decimal('1')
elif c in vDisapprovals and c not in vApprovals:
approvalBallot[v][c] = Decimal('-1')
else:
approvalBallot[v][c] = Decimal('0')
return approvalBallot
#--------------------------------
[docs]
class MajorityMarginsDigraph(Digraph,VotingProfile):
"""
Specialization of the general Digraph class for generating
bipolar-valued marginal pairwise majority margins digraphs.
Parameters:
| stored voting profile (fileName of valid py code) or voting profile object
| optional, coalition (sublist of voters)
Example Python3 session
>>> from votingProfiles import *
>>> v = RandomLinearVotingProfile(numberOfVoters=101,
... numberOfCandidates=5)
>>> v.showLinearBallots()
voters marginal
(weight) candidates rankings
v001(1): ['c5', 'c3', 'c1', 'c4', 'c2']
v002(1): ['c1', 'c3', 'c2', 'c5', 'c4']
v003(1): ['c5', 'c1', 'c2', 'c4', 'c3']
v004(1): ['c5', 'c1', 'c4', 'c2', 'c3']
v005(1): ['c4', 'c5', 'c3', 'c1', 'c2']
v006(1): ['c5', 'c1', 'c2', 'c4', 'c3']
...
...
>>> g = MajorityMarginsDigraph(v,IntegerValuation=True)
>>> g.showRelationTable()
* ---- Relation Table -----
S | 'c1' 'c2' 'c3' 'c4' 'c5'
------|--------------------------------
'c1' | 0 -3 -9 -11 1
'c2' | 3 0 -7 7 -3
'c3' | 9 7 0 17 -9
'c4' | 11 -7 -17 0 -1
'c5' | -1 3 9 1 0
Valuation domain: [-101;+101]
>>> g.computeCondorcetWinners()
[']
>>> g.exportGraphViz()
*---- exporting a dot file for GraphViz tools ---------*
Exporting to rel_randLinearProfile.dot
dot -Grankdir=BT -Tpng rel_randLinearProfile.dot -o rel_randLinearProfile.png
.. image:: rel_randLinearProfile.png
"""
def __init__(self,argVotingProfile=None,
coalition=None,
IntegerValuation=True,
Debug=False):
from copy import copy
if isinstance(argVotingProfile, (VotingProfile,
LinearVotingProfile,
BipolarApprovalVotingProfile)):
votingProfile = argVotingProfile
elif argVotingProfile is not None:
votingProfile = VotingProfile(argVotingProfile)
else:
print('Error: A valid voting profile has to be given!')
return
self.name = 'rel_' + votingProfile.name
self.actions = copy(votingProfile.candidates)
if coalition is None:
voters = copy(votingProfile.voters)
else:
voters = {}
for g in coalition:
voters[g] = votingProfile.voters[g]
self.criteria = voters
self.ballot = copy(votingProfile.ballot)
self.relation = self._constructMajorityMarginsRelation(
IntegerValuation=IntegerValuation,Debug=Debug)
self.order = len(self.actions)
self.gamma = self.gammaSets()
self.notGamma = self.notGammaSets()
def _constructMajorityMarginsRelation(self,IntegerValuation=True,Debug=False):
"""
Renders the marginal majority between candidates
on the basis of an approval ballot.
"""
candidates = set(self.actions)
voters = self.criteria
sumWeight = Decimal('0.0')
for v in voters:
sumWeight += Decimal(str(voters[v]['weight']))
if Debug:
print('sumweight',sumWeight)
ballot = self.ballot
relation = {}
Med = Decimal('0')
for x in candidates:
relation[x] = {}
for y in candidates:
relation[x][y] = Med
for v in voters:
bv = ballot[v]
for x in candidates:
for y in candidates:
relation[x][y] += Decimal(str(bv[x][y]))\
* Decimal(str(voters[v]['weight']))
if Debug:
print(relation)
if IntegerValuation:
Min = -sumWeight
Max = sumWeight
Med = Decimal('0')
else:
Min = Decimal('-1.0')
Max = Decimal('1.0')
Med = Decimal('0.0')
self.valuationdomain = {'min': Min, 'med': Med, 'max':Max,
'hasIntegerValuation': IntegerValuation}
if Debug:
print(self.valuationdomain)
if not IntegerValuation and Max != Decimal('0'):
for x in candidates:
for y in candidates:
relation[x][y] = relation[x][y]/sumWeight
if Debug:
print(relation)
return relation
[docs]
def showMajorityMargins(self,**args):
"""
Wrapper for the
Digraph.showRelationTable(Sorted=True, IntegerValues=False,
actionsSubset=None, relation=None, ndigits=2,
ReflexiveTerms=True, fromIndex=None, toIndex=None)
See the :py:meth:`digraphs.Digraph.showRelationTable` description.
"""
Digraph.showRelationTable(self,**args)
[docs]
class CondorcetDigraph(MajorityMarginsDigraph):
"""
Dummy obsolete class name for MajorityMarginsDigraph class.
"""
#----------test voting Digraph class ----------------
if __name__ == "__main__":
#from transitiveDigraphs import *
print('****************************************************')
print('* Python voting digraphs module *')
print('* $Revision: Python3.9 $ *')
print('* Copyright (C) 2006-2021 Raymond Bisdorff *')
print('* The module comes with ABSOLUTELY NO WARRANTY *')
print('* to the extent permitted by the applicable law. *')
print('* This is free software, and you are welcome to *')
print('* redistribute it if it remains free software. *')
print('****************************************************')
print('*-------- Testing classes and methods -------')
adA = RandomBipolarApprovalVotingProfile(numberOfVoters=4,
votersIdPrefix='a',
IntraGroup=True,
numberOfCandidates=5,
approvalProbability=0.3,
disapprovalProbability=0.3,
candidatesIdPrefix='b',
seed=1,Debug=True)
adA.showBipolarApprovals()
for a in adA.voters:
adA.showVoterBallot(a)
## adB = RandomBipolarApprovalVotingProfile(numberOfVoters=5,
## votersIdPrefix='b',
## approvalProbability=0.5,
## disapprovalProbability=0.5,
## numberOfCandidates=5,
## candidatesIdPrefix='a')
## pv = RandomPrerankedVotingProfile(numberOfVoters=5,
## numberOfCandidates=15,
## votersIdPrefix='a',
## candidatesIdPrefix='b',
## lengthProbability=0.3,
## votersWeights=[1,2,3,4,5],
## seed=1)
## pv.showPrerankedBallots()
k = 7
lvA = RandomLinearVotingProfile(numberOfVoters=k,numberOfCandidates=k,
votersIdPrefix='a',
candidatesIdPrefix='b',
PartialLinearBallots=True,
lengthProbability=0.5,
seed=50)
## lvB = RandomLinearVotingProfile(numberOfVoters=k,numberOfCandidates=k,
## votersIdPrefix='b',
## PartialLinearBallots=False,
## candidatesIdPrefix='a',seed=114)
##
## lvA.showLinearBallots()
## lvB.showLinearBallots()
## lvB.save('testpreorder')
## v = RandomBipolarApprovalVotingProfile(numberOfVoters=100,
## numberOfCandidates=10,
## approvalProbability=0.25,
## disapprovalProbability=0.25,
## seed=100,DivisivePolitics=True,
## WithPolls=True,Debug=False)
## print(v)
## v.showApprovalResults()
## v.showDisapprovalResults()
## v.showNetApprovalScores()
## v.save('testAV')
## v = BipolarApprovalVotingProfile('testAV')
## print(v)
## v.save2PerfTab('testODG')
## #v.showHTMLVotingHeatmap(rankingRule='NetFlows',Transposed=False)
## from outrankingDigraphs import BipolarOutrankingDigraph
## g = BipolarOutrankingDigraph('testODG')
## g.showHTMLPerformanceTableau(Transposed=True)
## print(g)
## v3 =
## RandomLinearVotingProfile(votersIdPrefix='m',candidatesIdPrefix='w')
## v3.showLinearBallots() m = MajorityMarginsDigraph(v3,Debug=True)
## print(m) m.showRelationTable() # m.showBestChoiceRecommendation()
print('*------------------*')
print('If you see this line all tests were passed successfully :-)')
print('Enjoy !')
print('*************************************')
print('* R.B. September 2008 *')
print('* $Revision: 2484 $ *')
print('*************************************')
#############################
# Log record for changes:
# $Log: votingDigraphs.py,v $#
#############################