Edit on GitHub

vse_sim.voter_models

  1import random
  2
  3from scipy.stats import beta
  4
  5from .compat import as_builtin_scalar, mean, sqrt, std  # noqa: F401
  6from .decorators import autoassign, cached_property
  7
  8
  9class Voter(tuple):
 10    """A tuple of candidate utilities."""
 11
 12    def __new__(cls, utils=()):
 13        return super().__new__(cls, (as_builtin_scalar(util) for util in utils))
 14
 15    @property
 16    def dataframe(self):
 17        """Return this voter's candidate utilities as a tidy DataFrame."""
 18        return self.to_dataframe()
 19
 20    @property
 21    def df(self):
 22        """Alias for ``dataframe``."""
 23        return self.dataframe
 24
 25    @classmethod
 26    def rand(cls, ncand):
 27        """Create a random voter with an independent standard normal
 28        utility for each candidate.
 29
 30        ncand determines the number of candidates a voter should have
 31        utilities for.
 32            >>> [len(Voter.rand(i)) for i in list(range(5))]
 33            [0, 1, 2, 3, 4]
 34
 35        utilities should be in a standard normal distribution
 36            >>> v100 = Voter.rand(100)
 37            >>> -0.5 < mean(v100) < 0.5
 38            True
 39            >>> 0.6 < std(v100) < 1.4
 40            True
 41        """
 42        return cls(random.gauss(0, 1) for _ in range(ncand))
 43
 44    def hybridWith(self, v2, w2):
 45        """Create a weighted average of two voters.
 46
 47        The weight of v1 is always 1; w2 is the weight of v2 relative to that.
 48
 49        If both are
 50        standard normal to start with, the result will be standard normal too.
 51
 52        Length must be the same
 53            >>> Voter([1,2]).hybridWith(Voter([1,2,3]),1)
 54            Traceback (most recent call last):
 55              ...
 56            AssertionError
 57
 58        A couple of basic sanity checks:
 59            >>> v2 = Voter([1,2]).hybridWith(Voter([3,2]),1)
 60            >>> [round(u,5) for u in v2.hybridWith(v2,1)]
 61            [4.0, 4.0]
 62            >>> Voter([1,2,5]).hybridWith(Voter([-0.5,-1,0]),0.75)
 63            (0.5, 1.0, 4.0)
 64        """
 65        assert len(self) == len(v2)
 66        return self.copyWithUtils(
 67            ((self[i] / sqrt(1 + w2**2)) + (w2 * v2[i] / sqrt(1 + w2**2))) for i in range(len(self))
 68        )
 69
 70    def copyWithUtils(self, utils):
 71        """create a new voter with attrs as self and given utils.
 72
 73        This version is a stub, since this voter class has no attrs."""
 74        return self.__class__(utils)
 75
 76    def to_dataframe(self, voter_id=None, **kwargs):
 77        """Return this voter's candidate utilities as a tidy DataFrame."""
 78        from .dataframe import voter_to_dataframe
 79
 80        return voter_to_dataframe(self, voter_id=voter_id, **kwargs)
 81
 82    def mutantChild(self, muteWeight):
 83        """Returns a copy hybridized with a random voter of weight muteWeight.
 84
 85        Should remain standard normal:
 86            >>> v100 = Voter.rand(100)
 87            >>> for i in range(30):
 88            ...     v100 = v100.mutantChild(random.random())
 89            ...
 90            >>> -0.3 < mean(v100) < 0.3 #3 sigma
 91            True
 92            >>> 0.8 < std(v100) < 1.2 #meh that's roughly 3 sigma
 93            True
 94
 95        """
 96        return self.hybridWith(self.__class__.rand(len(self)), muteWeight)
 97
 98
 99class PersonalityVoter(Voter):
100    cluster_count = 0
101
102    def __init__(self, *args, **kw):
103        super().__init__()  # *args, **kw) #WTF, python?
104        self.cluster = self.__class__.cluster_count
105        self.__class__.cluster_count += 1
106        self.personality = random.gauss(0, 1)  # probably to be used for strategic propensity
107        # but in future, could be other clustering voter variability, such as media awareness
108
109    @classmethod
110    def resetClusters(cls):
111        cls.cluster_count = 0
112
113    def copyWithUtils(self, utils):
114        voter = super().copyWithUtils(utils)
115        voter.copyAttrsFrom(self)
116        return voter
117
118    def copyAttrsFrom(self, model):
119        self.personality = model.personality
120        self.cluster = model.cluster
121
122
123class Electorate(list):
124    """A list of voters.
125    Each voter is a list of candidate utilities"""
126
127    @property
128    def dataframe(self):
129        """Return voter utilities as a tidy DataFrame."""
130        return self.to_dataframe()
131
132    @property
133    def df(self):
134        """Alias for ``dataframe``."""
135        return self.dataframe
136
137    @property
138    def wide_dataframe(self):
139        """Return one row per voter with one utility column per candidate."""
140        return self.to_dataframe(wide=True)
141
142    @cached_property
143    def socUtils(self):
144        """Return mean utility across electorate for each candidate: their social utilities.
145
146        >>> e = Electorate([[1,2],[3,4]])
147        >>> e.socUtils
148        [2.0, 3.0]
149        """
150        return list(map(mean, zip(*self)))
151
152    def to_dataframe(self, wide=False, **kwargs):
153        """Return voter utilities as a tidy or wide DataFrame."""
154        from .dataframe import voters_to_dataframe
155
156        return voters_to_dataframe(self, wide=wide, **kwargs)
157
158
159class RandomModel:
160    """Empty base class for election models; that is, electorate factories.
161
162    >>> e4 = RandomModel()(4,3)
163    >>> [len(v) for v in e4]
164    [3, 3, 3, 3]
165    """
166
167    def __str__(self):
168        return self.__class__.__name__
169
170    def __call__(self, nvot, ncand, vType=PersonalityVoter):
171        return Electorate(vType.rand(ncand) for _ in range(nvot))
172
173    def to_dataframe(self, nvot, ncand, vType=PersonalityVoter, wide=False, **kwargs):
174        """Generate an electorate and return its utilities as a DataFrame."""
175        return self(nvot, ncand, vType=vType).to_dataframe(wide=wide, **kwargs)
176
177
178class DeterministicModel(RandomModel):
179    """Basically, a somewhat non-boring stub for testing.
180
181    >>> DeterministicModel(3)(4, 3)
182    [(0, 1, 2), (1, 2, 0), (2, 0, 1), (0, 1, 2)]
183    """
184
185    @autoassign
186    def __init__(self, modulo):
187        pass
188
189    def __call__(self, nvot, ncand, vType=PersonalityVoter):
190        return Electorate(vType((i + j) % self.modulo for i in range(ncand)) for j in range(nvot))
191
192
193class ReverseModel(RandomModel):
194    """Creates an even number of voters in two diametrically-opposed camps
195    (ie, opposite utilities for all candidates)
196
197    >>> e4 = ReverseModel()(4,3)
198    >>> [len(v) for v in e4]
199    [3, 3, 3, 3]
200    >>> e4[0].hybridWith(e4[3],1)
201    (0.0, 0.0, 0.0)
202    """
203
204    def __call__(self, nvot, ncand, vType=PersonalityVoter):
205        if nvot % 2:
206            raise ValueError
207        basevoter = vType.rand(ncand)
208        return Electorate(
209            ([basevoter] * (nvot // 2)) + ([vType(-q for q in basevoter)] * (nvot // 2))
210        )
211
212
213class QModel(RandomModel):
214    """Adds a quality dimension to a base model,
215    by generating an election and then hybridizing all voters
216    with a common quality vector.
217
218    Useful along with ReverseModel to create a poor-man's 2d model.
219
220    Basic structure
221        >>> random.seed(0)
222        >>> e4 = QModel(sqrt(3), RandomModel())(100,1)
223        >>> len(e4)
224        100
225        >>> len(e4.socUtils)
226        1
227
228    Reduces the standard deviation
229        >>> 0.4 < std(list(zip(e4))) < 0.6
230        True
231
232    """
233
234    @autoassign
235    def __init__(self, qWeight=0.5, baseModel=ReverseModel()):
236        pass
237
238    def __call__(self, nvot, ncand, vType=PersonalityVoter):
239        qualities = vType.rand(ncand)
240        return Electorate(
241            [v.hybridWith(qualities, self.qWeight) for v in self.baseModel(nvot, ncand, vType)]
242        )
243
244
245class PolyaModel(RandomModel):
246    """This creates electorates based on a Polya/Hoppe/Dirichlet model, with mutation.
247    You start with an "urn" of n=seedVoter voters from seedModel,
248     plus alpha "wildcard" voters. Then you draw a voter from the urn,
249     clone and mutate them, and put the original and clone back into the urn.
250     If you draw a "wildcard", use voterGen to make a new voter.
251    """
252
253    @autoassign
254    def __init__(self, seedVoters=2, alpha=1, seedModel=QModel(), mutantFactor=0.2):
255        pass
256
257    def __call__(self, nvot, ncand, vType=PersonalityVoter):
258        """Tests? Making statistical tests that would pass reliably is
259        a huge hassle. Sorry, maybe later.
260        """
261        vType.resetClusters()
262        election = self.seedModel(self.seedVoters, ncand, vType)
263        while len(election) < nvot:
264            i = random.randrange(len(election) + self.alpha)
265            if i < len(election):
266                election.append(election[i].mutantChild(self.mutantFactor))
267            else:
268                election.append(vType.rand(ncand))
269        return election
270
271
272class DimVoter(PersonalityVoter):
273    """A voter in an n-dimensional model."""
274
275    @classmethod
276    def fromDims(cls, v, e, caring=None):
277        if caring is None:
278            caring = [1] * len(v)
279            totCaring = e.totWeight
280        else:
281            totCaring = sum((c * w) ** 2 for c, w in zip(caring, e.dimWeights))
282        me = cls(
283            -sqrt(
284                sum(
285                    ((vd - cd) * w * cares) ** 2
286                    for (vd, cd, w, cares) in zip(v, c, e.dimWeights, caring)
287                )
288                / totCaring
289            )
290            for c in e.cands
291        )
292        me.copyAttrsFrom(v)
293        me.dims = v
294        me.elec = e
295        return me
296
297
298class DimElectorate(Electorate):
299    def asDims(self, v, *args):
300        return v
301
302    def fromDims(self, dimvoters, vType):
303        for v in dimvoters:
304            self.append(vType.fromDims(v, self))
305
306    def calcTotWeight(self):
307        self.totWeight = sum(w**2 for w in self.dimWeights)
308
309
310class DimModel(RandomModel):
311    """
312
313    >>> dm = DimModel(2,baseElectorate=DeterministicModel(3))
314    >>> dm(2,4)
315    [(-1.8439088914585775, -0.0, -1.0, -1.8439088914585775), (-1.2649110640673518, -1.0, -0.0, -1.2649110640673518)]
316    >>> dm.dimWeights
317    [1, 0.5]
318
319
320    """
321
322    builtElectorate = DimElectorate
323
324    @autoassign
325    def __init__(self, ndims=3, dimWeights=None, baseElectorate=RandomModel()):
326        if self.dimWeights is None:
327            self.dimWeights = [2 ** (-n) for n in range(ndims)]
328        assert len(self.dimWeights) == self.ndims
329
330    def __call__(self, nvot, ncand, vType=DimVoter):
331        elec = self.builtElectorate()
332        elec.dimWeights = self.dimWeights
333        return self.makeElectorate(elec, nvot, ncand, vType)
334
335    def makeElectorate(self, elec, nvot, ncand, vType):
336        elec.calcTotWeight()
337        votersncands = self.baseElectorate(nvot + ncand, len(elec.dimWeights), vType)
338        elec.base = [elec.asDims(v, i) for i, v in enumerate(votersncands[:nvot])]
339        elec.cands = [elec.asDims(v, nvot + i) for i, v in enumerate(votersncands[nvot:])]
340        elec.fromDims(elec.base, vType)
341        return elec
342
343
344def rbeta(a, b):
345    return lambda: beta.rvs(a, b)
346
347
348class KSElectorate(DimElectorate):
349    def chooseClusters(self, n, alpha, caring):
350        self.clusters = []
351        for i in range(n):
352            item = []
353            for c in range(self.numClusters):
354                r = (i + alpha) * random.random()
355                if r > i:
356                    item.append(self.numSubclusters[c])
357                    self.numSubclusters[c] += 1
358                else:
359                    item.append(self.clusters[int(r)][c])
360            self.clusters.append(item)
361        self.clusterMeans = []
362        self.clusterCaring = []
363        for c in range(self.numClusters):
364            subclusterMeans = []
365            subclusterCaring = []
366            for _ in range(self.numSubclusters[c]):
367                cares = caring()
368
369                subclusterMeans.append([random.gauss(0, sqrt(cares)) for _ in range(self.dcs[c])])
370
371                subclusterCaring.append(caring())
372            self.clusterMeans.append(subclusterMeans)
373            self.clusterCaring.append(subclusterCaring)
374
375    def asDims(self, v, i):
376        result = []
377        cares = []
378        for dim, c in enumerate(range(self.numClusters)):
379            clusterMean = self.clusterMeans[c][self.clusters[i][c]]
380            for m in clusterMean:
381                acare = self.clusterCaring[c][self.clusters[i][c]]
382                result.append(m + (v[dim] * sqrt(1 - acare)))
383                cares.append(acare)
384        v = PersonalityVoter(result)  # TODO: do personality right
385        v.cares = cares
386        return v
387
388    def fromDims(self, dimvoters, vType):
389        for v in dimvoters:
390            self.append(vType.fromDims(v, self, v.cares))
391
392
393class KSModel(DimModel):  # Kitchen sink
394    builtElectorate = KSElectorate
395    baseElectorate = RandomModel()
396
397    @autoassign
398    # dc = dimensional cluster; vc = voter cluster
399    def __init__(
400        self,
401        dcdecay=(1, 1),
402        dccut=0.2,
403        wcdecay=(1, 1),
404        wccut=0.2,
405        wcalpha=1,
406        vccaring=(3, 1.5),
407    ):
408        DimModel.__init__(self)
409
410    def __str__(self):
411        return "_".join(
412            str(x)
413            for x in (self.__class__.__name__, self.wcalpha)
414            + self.dcdecay
415            + self.wcdecay
416            + self.vccaring
417        )
418
419    def __call__(self, nvot, ncand, vType=DimVoter):
420        """Tests? Making statistical tests that would pass reliably is
421        a huge hassle. Sorry, maybe later.
422        """
423        vType.resetClusters()
424        e = self.builtElectorate()
425        e.dcs = []  # number of dimensions in each dc
426        e.dimWeights = []  # raw importance of each dimension, regardless of dc
427        clusterWeight = 1
428        while clusterWeight > self.dccut:
429            dimweight = clusterWeight
430            dimnum = 0
431            while dimweight > self.wccut:
432                e.dimWeights.append(dimweight)
433                dimnum += 1
434                dimweight *= beta.rvs(*self.wcdecay)
435            e.dcs.append(dimnum)
436            clusterWeight *= beta.rvs(*self.dcdecay)
437        e.numClusters = len(e.dcs)
438        e.numSubclusters = [0] * e.numClusters
439        e.chooseClusters(nvot + ncand, self.wcalpha, lambda: beta.rvs(*self.vccaring))
440        return self.makeElectorate(e, nvot, ncand, vType)
441
442
443__all__ = [
444    "DeterministicModel",
445    "DimElectorate",
446    "DimModel",
447    "DimVoter",
448    "Electorate",
449    "KSElectorate",
450    "KSModel",
451    "PersonalityVoter",
452    "PolyaModel",
453    "QModel",
454    "RandomModel",
455    "ReverseModel",
456    "Voter",
457    "rbeta",
458]
class DeterministicModel(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(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(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(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 KSElectorate(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(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 PersonalityVoter(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 PolyaModel(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 QModel(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(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 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

def rbeta(a, b):
345def rbeta(a, b):
346    return lambda: beta.rvs(a, b)