vse-sim 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- compat.py +47 -0
- dataClasses.py +354 -0
- debugDump.py +11 -0
- methods.py +1158 -0
- mydecorators.py +193 -0
- sodaTest.py +429 -0
- stratFunctions.py +205 -0
- tests.py +23 -0
- voterModels.py +404 -0
- vse.py +179 -0
- vse_sim/__init__.py +105 -0
- vse_sim/compat.py +14 -0
- vse_sim/data_classes.py +23 -0
- vse_sim/methods.py +35 -0
- vse_sim/simulation.py +12 -0
- vse_sim/strategies.py +35 -0
- vse_sim/voter_models.py +35 -0
- vse_sim-0.1.0.dist-info/METADATA +171 -0
- vse_sim-0.1.0.dist-info/RECORD +22 -0
- vse_sim-0.1.0.dist-info/WHEEL +5 -0
- vse_sim-0.1.0.dist-info/licenses/LICENSE +20 -0
- vse_sim-0.1.0.dist-info/top_level.txt +11 -0
compat.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Small compatibility helpers shared across the simulation modules."""
|
|
2
|
+
|
|
3
|
+
from numbers import Number
|
|
4
|
+
|
|
5
|
+
from numpy import ceil as numpy_ceil
|
|
6
|
+
from numpy import floor as numpy_floor
|
|
7
|
+
from numpy import mean as numpy_mean
|
|
8
|
+
from numpy import median as numpy_median
|
|
9
|
+
from numpy import sqrt as numpy_sqrt
|
|
10
|
+
from numpy import std as numpy_std
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def as_builtin_scalar(value):
|
|
14
|
+
"""Convert NumPy scalar values to their Python built-in equivalents."""
|
|
15
|
+
try:
|
|
16
|
+
return value.item()
|
|
17
|
+
except (AttributeError, ValueError):
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ceil(*args, **kwargs):
|
|
22
|
+
return as_builtin_scalar(numpy_ceil(*args, **kwargs))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def floor(*args, **kwargs):
|
|
26
|
+
return as_builtin_scalar(numpy_floor(*args, **kwargs))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mean(*args, **kwargs):
|
|
30
|
+
return as_builtin_scalar(numpy_mean(*args, **kwargs))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def median(*args, **kwargs):
|
|
34
|
+
return as_builtin_scalar(numpy_median(*args, **kwargs))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sqrt(*args, **kwargs):
|
|
38
|
+
return as_builtin_scalar(numpy_sqrt(*args, **kwargs))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def std(*args, **kwargs):
|
|
42
|
+
return as_builtin_scalar(numpy_std(*args, **kwargs))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def isnum(x):
|
|
46
|
+
"""Return whether ``x`` is an instance of a numeric type."""
|
|
47
|
+
return isinstance(x, Number)
|
dataClasses.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from compat import isnum, mean
|
|
4
|
+
from mydecorators import autoassign, decorator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VseOneRun:
|
|
8
|
+
@autoassign
|
|
9
|
+
def __init__(self, result, tallyItems, strat):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VseMethodRun:
|
|
14
|
+
@autoassign
|
|
15
|
+
def __init__(self, method, choosers, results):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
####data holders for output
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SideTally(defaultdict):
|
|
24
|
+
"""Used for keeping track of how many voters are being strategic, etc.
|
|
25
|
+
|
|
26
|
+
DO NOT use plain +; for this class, it is equivalent to +=, but less readable.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__(int)
|
|
32
|
+
|
|
33
|
+
def initKeys(self, chooser):
|
|
34
|
+
try:
|
|
35
|
+
self.keyList = chooser.allTallyKeys()
|
|
36
|
+
except AttributeError:
|
|
37
|
+
try:
|
|
38
|
+
self.keyList = list(chooser)
|
|
39
|
+
except TypeError:
|
|
40
|
+
self.keyList = []
|
|
41
|
+
self.initKeys = staticmethod(lambda x: x) # don't do it again
|
|
42
|
+
|
|
43
|
+
def serialize(self):
|
|
44
|
+
try:
|
|
45
|
+
return [self[key] for key in self.keyList]
|
|
46
|
+
except AttributeError:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
def fullSerialize(self):
|
|
50
|
+
if not hasattr(self, "keyList"):
|
|
51
|
+
return [self[key] for key in self.keys()]
|
|
52
|
+
return [self[key] for key in self.keyList]
|
|
53
|
+
|
|
54
|
+
def itemList(self):
|
|
55
|
+
try:
|
|
56
|
+
kl = self.keyList
|
|
57
|
+
return [(k, self[k]) for k in kl] + [(k, self[k]) for k in self.keys() if k not in kl]
|
|
58
|
+
except AttributeError:
|
|
59
|
+
return list(self.items())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Tallies(list):
|
|
63
|
+
"""Used (ONCE) as an enumerator, gives an inexhaustible flow of SideTally objects.
|
|
64
|
+
After that, use as list to see those objects.
|
|
65
|
+
|
|
66
|
+
>>> ts = Tallies()
|
|
67
|
+
>>> for i, j in zip(ts, [5,4,3]):
|
|
68
|
+
... i[j] += j
|
|
69
|
+
...
|
|
70
|
+
>>> [t.serialize() for t in ts]
|
|
71
|
+
[[], [], [], []]
|
|
72
|
+
>>> [t.fullSerialize() for t in ts]
|
|
73
|
+
[[5], [4], [3], []]
|
|
74
|
+
>>> [t.initKeys([k]) for (t,k) in zip(ts,[6,4,3])]
|
|
75
|
+
[None, None, None]
|
|
76
|
+
>>> [t.serialize() for t in ts]
|
|
77
|
+
[[0], [4], [3], []]
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __iter__(self):
|
|
81
|
+
if getattr(self, "used", False):
|
|
82
|
+
return super().__iter__()
|
|
83
|
+
self.used = True
|
|
84
|
+
return self._generated_tallies()
|
|
85
|
+
|
|
86
|
+
def __eq__(self, other):
|
|
87
|
+
if not isinstance(other, Tallies):
|
|
88
|
+
return super().__eq__(other)
|
|
89
|
+
return super().__eq__(other) and getattr(self, "used", False) == getattr(
|
|
90
|
+
other, "used", False
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _generated_tallies(self):
|
|
94
|
+
while True:
|
|
95
|
+
tally = SideTally()
|
|
96
|
+
self.append(tally)
|
|
97
|
+
yield tally
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
##Election Methods
|
|
101
|
+
class Method:
|
|
102
|
+
"""Base class for election methods. Holds some of the duct tape."""
|
|
103
|
+
|
|
104
|
+
def __str__(self):
|
|
105
|
+
return self.__class__.__name__
|
|
106
|
+
|
|
107
|
+
def results(self, ballots, isHonest=False, **kwargs):
|
|
108
|
+
"""Combines ballots into results. Override for comparative
|
|
109
|
+
methods.
|
|
110
|
+
|
|
111
|
+
Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index.
|
|
112
|
+
|
|
113
|
+
Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index.
|
|
114
|
+
|
|
115
|
+
Test for subclasses, makes no sense to test this method in the abstract base class.
|
|
116
|
+
"""
|
|
117
|
+
if type(ballots) is not list:
|
|
118
|
+
ballots = list(ballots)
|
|
119
|
+
return list(map(self.candScore, zip(*ballots)))
|
|
120
|
+
|
|
121
|
+
@staticmethod # cls is provided explicitly, not through binding
|
|
122
|
+
def honBallot(cls, utils):
|
|
123
|
+
"""Takes utilities and returns an honest ballot"""
|
|
124
|
+
raise NotImplementedError(f"{cls} needs honBallot")
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def winner(results):
|
|
128
|
+
"""Simply find the winner once scores are already calculated. Override for
|
|
129
|
+
ranked methods.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
>>> Method().winner([1,2,3,2,-100])
|
|
133
|
+
2
|
|
134
|
+
>>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6
|
|
135
|
+
True
|
|
136
|
+
"""
|
|
137
|
+
winScore = max(result for result in results if isnum(result))
|
|
138
|
+
winners = [cand for (cand, score) in enumerate(results) if score == winScore]
|
|
139
|
+
return random.choice(winners)
|
|
140
|
+
|
|
141
|
+
def honBallotFor(self, voters):
|
|
142
|
+
"""This is where you would do any setup necessary and create an honBallot
|
|
143
|
+
function. But the base version just returns the honBallot function."""
|
|
144
|
+
return self.honBallot
|
|
145
|
+
|
|
146
|
+
def dummyBallotFor(self, polls):
|
|
147
|
+
"""Returns a (function which takes utilities and returns a dummy ballot)
|
|
148
|
+
for the given "polling" info."""
|
|
149
|
+
return lambda cls, utilities, stratTally: utilities
|
|
150
|
+
|
|
151
|
+
def resultsFor(self, voters, chooser, tally=None, **kwargs):
|
|
152
|
+
"""create ballots and get results.
|
|
153
|
+
|
|
154
|
+
Again, test on subclasses.
|
|
155
|
+
"""
|
|
156
|
+
if tally is None:
|
|
157
|
+
tally = SideTally()
|
|
158
|
+
tally.initKeys(chooser)
|
|
159
|
+
return dict(
|
|
160
|
+
results=self.results(
|
|
161
|
+
[chooser(self.__class__, voter, tally) for voter in voters], **kwargs
|
|
162
|
+
),
|
|
163
|
+
chooser=chooser.__name__,
|
|
164
|
+
tally=tally,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def multiResults(self, voters, chooserFuns=(), media=(lambda x, t: x), checkStrat=True):
|
|
168
|
+
"""Runs two base elections: first with honest votes, then
|
|
169
|
+
with strategic results based on the first results (filtered by
|
|
170
|
+
the media). Then, runs a series of elections using each chooserFun
|
|
171
|
+
in chooserFuns to select the votes for each voter.
|
|
172
|
+
|
|
173
|
+
Returns a tuple of (honResults, stratResults, ...). The stratresults
|
|
174
|
+
are based on common polling information, which is given by media(honresults).
|
|
175
|
+
"""
|
|
176
|
+
from stratFunctions import OssChooser
|
|
177
|
+
|
|
178
|
+
honTally = SideTally()
|
|
179
|
+
self.__class__.extraEvents = {}
|
|
180
|
+
hon = self.resultsFor(voters, self.honBallotFor(voters), honTally, isHonest=True)
|
|
181
|
+
|
|
182
|
+
stratTally = SideTally()
|
|
183
|
+
|
|
184
|
+
polls = media(hon["results"], stratTally)
|
|
185
|
+
winner, _w, target, _t = self.stratTargetFor(sorted(enumerate(polls), key=lambda x: -x[1]))
|
|
186
|
+
|
|
187
|
+
strat = self.resultsFor(voters, self.stratBallotFor(polls), stratTally)
|
|
188
|
+
|
|
189
|
+
ossTally = SideTally()
|
|
190
|
+
oss = self.resultsFor(voters, self.ballotChooserFor(OssChooser()), ossTally)
|
|
191
|
+
ossWinner = oss["results"].index(max(oss["results"]))
|
|
192
|
+
ossTally["worked"] += 1 if ossWinner == target else (0 if ossWinner == winner else -1)
|
|
193
|
+
|
|
194
|
+
smart = dict(
|
|
195
|
+
results=(hon["results"] if ossTally["worked"] == 1 else oss["results"]),
|
|
196
|
+
chooser="smartOss",
|
|
197
|
+
tally=SideTally(),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
extraTallies = Tallies()
|
|
201
|
+
results = [strat, oss, smart] + [
|
|
202
|
+
self.resultsFor(voters, self.ballotChooserFor(chooserFun), aTally)
|
|
203
|
+
for (chooserFun, aTally) in zip(chooserFuns, extraTallies)
|
|
204
|
+
]
|
|
205
|
+
return [(hon["results"], hon["chooser"], list(self.__class__.extraEvents.items()))] + [
|
|
206
|
+
(r["results"], r["chooser"], r["tally"].itemList()) for r in results
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
def vseOn(self, voters, chooserFuns=(), **args):
|
|
210
|
+
"""Finds honest and strategic voter satisfaction efficiency (VSE)
|
|
211
|
+
for this method on the given electorate.
|
|
212
|
+
"""
|
|
213
|
+
multiResults = self.multiResults(voters, chooserFuns, **args)
|
|
214
|
+
utils = voters.socUtils
|
|
215
|
+
best = max(utils)
|
|
216
|
+
rand = mean(utils)
|
|
217
|
+
|
|
218
|
+
# import pprint
|
|
219
|
+
# pprint.pprint(multiResults)
|
|
220
|
+
vses = VseMethodRun(
|
|
221
|
+
self.__class__,
|
|
222
|
+
chooserFuns,
|
|
223
|
+
[
|
|
224
|
+
VseOneRun(
|
|
225
|
+
[(utils[self.winner(result)] - rand) / (best - rand)],
|
|
226
|
+
tally,
|
|
227
|
+
chooser,
|
|
228
|
+
)
|
|
229
|
+
for (result, chooser, tally) in multiResults[0]
|
|
230
|
+
],
|
|
231
|
+
)
|
|
232
|
+
vses.extraEvents = multiResults[1]
|
|
233
|
+
return vses
|
|
234
|
+
|
|
235
|
+
def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args):
|
|
236
|
+
multiResults = self.multiResults(voters, chooserFuns, **args)
|
|
237
|
+
utils = voters.socUtils
|
|
238
|
+
best = max(utils)
|
|
239
|
+
rand = mean(utils)
|
|
240
|
+
rows = []
|
|
241
|
+
nvot = len(voters)
|
|
242
|
+
for result, chooser, tallyItems in multiResults:
|
|
243
|
+
row = {
|
|
244
|
+
"eid": eid,
|
|
245
|
+
"emodel": emodel,
|
|
246
|
+
"ncand": cands,
|
|
247
|
+
"nvot": nvot,
|
|
248
|
+
"best": best,
|
|
249
|
+
"rand": rand,
|
|
250
|
+
"method": str(self),
|
|
251
|
+
"chooser": chooser, # .getName(),
|
|
252
|
+
"util": utils[self.winner(result)],
|
|
253
|
+
"vse": (utils[self.winner(result)] - rand) / (best - rand),
|
|
254
|
+
}
|
|
255
|
+
# print(tallyItems)
|
|
256
|
+
for i, (k, v) in enumerate(tallyItems):
|
|
257
|
+
# print("Result: tally ",i,k,v)
|
|
258
|
+
row[f"tallyName{str(i)}"] = str(k)
|
|
259
|
+
row[f"tallyVal{str(i)}"] = str(v)
|
|
260
|
+
rows.append(row)
|
|
261
|
+
return rows
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def ballotChooserFor(chooserFun):
|
|
265
|
+
"""Takes a chooserFun; returns a ballot chooser using that chooserFun"""
|
|
266
|
+
|
|
267
|
+
def ballotChooser(cls, voter, tally):
|
|
268
|
+
return getattr(voter, f"{cls.__name__}_{chooserFun(cls, voter, tally)}")
|
|
269
|
+
|
|
270
|
+
ballotChooser.__name__ = chooserFun.getName()
|
|
271
|
+
return ballotChooser
|
|
272
|
+
|
|
273
|
+
def stratTarget2(self, places):
|
|
274
|
+
((frontId, frontResult), (targId, targResult)) = places[:2]
|
|
275
|
+
return (frontId, frontResult, targId, targResult)
|
|
276
|
+
|
|
277
|
+
def stratTarget3(self, places):
|
|
278
|
+
((frontId, frontResult), (targId, targResult)) = places[:3:2]
|
|
279
|
+
return (frontId, frontResult, targId, targResult)
|
|
280
|
+
|
|
281
|
+
stratTargetFor = stratTarget2
|
|
282
|
+
|
|
283
|
+
def stratBallotFor(self, polls):
|
|
284
|
+
"""Returns a (function which takes utilities and returns a strategic ballot)
|
|
285
|
+
for the given "polling" info."""
|
|
286
|
+
|
|
287
|
+
places = sorted(enumerate(polls), key=lambda x: -x[1]) # from high to low
|
|
288
|
+
# print("places",places)
|
|
289
|
+
(frontId, frontResult, targId, targResult) = self.stratTargetFor(places)
|
|
290
|
+
n = len(polls)
|
|
291
|
+
|
|
292
|
+
@rememberBallots
|
|
293
|
+
def stratBallot(cls, voter):
|
|
294
|
+
stratGap = voter[targId] - voter[frontId]
|
|
295
|
+
ballot = [0] * len(voter)
|
|
296
|
+
isStrat = stratGap > 0
|
|
297
|
+
extras = cls.fillStratBallot(
|
|
298
|
+
voter,
|
|
299
|
+
polls,
|
|
300
|
+
places,
|
|
301
|
+
n,
|
|
302
|
+
stratGap,
|
|
303
|
+
ballot,
|
|
304
|
+
frontId,
|
|
305
|
+
frontResult,
|
|
306
|
+
targId,
|
|
307
|
+
targResult,
|
|
308
|
+
)
|
|
309
|
+
result = dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
|
|
310
|
+
if extras:
|
|
311
|
+
result.update(extras)
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
return stratBallot
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@decorator
|
|
318
|
+
def rememberBallot(fun):
|
|
319
|
+
"""A decorator for a function of the form xxxBallot(cls, voter)
|
|
320
|
+
which memoizes the vote onto the voter in an attribute named <methName>_xxx
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def getAndRemember(cls, voter, tally=None):
|
|
324
|
+
ballot = fun(cls, voter)
|
|
325
|
+
setattr(voter, f"{cls.__name__}_{fun.__name__[:-6]}", ballot)
|
|
326
|
+
return ballot
|
|
327
|
+
|
|
328
|
+
getAndRemember.__name__ = fun.__name__
|
|
329
|
+
getAndRemember.allTallyKeys = lambda: []
|
|
330
|
+
return getAndRemember
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@decorator
|
|
334
|
+
def rememberBallots(fun):
|
|
335
|
+
"""A decorator for a function of the form xxxBallot(cls, voter)
|
|
336
|
+
which memoizes the vote onto the voter in an attribute named <methName>_xxx
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
def getAndRemember(cls, voter, tally=None):
|
|
340
|
+
ballots = fun(cls, voter)
|
|
341
|
+
for bType, ballot in ballots.items():
|
|
342
|
+
setattr(voter, f"{cls.__name__}_{bType}", ballot)
|
|
343
|
+
|
|
344
|
+
return ballots[fun.__name__[:-6]] # leave off the "...Ballot"
|
|
345
|
+
|
|
346
|
+
getAndRemember.__name__ = fun.__name__
|
|
347
|
+
getAndRemember.allTallyKeys = lambda: []
|
|
348
|
+
return getAndRemember
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class CandidateWithCount:
|
|
352
|
+
def __init__(self, c=[], v=0):
|
|
353
|
+
self.candidate = c
|
|
354
|
+
self.votes = v
|