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]
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)]
Inherited Members
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
Inherited Members
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]
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
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.
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
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
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.
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.
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]
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.
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
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)
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
Inherited Members
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]
Inherited Members
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.
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
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.
Inherited Members
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.
Inherited Members
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
Inherited Members
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]
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.
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
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.
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.
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
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)
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.
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.
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