Edit on GitHub

vse_sim.methods

   1import random
   2
   3from numpy import argsort, percentile, sign
   4
   5from .compat import as_builtin_scalar, floor, mean
   6from .data_classes import CandidateWithCount, Method, rememberBallot, rememberBallots
   7from .dataframe import ballots_from_dataframe
   8from .voter_models import DeterministicModel, Voter  # noqa: F401 - used by doctests
   9
  10
  11####EMs themselves
  12class Borda(Method):
  13    candScore = staticmethod(mean)
  14
  15    nRanks = 999  # infinity
  16
  17    @staticmethod
  18    def fillPrefOrder(
  19        voter,
  20        ballot,
  21        whichCands=None,  # None means "all"; otherwise, an iterable of cand indexes
  22        lowSlot=0,
  23        nSlots=None,  # again, None means "all"
  24        remainderScore=None,  # what to give candidates that don't fit in nSlots
  25    ):
  26
  27        venum = list(enumerate(voter))
  28        if whichCands:
  29            venum = [venum[c] for c in whichCands]
  30        prefOrder = sorted(venum, key=lambda x: -x[1])  # high to low
  31        Borda.fillCands(ballot, prefOrder, lowSlot, nSlots, remainderScore)
  32        # modifies ballot argument, returns nothing.
  33
  34    @staticmethod
  35    def fillCands(
  36        ballot,
  37        whichCands,  # list of tuples starting with cand id, in descending order
  38        lowSlot=0,
  39        nSlots=None,  # again, None means "all"
  40        remainderScore=None,  # what to give candidates that don't fit in nSlots
  41    ):
  42        if nSlots is None:
  43            nSlots = len(whichCands)
  44        cur = lowSlot + nSlots - 1
  45        for i in range(nSlots):
  46            ballot[whichCands[i][0]] = cur
  47            cur -= 1
  48        if remainderScore is not None:
  49            i += 1
  50            while i < len(whichCands):
  51                ballot[whichCands[i][0]] = remainderScore
  52                i += 1
  53        # modifies ballot argument, returns nothing.
  54
  55    @staticmethod  # cls is provided explicitly, not through binding
  56    @rememberBallot
  57    def honBallot(cls, utils):
  58        ballot = [0] * len(utils)
  59        cls.fillPrefOrder(utils, ballot)
  60        return ballot
  61
  62    @classmethod
  63    def fillStratBallot(
  64        cls,
  65        voter,
  66        polls,
  67        places,
  68        n,
  69        stratGap,
  70        ballot,
  71        frontId,
  72        frontResult,
  73        targId,
  74        targResult,
  75    ):
  76        """Mutates the `ballot` argument to be a strategic ballot.
  77
  78        >>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
  79        [3, 0, 1, 2]
  80        """
  81        nRanks = min(cls.nRanks, n)
  82        if stratGap <= 0:
  83            ballot[frontId], ballot[targId] = (nRanks - 1), 0
  84        else:
  85            ballot[frontId], ballot[targId] = 0, (nRanks - 1)
  86        nRanks -= 2
  87        if nRanks > 0:
  88            cls.fillCands(ballot, places[2:][::-1], lowSlot=1, nSlots=nRanks, remainderScore=0)
  89        # (don't) return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
  90
  91
  92RankedMethod = Borda  # alias
  93RatedMethod = RankedMethod  # Should have same strategies available, plus more
  94
  95
  96class Plurality(RankedMethod):
  97    nRanks = 2
  98
  99    @staticmethod
 100    def oneVote(utils, forWhom):
 101        ballot = [0] * len(utils)
 102        ballot[forWhom] = 1
 103        return ballot
 104
 105    @staticmethod  # cls is provided explicitly, not through binding
 106    @rememberBallot
 107    def honBallot(cls, utils):
 108        """Takes utilities and returns an honest ballot
 109
 110        >>> Plurality.honBallot(Plurality, Voter([-3,-2,-1]))
 111        [0, 0, 1]
 112        >>> Plurality().stratBallotFor([3,2,1])(Plurality, Voter([-3,-2,-1]))
 113        [0, 1, 0]
 114        """
 115        # return cls.oneVote(utils, cls.winner(utils))
 116        ballot = [0] * len(utils)
 117        cls.fillPrefOrder(utils, ballot, nSlots=1, lowSlot=1, remainderScore=0)
 118        return ballot
 119
 120    #
 121    # @classmethod
 122    # def xxstratBallot(cls, voter, polls, places, n,
 123    #                     frontId, frontResult, targId, targResult):
 124    #     """Takes utilities and returns a strategic ballot
 125    #     for the given "polling" info.
 126    #
 127    #     >>> Plurality().stratBallotFor([4,2,1])(Plurality, Voter([-4,-2,-1]))
 128    #     [0, 1, 0]
 129    #     """
 130    #     stratGap = voter[targId] - voter[frontId]
 131    #     if stratGap <= 0:
 132    #         #winner is preferred; be complacent.
 133    #         isStrat = False
 134    #         strat = cls.oneVote(voter, frontId)
 135    #     else:
 136    #         #runner-up is preferred; be strategic in iss run
 137    #         isStrat = True
 138    #         #sort cuts high to low
 139    #         #cuts = (cuts[1], cuts[0])
 140    #         strat = cls.oneVote(voter, targId)
 141    #     return dict(strat=strat, isStrat=isStrat, stratGap=stratGap)
 142
 143
 144def Score(topRank=10, asClass=False):
 145
 146    class Score0to(Method):
 147        """Score voting, 0-10.
 148
 149
 150        Strategy establishes pivots
 151            >>> Score().stratBallotFor([0,1,2])(Score, Voter([5,6,7]))
 152            [0, 0, 10]
 153            >>> Score().stratBallotFor([2,1,0])(Score, Voter([5,6,7]))
 154            [0, 10, 10]
 155            >>> Score().stratBallotFor([1,0,2])(Score, Voter([5,6,7]))
 156            [0, 5.0, 10]
 157
 158        Strategy (kinda) works for ties
 159            >>> Score().stratBallotFor([1,0,2])(Score, Voter([5,6,6]))
 160            [0, 10, 10]
 161            >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,6,7]))
 162            [0, 0, 10]
 163            >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,7,6]))
 164            [10, 10, 10]
 165            >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,5,6]))
 166            [10, 0, 10]
 167
 168        """
 169
 170        # >>> qs += [Score().resultsFor(PolyaModel()(101,2),Score.honBallot)[0] for i in range(800)]
 171        # >>> std(qs)
 172        # 2.770135393419682
 173        # >>> mean(qs)
 174        # 5.1467202970297032
 175        bias2 = 2.770135393419682
 176        # >>> qs5 = [Score().resultsFor(PolyaModel()(101,5),Score.honBallot)[0] for i in range(400)]
 177        # >>> mean(qs5)
 178        # 4.920247524752476
 179        # >>> std(qs5)
 180        # 2.3536762480634343
 181        bias5 = 2.3536762480634343
 182        candScore = staticmethod(mean)
 183        # """Takes the list of votes for a candidate; returns the candidate's score."""
 184
 185        def __str__(self):
 186            if self.topRank == 1:
 187                return "IdealApproval"
 188            return self.__class__.__name__ + str(self.topRank)
 189
 190        @staticmethod  # cls is provided explicitly, not through binding
 191        @rememberBallot
 192        def honBallot(cls, utils):
 193            """Takes utilities and returns an honest ballot (on 0..10)
 194
 195
 196            honest ballots work as expected
 197                >>> Score().honBallot(Score, Voter([5,6,7]))
 198                [0.0, 5.0, 10.0]
 199                >>> Score().resultsFor(DeterministicModel(3)(5,3),Score().honBallot)["results"]
 200                [4.0, 6.0, 5.0]
 201            """
 202            bot = min(utils)
 203            scale = max(utils) - bot
 204            return [floor((cls.topRank + 0.99) * (util - bot) / scale) for util in utils]
 205
 206        @classmethod
 207        def fillStratBallot(
 208            cls,
 209            voter,
 210            polls,
 211            places,
 212            n,
 213            stratGap,
 214            ballot,
 215            frontId,
 216            frontResult,
 217            targId,
 218            targResult,
 219        ):
 220            """Returns a (function which takes utilities and returns a strategic ballot)
 221            for the given "polling" info."""
 222
 223            cuts = [voter[frontId], voter[targId]]
 224            if stratGap > 0:
 225                # sort cuts high to low
 226                cuts = (cuts[1], cuts[0])
 227            if cuts[0] == cuts[1]:
 228                strat = [(cls.topRank if (util >= cuts[0]) else 0) for util in voter]
 229            else:
 230                strat = [
 231                    max(
 232                        0,
 233                        min(
 234                            cls.topRank,
 235                            floor((cls.topRank + 0.99) * (util - cuts[1]) / (cuts[0] - cuts[1])),
 236                        ),
 237                    )
 238                    for util in voter
 239                ]
 240            for i in range(n):
 241                ballot[i] = strat[i]
 242
 243    Score0to.topRank = topRank
 244    return Score0to if asClass else Score0to()
 245
 246
 247def BulletyApprovalWith(bullets=0.5, asClass=False):
 248
 249    class BulletyApproval((Score(1, True))):
 250        bulletiness = bullets
 251
 252        def __str__(self):
 253            return f"BulletyApproval{str(round(self.bulletiness * 100))}"
 254
 255        @staticmethod  # cls is provided explicitly, not through binding
 256        @rememberBallot
 257        def honBallot(cls, utils):
 258            """Takes utilities and returns an honest ballot (on 0..10)
 259
 260
 261            honest ballots work as expected
 262                >>> Score().honBallot(Score, Voter([5,6,7]))
 263                [0.0, 5.0, 10.0]
 264                >>> Score().resultsFor(DeterministicModel(3)(5,3),Score().honBallot)["results"]
 265                [4.0, 6.0, 5.0]
 266            """
 267            if random.random() > cls.bulletiness:
 268                return cls.__bases__[0].honBallot(cls, utils)
 269            best = max(utils)
 270            return [1 if util == best else 0 for util in utils]
 271
 272    return BulletyApproval if asClass else BulletyApproval()
 273
 274
 275def Srv(topRank=10):
 276    """Score Runoff Voting
 277    >>> Srv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
 278    [1.2, 0.8, 1.21]
 279    >>> Srv().results([[0,1,2]])[2]
 280    2.0
 281    >>> Srv().results([[0,1,2],[2,1,0]])[1]
 282    1.0
 283    >>> Srv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
 284    [0.8888888888888888, 1.2222222222222223, 0.8888888888888888]
 285    >>> Srv().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
 286    [1.502537313432836, 1.492537313432836, 0.5074626865671642]
 287    >>> Srv().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
 288    [1.0526315789473684, 1.105263157894737, 0.8421052631578947]
 289    >>> Srv().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
 290    [1.0666666666666667, 3.6, 3.4, 0.8666666666666667, 1.3333333333333333]
 291    """
 292
 293    score0to = Score(topRank, True)
 294
 295    class Srv0to(score0to):
 296        stratTargetFor = Method.stratTarget3
 297
 298        def results(self, ballots, **kwargs):
 299            """Srv results."""
 300            ballots = ballots_from_dataframe(ballots)
 301            baseResults = super(Srv0to, self).results(ballots, **kwargs)
 302            (runnerUp, top) = sorted(range(len(baseResults)), key=lambda i: baseResults[i])[-2:]
 303            upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots)
 304            if upset > 0:
 305                baseResults[runnerUp] = baseResults[top] + 0.01
 306            return baseResults
 307
 308    return Srv0to()
 309
 310
 311def toVote(cutoffs, util):
 312    """maps one util to a vote, using cutoffs.
 313
 314    Used by Mav, but declared outside to avoid method binding overhead."""
 315    for vote in range(len(cutoffs)):
 316        if util <= cutoffs[vote]:
 317            return vote
 318    return vote + 1
 319
 320
 321class Mav(Method):
 322    """Majority Approval Voting"""
 323
 324    # >>> mqs = [Mav().resultsFor(PolyaModel()(101,5),Mav.honBallot)[0] for i in range(400)]
 325    # >>> mean(mqs)
 326    # 1.5360519801980208
 327    # >>> mqs += [Mav().resultsFor(PolyaModel()(101,5),Mav.honBallot)[0] for i in range(1200)]
 328    # >>> mean(mqs)
 329    # 1.5343069306930679
 330    # >>> std(mqs)
 331    # 1.0970202515275356
 332    bias5 = 1.0970202515275356
 333
 334    baseCuts = [-0.8, 0, 0.8, 1.6]
 335    specificCuts = None
 336    specificPercentiles = [25, 50, 75, 90]
 337
 338    def candScore(self, scores):
 339        """For now, only works correctly for odd nvot
 340
 341        Basic tests
 342            >>> Mav().candScore([1,2,3,4,5])
 343            3.0
 344            >>> Mav().candScore([1,2,3,3,3])
 345            2.5
 346            >>> Mav().candScore([1,2,3,4])
 347            2.5
 348            >>> Mav().candScore([1,2,3,3])
 349            2.5
 350            >>> Mav().candScore([1,2,2,2])
 351            1.5
 352            >>> Mav().candScore([1,2,3,3,5])
 353            2.7
 354        """
 355        scores = sorted(scores)
 356        nvot = len(scores)
 357        nGrades = len(self.baseCuts) + 1
 358        i = int((nvot - 1) / 2)
 359        base = scores[i]
 360        while i < nvot and scores[i] == base:
 361            i += 1
 362        upper = (base + 0.5) - (i - nvot / 2) * nGrades / nvot
 363        lower = (base) - (i - nvot / 2) / nvot
 364        return max(upper, lower)
 365
 366    @classmethod
 367    def honBallotFor(cls, voters):
 368        cls.specificCuts = percentile(voters, cls.specificPercentiles)
 369        return cls.honBallot
 370
 371    @staticmethod  # cls is provided explicitly, not through binding
 372    @rememberBallot
 373    def honBallot(cls, voter):
 374        """Takes utilities and returns an honest ballot (on 0..4)
 375
 376        honest ballot works as intended, gives highest grade to highest utility:
 377            >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5,1,1.1]))
 378            [0, 1, 2, 3, 4]
 379
 380        Even if they don't rate at least an honest "B":
 381            >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5]))
 382            [0, 1, 4]
 383        """
 384        cuts = cls.specificCuts if (cls.specificCuts is not None) else cls.baseCuts
 385        cuts = [min(cut, max(voter) - 0.001) for cut in cuts]
 386        return [toVote(cuts, util) for util in voter]
 387
 388    def stratBallotFor(self, polls):
 389        """Returns a function which takes utilities and returns a dict(
 390            strat=<ballot in which all grades are exaggerated
 391                             to outside the range of the two honest frontrunners>,
 392            extraStrat=<ballot in which all grades are exaggerated to extremes>,
 393            isStrat=<whether the runner-up is preferred to the frontrunner (for reluctantStrat)>,
 394            stratGap=<utility of runner-up minus that of frontrunner>
 395            )
 396        for the given "polling" info.
 397
 398
 399
 400        Strategic tests:
 401            >>> Mav().stratBallotFor([0,1.1,1.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2]))
 402            [0, 1, 2, 3, 4]
 403            >>> Mav().stratBallotFor([0,2.1,2.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2]))
 404            [0, 1, 3, 3, 4]
 405            >>> Mav().stratBallotFor([0,2.1,1.9,0,0])(Mav, Voter([-1,0.4,0.5,1,2]))
 406            [0, 1, 3, 3, 4]
 407            >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,7,6]))
 408            [4, 4, 4]
 409            >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,5,6]))
 410            [4, 0, 4]
 411            >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6]))
 412            [4, 0, 4]
 413            >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6.1]))
 414            [2, 2, 4]
 415        """
 416        places = sorted(enumerate(polls), key=lambda x: -x[1])  # from high to low
 417        # print("places",places)
 418        ((frontId, frontResult), (targId, targResult)) = places[:2]
 419
 420        @rememberBallots
 421        def stratBallot(cls, voter):
 422            frontUtils = [voter[frontId], voter[targId]]  # utils of frontrunners
 423            stratGap = frontUtils[1] - frontUtils[0]
 424            if stratGap == 0:
 425                strat = extraStrat = [(4 if (util >= frontUtils[0]) else 0) for util in voter]
 426                isStrat = True
 427
 428            else:
 429                if stratGap < 0:
 430                    # winner is preferred; be complacent.
 431                    isStrat = False
 432                else:
 433                    # runner-up is preferred; be strategic in iss run
 434                    isStrat = True
 435                    # sort cuts high to low
 436                    frontUtils = (frontUtils[1], frontUtils[0])
 437                top = max(voter)
 438                # print("lll312")
 439                # print(self.baseCuts, front)
 440                cutoffs = [
 441                    (
 442                        (min(frontUtils[0], self.baseCuts[i]))
 443                        if (i < floor(targResult))
 444                        else (
 445                            (frontUtils[1])
 446                            if (i < floor(frontResult) + 1)
 447                            else min(top, self.baseCuts[i])
 448                        )
 449                    )
 450                    for i in range(len(self.baseCuts))
 451                ]
 452                strat = [toVote(cutoffs, util) for util in voter]
 453                extraStrat = [
 454                    max(
 455                        0,
 456                        min(
 457                            10,
 458                            floor(4.99 * (util - frontUtils[1]) / (frontUtils[0] - frontUtils[1])),
 459                        ),
 460                    )
 461                    for util in voter
 462                ]
 463            return dict(strat=strat, extraStrat=extraStrat, isStrat=isStrat, stratGap=stratGap)
 464
 465        return stratBallot
 466
 467
 468class Mj(Mav):
 469    def candScore(self, scores):
 470        """This formula will always give numbers within 0.5 of the raw median.
 471        Unfortunately, with 5 grade levels, these will tend to be within 0.1 of
 472        the raw median, leaving scores further from the integers mostly unused.
 473        This is only a problem aesthetically.
 474
 475        For now, only works correctly for odd nvot
 476
 477        tests:
 478            >>> Mj().candScore([1,2,3,4,5])
 479            3
 480            >>> Mj().candScore([1,2,3,3,5])
 481            2.7
 482            >>> Mj().candScore([1,3,3,3,5])
 483            3
 484            >>> Mj().candScore([1,3,3,4,5])
 485            3.3
 486            >>> Mj().candScore([1,3,3,3,3])
 487            2.9
 488            >>> Mj().candScore([3] * 24 + [1])
 489            2.98
 490            >>> Mj().candScore([3] * 24 + [4])
 491            3.02
 492            >>> Mj().candScore([3] * 13 + [4] * 12)
 493            3.46
 494        """
 495        scores = sorted(scores)
 496        nvot = len(scores)
 497        lo = hi = mid = nvot // 2
 498        base = scores[mid]
 499        while hi < nvot and scores[hi] == base:
 500            hi += 1
 501        while lo >= 0 and scores[lo] == base:
 502            lo -= 1
 503
 504        if (hi - mid) == (mid - lo):
 505            return base
 506        elif (hi - mid) < (mid - lo):
 507            return base + 0.5 - (hi - mid) / nvot
 508        else:
 509            return base - 0.5 + (mid - lo) / nvot
 510
 511
 512class Irv(Method):
 513    """
 514    IRV.
 515
 516    Ballots are ordered candidate rankings, from most to least preferred.
 517    Results are candidate-indexed elimination ranks, with higher numbers better.
 518    """
 519
 520    stratTargetFor = Method.stratTarget3
 521
 522    def buildPreferenceSchedule(self, ballots):
 523        """Gets a dictionary of the form {ranking as tuple, vote count}"""
 524
 525        prefs = {}
 526        for b in ballots:
 527            key = tuple(b)
 528            if key in prefs:
 529                prefs[key] += 1
 530            else:
 531                prefs[key] = 1
 532        return prefs
 533
 534    def eliminateCandidate(self, inputPrefs, toEliminate):
 535        """Gets a dictionary of the form {ranking as tuple, vote count} with toEliminate removed"""
 536
 537        if not isinstance(toEliminate, CandidateWithCount):
 538            return inputPrefs
 539
 540        prefs = {}
 541        for ranking, votes in inputPrefs.items():
 542            newranking = [candidate for candidate in ranking if candidate != toEliminate.candidate]
 543
 544            if not newranking:
 545                continue
 546            newkey = tuple(newranking)
 547            if newkey in prefs:
 548                prefs[newkey] += votes
 549            else:
 550                prefs[newkey] = votes
 551        return prefs
 552
 553    def candidateVotes(self, prefSchedule):
 554        """Gets a list of CandidateWithCount, from highest to lowest"""
 555        candidates = {}
 556        for ranking, votes in prefSchedule.items():
 557            candidate = ranking[0]
 558            if candidate in candidates:
 559                candidates[candidate].votes += votes
 560            else:
 561                candidates[candidate] = CandidateWithCount(candidate, votes)
 562
 563        # Simply for VSE which requires ranking of non-winners; in real election we don't really
 564        # care
 565        alternates = []
 566        trackedalt = set()
 567        for ranking, votes in prefSchedule.items():
 568            for alternate in ranking[1:]:
 569                if (alternate not in candidates) and alternate not in trackedalt:
 570                    alternates.append(CandidateWithCount(alternate, 0))
 571                    trackedalt.add(alternate)
 572
 573        return (
 574            sorted(candidates.values(), key=lambda c: (c.votes, c.candidate), reverse=True)
 575            + alternates
 576        )
 577
 578    def getLeast(self, voteRanking, keep=None):
 579        if keep is None:
 580            keep = {}
 581        for candidate in reversed(voteRanking):
 582            if candidate.candidate not in keep:
 583                return candidate
 584        return None
 585
 586    @staticmethod
 587    def scoresFromEliminations(eliminated, ncand):
 588        """Convert an IRV elimination order to candidate-indexed scores."""
 589        results = [-1] * ncand
 590        for score, candidate in enumerate(eliminated):
 591            results[candidate] = score
 592        return results
 593
 594    def runIrv(self, remaining, ncand):
 595        """IRV results."""
 596        eliminated = []
 597        for _ in range(ncand):
 598            votes = self.candidateVotes(remaining)
 599            toEliminate = self.getLeast(votes)
 600            if not isinstance(toEliminate, CandidateWithCount):
 601                break
 602            eliminated.append(toEliminate.candidate)
 603            remaining = self.eliminateCandidate(remaining, toEliminate)
 604        return self.scoresFromEliminations(eliminated, ncand)
 605
 606    def results(self, ballots, **kwargs):
 607        """IRV results.
 608
 609        >>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
 610        [0, 1, 2]
 611        >>> Irv().results([[0,1,2]])[2]
 612        0
 613        >>> Irv().results([[0,1,2],[2,1,0]])[1]
 614        0
 615        >>> Irv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
 616        [1, 0, 2]
 617        """
 618        ballots = ballots_from_dataframe(ballots)
 619        return self.runIrv(self.buildPreferenceSchedule(ballots), len(ballots[0]))
 620
 621    @staticmethod  # cls is provided explicitly, not through binding
 622    @rememberBallot
 623    def honBallot(cls, voter):
 624        """Takes utilities and returns an honest ballot
 625
 626        >>> Irv.honBallot(Irv,Voter([4,1,6,3]))
 627        [2, 0, 3, 1]
 628        >>> Irv.honBallot(Irv,Voter([0,1,2]))
 629        [2, 1, 0]
 630        """
 631        order = sorted(enumerate(voter), key=lambda x: (-x[1], x[0]))
 632        return [candidate for candidate, _utility in order]
 633
 634    @classmethod
 635    def fillStratBallot(
 636        cls,
 637        voter,
 638        polls,
 639        places,
 640        n,
 641        stratGap,
 642        ballot,
 643        frontId,
 644        frontResult,
 645        targId,
 646        targResult,
 647    ):
 648        """
 649        >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
 650        [2, 1, 0, 3]
 651        """
 652        i = n - 1
 653        winnerQ = voter[frontId]
 654        targQ = voter[targId]
 655        placesToFill = list(range(n - 1, 0, -1))
 656        if targQ > winnerQ:
 657            ballot[targId] = i
 658            i -= 1
 659            del placesToFill[-2]
 660        for j in placesToFill:
 661            nextLoser, loserScore = places[j]  # all but winner, low to high
 662            if voter[nextLoser] > winnerQ:
 663                ballot[nextLoser] = i
 664                i -= 1
 665        ballot[frontId] = i
 666        i -= 1
 667        for j in placesToFill:
 668            nextLoser, loserScore = places[j]
 669            if voter[nextLoser] <= winnerQ:
 670                ballot[nextLoser] = i
 671                i -= 1
 672        assert i == -1
 673        ballot[:] = [
 674            candidate for candidate, _rank in sorted(enumerate(ballot), key=lambda x: -x[1])
 675        ]
 676
 677
 678class IrvPrime(Irv):
 679    """
 680    IRV Prime.
 681
 682    See https://electowiki.org/wiki/IRV_Prime
 683    """
 684
 685    stratTargetFor = Method.stratTarget3
 686
 687    def results(self, ballots, **kwargs):
 688        """IRV Prime results.
 689
 690        >>> IrvPrime().results([[0,1,2]])[2]
 691        0
 692        >>> IrvPrime().results([[0,1,2],[2,1,0]])[1]
 693        0
 694        >>> IrvPrime().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
 695        [0, 2, 1]
 696        >>> IrvPrime().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
 697        [1, 2, 0]
 698        >>> # Favorite betrayal example from http://rangevoting.org/IncentToExagg.html
 699        >>> IrvPrime().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
 700        [2, 1, 0]
 701        >>> IrvPrime().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
 702        [1, 0, 3, 2, 4]
 703        >>> # Elections 3-5 from http://votingmatters.org.uk/ISSUE6/P4.HTM
 704        >>> IrvPrime().results([[0,1,2,3,4,5]] * 12 + [[2,0,1,3,4,5]] * 11 + [[1,2,0,3,4,5]] * 10 +
 705        ...     [[3,4,5]] * 27)
 706        [2, 5, 4, 3, 1, 0]
 707        >>> IrvPrime().results([[0,1]] * 11 + [[1]] * 7 + [[2]] * 12)
 708        [0, 2, 1]
 709        >>> IrvPrime().results([[0,3,2,1]] * 5 + [[1,2,0,3]] * 5 + [[2,0,1,3]] * 8 +
 710        ...    [[3,0,1,2]] * 4 + [[3,1,2,0]] * 8)
 711        [3, 0, 1, 2]
 712        >>> IrvPrime().results([[0,2,1,3]] * 6 + [[0,3,1,2]] * 3 + [[0,3,2,1]] * 3 +
 713        ...     [[1,2,0,3]] * 4 + [[2,0,1,3]] * 4 + [[3,1,2,0]] * 5)
 714        [2, 0, 3, 1]
 715        >>> # Failure of later-no-harm
 716        >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
 717        ...     [[1,0,2]] * 21 + [[2,0,1]] * 30 + [[2,1,0]] * 20)
 718        [1, 0, 2]
 719        >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
 720        ...     [[1,0,2]] * 21 + [[2,1,0]] * 30 + [[2,1,0]] * 20)
 721        [1, 2, 0]
 722        """
 723
 724        ballots = ballots_from_dataframe(ballots)
 725
 726        remaining = self.buildPreferenceSchedule(ballots)
 727        ncand = len(self.candidateVotes(remaining))
 728        classic = self.runIrv(remaining, ncand)
 729
 730        # Keep the winner from the classic IRV
 731        winners = {self.winner(classic)}
 732
 733        # Find all candidates that can beat classic IRV winner; this may be a superset
 734        # of schwartz/smith, but it's all that matters
 735        winnersPrime = set()
 736        for possibleWinner in range(ncand):
 737            if possibleWinner in winners:
 738                continue
 739
 740            numWins = 0
 741            numLosses = 0
 742            for ranking, votes in remaining.items():
 743                possibleWinnerRanking = winnerRanking = len(ranking) + 1
 744                for pos in range(len(ranking)):
 745                    if ranking[pos] == possibleWinner:
 746                        possibleWinnerRanking = pos
 747                    # We can change this to a loop if there's > 1 winner
 748                    elif ranking[pos] == next(iter(winners)):
 749                        winnerRanking = pos
 750                if possibleWinnerRanking < winnerRanking:
 751                    numWins += votes
 752                elif winnerRanking < possibleWinnerRanking:
 753                    numLosses += votes
 754            if numWins > numLosses:
 755                winnersPrime.add(possibleWinner)
 756
 757        # Now re-run IRV preserving all winners + winners prime
 758        keepers = winners.union(winnersPrime)
 759        eliminated = []
 760        for _ in range(ncand):
 761            votes = self.candidateVotes(remaining)
 762            toEliminate = self.getLeast(votes, keepers)
 763            if not isinstance(toEliminate, CandidateWithCount):
 764                # Begin "step 4", i.e. continue elimination without preserving anyone
 765                keepers = {}
 766                toEliminate = self.getLeast(votes)
 767            if not isinstance(toEliminate, CandidateWithCount):
 768                break
 769            eliminated.append(toEliminate.candidate)
 770            remaining = self.eliminateCandidate(remaining, toEliminate)
 771
 772        return self.scoresFromEliminations(eliminated, ncand)
 773
 774
 775class V321(Mav):
 776    baseCuts = [-0.1, 0.8]
 777    specificPercentiles = [45, 75]
 778
 779    stratTargetFor = Method.stratTarget3
 780
 781    def results(self, ballots, isHonest=False, **kwargs):
 782        """3-2-1 Voting results.
 783
 784        >>> V321().resultsFor(DeterministicModel(3)(5,3),V321().honBallot)["results"]
 785        [-0.75, 2, 1]
 786        >>> V321().results([[0,1,2]])[2]
 787        2
 788        >>> V321().results([[0,1,2],[2,1,0]])[1]
 789        2.5
 790        >>> V321().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
 791        [1, 1.5, -0.25]
 792        >>> V321().results([[0,1,2,1]]*29 + [[1,2,0,1]]*30 + [[2,0,1,1]]*31 + [[1,1,1,2]]*10)
 793        [3, 0.5, 1, 0]
 794        >>> V321().results([[1,0,2,1]]*29 + [[0,2,1,1]]*30 + [[2,1,0,1]]*31 + [[1,1,1,2]]*10)
 795        [3.375, 2.875, 0.25, 0]
 796        """
 797        ballots = ballots_from_dataframe(ballots)
 798        candScores = list(zip(*ballots))
 799        n2s = [sum(1 if s > 1 else 0 for s in c) for c in candScores]
 800        o2s = argsort(n2s)  # order
 801        r2s = [-1] * len(n2s)  # ranks
 802        for r, i in enumerate(o2s):
 803            r2s[i] = r
 804        semifinalists = o2s[-3:]  # [third, second, first] by top ranks
 805        # print(semifinalists)
 806        n1s = [sum(1 if s > 0 else 0 for s in candScores[sf]) for sf in semifinalists]
 807        o1s = argsort(n1s)
 808        # print("n1s",n1s)
 809        # print("o1s",o1s)
 810        # print([semifinalists[o] for o in o1s]) #[third, second, first] by above-bottom
 811        # print("r2s",r2s)
 812        r2s[semifinalists[o1s[0]]] -= (o1s[0] + 1) * 0.75  # non-finalist below finalists
 813
 814        (runnerUp, top) = semifinalists[o1s[1]], semifinalists[o1s[2]]
 815        upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots)
 816        if upset > 0:
 817            runnerUp, top = top, runnerUp
 818            r2s[runnerUp], r2s[top] = r2s[top] - 0.125, r2s[runnerUp] + 0.125
 819        r2s[top] = max(r2s[top], r2s[runnerUp] + 0.5)
 820        if isHonest:
 821            upset2 = sum(
 822                sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[2]]])
 823                for ballot in ballots
 824            )
 825            self.__class__.extraEvents["3beats1"] = upset2 > 0
 826            upset3 = sum(
 827                sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[1]]])
 828                for ballot in ballots
 829            )
 830            self.__class__.extraEvents["3beats2"] = upset3 > 0
 831            if len(o2s) > 3:
 832                fourth = o2s[-4]
 833                fourthNotLasts = sum(1 if s > 1 else 0 for s in candScores[fourth])
 834                fourthWin = (
 835                    fourthNotLasts > n1s[o1s[1]]
 836                    and sum(
 837                        sign(ballot[fourth] - ballot[semifinalists[o1s[2]]]) for ballot in ballots
 838                    )
 839                    > 0
 840                )
 841                self.__class__.extraEvents["4beats1"] = fourthWin
 842
 843        return [as_builtin_scalar(score) for score in r2s]
 844
 845    def stratBallotFor(self, polls):
 846        """Returns a function which takes utilities and returns a dict(
 847            isStrat=
 848        for the given "polling" info.
 849
 850
 851        >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
 852        [2, 1, 0, 3]
 853        """
 854        places = sorted(enumerate(polls), key=lambda x: -x[1])  # high to low
 855        top3 = [c for c, r in places[:3]]
 856
 857        # @rememberBallots ... do it later
 858        def stratBallot(cls, voter):
 859            stratGap = voter[top3[1]] - voter[top3[0]]
 860            myPrefs = [c for c, v in sorted(enumerate(voter), key=lambda x: -x[1])]  # high to low
 861            my3order = [myPrefs.index(c) for c in top3]
 862            rating = 2
 863            ballot = [0] * len(voter)
 864            if my3order[0] == min(my3order):  # agree on winner
 865                for i in range(my3order[0] + 1):
 866                    ballot[myPrefs[i]] = 2
 867                if my3order[1] <= my3order[2]:
 868                    for i in range(my3order[0] + 1, my3order[1] + 1):
 869                        ballot[myPrefs[i]] = 1
 870                # print("agree",top3, my3order,ballot,[float('%.1g' % c) for c in voter])
 871                return dict(strat=ballot, isStrat=False, stratGap=stratGap)
 872            for c in myPrefs:
 873                ballot[c] = rating
 874                if rating and (c in top3):
 875                    if c == top3[0]:
 876                        rating = 0
 877                    else:
 878                        rating -= 1
 879
 880            # print("disagree",top3,my3order,ballot,[float('%.1g' % c) for c in voter])
 881            return dict(strat=ballot, isStrat=True, stratGap=stratGap)
 882
 883        if self.extraEvents["3beats1"]:
 884
 885            @rememberBallots
 886            def stratBallo2(cls, voter):
 887                myprefs = sorted(enumerate(voter), key=lambda x: -x[1])  # high to low
 888                rating = 2
 889                ballot = [None] * len(voter)
 890                isStrat = False
 891                stratGap = 0
 892                for c, util in myprefs:
 893                    ballot[c] = rating
 894                    if rating and (c in top3):
 895                        if c == top3[2]:
 896                            rating = 0
 897                        else:
 898                            rating -= 1
 899                isStrat = voter[top3[0]] == max(voter[c] for c in top3)
 900                return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
 901
 902            stratBallo2.__name__ = "stratBallot"  # God, that's ugly.
 903            return stratBallo2
 904
 905        if self.extraEvents["4beats1"]:
 906            fourth = places[3][0]
 907            first = top3[1]
 908
 909            @rememberBallots
 910            def stratBallo3(cls, voter):
 911                stratGap = voter[top3[1]] - voter[top3[0]]
 912                myprefs = sorted(enumerate(voter), key=lambda x: -x[1])  # high to low
 913
 914                rating = 2
 915                ballot = [None] * len(voter)
 916                if voter[fourth] > voter[first]:
 917                    for c, util in myprefs:
 918                        ballot[c] = rating
 919                        if rating and (c == fourth):
 920                            rating -= 2
 921                    return dict(strat=ballot, isStrat=True, stratGap=stratGap)
 922
 923                return stratBallot(cls, voter)
 924
 925            stratBallo3.__name__ = "stratBallot"  # God, that's ugly.
 926            return stratBallo3
 927
 928        return rememberBallots(stratBallot)
 929
 930
 931class Schulze(RankedMethod):
 932    def resolveCycle(self, cmat, n):
 933
 934        beatStrength = [[0] * n] * n
 935        numWins = [0] * n
 936        for i in range(n):
 937            for j in range(n):
 938                if i != j:
 939                    beatStrength[i][j] = cmat[i][j] if cmat[i][j] > cmat[j][i] else 0
 940
 941                for pivot in range(n):
 942                    for source in range(n):
 943                        if pivot != source:
 944                            for target in range(n):
 945                                if pivot != target and source != target:
 946                                    beatStrength[source][target] = max(
 947                                        beatStrength[source][target],
 948                                        min(
 949                                            beatStrength[source][pivot],
 950                                            beatStrength[pivot][target],
 951                                        ),
 952                                    )
 953
 954        for i in range(n):
 955            for j in range(n):
 956                if i != j:
 957                    if beatStrength[i][j] > beatStrength[j][i]:
 958                        numWins[i] += 1
 959                    if (
 960                        beatStrength[i][j] == beatStrength[j][i] and i < j
 961                    ):  # break ties deterministically
 962                        numWins[i] += 1
 963
 964        return numWins
 965
 966    def results(self, ballots, isHonest=False, **kwargs):
 967        """Schulze results.
 968
 969        >>> Schulze().resultsFor(DeterministicModel(3)(5,3),Schulze().honBallot,isHonest=True)["results"]
 970        [2, 0, 1]
 971        >>> Schulze.extraEvents
 972        {'scenario': 'cycle'}
 973        >>> Schulze().results([[0,1,2]],isHonest=True)[2]
 974        2
 975        >>> Schulze.extraEvents
 976        {'scenario': 'easy'}
 977        >>> Schulze().results([[0,1,2],[2,1,0]],isHonest=True)[1]
 978        1
 979        >>> Schulze.extraEvents
 980        {'scenario': 'easy'}
 981        >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2,isHonest=True)
 982        [1, 2, 0]
 983        >>> Schulze.extraEvents
 984        {'scenario': 'chicken'}
 985        >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 2 + [[1,2,0]] * 3,isHonest=True)
 986        [1, 2, 0]
 987        >>> Schulze.extraEvents
 988        {'scenario': 'squeeze'}
 989        >>> Schulze().results([[3,2,1,0]] * 5 + [[2,3,1,0]] * 2 + [[0,1,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
 990        [2, 3, 1, 0]
 991        >>> Schulze.extraEvents
 992        {'scenario': 'other'}
 993        >>> Schulze().results([[3,0,0,0]] * 5 + [[2,3,0,0]] * 2 + [[0,0,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
 994        [3, 0, 1, 2]
 995        >>> Schulze.extraEvents
 996        {'scenario': 'spoiler'}
 997        """
 998        ballots = ballots_from_dataframe(ballots)
 999        n = len(ballots[0])
1000        cmat = [[0 for _ in range(n)] for _ in range(n)]
1001        numWins = [0] * n
1002        for i in range(n):
1003            for j in range(n):
1004                if i != j:
1005                    cmat[i][j] = sum(sign(ballot[i] - ballot[j]) for ballot in ballots)
1006                    if cmat[i][j] > 0:
1007                        numWins[i] += 1
1008                    elif cmat[i][j] == 0 and i < j:
1009                        numWins[i] += 1
1010        condOrder = sorted(enumerate(numWins), key=lambda x: -x[1])
1011        if condOrder[0][1] == n - 1:
1012            cycle = 0
1013            result = numWins
1014        else:  # cycle
1015            cycle = 1
1016            result = self.resolveCycle(cmat, n)
1017
1018        if isHonest:
1019            self.__class__.extraEvents = {}
1020            # check scenarios
1021            plurTally = [0] * n
1022            plur3Tally = [0] * 3
1023            cond3 = [c for c, v in condOrder[:3]]
1024            for b in ballots:
1025                b3 = [b[c] for c in cond3]
1026                plurTally[b.index(max(b))] += 1
1027                plur3Tally[b3.index(max(b3))] += 1
1028            plurOrder = sorted(enumerate(plurTally), key=lambda x: -x[1])
1029            plur3Order = sorted(enumerate(plur3Tally), key=lambda x: -x[1])
1030            if cycle:
1031                self.__class__.extraEvents["scenario"] = "cycle"
1032            elif plurOrder[0][0] == condOrder[0][0]:
1033                self.__class__.extraEvents["scenario"] = "easy"
1034            elif plur3Order[0][0] == condOrder[0][0]:
1035                self.__class__.extraEvents["scenario"] = "spoiler"
1036            elif plur3Order[2][0] == condOrder[0][0]:
1037                self.__class__.extraEvents["scenario"] = "squeeze"
1038            elif plur3Order[0][0] == condOrder[2][0]:
1039                self.__class__.extraEvents["scenario"] = "chicken"
1040            else:
1041                self.__class__.extraEvents["scenario"] = "other"
1042
1043        return result
1044
1045    @classmethod
1046    def fillStratBallot(
1047        cls,
1048        voter,
1049        polls,
1050        places,
1051        n,
1052        stratGap,
1053        ballot,
1054        frontId,
1055        frontResult,
1056        targId,
1057        targResult,
1058    ):
1059
1060        if stratGap > 0:
1061            others = [c for (c, r) in places[2:]]
1062            notTooBad = min(voter[frontId], voter[targId])
1063            decentOnes = [c for c in others if voter[c] >= notTooBad]
1064            cls.fillPrefOrder(voter, ballot, whichCands=decentOnes, lowSlot=n - len(decentOnes))
1065            # ballot[frontId], ballot[targId] = n-len(decentOnes)-1, n-len(decentOnes)-2
1066            ballot[frontId], ballot[targId] = 0, n - len(decentOnes) - 1
1067            cls.fillPrefOrder(
1068                voter,
1069                ballot,
1070                whichCands=[c for c in others if voter[c] < notTooBad],
1071                lowSlot=1,
1072            )
1073        else:
1074            ballot[frontId] = n - 1
1075            cls.fillPrefOrder(voter, ballot, whichCands=[c for (c, r) in places[1:]], lowSlot=0)
1076
1077
1078class Rp(Schulze):
1079    def resolveCycle(self, cmat, n):
1080        """Note: mutates cmat destructively.
1081
1082        >>> Rp().resultsFor(DeterministicModel(3)(5,3),Rp().honBallot,isHonest=True)["results"]
1083        [1, 2, 0]
1084        """
1085        matches = [(i, j, cmat[i][j]) for i in range(n) for j in range(i, n) if i != j]
1086        rps = sorted(matches, key=lambda x: -abs(x[2]))
1087        for i, j, margin in rps:
1088            if margin < 0:
1089                i, j = j, i
1090            if cmat[j][i] is not True:
1091                # print(i,j,cmat)
1092                cmat[i][j] = True
1093                # print("....",i,j,cmat)
1094                for k in range(n):
1095                    if k not in (i, j):
1096                        if cmat[j][k] is True:
1097                            cmat[i][k] = True
1098                        if cmat[k][i] is True:
1099                            cmat[k][j] = True
1100
1101                            # print(".......",i,j,k,cmat)
1102
1103        return [sum(cmat[i][j] is True for j in range(n)) for i in range(n)]
1104
1105
1106class IRNR(RankedMethod):
1107    stratMax = 10
1108
1109    stratTargetFor = Method.stratTarget3  # strategize in favor of third place, because second place is pointless (can't change pairwise)
1110
1111    def results(self, ballots, **kwargs):
1112        ballots = ballots_from_dataframe(ballots)
1113        enabled = [True] * len(ballots[0])
1114        numEnabled = sum(enabled)
1115        results = [None] * len(enabled)
1116        while numEnabled > 1:
1117            tsum = [0.0] * len(enabled)
1118            for bal in ballots:
1119                vsum = 0.0
1120                for i, v in enumerate(bal):
1121                    if enabled[i]:
1122                        vsum += abs(v)
1123                if vsum == 0.0:
1124                    # TODO: count spoiled ballot
1125                    continue
1126                for i, v in enumerate(bal):
1127                    if enabled[i]:
1128                        tsum[i] += v / vsum
1129            mini = None
1130            minv = None
1131            for i, v in enumerate(tsum):
1132                if enabled[i] and ((minv is None) or (tsum[i] < minv)):
1133                    minv = tsum[i]
1134                    mini = i
1135            enabled[mini] = False
1136            results[mini] = minv
1137            numEnabled -= 1
1138        for i, v in enumerate(tsum):
1139            if enabled[i]:
1140                results[i] = tsum[i]
1141        return results
1142
1143    @staticmethod  # cls is provided explicitly, not through binding
1144    @rememberBallot
1145    def honBallot(cls, utils):
1146        """Takes utilities and returns an honest ballot"""
1147        return utils
1148
1149    @classmethod
1150    def fillStratBallot(
1151        cls,
1152        voter,
1153        polls,
1154        places,
1155        n,
1156        stratGap,
1157        ballot,
1158        frontId,
1159        frontResult,
1160        targId,
1161        targResult,
1162    ):
1163        if stratGap <= 0:
1164            ballot[frontId], ballot[targId] = cls.stratMax, 0
1165        else:
1166            ballot[frontId], ballot[targId] = 0, cls.stratMax
1167        cls.fillPrefOrder(
1168            voter,
1169            ballot,
1170            whichCands=[c for (c, r) in places[2:]],
1171            nSlots=1,
1172            lowSlot=1,
1173            remainderScore=0,
1174        )
1175
1176
1177__all__ = [
1178    "Borda",
1179    "BulletyApprovalWith",
1180    "IRNR",
1181    "Irv",
1182    "IrvPrime",
1183    "Mav",
1184    "Mj",
1185    "Plurality",
1186    "RankedMethod",
1187    "RatedMethod",
1188    "Rp",
1189    "Schulze",
1190    "Score",
1191    "Srv",
1192    "V321",
1193    "toVote",
1194]
class Borda(vse_sim.data_classes.Method):
13class Borda(Method):
14    candScore = staticmethod(mean)
15
16    nRanks = 999  # infinity
17
18    @staticmethod
19    def fillPrefOrder(
20        voter,
21        ballot,
22        whichCands=None,  # None means "all"; otherwise, an iterable of cand indexes
23        lowSlot=0,
24        nSlots=None,  # again, None means "all"
25        remainderScore=None,  # what to give candidates that don't fit in nSlots
26    ):
27
28        venum = list(enumerate(voter))
29        if whichCands:
30            venum = [venum[c] for c in whichCands]
31        prefOrder = sorted(venum, key=lambda x: -x[1])  # high to low
32        Borda.fillCands(ballot, prefOrder, lowSlot, nSlots, remainderScore)
33        # modifies ballot argument, returns nothing.
34
35    @staticmethod
36    def fillCands(
37        ballot,
38        whichCands,  # list of tuples starting with cand id, in descending order
39        lowSlot=0,
40        nSlots=None,  # again, None means "all"
41        remainderScore=None,  # what to give candidates that don't fit in nSlots
42    ):
43        if nSlots is None:
44            nSlots = len(whichCands)
45        cur = lowSlot + nSlots - 1
46        for i in range(nSlots):
47            ballot[whichCands[i][0]] = cur
48            cur -= 1
49        if remainderScore is not None:
50            i += 1
51            while i < len(whichCands):
52                ballot[whichCands[i][0]] = remainderScore
53                i += 1
54        # modifies ballot argument, returns nothing.
55
56    @staticmethod  # cls is provided explicitly, not through binding
57    @rememberBallot
58    def honBallot(cls, utils):
59        ballot = [0] * len(utils)
60        cls.fillPrefOrder(utils, ballot)
61        return ballot
62
63    @classmethod
64    def fillStratBallot(
65        cls,
66        voter,
67        polls,
68        places,
69        n,
70        stratGap,
71        ballot,
72        frontId,
73        frontResult,
74        targId,
75        targResult,
76    ):
77        """Mutates the `ballot` argument to be a strategic ballot.
78
79        >>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
80        [3, 0, 1, 2]
81        """
82        nRanks = min(cls.nRanks, n)
83        if stratGap <= 0:
84            ballot[frontId], ballot[targId] = (nRanks - 1), 0
85        else:
86            ballot[frontId], ballot[targId] = 0, (nRanks - 1)
87        nRanks -= 2
88        if nRanks > 0:
89            cls.fillCands(ballot, places[2:][::-1], lowSlot=1, nSlots=nRanks, remainderScore=0)
90        # (don't) return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)

Base class for election methods. Holds some of the duct tape.

def candScore(*args, **kwargs):
30def mean(*args, **kwargs):
31    return as_builtin_scalar(numpy_mean(*args, **kwargs))
nRanks = 999
@staticmethod
def fillPrefOrder( voter, ballot, whichCands=None, lowSlot=0, nSlots=None, remainderScore=None):
18    @staticmethod
19    def fillPrefOrder(
20        voter,
21        ballot,
22        whichCands=None,  # None means "all"; otherwise, an iterable of cand indexes
23        lowSlot=0,
24        nSlots=None,  # again, None means "all"
25        remainderScore=None,  # what to give candidates that don't fit in nSlots
26    ):
27
28        venum = list(enumerate(voter))
29        if whichCands:
30            venum = [venum[c] for c in whichCands]
31        prefOrder = sorted(venum, key=lambda x: -x[1])  # high to low
32        Borda.fillCands(ballot, prefOrder, lowSlot, nSlots, remainderScore)
33        # modifies ballot argument, returns nothing.
@staticmethod
def fillCands(ballot, whichCands, lowSlot=0, nSlots=None, remainderScore=None):
35    @staticmethod
36    def fillCands(
37        ballot,
38        whichCands,  # list of tuples starting with cand id, in descending order
39        lowSlot=0,
40        nSlots=None,  # again, None means "all"
41        remainderScore=None,  # what to give candidates that don't fit in nSlots
42    ):
43        if nSlots is None:
44            nSlots = len(whichCands)
45        cur = lowSlot + nSlots - 1
46        for i in range(nSlots):
47            ballot[whichCands[i][0]] = cur
48            cur -= 1
49        if remainderScore is not None:
50            i += 1
51            while i < len(whichCands):
52                ballot[whichCands[i][0]] = remainderScore
53                i += 1
54        # modifies ballot argument, returns nothing.
@staticmethod
@rememberBallot
def honBallot(cls, utils):
56    @staticmethod  # cls is provided explicitly, not through binding
57    @rememberBallot
58    def honBallot(cls, utils):
59        ballot = [0] * len(utils)
60        cls.fillPrefOrder(utils, ballot)
61        return ballot

Takes utilities and returns an honest ballot

@classmethod
def fillStratBallot( cls, voter, polls, places, n, stratGap, ballot, frontId, frontResult, targId, targResult):
63    @classmethod
64    def fillStratBallot(
65        cls,
66        voter,
67        polls,
68        places,
69        n,
70        stratGap,
71        ballot,
72        frontId,
73        frontResult,
74        targId,
75        targResult,
76    ):
77        """Mutates the `ballot` argument to be a strategic ballot.
78
79        >>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
80        [3, 0, 1, 2]
81        """
82        nRanks = min(cls.nRanks, n)
83        if stratGap <= 0:
84            ballot[frontId], ballot[targId] = (nRanks - 1), 0
85        else:
86            ballot[frontId], ballot[targId] = 0, (nRanks - 1)
87        nRanks -= 2
88        if nRanks > 0:
89            cls.fillCands(ballot, places[2:][::-1], lowSlot=1, nSlots=nRanks, remainderScore=0)
90        # (don't) return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)

Mutates the ballot argument to be a strategic ballot.

>>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
[3, 0, 1, 2]
def BulletyApprovalWith(bullets=0.5, asClass=False):
248def BulletyApprovalWith(bullets=0.5, asClass=False):
249
250    class BulletyApproval((Score(1, True))):
251        bulletiness = bullets
252
253        def __str__(self):
254            return f"BulletyApproval{str(round(self.bulletiness * 100))}"
255
256        @staticmethod  # cls is provided explicitly, not through binding
257        @rememberBallot
258        def honBallot(cls, utils):
259            """Takes utilities and returns an honest ballot (on 0..10)
260
261
262            honest ballots work as expected
263                >>> Score().honBallot(Score, Voter([5,6,7]))
264                [0.0, 5.0, 10.0]
265                >>> Score().resultsFor(DeterministicModel(3)(5,3),Score().honBallot)["results"]
266                [4.0, 6.0, 5.0]
267            """
268            if random.random() > cls.bulletiness:
269                return cls.__bases__[0].honBallot(cls, utils)
270            best = max(utils)
271            return [1 if util == best else 0 for util in utils]
272
273    return BulletyApproval if asClass else BulletyApproval()
class IRNR(Borda):
1107class IRNR(RankedMethod):
1108    stratMax = 10
1109
1110    stratTargetFor = Method.stratTarget3  # strategize in favor of third place, because second place is pointless (can't change pairwise)
1111
1112    def results(self, ballots, **kwargs):
1113        ballots = ballots_from_dataframe(ballots)
1114        enabled = [True] * len(ballots[0])
1115        numEnabled = sum(enabled)
1116        results = [None] * len(enabled)
1117        while numEnabled > 1:
1118            tsum = [0.0] * len(enabled)
1119            for bal in ballots:
1120                vsum = 0.0
1121                for i, v in enumerate(bal):
1122                    if enabled[i]:
1123                        vsum += abs(v)
1124                if vsum == 0.0:
1125                    # TODO: count spoiled ballot
1126                    continue
1127                for i, v in enumerate(bal):
1128                    if enabled[i]:
1129                        tsum[i] += v / vsum
1130            mini = None
1131            minv = None
1132            for i, v in enumerate(tsum):
1133                if enabled[i] and ((minv is None) or (tsum[i] < minv)):
1134                    minv = tsum[i]
1135                    mini = i
1136            enabled[mini] = False
1137            results[mini] = minv
1138            numEnabled -= 1
1139        for i, v in enumerate(tsum):
1140            if enabled[i]:
1141                results[i] = tsum[i]
1142        return results
1143
1144    @staticmethod  # cls is provided explicitly, not through binding
1145    @rememberBallot
1146    def honBallot(cls, utils):
1147        """Takes utilities and returns an honest ballot"""
1148        return utils
1149
1150    @classmethod
1151    def fillStratBallot(
1152        cls,
1153        voter,
1154        polls,
1155        places,
1156        n,
1157        stratGap,
1158        ballot,
1159        frontId,
1160        frontResult,
1161        targId,
1162        targResult,
1163    ):
1164        if stratGap <= 0:
1165            ballot[frontId], ballot[targId] = cls.stratMax, 0
1166        else:
1167            ballot[frontId], ballot[targId] = 0, cls.stratMax
1168        cls.fillPrefOrder(
1169            voter,
1170            ballot,
1171            whichCands=[c for (c, r) in places[2:]],
1172            nSlots=1,
1173            lowSlot=1,
1174            remainderScore=0,
1175        )

Base class for election methods. Holds some of the duct tape.

stratMax = 10
def stratTargetFor(self, places):
311    def stratTarget3(self, places):
312        ((frontId, frontResult), (targId, targResult)) = places[:3:2]
313        return (frontId, frontResult, targId, targResult)
def results(self, ballots, **kwargs):
1112    def results(self, ballots, **kwargs):
1113        ballots = ballots_from_dataframe(ballots)
1114        enabled = [True] * len(ballots[0])
1115        numEnabled = sum(enabled)
1116        results = [None] * len(enabled)
1117        while numEnabled > 1:
1118            tsum = [0.0] * len(enabled)
1119            for bal in ballots:
1120                vsum = 0.0
1121                for i, v in enumerate(bal):
1122                    if enabled[i]:
1123                        vsum += abs(v)
1124                if vsum == 0.0:
1125                    # TODO: count spoiled ballot
1126                    continue
1127                for i, v in enumerate(bal):
1128                    if enabled[i]:
1129                        tsum[i] += v / vsum
1130            mini = None
1131            minv = None
1132            for i, v in enumerate(tsum):
1133                if enabled[i] and ((minv is None) or (tsum[i] < minv)):
1134                    minv = tsum[i]
1135                    mini = i
1136            enabled[mini] = False
1137            results[mini] = minv
1138            numEnabled -= 1
1139        for i, v in enumerate(tsum):
1140            if enabled[i]:
1141                results[i] = tsum[i]
1142        return results

Combines ballots into results. Override for comparative methods.

Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index.

Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index.

Test for subclasses, makes no sense to test this method in the abstract base class.

@staticmethod
@rememberBallot
def honBallot(cls, utils):
1144    @staticmethod  # cls is provided explicitly, not through binding
1145    @rememberBallot
1146    def honBallot(cls, utils):
1147        """Takes utilities and returns an honest ballot"""
1148        return utils

Takes utilities and returns an honest ballot

@classmethod
def fillStratBallot( cls, voter, polls, places, n, stratGap, ballot, frontId, frontResult, targId, targResult):
1150    @classmethod
1151    def fillStratBallot(
1152        cls,
1153        voter,
1154        polls,
1155        places,
1156        n,
1157        stratGap,
1158        ballot,
1159        frontId,
1160        frontResult,
1161        targId,
1162        targResult,
1163    ):
1164        if stratGap <= 0:
1165            ballot[frontId], ballot[targId] = cls.stratMax, 0
1166        else:
1167            ballot[frontId], ballot[targId] = 0, cls.stratMax
1168        cls.fillPrefOrder(
1169            voter,
1170            ballot,
1171            whichCands=[c for (c, r) in places[2:]],
1172            nSlots=1,
1173            lowSlot=1,
1174            remainderScore=0,
1175        )

Mutates the ballot argument to be a strategic ballot.

>>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
[3, 0, 1, 2]
class Irv(vse_sim.data_classes.Method):
513class Irv(Method):
514    """
515    IRV.
516
517    Ballots are ordered candidate rankings, from most to least preferred.
518    Results are candidate-indexed elimination ranks, with higher numbers better.
519    """
520
521    stratTargetFor = Method.stratTarget3
522
523    def buildPreferenceSchedule(self, ballots):
524        """Gets a dictionary of the form {ranking as tuple, vote count}"""
525
526        prefs = {}
527        for b in ballots:
528            key = tuple(b)
529            if key in prefs:
530                prefs[key] += 1
531            else:
532                prefs[key] = 1
533        return prefs
534
535    def eliminateCandidate(self, inputPrefs, toEliminate):
536        """Gets a dictionary of the form {ranking as tuple, vote count} with toEliminate removed"""
537
538        if not isinstance(toEliminate, CandidateWithCount):
539            return inputPrefs
540
541        prefs = {}
542        for ranking, votes in inputPrefs.items():
543            newranking = [candidate for candidate in ranking if candidate != toEliminate.candidate]
544
545            if not newranking:
546                continue
547            newkey = tuple(newranking)
548            if newkey in prefs:
549                prefs[newkey] += votes
550            else:
551                prefs[newkey] = votes
552        return prefs
553
554    def candidateVotes(self, prefSchedule):
555        """Gets a list of CandidateWithCount, from highest to lowest"""
556        candidates = {}
557        for ranking, votes in prefSchedule.items():
558            candidate = ranking[0]
559            if candidate in candidates:
560                candidates[candidate].votes += votes
561            else:
562                candidates[candidate] = CandidateWithCount(candidate, votes)
563
564        # Simply for VSE which requires ranking of non-winners; in real election we don't really
565        # care
566        alternates = []
567        trackedalt = set()
568        for ranking, votes in prefSchedule.items():
569            for alternate in ranking[1:]:
570                if (alternate not in candidates) and alternate not in trackedalt:
571                    alternates.append(CandidateWithCount(alternate, 0))
572                    trackedalt.add(alternate)
573
574        return (
575            sorted(candidates.values(), key=lambda c: (c.votes, c.candidate), reverse=True)
576            + alternates
577        )
578
579    def getLeast(self, voteRanking, keep=None):
580        if keep is None:
581            keep = {}
582        for candidate in reversed(voteRanking):
583            if candidate.candidate not in keep:
584                return candidate
585        return None
586
587    @staticmethod
588    def scoresFromEliminations(eliminated, ncand):
589        """Convert an IRV elimination order to candidate-indexed scores."""
590        results = [-1] * ncand
591        for score, candidate in enumerate(eliminated):
592            results[candidate] = score
593        return results
594
595    def runIrv(self, remaining, ncand):
596        """IRV results."""
597        eliminated = []
598        for _ in range(ncand):
599            votes = self.candidateVotes(remaining)
600            toEliminate = self.getLeast(votes)
601            if not isinstance(toEliminate, CandidateWithCount):
602                break
603            eliminated.append(toEliminate.candidate)
604            remaining = self.eliminateCandidate(remaining, toEliminate)
605        return self.scoresFromEliminations(eliminated, ncand)
606
607    def results(self, ballots, **kwargs):
608        """IRV results.
609
610        >>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
611        [0, 1, 2]
612        >>> Irv().results([[0,1,2]])[2]
613        0
614        >>> Irv().results([[0,1,2],[2,1,0]])[1]
615        0
616        >>> Irv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
617        [1, 0, 2]
618        """
619        ballots = ballots_from_dataframe(ballots)
620        return self.runIrv(self.buildPreferenceSchedule(ballots), len(ballots[0]))
621
622    @staticmethod  # cls is provided explicitly, not through binding
623    @rememberBallot
624    def honBallot(cls, voter):
625        """Takes utilities and returns an honest ballot
626
627        >>> Irv.honBallot(Irv,Voter([4,1,6,3]))
628        [2, 0, 3, 1]
629        >>> Irv.honBallot(Irv,Voter([0,1,2]))
630        [2, 1, 0]
631        """
632        order = sorted(enumerate(voter), key=lambda x: (-x[1], x[0]))
633        return [candidate for candidate, _utility in order]
634
635    @classmethod
636    def fillStratBallot(
637        cls,
638        voter,
639        polls,
640        places,
641        n,
642        stratGap,
643        ballot,
644        frontId,
645        frontResult,
646        targId,
647        targResult,
648    ):
649        """
650        >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
651        [2, 1, 0, 3]
652        """
653        i = n - 1
654        winnerQ = voter[frontId]
655        targQ = voter[targId]
656        placesToFill = list(range(n - 1, 0, -1))
657        if targQ > winnerQ:
658            ballot[targId] = i
659            i -= 1
660            del placesToFill[-2]
661        for j in placesToFill:
662            nextLoser, loserScore = places[j]  # all but winner, low to high
663            if voter[nextLoser] > winnerQ:
664                ballot[nextLoser] = i
665                i -= 1
666        ballot[frontId] = i
667        i -= 1
668        for j in placesToFill:
669            nextLoser, loserScore = places[j]
670            if voter[nextLoser] <= winnerQ:
671                ballot[nextLoser] = i
672                i -= 1
673        assert i == -1
674        ballot[:] = [
675            candidate for candidate, _rank in sorted(enumerate(ballot), key=lambda x: -x[1])
676        ]

IRV.

Ballots are ordered candidate rankings, from most to least preferred. Results are candidate-indexed elimination ranks, with higher numbers better.

def stratTargetFor(self, places):
311    def stratTarget3(self, places):
312        ((frontId, frontResult), (targId, targResult)) = places[:3:2]
313        return (frontId, frontResult, targId, targResult)
def buildPreferenceSchedule(self, ballots):
523    def buildPreferenceSchedule(self, ballots):
524        """Gets a dictionary of the form {ranking as tuple, vote count}"""
525
526        prefs = {}
527        for b in ballots:
528            key = tuple(b)
529            if key in prefs:
530                prefs[key] += 1
531            else:
532                prefs[key] = 1
533        return prefs

Gets a dictionary of the form {ranking as tuple, vote count}

def eliminateCandidate(self, inputPrefs, toEliminate):
535    def eliminateCandidate(self, inputPrefs, toEliminate):
536        """Gets a dictionary of the form {ranking as tuple, vote count} with toEliminate removed"""
537
538        if not isinstance(toEliminate, CandidateWithCount):
539            return inputPrefs
540
541        prefs = {}
542        for ranking, votes in inputPrefs.items():
543            newranking = [candidate for candidate in ranking if candidate != toEliminate.candidate]
544
545            if not newranking:
546                continue
547            newkey = tuple(newranking)
548            if newkey in prefs:
549                prefs[newkey] += votes
550            else:
551                prefs[newkey] = votes
552        return prefs

Gets a dictionary of the form {ranking as tuple, vote count} with toEliminate removed

def candidateVotes(self, prefSchedule):
554    def candidateVotes(self, prefSchedule):
555        """Gets a list of CandidateWithCount, from highest to lowest"""
556        candidates = {}
557        for ranking, votes in prefSchedule.items():
558            candidate = ranking[0]
559            if candidate in candidates:
560                candidates[candidate].votes += votes
561            else:
562                candidates[candidate] = CandidateWithCount(candidate, votes)
563
564        # Simply for VSE which requires ranking of non-winners; in real election we don't really
565        # care
566        alternates = []
567        trackedalt = set()
568        for ranking, votes in prefSchedule.items():
569            for alternate in ranking[1:]:
570                if (alternate not in candidates) and alternate not in trackedalt:
571                    alternates.append(CandidateWithCount(alternate, 0))
572                    trackedalt.add(alternate)
573
574        return (
575            sorted(candidates.values(), key=lambda c: (c.votes, c.candidate), reverse=True)
576            + alternates
577        )

Gets a list of CandidateWithCount, from highest to lowest

def getLeast(self, voteRanking, keep=None):
579    def getLeast(self, voteRanking, keep=None):
580        if keep is None:
581            keep = {}
582        for candidate in reversed(voteRanking):
583            if candidate.candidate not in keep:
584                return candidate
585        return None
@staticmethod
def scoresFromEliminations(eliminated, ncand):
587    @staticmethod
588    def scoresFromEliminations(eliminated, ncand):
589        """Convert an IRV elimination order to candidate-indexed scores."""
590        results = [-1] * ncand
591        for score, candidate in enumerate(eliminated):
592            results[candidate] = score
593        return results

Convert an IRV elimination order to candidate-indexed scores.

def runIrv(self, remaining, ncand):
595    def runIrv(self, remaining, ncand):
596        """IRV results."""
597        eliminated = []
598        for _ in range(ncand):
599            votes = self.candidateVotes(remaining)
600            toEliminate = self.getLeast(votes)
601            if not isinstance(toEliminate, CandidateWithCount):
602                break
603            eliminated.append(toEliminate.candidate)
604            remaining = self.eliminateCandidate(remaining, toEliminate)
605        return self.scoresFromEliminations(eliminated, ncand)

IRV results.

def results(self, ballots, **kwargs):
607    def results(self, ballots, **kwargs):
608        """IRV results.
609
610        >>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
611        [0, 1, 2]
612        >>> Irv().results([[0,1,2]])[2]
613        0
614        >>> Irv().results([[0,1,2],[2,1,0]])[1]
615        0
616        >>> Irv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
617        [1, 0, 2]
618        """
619        ballots = ballots_from_dataframe(ballots)
620        return self.runIrv(self.buildPreferenceSchedule(ballots), len(ballots[0]))

IRV results.

>>> Irv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
[0, 1, 2]
>>> Irv().results([[0,1,2]])[2]
0
>>> Irv().results([[0,1,2],[2,1,0]])[1]
0
>>> Irv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[1, 0, 2]
@staticmethod
@rememberBallot
def honBallot(cls, voter):
622    @staticmethod  # cls is provided explicitly, not through binding
623    @rememberBallot
624    def honBallot(cls, voter):
625        """Takes utilities and returns an honest ballot
626
627        >>> Irv.honBallot(Irv,Voter([4,1,6,3]))
628        [2, 0, 3, 1]
629        >>> Irv.honBallot(Irv,Voter([0,1,2]))
630        [2, 1, 0]
631        """
632        order = sorted(enumerate(voter), key=lambda x: (-x[1], x[0]))
633        return [candidate for candidate, _utility in order]

Takes utilities and returns an honest ballot

>>> Irv.honBallot(Irv,Voter([4,1,6,3]))
[2, 0, 3, 1]
>>> Irv.honBallot(Irv,Voter([0,1,2]))
[2, 1, 0]
@classmethod
def fillStratBallot( cls, voter, polls, places, n, stratGap, ballot, frontId, frontResult, targId, targResult):
635    @classmethod
636    def fillStratBallot(
637        cls,
638        voter,
639        polls,
640        places,
641        n,
642        stratGap,
643        ballot,
644        frontId,
645        frontResult,
646        targId,
647        targResult,
648    ):
649        """
650        >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
651        [2, 1, 0, 3]
652        """
653        i = n - 1
654        winnerQ = voter[frontId]
655        targQ = voter[targId]
656        placesToFill = list(range(n - 1, 0, -1))
657        if targQ > winnerQ:
658            ballot[targId] = i
659            i -= 1
660            del placesToFill[-2]
661        for j in placesToFill:
662            nextLoser, loserScore = places[j]  # all but winner, low to high
663            if voter[nextLoser] > winnerQ:
664                ballot[nextLoser] = i
665                i -= 1
666        ballot[frontId] = i
667        i -= 1
668        for j in placesToFill:
669            nextLoser, loserScore = places[j]
670            if voter[nextLoser] <= winnerQ:
671                ballot[nextLoser] = i
672                i -= 1
673        assert i == -1
674        ballot[:] = [
675            candidate for candidate, _rank in sorted(enumerate(ballot), key=lambda x: -x[1])
676        ]
>>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
[2, 1, 0, 3]
class IrvPrime(Irv):
679class IrvPrime(Irv):
680    """
681    IRV Prime.
682
683    See https://electowiki.org/wiki/IRV_Prime
684    """
685
686    stratTargetFor = Method.stratTarget3
687
688    def results(self, ballots, **kwargs):
689        """IRV Prime results.
690
691        >>> IrvPrime().results([[0,1,2]])[2]
692        0
693        >>> IrvPrime().results([[0,1,2],[2,1,0]])[1]
694        0
695        >>> IrvPrime().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
696        [0, 2, 1]
697        >>> IrvPrime().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
698        [1, 2, 0]
699        >>> # Favorite betrayal example from http://rangevoting.org/IncentToExagg.html
700        >>> IrvPrime().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
701        [2, 1, 0]
702        >>> IrvPrime().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
703        [1, 0, 3, 2, 4]
704        >>> # Elections 3-5 from http://votingmatters.org.uk/ISSUE6/P4.HTM
705        >>> IrvPrime().results([[0,1,2,3,4,5]] * 12 + [[2,0,1,3,4,5]] * 11 + [[1,2,0,3,4,5]] * 10 +
706        ...     [[3,4,5]] * 27)
707        [2, 5, 4, 3, 1, 0]
708        >>> IrvPrime().results([[0,1]] * 11 + [[1]] * 7 + [[2]] * 12)
709        [0, 2, 1]
710        >>> IrvPrime().results([[0,3,2,1]] * 5 + [[1,2,0,3]] * 5 + [[2,0,1,3]] * 8 +
711        ...    [[3,0,1,2]] * 4 + [[3,1,2,0]] * 8)
712        [3, 0, 1, 2]
713        >>> IrvPrime().results([[0,2,1,3]] * 6 + [[0,3,1,2]] * 3 + [[0,3,2,1]] * 3 +
714        ...     [[1,2,0,3]] * 4 + [[2,0,1,3]] * 4 + [[3,1,2,0]] * 5)
715        [2, 0, 3, 1]
716        >>> # Failure of later-no-harm
717        >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
718        ...     [[1,0,2]] * 21 + [[2,0,1]] * 30 + [[2,1,0]] * 20)
719        [1, 0, 2]
720        >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
721        ...     [[1,0,2]] * 21 + [[2,1,0]] * 30 + [[2,1,0]] * 20)
722        [1, 2, 0]
723        """
724
725        ballots = ballots_from_dataframe(ballots)
726
727        remaining = self.buildPreferenceSchedule(ballots)
728        ncand = len(self.candidateVotes(remaining))
729        classic = self.runIrv(remaining, ncand)
730
731        # Keep the winner from the classic IRV
732        winners = {self.winner(classic)}
733
734        # Find all candidates that can beat classic IRV winner; this may be a superset
735        # of schwartz/smith, but it's all that matters
736        winnersPrime = set()
737        for possibleWinner in range(ncand):
738            if possibleWinner in winners:
739                continue
740
741            numWins = 0
742            numLosses = 0
743            for ranking, votes in remaining.items():
744                possibleWinnerRanking = winnerRanking = len(ranking) + 1
745                for pos in range(len(ranking)):
746                    if ranking[pos] == possibleWinner:
747                        possibleWinnerRanking = pos
748                    # We can change this to a loop if there's > 1 winner
749                    elif ranking[pos] == next(iter(winners)):
750                        winnerRanking = pos
751                if possibleWinnerRanking < winnerRanking:
752                    numWins += votes
753                elif winnerRanking < possibleWinnerRanking:
754                    numLosses += votes
755            if numWins > numLosses:
756                winnersPrime.add(possibleWinner)
757
758        # Now re-run IRV preserving all winners + winners prime
759        keepers = winners.union(winnersPrime)
760        eliminated = []
761        for _ in range(ncand):
762            votes = self.candidateVotes(remaining)
763            toEliminate = self.getLeast(votes, keepers)
764            if not isinstance(toEliminate, CandidateWithCount):
765                # Begin "step 4", i.e. continue elimination without preserving anyone
766                keepers = {}
767                toEliminate = self.getLeast(votes)
768            if not isinstance(toEliminate, CandidateWithCount):
769                break
770            eliminated.append(toEliminate.candidate)
771            remaining = self.eliminateCandidate(remaining, toEliminate)
772
773        return self.scoresFromEliminations(eliminated, ncand)
def stratTargetFor(self, places):
311    def stratTarget3(self, places):
312        ((frontId, frontResult), (targId, targResult)) = places[:3:2]
313        return (frontId, frontResult, targId, targResult)
def results(self, ballots, **kwargs):
688    def results(self, ballots, **kwargs):
689        """IRV Prime results.
690
691        >>> IrvPrime().results([[0,1,2]])[2]
692        0
693        >>> IrvPrime().results([[0,1,2],[2,1,0]])[1]
694        0
695        >>> IrvPrime().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
696        [0, 2, 1]
697        >>> IrvPrime().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
698        [1, 2, 0]
699        >>> # Favorite betrayal example from http://rangevoting.org/IncentToExagg.html
700        >>> IrvPrime().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
701        [2, 1, 0]
702        >>> IrvPrime().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
703        [1, 0, 3, 2, 4]
704        >>> # Elections 3-5 from http://votingmatters.org.uk/ISSUE6/P4.HTM
705        >>> IrvPrime().results([[0,1,2,3,4,5]] * 12 + [[2,0,1,3,4,5]] * 11 + [[1,2,0,3,4,5]] * 10 +
706        ...     [[3,4,5]] * 27)
707        [2, 5, 4, 3, 1, 0]
708        >>> IrvPrime().results([[0,1]] * 11 + [[1]] * 7 + [[2]] * 12)
709        [0, 2, 1]
710        >>> IrvPrime().results([[0,3,2,1]] * 5 + [[1,2,0,3]] * 5 + [[2,0,1,3]] * 8 +
711        ...    [[3,0,1,2]] * 4 + [[3,1,2,0]] * 8)
712        [3, 0, 1, 2]
713        >>> IrvPrime().results([[0,2,1,3]] * 6 + [[0,3,1,2]] * 3 + [[0,3,2,1]] * 3 +
714        ...     [[1,2,0,3]] * 4 + [[2,0,1,3]] * 4 + [[3,1,2,0]] * 5)
715        [2, 0, 3, 1]
716        >>> # Failure of later-no-harm
717        >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
718        ...     [[1,0,2]] * 21 + [[2,0,1]] * 30 + [[2,1,0]] * 20)
719        [1, 0, 2]
720        >>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
721        ...     [[1,0,2]] * 21 + [[2,1,0]] * 30 + [[2,1,0]] * 20)
722        [1, 2, 0]
723        """
724
725        ballots = ballots_from_dataframe(ballots)
726
727        remaining = self.buildPreferenceSchedule(ballots)
728        ncand = len(self.candidateVotes(remaining))
729        classic = self.runIrv(remaining, ncand)
730
731        # Keep the winner from the classic IRV
732        winners = {self.winner(classic)}
733
734        # Find all candidates that can beat classic IRV winner; this may be a superset
735        # of schwartz/smith, but it's all that matters
736        winnersPrime = set()
737        for possibleWinner in range(ncand):
738            if possibleWinner in winners:
739                continue
740
741            numWins = 0
742            numLosses = 0
743            for ranking, votes in remaining.items():
744                possibleWinnerRanking = winnerRanking = len(ranking) + 1
745                for pos in range(len(ranking)):
746                    if ranking[pos] == possibleWinner:
747                        possibleWinnerRanking = pos
748                    # We can change this to a loop if there's > 1 winner
749                    elif ranking[pos] == next(iter(winners)):
750                        winnerRanking = pos
751                if possibleWinnerRanking < winnerRanking:
752                    numWins += votes
753                elif winnerRanking < possibleWinnerRanking:
754                    numLosses += votes
755            if numWins > numLosses:
756                winnersPrime.add(possibleWinner)
757
758        # Now re-run IRV preserving all winners + winners prime
759        keepers = winners.union(winnersPrime)
760        eliminated = []
761        for _ in range(ncand):
762            votes = self.candidateVotes(remaining)
763            toEliminate = self.getLeast(votes, keepers)
764            if not isinstance(toEliminate, CandidateWithCount):
765                # Begin "step 4", i.e. continue elimination without preserving anyone
766                keepers = {}
767                toEliminate = self.getLeast(votes)
768            if not isinstance(toEliminate, CandidateWithCount):
769                break
770            eliminated.append(toEliminate.candidate)
771            remaining = self.eliminateCandidate(remaining, toEliminate)
772
773        return self.scoresFromEliminations(eliminated, ncand)

IRV Prime results.

>>> IrvPrime().results([[0,1,2]])[2]
0
>>> IrvPrime().results([[0,1,2],[2,1,0]])[1]
0
>>> IrvPrime().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[0, 2, 1]
>>> IrvPrime().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
[1, 2, 0]
>>> # Favorite betrayal example from http://rangevoting.org/IncentToExagg.html
>>> IrvPrime().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
[2, 1, 0]
>>> IrvPrime().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
[1, 0, 3, 2, 4]
>>> # Elections 3-5 from http://votingmatters.org.uk/ISSUE6/P4.HTM
>>> IrvPrime().results([[0,1,2,3,4,5]] * 12 + [[2,0,1,3,4,5]] * 11 + [[1,2,0,3,4,5]] * 10 +
...     [[3,4,5]] * 27)
[2, 5, 4, 3, 1, 0]
>>> IrvPrime().results([[0,1]] * 11 + [[1]] * 7 + [[2]] * 12)
[0, 2, 1]
>>> IrvPrime().results([[0,3,2,1]] * 5 + [[1,2,0,3]] * 5 + [[2,0,1,3]] * 8 +
...    [[3,0,1,2]] * 4 + [[3,1,2,0]] * 8)
[3, 0, 1, 2]
>>> IrvPrime().results([[0,2,1,3]] * 6 + [[0,3,1,2]] * 3 + [[0,3,2,1]] * 3 +
...     [[1,2,0,3]] * 4 + [[2,0,1,3]] * 4 + [[3,1,2,0]] * 5)
[2, 0, 3, 1]
>>> # Failure of later-no-harm
>>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
...     [[1,0,2]] * 21 + [[2,0,1]] * 30 + [[2,1,0]] * 20)
[1, 0, 2]
>>> IrvPrime().results([[0, 1, 2]] * 32 + [[0, 2, 1]] * 20 + [[1,2,0]] * 30 +
...     [[1,0,2]] * 21 + [[2,1,0]] * 30 + [[2,1,0]] * 20)
[1, 2, 0]
class Mav(vse_sim.data_classes.Method):
322class Mav(Method):
323    """Majority Approval Voting"""
324
325    # >>> mqs = [Mav().resultsFor(PolyaModel()(101,5),Mav.honBallot)[0] for i in range(400)]
326    # >>> mean(mqs)
327    # 1.5360519801980208
328    # >>> mqs += [Mav().resultsFor(PolyaModel()(101,5),Mav.honBallot)[0] for i in range(1200)]
329    # >>> mean(mqs)
330    # 1.5343069306930679
331    # >>> std(mqs)
332    # 1.0970202515275356
333    bias5 = 1.0970202515275356
334
335    baseCuts = [-0.8, 0, 0.8, 1.6]
336    specificCuts = None
337    specificPercentiles = [25, 50, 75, 90]
338
339    def candScore(self, scores):
340        """For now, only works correctly for odd nvot
341
342        Basic tests
343            >>> Mav().candScore([1,2,3,4,5])
344            3.0
345            >>> Mav().candScore([1,2,3,3,3])
346            2.5
347            >>> Mav().candScore([1,2,3,4])
348            2.5
349            >>> Mav().candScore([1,2,3,3])
350            2.5
351            >>> Mav().candScore([1,2,2,2])
352            1.5
353            >>> Mav().candScore([1,2,3,3,5])
354            2.7
355        """
356        scores = sorted(scores)
357        nvot = len(scores)
358        nGrades = len(self.baseCuts) + 1
359        i = int((nvot - 1) / 2)
360        base = scores[i]
361        while i < nvot and scores[i] == base:
362            i += 1
363        upper = (base + 0.5) - (i - nvot / 2) * nGrades / nvot
364        lower = (base) - (i - nvot / 2) / nvot
365        return max(upper, lower)
366
367    @classmethod
368    def honBallotFor(cls, voters):
369        cls.specificCuts = percentile(voters, cls.specificPercentiles)
370        return cls.honBallot
371
372    @staticmethod  # cls is provided explicitly, not through binding
373    @rememberBallot
374    def honBallot(cls, voter):
375        """Takes utilities and returns an honest ballot (on 0..4)
376
377        honest ballot works as intended, gives highest grade to highest utility:
378            >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5,1,1.1]))
379            [0, 1, 2, 3, 4]
380
381        Even if they don't rate at least an honest "B":
382            >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5]))
383            [0, 1, 4]
384        """
385        cuts = cls.specificCuts if (cls.specificCuts is not None) else cls.baseCuts
386        cuts = [min(cut, max(voter) - 0.001) for cut in cuts]
387        return [toVote(cuts, util) for util in voter]
388
389    def stratBallotFor(self, polls):
390        """Returns a function which takes utilities and returns a dict(
391            strat=<ballot in which all grades are exaggerated
392                             to outside the range of the two honest frontrunners>,
393            extraStrat=<ballot in which all grades are exaggerated to extremes>,
394            isStrat=<whether the runner-up is preferred to the frontrunner (for reluctantStrat)>,
395            stratGap=<utility of runner-up minus that of frontrunner>
396            )
397        for the given "polling" info.
398
399
400
401        Strategic tests:
402            >>> Mav().stratBallotFor([0,1.1,1.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2]))
403            [0, 1, 2, 3, 4]
404            >>> Mav().stratBallotFor([0,2.1,2.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2]))
405            [0, 1, 3, 3, 4]
406            >>> Mav().stratBallotFor([0,2.1,1.9,0,0])(Mav, Voter([-1,0.4,0.5,1,2]))
407            [0, 1, 3, 3, 4]
408            >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,7,6]))
409            [4, 4, 4]
410            >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,5,6]))
411            [4, 0, 4]
412            >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6]))
413            [4, 0, 4]
414            >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6.1]))
415            [2, 2, 4]
416        """
417        places = sorted(enumerate(polls), key=lambda x: -x[1])  # from high to low
418        # print("places",places)
419        ((frontId, frontResult), (targId, targResult)) = places[:2]
420
421        @rememberBallots
422        def stratBallot(cls, voter):
423            frontUtils = [voter[frontId], voter[targId]]  # utils of frontrunners
424            stratGap = frontUtils[1] - frontUtils[0]
425            if stratGap == 0:
426                strat = extraStrat = [(4 if (util >= frontUtils[0]) else 0) for util in voter]
427                isStrat = True
428
429            else:
430                if stratGap < 0:
431                    # winner is preferred; be complacent.
432                    isStrat = False
433                else:
434                    # runner-up is preferred; be strategic in iss run
435                    isStrat = True
436                    # sort cuts high to low
437                    frontUtils = (frontUtils[1], frontUtils[0])
438                top = max(voter)
439                # print("lll312")
440                # print(self.baseCuts, front)
441                cutoffs = [
442                    (
443                        (min(frontUtils[0], self.baseCuts[i]))
444                        if (i < floor(targResult))
445                        else (
446                            (frontUtils[1])
447                            if (i < floor(frontResult) + 1)
448                            else min(top, self.baseCuts[i])
449                        )
450                    )
451                    for i in range(len(self.baseCuts))
452                ]
453                strat = [toVote(cutoffs, util) for util in voter]
454                extraStrat = [
455                    max(
456                        0,
457                        min(
458                            10,
459                            floor(4.99 * (util - frontUtils[1]) / (frontUtils[0] - frontUtils[1])),
460                        ),
461                    )
462                    for util in voter
463                ]
464            return dict(strat=strat, extraStrat=extraStrat, isStrat=isStrat, stratGap=stratGap)
465
466        return stratBallot

Majority Approval Voting

bias5 = 1.0970202515275356
baseCuts = [-0.8, 0, 0.8, 1.6]
specificCuts = None
specificPercentiles = [25, 50, 75, 90]
def candScore(self, scores):
339    def candScore(self, scores):
340        """For now, only works correctly for odd nvot
341
342        Basic tests
343            >>> Mav().candScore([1,2,3,4,5])
344            3.0
345            >>> Mav().candScore([1,2,3,3,3])
346            2.5
347            >>> Mav().candScore([1,2,3,4])
348            2.5
349            >>> Mav().candScore([1,2,3,3])
350            2.5
351            >>> Mav().candScore([1,2,2,2])
352            1.5
353            >>> Mav().candScore([1,2,3,3,5])
354            2.7
355        """
356        scores = sorted(scores)
357        nvot = len(scores)
358        nGrades = len(self.baseCuts) + 1
359        i = int((nvot - 1) / 2)
360        base = scores[i]
361        while i < nvot and scores[i] == base:
362            i += 1
363        upper = (base + 0.5) - (i - nvot / 2) * nGrades / nvot
364        lower = (base) - (i - nvot / 2) / nvot
365        return max(upper, lower)

For now, only works correctly for odd nvot

Basic tests

Mav().candScore([1,2,3,4,5]) 3.0 Mav().candScore([1,2,3,3,3]) 2.5 Mav().candScore([1,2,3,4]) 2.5 Mav().candScore([1,2,3,3]) 2.5 Mav().candScore([1,2,2,2]) 1.5 Mav().candScore([1,2,3,3,5]) 2.7

@classmethod
def honBallotFor(cls, voters):
367    @classmethod
368    def honBallotFor(cls, voters):
369        cls.specificCuts = percentile(voters, cls.specificPercentiles)
370        return cls.honBallot

This is where you would do any setup necessary and create an honBallot function. But the base version just returns the honBallot function.

@staticmethod
@rememberBallot
def honBallot(cls, voter):
372    @staticmethod  # cls is provided explicitly, not through binding
373    @rememberBallot
374    def honBallot(cls, voter):
375        """Takes utilities and returns an honest ballot (on 0..4)
376
377        honest ballot works as intended, gives highest grade to highest utility:
378            >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5,1,1.1]))
379            [0, 1, 2, 3, 4]
380
381        Even if they don't rate at least an honest "B":
382            >>> Mav().honBallot(Mav, Voter([-1,-0.5,0.5]))
383            [0, 1, 4]
384        """
385        cuts = cls.specificCuts if (cls.specificCuts is not None) else cls.baseCuts
386        cuts = [min(cut, max(voter) - 0.001) for cut in cuts]
387        return [toVote(cuts, util) for util in voter]

Takes utilities and returns an honest ballot (on 0..4)

honest ballot works as intended, gives highest grade to highest utility:

Mav().honBallot(Mav, Voter([-1,-0.5,0.5,1,1.1])) [0, 1, 2, 3, 4]

Even if they don't rate at least an honest "B":

Mav().honBallot(Mav, Voter([-1,-0.5,0.5])) [0, 1, 4]

def stratBallotFor(self, polls):
389    def stratBallotFor(self, polls):
390        """Returns a function which takes utilities and returns a dict(
391            strat=<ballot in which all grades are exaggerated
392                             to outside the range of the two honest frontrunners>,
393            extraStrat=<ballot in which all grades are exaggerated to extremes>,
394            isStrat=<whether the runner-up is preferred to the frontrunner (for reluctantStrat)>,
395            stratGap=<utility of runner-up minus that of frontrunner>
396            )
397        for the given "polling" info.
398
399
400
401        Strategic tests:
402            >>> Mav().stratBallotFor([0,1.1,1.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2]))
403            [0, 1, 2, 3, 4]
404            >>> Mav().stratBallotFor([0,2.1,2.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2]))
405            [0, 1, 3, 3, 4]
406            >>> Mav().stratBallotFor([0,2.1,1.9,0,0])(Mav, Voter([-1,0.4,0.5,1,2]))
407            [0, 1, 3, 3, 4]
408            >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,7,6]))
409            [4, 4, 4]
410            >>> Mav().stratBallotFor([1,0,2])(Mav, Voter([6,5,6]))
411            [4, 0, 4]
412            >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6]))
413            [4, 0, 4]
414            >>> Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6.1]))
415            [2, 2, 4]
416        """
417        places = sorted(enumerate(polls), key=lambda x: -x[1])  # from high to low
418        # print("places",places)
419        ((frontId, frontResult), (targId, targResult)) = places[:2]
420
421        @rememberBallots
422        def stratBallot(cls, voter):
423            frontUtils = [voter[frontId], voter[targId]]  # utils of frontrunners
424            stratGap = frontUtils[1] - frontUtils[0]
425            if stratGap == 0:
426                strat = extraStrat = [(4 if (util >= frontUtils[0]) else 0) for util in voter]
427                isStrat = True
428
429            else:
430                if stratGap < 0:
431                    # winner is preferred; be complacent.
432                    isStrat = False
433                else:
434                    # runner-up is preferred; be strategic in iss run
435                    isStrat = True
436                    # sort cuts high to low
437                    frontUtils = (frontUtils[1], frontUtils[0])
438                top = max(voter)
439                # print("lll312")
440                # print(self.baseCuts, front)
441                cutoffs = [
442                    (
443                        (min(frontUtils[0], self.baseCuts[i]))
444                        if (i < floor(targResult))
445                        else (
446                            (frontUtils[1])
447                            if (i < floor(frontResult) + 1)
448                            else min(top, self.baseCuts[i])
449                        )
450                    )
451                    for i in range(len(self.baseCuts))
452                ]
453                strat = [toVote(cutoffs, util) for util in voter]
454                extraStrat = [
455                    max(
456                        0,
457                        min(
458                            10,
459                            floor(4.99 * (util - frontUtils[1]) / (frontUtils[0] - frontUtils[1])),
460                        ),
461                    )
462                    for util in voter
463                ]
464            return dict(strat=strat, extraStrat=extraStrat, isStrat=isStrat, stratGap=stratGap)
465
466        return stratBallot

Returns a function which takes utilities and returns a dict( strat=, extraStrat=, isStrat= ) for the given "polling" info.

Strategic tests:

Mav().stratBallotFor([0,1.1,1.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) [0, 1, 2, 3, 4] Mav().stratBallotFor([0,2.1,2.9,0,0])(Mav, Voter([-1,-0.5,0.5,1,2])) [0, 1, 3, 3, 4] Mav().stratBallotFor([0,2.1,1.9,0,0])(Mav, Voter([-1,0.4,0.5,1,2])) [0, 1, 3, 3, 4] Mav().stratBallotFor([1,0,2])(Mav, Voter([6,7,6])) [4, 4, 4] Mav().stratBallotFor([1,0,2])(Mav, Voter([6,5,6])) [4, 0, 4] Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6])) [4, 0, 4] Mav().stratBallotFor([2.1,0,3])(Mav, Voter([6,5,6.1])) [2, 2, 4]

class Mj(Mav):
469class Mj(Mav):
470    def candScore(self, scores):
471        """This formula will always give numbers within 0.5 of the raw median.
472        Unfortunately, with 5 grade levels, these will tend to be within 0.1 of
473        the raw median, leaving scores further from the integers mostly unused.
474        This is only a problem aesthetically.
475
476        For now, only works correctly for odd nvot
477
478        tests:
479            >>> Mj().candScore([1,2,3,4,5])
480            3
481            >>> Mj().candScore([1,2,3,3,5])
482            2.7
483            >>> Mj().candScore([1,3,3,3,5])
484            3
485            >>> Mj().candScore([1,3,3,4,5])
486            3.3
487            >>> Mj().candScore([1,3,3,3,3])
488            2.9
489            >>> Mj().candScore([3] * 24 + [1])
490            2.98
491            >>> Mj().candScore([3] * 24 + [4])
492            3.02
493            >>> Mj().candScore([3] * 13 + [4] * 12)
494            3.46
495        """
496        scores = sorted(scores)
497        nvot = len(scores)
498        lo = hi = mid = nvot // 2
499        base = scores[mid]
500        while hi < nvot and scores[hi] == base:
501            hi += 1
502        while lo >= 0 and scores[lo] == base:
503            lo -= 1
504
505        if (hi - mid) == (mid - lo):
506            return base
507        elif (hi - mid) < (mid - lo):
508            return base + 0.5 - (hi - mid) / nvot
509        else:
510            return base - 0.5 + (mid - lo) / nvot

Majority Approval Voting

def candScore(self, scores):
470    def candScore(self, scores):
471        """This formula will always give numbers within 0.5 of the raw median.
472        Unfortunately, with 5 grade levels, these will tend to be within 0.1 of
473        the raw median, leaving scores further from the integers mostly unused.
474        This is only a problem aesthetically.
475
476        For now, only works correctly for odd nvot
477
478        tests:
479            >>> Mj().candScore([1,2,3,4,5])
480            3
481            >>> Mj().candScore([1,2,3,3,5])
482            2.7
483            >>> Mj().candScore([1,3,3,3,5])
484            3
485            >>> Mj().candScore([1,3,3,4,5])
486            3.3
487            >>> Mj().candScore([1,3,3,3,3])
488            2.9
489            >>> Mj().candScore([3] * 24 + [1])
490            2.98
491            >>> Mj().candScore([3] * 24 + [4])
492            3.02
493            >>> Mj().candScore([3] * 13 + [4] * 12)
494            3.46
495        """
496        scores = sorted(scores)
497        nvot = len(scores)
498        lo = hi = mid = nvot // 2
499        base = scores[mid]
500        while hi < nvot and scores[hi] == base:
501            hi += 1
502        while lo >= 0 and scores[lo] == base:
503            lo -= 1
504
505        if (hi - mid) == (mid - lo):
506            return base
507        elif (hi - mid) < (mid - lo):
508            return base + 0.5 - (hi - mid) / nvot
509        else:
510            return base - 0.5 + (mid - lo) / nvot

This formula will always give numbers within 0.5 of the raw median. Unfortunately, with 5 grade levels, these will tend to be within 0.1 of the raw median, leaving scores further from the integers mostly unused. This is only a problem aesthetically.

For now, only works correctly for odd nvot

tests:

Mj().candScore([1,2,3,4,5]) 3 Mj().candScore([1,2,3,3,5]) 2.7 Mj().candScore([1,3,3,3,5]) 3 Mj().candScore([1,3,3,4,5]) 3.3 Mj().candScore([1,3,3,3,3]) 2.9 Mj().candScore([3] * 24 + [1]) 2.98 Mj().candScore([3] * 24 + [4]) 3.02 Mj().candScore([3] * 13 + [4] * 12) 3.46

class Plurality(Borda):
 97class Plurality(RankedMethod):
 98    nRanks = 2
 99
100    @staticmethod
101    def oneVote(utils, forWhom):
102        ballot = [0] * len(utils)
103        ballot[forWhom] = 1
104        return ballot
105
106    @staticmethod  # cls is provided explicitly, not through binding
107    @rememberBallot
108    def honBallot(cls, utils):
109        """Takes utilities and returns an honest ballot
110
111        >>> Plurality.honBallot(Plurality, Voter([-3,-2,-1]))
112        [0, 0, 1]
113        >>> Plurality().stratBallotFor([3,2,1])(Plurality, Voter([-3,-2,-1]))
114        [0, 1, 0]
115        """
116        # return cls.oneVote(utils, cls.winner(utils))
117        ballot = [0] * len(utils)
118        cls.fillPrefOrder(utils, ballot, nSlots=1, lowSlot=1, remainderScore=0)
119        return ballot
120
121    #
122    # @classmethod
123    # def xxstratBallot(cls, voter, polls, places, n,
124    #                     frontId, frontResult, targId, targResult):
125    #     """Takes utilities and returns a strategic ballot
126    #     for the given "polling" info.
127    #
128    #     >>> Plurality().stratBallotFor([4,2,1])(Plurality, Voter([-4,-2,-1]))
129    #     [0, 1, 0]
130    #     """
131    #     stratGap = voter[targId] - voter[frontId]
132    #     if stratGap <= 0:
133    #         #winner is preferred; be complacent.
134    #         isStrat = False
135    #         strat = cls.oneVote(voter, frontId)
136    #     else:
137    #         #runner-up is preferred; be strategic in iss run
138    #         isStrat = True
139    #         #sort cuts high to low
140    #         #cuts = (cuts[1], cuts[0])
141    #         strat = cls.oneVote(voter, targId)
142    #     return dict(strat=strat, isStrat=isStrat, stratGap=stratGap)

Base class for election methods. Holds some of the duct tape.

nRanks = 2
@staticmethod
def oneVote(utils, forWhom):
100    @staticmethod
101    def oneVote(utils, forWhom):
102        ballot = [0] * len(utils)
103        ballot[forWhom] = 1
104        return ballot
@staticmethod
@rememberBallot
def honBallot(cls, utils):
106    @staticmethod  # cls is provided explicitly, not through binding
107    @rememberBallot
108    def honBallot(cls, utils):
109        """Takes utilities and returns an honest ballot
110
111        >>> Plurality.honBallot(Plurality, Voter([-3,-2,-1]))
112        [0, 0, 1]
113        >>> Plurality().stratBallotFor([3,2,1])(Plurality, Voter([-3,-2,-1]))
114        [0, 1, 0]
115        """
116        # return cls.oneVote(utils, cls.winner(utils))
117        ballot = [0] * len(utils)
118        cls.fillPrefOrder(utils, ballot, nSlots=1, lowSlot=1, remainderScore=0)
119        return ballot

Takes utilities and returns an honest ballot

>>> Plurality.honBallot(Plurality, Voter([-3,-2,-1]))
[0, 0, 1]
>>> Plurality().stratBallotFor([3,2,1])(Plurality, Voter([-3,-2,-1]))
[0, 1, 0]
RankedMethod = <class 'Borda'>
RatedMethod = <class 'Borda'>
class Rp(Schulze):
1079class Rp(Schulze):
1080    def resolveCycle(self, cmat, n):
1081        """Note: mutates cmat destructively.
1082
1083        >>> Rp().resultsFor(DeterministicModel(3)(5,3),Rp().honBallot,isHonest=True)["results"]
1084        [1, 2, 0]
1085        """
1086        matches = [(i, j, cmat[i][j]) for i in range(n) for j in range(i, n) if i != j]
1087        rps = sorted(matches, key=lambda x: -abs(x[2]))
1088        for i, j, margin in rps:
1089            if margin < 0:
1090                i, j = j, i
1091            if cmat[j][i] is not True:
1092                # print(i,j,cmat)
1093                cmat[i][j] = True
1094                # print("....",i,j,cmat)
1095                for k in range(n):
1096                    if k not in (i, j):
1097                        if cmat[j][k] is True:
1098                            cmat[i][k] = True
1099                        if cmat[k][i] is True:
1100                            cmat[k][j] = True
1101
1102                            # print(".......",i,j,k,cmat)
1103
1104        return [sum(cmat[i][j] is True for j in range(n)) for i in range(n)]

Base class for election methods. Holds some of the duct tape.

def resolveCycle(self, cmat, n):
1080    def resolveCycle(self, cmat, n):
1081        """Note: mutates cmat destructively.
1082
1083        >>> Rp().resultsFor(DeterministicModel(3)(5,3),Rp().honBallot,isHonest=True)["results"]
1084        [1, 2, 0]
1085        """
1086        matches = [(i, j, cmat[i][j]) for i in range(n) for j in range(i, n) if i != j]
1087        rps = sorted(matches, key=lambda x: -abs(x[2]))
1088        for i, j, margin in rps:
1089            if margin < 0:
1090                i, j = j, i
1091            if cmat[j][i] is not True:
1092                # print(i,j,cmat)
1093                cmat[i][j] = True
1094                # print("....",i,j,cmat)
1095                for k in range(n):
1096                    if k not in (i, j):
1097                        if cmat[j][k] is True:
1098                            cmat[i][k] = True
1099                        if cmat[k][i] is True:
1100                            cmat[k][j] = True
1101
1102                            # print(".......",i,j,k,cmat)
1103
1104        return [sum(cmat[i][j] is True for j in range(n)) for i in range(n)]

Note: mutates cmat destructively.

>>> Rp().resultsFor(DeterministicModel(3)(5,3),Rp().honBallot,isHonest=True)["results"]
[1, 2, 0]
class Schulze(Borda):
 932class Schulze(RankedMethod):
 933    def resolveCycle(self, cmat, n):
 934
 935        beatStrength = [[0] * n] * n
 936        numWins = [0] * n
 937        for i in range(n):
 938            for j in range(n):
 939                if i != j:
 940                    beatStrength[i][j] = cmat[i][j] if cmat[i][j] > cmat[j][i] else 0
 941
 942                for pivot in range(n):
 943                    for source in range(n):
 944                        if pivot != source:
 945                            for target in range(n):
 946                                if pivot != target and source != target:
 947                                    beatStrength[source][target] = max(
 948                                        beatStrength[source][target],
 949                                        min(
 950                                            beatStrength[source][pivot],
 951                                            beatStrength[pivot][target],
 952                                        ),
 953                                    )
 954
 955        for i in range(n):
 956            for j in range(n):
 957                if i != j:
 958                    if beatStrength[i][j] > beatStrength[j][i]:
 959                        numWins[i] += 1
 960                    if (
 961                        beatStrength[i][j] == beatStrength[j][i] and i < j
 962                    ):  # break ties deterministically
 963                        numWins[i] += 1
 964
 965        return numWins
 966
 967    def results(self, ballots, isHonest=False, **kwargs):
 968        """Schulze results.
 969
 970        >>> Schulze().resultsFor(DeterministicModel(3)(5,3),Schulze().honBallot,isHonest=True)["results"]
 971        [2, 0, 1]
 972        >>> Schulze.extraEvents
 973        {'scenario': 'cycle'}
 974        >>> Schulze().results([[0,1,2]],isHonest=True)[2]
 975        2
 976        >>> Schulze.extraEvents
 977        {'scenario': 'easy'}
 978        >>> Schulze().results([[0,1,2],[2,1,0]],isHonest=True)[1]
 979        1
 980        >>> Schulze.extraEvents
 981        {'scenario': 'easy'}
 982        >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2,isHonest=True)
 983        [1, 2, 0]
 984        >>> Schulze.extraEvents
 985        {'scenario': 'chicken'}
 986        >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 2 + [[1,2,0]] * 3,isHonest=True)
 987        [1, 2, 0]
 988        >>> Schulze.extraEvents
 989        {'scenario': 'squeeze'}
 990        >>> Schulze().results([[3,2,1,0]] * 5 + [[2,3,1,0]] * 2 + [[0,1,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
 991        [2, 3, 1, 0]
 992        >>> Schulze.extraEvents
 993        {'scenario': 'other'}
 994        >>> Schulze().results([[3,0,0,0]] * 5 + [[2,3,0,0]] * 2 + [[0,0,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
 995        [3, 0, 1, 2]
 996        >>> Schulze.extraEvents
 997        {'scenario': 'spoiler'}
 998        """
 999        ballots = ballots_from_dataframe(ballots)
1000        n = len(ballots[0])
1001        cmat = [[0 for _ in range(n)] for _ in range(n)]
1002        numWins = [0] * n
1003        for i in range(n):
1004            for j in range(n):
1005                if i != j:
1006                    cmat[i][j] = sum(sign(ballot[i] - ballot[j]) for ballot in ballots)
1007                    if cmat[i][j] > 0:
1008                        numWins[i] += 1
1009                    elif cmat[i][j] == 0 and i < j:
1010                        numWins[i] += 1
1011        condOrder = sorted(enumerate(numWins), key=lambda x: -x[1])
1012        if condOrder[0][1] == n - 1:
1013            cycle = 0
1014            result = numWins
1015        else:  # cycle
1016            cycle = 1
1017            result = self.resolveCycle(cmat, n)
1018
1019        if isHonest:
1020            self.__class__.extraEvents = {}
1021            # check scenarios
1022            plurTally = [0] * n
1023            plur3Tally = [0] * 3
1024            cond3 = [c for c, v in condOrder[:3]]
1025            for b in ballots:
1026                b3 = [b[c] for c in cond3]
1027                plurTally[b.index(max(b))] += 1
1028                plur3Tally[b3.index(max(b3))] += 1
1029            plurOrder = sorted(enumerate(plurTally), key=lambda x: -x[1])
1030            plur3Order = sorted(enumerate(plur3Tally), key=lambda x: -x[1])
1031            if cycle:
1032                self.__class__.extraEvents["scenario"] = "cycle"
1033            elif plurOrder[0][0] == condOrder[0][0]:
1034                self.__class__.extraEvents["scenario"] = "easy"
1035            elif plur3Order[0][0] == condOrder[0][0]:
1036                self.__class__.extraEvents["scenario"] = "spoiler"
1037            elif plur3Order[2][0] == condOrder[0][0]:
1038                self.__class__.extraEvents["scenario"] = "squeeze"
1039            elif plur3Order[0][0] == condOrder[2][0]:
1040                self.__class__.extraEvents["scenario"] = "chicken"
1041            else:
1042                self.__class__.extraEvents["scenario"] = "other"
1043
1044        return result
1045
1046    @classmethod
1047    def fillStratBallot(
1048        cls,
1049        voter,
1050        polls,
1051        places,
1052        n,
1053        stratGap,
1054        ballot,
1055        frontId,
1056        frontResult,
1057        targId,
1058        targResult,
1059    ):
1060
1061        if stratGap > 0:
1062            others = [c for (c, r) in places[2:]]
1063            notTooBad = min(voter[frontId], voter[targId])
1064            decentOnes = [c for c in others if voter[c] >= notTooBad]
1065            cls.fillPrefOrder(voter, ballot, whichCands=decentOnes, lowSlot=n - len(decentOnes))
1066            # ballot[frontId], ballot[targId] = n-len(decentOnes)-1, n-len(decentOnes)-2
1067            ballot[frontId], ballot[targId] = 0, n - len(decentOnes) - 1
1068            cls.fillPrefOrder(
1069                voter,
1070                ballot,
1071                whichCands=[c for c in others if voter[c] < notTooBad],
1072                lowSlot=1,
1073            )
1074        else:
1075            ballot[frontId] = n - 1
1076            cls.fillPrefOrder(voter, ballot, whichCands=[c for (c, r) in places[1:]], lowSlot=0)

Base class for election methods. Holds some of the duct tape.

def resolveCycle(self, cmat, n):
933    def resolveCycle(self, cmat, n):
934
935        beatStrength = [[0] * n] * n
936        numWins = [0] * n
937        for i in range(n):
938            for j in range(n):
939                if i != j:
940                    beatStrength[i][j] = cmat[i][j] if cmat[i][j] > cmat[j][i] else 0
941
942                for pivot in range(n):
943                    for source in range(n):
944                        if pivot != source:
945                            for target in range(n):
946                                if pivot != target and source != target:
947                                    beatStrength[source][target] = max(
948                                        beatStrength[source][target],
949                                        min(
950                                            beatStrength[source][pivot],
951                                            beatStrength[pivot][target],
952                                        ),
953                                    )
954
955        for i in range(n):
956            for j in range(n):
957                if i != j:
958                    if beatStrength[i][j] > beatStrength[j][i]:
959                        numWins[i] += 1
960                    if (
961                        beatStrength[i][j] == beatStrength[j][i] and i < j
962                    ):  # break ties deterministically
963                        numWins[i] += 1
964
965        return numWins
def results(self, ballots, isHonest=False, **kwargs):
 967    def results(self, ballots, isHonest=False, **kwargs):
 968        """Schulze results.
 969
 970        >>> Schulze().resultsFor(DeterministicModel(3)(5,3),Schulze().honBallot,isHonest=True)["results"]
 971        [2, 0, 1]
 972        >>> Schulze.extraEvents
 973        {'scenario': 'cycle'}
 974        >>> Schulze().results([[0,1,2]],isHonest=True)[2]
 975        2
 976        >>> Schulze.extraEvents
 977        {'scenario': 'easy'}
 978        >>> Schulze().results([[0,1,2],[2,1,0]],isHonest=True)[1]
 979        1
 980        >>> Schulze.extraEvents
 981        {'scenario': 'easy'}
 982        >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2,isHonest=True)
 983        [1, 2, 0]
 984        >>> Schulze.extraEvents
 985        {'scenario': 'chicken'}
 986        >>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 2 + [[1,2,0]] * 3,isHonest=True)
 987        [1, 2, 0]
 988        >>> Schulze.extraEvents
 989        {'scenario': 'squeeze'}
 990        >>> Schulze().results([[3,2,1,0]] * 5 + [[2,3,1,0]] * 2 + [[0,1,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
 991        [2, 3, 1, 0]
 992        >>> Schulze.extraEvents
 993        {'scenario': 'other'}
 994        >>> Schulze().results([[3,0,0,0]] * 5 + [[2,3,0,0]] * 2 + [[0,0,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
 995        [3, 0, 1, 2]
 996        >>> Schulze.extraEvents
 997        {'scenario': 'spoiler'}
 998        """
 999        ballots = ballots_from_dataframe(ballots)
1000        n = len(ballots[0])
1001        cmat = [[0 for _ in range(n)] for _ in range(n)]
1002        numWins = [0] * n
1003        for i in range(n):
1004            for j in range(n):
1005                if i != j:
1006                    cmat[i][j] = sum(sign(ballot[i] - ballot[j]) for ballot in ballots)
1007                    if cmat[i][j] > 0:
1008                        numWins[i] += 1
1009                    elif cmat[i][j] == 0 and i < j:
1010                        numWins[i] += 1
1011        condOrder = sorted(enumerate(numWins), key=lambda x: -x[1])
1012        if condOrder[0][1] == n - 1:
1013            cycle = 0
1014            result = numWins
1015        else:  # cycle
1016            cycle = 1
1017            result = self.resolveCycle(cmat, n)
1018
1019        if isHonest:
1020            self.__class__.extraEvents = {}
1021            # check scenarios
1022            plurTally = [0] * n
1023            plur3Tally = [0] * 3
1024            cond3 = [c for c, v in condOrder[:3]]
1025            for b in ballots:
1026                b3 = [b[c] for c in cond3]
1027                plurTally[b.index(max(b))] += 1
1028                plur3Tally[b3.index(max(b3))] += 1
1029            plurOrder = sorted(enumerate(plurTally), key=lambda x: -x[1])
1030            plur3Order = sorted(enumerate(plur3Tally), key=lambda x: -x[1])
1031            if cycle:
1032                self.__class__.extraEvents["scenario"] = "cycle"
1033            elif plurOrder[0][0] == condOrder[0][0]:
1034                self.__class__.extraEvents["scenario"] = "easy"
1035            elif plur3Order[0][0] == condOrder[0][0]:
1036                self.__class__.extraEvents["scenario"] = "spoiler"
1037            elif plur3Order[2][0] == condOrder[0][0]:
1038                self.__class__.extraEvents["scenario"] = "squeeze"
1039            elif plur3Order[0][0] == condOrder[2][0]:
1040                self.__class__.extraEvents["scenario"] = "chicken"
1041            else:
1042                self.__class__.extraEvents["scenario"] = "other"
1043
1044        return result

Schulze results.

>>> Schulze().resultsFor(DeterministicModel(3)(5,3),Schulze().honBallot,isHonest=True)["results"]
[2, 0, 1]
>>> Schulze.extraEvents
{'scenario': 'cycle'}
>>> Schulze().results([[0,1,2]],isHonest=True)[2]
2
>>> Schulze.extraEvents
{'scenario': 'easy'}
>>> Schulze().results([[0,1,2],[2,1,0]],isHonest=True)[1]
1
>>> Schulze.extraEvents
{'scenario': 'easy'}
>>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2,isHonest=True)
[1, 2, 0]
>>> Schulze.extraEvents
{'scenario': 'chicken'}
>>> Schulze().results([[0,1,2]] * 4 + [[2,1,0]] * 2 + [[1,2,0]] * 3,isHonest=True)
[1, 2, 0]
>>> Schulze.extraEvents
{'scenario': 'squeeze'}
>>> Schulze().results([[3,2,1,0]] * 5 + [[2,3,1,0]] * 2 + [[0,1,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
[2, 3, 1, 0]
>>> Schulze.extraEvents
{'scenario': 'other'}
>>> Schulze().results([[3,0,0,0]] * 5 + [[2,3,0,0]] * 2 + [[0,0,0,3]] * 6 + [[0,0,3,0]] * 3,isHonest=True)
[3, 0, 1, 2]
>>> Schulze.extraEvents
{'scenario': 'spoiler'}
@classmethod
def fillStratBallot( cls, voter, polls, places, n, stratGap, ballot, frontId, frontResult, targId, targResult):
1046    @classmethod
1047    def fillStratBallot(
1048        cls,
1049        voter,
1050        polls,
1051        places,
1052        n,
1053        stratGap,
1054        ballot,
1055        frontId,
1056        frontResult,
1057        targId,
1058        targResult,
1059    ):
1060
1061        if stratGap > 0:
1062            others = [c for (c, r) in places[2:]]
1063            notTooBad = min(voter[frontId], voter[targId])
1064            decentOnes = [c for c in others if voter[c] >= notTooBad]
1065            cls.fillPrefOrder(voter, ballot, whichCands=decentOnes, lowSlot=n - len(decentOnes))
1066            # ballot[frontId], ballot[targId] = n-len(decentOnes)-1, n-len(decentOnes)-2
1067            ballot[frontId], ballot[targId] = 0, n - len(decentOnes) - 1
1068            cls.fillPrefOrder(
1069                voter,
1070                ballot,
1071                whichCands=[c for c in others if voter[c] < notTooBad],
1072                lowSlot=1,
1073            )
1074        else:
1075            ballot[frontId] = n - 1
1076            cls.fillPrefOrder(voter, ballot, whichCands=[c for (c, r) in places[1:]], lowSlot=0)

Mutates the ballot argument to be a strategic ballot.

>>> Borda().stratBallotFor([4,5,2,1])(Borda, Voter([-4,-5,-2,-1]))
[3, 0, 1, 2]
def Score(topRank=10, asClass=False):
145def Score(topRank=10, asClass=False):
146
147    class Score0to(Method):
148        """Score voting, 0-10.
149
150
151        Strategy establishes pivots
152            >>> Score().stratBallotFor([0,1,2])(Score, Voter([5,6,7]))
153            [0, 0, 10]
154            >>> Score().stratBallotFor([2,1,0])(Score, Voter([5,6,7]))
155            [0, 10, 10]
156            >>> Score().stratBallotFor([1,0,2])(Score, Voter([5,6,7]))
157            [0, 5.0, 10]
158
159        Strategy (kinda) works for ties
160            >>> Score().stratBallotFor([1,0,2])(Score, Voter([5,6,6]))
161            [0, 10, 10]
162            >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,6,7]))
163            [0, 0, 10]
164            >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,7,6]))
165            [10, 10, 10]
166            >>> Score().stratBallotFor([1,0,2])(Score, Voter([6,5,6]))
167            [10, 0, 10]
168
169        """
170
171        # >>> qs += [Score().resultsFor(PolyaModel()(101,2),Score.honBallot)[0] for i in range(800)]
172        # >>> std(qs)
173        # 2.770135393419682
174        # >>> mean(qs)
175        # 5.1467202970297032
176        bias2 = 2.770135393419682
177        # >>> qs5 = [Score().resultsFor(PolyaModel()(101,5),Score.honBallot)[0] for i in range(400)]
178        # >>> mean(qs5)
179        # 4.920247524752476
180        # >>> std(qs5)
181        # 2.3536762480634343
182        bias5 = 2.3536762480634343
183        candScore = staticmethod(mean)
184        # """Takes the list of votes for a candidate; returns the candidate's score."""
185
186        def __str__(self):
187            if self.topRank == 1:
188                return "IdealApproval"
189            return self.__class__.__name__ + str(self.topRank)
190
191        @staticmethod  # cls is provided explicitly, not through binding
192        @rememberBallot
193        def honBallot(cls, utils):
194            """Takes utilities and returns an honest ballot (on 0..10)
195
196
197            honest ballots work as expected
198                >>> Score().honBallot(Score, Voter([5,6,7]))
199                [0.0, 5.0, 10.0]
200                >>> Score().resultsFor(DeterministicModel(3)(5,3),Score().honBallot)["results"]
201                [4.0, 6.0, 5.0]
202            """
203            bot = min(utils)
204            scale = max(utils) - bot
205            return [floor((cls.topRank + 0.99) * (util - bot) / scale) for util in utils]
206
207        @classmethod
208        def fillStratBallot(
209            cls,
210            voter,
211            polls,
212            places,
213            n,
214            stratGap,
215            ballot,
216            frontId,
217            frontResult,
218            targId,
219            targResult,
220        ):
221            """Returns a (function which takes utilities and returns a strategic ballot)
222            for the given "polling" info."""
223
224            cuts = [voter[frontId], voter[targId]]
225            if stratGap > 0:
226                # sort cuts high to low
227                cuts = (cuts[1], cuts[0])
228            if cuts[0] == cuts[1]:
229                strat = [(cls.topRank if (util >= cuts[0]) else 0) for util in voter]
230            else:
231                strat = [
232                    max(
233                        0,
234                        min(
235                            cls.topRank,
236                            floor((cls.topRank + 0.99) * (util - cuts[1]) / (cuts[0] - cuts[1])),
237                        ),
238                    )
239                    for util in voter
240                ]
241            for i in range(n):
242                ballot[i] = strat[i]
243
244    Score0to.topRank = topRank
245    return Score0to if asClass else Score0to()
def Srv(topRank=10):
276def Srv(topRank=10):
277    """Score Runoff Voting
278    >>> Srv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
279    [1.2, 0.8, 1.21]
280    >>> Srv().results([[0,1,2]])[2]
281    2.0
282    >>> Srv().results([[0,1,2],[2,1,0]])[1]
283    1.0
284    >>> Srv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
285    [0.8888888888888888, 1.2222222222222223, 0.8888888888888888]
286    >>> Srv().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
287    [1.502537313432836, 1.492537313432836, 0.5074626865671642]
288    >>> Srv().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
289    [1.0526315789473684, 1.105263157894737, 0.8421052631578947]
290    >>> Srv().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
291    [1.0666666666666667, 3.6, 3.4, 0.8666666666666667, 1.3333333333333333]
292    """
293
294    score0to = Score(topRank, True)
295
296    class Srv0to(score0to):
297        stratTargetFor = Method.stratTarget3
298
299        def results(self, ballots, **kwargs):
300            """Srv results."""
301            ballots = ballots_from_dataframe(ballots)
302            baseResults = super(Srv0to, self).results(ballots, **kwargs)
303            (runnerUp, top) = sorted(range(len(baseResults)), key=lambda i: baseResults[i])[-2:]
304            upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots)
305            if upset > 0:
306                baseResults[runnerUp] = baseResults[top] + 0.01
307            return baseResults
308
309    return Srv0to()

Score Runoff Voting

>>> Srv().resultsFor(DeterministicModel(3)(5,3),Irv().honBallot)["results"]
[1.2, 0.8, 1.21]
>>> Srv().results([[0,1,2]])[2]
2.0
>>> Srv().results([[0,1,2],[2,1,0]])[1]
1.0
>>> Srv().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[0.8888888888888888, 1.2222222222222223, 0.8888888888888888]
>>> Srv().results([[2,1,0]] * 100 + [[1,0,2]] + [[0,2,1]] * 100)
[1.502537313432836, 1.492537313432836, 0.5074626865671642]
>>> Srv().results([[1,2,0]] * 8 + [[2,0,1]] * 6 + [[0,1,2]] * 5)
[1.0526315789473684, 1.105263157894737, 0.8421052631578947]
>>> Srv().results([[0,4,3,1,2]] * 5 + [[1,4,3,2,1]] * 4 + [[2,3,4,0,1]] * 6)
[1.0666666666666667, 3.6, 3.4, 0.8666666666666667, 1.3333333333333333]
class V321(Mav):
776class V321(Mav):
777    baseCuts = [-0.1, 0.8]
778    specificPercentiles = [45, 75]
779
780    stratTargetFor = Method.stratTarget3
781
782    def results(self, ballots, isHonest=False, **kwargs):
783        """3-2-1 Voting results.
784
785        >>> V321().resultsFor(DeterministicModel(3)(5,3),V321().honBallot)["results"]
786        [-0.75, 2, 1]
787        >>> V321().results([[0,1,2]])[2]
788        2
789        >>> V321().results([[0,1,2],[2,1,0]])[1]
790        2.5
791        >>> V321().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
792        [1, 1.5, -0.25]
793        >>> V321().results([[0,1,2,1]]*29 + [[1,2,0,1]]*30 + [[2,0,1,1]]*31 + [[1,1,1,2]]*10)
794        [3, 0.5, 1, 0]
795        >>> V321().results([[1,0,2,1]]*29 + [[0,2,1,1]]*30 + [[2,1,0,1]]*31 + [[1,1,1,2]]*10)
796        [3.375, 2.875, 0.25, 0]
797        """
798        ballots = ballots_from_dataframe(ballots)
799        candScores = list(zip(*ballots))
800        n2s = [sum(1 if s > 1 else 0 for s in c) for c in candScores]
801        o2s = argsort(n2s)  # order
802        r2s = [-1] * len(n2s)  # ranks
803        for r, i in enumerate(o2s):
804            r2s[i] = r
805        semifinalists = o2s[-3:]  # [third, second, first] by top ranks
806        # print(semifinalists)
807        n1s = [sum(1 if s > 0 else 0 for s in candScores[sf]) for sf in semifinalists]
808        o1s = argsort(n1s)
809        # print("n1s",n1s)
810        # print("o1s",o1s)
811        # print([semifinalists[o] for o in o1s]) #[third, second, first] by above-bottom
812        # print("r2s",r2s)
813        r2s[semifinalists[o1s[0]]] -= (o1s[0] + 1) * 0.75  # non-finalist below finalists
814
815        (runnerUp, top) = semifinalists[o1s[1]], semifinalists[o1s[2]]
816        upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots)
817        if upset > 0:
818            runnerUp, top = top, runnerUp
819            r2s[runnerUp], r2s[top] = r2s[top] - 0.125, r2s[runnerUp] + 0.125
820        r2s[top] = max(r2s[top], r2s[runnerUp] + 0.5)
821        if isHonest:
822            upset2 = sum(
823                sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[2]]])
824                for ballot in ballots
825            )
826            self.__class__.extraEvents["3beats1"] = upset2 > 0
827            upset3 = sum(
828                sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[1]]])
829                for ballot in ballots
830            )
831            self.__class__.extraEvents["3beats2"] = upset3 > 0
832            if len(o2s) > 3:
833                fourth = o2s[-4]
834                fourthNotLasts = sum(1 if s > 1 else 0 for s in candScores[fourth])
835                fourthWin = (
836                    fourthNotLasts > n1s[o1s[1]]
837                    and sum(
838                        sign(ballot[fourth] - ballot[semifinalists[o1s[2]]]) for ballot in ballots
839                    )
840                    > 0
841                )
842                self.__class__.extraEvents["4beats1"] = fourthWin
843
844        return [as_builtin_scalar(score) for score in r2s]
845
846    def stratBallotFor(self, polls):
847        """Returns a function which takes utilities and returns a dict(
848            isStrat=
849        for the given "polling" info.
850
851
852        >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
853        [2, 1, 0, 3]
854        """
855        places = sorted(enumerate(polls), key=lambda x: -x[1])  # high to low
856        top3 = [c for c, r in places[:3]]
857
858        # @rememberBallots ... do it later
859        def stratBallot(cls, voter):
860            stratGap = voter[top3[1]] - voter[top3[0]]
861            myPrefs = [c for c, v in sorted(enumerate(voter), key=lambda x: -x[1])]  # high to low
862            my3order = [myPrefs.index(c) for c in top3]
863            rating = 2
864            ballot = [0] * len(voter)
865            if my3order[0] == min(my3order):  # agree on winner
866                for i in range(my3order[0] + 1):
867                    ballot[myPrefs[i]] = 2
868                if my3order[1] <= my3order[2]:
869                    for i in range(my3order[0] + 1, my3order[1] + 1):
870                        ballot[myPrefs[i]] = 1
871                # print("agree",top3, my3order,ballot,[float('%.1g' % c) for c in voter])
872                return dict(strat=ballot, isStrat=False, stratGap=stratGap)
873            for c in myPrefs:
874                ballot[c] = rating
875                if rating and (c in top3):
876                    if c == top3[0]:
877                        rating = 0
878                    else:
879                        rating -= 1
880
881            # print("disagree",top3,my3order,ballot,[float('%.1g' % c) for c in voter])
882            return dict(strat=ballot, isStrat=True, stratGap=stratGap)
883
884        if self.extraEvents["3beats1"]:
885
886            @rememberBallots
887            def stratBallo2(cls, voter):
888                myprefs = sorted(enumerate(voter), key=lambda x: -x[1])  # high to low
889                rating = 2
890                ballot = [None] * len(voter)
891                isStrat = False
892                stratGap = 0
893                for c, util in myprefs:
894                    ballot[c] = rating
895                    if rating and (c in top3):
896                        if c == top3[2]:
897                            rating = 0
898                        else:
899                            rating -= 1
900                isStrat = voter[top3[0]] == max(voter[c] for c in top3)
901                return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
902
903            stratBallo2.__name__ = "stratBallot"  # God, that's ugly.
904            return stratBallo2
905
906        if self.extraEvents["4beats1"]:
907            fourth = places[3][0]
908            first = top3[1]
909
910            @rememberBallots
911            def stratBallo3(cls, voter):
912                stratGap = voter[top3[1]] - voter[top3[0]]
913                myprefs = sorted(enumerate(voter), key=lambda x: -x[1])  # high to low
914
915                rating = 2
916                ballot = [None] * len(voter)
917                if voter[fourth] > voter[first]:
918                    for c, util in myprefs:
919                        ballot[c] = rating
920                        if rating and (c == fourth):
921                            rating -= 2
922                    return dict(strat=ballot, isStrat=True, stratGap=stratGap)
923
924                return stratBallot(cls, voter)
925
926            stratBallo3.__name__ = "stratBallot"  # God, that's ugly.
927            return stratBallo3
928
929        return rememberBallots(stratBallot)

Majority Approval Voting

baseCuts = [-0.1, 0.8]
specificPercentiles = [45, 75]
def stratTargetFor(self, places):
311    def stratTarget3(self, places):
312        ((frontId, frontResult), (targId, targResult)) = places[:3:2]
313        return (frontId, frontResult, targId, targResult)
def results(self, ballots, isHonest=False, **kwargs):
782    def results(self, ballots, isHonest=False, **kwargs):
783        """3-2-1 Voting results.
784
785        >>> V321().resultsFor(DeterministicModel(3)(5,3),V321().honBallot)["results"]
786        [-0.75, 2, 1]
787        >>> V321().results([[0,1,2]])[2]
788        2
789        >>> V321().results([[0,1,2],[2,1,0]])[1]
790        2.5
791        >>> V321().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
792        [1, 1.5, -0.25]
793        >>> V321().results([[0,1,2,1]]*29 + [[1,2,0,1]]*30 + [[2,0,1,1]]*31 + [[1,1,1,2]]*10)
794        [3, 0.5, 1, 0]
795        >>> V321().results([[1,0,2,1]]*29 + [[0,2,1,1]]*30 + [[2,1,0,1]]*31 + [[1,1,1,2]]*10)
796        [3.375, 2.875, 0.25, 0]
797        """
798        ballots = ballots_from_dataframe(ballots)
799        candScores = list(zip(*ballots))
800        n2s = [sum(1 if s > 1 else 0 for s in c) for c in candScores]
801        o2s = argsort(n2s)  # order
802        r2s = [-1] * len(n2s)  # ranks
803        for r, i in enumerate(o2s):
804            r2s[i] = r
805        semifinalists = o2s[-3:]  # [third, second, first] by top ranks
806        # print(semifinalists)
807        n1s = [sum(1 if s > 0 else 0 for s in candScores[sf]) for sf in semifinalists]
808        o1s = argsort(n1s)
809        # print("n1s",n1s)
810        # print("o1s",o1s)
811        # print([semifinalists[o] for o in o1s]) #[third, second, first] by above-bottom
812        # print("r2s",r2s)
813        r2s[semifinalists[o1s[0]]] -= (o1s[0] + 1) * 0.75  # non-finalist below finalists
814
815        (runnerUp, top) = semifinalists[o1s[1]], semifinalists[o1s[2]]
816        upset = sum(sign(ballot[runnerUp] - ballot[top]) for ballot in ballots)
817        if upset > 0:
818            runnerUp, top = top, runnerUp
819            r2s[runnerUp], r2s[top] = r2s[top] - 0.125, r2s[runnerUp] + 0.125
820        r2s[top] = max(r2s[top], r2s[runnerUp] + 0.5)
821        if isHonest:
822            upset2 = sum(
823                sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[2]]])
824                for ballot in ballots
825            )
826            self.__class__.extraEvents["3beats1"] = upset2 > 0
827            upset3 = sum(
828                sign(ballot[semifinalists[o1s[0]]] - ballot[semifinalists[o1s[1]]])
829                for ballot in ballots
830            )
831            self.__class__.extraEvents["3beats2"] = upset3 > 0
832            if len(o2s) > 3:
833                fourth = o2s[-4]
834                fourthNotLasts = sum(1 if s > 1 else 0 for s in candScores[fourth])
835                fourthWin = (
836                    fourthNotLasts > n1s[o1s[1]]
837                    and sum(
838                        sign(ballot[fourth] - ballot[semifinalists[o1s[2]]]) for ballot in ballots
839                    )
840                    > 0
841                )
842                self.__class__.extraEvents["4beats1"] = fourthWin
843
844        return [as_builtin_scalar(score) for score in r2s]

3-2-1 Voting results.

>>> V321().resultsFor(DeterministicModel(3)(5,3),V321().honBallot)["results"]
[-0.75, 2, 1]
>>> V321().results([[0,1,2]])[2]
2
>>> V321().results([[0,1,2],[2,1,0]])[1]
2.5
>>> V321().results([[0,1,2]] * 4 + [[2,1,0]] * 3 + [[1,2,0]] * 2)
[1, 1.5, -0.25]
>>> V321().results([[0,1,2,1]]*29 + [[1,2,0,1]]*30 + [[2,0,1,1]]*31 + [[1,1,1,2]]*10)
[3, 0.5, 1, 0]
>>> V321().results([[1,0,2,1]]*29 + [[0,2,1,1]]*30 + [[2,1,0,1]]*31 + [[1,1,1,2]]*10)
[3.375, 2.875, 0.25, 0]
def stratBallotFor(self, polls):
846    def stratBallotFor(self, polls):
847        """Returns a function which takes utilities and returns a dict(
848            isStrat=
849        for the given "polling" info.
850
851
852        >>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
853        [2, 1, 0, 3]
854        """
855        places = sorted(enumerate(polls), key=lambda x: -x[1])  # high to low
856        top3 = [c for c, r in places[:3]]
857
858        # @rememberBallots ... do it later
859        def stratBallot(cls, voter):
860            stratGap = voter[top3[1]] - voter[top3[0]]
861            myPrefs = [c for c, v in sorted(enumerate(voter), key=lambda x: -x[1])]  # high to low
862            my3order = [myPrefs.index(c) for c in top3]
863            rating = 2
864            ballot = [0] * len(voter)
865            if my3order[0] == min(my3order):  # agree on winner
866                for i in range(my3order[0] + 1):
867                    ballot[myPrefs[i]] = 2
868                if my3order[1] <= my3order[2]:
869                    for i in range(my3order[0] + 1, my3order[1] + 1):
870                        ballot[myPrefs[i]] = 1
871                # print("agree",top3, my3order,ballot,[float('%.1g' % c) for c in voter])
872                return dict(strat=ballot, isStrat=False, stratGap=stratGap)
873            for c in myPrefs:
874                ballot[c] = rating
875                if rating and (c in top3):
876                    if c == top3[0]:
877                        rating = 0
878                    else:
879                        rating -= 1
880
881            # print("disagree",top3,my3order,ballot,[float('%.1g' % c) for c in voter])
882            return dict(strat=ballot, isStrat=True, stratGap=stratGap)
883
884        if self.extraEvents["3beats1"]:
885
886            @rememberBallots
887            def stratBallo2(cls, voter):
888                myprefs = sorted(enumerate(voter), key=lambda x: -x[1])  # high to low
889                rating = 2
890                ballot = [None] * len(voter)
891                isStrat = False
892                stratGap = 0
893                for c, util in myprefs:
894                    ballot[c] = rating
895                    if rating and (c in top3):
896                        if c == top3[2]:
897                            rating = 0
898                        else:
899                            rating -= 1
900                isStrat = voter[top3[0]] == max(voter[c] for c in top3)
901                return dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
902
903            stratBallo2.__name__ = "stratBallot"  # God, that's ugly.
904            return stratBallo2
905
906        if self.extraEvents["4beats1"]:
907            fourth = places[3][0]
908            first = top3[1]
909
910            @rememberBallots
911            def stratBallo3(cls, voter):
912                stratGap = voter[top3[1]] - voter[top3[0]]
913                myprefs = sorted(enumerate(voter), key=lambda x: -x[1])  # high to low
914
915                rating = 2
916                ballot = [None] * len(voter)
917                if voter[fourth] > voter[first]:
918                    for c, util in myprefs:
919                        ballot[c] = rating
920                        if rating and (c == fourth):
921                            rating -= 2
922                    return dict(strat=ballot, isStrat=True, stratGap=stratGap)
923
924                return stratBallot(cls, voter)
925
926            stratBallo3.__name__ = "stratBallot"  # God, that's ugly.
927            return stratBallo3
928
929        return rememberBallots(stratBallot)

Returns a function which takes utilities and returns a dict( isStrat= for the given "polling" info.

>>> Irv().stratBallotFor([3,2,1,0])(Irv,Voter([3,6,5,2]))
[2, 1, 0, 3]
def toVote(cutoffs, util):
312def toVote(cutoffs, util):
313    """maps one util to a vote, using cutoffs.
314
315    Used by Mav, but declared outside to avoid method binding overhead."""
316    for vote in range(len(cutoffs)):
317        if util <= cutoffs[vote]:
318            return vote
319    return vote + 1

maps one util to a vote, using cutoffs.

Used by Mav, but declared outside to avoid method binding overhead.