Statistics
| Branch: | Revision:

root / stv.py @ 361b3942

History | View | Annotate | Download (12.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 = '%(levelname)s %(message)s'
43
LOG_MESSAGE = "{action} {desc}"
44

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

    
54
class Ballot:
55
    """A ballot class for Single Transferable Voting.
56

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

63
    """
64

    
65
    candidates = []
66
    weights = [1.0]
67
    current_preference = 0
68
    _value = 1.0
69

    
70
    def __init__(self, candidates=[]):
71
        self.candidates = filter(None, candidates)
72

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

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

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

92
    """
93

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

122
    Redistributes the ballots currently allocated to the selected
123
    candidate. The ballots are redistributed with the given weight.
124
    The total ballot allocation is given by the allocated dict. The current
125
    vote count is given by vote_count and is adjusted according to the
126
    redistribution.
127
    
128
    """
129

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

    
138
    for ballot in allocated[selected]:
139
        reallocated = False
140
        i = ballot.current_preference + 1
141
        while not reallocated and i < len(ballot.candidates):
142
            recipient = ballot.candidates[i]
143
            if recipient in hopefuls:
144
                ballot.current_preference = i
145
                ballot.add_weight(weight)
146
                current_value = ballot.get_value()
147
                if recipient in allocated:
148
                    allocated[recipient].append(ballot)
149
                else:
150
                    allocated[recipient] = [ballot]
151
                if recipient in vote_count:
152
                    vote_count[recipient] += current_value
153
                else:
154
                    vote_count[recipient] = current_value
155
                vote_count[selected] -= current_value
156
                reallocated = True
157
                if (selected, recipient, current_value) in moves:
158
                    moves[(selected, recipient, current_value)].append(ballot)
159
                else:
160
                    moves[(selected, recipient, current_value)] = [ballot]
161
                transferred.append(ballot)
162
            else:
163
                i += 1
164
    for move, ballots in moves.iteritems():
165
        times = len(ballots)
166
        description =  "from {0} to {1} {2}*{3}={4}".format(move[0],
167
                                                            move[1],
168
                                                            times,
169
                                                            move[2],
170
                                                            times * move[2])
171
        logger.debug(LOG_MESSAGE.format(action=Action.TRANSFER,
172
                                        desc=description))
173
    allocated[selected][:] = [x for x in allocated[selected]
174
                              if x not in transferred ]
175
    
176
def count_stv(ballots, droop, seats, rnd_gen=None):
177
    """Performs a SVT vote for the given ballots and number of seats.
178
    
179
    """
180
    
181
    allocated = {} # The allocation of ballots to candidates
182
    vote_count = {} # A hash of ballot counts, indexed by candidates
183
    candidates = [] # All candidates
184
    elected = [] # The candidates that have been elected
185
    hopefuls = [] # The candidates that may be elected
186

    
187
    seed()
188

    
189
    if droop:
190
            threshold = int(1 + (len(ballots) / (seats + 1.0)))
191
    else:
192
        threshold = int(math.ceil(1 + len(ballots) / (seats + 1.0)))
193

    
194
    logger = logging.getLogger(SVT_LOGGER)
195
    logger.info(LOG_MESSAGE.format(action=Action.THRESHOLD,
196
                                   desc=threshold))
197
    
198
    # Do initial count
199
    for ballot in ballots:
200
        selected = ballot.candidates[0]
201
        for candidate in ballot.candidates:
202
            if candidate not in candidates:
203
                candidates.append(candidate)
204
                vote_count[candidate] = 0
205
        if selected in allocated:
206
            allocated[selected].append(ballot)
207
        else:
208
            allocated[selected] = [ballot]
209
        vote_count[selected] += 1
210

    
211
    # In the beginning, all candidates are hopefuls
212
    hopefuls = [x for x in candidates]
213

    
214
    # Start rounds
215
    current_round = 1
216
    num_elected = len(elected)
217
    num_hopefuls = len(hopefuls)
218
    while num_elected < seats and num_hopefuls > 0:
219
        logger.info(LOG_MESSAGE.format(action=Action.COUNT_ROUND,
220
                                       desc=current_round))
221
        # Log count
222
        description  = ';'.join(map(lambda x: "{0} = {1}".format(x,
223
                                                                 vote_count[x]),
224
                                    hopefuls))
225
        logger.info(LOG_MESSAGE.format(action=Action.COUNT,
226
                                       desc=description))
227
        hopefuls_sorted = sorted(hopefuls, key=vote_count.get, reverse=True )
228
        # If there is a surplus try to redistribute the best candidate's votes
229
        # according to their next preferences
230
        surplus = vote_count[hopefuls_sorted[0]] - threshold
231
        remaining_seats = seats - num_elected
232
        if surplus >= 0 or num_hopefuls <= remaining_seats:
233
            best_candidate = randomly_select_first(hopefuls_sorted,
234
                                                   key=vote_count.get,
235
                                                   action=Action.ELECT,
236
                                                   random_generator=rnd_gen)
237
            hopefuls.remove(best_candidate)
238
            elected.append((best_candidate, current_round,
239
                            vote_count[best_candidate]))
240
            logger.info(LOG_MESSAGE.format(action=Action.ELECT,
241
                                           desc=best_candidate
242
                                           + " = " + str(vote_count[best_candidate])))
243
            if surplus > 0:
244
                # Calculate the weight for this round
245
                weight = float(surplus) / vote_count[best_candidate]
246
                # Find the next eligible preference for each one of the ballots
247
                # cast for the candidate, and transfer the vote to that
248
                # candidate with its value adjusted by the correct weight.
249
                redistribute_ballots(best_candidate, hopefuls, allocated,
250
                                     weight, vote_count)
251
        # If there is no surplus, take the least hopeful candidate
252
        # (i.e., the hopeful candidate with the less votes) and
253
        # redistribute that candidate's votes.
254
        else:
255
            hopefuls_sorted.reverse()
256
            worst_candidate = randomly_select_first(hopefuls_sorted,
257
                                                    key=vote_count.get,
258
                                                    action=Action.ELIMINATE,
259
                                                    random_generator=rnd_gen)
260
            hopefuls.remove(worst_candidate)
261
            logger.info(LOG_MESSAGE.format(action=Action.ELIMINATE,
262
                                           desc=worst_candidate
263
                                           + " = " + str(vote_count[worst_candidate])))
264
            redistribute_ballots(worst_candidate, hopefuls, allocated, 1.0,
265
                                 vote_count)
266
            
267
        current_round += 1
268
        num_hopefuls = len(hopefuls)
269
        num_elected = len(elected)
270

    
271
    return elected, vote_count
272

    
273
if __name__ == "__main__":
274
    parser = argparse.ArgumentParser(description='Perform STV')
275
    parser.add_argument('-b', '--ballots', default='sys.stdin',
276
                        dest='ballots_file', help='input ballots file')
277
    parser.add_argument('-n', '--not_droop', action="store_false",
278
                        dest='droop', help="don't use droop quota")
279
    parser.add_argument('-s', '--seats', type=int, default=0,
280
                        dest='seats', help='number of seats')
281
    parser.add_argument('-r', '--random', nargs='*',
282
                        dest='random', help='random selection results')
283
    parser.add_argument('-l', '--loglevel', default=logging.INFO,
284
                        dest='loglevel', help='logging level')
285
    args = parser.parse_args()
286
    logging.basicConfig(format=LOGGER_FORMAT)
287
    logging.getLogger(SVT_LOGGER).setLevel(args.loglevel)
288
    ballots = []
289
    ballots_file = sys.stdin
290
    if args.ballots_file != 'sys.stdin':
291
        ballots_file = open(args.ballots_file, 'U')
292
    ballots_reader = csv.reader(ballots_file, delimiter=',',
293
                                quotechar='"',
294
                                skipinitialspace = True)
295
    for ballot in ballots_reader:
296
        ballots.append(Ballot(ballot))
297

    
298
    if args.seats == 0:
299
        args.seats = len(ballots) / 2
300
    (elected, vote_count) = count_stv(ballots, args.droop, args.seats,
301
                                      args.random)
302

    
303
    print "Results:"
304
    for result in elected:
305
        print result