Statistics
| Branch: | Revision:

root / stv.py @ 50eaf441

History | View | Annotate | Download (17.5 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above
11
#     copyright notice, this list of conditions and the following
12
#     disclaimer in the documentation and/or other materials provided
13
#     with the distribution.
14
#
15
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS''
16
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
17
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
18
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
19
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
22
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
25
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26
# SUCH DAMAGE.
27
#
28
# The views and conclusions contained in the software and
29
# documentation are those of the authors and should not be interpreted
30
# as representing official policies, either expressed or implied, of
31
# GRNET S.A.
32

    
33
from operator import mul, itemgetter
34
from random import random, seed
35
import logging
36
import sys
37
import math
38
import csv
39
import argparse
40

    
41
SVT_LOGGER = 'SVT'
42
LOGGER_FORMAT = '%(message)s'
43
LOG_MESSAGE = "{action} {desc}"
44

    
45
class Action:
46
    COUNT_ROUND = "@ROUND"
47
    TRANSFER = ">TRANSFER"
48
    ELIMINATE = "-ELIMINATE"
49
    QUOTA ="!QUOTA"
50
    ELECT = "+ELECT"
51
    COUNT = ".COUNT"
52
    ZOMBIES = "~ZOMBIES"
53
    RANDOM = "*RANDOM"
54
    THRESHOLD = "^THRESHOLD"
55

    
56
class Ballot:
57
    """A ballot class for Single Transferable Voting.
58

59
    The ballot class contains an ordered list of candidates (in
60
    decreasing order of preference) and an ordered list of weights
61
    (new weights are added to the front of the list). The index of the
62
    current preference (for the first count and subsequent rounds)
63
    is also kept.
64

65
    """
66

    
67
    candidates = []
68
    weights = [1.0]
69
    current_preference = 0
70
    _value = 1.0
71

    
72
    def __init__(self, candidates=[]):
73
        self.candidates = candidates
74

    
75
    def add_weight(self, weight):
76
        self.weights.insert(0, weight)
77
        self._value *= weight
78

    
79
    def get_value(self):
80
        return self._value
81
    
82
def randomly_select_first(sequence, key, action, random_generator=None):
83
    """Selects the first item of equals in a sorted sequence of items.
84

85
    For the given sorted sequence, returns the first item if it
86
    is different than the second; if there are ties so that there
87
    are items with equal values, it randomly selects among those items.
88
    The value of each item in the sequence is provided by applying the
89
    function key to the item. The action parameter indicates the context
90
    in which the random selection takes place (election or elimination).
91
    random_generator, if given, is the function that produces the random
92
    selection.
93

94
    """
95

    
96
    first_value = key(sequence[0])
97
    collected = []
98
    for item in sequence:
99
        if key(item) == first_value:
100
            collected.append(item)
101
        else:
102
            break
103
    index = 0
104
    selected = collected[index]
105
    num_eligibles = len(collected)
106
    if (num_eligibles > 1):
107
        if random_generator is None:
108
            index = int(random() * num_eligibles)
109
            selected = collected[index]
110
        else:
111
            if not random_generator:
112
                print "Missing value for random selection among ", collected
113
                sys.exit(1)
114
            selected = random_generator.pop(0)
115
        logger = logging.getLogger(SVT_LOGGER)
116
        description = "{0} from {1} to {2}".format(selected, collected, action)
117
        logger.info(LOG_MESSAGE.format(action=Action.RANDOM, desc=description))
118
    return selected
119
        
120
    
121
def redistribute_ballots(selected, weight, hopefuls, allocated, vote_count):
122
    """Redistributes the ballots from selected to the hopefuls.
123

124
    Redistributes the ballots currently allocated to the selected
125
    candidate. The ballots are redistributed with the given weight.
126
    The total ballot allocation is given by the allocated map, which
127
    is modified accordingly. The current vote count is given by
128
    vote_count and is adjusted according to the redistribution.
129
    
130
    """
131

    
132
    logger = logging.getLogger(SVT_LOGGER)
133
    transferred = []
134
    # Keep a hash of ballot moves for logging purposes.
135
    # Keys are a tuple of the form (from_recipient, to_recipient, value)
136
    # where value is the current value of the ballot. Each tuple points
137
    # to the ballot being moved.
138
    moves = {}
139

    
140
    for ballot in allocated[selected]:
141
        reallocated = False
142
        i = ballot.current_preference + 1
143
        while not reallocated and i < len(ballot.candidates):
144
            recipient = ballot.candidates[i]
145
            if recipient in hopefuls:
146
                ballot.current_preference = i
147
                ballot.add_weight(weight)
148
                current_value = ballot.get_value()
149
                if recipient in allocated:
150
                    allocated[recipient].append(ballot)
151
                else:
152
                    allocated[recipient] = [ballot]
153
                if recipient in vote_count:
154
                    vote_count[recipient] += current_value
155
                else:
156
                    vote_count[recipient] = current_value
157
                vote_count[selected] -= current_value
158
                reallocated = True
159
                if (selected, recipient, current_value) in moves:
160
                    moves[(selected, recipient, current_value)].append(ballot)
161
                else:
162
                    moves[(selected, recipient, current_value)] = [ballot]
163
                transferred.append(ballot)
164
            else:
165
                i += 1
166
    for move, ballots in moves.iteritems():
167
        times = len(ballots)
168
        description =  "from {0} to {1} {2}*{3}={4}".format(move[0],
169
                                                            move[1],
170
                                                            times,
171
                                                            move[2],
172
                                                            times * move[2])
173
        logger.debug(LOG_MESSAGE.format(action=Action.TRANSFER,
174
                                        desc=description))
175
    allocated[selected][:] = [x for x in allocated[selected]
176
                              if x not in transferred ]
177

    
178
def elect_reject(candidate, vote_count, constituencies, quota_limit,
179
                 current_round, elected, rejected, constituencies_elected):
180
    """Elects or rejects the candidate, based on quota restrictions.
181

182
    If there are no quota limits, the candidate is elected. If there
183
    are quota limits, the candidate is either elected or rejected, if
184
    the quota limits are exceeded. The elected and rejected lists
185
    are modified accordingly, as well as the constituencies_elected map.
186

187
    Returns true if the candidate is elected, false otherwise.
188
    """
189
    
190
    
191
    logger = logging.getLogger(SVT_LOGGER)
192
    quota_exceeded = False
193
    # If there is a quota limit, check if it is exceeded
194
    if quota_limit > 0 and candidate in constituencies:
195
        current_constituency = constituencies[candidate]
196
        if constituencies_elected[current_constituency] >= quota_limit:
197
            quota_exceeded = True
198
    # If the quota limit has been exceeded, reject the candidate
199
    if quota_exceeded:
200
        rejected.append((candidate, current_round, vote_count[candidate]))
201
        d = candidate + " = " + str(vote_count[candidate])
202
        msg = LOG_MESSAGE.format(action=Action.QUOTA, desc=d)
203
        logger.info(msg)
204
        return False
205
    # Otherwise, elect the candidate
206
    else:
207
        elected.append((candidate, current_round, vote_count[candidate]))
208
        if constituencies:
209
            current_constituency = constituencies[candidate]
210
            constituencies_elected[current_constituency] += 1
211
        d = candidate + " = " + str(vote_count[candidate])
212
        msg = LOG_MESSAGE.format(action=Action.ELECT, desc=d)
213
        logger.info(msg)
214
        return True
215

    
216
def count_description(vote_count, candidates):
217
    """Returns a string with count results.
218

219
    The string is of the form of {0} = {1} separated by ; where each {0}
220
    is a candidate and each {1} is the corresponding vote count.
221
    """
222
    
223
    return  ';'.join(map(lambda x: "{0} = {1}".format(x, vote_count[x]),
224
                         candidates))
225

    
226
   
227
def count_stv(ballots, seats, droop = True, constituencies = None,
228
              quota_limit = 0, rnd_gen=None):
229
    """Performs a STV vote for the given ballots and number of seats.
230

231
    If droop is true the election threshold is calculated according to the
232
    Droop quota:
233
            threshold = int(1 + (len(ballots) / (seats + 1.0)))
234
    otherwise it is calculated according to the following formula:
235
            threshold = int(math.ceil(1 + len(ballots) / (seats + 1.0)))
236
    The constituencies argument is a map of candidates to constituencies, if
237
    any. The quota_limit, if different than zero, is the limit of candidates
238
    that can be elected by a constituency.
239
    """
240
    
241
    allocated = {} # The allocation of ballots to candidates
242
    vote_count = {} # A hash of ballot counts, indexed by candidates
243
    candidates = [] # All candidates
244
    elected = [] # The candidates that have been elected
245
    hopefuls = [] # The candidates that may be elected
246
    # The candidates that have been eliminated because of low counts
247
    eliminated = []
248
    # The candidates that have been eliminated because of quota restrictions
249
    rejected = []
250
    # The number of candidates elected per constituency
251
    constituencies_elected = {}
252
    for constituency in constituencies.values():
253
        constituencies_elected[constituency] = 0
254

    
255
    seed()
256

    
257
    if droop:
258
        threshold = int(1 + (len(ballots) / (seats + 1.0)))
259
    else:
260
        threshold = int(math.ceil(1 + len(ballots) / (seats + 1.0)))
261

    
262
    logger = logging.getLogger(SVT_LOGGER)
263
    logger.info(LOG_MESSAGE.format(action=Action.THRESHOLD,
264
                                   desc=threshold))
265
    
266
    # Do initial count
267
    for ballot in ballots:
268
        selected = ballot.candidates[0]
269
        for candidate in ballot.candidates:
270
            if candidate not in candidates:
271
                candidates.append(candidate)
272
                vote_count[candidate] = 0
273
            if candidate not in allocated:
274
                allocated[candidate] = []
275
        allocated[selected].append(ballot)
276
        vote_count[selected] += 1
277

    
278
    # In the beginning, all candidates are hopefuls
279
    hopefuls = [x for x in candidates]
280

    
281
    # Start rounds
282
    current_round = 1
283
    num_elected = len(elected)
284
    num_hopefuls = len(hopefuls)
285
    while num_elected < seats and num_hopefuls > 0:
286
        # Log round
287
        logger.info(LOG_MESSAGE.format(action=Action.COUNT_ROUND,
288
                                       desc=current_round))
289
        # Log count
290
        description  = count_description(vote_count, hopefuls)
291
       
292
        logger.info(LOG_MESSAGE.format(action=Action.COUNT,
293
                                       desc=description))
294
        hopefuls_sorted = sorted(hopefuls, key=vote_count.get, reverse=True )
295
        # If there is a surplus record it so that we can try to
296
        # redistribute the best candidate's votes according to their
297
        # next preferences
298
        surplus = vote_count[hopefuls_sorted[0]] - threshold
299
        # If there is either a candidate with surplus votes, or
300
        # there are hopeful candidates beneath the threshold.
301
        if surplus >= 0 or num_hopefuls <= (seats - num_elected):
302
            best_candidate = randomly_select_first(hopefuls_sorted,
303
                                                   key=vote_count.get,
304
                                                   action=Action.ELECT,
305
                                                   random_generator=rnd_gen)
306
            if best_candidate not in hopefuls:
307
                print "Not a valid candidate: ",best_candidate
308
                sys.exit(1)
309
            hopefuls.remove(best_candidate)
310
            was_elected = elect_reject(best_candidate, vote_count,
311
                                       constituencies, quota_limit,
312
                                       current_round, 
313
                                       elected, rejected,
314
                                       constituencies_elected)
315
            if not was_elected:
316
                redistribute_ballots(best_candidate, 1.0, hopefuls, allocated,
317
                                     vote_count)
318
            if surplus > 0:
319
                # Calculate the weight for this round
320
                weight = float(surplus) / vote_count[best_candidate]
321
                # Find the next eligible preference for each one of the ballots
322
                # cast for the candidate, and transfer the vote to that
323
                # candidate with its value adjusted by the correct weight.
324
                redistribute_ballots(best_candidate, weight, hopefuls,
325
                                     allocated, vote_count)
326
        # If nobody can get elected, take the least hopeful candidate
327
        # (i.e., the hopeful candidate with the less votes) and
328
        # redistribute that candidate's votes.
329
        else:
330
            hopefuls_sorted.reverse()
331
            worst_candidate = randomly_select_first(hopefuls_sorted,
332
                                                    key=vote_count.get,
333
                                                    action=Action.ELIMINATE,
334
                                                    random_generator=rnd_gen)
335
            hopefuls.remove(worst_candidate)
336
            eliminated.append(worst_candidate)
337
            d = worst_candidate + " = " + str(vote_count[worst_candidate])
338
            msg = LOG_MESSAGE.format(action=Action.ELIMINATE, desc=d)
339
            logger.info(msg)
340
            redistribute_ballots(worst_candidate, 1.0, hopefuls, allocated,
341
                                 vote_count)
342
            
343
        current_round += 1
344
        num_hopefuls = len(hopefuls)
345
        num_elected = len(elected)
346

    
347
    # If there is either a candidate with surplus votes, or
348
    # there are hopeful candidates beneath the threshold.
349
    while (seats - num_elected) > 0 and len(eliminated) > 0:
350
        logger.info(LOG_MESSAGE.format(action=Action.COUNT_ROUND,
351
                                       desc=current_round))
352
        description  = count_description(vote_count, eliminated)
353
        
354
        logger.info(LOG_MESSAGE.format(action=Action.ZOMBIES,
355
                                       desc=description))
356

    
357
        best_candidate = eliminated.pop()
358
        elect_reject(best_candidate, vote_count, constituencies,
359
                     quota_limit, current_round,
360
                     elected, rejected, constituencies_elected)
361
        current_round += 1
362

    
363
    return elected, vote_count
364

    
365
if __name__ == "__main__":
366
    parser = argparse.ArgumentParser(description='Perform STV')
367
    parser.add_argument('-b', '--ballots', default='sys.stdin',
368
                        dest='ballots_file', help='input ballots file')
369
    parser.add_argument('-n', '--not_droop', action="store_false",
370
                        dest='droop', help="don't use droop quota")
371
    parser.add_argument('-s', '--seats', type=int, default=0,
372
                        dest='seats', help='number of seats')
373
    parser.add_argument('-c', '--constituencies',
374
                        dest='constituencies_file',
375
                        help='input constituencies file')
376
    parser.add_argument('-q', '--quota', type=int, default=0,
377
                        dest='quota', help='constituency quota')
378
    parser.add_argument('-r', '--random', nargs='*',
379
                        dest='random', help='random selection results')
380
    parser.add_argument('-l', '--loglevel', default=logging.INFO,
381
                        dest='loglevel', help='logging level')
382
    args = parser.parse_args()
383

    
384
    stream_handler = logging.StreamHandler(stream=sys.stdout)
385
    logger = logging.getLogger(SVT_LOGGER)
386
    logger.setLevel(args.loglevel)
387
    logger.addHandler(stream_handler)
388

    
389
    ballots = []
390
    ballots_file = sys.stdin
391
    if args.ballots_file != 'sys.stdin':
392
        ballots_file = open(args.ballots_file, 'U')
393
    ballots_reader = csv.reader(ballots_file, delimiter=',',
394
                                quotechar='"',
395
                                skipinitialspace=True)
396
    for ballot in ballots_reader:
397
        ballots.append(Ballot(ballot))
398

    
399
    if args.seats == 0:
400
        args.seats = len(ballots) / 2
401

    
402
    constituencies = {}
403
    if args.constituencies_file:
404
        constituencies_file = open(args.constituencies_file, 'U')
405
        constituencies_reader = csv.reader(constituencies_file,
406
                                           delimiter=',',
407
                                           quotechar='"',
408
                                           skipinitialspace=True)
409
        constituency_id = 0
410
        for constituency in constituencies_reader:
411
            for candidate in constituency:
412
                constituencies[candidate] = constituency_id
413
            constituency_id += 1
414
        
415
    (elected, vote_count) = count_stv(ballots, args.seats, args.droop,
416
                                      constituencies,
417
                                      args.quota,
418
                                      args.random)
419

    
420
    print "Results:"
421
    for result in elected:
422
        print result