Edit on GitHub

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]
class CandidateWithCount:
385class CandidateWithCount:
386    def __init__(self, c=[], v=0):
387        self.candidate = c
388        self.votes = v
CandidateWithCount(c=[], v=0)
386    def __init__(self, c=[], v=0):
387        self.candidate = c
388        self.votes = v
candidate
votes
class Method:
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.

def results(self, ballots, isHonest=False, **kwargs):
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.

def honest_ballots(self, voters):
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.

def ballots_dataframe(self, voters, wide=False):
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.

def results_dataframe(self, ballots, isHonest=False, **kwargs):
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.

@staticmethod
def honBallot(cls, utils):
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

@staticmethod
def winner(results):
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
def honBallotFor(self, voters):
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.

def dummyBallotFor(self, polls):
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.

def resultsFor(self, voters, chooser, tally=None, **kwargs):
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.

def multiResults( self, voters, chooserFuns=(), media=<function Method.<lambda>>, checkStrat=True):
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).

def vseOn(self, voters, chooserFuns=(), **args):
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.

def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args):
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
@staticmethod
def ballotChooserFor(chooserFun):
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

def stratTarget2(self, places):
307    def stratTarget2(self, places):
308        ((frontId, frontResult), (targId, targResult)) = places[:2]
309        return (frontId, frontResult, targId, targResult)
def stratTarget3(self, places):
311    def stratTarget3(self, places):
312        ((frontId, frontResult), (targId, targResult)) = places[:3:2]
313        return (frontId, frontResult, targId, targResult)
def stratTargetFor(self, places):
307    def stratTarget2(self, places):
308        ((frontId, frontResult), (targId, targResult)) = places[:2]
309        return (frontId, frontResult, targId, targResult)
def stratBallotFor(self, polls):
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.

class SideTally(collections.defaultdict):
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.

def initKeys(self, chooser):
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
def serialize(self):
44    def serialize(self):
45        try:
46            return [self[key] for key in self.keyList]
47        except AttributeError:
48            return []
def fullSerialize(self):
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]
def itemList(self):
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())
class Tallies(builtins.list):
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], []]
class VseMethodRun:
14class VseMethodRun:
15    @autoassign
16    def __init__(self, method, choosers, results):
17        pass
@autoassign
VseMethodRun(method, choosers, results)
15    @autoassign
16    def __init__(self, method, choosers, results):
17        pass
class VseOneRun:
 8class VseOneRun:
 9    @autoassign
10    def __init__(self, result, tallyItems, strat):
11        pass
@autoassign
VseOneRun(result, tallyItems, strat)
 9    @autoassign
10    def __init__(self, result, tallyItems, strat):
11        pass
@decorator
def rememberBallot(fun):
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 _xxx

@decorator
def rememberBallots(fun):
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 _xxx