Edit on GitHub

vse_sim

Modern package facade for the VSE simulation library.

  1"""Modern package facade for the VSE simulation library."""
  2
  3from .data_classes import Method, SideTally, Tallies
  4from .dataframe import (
  5    VseResults,
  6    ballots_from_dataframe,
  7    ballots_to_dataframe,
  8    read_results_csv,
  9    rows_to_dataframe,
 10    scores_to_dataframe,
 11    summarize_vse,
 12    to_dataframe,
 13    voter_to_dataframe,
 14    voters_to_dataframe,
 15)
 16from .methods import (
 17    IRNR,
 18    V321,
 19    Borda,
 20    BulletyApprovalWith,
 21    Irv,
 22    IrvPrime,
 23    Mav,
 24    Mj,
 25    Plurality,
 26    Rp,
 27    Schulze,
 28    Score,
 29    Srv,
 30)
 31from .simulation import (
 32    CsvBatch,
 33    allSystems,
 34    baseRuns,
 35    markMethods,
 36    medianRuns,
 37    run_simulation,
 38    run_simulation_dataframe,
 39    uniquify,
 40)
 41from .strategies import (
 42    Chooser,
 43    LazyChooser,
 44    OssChooser,
 45    ProbChooser,
 46    beHon,
 47    beStrat,
 48    beX,
 49    biasedMediaFor,
 50    biaserAround,
 51    fuzzyMediaFor,
 52    orderOf,
 53    skewedMediaFor,
 54    topNMediaFor,
 55    truth,
 56)
 57from .voter_models import (
 58    DeterministicModel,
 59    DimElectorate,
 60    DimModel,
 61    DimVoter,
 62    Electorate,
 63    KSElectorate,
 64    KSModel,
 65    PersonalityVoter,
 66    PolyaModel,
 67    QModel,
 68    RandomModel,
 69    ReverseModel,
 70    Voter,
 71)
 72
 73__version__ = "0.1.5"
 74
 75__all__ = [
 76    "__version__",
 77    "Borda",
 78    "BulletyApprovalWith",
 79    "Chooser",
 80    "CsvBatch",
 81    "DeterministicModel",
 82    "DimElectorate",
 83    "DimModel",
 84    "DimVoter",
 85    "Electorate",
 86    "IRNR",
 87    "Irv",
 88    "IrvPrime",
 89    "KSElectorate",
 90    "KSModel",
 91    "LazyChooser",
 92    "Mav",
 93    "Method",
 94    "Mj",
 95    "OssChooser",
 96    "PersonalityVoter",
 97    "Plurality",
 98    "PolyaModel",
 99    "ProbChooser",
100    "QModel",
101    "RandomModel",
102    "ReverseModel",
103    "Rp",
104    "Schulze",
105    "Score",
106    "SideTally",
107    "Srv",
108    "Tallies",
109    "V321",
110    "VseResults",
111    "Voter",
112    "allSystems",
113    "ballots_from_dataframe",
114    "ballots_to_dataframe",
115    "baseRuns",
116    "beHon",
117    "beStrat",
118    "beX",
119    "biasedMediaFor",
120    "biaserAround",
121    "fuzzyMediaFor",
122    "markMethods",
123    "medianRuns",
124    "orderOf",
125    "read_results_csv",
126    "rows_to_dataframe",
127    "run_simulation",
128    "run_simulation_dataframe",
129    "scores_to_dataframe",
130    "skewedMediaFor",
131    "summarize_vse",
132    "to_dataframe",
133    "topNMediaFor",
134    "truth",
135    "uniquify",
136    "voter_to_dataframe",
137    "voters_to_dataframe",
138]
__version__ = '0.1.5'
class Borda(vse_sim.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 Chooser:
10class Chooser:
11    tallyKeys = []
12
13    def __init__(self, choice=None, subChoosers=None):
14        """Subclasses should just copy/paste this logic because
15        each will have its own parameters and so that's easiest."""
16        if choice is not None:
17            self.choice = choice
18        self.subChoosers = [] if subChoosers is None else subChoosers
19
20    def getName(self):
21        if hasattr(self, "choice"):  # only true for base class
22            # print("base")
23            return self.choice
24        if not hasattr(self, "name") or not self.name:
25            # print("generic")
26            self.name = self.__class__.__name__[:-7]  # drop the "Chooser"
27        # print("specific")
28        return self.name
29
30    def __call__(self, cls, voter, tally):
31        return self.choice
32
33    def addTallyKeys(self, tally):
34        for key in self.allTallyKeys:
35            tally[key] = 0
36
37    @cached_property
38    def myKeys(self):
39        prefix = f"{self.getName()}_"
40        return [prefix + key for key in self.tallyKeys]
41
42    @cached_property
43    def allTallyKeys(self):
44        keys = self.myKeys
45        for subChooser in self.subChoosers:
46            keys += subChooser.allTallyKeys
47        return keys
48
49    @cached_property
50    def __name__(self):
51        return self.__class__.__name__
Chooser(choice=None, subChoosers=None)
13    def __init__(self, choice=None, subChoosers=None):
14        """Subclasses should just copy/paste this logic because
15        each will have its own parameters and so that's easiest."""
16        if choice is not None:
17            self.choice = choice
18        self.subChoosers = [] if subChoosers is None else subChoosers

Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.

tallyKeys = []
subChoosers
def getName(self):
20    def getName(self):
21        if hasattr(self, "choice"):  # only true for base class
22            # print("base")
23            return self.choice
24        if not hasattr(self, "name") or not self.name:
25            # print("generic")
26            self.name = self.__class__.__name__[:-7]  # drop the "Chooser"
27        # print("specific")
28        return self.name
def addTallyKeys(self, tally):
33    def addTallyKeys(self, tally):
34        for key in self.allTallyKeys:
35            tally[key] = 0
@cached_property
def myKeys(self):
37    @cached_property
38    def myKeys(self):
39        prefix = f"{self.getName()}_"
40        return [prefix + key for key in self.tallyKeys]
@cached_property
def allTallyKeys(self):
42    @cached_property
43    def allTallyKeys(self):
44        keys = self.myKeys
45        for subChooser in self.subChoosers:
46            keys += subChooser.allTallyKeys
47        return keys
class CsvBatch:
 37class CsvBatch:
 38    @autoassign
 39    def __init__(
 40        self,
 41        model,
 42        methods,
 43        nvot,
 44        ncand,
 45        niter,
 46        baseName=None,
 47        media=truth,
 48        seed=None,
 49        force=False,
 50    ):
 51        """A harness function which creates niter elections from model and finds three kinds
 52        of utility for all methods given.
 53
 54        for instance:
 55
 56        >>> csvs = CsvBatch(PolyaModel(), [[Score(), baseRuns], [Mav(), medianRuns]], nvot=5, ncand=4, niter=3)
 57        >>> len(csvs.rows)
 58        60
 59        """
 60        rows = []
 61        emodel = str(model)
 62        if seed is None:
 63            seed = (baseName or "") + str(niter)
 64            self.seed = seed
 65        random.seed(seed)
 66        try:
 67            from git import Repo
 68
 69            repo = Repo(os.getcwd())
 70            if not force:
 71                assert not repo.is_dirty()
 72            self.repo_version = repo.head.commit.hexsha
 73        except Exception:
 74            self.repo_version = "unknown repo version"
 75        for i in range(niter):
 76            eid = uuid4()
 77            electorate = model(nvot, ncand)
 78            for method, chooserFuns in methods:
 79                results = method.resultsTable(
 80                    eid, emodel, ncand, electorate, chooserFuns, media=media
 81                )
 82                rows.extend(results)
 83            debug(i, results[1:3])
 84        self.rows = rows
 85        if baseName:
 86            self.saveFile(baseName)
 87
 88    def saveFile(self, baseName="SimResults"):
 89        """print the result of doVse in an accessible format.
 90        for instance:
 91
 92        csvs.saveFile()
 93        """
 94        i = 1
 95        while os.path.isfile(baseName + str(i) + ".csv"):
 96            i += 1
 97        keys = ["vse", "method", "chooser", *list(self.rows[0].keys())]
 98        for n in range(4):
 99            keys.extend([f"tallyName{str(n)}", f"tallyVal{str(n)}"])
100        keys = uniquify(keys)
101        path = baseName + str(i) + ".csv"
102        with open(path, "w") as myFile:
103            print(
104                f"# {dict(media=self.media.__name__, version=self.repo_version, seed=self.seed, model=self.model, methods=self.methods, nvot=self.nvot, ncand=self.ncand, niter=self.niter)}",
105                file=myFile,
106            )
107
108            dw = csv.DictWriter(myFile, keys, restval="NA")
109            dw.writeheader()
110            for r in self.rows:
111                dw.writerow(r)
112        return path
113
114    @property
115    def results(self):
116        """Return this batch as a pandas-backed ``VseResults`` object."""
117        from .dataframe import VseResults
118
119        return VseResults.from_rows(self.rows)
120
121    @property
122    def dataframe(self):
123        """Return this batch's rows as a pandas DataFrame."""
124        return self.to_dataframe()
125
126    @property
127    def df(self):
128        """Alias for ``dataframe``."""
129        return self.dataframe
130
131    def to_dataframe(self, copy=True):
132        """Return this batch's rows as a pandas DataFrame."""
133        return self.results.to_dataframe(copy=copy)
134
135    def summarize(self, group_by=("method", "chooser"), sort_by="mean_vse", ascending=False):
136        """Return a pandas DataFrame summarizing VSE scores for this batch."""
137        return self.results.summarize(
138            group_by=group_by,
139            sort_by=sort_by,
140            ascending=ascending,
141        )
142
143    def report(self, group_by=("method", "chooser")):
144        """Return common pandas report tables for this batch."""
145        return self.results.report(group_by=group_by)
146
147    def plot_vse(self, *args, **kwargs):
148        """Plot summarized VSE scores for this batch."""
149        return self.results.plot_vse(*args, **kwargs)
@autoassign
CsvBatch( model, methods, nvot, ncand, niter, baseName=None, media=<function truth>, seed=None, force=False)
38    @autoassign
39    def __init__(
40        self,
41        model,
42        methods,
43        nvot,
44        ncand,
45        niter,
46        baseName=None,
47        media=truth,
48        seed=None,
49        force=False,
50    ):
51        """A harness function which creates niter elections from model and finds three kinds
52        of utility for all methods given.
53
54        for instance:
55
56        >>> csvs = CsvBatch(PolyaModel(), [[Score(), baseRuns], [Mav(), medianRuns]], nvot=5, ncand=4, niter=3)
57        >>> len(csvs.rows)
58        60
59        """
60        rows = []
61        emodel = str(model)
62        if seed is None:
63            seed = (baseName or "") + str(niter)
64            self.seed = seed
65        random.seed(seed)
66        try:
67            from git import Repo
68
69            repo = Repo(os.getcwd())
70            if not force:
71                assert not repo.is_dirty()
72            self.repo_version = repo.head.commit.hexsha
73        except Exception:
74            self.repo_version = "unknown repo version"
75        for i in range(niter):
76            eid = uuid4()
77            electorate = model(nvot, ncand)
78            for method, chooserFuns in methods:
79                results = method.resultsTable(
80                    eid, emodel, ncand, electorate, chooserFuns, media=media
81                )
82                rows.extend(results)
83            debug(i, results[1:3])
84        self.rows = rows
85        if baseName:
86            self.saveFile(baseName)

A harness function which creates niter elections from model and finds three kinds of utility for all methods given.

for instance:

>>> csvs = CsvBatch(PolyaModel(), [[Score(), baseRuns], [Mav(), medianRuns]], nvot=5, ncand=4, niter=3)
>>> len(csvs.rows)
60
rows
def saveFile(self, baseName='SimResults'):
 88    def saveFile(self, baseName="SimResults"):
 89        """print the result of doVse in an accessible format.
 90        for instance:
 91
 92        csvs.saveFile()
 93        """
 94        i = 1
 95        while os.path.isfile(baseName + str(i) + ".csv"):
 96            i += 1
 97        keys = ["vse", "method", "chooser", *list(self.rows[0].keys())]
 98        for n in range(4):
 99            keys.extend([f"tallyName{str(n)}", f"tallyVal{str(n)}"])
100        keys = uniquify(keys)
101        path = baseName + str(i) + ".csv"
102        with open(path, "w") as myFile:
103            print(
104                f"# {dict(media=self.media.__name__, version=self.repo_version, seed=self.seed, model=self.model, methods=self.methods, nvot=self.nvot, ncand=self.ncand, niter=self.niter)}",
105                file=myFile,
106            )
107
108            dw = csv.DictWriter(myFile, keys, restval="NA")
109            dw.writeheader()
110            for r in self.rows:
111                dw.writerow(r)
112        return path

print the result of doVse in an accessible format. for instance:

csvs.saveFile()

results
114    @property
115    def results(self):
116        """Return this batch as a pandas-backed ``VseResults`` object."""
117        from .dataframe import VseResults
118
119        return VseResults.from_rows(self.rows)

Return this batch as a pandas-backed VseResults object.

dataframe
121    @property
122    def dataframe(self):
123        """Return this batch's rows as a pandas DataFrame."""
124        return self.to_dataframe()

Return this batch's rows as a pandas DataFrame.

df
126    @property
127    def df(self):
128        """Alias for ``dataframe``."""
129        return self.dataframe

Alias for dataframe.

def to_dataframe(self, copy=True):
131    def to_dataframe(self, copy=True):
132        """Return this batch's rows as a pandas DataFrame."""
133        return self.results.to_dataframe(copy=copy)

Return this batch's rows as a pandas DataFrame.

def summarize( self, group_by=('method', 'chooser'), sort_by='mean_vse', ascending=False):
135    def summarize(self, group_by=("method", "chooser"), sort_by="mean_vse", ascending=False):
136        """Return a pandas DataFrame summarizing VSE scores for this batch."""
137        return self.results.summarize(
138            group_by=group_by,
139            sort_by=sort_by,
140            ascending=ascending,
141        )

Return a pandas DataFrame summarizing VSE scores for this batch.

def report(self, group_by=('method', 'chooser')):
143    def report(self, group_by=("method", "chooser")):
144        """Return common pandas report tables for this batch."""
145        return self.results.report(group_by=group_by)

Return common pandas report tables for this batch.

def plot_vse(self, *args, **kwargs):
147    def plot_vse(self, *args, **kwargs):
148        """Plot summarized VSE scores for this batch."""
149        return self.results.plot_vse(*args, **kwargs)

Plot summarized VSE scores for this batch.

class DeterministicModel(vse_sim.RandomModel):
179class DeterministicModel(RandomModel):
180    """Basically, a somewhat non-boring stub for testing.
181
182    >>> DeterministicModel(3)(4, 3)
183    [(0, 1, 2), (1, 2, 0), (2, 0, 1), (0, 1, 2)]
184    """
185
186    @autoassign
187    def __init__(self, modulo):
188        pass
189
190    def __call__(self, nvot, ncand, vType=PersonalityVoter):
191        return Electorate(vType((i + j) % self.modulo for i in range(ncand)) for j in range(nvot))

Basically, a somewhat non-boring stub for testing.

>>> DeterministicModel(3)(4, 3)
[(0, 1, 2), (1, 2, 0), (2, 0, 1), (0, 1, 2)]
@autoassign
DeterministicModel(modulo)
186    @autoassign
187    def __init__(self, modulo):
188        pass
Inherited Members
RandomModel
to_dataframe
class DimElectorate(vse_sim.Electorate):
299class DimElectorate(Electorate):
300    def asDims(self, v, *args):
301        return v
302
303    def fromDims(self, dimvoters, vType):
304        for v in dimvoters:
305            self.append(vType.fromDims(v, self))
306
307    def calcTotWeight(self):
308        self.totWeight = sum(w**2 for w in self.dimWeights)

A list of voters. Each voter is a list of candidate utilities

def asDims(self, v, *args):
300    def asDims(self, v, *args):
301        return v
def fromDims(self, dimvoters, vType):
303    def fromDims(self, dimvoters, vType):
304        for v in dimvoters:
305            self.append(vType.fromDims(v, self))
def calcTotWeight(self):
307    def calcTotWeight(self):
308        self.totWeight = sum(w**2 for w in self.dimWeights)
class DimModel(vse_sim.RandomModel):
311class DimModel(RandomModel):
312    """
313
314    >>> dm = DimModel(2,baseElectorate=DeterministicModel(3))
315    >>> dm(2,4)
316    [(-1.8439088914585775, -0.0, -1.0, -1.8439088914585775), (-1.2649110640673518, -1.0, -0.0, -1.2649110640673518)]
317    >>> dm.dimWeights
318    [1, 0.5]
319
320
321    """
322
323    builtElectorate = DimElectorate
324
325    @autoassign
326    def __init__(self, ndims=3, dimWeights=None, baseElectorate=RandomModel()):
327        if self.dimWeights is None:
328            self.dimWeights = [2 ** (-n) for n in range(ndims)]
329        assert len(self.dimWeights) == self.ndims
330
331    def __call__(self, nvot, ncand, vType=DimVoter):
332        elec = self.builtElectorate()
333        elec.dimWeights = self.dimWeights
334        return self.makeElectorate(elec, nvot, ncand, vType)
335
336    def makeElectorate(self, elec, nvot, ncand, vType):
337        elec.calcTotWeight()
338        votersncands = self.baseElectorate(nvot + ncand, len(elec.dimWeights), vType)
339        elec.base = [elec.asDims(v, i) for i, v in enumerate(votersncands[:nvot])]
340        elec.cands = [elec.asDims(v, nvot + i) for i, v in enumerate(votersncands[nvot:])]
341        elec.fromDims(elec.base, vType)
342        return elec
>>> dm = DimModel(2,baseElectorate=DeterministicModel(3))
>>> dm(2,4)
[(-1.8439088914585775, -0.0, -1.0, -1.8439088914585775), (-1.2649110640673518, -1.0, -0.0, -1.2649110640673518)]
>>> dm.dimWeights
[1, 0.5]
@autoassign
DimModel( ndims=3, dimWeights=None, baseElectorate=<RandomModel object>)
325    @autoassign
326    def __init__(self, ndims=3, dimWeights=None, baseElectorate=RandomModel()):
327        if self.dimWeights is None:
328            self.dimWeights = [2 ** (-n) for n in range(ndims)]
329        assert len(self.dimWeights) == self.ndims
builtElectorate = <class 'DimElectorate'>
def makeElectorate(self, elec, nvot, ncand, vType):
336    def makeElectorate(self, elec, nvot, ncand, vType):
337        elec.calcTotWeight()
338        votersncands = self.baseElectorate(nvot + ncand, len(elec.dimWeights), vType)
339        elec.base = [elec.asDims(v, i) for i, v in enumerate(votersncands[:nvot])]
340        elec.cands = [elec.asDims(v, nvot + i) for i, v in enumerate(votersncands[nvot:])]
341        elec.fromDims(elec.base, vType)
342        return elec
Inherited Members
RandomModel
to_dataframe
class DimVoter(vse_sim.PersonalityVoter):
273class DimVoter(PersonalityVoter):
274    """A voter in an n-dimensional model."""
275
276    @classmethod
277    def fromDims(cls, v, e, caring=None):
278        if caring is None:
279            caring = [1] * len(v)
280            totCaring = e.totWeight
281        else:
282            totCaring = sum((c * w) ** 2 for c, w in zip(caring, e.dimWeights))
283        me = cls(
284            -sqrt(
285                sum(
286                    ((vd - cd) * w * cares) ** 2
287                    for (vd, cd, w, cares) in zip(v, c, e.dimWeights, caring)
288                )
289                / totCaring
290            )
291            for c in e.cands
292        )
293        me.copyAttrsFrom(v)
294        me.dims = v
295        me.elec = e
296        return me

A voter in an n-dimensional model.

@classmethod
def fromDims(cls, v, e, caring=None):
276    @classmethod
277    def fromDims(cls, v, e, caring=None):
278        if caring is None:
279            caring = [1] * len(v)
280            totCaring = e.totWeight
281        else:
282            totCaring = sum((c * w) ** 2 for c, w in zip(caring, e.dimWeights))
283        me = cls(
284            -sqrt(
285                sum(
286                    ((vd - cd) * w * cares) ** 2
287                    for (vd, cd, w, cares) in zip(v, c, e.dimWeights, caring)
288                )
289                / totCaring
290            )
291            for c in e.cands
292        )
293        me.copyAttrsFrom(v)
294        me.dims = v
295        me.elec = e
296        return me
class Electorate(builtins.list):
124class Electorate(list):
125    """A list of voters.
126    Each voter is a list of candidate utilities"""
127
128    @property
129    def dataframe(self):
130        """Return voter utilities as a tidy DataFrame."""
131        return self.to_dataframe()
132
133    @property
134    def df(self):
135        """Alias for ``dataframe``."""
136        return self.dataframe
137
138    @property
139    def wide_dataframe(self):
140        """Return one row per voter with one utility column per candidate."""
141        return self.to_dataframe(wide=True)
142
143    @cached_property
144    def socUtils(self):
145        """Return mean utility across electorate for each candidate: their social utilities.
146
147        >>> e = Electorate([[1,2],[3,4]])
148        >>> e.socUtils
149        [2.0, 3.0]
150        """
151        return list(map(mean, zip(*self)))
152
153    def to_dataframe(self, wide=False, **kwargs):
154        """Return voter utilities as a tidy or wide DataFrame."""
155        from .dataframe import voters_to_dataframe
156
157        return voters_to_dataframe(self, wide=wide, **kwargs)

A list of voters. Each voter is a list of candidate utilities

dataframe
128    @property
129    def dataframe(self):
130        """Return voter utilities as a tidy DataFrame."""
131        return self.to_dataframe()

Return voter utilities as a tidy DataFrame.

df
133    @property
134    def df(self):
135        """Alias for ``dataframe``."""
136        return self.dataframe

Alias for dataframe.

wide_dataframe
138    @property
139    def wide_dataframe(self):
140        """Return one row per voter with one utility column per candidate."""
141        return self.to_dataframe(wide=True)

Return one row per voter with one utility column per candidate.

@cached_property
def socUtils(self):
143    @cached_property
144    def socUtils(self):
145        """Return mean utility across electorate for each candidate: their social utilities.
146
147        >>> e = Electorate([[1,2],[3,4]])
148        >>> e.socUtils
149        [2.0, 3.0]
150        """
151        return list(map(mean, zip(*self)))

Return mean utility across electorate for each candidate: their social utilities.

>>> e = Electorate([[1,2],[3,4]])
>>> e.socUtils
[2.0, 3.0]
def to_dataframe(self, wide=False, **kwargs):
153    def to_dataframe(self, wide=False, **kwargs):
154        """Return voter utilities as a tidy or wide DataFrame."""
155        from .dataframe import voters_to_dataframe
156
157        return voters_to_dataframe(self, wide=wide, **kwargs)

Return voter utilities as a tidy or wide DataFrame.

class IRNR(vse_sim.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.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(vse_sim.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 KSElectorate(vse_sim.DimElectorate):
349class KSElectorate(DimElectorate):
350    def chooseClusters(self, n, alpha, caring):
351        self.clusters = []
352        for i in range(n):
353            item = []
354            for c in range(self.numClusters):
355                r = (i + alpha) * random.random()
356                if r > i:
357                    item.append(self.numSubclusters[c])
358                    self.numSubclusters[c] += 1
359                else:
360                    item.append(self.clusters[int(r)][c])
361            self.clusters.append(item)
362        self.clusterMeans = []
363        self.clusterCaring = []
364        for c in range(self.numClusters):
365            subclusterMeans = []
366            subclusterCaring = []
367            for _ in range(self.numSubclusters[c]):
368                cares = caring()
369
370                subclusterMeans.append([random.gauss(0, sqrt(cares)) for _ in range(self.dcs[c])])
371
372                subclusterCaring.append(caring())
373            self.clusterMeans.append(subclusterMeans)
374            self.clusterCaring.append(subclusterCaring)
375
376    def asDims(self, v, i):
377        result = []
378        cares = []
379        for dim, c in enumerate(range(self.numClusters)):
380            clusterMean = self.clusterMeans[c][self.clusters[i][c]]
381            for m in clusterMean:
382                acare = self.clusterCaring[c][self.clusters[i][c]]
383                result.append(m + (v[dim] * sqrt(1 - acare)))
384                cares.append(acare)
385        v = PersonalityVoter(result)  # TODO: do personality right
386        v.cares = cares
387        return v
388
389    def fromDims(self, dimvoters, vType):
390        for v in dimvoters:
391            self.append(vType.fromDims(v, self, v.cares))

A list of voters. Each voter is a list of candidate utilities

def chooseClusters(self, n, alpha, caring):
350    def chooseClusters(self, n, alpha, caring):
351        self.clusters = []
352        for i in range(n):
353            item = []
354            for c in range(self.numClusters):
355                r = (i + alpha) * random.random()
356                if r > i:
357                    item.append(self.numSubclusters[c])
358                    self.numSubclusters[c] += 1
359                else:
360                    item.append(self.clusters[int(r)][c])
361            self.clusters.append(item)
362        self.clusterMeans = []
363        self.clusterCaring = []
364        for c in range(self.numClusters):
365            subclusterMeans = []
366            subclusterCaring = []
367            for _ in range(self.numSubclusters[c]):
368                cares = caring()
369
370                subclusterMeans.append([random.gauss(0, sqrt(cares)) for _ in range(self.dcs[c])])
371
372                subclusterCaring.append(caring())
373            self.clusterMeans.append(subclusterMeans)
374            self.clusterCaring.append(subclusterCaring)
def asDims(self, v, i):
376    def asDims(self, v, i):
377        result = []
378        cares = []
379        for dim, c in enumerate(range(self.numClusters)):
380            clusterMean = self.clusterMeans[c][self.clusters[i][c]]
381            for m in clusterMean:
382                acare = self.clusterCaring[c][self.clusters[i][c]]
383                result.append(m + (v[dim] * sqrt(1 - acare)))
384                cares.append(acare)
385        v = PersonalityVoter(result)  # TODO: do personality right
386        v.cares = cares
387        return v
def fromDims(self, dimvoters, vType):
389    def fromDims(self, dimvoters, vType):
390        for v in dimvoters:
391            self.append(vType.fromDims(v, self, v.cares))
class KSModel(vse_sim.DimModel):
394class KSModel(DimModel):  # Kitchen sink
395    builtElectorate = KSElectorate
396    baseElectorate = RandomModel()
397
398    @autoassign
399    # dc = dimensional cluster; vc = voter cluster
400    def __init__(
401        self,
402        dcdecay=(1, 1),
403        dccut=0.2,
404        wcdecay=(1, 1),
405        wccut=0.2,
406        wcalpha=1,
407        vccaring=(3, 1.5),
408    ):
409        DimModel.__init__(self)
410
411    def __str__(self):
412        return "_".join(
413            str(x)
414            for x in (self.__class__.__name__, self.wcalpha)
415            + self.dcdecay
416            + self.wcdecay
417            + self.vccaring
418        )
419
420    def __call__(self, nvot, ncand, vType=DimVoter):
421        """Tests? Making statistical tests that would pass reliably is
422        a huge hassle. Sorry, maybe later.
423        """
424        vType.resetClusters()
425        e = self.builtElectorate()
426        e.dcs = []  # number of dimensions in each dc
427        e.dimWeights = []  # raw importance of each dimension, regardless of dc
428        clusterWeight = 1
429        while clusterWeight > self.dccut:
430            dimweight = clusterWeight
431            dimnum = 0
432            while dimweight > self.wccut:
433                e.dimWeights.append(dimweight)
434                dimnum += 1
435                dimweight *= beta.rvs(*self.wcdecay)
436            e.dcs.append(dimnum)
437            clusterWeight *= beta.rvs(*self.dcdecay)
438        e.numClusters = len(e.dcs)
439        e.numSubclusters = [0] * e.numClusters
440        e.chooseClusters(nvot + ncand, self.wcalpha, lambda: beta.rvs(*self.vccaring))
441        return self.makeElectorate(e, nvot, ncand, vType)
>>> dm = DimModel(2,baseElectorate=DeterministicModel(3))
>>> dm(2,4)
[(-1.8439088914585775, -0.0, -1.0, -1.8439088914585775), (-1.2649110640673518, -1.0, -0.0, -1.2649110640673518)]
>>> dm.dimWeights
[1, 0.5]
@autoassign
KSModel( dcdecay=(1, 1), dccut=0.2, wcdecay=(1, 1), wccut=0.2, wcalpha=1, vccaring=(3, 1.5))
398    @autoassign
399    # dc = dimensional cluster; vc = voter cluster
400    def __init__(
401        self,
402        dcdecay=(1, 1),
403        dccut=0.2,
404        wcdecay=(1, 1),
405        wccut=0.2,
406        wcalpha=1,
407        vccaring=(3, 1.5),
408    ):
409        DimModel.__init__(self)
builtElectorate = <class 'KSElectorate'>
baseElectorate = <RandomModel object>
class LazyChooser(vse_sim.Chooser):
59class LazyChooser(Chooser):
60    """Honest, if honest and strategic are the same. Otherwise, extra-strategic."""
61
62    tallyKeys = [""]
63
64    def __init__(self, subChoosers=None):
65        super().__init__(subChoosers=[beHon, beX] if subChoosers is None else subChoosers)
66
67    def __call__(self, cls, voter, tally):
68        if getattr(voter, f"{cls.__name__}_hon") == getattr(voter, f"{cls.__name__}_strat"):
69            tally[self.myKeys[0]] += 0
70            return self.subChoosers[0](cls, voter, tally)  # hon
71        tally[self.myKeys[0]] += 1
72        return self.subChoosers[1](cls, voter, tally)  # strat

Honest, if honest and strategic are the same. Otherwise, extra-strategic.

LazyChooser(subChoosers=None)
64    def __init__(self, subChoosers=None):
65        super().__init__(subChoosers=[beHon, beX] if subChoosers is None else subChoosers)

Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.

tallyKeys = ['']
class Mav(vse_sim.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 Method:
102class Method:
103    """Base class for election methods. Holds some of the duct tape."""
104
105    def __str__(self):
106        return self.__class__.__name__
107
108    def results(self, ballots, isHonest=False, **kwargs):
109        """Combines ballots into results. Override for comparative
110        methods.
111
112        Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index.
113
114        Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index.
115
116        Test for subclasses, makes no sense to test this method in the abstract base class.
117        """
118        from .dataframe import ballots_from_dataframe
119
120        ballots = ballots_from_dataframe(ballots)
121        return list(map(self.candScore, zip(*ballots)))
122
123    def honest_ballots(self, voters):
124        """Return honest ballots for voters as method-ready lists."""
125        sentinel = object()
126        previous_cuts = getattr(self.__class__, "specificCuts", sentinel)
127        try:
128            ballot_factory = self.honBallotFor(voters)
129            return [ballot_factory(self.__class__, voter) for voter in voters]
130        finally:
131            if previous_cuts is sentinel:
132                try:
133                    delattr(self.__class__, "specificCuts")
134                except AttributeError:
135                    pass
136            else:
137                self.__class__.specificCuts = previous_cuts
138
139    def ballots_dataframe(self, voters, wide=False):
140        """Return honest ballots for voters as a tidy or wide DataFrame."""
141        from .dataframe import ballots_to_dataframe
142
143        return ballots_to_dataframe(self.honest_ballots(voters), wide=wide, method=self)
144
145    def results_dataframe(self, ballots, isHonest=False, **kwargs):
146        """Return candidate scores for ballots as a DataFrame."""
147        from .dataframe import scores_to_dataframe
148
149        self.__class__.extraEvents = {}
150        return scores_to_dataframe(
151            self.results(ballots, isHonest=isHonest, **kwargs),
152            method=self,
153        )
154
155    @staticmethod  # cls is provided explicitly, not through binding
156    def honBallot(cls, utils):
157        """Takes utilities and returns an honest ballot"""
158        raise NotImplementedError(f"{cls} needs honBallot")
159
160    @staticmethod
161    def winner(results):
162        """Simply find the winner once scores are already calculated. Override for
163        ranked methods.
164
165
166        >>> Method().winner([1,2,3,2,-100])
167        2
168        >>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6
169        True
170        """
171        winScore = max(result for result in results if isnum(result))
172        winners = [cand for (cand, score) in enumerate(results) if score == winScore]
173        return random.choice(winners)
174
175    def honBallotFor(self, voters):
176        """This is where you would do any setup necessary and create an honBallot
177        function. But the base version just returns the honBallot function."""
178        return self.honBallot
179
180    def dummyBallotFor(self, polls):
181        """Returns a (function which takes utilities and returns a dummy ballot)
182        for the given "polling" info."""
183        return lambda cls, utilities, stratTally: utilities
184
185    def resultsFor(self, voters, chooser, tally=None, **kwargs):
186        """create ballots and get results.
187
188        Again, test on subclasses.
189        """
190        if tally is None:
191            tally = SideTally()
192        tally.initKeys(chooser)
193        return dict(
194            results=self.results(
195                [chooser(self.__class__, voter, tally) for voter in voters], **kwargs
196            ),
197            chooser=chooser.__name__,
198            tally=tally,
199        )
200
201    def multiResults(self, voters, chooserFuns=(), media=(lambda x, t: x), checkStrat=True):
202        """Runs two base elections: first with honest votes, then
203        with strategic results based on the first results (filtered by
204        the media). Then, runs a series of elections using each chooserFun
205        in chooserFuns to select the votes for each voter.
206
207        Returns a tuple of (honResults, stratResults, ...). The stratresults
208        are based on common polling information, which is given by media(honresults).
209        """
210        from .strategies import OssChooser
211
212        honTally = SideTally()
213        self.__class__.extraEvents = {}
214        hon = self.resultsFor(voters, self.honBallotFor(voters), honTally, isHonest=True)
215
216        stratTally = SideTally()
217
218        polls = media(hon["results"], stratTally)
219        winner, _w, target, _t = self.stratTargetFor(sorted(enumerate(polls), key=lambda x: -x[1]))
220
221        strat = self.resultsFor(voters, self.stratBallotFor(polls), stratTally)
222
223        ossTally = SideTally()
224        oss = self.resultsFor(voters, self.ballotChooserFor(OssChooser()), ossTally)
225        ossWinner = oss["results"].index(max(oss["results"]))
226        ossTally["worked"] += 1 if ossWinner == target else (0 if ossWinner == winner else -1)
227
228        smart = dict(
229            results=(hon["results"] if ossTally["worked"] == 1 else oss["results"]),
230            chooser="smartOss",
231            tally=SideTally(),
232        )
233
234        extraTallies = Tallies()
235        results = [strat, oss, smart] + [
236            self.resultsFor(voters, self.ballotChooserFor(chooserFun), aTally)
237            for (chooserFun, aTally) in zip(chooserFuns, extraTallies)
238        ]
239        return [(hon["results"], hon["chooser"], list(self.__class__.extraEvents.items()))] + [
240            (r["results"], r["chooser"], r["tally"].itemList()) for r in results
241        ]
242
243    def vseOn(self, voters, chooserFuns=(), **args):
244        """Finds honest and strategic voter satisfaction efficiency (VSE)
245        for this method on the given electorate.
246        """
247        multiResults = self.multiResults(voters, chooserFuns, **args)
248        utils = voters.socUtils
249        best = max(utils)
250        rand = mean(utils)
251
252        # import pprint
253        # pprint.pprint(multiResults)
254        vses = VseMethodRun(
255            self.__class__,
256            chooserFuns,
257            [
258                VseOneRun(
259                    [(utils[self.winner(result)] - rand) / (best - rand)],
260                    tally,
261                    chooser,
262                )
263                for (result, chooser, tally) in multiResults[0]
264            ],
265        )
266        vses.extraEvents = multiResults[1]
267        return vses
268
269    def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args):
270        multiResults = self.multiResults(voters, chooserFuns, **args)
271        utils = voters.socUtils
272        best = max(utils)
273        rand = mean(utils)
274        rows = []
275        nvot = len(voters)
276        for result, chooser, tallyItems in multiResults:
277            row = {
278                "eid": eid,
279                "emodel": emodel,
280                "ncand": cands,
281                "nvot": nvot,
282                "best": best,
283                "rand": rand,
284                "method": str(self),
285                "chooser": chooser,  # .getName(),
286                "util": utils[self.winner(result)],
287                "vse": (utils[self.winner(result)] - rand) / (best - rand),
288            }
289            # print(tallyItems)
290            for i, (k, v) in enumerate(tallyItems):
291                # print("Result: tally ",i,k,v)
292                row[f"tallyName{str(i)}"] = str(k)
293                row[f"tallyVal{str(i)}"] = str(v)
294            rows.append(row)
295        return rows
296
297    @staticmethod
298    def ballotChooserFor(chooserFun):
299        """Takes a chooserFun; returns a ballot chooser using that chooserFun"""
300
301        def ballotChooser(cls, voter, tally):
302            return getattr(voter, f"{cls.__name__}_{chooserFun(cls, voter, tally)}")
303
304        ballotChooser.__name__ = chooserFun.getName()
305        return ballotChooser
306
307    def stratTarget2(self, places):
308        ((frontId, frontResult), (targId, targResult)) = places[:2]
309        return (frontId, frontResult, targId, targResult)
310
311    def stratTarget3(self, places):
312        ((frontId, frontResult), (targId, targResult)) = places[:3:2]
313        return (frontId, frontResult, targId, targResult)
314
315    stratTargetFor = stratTarget2
316
317    def stratBallotFor(self, polls):
318        """Returns a (function which takes utilities and returns a strategic ballot)
319        for the given "polling" info."""
320
321        places = sorted(enumerate(polls), key=lambda x: -x[1])  # from high to low
322        # print("places",places)
323        (frontId, frontResult, targId, targResult) = self.stratTargetFor(places)
324        n = len(polls)
325
326        @rememberBallots
327        def stratBallot(cls, voter):
328            stratGap = voter[targId] - voter[frontId]
329            ballot = [0] * len(voter)
330            isStrat = stratGap > 0
331            extras = cls.fillStratBallot(
332                voter,
333                polls,
334                places,
335                n,
336                stratGap,
337                ballot,
338                frontId,
339                frontResult,
340                targId,
341                targResult,
342            )
343            result = dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
344            if extras:
345                result.update(extras)
346            return result
347
348        return stratBallot

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

def results(self, ballots, isHonest=False, **kwargs):
108    def results(self, ballots, isHonest=False, **kwargs):
109        """Combines ballots into results. Override for comparative
110        methods.
111
112        Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index.
113
114        Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index.
115
116        Test for subclasses, makes no sense to test this method in the abstract base class.
117        """
118        from .dataframe import ballots_from_dataframe
119
120        ballots = ballots_from_dataframe(ballots)
121        return list(map(self.candScore, zip(*ballots)))

Combines ballots into results. Override for comparative methods.

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

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

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

def honest_ballots(self, voters):
123    def honest_ballots(self, voters):
124        """Return honest ballots for voters as method-ready lists."""
125        sentinel = object()
126        previous_cuts = getattr(self.__class__, "specificCuts", sentinel)
127        try:
128            ballot_factory = self.honBallotFor(voters)
129            return [ballot_factory(self.__class__, voter) for voter in voters]
130        finally:
131            if previous_cuts is sentinel:
132                try:
133                    delattr(self.__class__, "specificCuts")
134                except AttributeError:
135                    pass
136            else:
137                self.__class__.specificCuts = previous_cuts

Return honest ballots for voters as method-ready lists.

def ballots_dataframe(self, voters, wide=False):
139    def ballots_dataframe(self, voters, wide=False):
140        """Return honest ballots for voters as a tidy or wide DataFrame."""
141        from .dataframe import ballots_to_dataframe
142
143        return ballots_to_dataframe(self.honest_ballots(voters), wide=wide, method=self)

Return honest ballots for voters as a tidy or wide DataFrame.

def results_dataframe(self, ballots, isHonest=False, **kwargs):
145    def results_dataframe(self, ballots, isHonest=False, **kwargs):
146        """Return candidate scores for ballots as a DataFrame."""
147        from .dataframe import scores_to_dataframe
148
149        self.__class__.extraEvents = {}
150        return scores_to_dataframe(
151            self.results(ballots, isHonest=isHonest, **kwargs),
152            method=self,
153        )

Return candidate scores for ballots as a DataFrame.

@staticmethod
def honBallot(cls, utils):
155    @staticmethod  # cls is provided explicitly, not through binding
156    def honBallot(cls, utils):
157        """Takes utilities and returns an honest ballot"""
158        raise NotImplementedError(f"{cls} needs honBallot")

Takes utilities and returns an honest ballot

@staticmethod
def winner(results):
160    @staticmethod
161    def winner(results):
162        """Simply find the winner once scores are already calculated. Override for
163        ranked methods.
164
165
166        >>> Method().winner([1,2,3,2,-100])
167        2
168        >>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6
169        True
170        """
171        winScore = max(result for result in results if isnum(result))
172        winners = [cand for (cand, score) in enumerate(results) if score == winScore]
173        return random.choice(winners)

Simply find the winner once scores are already calculated. Override for ranked methods.

>>> Method().winner([1,2,3,2,-100])
2
>>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6
True
def honBallotFor(self, voters):
175    def honBallotFor(self, voters):
176        """This is where you would do any setup necessary and create an honBallot
177        function. But the base version just returns the honBallot function."""
178        return self.honBallot

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

def dummyBallotFor(self, polls):
180    def dummyBallotFor(self, polls):
181        """Returns a (function which takes utilities and returns a dummy ballot)
182        for the given "polling" info."""
183        return lambda cls, utilities, stratTally: utilities

Returns a (function which takes utilities and returns a dummy ballot) for the given "polling" info.

def resultsFor(self, voters, chooser, tally=None, **kwargs):
185    def resultsFor(self, voters, chooser, tally=None, **kwargs):
186        """create ballots and get results.
187
188        Again, test on subclasses.
189        """
190        if tally is None:
191            tally = SideTally()
192        tally.initKeys(chooser)
193        return dict(
194            results=self.results(
195                [chooser(self.__class__, voter, tally) for voter in voters], **kwargs
196            ),
197            chooser=chooser.__name__,
198            tally=tally,
199        )

create ballots and get results.

Again, test on subclasses.

def multiResults( self, voters, chooserFuns=(), media=<function Method.<lambda>>, checkStrat=True):
201    def multiResults(self, voters, chooserFuns=(), media=(lambda x, t: x), checkStrat=True):
202        """Runs two base elections: first with honest votes, then
203        with strategic results based on the first results (filtered by
204        the media). Then, runs a series of elections using each chooserFun
205        in chooserFuns to select the votes for each voter.
206
207        Returns a tuple of (honResults, stratResults, ...). The stratresults
208        are based on common polling information, which is given by media(honresults).
209        """
210        from .strategies import OssChooser
211
212        honTally = SideTally()
213        self.__class__.extraEvents = {}
214        hon = self.resultsFor(voters, self.honBallotFor(voters), honTally, isHonest=True)
215
216        stratTally = SideTally()
217
218        polls = media(hon["results"], stratTally)
219        winner, _w, target, _t = self.stratTargetFor(sorted(enumerate(polls), key=lambda x: -x[1]))
220
221        strat = self.resultsFor(voters, self.stratBallotFor(polls), stratTally)
222
223        ossTally = SideTally()
224        oss = self.resultsFor(voters, self.ballotChooserFor(OssChooser()), ossTally)
225        ossWinner = oss["results"].index(max(oss["results"]))
226        ossTally["worked"] += 1 if ossWinner == target else (0 if ossWinner == winner else -1)
227
228        smart = dict(
229            results=(hon["results"] if ossTally["worked"] == 1 else oss["results"]),
230            chooser="smartOss",
231            tally=SideTally(),
232        )
233
234        extraTallies = Tallies()
235        results = [strat, oss, smart] + [
236            self.resultsFor(voters, self.ballotChooserFor(chooserFun), aTally)
237            for (chooserFun, aTally) in zip(chooserFuns, extraTallies)
238        ]
239        return [(hon["results"], hon["chooser"], list(self.__class__.extraEvents.items()))] + [
240            (r["results"], r["chooser"], r["tally"].itemList()) for r in results
241        ]

Runs two base elections: first with honest votes, then with strategic results based on the first results (filtered by the media). Then, runs a series of elections using each chooserFun in chooserFuns to select the votes for each voter.

Returns a tuple of (honResults, stratResults, ...). The stratresults are based on common polling information, which is given by media(honresults).

def vseOn(self, voters, chooserFuns=(), **args):
243    def vseOn(self, voters, chooserFuns=(), **args):
244        """Finds honest and strategic voter satisfaction efficiency (VSE)
245        for this method on the given electorate.
246        """
247        multiResults = self.multiResults(voters, chooserFuns, **args)
248        utils = voters.socUtils
249        best = max(utils)
250        rand = mean(utils)
251
252        # import pprint
253        # pprint.pprint(multiResults)
254        vses = VseMethodRun(
255            self.__class__,
256            chooserFuns,
257            [
258                VseOneRun(
259                    [(utils[self.winner(result)] - rand) / (best - rand)],
260                    tally,
261                    chooser,
262                )
263                for (result, chooser, tally) in multiResults[0]
264            ],
265        )
266        vses.extraEvents = multiResults[1]
267        return vses

Finds honest and strategic voter satisfaction efficiency (VSE) for this method on the given electorate.

def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args):
269    def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args):
270        multiResults = self.multiResults(voters, chooserFuns, **args)
271        utils = voters.socUtils
272        best = max(utils)
273        rand = mean(utils)
274        rows = []
275        nvot = len(voters)
276        for result, chooser, tallyItems in multiResults:
277            row = {
278                "eid": eid,
279                "emodel": emodel,
280                "ncand": cands,
281                "nvot": nvot,
282                "best": best,
283                "rand": rand,
284                "method": str(self),
285                "chooser": chooser,  # .getName(),
286                "util": utils[self.winner(result)],
287                "vse": (utils[self.winner(result)] - rand) / (best - rand),
288            }
289            # print(tallyItems)
290            for i, (k, v) in enumerate(tallyItems):
291                # print("Result: tally ",i,k,v)
292                row[f"tallyName{str(i)}"] = str(k)
293                row[f"tallyVal{str(i)}"] = str(v)
294            rows.append(row)
295        return rows
@staticmethod
def ballotChooserFor(chooserFun):
297    @staticmethod
298    def ballotChooserFor(chooserFun):
299        """Takes a chooserFun; returns a ballot chooser using that chooserFun"""
300
301        def ballotChooser(cls, voter, tally):
302            return getattr(voter, f"{cls.__name__}_{chooserFun(cls, voter, tally)}")
303
304        ballotChooser.__name__ = chooserFun.getName()
305        return ballotChooser

Takes a chooserFun; returns a ballot chooser using that chooserFun

def stratTarget2(self, places):
307    def stratTarget2(self, places):
308        ((frontId, frontResult), (targId, targResult)) = places[:2]
309        return (frontId, frontResult, targId, targResult)
def stratTarget3(self, places):
311    def stratTarget3(self, places):
312        ((frontId, frontResult), (targId, targResult)) = places[:3:2]
313        return (frontId, frontResult, targId, targResult)
def stratTargetFor(self, places):
307    def stratTarget2(self, places):
308        ((frontId, frontResult), (targId, targResult)) = places[:2]
309        return (frontId, frontResult, targId, targResult)
def stratBallotFor(self, polls):
317    def stratBallotFor(self, polls):
318        """Returns a (function which takes utilities and returns a strategic ballot)
319        for the given "polling" info."""
320
321        places = sorted(enumerate(polls), key=lambda x: -x[1])  # from high to low
322        # print("places",places)
323        (frontId, frontResult, targId, targResult) = self.stratTargetFor(places)
324        n = len(polls)
325
326        @rememberBallots
327        def stratBallot(cls, voter):
328            stratGap = voter[targId] - voter[frontId]
329            ballot = [0] * len(voter)
330            isStrat = stratGap > 0
331            extras = cls.fillStratBallot(
332                voter,
333                polls,
334                places,
335                n,
336                stratGap,
337                ballot,
338                frontId,
339                frontResult,
340                targId,
341                targResult,
342            )
343            result = dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
344            if extras:
345                result.update(extras)
346            return result
347
348        return stratBallot

Returns a (function which takes utilities and returns a strategic ballot) for the given "polling" info.

class Mj(vse_sim.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 OssChooser(vse_sim.Chooser):
75class OssChooser(Chooser):
76    tallyKeys = ["", "gap"]
77    """one-sided strategy:
78    returns a 'strategic' ballot for those who prefer the strategic target,
79    and an honest ballot for those who prefer the honest winner. Only works
80    if honBallot and stratBallot have already been called for the voter.
81
82
83    """
84
85    def __init__(self, subChoosers=None):
86        super().__init__(subChoosers=[beHon, beStrat] if subChoosers is None else subChoosers)
87
88    def __call__(self, cls, voter, tally):
89        hon, strat = self.subChoosers
90        if not getattr(voter, f"{cls.__name__}_isStrat", False):
91            return hon(cls, voter, tally) if callable(hon) else hon
92        tally[self.myKeys[0]] += 1
93        tally[self.myKeys[1]] += getattr(voter, f"{cls.__name__}_stratGap", 0)
94        return strat(cls, voter, tally) if callable(strat) else strat
95
96    def getName(self):
97        baseName = super(OssChooser, self).getName()
98        return f"{baseName}." + "_".join(s.getName() for s in self.subChoosers) + "."
OssChooser(subChoosers=None)
85    def __init__(self, subChoosers=None):
86        super().__init__(subChoosers=[beHon, beStrat] if subChoosers is None else subChoosers)

Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.

tallyKeys = ['', 'gap']

one-sided strategy: returns a 'strategic' ballot for those who prefer the strategic target, and an honest ballot for those who prefer the honest winner. Only works if honBallot and stratBallot have already been called for the voter.

def getName(self):
96    def getName(self):
97        baseName = super(OssChooser, self).getName()
98        return f"{baseName}." + "_".join(s.getName() for s in self.subChoosers) + "."
class PersonalityVoter(vse_sim.Voter):
100class PersonalityVoter(Voter):
101    cluster_count = 0
102
103    def __init__(self, *args, **kw):
104        super().__init__()  # *args, **kw) #WTF, python?
105        self.cluster = self.__class__.cluster_count
106        self.__class__.cluster_count += 1
107        self.personality = random.gauss(0, 1)  # probably to be used for strategic propensity
108        # but in future, could be other clustering voter variability, such as media awareness
109
110    @classmethod
111    def resetClusters(cls):
112        cls.cluster_count = 0
113
114    def copyWithUtils(self, utils):
115        voter = super().copyWithUtils(utils)
116        voter.copyAttrsFrom(self)
117        return voter
118
119    def copyAttrsFrom(self, model):
120        self.personality = model.personality
121        self.cluster = model.cluster

A tuple of candidate utilities.

PersonalityVoter(*args, **kw)
103    def __init__(self, *args, **kw):
104        super().__init__()  # *args, **kw) #WTF, python?
105        self.cluster = self.__class__.cluster_count
106        self.__class__.cluster_count += 1
107        self.personality = random.gauss(0, 1)  # probably to be used for strategic propensity
108        # but in future, could be other clustering voter variability, such as media awareness
cluster_count = 0
cluster
personality
@classmethod
def resetClusters(cls):
110    @classmethod
111    def resetClusters(cls):
112        cls.cluster_count = 0
def copyWithUtils(self, utils):
114    def copyWithUtils(self, utils):
115        voter = super().copyWithUtils(utils)
116        voter.copyAttrsFrom(self)
117        return voter

create a new voter with attrs as self and given utils.

This version is a stub, since this voter class has no attrs.

def copyAttrsFrom(self, model):
119    def copyAttrsFrom(self, model):
120        self.personality = model.personality
121        self.cluster = model.cluster
class Plurality(vse_sim.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]
class PolyaModel(vse_sim.RandomModel):
246class PolyaModel(RandomModel):
247    """This creates electorates based on a Polya/Hoppe/Dirichlet model, with mutation.
248    You start with an "urn" of n=seedVoter voters from seedModel,
249     plus alpha "wildcard" voters. Then you draw a voter from the urn,
250     clone and mutate them, and put the original and clone back into the urn.
251     If you draw a "wildcard", use voterGen to make a new voter.
252    """
253
254    @autoassign
255    def __init__(self, seedVoters=2, alpha=1, seedModel=QModel(), mutantFactor=0.2):
256        pass
257
258    def __call__(self, nvot, ncand, vType=PersonalityVoter):
259        """Tests? Making statistical tests that would pass reliably is
260        a huge hassle. Sorry, maybe later.
261        """
262        vType.resetClusters()
263        election = self.seedModel(self.seedVoters, ncand, vType)
264        while len(election) < nvot:
265            i = random.randrange(len(election) + self.alpha)
266            if i < len(election):
267                election.append(election[i].mutantChild(self.mutantFactor))
268            else:
269                election.append(vType.rand(ncand))
270        return election

This creates electorates based on a Polya/Hoppe/Dirichlet model, with mutation. You start with an "urn" of n=seedVoter voters from seedModel, plus alpha "wildcard" voters. Then you draw a voter from the urn, clone and mutate them, and put the original and clone back into the urn. If you draw a "wildcard", use voterGen to make a new voter.

@autoassign
PolyaModel( seedVoters=2, alpha=1, seedModel=<QModel object>, mutantFactor=0.2)
254    @autoassign
255    def __init__(self, seedVoters=2, alpha=1, seedModel=QModel(), mutantFactor=0.2):
256        pass
Inherited Members
RandomModel
to_dataframe
class ProbChooser(vse_sim.Chooser):
101class ProbChooser(Chooser):
102    def __init__(self, probs):
103        self.probs = probs
104        subChoosers = [chooser for (p, chooser) in probs]
105        super().__init__(subChoosers=subChoosers)
106
107    def __call__(self, cls, voter, tally):
108        r = random.random()
109        for i, (p, chooser) in enumerate(self.probs):
110            r -= p
111            if r < 0:
112                if i > 0:  # keep tally for all but first option
113                    tally[f"{self.getName()}_{chooser.getName()}"] += 1
114                return chooser(cls, voter, tally)
115        return self.subChoosers[-1](cls, voter, tally) if self.subChoosers else None
116
117    def getName(self):
118        baseName = super(ProbChooser, self).getName()
119        return (
120            f"{baseName}."
121            + "_".join(s.getName() + str(round(p * 100)) for p, s in self.probs)
122            + "."
123        )
ProbChooser(probs)
102    def __init__(self, probs):
103        self.probs = probs
104        subChoosers = [chooser for (p, chooser) in probs]
105        super().__init__(subChoosers=subChoosers)

Subclasses should just copy/paste this logic because each will have its own parameters and so that's easiest.

probs
def getName(self):
117    def getName(self):
118        baseName = super(ProbChooser, self).getName()
119        return (
120            f"{baseName}."
121            + "_".join(s.getName() + str(round(p * 100)) for p, s in self.probs)
122            + "."
123        )
class QModel(vse_sim.RandomModel):
214class QModel(RandomModel):
215    """Adds a quality dimension to a base model,
216    by generating an election and then hybridizing all voters
217    with a common quality vector.
218
219    Useful along with ReverseModel to create a poor-man's 2d model.
220
221    Basic structure
222        >>> random.seed(0)
223        >>> e4 = QModel(sqrt(3), RandomModel())(100,1)
224        >>> len(e4)
225        100
226        >>> len(e4.socUtils)
227        1
228
229    Reduces the standard deviation
230        >>> 0.4 < std(list(zip(e4))) < 0.6
231        True
232
233    """
234
235    @autoassign
236    def __init__(self, qWeight=0.5, baseModel=ReverseModel()):
237        pass
238
239    def __call__(self, nvot, ncand, vType=PersonalityVoter):
240        qualities = vType.rand(ncand)
241        return Electorate(
242            [v.hybridWith(qualities, self.qWeight) for v in self.baseModel(nvot, ncand, vType)]
243        )

Adds a quality dimension to a base model, by generating an election and then hybridizing all voters with a common quality vector.

Useful along with ReverseModel to create a poor-man's 2d model.

Basic structure

random.seed(0) e4 = QModel(sqrt(3), RandomModel())(100,1) len(e4) 100 len(e4.socUtils) 1

Reduces the standard deviation

0.4 < std(list(zip(e4))) < 0.6 True

@autoassign
QModel(qWeight=0.5, baseModel=<ReverseModel object>)
235    @autoassign
236    def __init__(self, qWeight=0.5, baseModel=ReverseModel()):
237        pass
Inherited Members
RandomModel
to_dataframe
class RandomModel:
160class RandomModel:
161    """Empty base class for election models; that is, electorate factories.
162
163    >>> e4 = RandomModel()(4,3)
164    >>> [len(v) for v in e4]
165    [3, 3, 3, 3]
166    """
167
168    def __str__(self):
169        return self.__class__.__name__
170
171    def __call__(self, nvot, ncand, vType=PersonalityVoter):
172        return Electorate(vType.rand(ncand) for _ in range(nvot))
173
174    def to_dataframe(self, nvot, ncand, vType=PersonalityVoter, wide=False, **kwargs):
175        """Generate an electorate and return its utilities as a DataFrame."""
176        return self(nvot, ncand, vType=vType).to_dataframe(wide=wide, **kwargs)

Empty base class for election models; that is, electorate factories.

>>> e4 = RandomModel()(4,3)
>>> [len(v) for v in e4]
[3, 3, 3, 3]
def to_dataframe( self, nvot, ncand, vType=<class 'PersonalityVoter'>, wide=False, **kwargs):
174    def to_dataframe(self, nvot, ncand, vType=PersonalityVoter, wide=False, **kwargs):
175        """Generate an electorate and return its utilities as a DataFrame."""
176        return self(nvot, ncand, vType=vType).to_dataframe(wide=wide, **kwargs)

Generate an electorate and return its utilities as a DataFrame.

class ReverseModel(vse_sim.RandomModel):
194class ReverseModel(RandomModel):
195    """Creates an even number of voters in two diametrically-opposed camps
196    (ie, opposite utilities for all candidates)
197
198    >>> e4 = ReverseModel()(4,3)
199    >>> [len(v) for v in e4]
200    [3, 3, 3, 3]
201    >>> e4[0].hybridWith(e4[3],1)
202    (0.0, 0.0, 0.0)
203    """
204
205    def __call__(self, nvot, ncand, vType=PersonalityVoter):
206        if nvot % 2:
207            raise ValueError
208        basevoter = vType.rand(ncand)
209        return Electorate(
210            ([basevoter] * (nvot // 2)) + ([vType(-q for q in basevoter)] * (nvot // 2))
211        )

Creates an even number of voters in two diametrically-opposed camps (ie, opposite utilities for all candidates)

>>> e4 = ReverseModel()(4,3)
>>> [len(v) for v in e4]
[3, 3, 3, 3]
>>> e4[0].hybridWith(e4[3],1)
(0.0, 0.0, 0.0)
Inherited Members
RandomModel
to_dataframe
class Rp(vse_sim.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(vse_sim.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()
class SideTally(collections.defaultdict):
24class SideTally(defaultdict):
25    """Used for keeping track of how many voters are being strategic, etc.
26
27    DO NOT use plain +; for this class, it is equivalent to +=, but less readable.
28
29    """
30
31    def __init__(self):
32        super().__init__(int)
33
34    def initKeys(self, chooser):
35        try:
36            self.keyList = chooser.allTallyKeys()
37        except AttributeError:
38            try:
39                self.keyList = list(chooser)
40            except TypeError:
41                self.keyList = []
42        self.initKeys = staticmethod(lambda x: x)  # don't do it again
43
44    def serialize(self):
45        try:
46            return [self[key] for key in self.keyList]
47        except AttributeError:
48            return []
49
50    def fullSerialize(self):
51        if not hasattr(self, "keyList"):
52            return [self[key] for key in self.keys()]
53        return [self[key] for key in self.keyList]
54
55    def itemList(self):
56        try:
57            kl = self.keyList
58            return [(k, self[k]) for k in kl] + [(k, self[k]) for k in self.keys() if k not in kl]
59        except AttributeError:
60            return list(self.items())

Used for keeping track of how many voters are being strategic, etc.

DO NOT use plain +; for this class, it is equivalent to +=, but less readable.

def initKeys(self, chooser):
34    def initKeys(self, chooser):
35        try:
36            self.keyList = chooser.allTallyKeys()
37        except AttributeError:
38            try:
39                self.keyList = list(chooser)
40            except TypeError:
41                self.keyList = []
42        self.initKeys = staticmethod(lambda x: x)  # don't do it again
def serialize(self):
44    def serialize(self):
45        try:
46            return [self[key] for key in self.keyList]
47        except AttributeError:
48            return []
def fullSerialize(self):
50    def fullSerialize(self):
51        if not hasattr(self, "keyList"):
52            return [self[key] for key in self.keys()]
53        return [self[key] for key in self.keyList]
def itemList(self):
55    def itemList(self):
56        try:
57            kl = self.keyList
58            return [(k, self[k]) for k in kl] + [(k, self[k]) for k in self.keys() if k not in kl]
59        except AttributeError:
60            return list(self.items())
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 Tallies(builtins.list):
63class Tallies(list):
64    """Used (ONCE) as an enumerator, gives an inexhaustible flow of SideTally objects.
65    After that, use as list to see those objects.
66
67    >>> ts = Tallies()
68    >>> for i, j in zip(ts, [5,4,3]):
69    ...     i[j] += j
70    ...
71    >>> [t.serialize() for t in ts]
72    [[], [], [], []]
73    >>> [t.fullSerialize() for t in ts]
74    [[5], [4], [3], []]
75    >>> [t.initKeys([k]) for (t,k) in zip(ts,[6,4,3])]
76    [None, None, None]
77    >>> [t.serialize() for t in ts]
78    [[0], [4], [3], []]
79    """
80
81    def __iter__(self):
82        if getattr(self, "used", False):
83            return super().__iter__()
84        self.used = True
85        return self._generated_tallies()
86
87    def __eq__(self, other):
88        if not isinstance(other, Tallies):
89            return super().__eq__(other)
90        return super().__eq__(other) and getattr(self, "used", False) == getattr(
91            other, "used", False
92        )
93
94    def _generated_tallies(self):
95        while True:
96            tally = SideTally()
97            self.append(tally)
98            yield tally

Used (ONCE) as an enumerator, gives an inexhaustible flow of SideTally objects. After that, use as list to see those objects.

>>> ts = Tallies()
>>> for i, j in zip(ts, [5,4,3]):
...     i[j] += j
...
>>> [t.serialize() for t in ts]
[[], [], [], []]
>>> [t.fullSerialize() for t in ts]
[[5], [4], [3], []]
>>> [t.initKeys([k]) for (t,k) in zip(ts,[6,4,3])]
[None, None, None]
>>> [t.serialize() for t in ts]
[[0], [4], [3], []]
class V321(vse_sim.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]
@dataclass(frozen=True)
class VseResults:
228@dataclass(frozen=True)
229class VseResults:
230    """Pandas-backed simulation result set.
231
232    ``frame`` is the canonical tabular representation. Convenience methods
233    return DataFrames or matplotlib axes so notebook workflows can keep chaining.
234    """
235
236    frame: pd.DataFrame
237
238    @classmethod
239    def from_rows(cls, rows: Iterable[dict] | pd.DataFrame | VseResults) -> "VseResults":
240        return cls(to_dataframe(rows))
241
242    @classmethod
243    def from_csv(cls, path) -> "VseResults":
244        return cls(pd.read_csv(Path(path), comment="#"))
245
246    @classmethod
247    def concat(cls, results: Iterable[VseResults | pd.DataFrame | Iterable[dict]]) -> "VseResults":
248        frames = [rows_to_dataframe(result) for result in results]
249        return cls(pd.concat(frames, ignore_index=True))
250
251    def __len__(self) -> int:
252        return len(self.frame)
253
254    @property
255    def dataframe(self) -> pd.DataFrame:
256        """Return the backing DataFrame for fluent notebook work."""
257        return self.frame
258
259    @property
260    def df(self) -> pd.DataFrame:
261        """Alias for ``dataframe``."""
262        return self.frame
263
264    def to_dataframe(self, copy=True) -> pd.DataFrame:
265        return self.frame.copy() if copy else self.frame
266
267    def to_csv(self, path, index=False, **kwargs):
268        """Write the result DataFrame to CSV and return the path."""
269        self.frame.to_csv(path, index=index, **kwargs)
270        return path
271
272    def summarize(
273        self,
274        group_by=DEFAULT_GROUP_BY,
275        sort_by="mean_vse",
276        ascending=False,
277    ) -> pd.DataFrame:
278        """Return aggregate VSE metrics grouped by one or more columns."""
279        group_columns = _group_columns(group_by)
280        summary = (
281            self.frame.groupby(group_columns, as_index=False)
282            .agg(
283                rows=("vse", "size"),
284                elections=("eid", "nunique"),
285                mean_vse=("vse", "mean"),
286                median_vse=("vse", "median"),
287                min_vse=("vse", "min"),
288                max_vse=("vse", "max"),
289                std_vse=("vse", "std"),
290            )
291            .fillna({"std_vse": 0})
292        )
293        return summary.sort_values(
294            [sort_by, *group_columns],
295            ascending=[ascending, *[True] * len(group_columns)],
296        ).reset_index(drop=True)
297
298    def leaderboard(self, n=10, group_by="method", by="mean_vse") -> pd.DataFrame:
299        """Return the top groups by a summary metric."""
300        return self.summarize(group_by=group_by, sort_by=by).head(n)
301
302    def pivot(
303        self,
304        index="method",
305        columns="chooser",
306        values="mean_vse",
307        group_by=None,
308    ) -> pd.DataFrame:
309        """Return a comparison matrix from summarized result data."""
310        index_columns = _group_columns(index)
311        column_columns = _group_columns(columns)
312        if group_by is None:
313            group_by = _unique_columns(index, columns)
314        summary = self.summarize(group_by=group_by).copy()
315        pivot_columns = []
316        for column in column_columns:
317            if column in index_columns:
318                column_alias = f"__vse_sim_pivot_{column}"
319                summary[column_alias] = summary[column]
320                pivot_columns.append(column_alias)
321            else:
322                pivot_columns.append(column)
323        pivoted = summary.pivot(
324            index=index,
325            columns=pivot_columns[0] if isinstance(columns, str) else pivot_columns,
326            values=values,
327        )
328        pivoted.columns = pivoted.columns.set_names(column_columns)
329        return pivoted
330
331    def report(self, group_by=DEFAULT_GROUP_BY) -> dict[str, pd.DataFrame]:
332        """Build common report tables from a result set."""
333        tables = {
334            "results": self.to_dataframe(),
335            "summary": self.summarize(group_by=group_by),
336            "method_summary": self.summarize(group_by="method"),
337        }
338        if "chooser" in self.frame:
339            tables["chooser_summary"] = self.summarize(group_by="chooser")
340            tables["method_by_chooser"] = self.pivot()
341        return tables
342
343    def plot_vse(
344        self,
345        group_by="method",
346        value="mean_vse",
347        kind="bar",
348        ax=None,
349        title=None,
350        **kwargs,
351    ):
352        """Plot summarized VSE scores and return the matplotlib axes."""
353        summary = self.summarize(group_by=group_by)
354        group_columns = _group_columns(group_by)
355        labels = summary[group_columns].astype(str).agg(" | ".join, axis=1)
356        plot_frame = summary.assign(label=labels).set_index("label")
357        axes = plot_frame[value].plot(kind=kind, ax=ax, **kwargs)
358        axes.set_xlabel("VSE" if kind == "barh" else "")
359        axes.set_ylabel("" if kind == "barh" else "VSE")
360        axes.set_title(title or f"{value} by {' / '.join(group_columns)}")
361        return axes

Pandas-backed simulation result set.

frame is the canonical tabular representation. Convenience methods return DataFrames or matplotlib axes so notebook workflows can keep chaining.

VseResults(frame: pandas.DataFrame)
frame: pandas.DataFrame
@classmethod
def from_rows( cls, rows: Iterable[dict] | pandas.DataFrame | VseResults) -> VseResults:
238    @classmethod
239    def from_rows(cls, rows: Iterable[dict] | pd.DataFrame | VseResults) -> "VseResults":
240        return cls(to_dataframe(rows))
@classmethod
def from_csv(cls, path) -> VseResults:
242    @classmethod
243    def from_csv(cls, path) -> "VseResults":
244        return cls(pd.read_csv(Path(path), comment="#"))
@classmethod
def concat( cls, results: Iterable[VseResults | pandas.DataFrame | Iterable[dict]]) -> VseResults:
246    @classmethod
247    def concat(cls, results: Iterable[VseResults | pd.DataFrame | Iterable[dict]]) -> "VseResults":
248        frames = [rows_to_dataframe(result) for result in results]
249        return cls(pd.concat(frames, ignore_index=True))
dataframe: pandas.DataFrame
254    @property
255    def dataframe(self) -> pd.DataFrame:
256        """Return the backing DataFrame for fluent notebook work."""
257        return self.frame

Return the backing DataFrame for fluent notebook work.

df: pandas.DataFrame
259    @property
260    def df(self) -> pd.DataFrame:
261        """Alias for ``dataframe``."""
262        return self.frame

Alias for dataframe.

def to_dataframe(self, copy=True) -> pandas.DataFrame:
264    def to_dataframe(self, copy=True) -> pd.DataFrame:
265        return self.frame.copy() if copy else self.frame
def to_csv(self, path, index=False, **kwargs):
267    def to_csv(self, path, index=False, **kwargs):
268        """Write the result DataFrame to CSV and return the path."""
269        self.frame.to_csv(path, index=index, **kwargs)
270        return path

Write the result DataFrame to CSV and return the path.

def summarize( self, group_by=('method', 'chooser'), sort_by='mean_vse', ascending=False) -> pandas.DataFrame:
272    def summarize(
273        self,
274        group_by=DEFAULT_GROUP_BY,
275        sort_by="mean_vse",
276        ascending=False,
277    ) -> pd.DataFrame:
278        """Return aggregate VSE metrics grouped by one or more columns."""
279        group_columns = _group_columns(group_by)
280        summary = (
281            self.frame.groupby(group_columns, as_index=False)
282            .agg(
283                rows=("vse", "size"),
284                elections=("eid", "nunique"),
285                mean_vse=("vse", "mean"),
286                median_vse=("vse", "median"),
287                min_vse=("vse", "min"),
288                max_vse=("vse", "max"),
289                std_vse=("vse", "std"),
290            )
291            .fillna({"std_vse": 0})
292        )
293        return summary.sort_values(
294            [sort_by, *group_columns],
295            ascending=[ascending, *[True] * len(group_columns)],
296        ).reset_index(drop=True)

Return aggregate VSE metrics grouped by one or more columns.

def leaderboard(self, n=10, group_by='method', by='mean_vse') -> pandas.DataFrame:
298    def leaderboard(self, n=10, group_by="method", by="mean_vse") -> pd.DataFrame:
299        """Return the top groups by a summary metric."""
300        return self.summarize(group_by=group_by, sort_by=by).head(n)

Return the top groups by a summary metric.

def pivot( self, index='method', columns='chooser', values='mean_vse', group_by=None) -> pandas.DataFrame:
302    def pivot(
303        self,
304        index="method",
305        columns="chooser",
306        values="mean_vse",
307        group_by=None,
308    ) -> pd.DataFrame:
309        """Return a comparison matrix from summarized result data."""
310        index_columns = _group_columns(index)
311        column_columns = _group_columns(columns)
312        if group_by is None:
313            group_by = _unique_columns(index, columns)
314        summary = self.summarize(group_by=group_by).copy()
315        pivot_columns = []
316        for column in column_columns:
317            if column in index_columns:
318                column_alias = f"__vse_sim_pivot_{column}"
319                summary[column_alias] = summary[column]
320                pivot_columns.append(column_alias)
321            else:
322                pivot_columns.append(column)
323        pivoted = summary.pivot(
324            index=index,
325            columns=pivot_columns[0] if isinstance(columns, str) else pivot_columns,
326            values=values,
327        )
328        pivoted.columns = pivoted.columns.set_names(column_columns)
329        return pivoted

Return a comparison matrix from summarized result data.

def report(self, group_by=('method', 'chooser')) -> dict[str, pandas.DataFrame]:
331    def report(self, group_by=DEFAULT_GROUP_BY) -> dict[str, pd.DataFrame]:
332        """Build common report tables from a result set."""
333        tables = {
334            "results": self.to_dataframe(),
335            "summary": self.summarize(group_by=group_by),
336            "method_summary": self.summarize(group_by="method"),
337        }
338        if "chooser" in self.frame:
339            tables["chooser_summary"] = self.summarize(group_by="chooser")
340            tables["method_by_chooser"] = self.pivot()
341        return tables

Build common report tables from a result set.

def plot_vse( self, group_by='method', value='mean_vse', kind='bar', ax=None, title=None, **kwargs):
343    def plot_vse(
344        self,
345        group_by="method",
346        value="mean_vse",
347        kind="bar",
348        ax=None,
349        title=None,
350        **kwargs,
351    ):
352        """Plot summarized VSE scores and return the matplotlib axes."""
353        summary = self.summarize(group_by=group_by)
354        group_columns = _group_columns(group_by)
355        labels = summary[group_columns].astype(str).agg(" | ".join, axis=1)
356        plot_frame = summary.assign(label=labels).set_index("label")
357        axes = plot_frame[value].plot(kind=kind, ax=ax, **kwargs)
358        axes.set_xlabel("VSE" if kind == "barh" else "")
359        axes.set_ylabel("" if kind == "barh" else "VSE")
360        axes.set_title(title or f"{value} by {' / '.join(group_columns)}")
361        return axes

Plot summarized VSE scores and return the matplotlib axes.

class Voter(builtins.tuple):
10class Voter(tuple):
11    """A tuple of candidate utilities."""
12
13    def __new__(cls, utils=()):
14        return super().__new__(cls, (as_builtin_scalar(util) for util in utils))
15
16    @property
17    def dataframe(self):
18        """Return this voter's candidate utilities as a tidy DataFrame."""
19        return self.to_dataframe()
20
21    @property
22    def df(self):
23        """Alias for ``dataframe``."""
24        return self.dataframe
25
26    @classmethod
27    def rand(cls, ncand):
28        """Create a random voter with an independent standard normal
29        utility for each candidate.
30
31        ncand determines the number of candidates a voter should have
32        utilities for.
33            >>> [len(Voter.rand(i)) for i in list(range(5))]
34            [0, 1, 2, 3, 4]
35
36        utilities should be in a standard normal distribution
37            >>> v100 = Voter.rand(100)
38            >>> -0.5 < mean(v100) < 0.5
39            True
40            >>> 0.6 < std(v100) < 1.4
41            True
42        """
43        return cls(random.gauss(0, 1) for _ in range(ncand))
44
45    def hybridWith(self, v2, w2):
46        """Create a weighted average of two voters.
47
48        The weight of v1 is always 1; w2 is the weight of v2 relative to that.
49
50        If both are
51        standard normal to start with, the result will be standard normal too.
52
53        Length must be the same
54            >>> Voter([1,2]).hybridWith(Voter([1,2,3]),1)
55            Traceback (most recent call last):
56              ...
57            AssertionError
58
59        A couple of basic sanity checks:
60            >>> v2 = Voter([1,2]).hybridWith(Voter([3,2]),1)
61            >>> [round(u,5) for u in v2.hybridWith(v2,1)]
62            [4.0, 4.0]
63            >>> Voter([1,2,5]).hybridWith(Voter([-0.5,-1,0]),0.75)
64            (0.5, 1.0, 4.0)
65        """
66        assert len(self) == len(v2)
67        return self.copyWithUtils(
68            ((self[i] / sqrt(1 + w2**2)) + (w2 * v2[i] / sqrt(1 + w2**2))) for i in range(len(self))
69        )
70
71    def copyWithUtils(self, utils):
72        """create a new voter with attrs as self and given utils.
73
74        This version is a stub, since this voter class has no attrs."""
75        return self.__class__(utils)
76
77    def to_dataframe(self, voter_id=None, **kwargs):
78        """Return this voter's candidate utilities as a tidy DataFrame."""
79        from .dataframe import voter_to_dataframe
80
81        return voter_to_dataframe(self, voter_id=voter_id, **kwargs)
82
83    def mutantChild(self, muteWeight):
84        """Returns a copy hybridized with a random voter of weight muteWeight.
85
86        Should remain standard normal:
87            >>> v100 = Voter.rand(100)
88            >>> for i in range(30):
89            ...     v100 = v100.mutantChild(random.random())
90            ...
91            >>> -0.3 < mean(v100) < 0.3 #3 sigma
92            True
93            >>> 0.8 < std(v100) < 1.2 #meh that's roughly 3 sigma
94            True
95
96        """
97        return self.hybridWith(self.__class__.rand(len(self)), muteWeight)

A tuple of candidate utilities.

dataframe
16    @property
17    def dataframe(self):
18        """Return this voter's candidate utilities as a tidy DataFrame."""
19        return self.to_dataframe()

Return this voter's candidate utilities as a tidy DataFrame.

df
21    @property
22    def df(self):
23        """Alias for ``dataframe``."""
24        return self.dataframe

Alias for dataframe.

@classmethod
def rand(cls, ncand):
26    @classmethod
27    def rand(cls, ncand):
28        """Create a random voter with an independent standard normal
29        utility for each candidate.
30
31        ncand determines the number of candidates a voter should have
32        utilities for.
33            >>> [len(Voter.rand(i)) for i in list(range(5))]
34            [0, 1, 2, 3, 4]
35
36        utilities should be in a standard normal distribution
37            >>> v100 = Voter.rand(100)
38            >>> -0.5 < mean(v100) < 0.5
39            True
40            >>> 0.6 < std(v100) < 1.4
41            True
42        """
43        return cls(random.gauss(0, 1) for _ in range(ncand))

Create a random voter with an independent standard normal utility for each candidate.

ncand determines the number of candidates a voter should have utilities for.

[len(Voter.rand(i)) for i in list(range(5))] [0, 1, 2, 3, 4]

utilities should be in a standard normal distribution

v100 = Voter.rand(100) -0.5 < mean(v100) < 0.5 True 0.6 < std(v100) < 1.4 True

def hybridWith(self, v2, w2):
45    def hybridWith(self, v2, w2):
46        """Create a weighted average of two voters.
47
48        The weight of v1 is always 1; w2 is the weight of v2 relative to that.
49
50        If both are
51        standard normal to start with, the result will be standard normal too.
52
53        Length must be the same
54            >>> Voter([1,2]).hybridWith(Voter([1,2,3]),1)
55            Traceback (most recent call last):
56              ...
57            AssertionError
58
59        A couple of basic sanity checks:
60            >>> v2 = Voter([1,2]).hybridWith(Voter([3,2]),1)
61            >>> [round(u,5) for u in v2.hybridWith(v2,1)]
62            [4.0, 4.0]
63            >>> Voter([1,2,5]).hybridWith(Voter([-0.5,-1,0]),0.75)
64            (0.5, 1.0, 4.0)
65        """
66        assert len(self) == len(v2)
67        return self.copyWithUtils(
68            ((self[i] / sqrt(1 + w2**2)) + (w2 * v2[i] / sqrt(1 + w2**2))) for i in range(len(self))
69        )

Create a weighted average of two voters.

The weight of v1 is always 1; w2 is the weight of v2 relative to that.

If both are standard normal to start with, the result will be standard normal too.

Length must be the same

Voter([1,2]).hybridWith(Voter([1,2,3]),1) Traceback (most recent call last): ... AssertionError

A couple of basic sanity checks:

v2 = Voter([1,2]).hybridWith(Voter([3,2]),1) [round(u,5) for u in v2.hybridWith(v2,1)] [4.0, 4.0] Voter([1,2,5]).hybridWith(Voter([-0.5,-1,0]),0.75) (0.5, 1.0, 4.0)

def copyWithUtils(self, utils):
71    def copyWithUtils(self, utils):
72        """create a new voter with attrs as self and given utils.
73
74        This version is a stub, since this voter class has no attrs."""
75        return self.__class__(utils)

create a new voter with attrs as self and given utils.

This version is a stub, since this voter class has no attrs.

def to_dataframe(self, voter_id=None, **kwargs):
77    def to_dataframe(self, voter_id=None, **kwargs):
78        """Return this voter's candidate utilities as a tidy DataFrame."""
79        from .dataframe import voter_to_dataframe
80
81        return voter_to_dataframe(self, voter_id=voter_id, **kwargs)

Return this voter's candidate utilities as a tidy DataFrame.

def mutantChild(self, muteWeight):
83    def mutantChild(self, muteWeight):
84        """Returns a copy hybridized with a random voter of weight muteWeight.
85
86        Should remain standard normal:
87            >>> v100 = Voter.rand(100)
88            >>> for i in range(30):
89            ...     v100 = v100.mutantChild(random.random())
90            ...
91            >>> -0.3 < mean(v100) < 0.3 #3 sigma
92            True
93            >>> 0.8 < std(v100) < 1.2 #meh that's roughly 3 sigma
94            True
95
96        """
97        return self.hybridWith(self.__class__.rand(len(self)), muteWeight)

Returns a copy hybridized with a random voter of weight muteWeight.

Should remain standard normal:

v100 = Voter.rand(100) for i in range(30): ... v100 = v100.mutantChild(random.random()) ... -0.3 < mean(v100) < 0.3 #3 sigma True 0.8 < std(v100) < 1.2 #meh that's roughly 3 sigma True

allSystems = [[<Score.<locals>.Score0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Score.<locals>.Score0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Score.<locals>.Score0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Score.<locals>.Score0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<BulletyApprovalWith.<locals>.BulletyApproval object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Plurality object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Borda object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Irv object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<IrvPrime object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Schulze object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Rp object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<V321 object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Mav object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <LazyChooser object>, <ProbChooser object>]], [<Mj object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <LazyChooser object>, <ProbChooser object>]], [<IRNR object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]]]
def ballots_from_dataframe( ballots, voter_column='voter', candidate_column='candidate', value_column='ballot', candidate_prefix='candidate_'):
157def ballots_from_dataframe(
158    ballots,
159    voter_column="voter",
160    candidate_column="candidate",
161    value_column="ballot",
162    candidate_prefix="candidate_",
163):
164    """Convert tidy or wide ballot DataFrames back to method-ready ballots."""
165    if not isinstance(ballots, pd.DataFrame):
166        return ballots if type(ballots) is list else list(ballots)
167
168    if {voter_column, candidate_column, value_column} <= set(ballots.columns):
169        return (
170            ballots.pivot(index=voter_column, columns=candidate_column, values=value_column)
171            .sort_index()
172            .sort_index(axis=1)
173            .to_numpy()
174            .tolist()
175        )
176
177    candidate_columns = [
178        column for column in ballots.columns if str(column).startswith(candidate_prefix)
179    ]
180    if candidate_columns:
181        candidate_columns = sorted(
182            candidate_columns, key=lambda column: int(str(column).split("_")[-1])
183        )
184        return ballots[candidate_columns].to_numpy().tolist()
185
186    return ballots.to_numpy().tolist()

Convert tidy or wide ballot DataFrames back to method-ready ballots.

def ballots_to_dataframe( ballots, wide=False, method=None, voter_column='voter', candidate_column='candidate', value_column='ballot', candidate_prefix='candidate_') -> pandas.DataFrame:
118def ballots_to_dataframe(
119    ballots,
120    wide=False,
121    method=None,
122    voter_column="voter",
123    candidate_column="candidate",
124    value_column="ballot",
125    candidate_prefix="candidate_",
126) -> pd.DataFrame:
127    """Return ballots as a tidy or wide DataFrame."""
128    if wide:
129        rows = []
130        for voter_id, ballot in enumerate(ballots):
131            row = {
132                voter_column: voter_id,
133                **{
134                    f"{candidate_prefix}{candidate}": value
135                    for candidate, value in enumerate(ballot)
136                },
137            }
138            if method is not None:
139                row["method"] = str(method)
140            rows.append(row)
141        return pd.DataFrame(rows)
142
143    rows = []
144    for voter_id, ballot in enumerate(ballots):
145        for candidate, value in enumerate(ballot):
146            row = {
147                voter_column: voter_id,
148                candidate_column: candidate,
149                value_column: value,
150            }
151            if method is not None:
152                row["method"] = str(method)
153            rows.append(row)
154    return pd.DataFrame(rows)

Return ballots as a tidy or wide DataFrame.

baseRuns = [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]
beHon = <Chooser object>
beStrat = <Chooser object>
beX = <Chooser object>
def biasedMediaFor(biaser=<function biaserAround.<locals>.biaser>, numerator=1):
163def biasedMediaFor(biaser=biaserAround(1), numerator=1):
164    """
165    if numerator is 1:
166    0, 0, -1/2, -2/3, -3/4....
167    if numerator is 1.5:
168        0,0,-.25, -.5, -.625, -.7
169    numerator shouldn't be over 2 unless you want strangeness.
170
171
172    """
173
174    def biasedMedia(standings, tally=None):
175        if tally is None:
176            tally = defaultdict(int)
177        bias = biaser(standings) if callable(biaser) else biaser
178        result = standings[:2] + [
179            (standing - bias + numerator * (bias / max(i + 2, 1)))
180            for i, standing in enumerate(standings[2:])
181        ]
182
183        tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1
184        return result
185
186    return biasedMedia

if numerator is 1: 0, 0, -1/2, -2/3, -3/4.... if numerator is 1.5: 0,0,-.25, -.5, -.625, -.7 numerator shouldn't be over 2 unless you want strangeness.

def biaserAround(scale):
140def biaserAround(scale):
141    def biaser(standings):
142        return scale * std(standings, ddof=1)
143
144    return biaser
def fuzzyMediaFor(biaser=<function biaserAround.<locals>.biaser>):
151def fuzzyMediaFor(biaser=biaserAround(1)):
152    def fuzzyMedia(standings, tally=None):
153        if tally is None:
154            tally = defaultdict(int)
155        bias = biaser(standings) if callable(biaser) else biaser
156        result = [s + random.gauss(0, bias) for s in standings]
157        tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1
158        return result
159
160    return fuzzyMedia
markMethods = [[<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Srv.<locals>.Srv0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Score.<locals>.Score0to object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<V321 object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<BulletyApprovalWith.<locals>.BulletyApproval object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Irv object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]], [<Plurality object>, [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>]]]
medianRuns = [<OssChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <ProbChooser object>, <LazyChooser object>, <ProbChooser object>]
def orderOf(standings):
147def orderOf(standings):
148    return [i for i, val in sorted(list(enumerate(standings)), key=lambda x: x[1], reverse=True)]
def read_results_csv(path) -> VseResults:
223def read_results_csv(path) -> "VseResults":
224    """Load a VSE result CSV written by ``CsvBatch.saveFile``."""
225    return VseResults.from_csv(path)

Load a VSE result CSV written by CsvBatch.saveFile.

def rows_to_dataframe( rows: Iterable[dict] | pandas.DataFrame | VseResults, copy=True) -> pandas.DataFrame:
59def rows_to_dataframe(rows: Iterable[dict] | pd.DataFrame | VseResults, copy=True) -> pd.DataFrame:
60    """Convert simulation rows, a DataFrame, or ``VseResults`` to a DataFrame."""
61    return to_dataframe(rows, copy=copy)

Convert simulation rows, a DataFrame, or VseResults to a DataFrame.

def run_simulation( model, methods, nvot, ncand, niter, baseName=None, media=<function truth>, seed=None, force=False):
208def run_simulation(
209    model,
210    methods,
211    nvot,
212    ncand,
213    niter,
214    baseName=None,
215    media=truth,
216    seed=None,
217    force=False,
218):
219    """Run a simulation and return a pandas-backed ``VseResults`` object."""
220    batch = CsvBatch(
221        model,
222        methods,
223        nvot=nvot,
224        ncand=ncand,
225        niter=niter,
226        baseName=baseName,
227        media=media,
228        seed=seed,
229        force=force,
230    )
231    return batch.results

Run a simulation and return a pandas-backed VseResults object.

def run_simulation_dataframe(*args, **kwargs):
234def run_simulation_dataframe(*args, **kwargs):
235    """Run a simulation and return its result rows as a pandas DataFrame."""
236    return run_simulation(*args, **kwargs).dataframe

Run a simulation and return its result rows as a pandas DataFrame.

def scores_to_dataframe( scores, method=None, candidate_column='candidate', value_column='score') -> pandas.DataFrame:
189def scores_to_dataframe(
190    scores,
191    method=None,
192    candidate_column="candidate",
193    value_column="score",
194) -> pd.DataFrame:
195    """Return candidate-level method scores as a DataFrame."""
196    rows = [
197        {
198            candidate_column: candidate,
199            value_column: score,
200        }
201        for candidate, score in enumerate(scores)
202    ]
203    frame = pd.DataFrame(rows)
204    if method is not None:
205        frame.insert(0, "method", str(method))
206    return frame

Return candidate-level method scores as a DataFrame.

def skewedMediaFor(biaser):
189def skewedMediaFor(biaser):
190    """
191
192    [0, -1/3, -2/3, -1]
193    """
194
195    def skewedMedia(standings, tally=None):
196        if tally is None:
197            tally = defaultdict(int)
198        bias = biaser(standings) if callable(biaser) else biaser
199        result = [
200            (standing - bias * i / (len(standings) - 1)) for i, standing in enumerate(standings)
201        ]
202
203        tally["changed"] += 0 if orderOf(result)[:2] == orderOf(standings)[:2] else 1
204        return result
205
206    return skewedMedia

[0, -1/3, -2/3, -1]

def summarize_vse( rows: Iterable[dict] | pandas.DataFrame | VseResults, group_by=('method', 'chooser'), sort_by='mean_vse', ascending=False) -> pandas.DataFrame:
209def summarize_vse(
210    rows: Iterable[dict] | pd.DataFrame | VseResults,
211    group_by=DEFAULT_GROUP_BY,
212    sort_by="mean_vse",
213    ascending=False,
214) -> pd.DataFrame:
215    """Summarize VSE scores by method, chooser, or another grouping."""
216    return VseResults(rows_to_dataframe(rows, copy=False)).summarize(
217        group_by=group_by,
218        sort_by=sort_by,
219        ascending=ascending,
220    )

Summarize VSE scores by method, chooser, or another grouping.

def to_dataframe(data, copy=True, **kwargs) -> pandas.DataFrame:
47def to_dataframe(data, copy=True, **kwargs) -> pd.DataFrame:
48    """Convert VSE objects, result rows, or records to a pandas DataFrame."""
49    if isinstance(data, VseResults):
50        return data.to_dataframe(copy=copy)
51    if isinstance(data, pd.DataFrame):
52        return data.copy() if copy else data
53    dataframe_method = getattr(data, "to_dataframe", None)
54    if callable(dataframe_method):
55        return _call_dataframe_method(dataframe_method, copy=copy, **kwargs)
56    return pd.DataFrame(data, **kwargs)

Convert VSE objects, result rows, or records to a pandas DataFrame.

def topNMediaFor(n):
133def topNMediaFor(n):
134    def topNMedia(standings, tally=None):
135        return list(standings[:n]) + [min(standings)] * (len(standings) - n)
136
137    return topNMedia
def truth(standings, tally=None):
129def truth(standings, tally=None):
130    return standings
def uniquify(seq):
28def uniquify(seq):
29    # order preserving
30    checked = []
31    for e in seq:
32        if e not in checked:
33            checked.append(e)
34    return checked
def voter_to_dataframe( voter, voter_id=None, voter_column='voter', candidate_column='candidate', value_column='utility') -> pandas.DataFrame:
64def voter_to_dataframe(
65    voter,
66    voter_id=None,
67    voter_column="voter",
68    candidate_column="candidate",
69    value_column="utility",
70) -> pd.DataFrame:
71    """Return one voter's candidate utilities as a tidy DataFrame."""
72    metadata = _voter_metadata(voter)
73    rows = []
74    for candidate, value in enumerate(voter):
75        row = {candidate_column: candidate, value_column: value, **metadata}
76        if voter_id is not None:
77            row[voter_column] = voter_id
78        rows.append(row)
79    return pd.DataFrame(rows)

Return one voter's candidate utilities as a tidy DataFrame.

def voters_to_dataframe( voters, wide=False, voter_column='voter', candidate_column='candidate', value_column='utility', candidate_prefix='candidate_') -> pandas.DataFrame:
 82def voters_to_dataframe(
 83    voters,
 84    wide=False,
 85    voter_column="voter",
 86    candidate_column="candidate",
 87    value_column="utility",
 88    candidate_prefix="candidate_",
 89) -> pd.DataFrame:
 90    """Return voter utilities as a tidy or wide DataFrame."""
 91    if wide:
 92        rows = []
 93        for voter_id, voter in enumerate(voters):
 94            row = {
 95                voter_column: voter_id,
 96                **{
 97                    f"{candidate_prefix}{candidate}": value for candidate, value in enumerate(voter)
 98                },
 99                **_voter_metadata(voter),
100            }
101            rows.append(row)
102        return pd.DataFrame(rows)
103
104    rows = []
105    for voter_id, voter in enumerate(voters):
106        rows.extend(
107            voter_to_dataframe(
108                voter,
109                voter_id=voter_id,
110                voter_column=voter_column,
111                candidate_column=candidate_column,
112                value_column=value_column,
113            ).to_dict("records")
114        )
115    return pd.DataFrame(rows)

Return voter utilities as a tidy or wide DataFrame.