Edit on GitHub

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]
class Chooser:
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__
Chooser(choice=None, subChoosers=None)
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.

tallyKeys = []
subChoosers
def getName(self):
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
def addTallyKeys(self, tally):
33    def addTallyKeys(self, tally):
34        for key in self.allTallyKeys:
35            tally[key] = 0
@cached_property
def myKeys(self):
37    @cached_property
38    def myKeys(self):
39        prefix = f"{self.getName()}_"
40        return [prefix + key for key in self.tallyKeys]
@cached_property
def allTallyKeys(self):
42    @cached_property
43    def allTallyKeys(self):
44        keys = self.myKeys
45        for subChooser in self.subChoosers:
46            keys += subChooser.allTallyKeys
47        return keys
class LazyChooser(Chooser):
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.

LazyChooser(subChoosers=None)
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.

tallyKeys = ['']
class OssChooser(Chooser):
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) + "."
OssChooser(subChoosers=None)
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.

tallyKeys = ['', 'gap']

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.

def getName(self):
96    def getName(self):
97        baseName = super(OssChooser, self).getName()
98        return f"{baseName}." + "_".join(s.getName() for s in self.subChoosers) + "."
class ProbChooser(Chooser):
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        )
ProbChooser(probs)
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.

probs
def getName(self):
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        )
beHon = <Chooser object>
beStrat = <Chooser object>
beX = <Chooser object>
def biasedMediaFor(biaser=<function biaserAround.<locals>.biaser>, numerator=1):
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.

def biaserAround(scale):
140def biaserAround(scale):
141    def biaser(standings):
142        return scale * std(standings, ddof=1)
143
144    return biaser
def fuzzyMediaFor(biaser=<function biaserAround.<locals>.biaser>):
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
def orderOf(standings):
147def orderOf(standings):
148    return [i for i, val in sorted(list(enumerate(standings)), key=lambda x: x[1], reverse=True)]
def skewedMediaFor(biaser):
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]

def topNMediaFor(n):
133def topNMediaFor(n):
134    def topNMedia(standings, tally=None):
135        return list(standings[:n]) + [min(standings)] * (len(standings) - n)
136
137    return topNMedia
def truth(standings, tally=None):
129def truth(standings, tally=None):
130    return standings