vse_sim.strategies
1import random 2from collections import defaultdict 3 4from .compat import std 5from .decorators import cached_property 6 7 8###################Choosers 9class Chooser: 10 tallyKeys = [] 11 12 def __init__(self, choice=None, subChoosers=None): 13 """Subclasses should just copy/paste this logic because 14 each will have its own parameters and so that's easiest.""" 15 if choice is not None: 16 self.choice = choice 17 self.subChoosers = [] if subChoosers is None else subChoosers 18 19 def getName(self): 20 if hasattr(self, "choice"): # only true for base class 21 # print("base") 22 return self.choice 23 if not hasattr(self, "name") or not self.name: 24 # print("generic") 25 self.name = self.__class__.__name__[:-7] # drop the "Chooser" 26 # print("specific") 27 return self.name 28 29 def __call__(self, cls, voter, tally): 30 return self.choice 31 32 def addTallyKeys(self, tally): 33 for key in self.allTallyKeys: 34 tally[key] = 0 35 36 @cached_property 37 def myKeys(self): 38 prefix = f"{self.getName()}_" 39 return [prefix + key for key in self.tallyKeys] 40 41 @cached_property 42 def allTallyKeys(self): 43 keys = self.myKeys 44 for subChooser in self.subChoosers: 45 keys += subChooser.allTallyKeys 46 return keys 47 48 @cached_property 49 def __name__(self): 50 return self.__class__.__name__ 51 52 53beHon = Chooser("hon") 54beStrat = Chooser("strat") 55beX = Chooser("extraStrat") 56 57 58class LazyChooser(Chooser): 59 """Honest, if honest and strategic are the same. Otherwise, extra-strategic.""" 60 61 tallyKeys = [""] 62 63 def __init__(self, subChoosers=None): 64 super().__init__(subChoosers=[beHon, beX] if subChoosers is None else subChoosers) 65 66 def __call__(self, cls, voter, tally): 67 if getattr(voter, f"{cls.__name__}_hon") == getattr(voter, f"{cls.__name__}_strat"): 68 tally[self.myKeys[0]] += 0 69 return self.subChoosers[0](cls, voter, tally) # hon 70 tally[self.myKeys[0]] += 1 71 return self.subChoosers[1](cls, voter, tally) # strat 72 73 74class OssChooser(Chooser): 75 tallyKeys = ["", "gap"] 76 """one-sided strategy: 77 returns a 'strategic' ballot for those who prefer the strategic target, 78 and an honest ballot for those who prefer the honest winner. Only works 79 if honBallot and stratBallot have already been called for the voter. 80 81 82 """ 83 84 def __init__(self, subChoosers=None): 85 super().__init__(subChoosers=[beHon, beStrat] if subChoosers is None else subChoosers) 86 87 def __call__(self, cls, voter, tally): 88 hon, strat = self.subChoosers 89 if not getattr(voter, f"{cls.__name__}_isStrat", False): 90 return hon(cls, voter, tally) if callable(hon) else hon 91 tally[self.myKeys[0]] += 1 92 tally[self.myKeys[1]] += getattr(voter, f"{cls.__name__}_stratGap", 0) 93 return strat(cls, voter, tally) if callable(strat) else strat 94 95 def getName(self): 96 baseName = super(OssChooser, self).getName() 97 return f"{baseName}." + "_".join(s.getName() for s in self.subChoosers) + "." 98 99 100class ProbChooser(Chooser): 101 def __init__(self, probs): 102 self.probs = probs 103 subChoosers = [chooser for (p, chooser) in probs] 104 super().__init__(subChoosers=subChoosers) 105 106 def __call__(self, cls, voter, tally): 107 r = random.random() 108 for i, (p, chooser) in enumerate(self.probs): 109 r -= p 110 if r < 0: 111 if i > 0: # keep tally for all but first option 112 tally[f"{self.getName()}_{chooser.getName()}"] += 1 113 return chooser(cls, voter, tally) 114 return self.subChoosers[-1](cls, voter, tally) if self.subChoosers else None 115 116 def getName(self): 117 baseName = super(ProbChooser, self).getName() 118 return ( 119 f"{baseName}." 120 + "_".join(s.getName() + str(round(p * 100)) for p, s in self.probs) 121 + "." 122 ) 123 124 125###media 126 127 128def truth(standings, tally=None): 129 return standings 130 131 132def topNMediaFor(n): 133 def topNMedia(standings, tally=None): 134 return list(standings[:n]) + [min(standings)] * (len(standings) - n) 135 136 return topNMedia 137 138 139def biaserAround(scale): 140 def biaser(standings): 141 return scale * std(standings, ddof=1) 142 143 return biaser 144 145 146def orderOf(standings): 147 return [i for i, val in sorted(list(enumerate(standings)), key=lambda x: x[1], reverse=True)] 148 149 150def fuzzyMediaFor(biaser=biaserAround(1)): 151 def fuzzyMedia(standings, tally=None): 152 if tally is None: 153 tally = defaultdict(int) 154 bias = biaser(standings) if callable(biaser) else biaser 155 result = [s + random.gauss(0, bias) for s in standings] 156 tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1 157 return result 158 159 return fuzzyMedia 160 161 162def biasedMediaFor(biaser=biaserAround(1), numerator=1): 163 """ 164 if numerator is 1: 165 0, 0, -1/2, -2/3, -3/4.... 166 if numerator is 1.5: 167 0,0,-.25, -.5, -.625, -.7 168 numerator shouldn't be over 2 unless you want strangeness. 169 170 171 """ 172 173 def biasedMedia(standings, tally=None): 174 if tally is None: 175 tally = defaultdict(int) 176 bias = biaser(standings) if callable(biaser) else biaser 177 result = standings[:2] + [ 178 (standing - bias + numerator * (bias / max(i + 2, 1))) 179 for i, standing in enumerate(standings[2:]) 180 ] 181 182 tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1 183 return result 184 185 return biasedMedia 186 187 188def skewedMediaFor(biaser): 189 """ 190 191 [0, -1/3, -2/3, -1] 192 """ 193 194 def skewedMedia(standings, tally=None): 195 if tally is None: 196 tally = defaultdict(int) 197 bias = biaser(standings) if callable(biaser) else biaser 198 result = [ 199 (standing - bias * i / (len(standings) - 1)) for i, standing in enumerate(standings) 200 ] 201 202 tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1 203 return result 204 205 return skewedMedia 206 207 208__all__ = [ 209 "Chooser", 210 "LazyChooser", 211 "OssChooser", 212 "ProbChooser", 213 "beHon", 214 "beStrat", 215 "beX", 216 "biasedMediaFor", 217 "biaserAround", 218 "fuzzyMediaFor", 219 "orderOf", 220 "skewedMediaFor", 221 "topNMediaFor", 222 "truth", 223]
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
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
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
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
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
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]