vote_tally.vote_tally

Determine a board for a committee from a set of votes

  1#! usr/bin/env python
  2"""
  3Determine a board for a committee from a set of votes
  4"""
  5import sys
  6import argparse
  7import logging
  8from math import floor
  9import pandas as pd
 10
 11logging.basicConfig(
 12    format="\x1b[33;20m[%(levelname)s] %(name)s:%(funcName)s:%(lineno)d\033[0m %(message)s",
 13    level=logging.INFO)
 14LOG = logging.getLogger("vote_tally")
 15
 16def winner(total_votes,min_votes_req,show_order=False):
 17    """
 18    # Tells you the elected candidate for a role
 19
 20    This gives the name of the candidate who reached the minimum
 21    number of votes necessary to be elected, or the bool False
 22    if nobody has won yet.
 23
 24    Parameters
 25    ----------
 26    total_votes : pandas dataframe
 27        Dataframe of candidates and the amount of votes they have
 28    min_votes_req : int
 29        The minimum number of votes required to be elected
 30
 31    Returns
 32    -------
 33    candidate : string
 34        The name of the elected candidate.
 35        Returns with the bool False if there is not yet a winner.
 36    """
 37    winners = total_votes[total_votes['votes']>=min_votes_req]['candidate'].values
 38    if len(winners) == 0:
 39        return False
 40    if len(winners) == 1:
 41        if show_order:
 42            LOG.info(f"Final candidates:\n{total_votes[total_votes['votes']>0]}")
 43        return winners[0]
 44    sys.exit('You have two winners:'+str(winners))
 45
 46def tidy(votes):
 47    """
 48    # Tidies the ballots
 49
 50    Ensures every ballot starts at 1 and goes up from there
 51    Also ensure no ranks are negative
 52
 53    Parameters
 54    ----------
 55    votes : pandas dataframe
 56        Every column is a candidate. Every row is one ballot and
 57        the preferential order for their votes.
 58
 59    Returns
 60    -------
 61    votes : pandas dataframe
 62        Every column is a candidate. Every row is one ballot and
 63        the preferential order for their votes.
 64        Now all ballots are properly formatted.
 65    """
 66    votes[votes<0] = 0
 67    for _, row in votes.iterrows():
 68        if max(row) > 1:
 69            row_values = list(row[row>0].values)
 70            row_values.sort()
 71            replacements = range(1,len(row_values)+1)
 72            for row_value, replacement in zip(row_values,replacements):
 73                row[row==row_value] = replacement
 74    votes[votes<0] = 0
 75    return votes
 76
 77def remove_lowest(votes,show_order=False):
 78    """
 79    # Removes the least-votes candidate and redistributes
 80
 81    Using the single-transferrable-vote method, the candidate
 82    with the fewest first-place votes is removed from the
 83    election and the votes for them redistributed.
 84
 85    We find those ballots which list said candidate
 86    first, then redistribute those votes by subtracting 1 from
 87    every element in the vote order until there is a new 1st
 88    ranked vote per ballot. Also, all votes for the removed
 89    candidated are removed.
 90
 91    Thus, for those affected ballots, their no. 2 choice
 92    becomes their no. 1 choice, and so on.
 93
 94    Parameters
 95    ----------
 96
 97    total_votes : pandas dataframe
 98        Dataframe of candidates and the amount of votes they have
 99    votes : pandas dataframe
100        Every column is a candidate. Every row is one ballot and
101        the preferential order for their votes.
102
103    Returns
104    -------
105    votes : pandas dataframe
106        Every column is a candidate. Every row is one ballot and
107        the preferential order for their votes.
108        Now the worst-performing candidate has been removed.
109
110    """
111
112    candidates = list(votes.columns)
113    removable_candidates = candidates
114
115    for rank in range(1,max(votes.values[0])+1):
116
117        total_votes = count_total_votes(votes[removable_candidates],rank=rank)
118        if max(total_votes['votes'])==0:
119            continue
120
121        lowest_votes = min(total_votes[total_votes['votes']>0]['votes'])
122        lowest_candidates = total_votes[total_votes['votes']==lowest_votes]['candidate'].values
123
124        if len(lowest_candidates) == 1:
125            votes[votes[lowest_candidates[0]]==1]-=1
126            votes[lowest_candidates[0]] = 0
127            votes = tidy(votes)
128            if show_order:
129                LOG.info(f"Removed: {lowest_candidates[0]}")
130            return votes
131
132        removable_candidates = lowest_candidates
133    if show_order:
134        LOG.info(f"Final candidates:\n{total_votes[total_votes['votes']>0]}")
135    sys.exit('No more votes to redistribute but no winners')
136
137def count_total_votes(votes,rank=1):
138    """
139    # Count how many votes each candidate has
140
141    Parameters
142    ----------
143    votes : pandas dataframe
144        Every column is a candidate. Every row is one ballot and
145        the preferential order for their votes.
146
147    Returns
148    -------
149    total_votes : pandas dataframe
150        Dataframe of candidates and the amount of votes they have
151    """
152    candidates = list(votes.columns)
153    votes_count = []
154    for candidate in candidates:
155        votes_count.append(len(votes[votes[candidate]==rank]))
156    total_votes = pd.DataFrame({'candidate':candidates,'votes':votes_count})
157    return total_votes
158
159def first_algorithm(votes,show_order=False,people=1):
160    """
161    # Calculates the elected candidate for a role
162
163    Parameters
164    ----------
165    votes : pandas dataframe
166        Every column is a candidate. Every row is one ballot and
167        the preferential order for their votes.
168
169    Returns
170    -------
171    winner : string
172        The succesfully elected candidate
173    """
174
175    votes = verify(votes) # remove invalid votes
176    n_voters = len(votes)
177    min_votes_req = int(floor(n_voters/(people+1))+1)
178    total_votes = count_total_votes(votes)
179
180    LOG.debug(f"initial votes = \n{votes}")
181    LOG.debug(f"initial total_votes = \n{total_votes}")
182
183    while not winner(total_votes,min_votes_req,show_order=show_order):
184        votes = remove_lowest(votes,show_order=show_order)
185        total_votes = count_total_votes(votes)
186        LOG.debug(f"current votes = \n{votes}")
187        LOG.debug(f"current total_votes = \n{total_votes}")
188    return winner(total_votes,min_votes_req)
189
190def read_votes(input_file):
191    """
192    # Read votes in from a csv file into a data frame
193    """
194
195    votes_df = pd.read_csv(input_file, dtype=float)
196
197    return votes_df
198
199def verify(votes):
200    """
201    Removes any invalid votes from the collected votes.
202    Valid votes include every number is sequential,
203    between 1 and number of votes, no repeated numbers and
204    a vote made for every candidate.
205
206    Parameters
207    ----------
208    votes : pandas dataframe
209        Every column is a candidate. Every row is one ballot and
210        the preferential order for their votes.
211
212    Returns
213    -------
214    verified_votes : pandas dataframe
215        Dataframe of only votes which are valid
216    """
217
218    n_votes = votes.shape[0] # Number of votes
219    n_candids = votes.shape[1] # Number of candiates
220    idx_drop = [] # Indices that are invalid and will be dropped
221
222    for i in range(n_votes):
223        # Votes for voter i
224        v_i = votes[i:i+1].values[0]
225
226        # Check that voter has voted for each candidate
227        # Each vote must be unique
228        for j in range(1,n_candids+1):
229            # Vote is invalid, break
230            if not j in v_i:
231                idx_drop.append(i)
232                break
233
234    # Drop invalid votes and return only valid votes
235    verified_votes = votes.drop(index=(idx_drop))
236    verified_votes = verified_votes.astype(int)
237    LOG.info("Dropped "+str(len(idx_drop))+" invalid ballot(s)")
238    LOG.debug("ID(s) of Invalid ballots are: "+str(idx_drop))
239
240    return verified_votes
241
242def main():
243    """
244    # Reports the winner of the election from test data
245    """
246    parser = argparse.ArgumentParser(
247            description='Determine the winner of an election')
248    parser.add_argument('-i','--input',
249        type=str,
250        default='data/test_votes.csv',
251        help='Input location of votes csv file')
252    parser.add_argument('-v','--verbose',action='store_true',help='Enable debug logging')
253    parser.add_argument('--order',action='store_true',help='Show candidate order')
254    args = parser.parse_args()
255
256    # configure logger
257    if args.verbose:
258        LOG.setLevel(logging.DEBUG)
259
260    votes_df = read_votes(args.input)
261    output = first_algorithm(votes_df,show_order=args.order)
262    LOG.info(f"The winner is: {output}")
263
264if __name__ == "__main__":
265    main()
def winner(total_votes, min_votes_req, show_order=False):
17def winner(total_votes,min_votes_req,show_order=False):
18    """
19    # Tells you the elected candidate for a role
20
21    This gives the name of the candidate who reached the minimum
22    number of votes necessary to be elected, or the bool False
23    if nobody has won yet.
24
25    Parameters
26    ----------
27    total_votes : pandas dataframe
28        Dataframe of candidates and the amount of votes they have
29    min_votes_req : int
30        The minimum number of votes required to be elected
31
32    Returns
33    -------
34    candidate : string
35        The name of the elected candidate.
36        Returns with the bool False if there is not yet a winner.
37    """
38    winners = total_votes[total_votes['votes']>=min_votes_req]['candidate'].values
39    if len(winners) == 0:
40        return False
41    if len(winners) == 1:
42        if show_order:
43            LOG.info(f"Final candidates:\n{total_votes[total_votes['votes']>0]}")
44        return winners[0]
45    sys.exit('You have two winners:'+str(winners))

Tells you the elected candidate for a role

This gives the name of the candidate who reached the minimum number of votes necessary to be elected, or the bool False if nobody has won yet.

Parameters
  • total_votes (pandas dataframe): Dataframe of candidates and the amount of votes they have
  • min_votes_req (int): The minimum number of votes required to be elected
Returns
  • candidate (string): The name of the elected candidate. Returns with the bool False if there is not yet a winner.
def tidy(votes):
47def tidy(votes):
48    """
49    # Tidies the ballots
50
51    Ensures every ballot starts at 1 and goes up from there
52    Also ensure no ranks are negative
53
54    Parameters
55    ----------
56    votes : pandas dataframe
57        Every column is a candidate. Every row is one ballot and
58        the preferential order for their votes.
59
60    Returns
61    -------
62    votes : pandas dataframe
63        Every column is a candidate. Every row is one ballot and
64        the preferential order for their votes.
65        Now all ballots are properly formatted.
66    """
67    votes[votes<0] = 0
68    for _, row in votes.iterrows():
69        if max(row) > 1:
70            row_values = list(row[row>0].values)
71            row_values.sort()
72            replacements = range(1,len(row_values)+1)
73            for row_value, replacement in zip(row_values,replacements):
74                row[row==row_value] = replacement
75    votes[votes<0] = 0
76    return votes

Tidies the ballots

Ensures every ballot starts at 1 and goes up from there Also ensure no ranks are negative

Parameters
  • votes (pandas dataframe): Every column is a candidate. Every row is one ballot and the preferential order for their votes.
Returns
  • votes (pandas dataframe): Every column is a candidate. Every row is one ballot and the preferential order for their votes. Now all ballots are properly formatted.
def remove_lowest(votes, show_order=False):
 78def remove_lowest(votes,show_order=False):
 79    """
 80    # Removes the least-votes candidate and redistributes
 81
 82    Using the single-transferrable-vote method, the candidate
 83    with the fewest first-place votes is removed from the
 84    election and the votes for them redistributed.
 85
 86    We find those ballots which list said candidate
 87    first, then redistribute those votes by subtracting 1 from
 88    every element in the vote order until there is a new 1st
 89    ranked vote per ballot. Also, all votes for the removed
 90    candidated are removed.
 91
 92    Thus, for those affected ballots, their no. 2 choice
 93    becomes their no. 1 choice, and so on.
 94
 95    Parameters
 96    ----------
 97
 98    total_votes : pandas dataframe
 99        Dataframe of candidates and the amount of votes they have
100    votes : pandas dataframe
101        Every column is a candidate. Every row is one ballot and
102        the preferential order for their votes.
103
104    Returns
105    -------
106    votes : pandas dataframe
107        Every column is a candidate. Every row is one ballot and
108        the preferential order for their votes.
109        Now the worst-performing candidate has been removed.
110
111    """
112
113    candidates = list(votes.columns)
114    removable_candidates = candidates
115
116    for rank in range(1,max(votes.values[0])+1):
117
118        total_votes = count_total_votes(votes[removable_candidates],rank=rank)
119        if max(total_votes['votes'])==0:
120            continue
121
122        lowest_votes = min(total_votes[total_votes['votes']>0]['votes'])
123        lowest_candidates = total_votes[total_votes['votes']==lowest_votes]['candidate'].values
124
125        if len(lowest_candidates) == 1:
126            votes[votes[lowest_candidates[0]]==1]-=1
127            votes[lowest_candidates[0]] = 0
128            votes = tidy(votes)
129            if show_order:
130                LOG.info(f"Removed: {lowest_candidates[0]}")
131            return votes
132
133        removable_candidates = lowest_candidates
134    if show_order:
135        LOG.info(f"Final candidates:\n{total_votes[total_votes['votes']>0]}")
136    sys.exit('No more votes to redistribute but no winners')

Removes the least-votes candidate and redistributes

Using the single-transferrable-vote method, the candidate with the fewest first-place votes is removed from the election and the votes for them redistributed.

We find those ballots which list said candidate first, then redistribute those votes by subtracting 1 from every element in the vote order until there is a new 1st ranked vote per ballot. Also, all votes for the removed candidated are removed.

Thus, for those affected ballots, their no. 2 choice becomes their no. 1 choice, and so on.

Parameters
  • total_votes (pandas dataframe): Dataframe of candidates and the amount of votes they have
  • votes (pandas dataframe): Every column is a candidate. Every row is one ballot and the preferential order for their votes.
Returns
  • votes (pandas dataframe): Every column is a candidate. Every row is one ballot and the preferential order for their votes. Now the worst-performing candidate has been removed.
def count_total_votes(votes, rank=1):
138def count_total_votes(votes,rank=1):
139    """
140    # Count how many votes each candidate has
141
142    Parameters
143    ----------
144    votes : pandas dataframe
145        Every column is a candidate. Every row is one ballot and
146        the preferential order for their votes.
147
148    Returns
149    -------
150    total_votes : pandas dataframe
151        Dataframe of candidates and the amount of votes they have
152    """
153    candidates = list(votes.columns)
154    votes_count = []
155    for candidate in candidates:
156        votes_count.append(len(votes[votes[candidate]==rank]))
157    total_votes = pd.DataFrame({'candidate':candidates,'votes':votes_count})
158    return total_votes

Count how many votes each candidate has

Parameters
  • votes (pandas dataframe): Every column is a candidate. Every row is one ballot and the preferential order for their votes.
Returns
  • total_votes (pandas dataframe): Dataframe of candidates and the amount of votes they have
def first_algorithm(votes, show_order=False, people=1):
160def first_algorithm(votes,show_order=False,people=1):
161    """
162    # Calculates the elected candidate for a role
163
164    Parameters
165    ----------
166    votes : pandas dataframe
167        Every column is a candidate. Every row is one ballot and
168        the preferential order for their votes.
169
170    Returns
171    -------
172    winner : string
173        The succesfully elected candidate
174    """
175
176    votes = verify(votes) # remove invalid votes
177    n_voters = len(votes)
178    min_votes_req = int(floor(n_voters/(people+1))+1)
179    total_votes = count_total_votes(votes)
180
181    LOG.debug(f"initial votes = \n{votes}")
182    LOG.debug(f"initial total_votes = \n{total_votes}")
183
184    while not winner(total_votes,min_votes_req,show_order=show_order):
185        votes = remove_lowest(votes,show_order=show_order)
186        total_votes = count_total_votes(votes)
187        LOG.debug(f"current votes = \n{votes}")
188        LOG.debug(f"current total_votes = \n{total_votes}")
189    return winner(total_votes,min_votes_req)

Calculates the elected candidate for a role

Parameters
  • votes (pandas dataframe): Every column is a candidate. Every row is one ballot and the preferential order for their votes.
Returns
  • winner (string): The succesfully elected candidate
def read_votes(input_file):
191def read_votes(input_file):
192    """
193    # Read votes in from a csv file into a data frame
194    """
195
196    votes_df = pd.read_csv(input_file, dtype=float)
197
198    return votes_df

Read votes in from a csv file into a data frame

def verify(votes):
200def verify(votes):
201    """
202    Removes any invalid votes from the collected votes.
203    Valid votes include every number is sequential,
204    between 1 and number of votes, no repeated numbers and
205    a vote made for every candidate.
206
207    Parameters
208    ----------
209    votes : pandas dataframe
210        Every column is a candidate. Every row is one ballot and
211        the preferential order for their votes.
212
213    Returns
214    -------
215    verified_votes : pandas dataframe
216        Dataframe of only votes which are valid
217    """
218
219    n_votes = votes.shape[0] # Number of votes
220    n_candids = votes.shape[1] # Number of candiates
221    idx_drop = [] # Indices that are invalid and will be dropped
222
223    for i in range(n_votes):
224        # Votes for voter i
225        v_i = votes[i:i+1].values[0]
226
227        # Check that voter has voted for each candidate
228        # Each vote must be unique
229        for j in range(1,n_candids+1):
230            # Vote is invalid, break
231            if not j in v_i:
232                idx_drop.append(i)
233                break
234
235    # Drop invalid votes and return only valid votes
236    verified_votes = votes.drop(index=(idx_drop))
237    verified_votes = verified_votes.astype(int)
238    LOG.info("Dropped "+str(len(idx_drop))+" invalid ballot(s)")
239    LOG.debug("ID(s) of Invalid ballots are: "+str(idx_drop))
240
241    return verified_votes

Removes any invalid votes from the collected votes. Valid votes include every number is sequential, between 1 and number of votes, no repeated numbers and a vote made for every candidate.

Parameters
  • votes (pandas dataframe): Every column is a candidate. Every row is one ballot and the preferential order for their votes.
Returns
  • verified_votes (pandas dataframe): Dataframe of only votes which are valid
def main():
243def main():
244    """
245    # Reports the winner of the election from test data
246    """
247    parser = argparse.ArgumentParser(
248            description='Determine the winner of an election')
249    parser.add_argument('-i','--input',
250        type=str,
251        default='data/test_votes.csv',
252        help='Input location of votes csv file')
253    parser.add_argument('-v','--verbose',action='store_true',help='Enable debug logging')
254    parser.add_argument('--order',action='store_true',help='Show candidate order')
255    args = parser.parse_args()
256
257    # configure logger
258    if args.verbose:
259        LOG.setLevel(logging.DEBUG)
260
261    votes_df = read_votes(args.input)
262    output = first_algorithm(votes_df,show_order=args.order)
263    LOG.info(f"The winner is: {output}")

Reports the winner of the election from test data