vse_sim.data_classes
1import random 2 3from .compat import isnum, mean 4from .decorators import autoassign, decorator 5 6 7class VseOneRun: 8 @autoassign 9 def __init__(self, result, tallyItems, strat): 10 pass 11 12 13class VseMethodRun: 14 @autoassign 15 def __init__(self, method, choosers, results): 16 pass 17 18 19####data holders for output 20from collections import defaultdict 21 22 23class SideTally(defaultdict): 24 """Used for keeping track of how many voters are being strategic, etc. 25 26 DO NOT use plain +; for this class, it is equivalent to +=, but less readable. 27 28 """ 29 30 def __init__(self): 31 super().__init__(int) 32 33 def initKeys(self, chooser): 34 try: 35 self.keyList = chooser.allTallyKeys() 36 except AttributeError: 37 try: 38 self.keyList = list(chooser) 39 except TypeError: 40 self.keyList = [] 41 self.initKeys = staticmethod(lambda x: x) # don't do it again 42 43 def serialize(self): 44 try: 45 return [self[key] for key in self.keyList] 46 except AttributeError: 47 return [] 48 49 def fullSerialize(self): 50 if not hasattr(self, "keyList"): 51 return [self[key] for key in self.keys()] 52 return [self[key] for key in self.keyList] 53 54 def itemList(self): 55 try: 56 kl = self.keyList 57 return [(k, self[k]) for k in kl] + [(k, self[k]) for k in self.keys() if k not in kl] 58 except AttributeError: 59 return list(self.items()) 60 61 62class Tallies(list): 63 """Used (ONCE) as an enumerator, gives an inexhaustible flow of SideTally objects. 64 After that, use as list to see those objects. 65 66 >>> ts = Tallies() 67 >>> for i, j in zip(ts, [5,4,3]): 68 ... i[j] += j 69 ... 70 >>> [t.serialize() for t in ts] 71 [[], [], [], []] 72 >>> [t.fullSerialize() for t in ts] 73 [[5], [4], [3], []] 74 >>> [t.initKeys([k]) for (t,k) in zip(ts,[6,4,3])] 75 [None, None, None] 76 >>> [t.serialize() for t in ts] 77 [[0], [4], [3], []] 78 """ 79 80 def __iter__(self): 81 if getattr(self, "used", False): 82 return super().__iter__() 83 self.used = True 84 return self._generated_tallies() 85 86 def __eq__(self, other): 87 if not isinstance(other, Tallies): 88 return super().__eq__(other) 89 return super().__eq__(other) and getattr(self, "used", False) == getattr( 90 other, "used", False 91 ) 92 93 def _generated_tallies(self): 94 while True: 95 tally = SideTally() 96 self.append(tally) 97 yield tally 98 99 100##Election Methods 101class Method: 102 """Base class for election methods. Holds some of the duct tape.""" 103 104 def __str__(self): 105 return self.__class__.__name__ 106 107 def results(self, ballots, isHonest=False, **kwargs): 108 """Combines ballots into results. Override for comparative 109 methods. 110 111 Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index. 112 113 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. 114 115 Test for subclasses, makes no sense to test this method in the abstract base class. 116 """ 117 from .dataframe import ballots_from_dataframe 118 119 ballots = ballots_from_dataframe(ballots) 120 return list(map(self.candScore, zip(*ballots))) 121 122 def honest_ballots(self, voters): 123 """Return honest ballots for voters as method-ready lists.""" 124 sentinel = object() 125 previous_cuts = getattr(self.__class__, "specificCuts", sentinel) 126 try: 127 ballot_factory = self.honBallotFor(voters) 128 return [ballot_factory(self.__class__, voter) for voter in voters] 129 finally: 130 if previous_cuts is sentinel: 131 try: 132 delattr(self.__class__, "specificCuts") 133 except AttributeError: 134 pass 135 else: 136 self.__class__.specificCuts = previous_cuts 137 138 def ballots_dataframe(self, voters, wide=False): 139 """Return honest ballots for voters as a tidy or wide DataFrame.""" 140 from .dataframe import ballots_to_dataframe 141 142 return ballots_to_dataframe(self.honest_ballots(voters), wide=wide, method=self) 143 144 def results_dataframe(self, ballots, isHonest=False, **kwargs): 145 """Return candidate scores for ballots as a DataFrame.""" 146 from .dataframe import scores_to_dataframe 147 148 self.__class__.extraEvents = {} 149 return scores_to_dataframe( 150 self.results(ballots, isHonest=isHonest, **kwargs), 151 method=self, 152 ) 153 154 @staticmethod # cls is provided explicitly, not through binding 155 def honBallot(cls, utils): 156 """Takes utilities and returns an honest ballot""" 157 raise NotImplementedError(f"{cls} needs honBallot") 158 159 @staticmethod 160 def winner(results): 161 """Simply find the winner once scores are already calculated. Override for 162 ranked methods. 163 164 165 >>> Method().winner([1,2,3,2,-100]) 166 2 167 >>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6 168 True 169 """ 170 winScore = max(result for result in results if isnum(result)) 171 winners = [cand for (cand, score) in enumerate(results) if score == winScore] 172 return random.choice(winners) 173 174 def honBallotFor(self, voters): 175 """This is where you would do any setup necessary and create an honBallot 176 function. But the base version just returns the honBallot function.""" 177 return self.honBallot 178 179 def dummyBallotFor(self, polls): 180 """Returns a (function which takes utilities and returns a dummy ballot) 181 for the given "polling" info.""" 182 return lambda cls, utilities, stratTally: utilities 183 184 def resultsFor(self, voters, chooser, tally=None, **kwargs): 185 """create ballots and get results. 186 187 Again, test on subclasses. 188 """ 189 if tally is None: 190 tally = SideTally() 191 tally.initKeys(chooser) 192 return dict( 193 results=self.results( 194 [chooser(self.__class__, voter, tally) for voter in voters], **kwargs 195 ), 196 chooser=chooser.__name__, 197 tally=tally, 198 ) 199 200 def multiResults(self, voters, chooserFuns=(), media=(lambda x, t: x), checkStrat=True): 201 """Runs two base elections: first with honest votes, then 202 with strategic results based on the first results (filtered by 203 the media). Then, runs a series of elections using each chooserFun 204 in chooserFuns to select the votes for each voter. 205 206 Returns a tuple of (honResults, stratResults, ...). The stratresults 207 are based on common polling information, which is given by media(honresults). 208 """ 209 from .strategies import OssChooser 210 211 honTally = SideTally() 212 self.__class__.extraEvents = {} 213 hon = self.resultsFor(voters, self.honBallotFor(voters), honTally, isHonest=True) 214 215 stratTally = SideTally() 216 217 polls = media(hon["results"], stratTally) 218 winner, _w, target, _t = self.stratTargetFor(sorted(enumerate(polls), key=lambda x: -x[1])) 219 220 strat = self.resultsFor(voters, self.stratBallotFor(polls), stratTally) 221 222 ossTally = SideTally() 223 oss = self.resultsFor(voters, self.ballotChooserFor(OssChooser()), ossTally) 224 ossWinner = oss["results"].index(max(oss["results"])) 225 ossTally["worked"] += 1 if ossWinner == target else (0 if ossWinner == winner else -1) 226 227 smart = dict( 228 results=(hon["results"] if ossTally["worked"] == 1 else oss["results"]), 229 chooser="smartOss", 230 tally=SideTally(), 231 ) 232 233 extraTallies = Tallies() 234 results = [strat, oss, smart] + [ 235 self.resultsFor(voters, self.ballotChooserFor(chooserFun), aTally) 236 for (chooserFun, aTally) in zip(chooserFuns, extraTallies) 237 ] 238 return [(hon["results"], hon["chooser"], list(self.__class__.extraEvents.items()))] + [ 239 (r["results"], r["chooser"], r["tally"].itemList()) for r in results 240 ] 241 242 def vseOn(self, voters, chooserFuns=(), **args): 243 """Finds honest and strategic voter satisfaction efficiency (VSE) 244 for this method on the given electorate. 245 """ 246 multiResults = self.multiResults(voters, chooserFuns, **args) 247 utils = voters.socUtils 248 best = max(utils) 249 rand = mean(utils) 250 251 # import pprint 252 # pprint.pprint(multiResults) 253 vses = VseMethodRun( 254 self.__class__, 255 chooserFuns, 256 [ 257 VseOneRun( 258 [(utils[self.winner(result)] - rand) / (best - rand)], 259 tally, 260 chooser, 261 ) 262 for (result, chooser, tally) in multiResults[0] 263 ], 264 ) 265 vses.extraEvents = multiResults[1] 266 return vses 267 268 def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args): 269 multiResults = self.multiResults(voters, chooserFuns, **args) 270 utils = voters.socUtils 271 best = max(utils) 272 rand = mean(utils) 273 rows = [] 274 nvot = len(voters) 275 for result, chooser, tallyItems in multiResults: 276 row = { 277 "eid": eid, 278 "emodel": emodel, 279 "ncand": cands, 280 "nvot": nvot, 281 "best": best, 282 "rand": rand, 283 "method": str(self), 284 "chooser": chooser, # .getName(), 285 "util": utils[self.winner(result)], 286 "vse": (utils[self.winner(result)] - rand) / (best - rand), 287 } 288 # print(tallyItems) 289 for i, (k, v) in enumerate(tallyItems): 290 # print("Result: tally ",i,k,v) 291 row[f"tallyName{str(i)}"] = str(k) 292 row[f"tallyVal{str(i)}"] = str(v) 293 rows.append(row) 294 return rows 295 296 @staticmethod 297 def ballotChooserFor(chooserFun): 298 """Takes a chooserFun; returns a ballot chooser using that chooserFun""" 299 300 def ballotChooser(cls, voter, tally): 301 return getattr(voter, f"{cls.__name__}_{chooserFun(cls, voter, tally)}") 302 303 ballotChooser.__name__ = chooserFun.getName() 304 return ballotChooser 305 306 def stratTarget2(self, places): 307 ((frontId, frontResult), (targId, targResult)) = places[:2] 308 return (frontId, frontResult, targId, targResult) 309 310 def stratTarget3(self, places): 311 ((frontId, frontResult), (targId, targResult)) = places[:3:2] 312 return (frontId, frontResult, targId, targResult) 313 314 stratTargetFor = stratTarget2 315 316 def stratBallotFor(self, polls): 317 """Returns a (function which takes utilities and returns a strategic ballot) 318 for the given "polling" info.""" 319 320 places = sorted(enumerate(polls), key=lambda x: -x[1]) # from high to low 321 # print("places",places) 322 (frontId, frontResult, targId, targResult) = self.stratTargetFor(places) 323 n = len(polls) 324 325 @rememberBallots 326 def stratBallot(cls, voter): 327 stratGap = voter[targId] - voter[frontId] 328 ballot = [0] * len(voter) 329 isStrat = stratGap > 0 330 extras = cls.fillStratBallot( 331 voter, 332 polls, 333 places, 334 n, 335 stratGap, 336 ballot, 337 frontId, 338 frontResult, 339 targId, 340 targResult, 341 ) 342 result = dict(strat=ballot, isStrat=isStrat, stratGap=stratGap) 343 if extras: 344 result.update(extras) 345 return result 346 347 return stratBallot 348 349 350@decorator 351def rememberBallot(fun): 352 """A decorator for a function of the form xxxBallot(cls, voter) 353 which memoizes the vote onto the voter in an attribute named <methName>_xxx 354 """ 355 356 def getAndRemember(cls, voter, tally=None): 357 ballot = fun(cls, voter) 358 setattr(voter, f"{cls.__name__}_{fun.__name__[:-6]}", ballot) 359 return ballot 360 361 getAndRemember.__name__ = fun.__name__ 362 getAndRemember.allTallyKeys = lambda: [] 363 return getAndRemember 364 365 366@decorator 367def rememberBallots(fun): 368 """A decorator for a function of the form xxxBallot(cls, voter) 369 which memoizes the vote onto the voter in an attribute named <methName>_xxx 370 """ 371 372 def getAndRemember(cls, voter, tally=None): 373 ballots = fun(cls, voter) 374 for bType, ballot in ballots.items(): 375 setattr(voter, f"{cls.__name__}_{bType}", ballot) 376 377 return ballots[fun.__name__[:-6]] # leave off the "...Ballot" 378 379 getAndRemember.__name__ = fun.__name__ 380 getAndRemember.allTallyKeys = lambda: [] 381 return getAndRemember 382 383 384class CandidateWithCount: 385 def __init__(self, c=[], v=0): 386 self.candidate = c 387 self.votes = v 388 389 390__all__ = [ 391 "CandidateWithCount", 392 "Method", 393 "SideTally", 394 "Tallies", 395 "VseMethodRun", 396 "VseOneRun", 397 "rememberBallot", 398 "rememberBallots", 399]
385class CandidateWithCount: 386 def __init__(self, c=[], v=0): 387 self.candidate = c 388 self.votes = v
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.
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.
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], []]
351@decorator 352def rememberBallot(fun): 353 """A decorator for a function of the form xxxBallot(cls, voter) 354 which memoizes the vote onto the voter in an attribute named <methName>_xxx 355 """ 356 357 def getAndRemember(cls, voter, tally=None): 358 ballot = fun(cls, voter) 359 setattr(voter, f"{cls.__name__}_{fun.__name__[:-6]}", ballot) 360 return ballot 361 362 getAndRemember.__name__ = fun.__name__ 363 getAndRemember.allTallyKeys = lambda: [] 364 return getAndRemember
A decorator for a function of the form xxxBallot(cls, voter)
which memoizes the vote onto the voter in an attribute named
367@decorator 368def rememberBallots(fun): 369 """A decorator for a function of the form xxxBallot(cls, voter) 370 which memoizes the vote onto the voter in an attribute named <methName>_xxx 371 """ 372 373 def getAndRemember(cls, voter, tally=None): 374 ballots = fun(cls, voter) 375 for bType, ballot in ballots.items(): 376 setattr(voter, f"{cls.__name__}_{bType}", ballot) 377 378 return ballots[fun.__name__[:-6]] # leave off the "...Ballot" 379 380 getAndRemember.__name__ = fun.__name__ 381 getAndRemember.allTallyKeys = lambda: [] 382 return getAndRemember
A decorator for a function of the form xxxBallot(cls, voter)
which memoizes the vote onto the voter in an attribute named