vse_sim
Modern package facade for the VSE simulation library.
1"""Modern package facade for the VSE simulation library.""" 2 3from .data_classes import Method, SideTally, Tallies 4from .dataframe import ( 5 VseResults, 6 ballots_from_dataframe, 7 ballots_to_dataframe, 8 read_results_csv, 9 rows_to_dataframe, 10 scores_to_dataframe, 11 summarize_vse, 12 to_dataframe, 13 voter_to_dataframe, 14 voters_to_dataframe, 15) 16from .methods import ( 17 IRNR, 18 V321, 19 Borda, 20 BulletyApprovalWith, 21 Irv, 22 IrvPrime, 23 Mav, 24 Mj, 25 Plurality, 26 Rp, 27 Schulze, 28 Score, 29 Srv, 30) 31from .simulation import ( 32 CsvBatch, 33 allSystems, 34 baseRuns, 35 markMethods, 36 medianRuns, 37 run_simulation, 38 run_simulation_dataframe, 39 uniquify, 40) 41from .strategies import ( 42 Chooser, 43 LazyChooser, 44 OssChooser, 45 ProbChooser, 46 beHon, 47 beStrat, 48 beX, 49 biasedMediaFor, 50 biaserAround, 51 fuzzyMediaFor, 52 orderOf, 53 skewedMediaFor, 54 topNMediaFor, 55 truth, 56) 57from .voter_models import ( 58 DeterministicModel, 59 DimElectorate, 60 DimModel, 61 DimVoter, 62 Electorate, 63 KSElectorate, 64 KSModel, 65 PersonalityVoter, 66 PolyaModel, 67 QModel, 68 RandomModel, 69 ReverseModel, 70 Voter, 71) 72 73__version__ = "0.1.5" 74 75__all__ = [ 76 "__version__", 77 "Borda", 78 "BulletyApprovalWith", 79 "Chooser", 80 "CsvBatch", 81 "DeterministicModel", 82 "DimElectorate", 83 "DimModel", 84 "DimVoter", 85 "Electorate", 86 "IRNR", 87 "Irv", 88 "IrvPrime", 89 "KSElectorate", 90 "KSModel", 91 "LazyChooser", 92 "Mav", 93 "Method", 94 "Mj", 95 "OssChooser", 96 "PersonalityVoter", 97 "Plurality", 98 "PolyaModel", 99 "ProbChooser", 100 "QModel", 101 "RandomModel", 102 "ReverseModel", 103 "Rp", 104 "Schulze", 105 "Score", 106 "SideTally", 107 "Srv", 108 "Tallies", 109 "V321", 110 "VseResults", 111 "Voter", 112 "allSystems", 113 "ballots_from_dataframe", 114 "ballots_to_dataframe", 115 "baseRuns", 116 "beHon", 117 "beStrat", 118 "beX", 119 "biasedMediaFor", 120 "biaserAround", 121 "fuzzyMediaFor", 122 "markMethods", 123 "medianRuns", 124 "orderOf", 125 "read_results_csv", 126 "rows_to_dataframe", 127 "run_simulation", 128 "run_simulation_dataframe", 129 "scores_to_dataframe", 130 "skewedMediaFor", 131 "summarize_vse", 132 "to_dataframe", 133 "topNMediaFor", 134 "truth", 135 "uniquify", 136 "voter_to_dataframe", 137 "voters_to_dataframe", 138]
13class Borda(Method): 14 candScore = staticmethod(mean) 15 16 nRanks = 999 # infinity 17 18 @staticmethod 19 def fillPrefOrder( 20 voter, 21 ballot, 22 whichCands=None, # None means "all"; otherwise, an iterable of cand indexes 23 lowSlot=0, 24 nSlots=None, # again, None means "all" 25 remainderScore=None, # what to give candidates that don't fit in nSlots 26 ): 27 28 venum = list(enumerate(voter)) 29 if whichCands: 30 venum = [venum[c] for c in whichCands] 31 prefOrder = sorted(venum, key=lambda x: -x[1]) # high to low 32 Borda.fillCands(ballot, prefOrder, lowSlot, nSlots, remainderScore) 33 # modifies ballot argument, returns nothing. 34 35 @staticmethod 36 def fillCands( 37 ballot, 38 whichCands, # list of tuples starting with cand id, in descending order 39 lowSlot=0, 40 nSlots=None, # again, None means "all" 41 remainderScore=None, # what to give candidates that don't fit in nSlots 42 ): 43 if nSlots is None: 44 nSlots = len(whichCands) 45 cur = lowSlot + nSlots - 1 46 for i in range(nSlots): 47 ballot[whichCands[i][0]] = cur 48 cur -= 1 49 if remainderScore is not None: 50 i += 1 51 while i < len(whichCands): 52 ballot[whichCands[i][0]] = remainderScore 53 i += 1 54 # modifies ballot argument, returns nothing. 55 56 @staticmethod # cls is provided explicitly, not through binding 57 @rememberBallot 58 def honBallot(cls, utils): 59 ballot = [0] * len(utils) 60 cls.fillPrefOrder(utils, ballot) 61 return ballot 62 63 @classmethod 64 def fillStratBallot( 65 cls, 66 voter, 67 polls, 68 places, 69 n, 70 stratGap, 71 ballot, 72 frontId, 73 frontResult, 74 targId, 75 targResult, 76 ): 77 """Mutates the `ballot` argument to be a strategic ballot. 78 79 >>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1])) 80 [3, 0, 1, 2] 81 """ 82 nRanks = min(cls.nRanks, n) 83 if stratGap <= 0: 84 ballot[frontId], ballot[targId] = (nRanks - 1), 0 85 else: 86 ballot[frontId], ballot[targId] = 0, (nRanks - 1) 87 nRanks -= 2 88 if nRanks > 0: 89 cls.fillCands(ballot, places[2:][::-1], lowSlot=1, nSlots=nRanks, remainderScore=0) 90 # (don't) return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
Base class for election methods. Holds some of the duct tape.
18 @staticmethod 19 def fillPrefOrder( 20 voter, 21 ballot, 22 whichCands=None, # None means "all"; otherwise, an iterable of cand indexes 23 lowSlot=0, 24 nSlots=None, # again, None means "all" 25 remainderScore=None, # what to give candidates that don't fit in nSlots 26 ): 27 28 venum = list(enumerate(voter)) 29 if whichCands: 30 venum = [venum[c] for c in whichCands] 31 prefOrder = sorted(venum, key=lambda x: -x[1]) # high to low 32 Borda.fillCands(ballot, prefOrder, lowSlot, nSlots, remainderScore) 33 # modifies ballot argument, returns nothing.
35 @staticmethod 36 def fillCands( 37 ballot, 38 whichCands, # list of tuples starting with cand id, in descending order 39 lowSlot=0, 40 nSlots=None, # again, None means "all" 41 remainderScore=None, # what to give candidates that don't fit in nSlots 42 ): 43 if nSlots is None: 44 nSlots = len(whichCands) 45 cur = lowSlot + nSlots - 1 46 for i in range(nSlots): 47 ballot[whichCands[i][0]] = cur 48 cur -= 1 49 if remainderScore is not None: 50 i += 1 51 while i < len(whichCands): 52 ballot[whichCands[i][0]] = remainderScore 53 i += 1 54 # modifies ballot argument, returns nothing.
56 @staticmethod # cls is provided explicitly, not through binding 57 @rememberBallot 58 def honBallot(cls, utils): 59 ballot = [0] * len(utils) 60 cls.fillPrefOrder(utils, ballot) 61 return ballot
Takes utilities and returns an honest ballot
63 @classmethod 64 def fillStratBallot( 65 cls, 66 voter, 67 polls, 68 places, 69 n, 70 stratGap, 71 ballot, 72 frontId, 73 frontResult, 74 targId, 75 targResult, 76 ): 77 """Mutates the `ballot` argument to be a strategic ballot. 78 79 >>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1])) 80 [3, 0, 1, 2] 81 """ 82 nRanks = min(cls.nRanks, n) 83 if stratGap <= 0: 84 ballot[frontId], ballot[targId] = (nRanks - 1), 0 85 else: 86 ballot[frontId], ballot[targId] = 0, (nRanks - 1) 87 nRanks -= 2 88 if nRanks > 0: 89 cls.fillCands(ballot, places[2:][::-1], lowSlot=1, nSlots=nRanks, remainderScore=0) 90 # (don't) return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
Mutates the ballot argument to be a strategic ballot.
>>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
[3, 0, 1, 2]
248def BulletyApprovalWith(bullets=0.5, asClass=False): 249 250 class BulletyApproval((Score(1, True))): 251 bulletiness = bullets 252 253 def __str__(self): 254 return f"BulletyApproval{str(round(self.bulletiness * 100))}" 255 256 @staticmethod # cls is provided explicitly, not through binding 257 @rememberBallot 258 def honBallot(cls, utils): 259 """Takes utilities and returns an honest ballot (on 0..10) 260 261 262 honest ballots work as expected 263 >>> Score().honBallot(Score, Voter([5,6,7])) 264 [0.0, 5.0, 10.0] 265 >>> Score().resultsFor(DeterministicModel(3)(5,3),Score().honBallot)["results"] 266 [4.0, 6.0, 5.0] 267 """ 268 if random.random() > cls.bulletiness: 269 return cls.__bases__[0].honBallot(cls, utils) 270 best = max(utils) 271 return [1 if util == best else 0 for util in utils] 272 273 return BulletyApproval if asClass else BulletyApproval()
10class Chooser: 11 tallyKeys = [] 12 13 def __init__(self, choice=None, subChoosers=None): 14 """Subclasses should just copy/paste this logic because 15 each will have its own parameters and so that's easiest.""" 16 if choice is not None: 17 self.choice = choice 18 self.subChoosers = [] if subChoosers is None else subChoosers 19 20 def getName(self): 21 if hasattr(self, "choice"): # only true for base class 22 # print("base") 23 return self.choice 24 if not hasattr(self, "name") or not self.name: 25 # print("generic") 26 self.name = self.__class__.__name__[:-7] # drop the "Chooser" 27 # print("specific") 28 return self.name 29 30 def __call__(self, cls, voter, tally): 31 return self.choice 32 33 def addTallyKeys(self, tally): 34 for key in self.allTallyKeys: 35 tally[key] = 0 36 37 @cached_property 38 def myKeys(self): 39 prefix = f"{self.getName()}_" 40 return [prefix + key for key in self.tallyKeys] 41 42 @cached_property 43 def allTallyKeys(self): 44 keys = self.myKeys 45 for subChooser in self.subChoosers: 46 keys += subChooser.allTallyKeys 47 return keys 48 49 @cached_property 50 def __name__(self): 51 return self.__class__.__name__
13 def __init__(self, choice=None, subChoosers=None): 14 """Subclasses should just copy/paste this logic because 15 each will have its own parameters and so that's easiest.""" 16 if choice is not None: 17 self.choice = choice 18 self.subChoosers = [] if subChoosers is None else subChoosers
Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.
20 def getName(self): 21 if hasattr(self, "choice"): # only true for base class 22 # print("base") 23 return self.choice 24 if not hasattr(self, "name") or not self.name: 25 # print("generic") 26 self.name = self.__class__.__name__[:-7] # drop the "Chooser" 27 # print("specific") 28 return self.name
37class CsvBatch: 38 @autoassign 39 def __init__( 40 self, 41 model, 42 methods, 43 nvot, 44 ncand, 45 niter, 46 baseName=None, 47 media=truth, 48 seed=None, 49 force=False, 50 ): 51 """A harness function which creates niter elections from model and finds three kinds 52 of utility for all methods given. 53 54 for instance: 55 56 >>> csvs = CsvBatch(PolyaModel(), [[Score(), baseRuns], [Mav(), medianRuns]], nvot=5, ncand=4, niter=3) 57 >>> len(csvs.rows) 58 60 59 """ 60 rows = [] 61 emodel = str(model) 62 if seed is None: 63 seed = (baseName or "") + str(niter) 64 self.seed = seed 65 random.seed(seed) 66 try: 67 from git import Repo 68 69 repo = Repo(os.getcwd()) 70 if not force: 71 assert not repo.is_dirty() 72 self.repo_version = repo.head.commit.hexsha 73 except Exception: 74 self.repo_version = "unknown repo version" 75 for i in range(niter): 76 eid = uuid4() 77 electorate = model(nvot, ncand) 78 for method, chooserFuns in methods: 79 results = method.resultsTable( 80 eid, emodel, ncand, electorate, chooserFuns, media=media 81 ) 82 rows.extend(results) 83 debug(i, results[1:3]) 84 self.rows = rows 85 if baseName: 86 self.saveFile(baseName) 87 88 def saveFile(self, baseName="SimResults"): 89 """print the result of doVse in an accessible format. 90 for instance: 91 92 csvs.saveFile() 93 """ 94 i = 1 95 while os.path.isfile(baseName + str(i) + ".csv"): 96 i += 1 97 keys = ["vse", "method", "chooser", *list(self.rows[0].keys())] 98 for n in range(4): 99 keys.extend([f"tallyName{str(n)}", f"tallyVal{str(n)}"]) 100 keys = uniquify(keys) 101 path = baseName + str(i) + ".csv" 102 with open(path, "w") as myFile: 103 print( 104 f"# {dict(media=self.media.__name__, version=self.repo_version, seed=self.seed, model=self.model, methods=self.methods, nvot=self.nvot, ncand=self.ncand, niter=self.niter)}", 105 file=myFile, 106 ) 107 108 dw = csv.DictWriter(myFile, keys, restval="NA") 109 dw.writeheader() 110 for r in self.rows: 111 dw.writerow(r) 112 return path 113 114 @property 115 def results(self): 116 """Return this batch as a pandas-backed ``VseResults`` object.""" 117 from .dataframe import VseResults 118 119 return VseResults.from_rows(self.rows) 120 121 @property 122 def dataframe(self): 123 """Return this batch's rows as a pandas DataFrame.""" 124 return self.to_dataframe() 125 126 @property 127 def df(self): 128 """Alias for ``dataframe``.""" 129 return self.dataframe 130 131 def to_dataframe(self, copy=True): 132 """Return this batch's rows as a pandas DataFrame.""" 133 return self.results.to_dataframe(copy=copy) 134 135 def summarize(self, group_by=("method", "chooser"), sort_by="mean_vse", ascending=False): 136 """Return a pandas DataFrame summarizing VSE scores for this batch.""" 137 return self.results.summarize( 138 group_by=group_by, 139 sort_by=sort_by, 140 ascending=ascending, 141 ) 142 143 def report(self, group_by=("method", "chooser")): 144 """Return common pandas report tables for this batch.""" 145 return self.results.report(group_by=group_by) 146 147 def plot_vse(self, *args, **kwargs): 148 """Plot summarized VSE scores for this batch.""" 149 return self.results.plot_vse(*args, **kwargs)
38 @autoassign 39 def __init__( 40 self, 41 model, 42 methods, 43 nvot, 44 ncand, 45 niter, 46 baseName=None, 47 media=truth, 48 seed=None, 49 force=False, 50 ): 51 """A harness function which creates niter elections from model and finds three kinds 52 of utility for all methods given. 53 54 for instance: 55 56 >>> csvs = CsvBatch(PolyaModel(), [[Score(), baseRuns], [Mav(), medianRuns]], nvot=5, ncand=4, niter=3) 57 >>> len(csvs.rows) 58 60 59 """ 60 rows = [] 61 emodel = str(model) 62 if seed is None: 63 seed = (baseName or "") + str(niter) 64 self.seed = seed 65 random.seed(seed) 66 try: 67 from git import Repo 68 69 repo = Repo(os.getcwd()) 70 if not force: 71 assert not repo.is_dirty() 72 self.repo_version = repo.head.commit.hexsha 73 except Exception: 74 self.repo_version = "unknown repo version" 75 for i in range(niter): 76 eid = uuid4() 77 electorate = model(nvot, ncand) 78 for method, chooserFuns in methods: 79 results = method.resultsTable( 80 eid, emodel, ncand, electorate, chooserFuns, media=media 81 ) 82 rows.extend(results) 83 debug(i, results[1:3]) 84 self.rows = rows 85 if baseName: 86 self.saveFile(baseName)
A harness function which creates niter elections from model and finds three kinds of utility for all methods given.
for instance:
>>> csvs = CsvBatch(PolyaModel(), [[Score(), baseRuns], [Mav(), medianRuns]], nvot=5, ncand=4, niter=3)
>>> len(csvs.rows)
60
88 def saveFile(self, baseName="SimResults"): 89 """print the result of doVse in an accessible format. 90 for instance: 91 92 csvs.saveFile() 93 """ 94 i = 1 95 while os.path.isfile(baseName + str(i) + ".csv"): 96 i += 1 97 keys = ["vse", "method", "chooser", *list(self.rows[0].keys())] 98 for n in range(4): 99 keys.extend([f"tallyName{str(n)}", f"tallyVal{str(n)}"]) 100 keys = uniquify(keys) 101 path = baseName + str(i) + ".csv" 102 with open(path, "w") as myFile: 103 print( 104 f"# {dict(media=self.media.__name__, version=self.repo_version, seed=self.seed, model=self.model, methods=self.methods, nvot=self.nvot, ncand=self.ncand, niter=self.niter)}", 105 file=myFile, 106 ) 107 108 dw = csv.DictWriter(myFile, keys, restval="NA") 109 dw.writeheader() 110 for r in self.rows: 111 dw.writerow(r) 112 return path
print the result of doVse in an accessible format. for instance:
csvs.saveFile()
114 @property 115 def results(self): 116 """Return this batch as a pandas-backed ``VseResults`` object.""" 117 from .dataframe import VseResults 118 119 return VseResults.from_rows(self.rows)
Return this batch as a pandas-backed VseResults object.
121 @property 122 def dataframe(self): 123 """Return this batch's rows as a pandas DataFrame.""" 124 return self.to_dataframe()
Return this batch's rows as a pandas DataFrame.
131 def to_dataframe(self, copy=True): 132 """Return this batch's rows as a pandas DataFrame.""" 133 return self.results.to_dataframe(copy=copy)
Return this batch's rows as a pandas DataFrame.
135 def summarize(self, group_by=("method", "chooser"), sort_by="mean_vse", ascending=False): 136 """Return a pandas DataFrame summarizing VSE scores for this batch.""" 137 return self.results.summarize( 138 group_by=group_by, 139 sort_by=sort_by, 140 ascending=ascending, 141 )
Return a pandas DataFrame summarizing VSE scores for this batch.
179class DeterministicModel(RandomModel): 180 """Basically, a somewhat non-boring stub for testing. 181 182 >>> DeterministicModel(3)(4, 3) 183 [(0, 1, 2), (1, 2, 0), (2, 0, 1), (0, 1, 2)] 184 """ 185 186 @autoassign 187 def __init__(self, modulo): 188 pass 189 190 def __call__(self, nvot, ncand, vType=PersonalityVoter): 191 return Electorate(vType((i + j) % self.modulo for i in range(ncand)) for j in range(nvot))
Basically, a somewhat non-boring stub for testing.
>>> DeterministicModel(3)(4, 3)
[(0, 1, 2), (1, 2, 0), (2, 0, 1), (0, 1, 2)]
Inherited Members
299class DimElectorate(Electorate): 300 def asDims(self, v, *args): 301 return v 302 303 def fromDims(self, dimvoters, vType): 304 for v in dimvoters: 305 self.append(vType.fromDims(v, self)) 306 307 def calcTotWeight(self): 308 self.totWeight = sum(w**2 for w in self.dimWeights)
A list of voters. Each voter is a list of candidate utilities
Inherited Members
311class DimModel(RandomModel): 312 """ 313 314 >>> dm = DimModel(2,baseElectorate=DeterministicModel(3)) 315 >>> dm(2,4) 316 [(-1.8439088914585775, -0.0, -1.0, -1.8439088914585775), (-1.2649110640673518, -1.0, -0.0, -1.2649110640673518)] 317 >>> dm.dimWeights 318 [1, 0.5] 319 320 321 """ 322 323 builtElectorate = DimElectorate 324 325 @autoassign 326 def __init__(self, ndims=3, dimWeights=None, baseElectorate=RandomModel()): 327 if self.dimWeights is None: 328 self.dimWeights = [2 ** (-n) for n in range(ndims)] 329 assert len(self.dimWeights) == self.ndims 330 331 def __call__(self, nvot, ncand, vType=DimVoter): 332 elec = self.builtElectorate() 333 elec.dimWeights = self.dimWeights 334 return self.makeElectorate(elec, nvot, ncand, vType) 335 336 def makeElectorate(self, elec, nvot, ncand, vType): 337 elec.calcTotWeight() 338 votersncands = self.baseElectorate(nvot + ncand, len(elec.dimWeights), vType) 339 elec.base = [elec.asDims(v, i) for i, v in enumerate(votersncands[:nvot])] 340 elec.cands = [elec.asDims(v, nvot + i) for i, v in enumerate(votersncands[nvot:])] 341 elec.fromDims(elec.base, vType) 342 return elec
>>> dm = DimModel(2,baseElectorate=DeterministicModel(3))
>>> dm(2,4)
[(-1.8439088914585775, -0.0, -1.0, -1.8439088914585775), (-1.2649110640673518, -1.0, -0.0, -1.2649110640673518)]
>>> dm.dimWeights
[1, 0.5]
336 def makeElectorate(self, elec, nvot, ncand, vType): 337 elec.calcTotWeight() 338 votersncands = self.baseElectorate(nvot + ncand, len(elec.dimWeights), vType) 339 elec.base = [elec.asDims(v, i) for i, v in enumerate(votersncands[:nvot])] 340 elec.cands = [elec.asDims(v, nvot + i) for i, v in enumerate(votersncands[nvot:])] 341 elec.fromDims(elec.base, vType) 342 return elec
Inherited Members
273class DimVoter(PersonalityVoter): 274 """A voter in an n-dimensional model.""" 275 276 @classmethod 277 def fromDims(cls, v, e, caring=None): 278 if caring is None: 279 caring = [1] * len(v) 280 totCaring = e.totWeight 281 else: 282 totCaring = sum((c * w) ** 2 for c, w in zip(caring, e.dimWeights)) 283 me = cls( 284 -sqrt( 285 sum( 286 ((vd - cd) * w * cares) ** 2 287 for (vd, cd, w, cares) in zip(v, c, e.dimWeights, caring) 288 ) 289 / totCaring 290 ) 291 for c in e.cands 292 ) 293 me.copyAttrsFrom(v) 294 me.dims = v 295 me.elec = e 296 return me
A voter in an n-dimensional model.
276 @classmethod 277 def fromDims(cls, v, e, caring=None): 278 if caring is None: 279 caring = [1] * len(v) 280 totCaring = e.totWeight 281 else: 282 totCaring = sum((c * w) ** 2 for c, w in zip(caring, e.dimWeights)) 283 me = cls( 284 -sqrt( 285 sum( 286 ((vd - cd) * w * cares) ** 2 287 for (vd, cd, w, cares) in zip(v, c, e.dimWeights, caring) 288 ) 289 / totCaring 290 ) 291 for c in e.cands 292 ) 293 me.copyAttrsFrom(v) 294 me.dims = v 295 me.elec = e 296 return me
124class Electorate(list): 125 """A list of voters. 126 Each voter is a list of candidate utilities""" 127 128 @property 129 def dataframe(self): 130 """Return voter utilities as a tidy DataFrame.""" 131 return self.to_dataframe() 132 133 @property 134 def df(self): 135 """Alias for ``dataframe``.""" 136 return self.dataframe 137 138 @property 139 def wide_dataframe(self): 140 """Return one row per voter with one utility column per candidate.""" 141 return self.to_dataframe(wide=True) 142 143 @cached_property 144 def socUtils(self): 145 """Return mean utility across electorate for each candidate: their social utilities. 146 147 >>> e = Electorate([[1,2],[3,4]]) 148 >>> e.socUtils 149 [2.0, 3.0] 150 """ 151 return list(map(mean, zip(*self))) 152 153 def to_dataframe(self, wide=False, **kwargs): 154 """Return voter utilities as a tidy or wide DataFrame.""" 155 from .dataframe import voters_to_dataframe 156 157 return voters_to_dataframe(self, wide=wide, **kwargs)
A list of voters. Each voter is a list of candidate utilities
128 @property 129 def dataframe(self): 130 """Return voter utilities as a tidy DataFrame.""" 131 return self.to_dataframe()
Return voter utilities as a tidy DataFrame.
138 @property 139 def wide_dataframe(self): 140 """Return one row per voter with one utility column per candidate.""" 141 return self.to_dataframe(wide=True)
Return one row per voter with one utility column per candidate.
143 @cached_property 144 def socUtils(self): 145 """Return mean utility across electorate for each candidate: their social utilities. 146 147 >>> e = Electorate([[1,2],[3,4]]) 148 >>> e.socUtils 149 [2.0, 3.0] 150 """ 151 return list(map(mean, zip(*self)))
Return mean utility across electorate for each candidate: their social utilities.
>>> e = Electorate([[1,2],[3,4]])
>>> e.socUtils
[2.0, 3.0]
153 def to_dataframe(self, wide=False, **kwargs): 154 """Return voter utilities as a tidy or wide DataFrame.""" 155 from .dataframe import voters_to_dataframe 156 157 return voters_to_dataframe(self, wide=wide, **kwargs)
Return voter utilities as a tidy or wide DataFrame.
1107class IRNR(RankedMethod): 1108 stratMax = 10 1109 1110 stratTargetFor = Method.stratTarget3 # strategize in favor of third place, because second place is pointless (can't change pairwise) 1111 1112 def results(self, ballots, **kwargs): 1113 ballots = ballots_from_dataframe(ballots) 1114 enabled = [True] * len(ballots[0]) 1115 numEnabled = sum(enabled) 1116 results = [None] * len(enabled) 1117 while numEnabled > 1: 1118 tsum = [0.0] * len(enabled) 1119 for bal in ballots: 1120 vsum = 0.0 1121 for i, v in enumerate(bal): 1122 if enabled[i]: 1123 vsum += abs(v) 1124 if vsum == 0.0: 1125 # TODO: count spoiled ballot 1126 continue 1127 for i, v in enumerate(bal): 1128 if enabled[i]: 1129 tsum[i] += v / vsum 1130 mini = None 1131 minv = None 1132 for i, v in enumerate(tsum): 1133 if enabled[i] and ((minv is None) or (tsum[i] < minv)): 1134 minv = tsum[i] 1135 mini = i 1136 enabled[mini] = False 1137 results[mini] = minv 1138 numEnabled -= 1 1139 for i, v in enumerate(tsum): 1140 if enabled[i]: 1141 results[i] = tsum[i] 1142 return results 1143 1144 @staticmethod # cls is provided explicitly, not through binding 1145 @rememberBallot 1146 def honBallot(cls, utils): 1147 """Takes utilities and returns an honest ballot""" 1148 return utils 1149 1150 @classmethod 1151 def fillStratBallot( 1152 cls, 1153 voter, 1154 polls, 1155 places, 1156 n, 1157 stratGap, 1158 ballot, 1159 frontId, 1160 frontResult, 1161 targId, 1162 targResult, 1163 ): 1164 if stratGap <= 0: 1165 ballot[frontId], ballot[targId] = cls.stratMax, 0 1166 else: 1167 ballot[frontId], ballot[targId] = 0, cls.stratMax 1168 cls.fillPrefOrder( 1169 voter, 1170 ballot, 1171 whichCands=[c for (c, r) in places[2:]], 1172 nSlots=1, 1173 lowSlot=1, 1174 remainderScore=0, 1175 )
Base class for election methods. Holds some of the duct tape.
1112 def results(self, ballots, **kwargs): 1113 ballots = ballots_from_dataframe(ballots) 1114 enabled = [True] * len(ballots[0]) 1115 numEnabled = sum(enabled) 1116 results = [None] * len(enabled) 1117 while numEnabled > 1: 1118 tsum = [0.0] * len(enabled) 1119 for bal in ballots: 1120 vsum = 0.0 1121 for i, v in enumerate(bal): 1122 if enabled[i]: 1123 vsum += abs(v) 1124 if vsum == 0.0: 1125 # TODO: count spoiled ballot 1126 continue 1127 for i, v in enumerate(bal): 1128 if enabled[i]: 1129 tsum[i] += v / vsum 1130 mini = None 1131 minv = None 1132 for i, v in enumerate(tsum): 1133 if enabled[i] and ((minv is None) or (tsum[i] < minv)): 1134 minv = tsum[i] 1135 mini = i 1136 enabled[mini] = False 1137 results[mini] = minv 1138 numEnabled -= 1 1139 for i, v in enumerate(tsum): 1140 if enabled[i]: 1141 results[i] = tsum[i] 1142 return results
Combines ballots into results. Override for comparative methods.
Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index.
Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index.
Test for subclasses, makes no sense to test this method in the abstract base class.
1144 @staticmethod # cls is provided explicitly, not through binding 1145 @rememberBallot 1146 def honBallot(cls, utils): 1147 """Takes utilities and returns an honest ballot""" 1148 return utils
Takes utilities and returns an honest ballot
1150 @classmethod 1151 def fillStratBallot( 1152 cls, 1153 voter, 1154 polls, 1155 places, 1156 n, 1157 stratGap, 1158 ballot, 1159 frontId, 1160 frontResult, 1161 targId, 1162 targResult, 1163 ): 1164 if stratGap <= 0: 1165 ballot[frontId], ballot[targId] = cls.stratMax, 0 1166 else: 1167 ballot[frontId], ballot[targId] = 0, cls.stratMax 1168 cls.fillPrefOrder( 1169 voter, 1170 ballot, 1171 whichCands=[c for (c, r) in places[2:]], 1172 nSlots=1, 1173 lowSlot=1, 1174 remainderScore=0, 1175 )
Mutates the ballot argument to be a strategic ballot.
>>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
[3, 0, 1, 2]
513class Irv(Method): 514 """ 515 IRV. 516 517 Ballots are ordered candidate rankings, from most to least preferred. 518 Results are candidate-indexed elimination ranks, with higher numbers better. 519 """ 520 521 stratTargetFor = Method.stratTarget3 522 523 def buildPreferenceSchedule(self, ballots): 524 """Gets a dictionary of the form {ranking as tuple, vote count}""" 525 526 prefs = {} 527 for b in ballots: 528 key = tuple(b) 529 if key in prefs: 530 prefs[key] += 1 531 else: 532 prefs[key] = 1 533 return prefs 534 535 def eliminateCandidate(self, inputPrefs, toEliminate): 536 """Gets a dictionary of the form {ranking as tuple, vote count} with toEliminate removed""" 537 538 if not isinstance(toEliminate, CandidateWithCount): 539 return inputPrefs 540 541 prefs = {} 542 for ranking, votes in inputPrefs.items(): 543 newranking = [candidate for candidate in ranking if candidate != toEliminate.candidate] 544 545 if not newranking: 546 continue 547 newkey = tuple(newranking) 548 if newkey in prefs: 549 prefs[newkey] += votes 550 else: 551 prefs[newkey] = votes 552 return prefs 553 554 def candidateVotes(self, prefSchedule): 555 """Gets a list of CandidateWithCount, from highest to lowest""" 556 candidates = {} 557 for ranking, votes in prefSchedule.items(): 558 candidate = ranking[0] 559 if candidate in candidates: 560 candidates[candidate].votes += votes 561 else: 562 candidates[candidate] = CandidateWithCount(candidate, votes) 563 564 # Simply for VSE which requires ranking of non-winners; in real election we don't really 565 # care 566 alternates = [] 567 trackedalt = set() 568 for ranking, votes in prefSchedule.items(): 569 for alternate in ranking[1:]: 570 if (alternate not in candidates) and alternate not in trackedalt: 571 alternates.append(CandidateWithCount(alternate, 0)) 572 trackedalt.add(alternate) 573 574 return ( 575 sorted(candidates.values(), key=lambda c: (c.votes, c.candidate), reverse=True) 576 + alternates 577 ) 578 579 def getLeast(self, voteRanking, keep=None): 580 if keep is None: 581 keep = {} 582 for candidate in reversed(voteRanking): 583 if candidate.candidate not in keep: 584 return candidate 585 return None 586 587 @staticmethod 588 def scoresFromEliminations(eliminated, ncand): 589 """Convert an IRV elimination order to candidate-indexed scores.""" 590 results = [-1] * ncand 591 for score, candidate in enumerate(eliminated): 592 results[candidate] = score 593 return results 594 595 def runIrv(self, remaining, ncand): 596 """IRV results.""" 597 eliminated = [] 598 for _ in range(ncand): 599 votes = self.candidateVotes(remaining) 600 toEliminate = self.getLeast(votes) 601 if not isinstance(toEliminate, CandidateWithCount): 602 break 603 eliminated.append(toEliminate.candidate) 604 remaining = self.eliminateCandidate(remaining, toEliminate) 605 return self.scoresFromEliminations(eliminated, ncand) 606 607 def results(self, ballots, **kwargs): 608 """IRV results. 609 610 >>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"] 611 [0, 1, 2] 612 >>> Irv().results([[0,1,2]])[2] 613 0 614 >>> Irv().results([[0,1,2],[2,1,0]])[1] 615 0 616 >>> Irv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2) 617 [1, 0, 2] 618 """ 619 ballots = ballots_from_dataframe(ballots) 620 return self.runIrv(self.buildPreferenceSchedule(ballots), len(ballots[0])) 621 622 @staticmethod # cls is provided explicitly, not through binding 623 @rememberBallot 624 def honBallot(cls, voter): 625 """Takes utilities and returns an honest ballot 626 627 >>> Irv.honBallot(Irv,Voter([4,1,6,3])) 628 [2, 0, 3, 1] 629 >>> Irv.honBallot(Irv,Voter([0,1,2])) 630 [2, 1, 0] 631 """ 632 order = sorted(enumerate(voter), key=lambda x: (-x[1], x[0])) 633 return [candidate for candidate, _utility in order] 634 635 @classmethod 636 def fillStratBallot( 637 cls, 638 voter, 639 polls, 640 places, 641 n, 642 stratGap, 643 ballot, 644 frontId, 645 frontResult, 646 targId, 647 targResult, 648 ): 649 """ 650 >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2])) 651 [2, 1, 0, 3] 652 """ 653 i = n - 1 654 winnerQ = voter[frontId] 655 targQ = voter[targId] 656 placesToFill = list(range(n - 1, 0, -1)) 657 if targQ > winnerQ: 658 ballot[targId] = i 659 i -= 1 660 del placesToFill[-2] 661 for j in placesToFill: 662 nextLoser, loserScore = places[j] # all but winner, low to high 663 if voter[nextLoser] > winnerQ: 664 ballot[nextLoser] = i 665 i -= 1 666 ballot[frontId] = i 667 i -= 1 668 for j in placesToFill: 669 nextLoser, loserScore = places[j] 670 if voter[nextLoser] <= winnerQ: 671 ballot[nextLoser] = i 672 i -= 1 673 assert i == -1 674 ballot[:] = [ 675 candidate for candidate, _rank in sorted(enumerate(ballot), key=lambda x: -x[1]) 676 ]
IRV.
Ballots are ordered candidate rankings, from most to least preferred. Results are candidate-indexed elimination ranks, with higher numbers better.
523 def buildPreferenceSchedule(self, ballots): 524 """Gets a dictionary of the form {ranking as tuple, vote count}""" 525 526 prefs = {} 527 for b in ballots: 528 key = tuple(b) 529 if key in prefs: 530 prefs[key] += 1 531 else: 532 prefs[key] = 1 533 return prefs
Gets a dictionary of the form {ranking as tuple, vote count}
535 def eliminateCandidate(self, inputPrefs, toEliminate): 536 """Gets a dictionary of the form {ranking as tuple, vote count} with toEliminate removed""" 537 538 if not isinstance(toEliminate, CandidateWithCount): 539 return inputPrefs 540 541 prefs = {} 542 for ranking, votes in inputPrefs.items(): 543 newranking = [candidate for candidate in ranking if candidate != toEliminate.candidate] 544 545 if not newranking: 546 continue 547 newkey = tuple(newranking) 548 if newkey in prefs: 549 prefs[newkey] += votes 550 else: 551 prefs[newkey] = votes 552 return prefs
Gets a dictionary of the form {ranking as tuple, vote count} with toEliminate removed
554 def candidateVotes(self, prefSchedule): 555 """Gets a list of CandidateWithCount, from highest to lowest""" 556 candidates = {} 557 for ranking, votes in prefSchedule.items(): 558 candidate = ranking[0] 559 if candidate in candidates: 560 candidates[candidate].votes += votes 561 else: 562 candidates[candidate] = CandidateWithCount(candidate, votes) 563 564 # Simply for VSE which requires ranking of non-winners; in real election we don't really 565 # care 566 alternates = [] 567 trackedalt = set() 568 for ranking, votes in prefSchedule.items(): 569 for alternate in ranking[1:]: 570 if (alternate not in candidates) and alternate not in trackedalt: 571 alternates.append(CandidateWithCount(alternate, 0)) 572 trackedalt.add(alternate) 573 574 return ( 575 sorted(candidates.values(), key=lambda c: (c.votes, c.candidate), reverse=True) 576 + alternates 577 )
Gets a list of CandidateWithCount, from highest to lowest
587 @staticmethod 588 def scoresFromEliminations(eliminated, ncand): 589 """Convert an IRV elimination order to candidate-indexed scores.""" 590 results = [-1] * ncand 591 for score, candidate in enumerate(eliminated): 592 results[candidate] = score 593 return results
Convert an IRV elimination order to candidate-indexed scores.
595 def runIrv(self, remaining, ncand): 596 """IRV results.""" 597 eliminated = [] 598 for _ in range(ncand): 599 votes = self.candidateVotes(remaining) 600 toEliminate = self.getLeast(votes) 601 if not isinstance(toEliminate, CandidateWithCount): 602 break 603 eliminated.append(toEliminate.candidate) 604 remaining = self.eliminateCandidate(remaining, toEliminate) 605 return self.scoresFromEliminations(eliminated, ncand)
IRV results.
607 def results(self, ballots, **kwargs): 608 """IRV results. 609 610 >>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"] 611 [0, 1, 2] 612 >>> Irv().results([[0,1,2]])[2] 613 0 614 >>> Irv().results([[0,1,2],[2,1,0]])[1] 615 0 616 >>> Irv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2) 617 [1, 0, 2] 618 """ 619 ballots = ballots_from_dataframe(ballots) 620 return self.runIrv(self.buildPreferenceSchedule(ballots), len(ballots[0]))
IRV results.
>>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
[0, 1, 2]
>>> Irv().results([[0,1,2]])[2]
0
>>> Irv().results([[0,1,2],[2,1,0]])[1]
0
>>> Irv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[1, 0, 2]
622 @staticmethod # cls is provided explicitly, not through binding 623 @rememberBallot 624 def honBallot(cls, voter): 625 """Takes utilities and returns an honest ballot 626 627 >>> Irv.honBallot(Irv,Voter([4,1,6,3])) 628 [2, 0, 3, 1] 629 >>> Irv.honBallot(Irv,Voter([0,1,2])) 630 [2, 1, 0] 631 """ 632 order = sorted(enumerate(voter), key=lambda x: (-x[1], x[0])) 633 return [candidate for candidate, _utility in order]
Takes utilities and returns an honest ballot
>>> Irv.honBallot(Irv,Voter([4,1,6,3]))
[2, 0, 3, 1]
>>> Irv.honBallot(Irv,Voter([0,1,2]))
[2, 1, 0]
635 @classmethod 636 def fillStratBallot( 637 cls, 638 voter, 639 polls, 640 places, 641 n, 642 stratGap, 643 ballot, 644 frontId, 645 frontResult, 646 targId, 647 targResult, 648 ): 649 """ 650 >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2])) 651 [2, 1, 0, 3] 652 """ 653 i = n - 1 654 winnerQ = voter[frontId] 655 targQ = voter[targId] 656 placesToFill = list(range(n - 1, 0, -1)) 657 if targQ > winnerQ: 658 ballot[targId] = i 659 i -= 1 660 del placesToFill[-2] 661 for j in placesToFill: 662 nextLoser, loserScore = places[j] # all but winner, low to high 663 if voter[nextLoser] > winnerQ: 664 ballot[nextLoser] = i 665 i -= 1 666 ballot[frontId] = i 667 i -= 1 668 for j in placesToFill: 669 nextLoser, loserScore = places[j] 670 if voter[nextLoser] <= winnerQ: 671 ballot[nextLoser] = i 672 i -= 1 673 assert i == -1 674 ballot[:] = [ 675 candidate for candidate, _rank in sorted(enumerate(ballot), key=lambda x: -x[1]) 676 ]
>>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
[2, 1, 0, 3]
679class IrvPrime(Irv): 680 """ 681 IRV Prime. 682 683 See https://electowiki.org/wiki/IRV_Prime 684 """ 685 686 stratTargetFor = Method.stratTarget3 687 688 def results(self, ballots, **kwargs): 689 """IRV Prime results. 690 691 >>> IrvPrime().results([[0,1,2]])[2] 692 0 693 >>> IrvPrime().results([[0,1,2],[2,1,0]])[1] 694 0 695 >>> IrvPrime().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2) 696 [0, 2, 1] 697 >>> IrvPrime().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100) 698 [1, 2, 0] 699 >>> # Favorite betrayal example from http://rangevoting.org/IncentToExagg.html 700 >>> IrvPrime().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5) 701 [2, 1, 0] 702 >>> IrvPrime().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6) 703 [1, 0, 3, 2, 4] 704 >>> # Elections 3-5 from http://votingmatters.org.uk/ISSUE6/P4.HTM 705 >>> IrvPrime().results([[0,1,2,3,4,5]] * 12 + [[2,0,1,3,4,5]] * 11 + [[1,2,0,3,4,5]] * 10 + 706 ... [[3,4,5]] * 27) 707 [2, 5, 4, 3, 1, 0] 708 >>> IrvPrime().results([[0,1]] * 11 + [[1]] * 7 + [[2]] * 12) 709 [0, 2, 1] 710 >>> IrvPrime().results([[0,3,2,1]] * 5 + [[1,2,0,3]] * 5 + [[2,0,1,3]] * 8 + 711 ... [[3,0,1,2]] * 4 + [[3,1,2,0]] * 8) 712 [3, 0, 1, 2] 713 >>> IrvPrime().results([[0,2,1,3]] * 6 + [[0,3,1,2]] * 3 + [[0,3,2,1]] * 3 + 714 ... [[1,2,0,3]] * 4 + [[2,0,1,3]] * 4 + [[3,1,2,0]] * 5) 715 [2, 0, 3, 1] 716 >>> # Failure of later-no-harm 717 >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 + 718 ... [[1,0,2]] * 21 + [[2,0,1]] * 30 + [[2,1,0]] * 20) 719 [1, 0, 2] 720 >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 + 721 ... [[1,0,2]] * 21 + [[2,1,0]] * 30 + [[2,1,0]] * 20) 722 [1, 2, 0] 723 """ 724 725 ballots = ballots_from_dataframe(ballots) 726 727 remaining = self.buildPreferenceSchedule(ballots) 728 ncand = len(self.candidateVotes(remaining)) 729 classic = self.runIrv(remaining, ncand) 730 731 # Keep the winner from the classic IRV 732 winners = {self.winner(classic)} 733 734 # Find all candidates that can beat classic IRV winner; this may be a superset 735 # of schwartz/smith, but it's all that matters 736 winnersPrime = set() 737 for possibleWinner in range(ncand): 738 if possibleWinner in winners: 739 continue 740 741 numWins = 0 742 numLosses = 0 743 for ranking, votes in remaining.items(): 744 possibleWinnerRanking = winnerRanking = len(ranking) + 1 745 for pos in range(len(ranking)): 746 if ranking[pos] == possibleWinner: 747 possibleWinnerRanking = pos 748 # We can change this to a loop if there's > 1 winner 749 elif ranking[pos] == next(iter(winners)): 750 winnerRanking = pos 751 if possibleWinnerRanking < winnerRanking: 752 numWins += votes 753 elif winnerRanking < possibleWinnerRanking: 754 numLosses += votes 755 if numWins > numLosses: 756 winnersPrime.add(possibleWinner) 757 758 # Now re-run IRV preserving all winners + winners prime 759 keepers = winners.union(winnersPrime) 760 eliminated = [] 761 for _ in range(ncand): 762 votes = self.candidateVotes(remaining) 763 toEliminate = self.getLeast(votes, keepers) 764 if not isinstance(toEliminate, CandidateWithCount): 765 # Begin "step 4", i.e. continue elimination without preserving anyone 766 keepers = {} 767 toEliminate = self.getLeast(votes) 768 if not isinstance(toEliminate, CandidateWithCount): 769 break 770 eliminated.append(toEliminate.candidate) 771 remaining = self.eliminateCandidate(remaining, toEliminate) 772 773 return self.scoresFromEliminations(eliminated, ncand)
IRV Prime.
688 def results(self, ballots, **kwargs): 689 """IRV Prime results. 690 691 >>> IrvPrime().results([[0,1,2]])[2] 692 0 693 >>> IrvPrime().results([[0,1,2],[2,1,0]])[1] 694 0 695 >>> IrvPrime().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2) 696 [0, 2, 1] 697 >>> IrvPrime().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100) 698 [1, 2, 0] 699 >>> # Favorite betrayal example from http://rangevoting.org/IncentToExagg.html 700 >>> IrvPrime().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5) 701 [2, 1, 0] 702 >>> IrvPrime().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6) 703 [1, 0, 3, 2, 4] 704 >>> # Elections 3-5 from http://votingmatters.org.uk/ISSUE6/P4.HTM 705 >>> IrvPrime().results([[0,1,2,3,4,5]] * 12 + [[2,0,1,3,4,5]] * 11 + [[1,2,0,3,4,5]] * 10 + 706 ... [[3,4,5]] * 27) 707 [2, 5, 4, 3, 1, 0] 708 >>> IrvPrime().results([[0,1]] * 11 + [[1]] * 7 + [[2]] * 12) 709 [0, 2, 1] 710 >>> IrvPrime().results([[0,3,2,1]] * 5 + [[1,2,0,3]] * 5 + [[2,0,1,3]] * 8 + 711 ... [[3,0,1,2]] * 4 + [[3,1,2,0]] * 8) 712 [3, 0, 1, 2] 713 >>> IrvPrime().results([[0,2,1,3]] * 6 + [[0,3,1,2]] * 3 + [[0,3,2,1]] * 3 + 714 ... [[1,2,0,3]] * 4 + [[2,0,1,3]] * 4 + [[3,1,2,0]] * 5) 715 [2, 0, 3, 1] 716 >>> # Failure of later-no-harm 717 >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 + 718 ... [[1,0,2]] * 21 + [[2,0,1]] * 30 + [[2,1,0]] * 20) 719 [1, 0, 2] 720 >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 + 721 ... [[1,0,2]] * 21 + [[2,1,0]] * 30 + [[2,1,0]] * 20) 722 [1, 2, 0] 723 """ 724 725 ballots = ballots_from_dataframe(ballots) 726 727 remaining = self.buildPreferenceSchedule(ballots) 728 ncand = len(self.candidateVotes(remaining)) 729 classic = self.runIrv(remaining, ncand) 730 731 # Keep the winner from the classic IRV 732 winners = {self.winner(classic)} 733 734 # Find all candidates that can beat classic IRV winner; this may be a superset 735 # of schwartz/smith, but it's all that matters 736 winnersPrime = set() 737 for possibleWinner in range(ncand): 738 if possibleWinner in winners: 739 continue 740 741 numWins = 0 742 numLosses = 0 743 for ranking, votes in remaining.items(): 744 possibleWinnerRanking = winnerRanking = len(ranking) + 1 745 for pos in range(len(ranking)): 746 if ranking[pos] == possibleWinner: 747 possibleWinnerRanking = pos 748 # We can change this to a loop if there's > 1 winner 749 elif ranking[pos] == next(iter(winners)): 750 winnerRanking = pos 751 if possibleWinnerRanking < winnerRanking: 752 numWins += votes 753 elif winnerRanking < possibleWinnerRanking: 754 numLosses += votes 755 if numWins > numLosses: 756 winnersPrime.add(possibleWinner) 757 758 # Now re-run IRV preserving all winners + winners prime 759 keepers = winners.union(winnersPrime) 760 eliminated = [] 761 for _ in range(ncand): 762 votes = self.candidateVotes(remaining) 763 toEliminate = self.getLeast(votes, keepers) 764 if not isinstance(toEliminate, CandidateWithCount): 765 # Begin "step 4", i.e. continue elimination without preserving anyone 766 keepers = {} 767 toEliminate = self.getLeast(votes) 768 if not isinstance(toEliminate, CandidateWithCount): 769 break 770 eliminated.append(toEliminate.candidate) 771 remaining = self.eliminateCandidate(remaining, toEliminate) 772 773 return self.scoresFromEliminations(eliminated, ncand)
IRV Prime results.
>>> IrvPrime().results([[0,1,2]])[2]
0
>>> IrvPrime().results([[0,1,2],[2,1,0]])[1]
0
>>> IrvPrime().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[0, 2, 1]
>>> IrvPrime().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
[1, 2, 0]
>>> # Favorite betrayal example from http://rangevoting.org/IncentToExagg.html
>>> IrvPrime().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
[2, 1, 0]
>>> IrvPrime().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
[1, 0, 3, 2, 4]
>>> # Elections 3-5 from http://votingmatters.org.uk/ISSUE6/P4.HTM
>>> IrvPrime().results([[0,1,2,3,4,5]] * 12 + [[2,0,1,3,4,5]] * 11 + [[1,2,0,3,4,5]] * 10 +
... [[3,4,5]] * 27)
[2, 5, 4, 3, 1, 0]
>>> IrvPrime().results([[0,1]] * 11 + [[1]] * 7 + [[2]] * 12)
[0, 2, 1]
>>> IrvPrime().results([[0,3,2,1]] * 5 + [[1,2,0,3]] * 5 + [[2,0,1,3]] * 8 +
... [[3,0,1,2]] * 4 + [[3,1,2,0]] * 8)
[3, 0, 1, 2]
>>> IrvPrime().results([[0,2,1,3]] * 6 + [[0,3,1,2]] * 3 + [[0,3,2,1]] * 3 +
... [[1,2,0,3]] * 4 + [[2,0,1,3]] * 4 + [[3,1,2,0]] * 5)
[2, 0, 3, 1]
>>> # Failure of later-no-harm
>>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
... [[1,0,2]] * 21 + [[2,0,1]] * 30 + [[2,1,0]] * 20)
[1, 0, 2]
>>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
... [[1,0,2]] * 21 + [[2,1,0]] * 30 + [[2,1,0]] * 20)
[1, 2, 0]
Inherited Members
349class KSElectorate(DimElectorate): 350 def chooseClusters(self, n, alpha, caring): 351 self.clusters = [] 352 for i in range(n): 353 item = [] 354 for c in range(self.numClusters): 355 r = (i + alpha) * random.random() 356 if r > i: 357 item.append(self.numSubclusters[c]) 358 self.numSubclusters[c] += 1 359 else: 360 item.append(self.clusters[int(r)][c]) 361 self.clusters.append(item) 362 self.clusterMeans = [] 363 self.clusterCaring = [] 364 for c in range(self.numClusters): 365 subclusterMeans = [] 366 subclusterCaring = [] 367 for _ in range(self.numSubclusters[c]): 368 cares = caring() 369 370 subclusterMeans.append([random.gauss(0, sqrt(cares)) for _ in range(self.dcs[c])]) 371 372 subclusterCaring.append(caring()) 373 self.clusterMeans.append(subclusterMeans) 374 self.clusterCaring.append(subclusterCaring) 375 376 def asDims(self, v, i): 377 result = [] 378 cares = [] 379 for dim, c in enumerate(range(self.numClusters)): 380 clusterMean = self.clusterMeans[c][self.clusters[i][c]] 381 for m in clusterMean: 382 acare = self.clusterCaring[c][self.clusters[i][c]] 383 result.append(m + (v[dim] * sqrt(1 - acare))) 384 cares.append(acare) 385 v = PersonalityVoter(result) # TODO: do personality right 386 v.cares = cares 387 return v 388 389 def fromDims(self, dimvoters, vType): 390 for v in dimvoters: 391 self.append(vType.fromDims(v, self, v.cares))
A list of voters. Each voter is a list of candidate utilities
350 def chooseClusters(self, n, alpha, caring): 351 self.clusters = [] 352 for i in range(n): 353 item = [] 354 for c in range(self.numClusters): 355 r = (i + alpha) * random.random() 356 if r > i: 357 item.append(self.numSubclusters[c]) 358 self.numSubclusters[c] += 1 359 else: 360 item.append(self.clusters[int(r)][c]) 361 self.clusters.append(item) 362 self.clusterMeans = [] 363 self.clusterCaring = [] 364 for c in range(self.numClusters): 365 subclusterMeans = [] 366 subclusterCaring = [] 367 for _ in range(self.numSubclusters[c]): 368 cares = caring() 369 370 subclusterMeans.append([random.gauss(0, sqrt(cares)) for _ in range(self.dcs[c])]) 371 372 subclusterCaring.append(caring()) 373 self.clusterMeans.append(subclusterMeans) 374 self.clusterCaring.append(subclusterCaring)
376 def asDims(self, v, i): 377 result = [] 378 cares = [] 379 for dim, c in enumerate(range(self.numClusters)): 380 clusterMean = self.clusterMeans[c][self.clusters[i][c]] 381 for m in clusterMean: 382 acare = self.clusterCaring[c][self.clusters[i][c]] 383 result.append(m + (v[dim] * sqrt(1 - acare))) 384 cares.append(acare) 385 v = PersonalityVoter(result) # TODO: do personality right 386 v.cares = cares 387 return v
Inherited Members
394class KSModel(DimModel): # Kitchen sink 395 builtElectorate = KSElectorate 396 baseElectorate = RandomModel() 397 398 @autoassign 399 # dc = dimensional cluster; vc = voter cluster 400 def __init__( 401 self, 402 dcdecay=(1, 1), 403 dccut=0.2, 404 wcdecay=(1, 1), 405 wccut=0.2, 406 wcalpha=1, 407 vccaring=(3, 1.5), 408 ): 409 DimModel.__init__(self) 410 411 def __str__(self): 412 return "_".join( 413 str(x) 414 for x in (self.__class__.__name__, self.wcalpha) 415 + self.dcdecay 416 + self.wcdecay 417 + self.vccaring 418 ) 419 420 def __call__(self, nvot, ncand, vType=DimVoter): 421 """Tests? Making statistical tests that would pass reliably is 422 a huge hassle. Sorry, maybe later. 423 """ 424 vType.resetClusters() 425 e = self.builtElectorate() 426 e.dcs = [] # number of dimensions in each dc 427 e.dimWeights = [] # raw importance of each dimension, regardless of dc 428 clusterWeight = 1 429 while clusterWeight > self.dccut: 430 dimweight = clusterWeight 431 dimnum = 0 432 while dimweight > self.wccut: 433 e.dimWeights.append(dimweight) 434 dimnum += 1 435 dimweight *= beta.rvs(*self.wcdecay) 436 e.dcs.append(dimnum) 437 clusterWeight *= beta.rvs(*self.dcdecay) 438 e.numClusters = len(e.dcs) 439 e.numSubclusters = [0] * e.numClusters 440 e.chooseClusters(nvot + ncand, self.wcalpha, lambda: beta.rvs(*self.vccaring)) 441 return self.makeElectorate(e, nvot, ncand, vType)
>>> dm = DimModel(2,baseElectorate=DeterministicModel(3))
>>> dm(2,4)
[(-1.8439088914585775, -0.0, -1.0, -1.8439088914585775), (-1.2649110640673518, -1.0, -0.0, -1.2649110640673518)]
>>> dm.dimWeights
[1, 0.5]
Inherited Members
59class LazyChooser(Chooser): 60 """Honest, if honest and strategic are the same. Otherwise, extra-strategic.""" 61 62 tallyKeys = [""] 63 64 def __init__(self, subChoosers=None): 65 super().__init__(subChoosers=[beHon, beX] if subChoosers is None else subChoosers) 66 67 def __call__(self, cls, voter, tally): 68 if getattr(voter, f"{cls.__name__}_hon") == getattr(voter, f"{cls.__name__}_strat"): 69 tally[self.myKeys[0]] += 0 70 return self.subChoosers[0](cls, voter, tally) # hon 71 tally[self.myKeys[0]] += 1 72 return self.subChoosers[1](cls, voter, tally) # strat
Honest, if honest and strategic are the same. Otherwise, extra-strategic.
64 def __init__(self, subChoosers=None): 65 super().__init__(subChoosers=[beHon, beX] if subChoosers is None else subChoosers)
Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.
Inherited Members
322class Mav(Method): 323 """Majority Approval Voting""" 324 325 # >>> mqs = [Mav().resultsFor(PolyaModel()(101,5),Mav.honBallot)[0] for i in range(400)] 326 # >>> mean(mqs) 327 # 1.5360519801980208 328 # >>> mqs += [Mav().resultsFor(PolyaModel()(101,5),Mav.honBallot)[0] for i in range(1200)] 329 # >>> mean(mqs) 330 # 1.5343069306930679 331 # >>> std(mqs) 332 # 1.0970202515275356 333 bias5 = 1.0970202515275356 334 335 baseCuts = [-0.8, 0, 0.8, 1.6] 336 specificCuts = None 337 specificPercentiles = [25, 50, 75, 90] 338 339 def candScore(self, scores): 340 """For now, only works correctly for odd nvot 341 342 Basic tests 343 >>> Mav().candScore([1,2,3,4,5]) 344 3.0 345 >>> Mav().candScore([1,2,3,3,3]) 346 2.5 347 >>> Mav().candScore([1,2,3,4]) 348 2.5 349 >>> Mav().candScore([1,2,3,3]) 350 2.5 351 >>> Mav().candScore([1,2,2,2]) 352 1.5 353 >>> Mav().candScore([1,2,3,3,5]) 354 2.7 355 """ 356 scores = sorted(scores) 357 nvot = len(scores) 358 nGrades = len(self.baseCuts) + 1 359 i = int((nvot - 1) / 2) 360 base = scores[i] 361 while i < nvot and scores[i] == base: 362 i += 1 363 upper = (base + 0.5) - (i - nvot / 2) * nGrades / nvot 364 lower = (base) - (i - nvot / 2) / nvot 365 return max(upper, lower) 366 367 @classmethod 368 def honBallotFor(cls, voters): 369 cls.specificCuts = percentile(voters, cls.specificPercentiles) 370 return cls.honBallot 371 372 @staticmethod # cls is provided explicitly, not through binding 373 @rememberBallot 374 def honBallot(cls, voter): 375 """Takes utilities and returns an honest ballot (on 0..4) 376 377 honest ballot works as intended, gives highest grade to highest utility: 378 >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5,1,1.1])) 379 [0, 1, 2, 3, 4] 380 381 Even if they don't rate at least an honest "B": 382 >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5])) 383 [0, 1, 4] 384 """ 385 cuts = cls.specificCuts if (cls.specificCuts is not None) else cls.baseCuts 386 cuts = [min(cut, max(voter) - 0.001) for cut in cuts] 387 return [toVote(cuts, util) for util in voter] 388 389 def stratBallotFor(self, polls): 390 """Returns a function which takes utilities and returns a dict( 391 strat=<ballot in which all grades are exaggerated 392 to outside the range of the two honest frontrunners>, 393 extraStrat=<ballot in which all grades are exaggerated to extremes>, 394 isStrat=<whether the runner-up is preferred to the frontrunner (for reluctantStrat)>, 395 stratGap=<utility of runner-up minus that of frontrunner> 396 ) 397 for the given "polling" info. 398 399 400 401 Strategic tests: 402 >>> Mav().stratBallotFor([0,1.1,1.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) 403 [0, 1, 2, 3, 4] 404 >>> Mav().stratBallotFor([0,2.1,2.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) 405 [0, 1, 3, 3, 4] 406 >>> Mav().stratBallotFor([0,2.1,1.9,0,0])(Mav, Voter([-1,0.4,0.5,1,2])) 407 [0, 1, 3, 3, 4] 408 >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,7,6])) 409 [4, 4, 4] 410 >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,5,6])) 411 [4, 0, 4] 412 >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6])) 413 [4, 0, 4] 414 >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6.1])) 415 [2, 2, 4] 416 """ 417 places = sorted(enumerate(polls), key=lambda x: -x[1]) # from high to low 418 # print("places",places) 419 ((frontId, frontResult), (targId, targResult)) = places[:2] 420 421 @rememberBallots 422 def stratBallot(cls, voter): 423 frontUtils = [voter[frontId], voter[targId]] # utils of frontrunners 424 stratGap = frontUtils[1] - frontUtils[0] 425 if stratGap == 0: 426 strat = extraStrat = [(4 if (util >= frontUtils[0]) else 0) for util in voter] 427 isStrat = True 428 429 else: 430 if stratGap < 0: 431 # winner is preferred; be complacent. 432 isStrat = False 433 else: 434 # runner-up is preferred; be strategic in iss run 435 isStrat = True 436 # sort cuts high to low 437 frontUtils = (frontUtils[1], frontUtils[0]) 438 top = max(voter) 439 # print("lll312") 440 # print(self.baseCuts, front) 441 cutoffs = [ 442 ( 443 (min(frontUtils[0], self.baseCuts[i])) 444 if (i < floor(targResult)) 445 else ( 446 (frontUtils[1]) 447 if (i < floor(frontResult) + 1) 448 else min(top, self.baseCuts[i]) 449 ) 450 ) 451 for i in range(len(self.baseCuts)) 452 ] 453 strat = [toVote(cutoffs, util) for util in voter] 454 extraStrat = [ 455 max( 456 0, 457 min( 458 10, 459 floor(4.99 * (util - frontUtils[1]) / (frontUtils[0] - frontUtils[1])), 460 ), 461 ) 462 for util in voter 463 ] 464 return dict(strat=strat, extraStrat=extraStrat, isStrat=isStrat, stratGap=stratGap) 465 466 return stratBallot
Majority Approval Voting
339 def candScore(self, scores): 340 """For now, only works correctly for odd nvot 341 342 Basic tests 343 >>> Mav().candScore([1,2,3,4,5]) 344 3.0 345 >>> Mav().candScore([1,2,3,3,3]) 346 2.5 347 >>> Mav().candScore([1,2,3,4]) 348 2.5 349 >>> Mav().candScore([1,2,3,3]) 350 2.5 351 >>> Mav().candScore([1,2,2,2]) 352 1.5 353 >>> Mav().candScore([1,2,3,3,5]) 354 2.7 355 """ 356 scores = sorted(scores) 357 nvot = len(scores) 358 nGrades = len(self.baseCuts) + 1 359 i = int((nvot - 1) / 2) 360 base = scores[i] 361 while i < nvot and scores[i] == base: 362 i += 1 363 upper = (base + 0.5) - (i - nvot / 2) * nGrades / nvot 364 lower = (base) - (i - nvot / 2) / nvot 365 return max(upper, lower)
For now, only works correctly for odd nvot
Basic tests
Mav().candScore([1,2,3,4,5]) 3.0 Mav().candScore([1,2,3,3,3]) 2.5 Mav().candScore([1,2,3,4]) 2.5 Mav().candScore([1,2,3,3]) 2.5 Mav().candScore([1,2,2,2]) 1.5 Mav().candScore([1,2,3,3,5]) 2.7
367 @classmethod 368 def honBallotFor(cls, voters): 369 cls.specificCuts = percentile(voters, cls.specificPercentiles) 370 return cls.honBallot
This is where you would do any setup necessary and create an honBallot function. But the base version just returns the honBallot function.
372 @staticmethod # cls is provided explicitly, not through binding 373 @rememberBallot 374 def honBallot(cls, voter): 375 """Takes utilities and returns an honest ballot (on 0..4) 376 377 honest ballot works as intended, gives highest grade to highest utility: 378 >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5,1,1.1])) 379 [0, 1, 2, 3, 4] 380 381 Even if they don't rate at least an honest "B": 382 >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5])) 383 [0, 1, 4] 384 """ 385 cuts = cls.specificCuts if (cls.specificCuts is not None) else cls.baseCuts 386 cuts = [min(cut, max(voter) - 0.001) for cut in cuts] 387 return [toVote(cuts, util) for util in voter]
Takes utilities and returns an honest ballot (on 0..4)
honest ballot works as intended, gives highest grade to highest utility:
Mav().honBallot(Mav, Voter([-1,-0.5,0.5,1,1.1])) [0, 1, 2, 3, 4]
Even if they don't rate at least an honest "B":
Mav().honBallot(Mav, Voter([-1,-0.5,0.5])) [0, 1, 4]
389 def stratBallotFor(self, polls): 390 """Returns a function which takes utilities and returns a dict( 391 strat=<ballot in which all grades are exaggerated 392 to outside the range of the two honest frontrunners>, 393 extraStrat=<ballot in which all grades are exaggerated to extremes>, 394 isStrat=<whether the runner-up is preferred to the frontrunner (for reluctantStrat)>, 395 stratGap=<utility of runner-up minus that of frontrunner> 396 ) 397 for the given "polling" info. 398 399 400 401 Strategic tests: 402 >>> Mav().stratBallotFor([0,1.1,1.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) 403 [0, 1, 2, 3, 4] 404 >>> Mav().stratBallotFor([0,2.1,2.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) 405 [0, 1, 3, 3, 4] 406 >>> Mav().stratBallotFor([0,2.1,1.9,0,0])(Mav, Voter([-1,0.4,0.5,1,2])) 407 [0, 1, 3, 3, 4] 408 >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,7,6])) 409 [4, 4, 4] 410 >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,5,6])) 411 [4, 0, 4] 412 >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6])) 413 [4, 0, 4] 414 >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6.1])) 415 [2, 2, 4] 416 """ 417 places = sorted(enumerate(polls), key=lambda x: -x[1]) # from high to low 418 # print("places",places) 419 ((frontId, frontResult), (targId, targResult)) = places[:2] 420 421 @rememberBallots 422 def stratBallot(cls, voter): 423 frontUtils = [voter[frontId], voter[targId]] # utils of frontrunners 424 stratGap = frontUtils[1] - frontUtils[0] 425 if stratGap == 0: 426 strat = extraStrat = [(4 if (util >= frontUtils[0]) else 0) for util in voter] 427 isStrat = True 428 429 else: 430 if stratGap < 0: 431 # winner is preferred; be complacent. 432 isStrat = False 433 else: 434 # runner-up is preferred; be strategic in iss run 435 isStrat = True 436 # sort cuts high to low 437 frontUtils = (frontUtils[1], frontUtils[0]) 438 top = max(voter) 439 # print("lll312") 440 # print(self.baseCuts, front) 441 cutoffs = [ 442 ( 443 (min(frontUtils[0], self.baseCuts[i])) 444 if (i < floor(targResult)) 445 else ( 446 (frontUtils[1]) 447 if (i < floor(frontResult) + 1) 448 else min(top, self.baseCuts[i]) 449 ) 450 ) 451 for i in range(len(self.baseCuts)) 452 ] 453 strat = [toVote(cutoffs, util) for util in voter] 454 extraStrat = [ 455 max( 456 0, 457 min( 458 10, 459 floor(4.99 * (util - frontUtils[1]) / (frontUtils[0] - frontUtils[1])), 460 ), 461 ) 462 for util in voter 463 ] 464 return dict(strat=strat, extraStrat=extraStrat, isStrat=isStrat, stratGap=stratGap) 465 466 return stratBallot
Returns a function which takes utilities and returns a dict(
strat=
Strategic tests:
Mav().stratBallotFor([0,1.1,1.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) [0, 1, 2, 3, 4] Mav().stratBallotFor([0,2.1,2.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) [0, 1, 3, 3, 4] Mav().stratBallotFor([0,2.1,1.9,0,0])(Mav, Voter([-1,0.4,0.5,1,2])) [0, 1, 3, 3, 4] Mav().stratBallotFor([1,0,2])(Mav, Voter([6,7,6])) [4, 4, 4] Mav().stratBallotFor([1,0,2])(Mav, Voter([6,5,6])) [4, 0, 4] Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6])) [4, 0, 4] Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6.1])) [2, 2, 4]
102class Method: 103 """Base class for election methods. Holds some of the duct tape.""" 104 105 def __str__(self): 106 return self.__class__.__name__ 107 108 def results(self, ballots, isHonest=False, **kwargs): 109 """Combines ballots into results. Override for comparative 110 methods. 111 112 Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index. 113 114 Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index. 115 116 Test for subclasses, makes no sense to test this method in the abstract base class. 117 """ 118 from .dataframe import ballots_from_dataframe 119 120 ballots = ballots_from_dataframe(ballots) 121 return list(map(self.candScore, zip(*ballots))) 122 123 def honest_ballots(self, voters): 124 """Return honest ballots for voters as method-ready lists.""" 125 sentinel = object() 126 previous_cuts = getattr(self.__class__, "specificCuts", sentinel) 127 try: 128 ballot_factory = self.honBallotFor(voters) 129 return [ballot_factory(self.__class__, voter) for voter in voters] 130 finally: 131 if previous_cuts is sentinel: 132 try: 133 delattr(self.__class__, "specificCuts") 134 except AttributeError: 135 pass 136 else: 137 self.__class__.specificCuts = previous_cuts 138 139 def ballots_dataframe(self, voters, wide=False): 140 """Return honest ballots for voters as a tidy or wide DataFrame.""" 141 from .dataframe import ballots_to_dataframe 142 143 return ballots_to_dataframe(self.honest_ballots(voters), wide=wide, method=self) 144 145 def results_dataframe(self, ballots, isHonest=False, **kwargs): 146 """Return candidate scores for ballots as a DataFrame.""" 147 from .dataframe import scores_to_dataframe 148 149 self.__class__.extraEvents = {} 150 return scores_to_dataframe( 151 self.results(ballots, isHonest=isHonest, **kwargs), 152 method=self, 153 ) 154 155 @staticmethod # cls is provided explicitly, not through binding 156 def honBallot(cls, utils): 157 """Takes utilities and returns an honest ballot""" 158 raise NotImplementedError(f"{cls} needs honBallot") 159 160 @staticmethod 161 def winner(results): 162 """Simply find the winner once scores are already calculated. Override for 163 ranked methods. 164 165 166 >>> Method().winner([1,2,3,2,-100]) 167 2 168 >>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6 169 True 170 """ 171 winScore = max(result for result in results if isnum(result)) 172 winners = [cand for (cand, score) in enumerate(results) if score == winScore] 173 return random.choice(winners) 174 175 def honBallotFor(self, voters): 176 """This is where you would do any setup necessary and create an honBallot 177 function. But the base version just returns the honBallot function.""" 178 return self.honBallot 179 180 def dummyBallotFor(self, polls): 181 """Returns a (function which takes utilities and returns a dummy ballot) 182 for the given "polling" info.""" 183 return lambda cls, utilities, stratTally: utilities 184 185 def resultsFor(self, voters, chooser, tally=None, **kwargs): 186 """create ballots and get results. 187 188 Again, test on subclasses. 189 """ 190 if tally is None: 191 tally = SideTally() 192 tally.initKeys(chooser) 193 return dict( 194 results=self.results( 195 [chooser(self.__class__, voter, tally) for voter in voters], **kwargs 196 ), 197 chooser=chooser.__name__, 198 tally=tally, 199 ) 200 201 def multiResults(self, voters, chooserFuns=(), media=(lambda x, t: x), checkStrat=True): 202 """Runs two base elections: first with honest votes, then 203 with strategic results based on the first results (filtered by 204 the media). Then, runs a series of elections using each chooserFun 205 in chooserFuns to select the votes for each voter. 206 207 Returns a tuple of (honResults, stratResults, ...). The stratresults 208 are based on common polling information, which is given by media(honresults). 209 """ 210 from .strategies import OssChooser 211 212 honTally = SideTally() 213 self.__class__.extraEvents = {} 214 hon = self.resultsFor(voters, self.honBallotFor(voters), honTally, isHonest=True) 215 216 stratTally = SideTally() 217 218 polls = media(hon["results"], stratTally) 219 winner, _w, target, _t = self.stratTargetFor(sorted(enumerate(polls), key=lambda x: -x[1])) 220 221 strat = self.resultsFor(voters, self.stratBallotFor(polls), stratTally) 222 223 ossTally = SideTally() 224 oss = self.resultsFor(voters, self.ballotChooserFor(OssChooser()), ossTally) 225 ossWinner = oss["results"].index(max(oss["results"])) 226 ossTally["worked"] += 1 if ossWinner == target else (0 if ossWinner == winner else -1) 227 228 smart = dict( 229 results=(hon["results"] if ossTally["worked"] == 1 else oss["results"]), 230 chooser="smartOss", 231 tally=SideTally(), 232 ) 233 234 extraTallies = Tallies() 235 results = [strat, oss, smart] + [ 236 self.resultsFor(voters, self.ballotChooserFor(chooserFun), aTally) 237 for (chooserFun, aTally) in zip(chooserFuns, extraTallies) 238 ] 239 return [(hon["results"], hon["chooser"], list(self.__class__.extraEvents.items()))] + [ 240 (r["results"], r["chooser"], r["tally"].itemList()) for r in results 241 ] 242 243 def vseOn(self, voters, chooserFuns=(), **args): 244 """Finds honest and strategic voter satisfaction efficiency (VSE) 245 for this method on the given electorate. 246 """ 247 multiResults = self.multiResults(voters, chooserFuns, **args) 248 utils = voters.socUtils 249 best = max(utils) 250 rand = mean(utils) 251 252 # import pprint 253 # pprint.pprint(multiResults) 254 vses = VseMethodRun( 255 self.__class__, 256 chooserFuns, 257 [ 258 VseOneRun( 259 [(utils[self.winner(result)] - rand) / (best - rand)], 260 tally, 261 chooser, 262 ) 263 for (result, chooser, tally) in multiResults[0] 264 ], 265 ) 266 vses.extraEvents = multiResults[1] 267 return vses 268 269 def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args): 270 multiResults = self.multiResults(voters, chooserFuns, **args) 271 utils = voters.socUtils 272 best = max(utils) 273 rand = mean(utils) 274 rows = [] 275 nvot = len(voters) 276 for result, chooser, tallyItems in multiResults: 277 row = { 278 "eid": eid, 279 "emodel": emodel, 280 "ncand": cands, 281 "nvot": nvot, 282 "best": best, 283 "rand": rand, 284 "method": str(self), 285 "chooser": chooser, # .getName(), 286 "util": utils[self.winner(result)], 287 "vse": (utils[self.winner(result)] - rand) / (best - rand), 288 } 289 # print(tallyItems) 290 for i, (k, v) in enumerate(tallyItems): 291 # print("Result: tally ",i,k,v) 292 row[f"tallyName{str(i)}"] = str(k) 293 row[f"tallyVal{str(i)}"] = str(v) 294 rows.append(row) 295 return rows 296 297 @staticmethod 298 def ballotChooserFor(chooserFun): 299 """Takes a chooserFun; returns a ballot chooser using that chooserFun""" 300 301 def ballotChooser(cls, voter, tally): 302 return getattr(voter, f"{cls.__name__}_{chooserFun(cls, voter, tally)}") 303 304 ballotChooser.__name__ = chooserFun.getName() 305 return ballotChooser 306 307 def stratTarget2(self, places): 308 ((frontId, frontResult), (targId, targResult)) = places[:2] 309 return (frontId, frontResult, targId, targResult) 310 311 def stratTarget3(self, places): 312 ((frontId, frontResult), (targId, targResult)) = places[:3:2] 313 return (frontId, frontResult, targId, targResult) 314 315 stratTargetFor = stratTarget2 316 317 def stratBallotFor(self, polls): 318 """Returns a (function which takes utilities and returns a strategic ballot) 319 for the given "polling" info.""" 320 321 places = sorted(enumerate(polls), key=lambda x: -x[1]) # from high to low 322 # print("places",places) 323 (frontId, frontResult, targId, targResult) = self.stratTargetFor(places) 324 n = len(polls) 325 326 @rememberBallots 327 def stratBallot(cls, voter): 328 stratGap = voter[targId] - voter[frontId] 329 ballot = [0] * len(voter) 330 isStrat = stratGap > 0 331 extras = cls.fillStratBallot( 332 voter, 333 polls, 334 places, 335 n, 336 stratGap, 337 ballot, 338 frontId, 339 frontResult, 340 targId, 341 targResult, 342 ) 343 result = dict(strat=ballot, isStrat=isStrat, stratGap=stratGap) 344 if extras: 345 result.update(extras) 346 return result 347 348 return stratBallot
Base class for election methods. Holds some of the duct tape.
108 def results(self, ballots, isHonest=False, **kwargs): 109 """Combines ballots into results. Override for comparative 110 methods. 111 112 Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index. 113 114 Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index. 115 116 Test for subclasses, makes no sense to test this method in the abstract base class. 117 """ 118 from .dataframe import ballots_from_dataframe 119 120 ballots = ballots_from_dataframe(ballots) 121 return list(map(self.candScore, zip(*ballots)))
Combines ballots into results. Override for comparative methods.
Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index.
Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index.
Test for subclasses, makes no sense to test this method in the abstract base class.
123 def honest_ballots(self, voters): 124 """Return honest ballots for voters as method-ready lists.""" 125 sentinel = object() 126 previous_cuts = getattr(self.__class__, "specificCuts", sentinel) 127 try: 128 ballot_factory = self.honBallotFor(voters) 129 return [ballot_factory(self.__class__, voter) for voter in voters] 130 finally: 131 if previous_cuts is sentinel: 132 try: 133 delattr(self.__class__, "specificCuts") 134 except AttributeError: 135 pass 136 else: 137 self.__class__.specificCuts = previous_cuts
Return honest ballots for voters as method-ready lists.
139 def ballots_dataframe(self, voters, wide=False): 140 """Return honest ballots for voters as a tidy or wide DataFrame.""" 141 from .dataframe import ballots_to_dataframe 142 143 return ballots_to_dataframe(self.honest_ballots(voters), wide=wide, method=self)
Return honest ballots for voters as a tidy or wide DataFrame.
145 def results_dataframe(self, ballots, isHonest=False, **kwargs): 146 """Return candidate scores for ballots as a DataFrame.""" 147 from .dataframe import scores_to_dataframe 148 149 self.__class__.extraEvents = {} 150 return scores_to_dataframe( 151 self.results(ballots, isHonest=isHonest, **kwargs), 152 method=self, 153 )
Return candidate scores for ballots as a DataFrame.
155 @staticmethod # cls is provided explicitly, not through binding 156 def honBallot(cls, utils): 157 """Takes utilities and returns an honest ballot""" 158 raise NotImplementedError(f"{cls} needs honBallot")
Takes utilities and returns an honest ballot
160 @staticmethod 161 def winner(results): 162 """Simply find the winner once scores are already calculated. Override for 163 ranked methods. 164 165 166 >>> Method().winner([1,2,3,2,-100]) 167 2 168 >>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6 169 True 170 """ 171 winScore = max(result for result in results if isnum(result)) 172 winners = [cand for (cand, score) in enumerate(results) if score == winScore] 173 return random.choice(winners)
Simply find the winner once scores are already calculated. Override for ranked methods.
>>> Method().winner([1,2,3,2,-100])
2
>>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6
True
175 def honBallotFor(self, voters): 176 """This is where you would do any setup necessary and create an honBallot 177 function. But the base version just returns the honBallot function.""" 178 return self.honBallot
This is where you would do any setup necessary and create an honBallot function. But the base version just returns the honBallot function.
180 def dummyBallotFor(self, polls): 181 """Returns a (function which takes utilities and returns a dummy ballot) 182 for the given "polling" info.""" 183 return lambda cls, utilities, stratTally: utilities
Returns a (function which takes utilities and returns a dummy ballot) for the given "polling" info.
185 def resultsFor(self, voters, chooser, tally=None, **kwargs): 186 """create ballots and get results. 187 188 Again, test on subclasses. 189 """ 190 if tally is None: 191 tally = SideTally() 192 tally.initKeys(chooser) 193 return dict( 194 results=self.results( 195 [chooser(self.__class__, voter, tally) for voter in voters], **kwargs 196 ), 197 chooser=chooser.__name__, 198 tally=tally, 199 )
create ballots and get results.
Again, test on subclasses.
201 def multiResults(self, voters, chooserFuns=(), media=(lambda x, t: x), checkStrat=True): 202 """Runs two base elections: first with honest votes, then 203 with strategic results based on the first results (filtered by 204 the media). Then, runs a series of elections using each chooserFun 205 in chooserFuns to select the votes for each voter. 206 207 Returns a tuple of (honResults, stratResults, ...). The stratresults 208 are based on common polling information, which is given by media(honresults). 209 """ 210 from .strategies import OssChooser 211 212 honTally = SideTally() 213 self.__class__.extraEvents = {} 214 hon = self.resultsFor(voters, self.honBallotFor(voters), honTally, isHonest=True) 215 216 stratTally = SideTally() 217 218 polls = media(hon["results"], stratTally) 219 winner, _w, target, _t = self.stratTargetFor(sorted(enumerate(polls), key=lambda x: -x[1])) 220 221 strat = self.resultsFor(voters, self.stratBallotFor(polls), stratTally) 222 223 ossTally = SideTally() 224 oss = self.resultsFor(voters, self.ballotChooserFor(OssChooser()), ossTally) 225 ossWinner = oss["results"].index(max(oss["results"])) 226 ossTally["worked"] += 1 if ossWinner == target else (0 if ossWinner == winner else -1) 227 228 smart = dict( 229 results=(hon["results"] if ossTally["worked"] == 1 else oss["results"]), 230 chooser="smartOss", 231 tally=SideTally(), 232 ) 233 234 extraTallies = Tallies() 235 results = [strat, oss, smart] + [ 236 self.resultsFor(voters, self.ballotChooserFor(chooserFun), aTally) 237 for (chooserFun, aTally) in zip(chooserFuns, extraTallies) 238 ] 239 return [(hon["results"], hon["chooser"], list(self.__class__.extraEvents.items()))] + [ 240 (r["results"], r["chooser"], r["tally"].itemList()) for r in results 241 ]
Runs two base elections: first with honest votes, then with strategic results based on the first results (filtered by the media). Then, runs a series of elections using each chooserFun in chooserFuns to select the votes for each voter.
Returns a tuple of (honResults, stratResults, ...). The stratresults are based on common polling information, which is given by media(honresults).
243 def vseOn(self, voters, chooserFuns=(), **args): 244 """Finds honest and strategic voter satisfaction efficiency (VSE) 245 for this method on the given electorate. 246 """ 247 multiResults = self.multiResults(voters, chooserFuns, **args) 248 utils = voters.socUtils 249 best = max(utils) 250 rand = mean(utils) 251 252 # import pprint 253 # pprint.pprint(multiResults) 254 vses = VseMethodRun( 255 self.__class__, 256 chooserFuns, 257 [ 258 VseOneRun( 259 [(utils[self.winner(result)] - rand) / (best - rand)], 260 tally, 261 chooser, 262 ) 263 for (result, chooser, tally) in multiResults[0] 264 ], 265 ) 266 vses.extraEvents = multiResults[1] 267 return vses
Finds honest and strategic voter satisfaction efficiency (VSE) for this method on the given electorate.
269 def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args): 270 multiResults = self.multiResults(voters, chooserFuns, **args) 271 utils = voters.socUtils 272 best = max(utils) 273 rand = mean(utils) 274 rows = [] 275 nvot = len(voters) 276 for result, chooser, tallyItems in multiResults: 277 row = { 278 "eid": eid, 279 "emodel": emodel, 280 "ncand": cands, 281 "nvot": nvot, 282 "best": best, 283 "rand": rand, 284 "method": str(self), 285 "chooser": chooser, # .getName(), 286 "util": utils[self.winner(result)], 287 "vse": (utils[self.winner(result)] - rand) / (best - rand), 288 } 289 # print(tallyItems) 290 for i, (k, v) in enumerate(tallyItems): 291 # print("Result: tally ",i,k,v) 292 row[f"tallyName{str(i)}"] = str(k) 293 row[f"tallyVal{str(i)}"] = str(v) 294 rows.append(row) 295 return rows
297 @staticmethod 298 def ballotChooserFor(chooserFun): 299 """Takes a chooserFun; returns a ballot chooser using that chooserFun""" 300 301 def ballotChooser(cls, voter, tally): 302 return getattr(voter, f"{cls.__name__}_{chooserFun(cls, voter, tally)}") 303 304 ballotChooser.__name__ = chooserFun.getName() 305 return ballotChooser
Takes a chooserFun; returns a ballot chooser using that chooserFun
317 def stratBallotFor(self, polls): 318 """Returns a (function which takes utilities and returns a strategic ballot) 319 for the given "polling" info.""" 320 321 places = sorted(enumerate(polls), key=lambda x: -x[1]) # from high to low 322 # print("places",places) 323 (frontId, frontResult, targId, targResult) = self.stratTargetFor(places) 324 n = len(polls) 325 326 @rememberBallots 327 def stratBallot(cls, voter): 328 stratGap = voter[targId] - voter[frontId] 329 ballot = [0] * len(voter) 330 isStrat = stratGap > 0 331 extras = cls.fillStratBallot( 332 voter, 333 polls, 334 places, 335 n, 336 stratGap, 337 ballot, 338 frontId, 339 frontResult, 340 targId, 341 targResult, 342 ) 343 result = dict(strat=ballot, isStrat=isStrat, stratGap=stratGap) 344 if extras: 345 result.update(extras) 346 return result 347 348 return stratBallot
Returns a (function which takes utilities and returns a strategic ballot) for the given "polling" info.
469class Mj(Mav): 470 def candScore(self, scores): 471 """This formula will always give numbers within 0.5 of the raw median. 472 Unfortunately, with 5 grade levels, these will tend to be within 0.1 of 473 the raw median, leaving scores further from the integers mostly unused. 474 This is only a problem aesthetically. 475 476 For now, only works correctly for odd nvot 477 478 tests: 479 >>> Mj().candScore([1,2,3,4,5]) 480 3 481 >>> Mj().candScore([1,2,3,3,5]) 482 2.7 483 >>> Mj().candScore([1,3,3,3,5]) 484 3 485 >>> Mj().candScore([1,3,3,4,5]) 486 3.3 487 >>> Mj().candScore([1,3,3,3,3]) 488 2.9 489 >>> Mj().candScore([3] * 24 + [1]) 490 2.98 491 >>> Mj().candScore([3] * 24 + [4]) 492 3.02 493 >>> Mj().candScore([3] * 13 + [4] * 12) 494 3.46 495 """ 496 scores = sorted(scores) 497 nvot = len(scores) 498 lo = hi = mid = nvot // 2 499 base = scores[mid] 500 while hi < nvot and scores[hi] == base: 501 hi += 1 502 while lo >= 0 and scores[lo] == base: 503 lo -= 1 504 505 if (hi - mid) == (mid - lo): 506 return base 507 elif (hi - mid) < (mid - lo): 508 return base + 0.5 - (hi - mid) / nvot 509 else: 510 return base - 0.5 + (mid - lo) / nvot
Majority Approval Voting
470 def candScore(self, scores): 471 """This formula will always give numbers within 0.5 of the raw median. 472 Unfortunately, with 5 grade levels, these will tend to be within 0.1 of 473 the raw median, leaving scores further from the integers mostly unused. 474 This is only a problem aesthetically. 475 476 For now, only works correctly for odd nvot 477 478 tests: 479 >>> Mj().candScore([1,2,3,4,5]) 480 3 481 >>> Mj().candScore([1,2,3,3,5]) 482 2.7 483 >>> Mj().candScore([1,3,3,3,5]) 484 3 485 >>> Mj().candScore([1,3,3,4,5]) 486 3.3 487 >>> Mj().candScore([1,3,3,3,3]) 488 2.9 489 >>> Mj().candScore([3] * 24 + [1]) 490 2.98 491 >>> Mj().candScore([3] * 24 + [4]) 492 3.02 493 >>> Mj().candScore([3] * 13 + [4] * 12) 494 3.46 495 """ 496 scores = sorted(scores) 497 nvot = len(scores) 498 lo = hi = mid = nvot // 2 499 base = scores[mid] 500 while hi < nvot and scores[hi] == base: 501 hi += 1 502 while lo >= 0 and scores[lo] == base: 503 lo -= 1 504 505 if (hi - mid) == (mid - lo): 506 return base 507 elif (hi - mid) < (mid - lo): 508 return base + 0.5 - (hi - mid) / nvot 509 else: 510 return base - 0.5 + (mid - lo) / nvot
This formula will always give numbers within 0.5 of the raw median. Unfortunately, with 5 grade levels, these will tend to be within 0.1 of the raw median, leaving scores further from the integers mostly unused. This is only a problem aesthetically.
For now, only works correctly for odd nvot
tests:
Mj().candScore([1,2,3,4,5]) 3 Mj().candScore([1,2,3,3,5]) 2.7 Mj().candScore([1,3,3,3,5]) 3 Mj().candScore([1,3,3,4,5]) 3.3 Mj().candScore([1,3,3,3,3]) 2.9 Mj().candScore([3] * 24 + [1]) 2.98 Mj().candScore([3] * 24 + [4]) 3.02 Mj().candScore([3] * 13 + [4] * 12) 3.46
75class OssChooser(Chooser): 76 tallyKeys = ["", "gap"] 77 """one-sided strategy: 78 returns a 'strategic' ballot for those who prefer the strategic target, 79 and an honest ballot for those who prefer the honest winner. Only works 80 if honBallot and stratBallot have already been called for the voter. 81 82 83 """ 84 85 def __init__(self, subChoosers=None): 86 super().__init__(subChoosers=[beHon, beStrat] if subChoosers is None else subChoosers) 87 88 def __call__(self, cls, voter, tally): 89 hon, strat = self.subChoosers 90 if not getattr(voter, f"{cls.__name__}_isStrat", False): 91 return hon(cls, voter, tally) if callable(hon) else hon 92 tally[self.myKeys[0]] += 1 93 tally[self.myKeys[1]] += getattr(voter, f"{cls.__name__}_stratGap", 0) 94 return strat(cls, voter, tally) if callable(strat) else strat 95 96 def getName(self): 97 baseName = super(OssChooser, self).getName() 98 return f"{baseName}." + "_".join(s.getName() for s in self.subChoosers) + "."
85 def __init__(self, subChoosers=None): 86 super().__init__(subChoosers=[beHon, beStrat] if subChoosers is None else subChoosers)
Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.
one-sided strategy: returns a 'strategic' ballot for those who prefer the strategic target, and an honest ballot for those who prefer the honest winner. Only works if honBallot and stratBallot have already been called for the voter.
Inherited Members
100class PersonalityVoter(Voter): 101 cluster_count = 0 102 103 def __init__(self, *args, **kw): 104 super().__init__() # *args, **kw) #WTF, python? 105 self.cluster = self.__class__.cluster_count 106 self.__class__.cluster_count += 1 107 self.personality = random.gauss(0, 1) # probably to be used for strategic propensity 108 # but in future, could be other clustering voter variability, such as media awareness 109 110 @classmethod 111 def resetClusters(cls): 112 cls.cluster_count = 0 113 114 def copyWithUtils(self, utils): 115 voter = super().copyWithUtils(utils) 116 voter.copyAttrsFrom(self) 117 return voter 118 119 def copyAttrsFrom(self, model): 120 self.personality = model.personality 121 self.cluster = model.cluster
A tuple of candidate utilities.
103 def __init__(self, *args, **kw): 104 super().__init__() # *args, **kw) #WTF, python? 105 self.cluster = self.__class__.cluster_count 106 self.__class__.cluster_count += 1 107 self.personality = random.gauss(0, 1) # probably to be used for strategic propensity 108 # but in future, could be other clustering voter variability, such as media awareness
114 def copyWithUtils(self, utils): 115 voter = super().copyWithUtils(utils) 116 voter.copyAttrsFrom(self) 117 return voter
create a new voter with attrs as self and given utils.
This version is a stub, since this voter class has no attrs.
Inherited Members
97class Plurality(RankedMethod): 98 nRanks = 2 99 100 @staticmethod 101 def oneVote(utils, forWhom): 102 ballot = [0] * len(utils) 103 ballot[forWhom] = 1 104 return ballot 105 106 @staticmethod # cls is provided explicitly, not through binding 107 @rememberBallot 108 def honBallot(cls, utils): 109 """Takes utilities and returns an honest ballot 110 111 >>> Plurality.honBallot(Plurality, Voter([-3,-2,-1])) 112 [0, 0, 1] 113 >>> Plurality().stratBallotFor([3,2,1])(Plurality, Voter([-3,-2,-1])) 114 [0, 1, 0] 115 """ 116 # return cls.oneVote(utils, cls.winner(utils)) 117 ballot = [0] * len(utils) 118 cls.fillPrefOrder(utils, ballot, nSlots=1, lowSlot=1, remainderScore=0) 119 return ballot 120 121 # 122 # @classmethod 123 # def xxstratBallot(cls, voter, polls, places, n, 124 # frontId, frontResult, targId, targResult): 125 # """Takes utilities and returns a strategic ballot 126 # for the given "polling" info. 127 # 128 # >>> Plurality().stratBallotFor([4,2,1])(Plurality, Voter([-4,-2,-1])) 129 # [0, 1, 0] 130 # """ 131 # stratGap = voter[targId] - voter[frontId] 132 # if stratGap <= 0: 133 # #winner is preferred; be complacent. 134 # isStrat = False 135 # strat = cls.oneVote(voter, frontId) 136 # else: 137 # #runner-up is preferred; be strategic in iss run 138 # isStrat = True 139 # #sort cuts high to low 140 # #cuts = (cuts[1], cuts[0]) 141 # strat = cls.oneVote(voter, targId) 142 # return dict(strat=strat, isStrat=isStrat, stratGap=stratGap)
Base class for election methods. Holds some of the duct tape.
106 @staticmethod # cls is provided explicitly, not through binding 107 @rememberBallot 108 def honBallot(cls, utils): 109 """Takes utilities and returns an honest ballot 110 111 >>> Plurality.honBallot(Plurality, Voter([-3,-2,-1])) 112 [0, 0, 1] 113 >>> Plurality().stratBallotFor([3,2,1])(Plurality, Voter([-3,-2,-1])) 114 [0, 1, 0] 115 """ 116 # return cls.oneVote(utils, cls.winner(utils)) 117 ballot = [0] * len(utils) 118 cls.fillPrefOrder(utils, ballot, nSlots=1, lowSlot=1, remainderScore=0) 119 return ballot
Takes utilities and returns an honest ballot
>>> Plurality.honBallot(Plurality, Voter([-3,-2,-1]))
[0, 0, 1]
>>> Plurality().stratBallotFor([3,2,1])(Plurality, Voter([-3,-2,-1]))
[0, 1, 0]
246class PolyaModel(RandomModel): 247 """This creates electorates based on a Polya/Hoppe/Dirichlet model, with mutation. 248 You start with an "urn" of n=seedVoter voters from seedModel, 249 plus alpha "wildcard" voters. Then you draw a voter from the urn, 250 clone and mutate them, and put the original and clone back into the urn. 251 If you draw a "wildcard", use voterGen to make a new voter. 252 """ 253 254 @autoassign 255 def __init__(self, seedVoters=2, alpha=1, seedModel=QModel(), mutantFactor=0.2): 256 pass 257 258 def __call__(self, nvot, ncand, vType=PersonalityVoter): 259 """Tests? Making statistical tests that would pass reliably is 260 a huge hassle. Sorry, maybe later. 261 """ 262 vType.resetClusters() 263 election = self.seedModel(self.seedVoters, ncand, vType) 264 while len(election) < nvot: 265 i = random.randrange(len(election) + self.alpha) 266 if i < len(election): 267 election.append(election[i].mutantChild(self.mutantFactor)) 268 else: 269 election.append(vType.rand(ncand)) 270 return election
This creates electorates based on a Polya/Hoppe/Dirichlet model, with mutation. You start with an "urn" of n=seedVoter voters from seedModel, plus alpha "wildcard" voters. Then you draw a voter from the urn, clone and mutate them, and put the original and clone back into the urn. If you draw a "wildcard", use voterGen to make a new voter.
Inherited Members
101class ProbChooser(Chooser): 102 def __init__(self, probs): 103 self.probs = probs 104 subChoosers = [chooser for (p, chooser) in probs] 105 super().__init__(subChoosers=subChoosers) 106 107 def __call__(self, cls, voter, tally): 108 r = random.random() 109 for i, (p, chooser) in enumerate(self.probs): 110 r -= p 111 if r < 0: 112 if i > 0: # keep tally for all but first option 113 tally[f"{self.getName()}_{chooser.getName()}"] += 1 114 return chooser(cls, voter, tally) 115 return self.subChoosers[-1](cls, voter, tally) if self.subChoosers else None 116 117 def getName(self): 118 baseName = super(ProbChooser, self).getName() 119 return ( 120 f"{baseName}." 121 + "_".join(s.getName() + str(round(p * 100)) for p, s in self.probs) 122 + "." 123 )
102 def __init__(self, probs): 103 self.probs = probs 104 subChoosers = [chooser for (p, chooser) in probs] 105 super().__init__(subChoosers=subChoosers)
Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.
Inherited Members
214class QModel(RandomModel): 215 """Adds a quality dimension to a base model, 216 by generating an election and then hybridizing all voters 217 with a common quality vector. 218 219 Useful along with ReverseModel to create a poor-man's 2d model. 220 221 Basic structure 222 >>> random.seed(0) 223 >>> e4 = QModel(sqrt(3), RandomModel())(100,1) 224 >>> len(e4) 225 100 226 >>> len(e4.socUtils) 227 1 228 229 Reduces the standard deviation 230 >>> 0.4 < std(list(zip(e4))) < 0.6 231 True 232 233 """ 234 235 @autoassign 236 def __init__(self, qWeight=0.5, baseModel=ReverseModel()): 237 pass 238 239 def __call__(self, nvot, ncand, vType=PersonalityVoter): 240 qualities = vType.rand(ncand) 241 return Electorate( 242 [v.hybridWith(qualities, self.qWeight) for v in self.baseModel(nvot, ncand, vType)] 243 )
Adds a quality dimension to a base model, by generating an election and then hybridizing all voters with a common quality vector.
Useful along with ReverseModel to create a poor-man's 2d model.
Basic structure
random.seed(0) e4 = QModel(sqrt(3), RandomModel())(100,1) len(e4) 100 len(e4.socUtils) 1
Reduces the standard deviation
0.4 < std(list(zip(e4))) < 0.6 True
Inherited Members
160class RandomModel: 161 """Empty base class for election models; that is, electorate factories. 162 163 >>> e4 = RandomModel()(4,3) 164 >>> [len(v) for v in e4] 165 [3, 3, 3, 3] 166 """ 167 168 def __str__(self): 169 return self.__class__.__name__ 170 171 def __call__(self, nvot, ncand, vType=PersonalityVoter): 172 return Electorate(vType.rand(ncand) for _ in range(nvot)) 173 174 def to_dataframe(self, nvot, ncand, vType=PersonalityVoter, wide=False, **kwargs): 175 """Generate an electorate and return its utilities as a DataFrame.""" 176 return self(nvot, ncand, vType=vType).to_dataframe(wide=wide, **kwargs)
Empty base class for election models; that is, electorate factories.
>>> e4 = RandomModel()(4,3)
>>> [len(v) for v in e4]
[3, 3, 3, 3]
174 def to_dataframe(self, nvot, ncand, vType=PersonalityVoter, wide=False, **kwargs): 175 """Generate an electorate and return its utilities as a DataFrame.""" 176 return self(nvot, ncand, vType=vType).to_dataframe(wide=wide, **kwargs)
Generate an electorate and return its utilities as a DataFrame.
194class ReverseModel(RandomModel): 195 """Creates an even number of voters in two diametrically-opposed camps 196 (ie, opposite utilities for all candidates) 197 198 >>> e4 = ReverseModel()(4,3) 199 >>> [len(v) for v in e4] 200 [3, 3, 3, 3] 201 >>> e4[0].hybridWith(e4[3],1) 202 (0.0, 0.0, 0.0) 203 """ 204 205 def __call__(self, nvot, ncand, vType=PersonalityVoter): 206 if nvot % 2: 207 raise ValueError 208 basevoter = vType.rand(ncand) 209 return Electorate( 210 ([basevoter] * (nvot // 2)) + ([vType(-q for q in basevoter)] * (nvot // 2)) 211 )
Creates an even number of voters in two diametrically-opposed camps (ie, opposite utilities for all candidates)
>>> e4 = ReverseModel()(4,3)
>>> [len(v) for v in e4]
[3, 3, 3, 3]
>>> e4[0].hybridWith(e4[3],1)
(0.0, 0.0, 0.0)
Inherited Members
1079class Rp(Schulze): 1080 def resolveCycle(self, cmat, n): 1081 """Note: mutates cmat destructively. 1082 1083 >>> Rp().resultsFor(DeterministicModel(3)(5,3),Rp().honBallot,isHonest=True)["results"] 1084 [1, 2, 0] 1085 """ 1086 matches = [(i, j, cmat[i][j]) for i in range(n) for j in range(i, n) if i != j] 1087 rps = sorted(matches, key=lambda x: -abs(x[2])) 1088 for i, j, margin in rps: 1089 if margin < 0: 1090 i, j = j, i 1091 if cmat[j][i] is not True: 1092 # print(i,j,cmat) 1093 cmat[i][j] = True 1094 # print("....",i,j,cmat) 1095 for k in range(n): 1096 if k not in (i, j): 1097 if cmat[j][k] is True: 1098 cmat[i][k] = True 1099 if cmat[k][i] is True: 1100 cmat[k][j] = True 1101 1102 # print(".......",i,j,k,cmat) 1103 1104 return [sum(cmat[i][j] is True for j in range(n)) for i in range(n)]
Base class for election methods. Holds some of the duct tape.
1080 def resolveCycle(self, cmat, n): 1081 """Note: mutates cmat destructively. 1082 1083 >>> Rp().resultsFor(DeterministicModel(3)(5,3),Rp().honBallot,isHonest=True)["results"] 1084 [1, 2, 0] 1085 """ 1086 matches = [(i, j, cmat[i][j]) for i in range(n) for j in range(i, n) if i != j] 1087 rps = sorted(matches, key=lambda x: -abs(x[2])) 1088 for i, j, margin in rps: 1089 if margin < 0: 1090 i, j = j, i 1091 if cmat[j][i] is not True: 1092 # print(i,j,cmat) 1093 cmat[i][j] = True 1094 # print("....",i,j,cmat) 1095 for k in range(n): 1096 if k not in (i, j): 1097 if cmat[j][k] is True: 1098 cmat[i][k] = True 1099 if cmat[k][i] is True: 1100 cmat[k][j] = True 1101 1102 # print(".......",i,j,k,cmat) 1103 1104 return [sum(cmat[i][j] is True for j in range(n)) for i in range(n)]
Note: mutates cmat destructively.
>>> Rp().resultsFor(DeterministicModel(3)(5,3),Rp().honBallot,isHonest=True)["results"]
[1, 2, 0]
Inherited Members
932class Schulze(RankedMethod): 933 def resolveCycle(self, cmat, n): 934 935 beatStrength = [[0] * n] * n 936 numWins = [0] * n 937 for i in range(n): 938 for j in range(n): 939 if i != j: 940 beatStrength[i][j] = cmat[i][j] if cmat[i][j] > cmat[j][i] else 0 941 942 for pivot in range(n): 943 for source in range(n): 944 if pivot != source: 945 for target in range(n): 946 if pivot != target and source != target: 947 beatStrength[source][target] = max( 948 beatStrength[source][target], 949 min( 950 beatStrength[source][pivot], 951 beatStrength[pivot][target], 952 ), 953 ) 954 955 for i in range(n): 956 for j in range(n): 957 if i != j: 958 if beatStrength[i][j] > beatStrength[j][i]: 959 numWins[i] += 1 960 if ( 961 beatStrength[i][j] == beatStrength[j][i] and i < j 962 ): # break ties deterministically 963 numWins[i] += 1 964 965 return numWins 966 967 def results(self, ballots, isHonest=False, **kwargs): 968 """Schulze results. 969 970 >>> Schulze().resultsFor(DeterministicModel(3)(5,3),Schulze().honBallot,isHonest=True)["results"] 971 [2, 0, 1] 972 >>> Schulze.extraEvents 973 {'scenario': 'cycle'} 974 >>> Schulze().results([[0,1,2]],isHonest=True)[2] 975 2 976 >>> Schulze.extraEvents 977 {'scenario': 'easy'} 978 >>> Schulze().results([[0,1,2],[2,1,0]],isHonest=True)[1] 979 1 980 >>> Schulze.extraEvents 981 {'scenario': 'easy'} 982 >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2,isHonest=True) 983 [1, 2, 0] 984 >>> Schulze.extraEvents 985 {'scenario': 'chicken'} 986 >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 2 + [[1,2,0]] * 3,isHonest=True) 987 [1, 2, 0] 988 >>> Schulze.extraEvents 989 {'scenario': 'squeeze'} 990 >>> Schulze().results([[3,2,1,0]] * 5 + [[2,3,1,0]] * 2 + [[0,1,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True) 991 [2, 3, 1, 0] 992 >>> Schulze.extraEvents 993 {'scenario': 'other'} 994 >>> Schulze().results([[3,0,0,0]] * 5 + [[2,3,0,0]] * 2 + [[0,0,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True) 995 [3, 0, 1, 2] 996 >>> Schulze.extraEvents 997 {'scenario': 'spoiler'} 998 """ 999 ballots = ballots_from_dataframe(ballots) 1000 n = len(ballots[0]) 1001 cmat = [[0 for _ in range(n)] for _ in range(n)] 1002 numWins = [0] * n 1003 for i in range(n): 1004 for j in range(n): 1005 if i != j: 1006 cmat[i][j] = sum(sign(ballot[i] - ballot[j]) for ballot in ballots) 1007 if cmat[i][j] > 0: 1008 numWins[i] += 1 1009 elif cmat[i][j] == 0 and i < j: 1010 numWins[i] += 1 1011 condOrder = sorted(enumerate(numWins), key=lambda x: -x[1]) 1012 if condOrder[0][1] == n - 1: 1013 cycle = 0 1014 result = numWins 1015 else: # cycle 1016 cycle = 1 1017 result = self.resolveCycle(cmat, n) 1018 1019 if isHonest: 1020 self.__class__.extraEvents = {} 1021 # check scenarios 1022 plurTally = [0] * n 1023 plur3Tally = [0] * 3 1024 cond3 = [c for c, v in condOrder[:3]] 1025 for b in ballots: 1026 b3 = [b[c] for c in cond3] 1027 plurTally[b.index(max(b))] += 1 1028 plur3Tally[b3.index(max(b3))] += 1 1029 plurOrder = sorted(enumerate(plurTally), key=lambda x: -x[1]) 1030 plur3Order = sorted(enumerate(plur3Tally), key=lambda x: -x[1]) 1031 if cycle: 1032 self.__class__.extraEvents["scenario"] = "cycle" 1033 elif plurOrder[0][0] == condOrder[0][0]: 1034 self.__class__.extraEvents["scenario"] = "easy" 1035 elif plur3Order[0][0] == condOrder[0][0]: 1036 self.__class__.extraEvents["scenario"] = "spoiler" 1037 elif plur3Order[2][0] == condOrder[0][0]: 1038 self.__class__.extraEvents["scenario"] = "squeeze" 1039 elif plur3Order[0][0] == condOrder[2][0]: 1040 self.__class__.extraEvents["scenario"] = "chicken" 1041 else: 1042 self.__class__.extraEvents["scenario"] = "other" 1043 1044 return result 1045 1046 @classmethod 1047 def fillStratBallot( 1048 cls, 1049 voter, 1050 polls, 1051 places, 1052 n, 1053 stratGap, 1054 ballot, 1055 frontId, 1056 frontResult, 1057 targId, 1058 targResult, 1059 ): 1060 1061 if stratGap > 0: 1062 others = [c for (c, r) in places[2:]] 1063 notTooBad = min(voter[frontId], voter[targId]) 1064 decentOnes = [c for c in others if voter[c] >= notTooBad] 1065 cls.fillPrefOrder(voter, ballot, whichCands=decentOnes, lowSlot=n - len(decentOnes)) 1066 # ballot[frontId], ballot[targId] = n-len(decentOnes)-1, n-len(decentOnes)-2 1067 ballot[frontId], ballot[targId] = 0, n - len(decentOnes) - 1 1068 cls.fillPrefOrder( 1069 voter, 1070 ballot, 1071 whichCands=[c for c in others if voter[c] < notTooBad], 1072 lowSlot=1, 1073 ) 1074 else: 1075 ballot[frontId] = n - 1 1076 cls.fillPrefOrder(voter, ballot, whichCands=[c for (c, r) in places[1:]], lowSlot=0)
Base class for election methods. Holds some of the duct tape.
933 def resolveCycle(self, cmat, n): 934 935 beatStrength = [[0] * n] * n 936 numWins = [0] * n 937 for i in range(n): 938 for j in range(n): 939 if i != j: 940 beatStrength[i][j] = cmat[i][j] if cmat[i][j] > cmat[j][i] else 0 941 942 for pivot in range(n): 943 for source in range(n): 944 if pivot != source: 945 for target in range(n): 946 if pivot != target and source != target: 947 beatStrength[source][target] = max( 948 beatStrength[source][target], 949 min( 950 beatStrength[source][pivot], 951 beatStrength[pivot][target], 952 ), 953 ) 954 955 for i in range(n): 956 for j in range(n): 957 if i != j: 958 if beatStrength[i][j] > beatStrength[j][i]: 959 numWins[i] += 1 960 if ( 961 beatStrength[i][j] == beatStrength[j][i] and i < j 962 ): # break ties deterministically 963 numWins[i] += 1 964 965 return numWins
967 def results(self, ballots, isHonest=False, **kwargs): 968 """Schulze results. 969 970 >>> Schulze().resultsFor(DeterministicModel(3)(5,3),Schulze().honBallot,isHonest=True)["results"] 971 [2, 0, 1] 972 >>> Schulze.extraEvents 973 {'scenario': 'cycle'} 974 >>> Schulze().results([[0,1,2]],isHonest=True)[2] 975 2 976 >>> Schulze.extraEvents 977 {'scenario': 'easy'} 978 >>> Schulze().results([[0,1,2],[2,1,0]],isHonest=True)[1] 979 1 980 >>> Schulze.extraEvents 981 {'scenario': 'easy'} 982 >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2,isHonest=True) 983 [1, 2, 0] 984 >>> Schulze.extraEvents 985 {'scenario': 'chicken'} 986 >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 2 + [[1,2,0]] * 3,isHonest=True) 987 [1, 2, 0] 988 >>> Schulze.extraEvents 989 {'scenario': 'squeeze'} 990 >>> Schulze().results([[3,2,1,0]] * 5 + [[2,3,1,0]] * 2 + [[0,1,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True) 991 [2, 3, 1, 0] 992 >>> Schulze.extraEvents 993 {'scenario': 'other'} 994 >>> Schulze().results([[3,0,0,0]] * 5 + [[2,3,0,0]] * 2 + [[0,0,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True) 995 [3, 0, 1, 2] 996 >>> Schulze.extraEvents 997 {'scenario': 'spoiler'} 998 """ 999 ballots = ballots_from_dataframe(ballots) 1000 n = len(ballots[0]) 1001 cmat = [[0 for _ in range(n)] for _ in range(n)] 1002 numWins = [0] * n 1003 for i in range(n): 1004 for j in range(n): 1005 if i != j: 1006 cmat[i][j] = sum(sign(ballot[i] - ballot[j]) for ballot in ballots) 1007 if cmat[i][j] > 0: 1008 numWins[i] += 1 1009 elif cmat[i][j] == 0 and i < j: 1010 numWins[i] += 1 1011 condOrder = sorted(enumerate(numWins), key=lambda x: -x[1]) 1012 if condOrder[0][1] == n - 1: 1013 cycle = 0 1014 result = numWins 1015 else: # cycle 1016 cycle = 1 1017 result = self.resolveCycle(cmat, n) 1018 1019 if isHonest: 1020 self.__class__.extraEvents = {} 1021 # check scenarios 1022 plurTally = [0] * n 1023 plur3Tally = [0] * 3 1024 cond3 = [c for c, v in condOrder[:3]] 1025 for b in ballots: 1026 b3 = [b[c] for c in cond3] 1027 plurTally[b.index(max(b))] += 1 1028 plur3Tally[b3.index(max(b3))] += 1 1029 plurOrder = sorted(enumerate(plurTally), key=lambda x: -x[1]) 1030 plur3Order = sorted(enumerate(plur3Tally), key=lambda x: -x[1]) 1031 if cycle: 1032 self.__class__.extraEvents["scenario"] = "cycle" 1033 elif plurOrder[0][0] == condOrder[0][0]: 1034 self.__class__.extraEvents["scenario"] = "easy" 1035 elif plur3Order[0][0] == condOrder[0][0]: 1036 self.__class__.extraEvents["scenario"] = "spoiler" 1037 elif plur3Order[2][0] == condOrder[0][0]: 1038 self.__class__.extraEvents["scenario"] = "squeeze" 1039 elif plur3Order[0][0] == condOrder[2][0]: 1040 self.__class__.extraEvents["scenario"] = "chicken" 1041 else: 1042 self.__class__.extraEvents["scenario"] = "other" 1043 1044 return result
Schulze results.
>>> Schulze().resultsFor(DeterministicModel(3)(5,3),Schulze().honBallot,isHonest=True)["results"]
[2, 0, 1]
>>> Schulze.extraEvents
{'scenario': 'cycle'}
>>> Schulze().results([[0,1,2]],isHonest=True)[2]
2
>>> Schulze.extraEvents
{'scenario': 'easy'}
>>> Schulze().results([[0,1,2],[2,1,0]],isHonest=True)[1]
1
>>> Schulze.extraEvents
{'scenario': 'easy'}
>>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2,isHonest=True)
[1, 2, 0]
>>> Schulze.extraEvents
{'scenario': 'chicken'}
>>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 2 + [[1,2,0]] * 3,isHonest=True)
[1, 2, 0]
>>> Schulze.extraEvents
{'scenario': 'squeeze'}
>>> Schulze().results([[3,2,1,0]] * 5 + [[2,3,1,0]] * 2 + [[0,1,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
[2, 3, 1, 0]
>>> Schulze.extraEvents
{'scenario': 'other'}
>>> Schulze().results([[3,0,0,0]] * 5 + [[2,3,0,0]] * 2 + [[0,0,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
[3, 0, 1, 2]
>>> Schulze.extraEvents
{'scenario': 'spoiler'}
1046 @classmethod 1047 def fillStratBallot( 1048 cls, 1049 voter, 1050 polls, 1051 places, 1052 n, 1053 stratGap, 1054 ballot, 1055 frontId, 1056 frontResult, 1057 targId, 1058 targResult, 1059 ): 1060 1061 if stratGap > 0: 1062 others = [c for (c, r) in places[2:]] 1063 notTooBad = min(voter[frontId], voter[targId]) 1064 decentOnes = [c for c in others if voter[c] >= notTooBad] 1065 cls.fillPrefOrder(voter, ballot, whichCands=decentOnes, lowSlot=n - len(decentOnes)) 1066 # ballot[frontId], ballot[targId] = n-len(decentOnes)-1, n-len(decentOnes)-2 1067 ballot[frontId], ballot[targId] = 0, n - len(decentOnes) - 1 1068 cls.fillPrefOrder( 1069 voter, 1070 ballot, 1071 whichCands=[c for c in others if voter[c] < notTooBad], 1072 lowSlot=1, 1073 ) 1074 else: 1075 ballot[frontId] = n - 1 1076 cls.fillPrefOrder(voter, ballot, whichCands=[c for (c, r) in places[1:]], lowSlot=0)
Mutates the ballot argument to be a strategic ballot.
>>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
[3, 0, 1, 2]
145def Score(topRank=10, asClass=False): 146 147 class Score0to(Method): 148 """Score voting, 0-10. 149 150 151 Strategy establishes pivots 152 >>> Score().stratBallotFor([0,1,2])(Score, Voter([5,6,7])) 153 [0, 0, 10] 154 >>> Score().stratBallotFor([2,1,0])(Score, Voter([5,6,7])) 155 [0, 10, 10] 156 >>> Score().stratBallotFor([1,0,2])(Score, Voter([5,6,7])) 157 [0, 5.0, 10] 158 159 Strategy (kinda) works for ties 160 >>> Score().stratBallotFor([1,0,2])(Score, Voter([5,6,6])) 161 [0, 10, 10] 162 >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,6,7])) 163 [0, 0, 10] 164 >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,7,6])) 165 [10, 10, 10] 166 >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,5,6])) 167 [10, 0, 10] 168 169 """ 170 171 # >>> qs += [Score().resultsFor(PolyaModel()(101,2),Score.honBallot)[0] for i in range(800)] 172 # >>> std(qs) 173 # 2.770135393419682 174 # >>> mean(qs) 175 # 5.1467202970297032 176 bias2 = 2.770135393419682 177 # >>> qs5 = [Score().resultsFor(PolyaModel()(101,5),Score.honBallot)[0] for i in range(400)] 178 # >>> mean(qs5) 179 # 4.920247524752476 180 # >>> std(qs5) 181 # 2.3536762480634343 182 bias5 = 2.3536762480634343 183 candScore = staticmethod(mean) 184 # """Takes the list of votes for a candidate; returns the candidate's score.""" 185 186 def __str__(self): 187 if self.topRank == 1: 188 return "IdealApproval" 189 return self.__class__.__name__ + str(self.topRank) 190 191 @staticmethod # cls is provided explicitly, not through binding 192 @rememberBallot 193 def honBallot(cls, utils): 194 """Takes utilities and returns an honest ballot (on 0..10) 195 196 197 honest ballots work as expected 198 >>> Score().honBallot(Score, Voter([5,6,7])) 199 [0.0, 5.0, 10.0] 200 >>> Score().resultsFor(DeterministicModel(3)(5,3),Score().honBallot)["results"] 201 [4.0, 6.0, 5.0] 202 """ 203 bot = min(utils) 204 scale = max(utils) - bot 205 return [floor((cls.topRank + 0.99) * (util - bot) / scale) for util in utils] 206 207 @classmethod 208 def fillStratBallot( 209 cls, 210 voter, 211 polls, 212 places, 213 n, 214 stratGap, 215 ballot, 216 frontId, 217 frontResult, 218 targId, 219 targResult, 220 ): 221 """Returns a (function which takes utilities and returns a strategic ballot) 222 for the given "polling" info.""" 223 224 cuts = [voter[frontId], voter[targId]] 225 if stratGap > 0: 226 # sort cuts high to low 227 cuts = (cuts[1], cuts[0]) 228 if cuts[0] == cuts[1]: 229 strat = [(cls.topRank if (util >= cuts[0]) else 0) for util in voter] 230 else: 231 strat = [ 232 max( 233 0, 234 min( 235 cls.topRank, 236 floor((cls.topRank + 0.99) * (util - cuts[1]) / (cuts[0] - cuts[1])), 237 ), 238 ) 239 for util in voter 240 ] 241 for i in range(n): 242 ballot[i] = strat[i] 243 244 Score0to.topRank = topRank 245 return Score0to if asClass else Score0to()
24class SideTally(defaultdict): 25 """Used for keeping track of how many voters are being strategic, etc. 26 27 DO NOT use plain +; for this class, it is equivalent to +=, but less readable. 28 29 """ 30 31 def __init__(self): 32 super().__init__(int) 33 34 def initKeys(self, chooser): 35 try: 36 self.keyList = chooser.allTallyKeys() 37 except AttributeError: 38 try: 39 self.keyList = list(chooser) 40 except TypeError: 41 self.keyList = [] 42 self.initKeys = staticmethod(lambda x: x) # don't do it again 43 44 def serialize(self): 45 try: 46 return [self[key] for key in self.keyList] 47 except AttributeError: 48 return [] 49 50 def fullSerialize(self): 51 if not hasattr(self, "keyList"): 52 return [self[key] for key in self.keys()] 53 return [self[key] for key in self.keyList] 54 55 def itemList(self): 56 try: 57 kl = self.keyList 58 return [(k, self[k]) for k in kl] + [(k, self[k]) for k in self.keys() if k not in kl] 59 except AttributeError: 60 return list(self.items())
Used for keeping track of how many voters are being strategic, etc.
DO NOT use plain +; for this class, it is equivalent to +=, but less readable.
276def Srv(topRank=10): 277 """Score Runoff Voting 278 >>> Srv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"] 279 [1.2, 0.8, 1.21] 280 >>> Srv().results([[0,1,2]])[2] 281 2.0 282 >>> Srv().results([[0,1,2],[2,1,0]])[1] 283 1.0 284 >>> Srv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2) 285 [0.8888888888888888, 1.2222222222222223, 0.8888888888888888] 286 >>> Srv().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100) 287 [1.502537313432836, 1.492537313432836, 0.5074626865671642] 288 >>> Srv().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5) 289 [1.0526315789473684, 1.105263157894737, 0.8421052631578947] 290 >>> Srv().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6) 291 [1.0666666666666667, 3.6, 3.4, 0.8666666666666667, 1.3333333333333333] 292 """ 293 294 score0to = Score(topRank, True) 295 296 class Srv0to(score0to): 297 stratTargetFor = Method.stratTarget3 298 299 def results(self, ballots, **kwargs): 300 """Srv results.""" 301 ballots = ballots_from_dataframe(ballots) 302 baseResults = super(Srv0to, self).results(ballots, **kwargs) 303 (runnerUp, top) = sorted(range(len(baseResults)), key=lambda i: baseResults[i])[-2:] 304 upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots) 305 if upset > 0: 306 baseResults[runnerUp] = baseResults[top] + 0.01 307 return baseResults 308 309 return Srv0to()
Score Runoff Voting
>>> Srv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
[1.2, 0.8, 1.21]
>>> Srv().results([[0,1,2]])[2]
2.0
>>> Srv().results([[0,1,2],[2,1,0]])[1]
1.0
>>> Srv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[0.8888888888888888, 1.2222222222222223, 0.8888888888888888]
>>> Srv().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
[1.502537313432836, 1.492537313432836, 0.5074626865671642]
>>> Srv().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
[1.0526315789473684, 1.105263157894737, 0.8421052631578947]
>>> Srv().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
[1.0666666666666667, 3.6, 3.4, 0.8666666666666667, 1.3333333333333333]
63class Tallies(list): 64 """Used (ONCE) as an enumerator, gives an inexhaustible flow of SideTally objects. 65 After that, use as list to see those objects. 66 67 >>> ts = Tallies() 68 >>> for i, j in zip(ts, [5,4,3]): 69 ... i[j] += j 70 ... 71 >>> [t.serialize() for t in ts] 72 [[], [], [], []] 73 >>> [t.fullSerialize() for t in ts] 74 [[5], [4], [3], []] 75 >>> [t.initKeys([k]) for (t,k) in zip(ts,[6,4,3])] 76 [None, None, None] 77 >>> [t.serialize() for t in ts] 78 [[0], [4], [3], []] 79 """ 80 81 def __iter__(self): 82 if getattr(self, "used", False): 83 return super().__iter__() 84 self.used = True 85 return self._generated_tallies() 86 87 def __eq__(self, other): 88 if not isinstance(other, Tallies): 89 return super().__eq__(other) 90 return super().__eq__(other) and getattr(self, "used", False) == getattr( 91 other, "used", False 92 ) 93 94 def _generated_tallies(self): 95 while True: 96 tally = SideTally() 97 self.append(tally) 98 yield tally
Used (ONCE) as an enumerator, gives an inexhaustible flow of SideTally objects. After that, use as list to see those objects.
>>> ts = Tallies()
>>> for i, j in zip(ts, [5,4,3]):
... i[j] += j
...
>>> [t.serialize() for t in ts]
[[], [], [], []]
>>> [t.fullSerialize() for t in ts]
[[5], [4], [3], []]
>>> [t.initKeys([k]) for (t,k) in zip(ts,[6,4,3])]
[None, None, None]
>>> [t.serialize() for t in ts]
[[0], [4], [3], []]
776class V321(Mav): 777 baseCuts = [-0.1, 0.8] 778 specificPercentiles = [45, 75] 779 780 stratTargetFor = Method.stratTarget3 781 782 def results(self, ballots, isHonest=False, **kwargs): 783 """3-2-1 Voting results. 784 785 >>> V321().resultsFor(DeterministicModel(3)(5,3),V321().honBallot)["results"] 786 [-0.75, 2, 1] 787 >>> V321().results([[0,1,2]])[2] 788 2 789 >>> V321().results([[0,1,2],[2,1,0]])[1] 790 2.5 791 >>> V321().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2) 792 [1, 1.5, -0.25] 793 >>> V321().results([[0,1,2,1]]*29 + [[1,2,0,1]]*30 + [[2,0,1,1]]*31 + [[1,1,1,2]]*10) 794 [3, 0.5, 1, 0] 795 >>> V321().results([[1,0,2,1]]*29 + [[0,2,1,1]]*30 + [[2,1,0,1]]*31 + [[1,1,1,2]]*10) 796 [3.375, 2.875, 0.25, 0] 797 """ 798 ballots = ballots_from_dataframe(ballots) 799 candScores = list(zip(*ballots)) 800 n2s = [sum(1 if s > 1 else 0 for s in c) for c in candScores] 801 o2s = argsort(n2s) # order 802 r2s = [-1] * len(n2s) # ranks 803 for r, i in enumerate(o2s): 804 r2s[i] = r 805 semifinalists = o2s[-3:] # [third, second, first] by top ranks 806 # print(semifinalists) 807 n1s = [sum(1 if s > 0 else 0 for s in candScores[sf]) for sf in semifinalists] 808 o1s = argsort(n1s) 809 # print("n1s",n1s) 810 # print("o1s",o1s) 811 # print([semifinalists[o] for o in o1s]) #[third, second, first] by above-bottom 812 # print("r2s",r2s) 813 r2s[semifinalists[o1s[0]]] -= (o1s[0] + 1) * 0.75 # non-finalist below finalists 814 815 (runnerUp, top) = semifinalists[o1s[1]], semifinalists[o1s[2]] 816 upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots) 817 if upset > 0: 818 runnerUp, top = top, runnerUp 819 r2s[runnerUp], r2s[top] = r2s[top] - 0.125, r2s[runnerUp] + 0.125 820 r2s[top] = max(r2s[top], r2s[runnerUp] + 0.5) 821 if isHonest: 822 upset2 = sum( 823 sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[2]]]) 824 for ballot in ballots 825 ) 826 self.__class__.extraEvents["3beats1"] = upset2 > 0 827 upset3 = sum( 828 sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[1]]]) 829 for ballot in ballots 830 ) 831 self.__class__.extraEvents["3beats2"] = upset3 > 0 832 if len(o2s) > 3: 833 fourth = o2s[-4] 834 fourthNotLasts = sum(1 if s > 1 else 0 for s in candScores[fourth]) 835 fourthWin = ( 836 fourthNotLasts > n1s[o1s[1]] 837 and sum( 838 sign(ballot[fourth] - ballot[semifinalists[o1s[2]]]) for ballot in ballots 839 ) 840 > 0 841 ) 842 self.__class__.extraEvents["4beats1"] = fourthWin 843 844 return [as_builtin_scalar(score) for score in r2s] 845 846 def stratBallotFor(self, polls): 847 """Returns a function which takes utilities and returns a dict( 848 isStrat= 849 for the given "polling" info. 850 851 852 >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2])) 853 [2, 1, 0, 3] 854 """ 855 places = sorted(enumerate(polls), key=lambda x: -x[1]) # high to low 856 top3 = [c for c, r in places[:3]] 857 858 # @rememberBallots ... do it later 859 def stratBallot(cls, voter): 860 stratGap = voter[top3[1]] - voter[top3[0]] 861 myPrefs = [c for c, v in sorted(enumerate(voter), key=lambda x: -x[1])] # high to low 862 my3order = [myPrefs.index(c) for c in top3] 863 rating = 2 864 ballot = [0] * len(voter) 865 if my3order[0] == min(my3order): # agree on winner 866 for i in range(my3order[0] + 1): 867 ballot[myPrefs[i]] = 2 868 if my3order[1] <= my3order[2]: 869 for i in range(my3order[0] + 1, my3order[1] + 1): 870 ballot[myPrefs[i]] = 1 871 # print("agree",top3, my3order,ballot,[float('%.1g' % c) for c in voter]) 872 return dict(strat=ballot, isStrat=False, stratGap=stratGap) 873 for c in myPrefs: 874 ballot[c] = rating 875 if rating and (c in top3): 876 if c == top3[0]: 877 rating = 0 878 else: 879 rating -= 1 880 881 # print("disagree",top3,my3order,ballot,[float('%.1g' % c) for c in voter]) 882 return dict(strat=ballot, isStrat=True, stratGap=stratGap) 883 884 if self.extraEvents["3beats1"]: 885 886 @rememberBallots 887 def stratBallo2(cls, voter): 888 myprefs = sorted(enumerate(voter), key=lambda x: -x[1]) # high to low 889 rating = 2 890 ballot = [None] * len(voter) 891 isStrat = False 892 stratGap = 0 893 for c, util in myprefs: 894 ballot[c] = rating 895 if rating and (c in top3): 896 if c == top3[2]: 897 rating = 0 898 else: 899 rating -= 1 900 isStrat = voter[top3[0]] == max(voter[c] for c in top3) 901 return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap) 902 903 stratBallo2.__name__ = "stratBallot" # God, that's ugly. 904 return stratBallo2 905 906 if self.extraEvents["4beats1"]: 907 fourth = places[3][0] 908 first = top3[1] 909 910 @rememberBallots 911 def stratBallo3(cls, voter): 912 stratGap = voter[top3[1]] - voter[top3[0]] 913 myprefs = sorted(enumerate(voter), key=lambda x: -x[1]) # high to low 914 915 rating = 2 916 ballot = [None] * len(voter) 917 if voter[fourth] > voter[first]: 918 for c, util in myprefs: 919 ballot[c] = rating 920 if rating and (c == fourth): 921 rating -= 2 922 return dict(strat=ballot, isStrat=True, stratGap=stratGap) 923 924 return stratBallot(cls, voter) 925 926 stratBallo3.__name__ = "stratBallot" # God, that's ugly. 927 return stratBallo3 928 929 return rememberBallots(stratBallot)
Majority Approval Voting
782 def results(self, ballots, isHonest=False, **kwargs): 783 """3-2-1 Voting results. 784 785 >>> V321().resultsFor(DeterministicModel(3)(5,3),V321().honBallot)["results"] 786 [-0.75, 2, 1] 787 >>> V321().results([[0,1,2]])[2] 788 2 789 >>> V321().results([[0,1,2],[2,1,0]])[1] 790 2.5 791 >>> V321().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2) 792 [1, 1.5, -0.25] 793 >>> V321().results([[0,1,2,1]]*29 + [[1,2,0,1]]*30 + [[2,0,1,1]]*31 + [[1,1,1,2]]*10) 794 [3, 0.5, 1, 0] 795 >>> V321().results([[1,0,2,1]]*29 + [[0,2,1,1]]*30 + [[2,1,0,1]]*31 + [[1,1,1,2]]*10) 796 [3.375, 2.875, 0.25, 0] 797 """ 798 ballots = ballots_from_dataframe(ballots) 799 candScores = list(zip(*ballots)) 800 n2s = [sum(1 if s > 1 else 0 for s in c) for c in candScores] 801 o2s = argsort(n2s) # order 802 r2s = [-1] * len(n2s) # ranks 803 for r, i in enumerate(o2s): 804 r2s[i] = r 805 semifinalists = o2s[-3:] # [third, second, first] by top ranks 806 # print(semifinalists) 807 n1s = [sum(1 if s > 0 else 0 for s in candScores[sf]) for sf in semifinalists] 808 o1s = argsort(n1s) 809 # print("n1s",n1s) 810 # print("o1s",o1s) 811 # print([semifinalists[o] for o in o1s]) #[third, second, first] by above-bottom 812 # print("r2s",r2s) 813 r2s[semifinalists[o1s[0]]] -= (o1s[0] + 1) * 0.75 # non-finalist below finalists 814 815 (runnerUp, top) = semifinalists[o1s[1]], semifinalists[o1s[2]] 816 upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots) 817 if upset > 0: 818 runnerUp, top = top, runnerUp 819 r2s[runnerUp], r2s[top] = r2s[top] - 0.125, r2s[runnerUp] + 0.125 820 r2s[top] = max(r2s[top], r2s[runnerUp] + 0.5) 821 if isHonest: 822 upset2 = sum( 823 sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[2]]]) 824 for ballot in ballots 825 ) 826 self.__class__.extraEvents["3beats1"] = upset2 > 0 827 upset3 = sum( 828 sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[1]]]) 829 for ballot in ballots 830 ) 831 self.__class__.extraEvents["3beats2"] = upset3 > 0 832 if len(o2s) > 3: 833 fourth = o2s[-4] 834 fourthNotLasts = sum(1 if s > 1 else 0 for s in candScores[fourth]) 835 fourthWin = ( 836 fourthNotLasts > n1s[o1s[1]] 837 and sum( 838 sign(ballot[fourth] - ballot[semifinalists[o1s[2]]]) for ballot in ballots 839 ) 840 > 0 841 ) 842 self.__class__.extraEvents["4beats1"] = fourthWin 843 844 return [as_builtin_scalar(score) for score in r2s]
3-2-1 Voting results.
>>> V321().resultsFor(DeterministicModel(3)(5,3),V321().honBallot)["results"]
[-0.75, 2, 1]
>>> V321().results([[0,1,2]])[2]
2
>>> V321().results([[0,1,2],[2,1,0]])[1]
2.5
>>> V321().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[1, 1.5, -0.25]
>>> V321().results([[0,1,2,1]]*29 + [[1,2,0,1]]*30 + [[2,0,1,1]]*31 + [[1,1,1,2]]*10)
[3, 0.5, 1, 0]
>>> V321().results([[1,0,2,1]]*29 + [[0,2,1,1]]*30 + [[2,1,0,1]]*31 + [[1,1,1,2]]*10)
[3.375, 2.875, 0.25, 0]
846 def stratBallotFor(self, polls): 847 """Returns a function which takes utilities and returns a dict( 848 isStrat= 849 for the given "polling" info. 850 851 852 >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2])) 853 [2, 1, 0, 3] 854 """ 855 places = sorted(enumerate(polls), key=lambda x: -x[1]) # high to low 856 top3 = [c for c, r in places[:3]] 857 858 # @rememberBallots ... do it later 859 def stratBallot(cls, voter): 860 stratGap = voter[top3[1]] - voter[top3[0]] 861 myPrefs = [c for c, v in sorted(enumerate(voter), key=lambda x: -x[1])] # high to low 862 my3order = [myPrefs.index(c) for c in top3] 863 rating = 2 864 ballot = [0] * len(voter) 865 if my3order[0] == min(my3order): # agree on winner 866 for i in range(my3order[0] + 1): 867 ballot[myPrefs[i]] = 2 868 if my3order[1] <= my3order[2]: 869 for i in range(my3order[0] + 1, my3order[1] + 1): 870 ballot[myPrefs[i]] = 1 871 # print("agree",top3, my3order,ballot,[float('%.1g' % c) for c in voter]) 872 return dict(strat=ballot, isStrat=False, stratGap=stratGap) 873 for c in myPrefs: 874 ballot[c] = rating 875 if rating and (c in top3): 876 if c == top3[0]: 877 rating = 0 878 else: 879 rating -= 1 880 881 # print("disagree",top3,my3order,ballot,[float('%.1g' % c) for c in voter]) 882 return dict(strat=ballot, isStrat=True, stratGap=stratGap) 883 884 if self.extraEvents["3beats1"]: 885 886 @rememberBallots 887 def stratBallo2(cls, voter): 888 myprefs = sorted(enumerate(voter), key=lambda x: -x[1]) # high to low 889 rating = 2 890 ballot = [None] * len(voter) 891 isStrat = False 892 stratGap = 0 893 for c, util in myprefs: 894 ballot[c] = rating 895 if rating and (c in top3): 896 if c == top3[2]: 897 rating = 0 898 else: 899 rating -= 1 900 isStrat = voter[top3[0]] == max(voter[c] for c in top3) 901 return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap) 902 903 stratBallo2.__name__ = "stratBallot" # God, that's ugly. 904 return stratBallo2 905 906 if self.extraEvents["4beats1"]: 907 fourth = places[3][0] 908 first = top3[1] 909 910 @rememberBallots 911 def stratBallo3(cls, voter): 912 stratGap = voter[top3[1]] - voter[top3[0]] 913 myprefs = sorted(enumerate(voter), key=lambda x: -x[1]) # high to low 914 915 rating = 2 916 ballot = [None] * len(voter) 917 if voter[fourth] > voter[first]: 918 for c, util in myprefs: 919 ballot[c] = rating 920 if rating and (c == fourth): 921 rating -= 2 922 return dict(strat=ballot, isStrat=True, stratGap=stratGap) 923 924 return stratBallot(cls, voter) 925 926 stratBallo3.__name__ = "stratBallot" # God, that's ugly. 927 return stratBallo3 928 929 return rememberBallots(stratBallot)
Returns a function which takes utilities and returns a dict( isStrat= for the given "polling" info.
>>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
[2, 1, 0, 3]
228@dataclass(frozen=True) 229class VseResults: 230 """Pandas-backed simulation result set. 231 232 ``frame`` is the canonical tabular representation. Convenience methods 233 return DataFrames or matplotlib axes so notebook workflows can keep chaining. 234 """ 235 236 frame: pd.DataFrame 237 238 @classmethod 239 def from_rows(cls, rows: Iterable[dict] | pd.DataFrame | VseResults) -> "VseResults": 240 return cls(to_dataframe(rows)) 241 242 @classmethod 243 def from_csv(cls, path) -> "VseResults": 244 return cls(pd.read_csv(Path(path), comment="#")) 245 246 @classmethod 247 def concat(cls, results: Iterable[VseResults | pd.DataFrame | Iterable[dict]]) -> "VseResults": 248 frames = [rows_to_dataframe(result) for result in results] 249 return cls(pd.concat(frames, ignore_index=True)) 250 251 def __len__(self) -> int: 252 return len(self.frame) 253 254 @property 255 def dataframe(self) -> pd.DataFrame: 256 """Return the backing DataFrame for fluent notebook work.""" 257 return self.frame 258 259 @property 260 def df(self) -> pd.DataFrame: 261 """Alias for ``dataframe``.""" 262 return self.frame 263 264 def to_dataframe(self, copy=True) -> pd.DataFrame: 265 return self.frame.copy() if copy else self.frame 266 267 def to_csv(self, path, index=False, **kwargs): 268 """Write the result DataFrame to CSV and return the path.""" 269 self.frame.to_csv(path, index=index, **kwargs) 270 return path 271 272 def summarize( 273 self, 274 group_by=DEFAULT_GROUP_BY, 275 sort_by="mean_vse", 276 ascending=False, 277 ) -> pd.DataFrame: 278 """Return aggregate VSE metrics grouped by one or more columns.""" 279 group_columns = _group_columns(group_by) 280 summary = ( 281 self.frame.groupby(group_columns, as_index=False) 282 .agg( 283 rows=("vse", "size"), 284 elections=("eid", "nunique"), 285 mean_vse=("vse", "mean"), 286 median_vse=("vse", "median"), 287 min_vse=("vse", "min"), 288 max_vse=("vse", "max"), 289 std_vse=("vse", "std"), 290 ) 291 .fillna({"std_vse": 0}) 292 ) 293 return summary.sort_values( 294 [sort_by, *group_columns], 295 ascending=[ascending, *[True] * len(group_columns)], 296 ).reset_index(drop=True) 297 298 def leaderboard(self, n=10, group_by="method", by="mean_vse") -> pd.DataFrame: 299 """Return the top groups by a summary metric.""" 300 return self.summarize(group_by=group_by, sort_by=by).head(n) 301 302 def pivot( 303 self, 304 index="method", 305 columns="chooser", 306 values="mean_vse", 307 group_by=None, 308 ) -> pd.DataFrame: 309 """Return a comparison matrix from summarized result data.""" 310 index_columns = _group_columns(index) 311 column_columns = _group_columns(columns) 312 if group_by is None: 313 group_by = _unique_columns(index, columns) 314 summary = self.summarize(group_by=group_by).copy() 315 pivot_columns = [] 316 for column in column_columns: 317 if column in index_columns: 318 column_alias = f"__vse_sim_pivot_{column}" 319 summary[column_alias] = summary[column] 320 pivot_columns.append(column_alias) 321 else: 322 pivot_columns.append(column) 323 pivoted = summary.pivot( 324 index=index, 325 columns=pivot_columns[0] if isinstance(columns, str) else pivot_columns, 326 values=values, 327 ) 328 pivoted.columns = pivoted.columns.set_names(column_columns) 329 return pivoted 330 331 def report(self, group_by=DEFAULT_GROUP_BY) -> dict[str, pd.DataFrame]: 332 """Build common report tables from a result set.""" 333 tables = { 334 "results": self.to_dataframe(), 335 "summary": self.summarize(group_by=group_by), 336 "method_summary": self.summarize(group_by="method"), 337 } 338 if "chooser" in self.frame: 339 tables["chooser_summary"] = self.summarize(group_by="chooser") 340 tables["method_by_chooser"] = self.pivot() 341 return tables 342 343 def plot_vse( 344 self, 345 group_by="method", 346 value="mean_vse", 347 kind="bar", 348 ax=None, 349 title=None, 350 **kwargs, 351 ): 352 """Plot summarized VSE scores and return the matplotlib axes.""" 353 summary = self.summarize(group_by=group_by) 354 group_columns = _group_columns(group_by) 355 labels = summary[group_columns].astype(str).agg(" | ".join, axis=1) 356 plot_frame = summary.assign(label=labels).set_index("label") 357 axes = plot_frame[value].plot(kind=kind, ax=ax, **kwargs) 358 axes.set_xlabel("VSE" if kind == "barh" else "") 359 axes.set_ylabel("" if kind == "barh" else "VSE") 360 axes.set_title(title or f"{value} by {' / '.join(group_columns)}") 361 return axes
Pandas-backed simulation result set.
frame is the canonical tabular representation. Convenience methods
return DataFrames or matplotlib axes so notebook workflows can keep chaining.
254 @property 255 def dataframe(self) -> pd.DataFrame: 256 """Return the backing DataFrame for fluent notebook work.""" 257 return self.frame
Return the backing DataFrame for fluent notebook work.
259 @property 260 def df(self) -> pd.DataFrame: 261 """Alias for ``dataframe``.""" 262 return self.frame
Alias for dataframe.
267 def to_csv(self, path, index=False, **kwargs): 268 """Write the result DataFrame to CSV and return the path.""" 269 self.frame.to_csv(path, index=index, **kwargs) 270 return path
Write the result DataFrame to CSV and return the path.
272 def summarize( 273 self, 274 group_by=DEFAULT_GROUP_BY, 275 sort_by="mean_vse", 276 ascending=False, 277 ) -> pd.DataFrame: 278 """Return aggregate VSE metrics grouped by one or more columns.""" 279 group_columns = _group_columns(group_by) 280 summary = ( 281 self.frame.groupby(group_columns, as_index=False) 282 .agg( 283 rows=("vse", "size"), 284 elections=("eid", "nunique"), 285 mean_vse=("vse", "mean"), 286 median_vse=("vse", "median"), 287 min_vse=("vse", "min"), 288 max_vse=("vse", "max"), 289 std_vse=("vse", "std"), 290 ) 291 .fillna({"std_vse": 0}) 292 ) 293 return summary.sort_values( 294 [sort_by, *group_columns], 295 ascending=[ascending, *[True] * len(group_columns)], 296 ).reset_index(drop=True)
Return aggregate VSE metrics grouped by one or more columns.
298 def leaderboard(self, n=10, group_by="method", by="mean_vse") -> pd.DataFrame: 299 """Return the top groups by a summary metric.""" 300 return self.summarize(group_by=group_by, sort_by=by).head(n)
Return the top groups by a summary metric.
302 def pivot( 303 self, 304 index="method", 305 columns="chooser", 306 values="mean_vse", 307 group_by=None, 308 ) -> pd.DataFrame: 309 """Return a comparison matrix from summarized result data.""" 310 index_columns = _group_columns(index) 311 column_columns = _group_columns(columns) 312 if group_by is None: 313 group_by = _unique_columns(index, columns) 314 summary = self.summarize(group_by=group_by).copy() 315 pivot_columns = [] 316 for column in column_columns: 317 if column in index_columns: 318 column_alias = f"__vse_sim_pivot_{column}" 319 summary[column_alias] = summary[column] 320 pivot_columns.append(column_alias) 321 else: 322 pivot_columns.append(column) 323 pivoted = summary.pivot( 324 index=index, 325 columns=pivot_columns[0] if isinstance(columns, str) else pivot_columns, 326 values=values, 327 ) 328 pivoted.columns = pivoted.columns.set_names(column_columns) 329 return pivoted
Return a comparison matrix from summarized result data.
331 def report(self, group_by=DEFAULT_GROUP_BY) -> dict[str, pd.DataFrame]: 332 """Build common report tables from a result set.""" 333 tables = { 334 "results": self.to_dataframe(), 335 "summary": self.summarize(group_by=group_by), 336 "method_summary": self.summarize(group_by="method"), 337 } 338 if "chooser" in self.frame: 339 tables["chooser_summary"] = self.summarize(group_by="chooser") 340 tables["method_by_chooser"] = self.pivot() 341 return tables
Build common report tables from a result set.
343 def plot_vse( 344 self, 345 group_by="method", 346 value="mean_vse", 347 kind="bar", 348 ax=None, 349 title=None, 350 **kwargs, 351 ): 352 """Plot summarized VSE scores and return the matplotlib axes.""" 353 summary = self.summarize(group_by=group_by) 354 group_columns = _group_columns(group_by) 355 labels = summary[group_columns].astype(str).agg(" | ".join, axis=1) 356 plot_frame = summary.assign(label=labels).set_index("label") 357 axes = plot_frame[value].plot(kind=kind, ax=ax, **kwargs) 358 axes.set_xlabel("VSE" if kind == "barh" else "") 359 axes.set_ylabel("" if kind == "barh" else "VSE") 360 axes.set_title(title or f"{value} by {' / '.join(group_columns)}") 361 return axes
Plot summarized VSE scores and return the matplotlib axes.
10class Voter(tuple): 11 """A tuple of candidate utilities.""" 12 13 def __new__(cls, utils=()): 14 return super().__new__(cls, (as_builtin_scalar(util) for util in utils)) 15 16 @property 17 def dataframe(self): 18 """Return this voter's candidate utilities as a tidy DataFrame.""" 19 return self.to_dataframe() 20 21 @property 22 def df(self): 23 """Alias for ``dataframe``.""" 24 return self.dataframe 25 26 @classmethod 27 def rand(cls, ncand): 28 """Create a random voter with an independent standard normal 29 utility for each candidate. 30 31 ncand determines the number of candidates a voter should have 32 utilities for. 33 >>> [len(Voter.rand(i)) for i in list(range(5))] 34 [0, 1, 2, 3, 4] 35 36 utilities should be in a standard normal distribution 37 >>> v100 = Voter.rand(100) 38 >>> -0.5 < mean(v100) < 0.5 39 True 40 >>> 0.6 < std(v100) < 1.4 41 True 42 """ 43 return cls(random.gauss(0, 1) for _ in range(ncand)) 44 45 def hybridWith(self, v2, w2): 46 """Create a weighted average of two voters. 47 48 The weight of v1 is always 1; w2 is the weight of v2 relative to that. 49 50 If both are 51 standard normal to start with, the result will be standard normal too. 52 53 Length must be the same 54 >>> Voter([1,2]).hybridWith(Voter([1,2,3]),1) 55 Traceback (most recent call last): 56 ... 57 AssertionError 58 59 A couple of basic sanity checks: 60 >>> v2 = Voter([1,2]).hybridWith(Voter([3,2]),1) 61 >>> [round(u,5) for u in v2.hybridWith(v2,1)] 62 [4.0, 4.0] 63 >>> Voter([1,2,5]).hybridWith(Voter([-0.5,-1,0]),0.75) 64 (0.5, 1.0, 4.0) 65 """ 66 assert len(self) == len(v2) 67 return self.copyWithUtils( 68 ((self[i] / sqrt(1 + w2**2)) + (w2 * v2[i] / sqrt(1 + w2**2))) for i in range(len(self)) 69 ) 70 71 def copyWithUtils(self, utils): 72 """create a new voter with attrs as self and given utils. 73 74 This version is a stub, since this voter class has no attrs.""" 75 return self.__class__(utils) 76 77 def to_dataframe(self, voter_id=None, **kwargs): 78 """Return this voter's candidate utilities as a tidy DataFrame.""" 79 from .dataframe import voter_to_dataframe 80 81 return voter_to_dataframe(self, voter_id=voter_id, **kwargs) 82 83 def mutantChild(self, muteWeight): 84 """Returns a copy hybridized with a random voter of weight muteWeight. 85 86 Should remain standard normal: 87 >>> v100 = Voter.rand(100) 88 >>> for i in range(30): 89 ... v100 = v100.mutantChild(random.random()) 90 ... 91 >>> -0.3 < mean(v100) < 0.3 #3 sigma 92 True 93 >>> 0.8 < std(v100) < 1.2 #meh that's roughly 3 sigma 94 True 95 96 """ 97 return self.hybridWith(self.__class__.rand(len(self)), muteWeight)
A tuple of candidate utilities.
16 @property 17 def dataframe(self): 18 """Return this voter's candidate utilities as a tidy DataFrame.""" 19 return self.to_dataframe()
Return this voter's candidate utilities as a tidy DataFrame.
26 @classmethod 27 def rand(cls, ncand): 28 """Create a random voter with an independent standard normal 29 utility for each candidate. 30 31 ncand determines the number of candidates a voter should have 32 utilities for. 33 >>> [len(Voter.rand(i)) for i in list(range(5))] 34 [0, 1, 2, 3, 4] 35 36 utilities should be in a standard normal distribution 37 >>> v100 = Voter.rand(100) 38 >>> -0.5 < mean(v100) < 0.5 39 True 40 >>> 0.6 < std(v100) < 1.4 41 True 42 """ 43 return cls(random.gauss(0, 1) for _ in range(ncand))
Create a random voter with an independent standard normal utility for each candidate.
ncand determines the number of candidates a voter should have utilities for.
[len(Voter.rand(i)) for i in list(range(5))] [0, 1, 2, 3, 4]
utilities should be in a standard normal distribution
v100 = Voter.rand(100) -0.5 < mean(v100) < 0.5 True 0.6 < std(v100) < 1.4 True
45 def hybridWith(self, v2, w2): 46 """Create a weighted average of two voters. 47 48 The weight of v1 is always 1; w2 is the weight of v2 relative to that. 49 50 If both are 51 standard normal to start with, the result will be standard normal too. 52 53 Length must be the same 54 >>> Voter([1,2]).hybridWith(Voter([1,2,3]),1) 55 Traceback (most recent call last): 56 ... 57 AssertionError 58 59 A couple of basic sanity checks: 60 >>> v2 = Voter([1,2]).hybridWith(Voter([3,2]),1) 61 >>> [round(u,5) for u in v2.hybridWith(v2,1)] 62 [4.0, 4.0] 63 >>> Voter([1,2,5]).hybridWith(Voter([-0.5,-1,0]),0.75) 64 (0.5, 1.0, 4.0) 65 """ 66 assert len(self) == len(v2) 67 return self.copyWithUtils( 68 ((self[i] / sqrt(1 + w2**2)) + (w2 * v2[i] / sqrt(1 + w2**2))) for i in range(len(self)) 69 )
Create a weighted average of two voters.
The weight of v1 is always 1; w2 is the weight of v2 relative to that.
If both are standard normal to start with, the result will be standard normal too.
Length must be the same
Voter([1,2]).hybridWith(Voter([1,2,3]),1) Traceback (most recent call last): ... AssertionError
A couple of basic sanity checks:
v2 = Voter([1,2]).hybridWith(Voter([3,2]),1) [round(u,5) for u in v2.hybridWith(v2,1)] [4.0, 4.0] Voter([1,2,5]).hybridWith(Voter([-0.5,-1,0]),0.75) (0.5, 1.0, 4.0)
71 def copyWithUtils(self, utils): 72 """create a new voter with attrs as self and given utils. 73 74 This version is a stub, since this voter class has no attrs.""" 75 return self.__class__(utils)
create a new voter with attrs as self and given utils.
This version is a stub, since this voter class has no attrs.
77 def to_dataframe(self, voter_id=None, **kwargs): 78 """Return this voter's candidate utilities as a tidy DataFrame.""" 79 from .dataframe import voter_to_dataframe 80 81 return voter_to_dataframe(self, voter_id=voter_id, **kwargs)
Return this voter's candidate utilities as a tidy DataFrame.
83 def mutantChild(self, muteWeight): 84 """Returns a copy hybridized with a random voter of weight muteWeight. 85 86 Should remain standard normal: 87 >>> v100 = Voter.rand(100) 88 >>> for i in range(30): 89 ... v100 = v100.mutantChild(random.random()) 90 ... 91 >>> -0.3 < mean(v100) < 0.3 #3 sigma 92 True 93 >>> 0.8 < std(v100) < 1.2 #meh that's roughly 3 sigma 94 True 95 96 """ 97 return self.hybridWith(self.__class__.rand(len(self)), muteWeight)
Returns a copy hybridized with a random voter of weight muteWeight.
Should remain standard normal:
v100 = Voter.rand(100) for i in range(30): ... v100 = v100.mutantChild(random.random()) ... -0.3 < mean(v100) < 0.3 #3 sigma True 0.8 < std(v100) < 1.2 #meh that's roughly 3 sigma True
157def ballots_from_dataframe( 158 ballots, 159 voter_column="voter", 160 candidate_column="candidate", 161 value_column="ballot", 162 candidate_prefix="candidate_", 163): 164 """Convert tidy or wide ballot DataFrames back to method-ready ballots.""" 165 if not isinstance(ballots, pd.DataFrame): 166 return ballots if type(ballots) is list else list(ballots) 167 168 if {voter_column, candidate_column, value_column} <= set(ballots.columns): 169 return ( 170 ballots.pivot(index=voter_column, columns=candidate_column, values=value_column) 171 .sort_index() 172 .sort_index(axis=1) 173 .to_numpy() 174 .tolist() 175 ) 176 177 candidate_columns = [ 178 column for column in ballots.columns if str(column).startswith(candidate_prefix) 179 ] 180 if candidate_columns: 181 candidate_columns = sorted( 182 candidate_columns, key=lambda column: int(str(column).split("_")[-1]) 183 ) 184 return ballots[candidate_columns].to_numpy().tolist() 185 186 return ballots.to_numpy().tolist()
Convert tidy or wide ballot DataFrames back to method-ready ballots.
118def ballots_to_dataframe( 119 ballots, 120 wide=False, 121 method=None, 122 voter_column="voter", 123 candidate_column="candidate", 124 value_column="ballot", 125 candidate_prefix="candidate_", 126) -> pd.DataFrame: 127 """Return ballots as a tidy or wide DataFrame.""" 128 if wide: 129 rows = [] 130 for voter_id, ballot in enumerate(ballots): 131 row = { 132 voter_column: voter_id, 133 **{ 134 f"{candidate_prefix}{candidate}": value 135 for candidate, value in enumerate(ballot) 136 }, 137 } 138 if method is not None: 139 row["method"] = str(method) 140 rows.append(row) 141 return pd.DataFrame(rows) 142 143 rows = [] 144 for voter_id, ballot in enumerate(ballots): 145 for candidate, value in enumerate(ballot): 146 row = { 147 voter_column: voter_id, 148 candidate_column: candidate, 149 value_column: value, 150 } 151 if method is not None: 152 row["method"] = str(method) 153 rows.append(row) 154 return pd.DataFrame(rows)
Return ballots as a tidy or wide DataFrame.
163def biasedMediaFor(biaser=biaserAround(1), numerator=1): 164 """ 165 if numerator is 1: 166 0, 0, -1/2, -2/3, -3/4.... 167 if numerator is 1.5: 168 0,0,-.25, -.5, -.625, -.7 169 numerator shouldn't be over 2 unless you want strangeness. 170 171 172 """ 173 174 def biasedMedia(standings, tally=None): 175 if tally is None: 176 tally = defaultdict(int) 177 bias = biaser(standings) if callable(biaser) else biaser 178 result = standings[:2] + [ 179 (standing - bias + numerator * (bias / max(i + 2, 1))) 180 for i, standing in enumerate(standings[2:]) 181 ] 182 183 tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1 184 return result 185 186 return biasedMedia
if numerator is 1: 0, 0, -1/2, -2/3, -3/4.... if numerator is 1.5: 0,0,-.25, -.5, -.625, -.7 numerator shouldn't be over 2 unless you want strangeness.
151def fuzzyMediaFor(biaser=biaserAround(1)): 152 def fuzzyMedia(standings, tally=None): 153 if tally is None: 154 tally = defaultdict(int) 155 bias = biaser(standings) if callable(biaser) else biaser 156 result = [s + random.gauss(0, bias) for s in standings] 157 tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1 158 return result 159 160 return fuzzyMedia
223def read_results_csv(path) -> "VseResults": 224 """Load a VSE result CSV written by ``CsvBatch.saveFile``.""" 225 return VseResults.from_csv(path)
Load a VSE result CSV written by CsvBatch.saveFile.
59def rows_to_dataframe(rows: Iterable[dict] | pd.DataFrame | VseResults, copy=True) -> pd.DataFrame: 60 """Convert simulation rows, a DataFrame, or ``VseResults`` to a DataFrame.""" 61 return to_dataframe(rows, copy=copy)
Convert simulation rows, a DataFrame, or VseResults to a DataFrame.
208def run_simulation( 209 model, 210 methods, 211 nvot, 212 ncand, 213 niter, 214 baseName=None, 215 media=truth, 216 seed=None, 217 force=False, 218): 219 """Run a simulation and return a pandas-backed ``VseResults`` object.""" 220 batch = CsvBatch( 221 model, 222 methods, 223 nvot=nvot, 224 ncand=ncand, 225 niter=niter, 226 baseName=baseName, 227 media=media, 228 seed=seed, 229 force=force, 230 ) 231 return batch.results
Run a simulation and return a pandas-backed VseResults object.
234def run_simulation_dataframe(*args, **kwargs): 235 """Run a simulation and return its result rows as a pandas DataFrame.""" 236 return run_simulation(*args, **kwargs).dataframe
Run a simulation and return its result rows as a pandas DataFrame.
189def scores_to_dataframe( 190 scores, 191 method=None, 192 candidate_column="candidate", 193 value_column="score", 194) -> pd.DataFrame: 195 """Return candidate-level method scores as a DataFrame.""" 196 rows = [ 197 { 198 candidate_column: candidate, 199 value_column: score, 200 } 201 for candidate, score in enumerate(scores) 202 ] 203 frame = pd.DataFrame(rows) 204 if method is not None: 205 frame.insert(0, "method", str(method)) 206 return frame
Return candidate-level method scores as a DataFrame.
189def skewedMediaFor(biaser): 190 """ 191 192 [0, -1/3, -2/3, -1] 193 """ 194 195 def skewedMedia(standings, tally=None): 196 if tally is None: 197 tally = defaultdict(int) 198 bias = biaser(standings) if callable(biaser) else biaser 199 result = [ 200 (standing - bias * i / (len(standings) - 1)) for i, standing in enumerate(standings) 201 ] 202 203 tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1 204 return result 205 206 return skewedMedia
[0, -1/3, -2/3, -1]
209def summarize_vse( 210 rows: Iterable[dict] | pd.DataFrame | VseResults, 211 group_by=DEFAULT_GROUP_BY, 212 sort_by="mean_vse", 213 ascending=False, 214) -> pd.DataFrame: 215 """Summarize VSE scores by method, chooser, or another grouping.""" 216 return VseResults(rows_to_dataframe(rows, copy=False)).summarize( 217 group_by=group_by, 218 sort_by=sort_by, 219 ascending=ascending, 220 )
Summarize VSE scores by method, chooser, or another grouping.
47def to_dataframe(data, copy=True, **kwargs) -> pd.DataFrame: 48 """Convert VSE objects, result rows, or records to a pandas DataFrame.""" 49 if isinstance(data, VseResults): 50 return data.to_dataframe(copy=copy) 51 if isinstance(data, pd.DataFrame): 52 return data.copy() if copy else data 53 dataframe_method = getattr(data, "to_dataframe", None) 54 if callable(dataframe_method): 55 return _call_dataframe_method(dataframe_method, copy=copy, **kwargs) 56 return pd.DataFrame(data, **kwargs)
Convert VSE objects, result rows, or records to a pandas DataFrame.
64def voter_to_dataframe( 65 voter, 66 voter_id=None, 67 voter_column="voter", 68 candidate_column="candidate", 69 value_column="utility", 70) -> pd.DataFrame: 71 """Return one voter's candidate utilities as a tidy DataFrame.""" 72 metadata = _voter_metadata(voter) 73 rows = [] 74 for candidate, value in enumerate(voter): 75 row = {candidate_column: candidate, value_column: value, **metadata} 76 if voter_id is not None: 77 row[voter_column] = voter_id 78 rows.append(row) 79 return pd.DataFrame(rows)
Return one voter's candidate utilities as a tidy DataFrame.
82def voters_to_dataframe( 83 voters, 84 wide=False, 85 voter_column="voter", 86 candidate_column="candidate", 87 value_column="utility", 88 candidate_prefix="candidate_", 89) -> pd.DataFrame: 90 """Return voter utilities as a tidy or wide DataFrame.""" 91 if wide: 92 rows = [] 93 for voter_id, voter in enumerate(voters): 94 row = { 95 voter_column: voter_id, 96 **{ 97 f"{candidate_prefix}{candidate}": value for candidate, value in enumerate(voter) 98 }, 99 **_voter_metadata(voter), 100 } 101 rows.append(row) 102 return pd.DataFrame(rows) 103 104 rows = [] 105 for voter_id, voter in enumerate(voters): 106 rows.extend( 107 voter_to_dataframe( 108 voter, 109 voter_id=voter_id, 110 voter_column=voter_column, 111 candidate_column=candidate_column, 112 value_column=value_column, 113 ).to_dict("records") 114 ) 115 return pd.DataFrame(rows)
Return voter utilities as a tidy or wide DataFrame.