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()
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.
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.
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.
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
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
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
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
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}")