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