Skip to content

Instantly share code, notes, and snippets.

@Sam-Belliveau
Last active May 6, 2023 02:29
Show Gist options
  • Save Sam-Belliveau/943142dab814b674ecf6f6a852aba0cf to your computer and use it in GitHub Desktop.
Save Sam-Belliveau/943142dab814b674ecf6f6a852aba0cf to your computer and use it in GitHub Desktop.
a simple python script that goes through a CSV file that is created when you use a google form for ranked choice voting.
'''
Copyright (c) 2021 StuyPulse
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
from csv import reader as csv_reader
from sys import argv
# from termcolor import colored
def colored(s, *args, **kwargs):
return s
# print colored error with tag at the beginning
def rcv_print_error(message):
ERROR_HEADER = colored("[ERROR]", color="red", attrs=["bold", "blink"])
print(f"{ERROR_HEADER} {message.strip()}")
# class representing a ballot (name and choices)
class Ballot:
def __init__(self, name, choices):
self.name = name
self.choices = choices
# get top choice
def get_pick(self):
if self.has_pick():
return self.choices[0]
else:
return ""
# check to see if there are any picks left
def has_pick(self):
return len(self.choices) > 0
# move onto the next choice if first choice does not have majority
def discard_pick(self):
self.choices = self.choices[1:]
# class representing a candidate and how many votes they have
class Candidate:
MAX_PLACES = 2**12
# create a candidate, with 0 counted votes
def __init__(self, name):
self.name = name
self.count = [0] * Candidate.MAX_PLACES
# count the number of votes a candidate got
def add_count(self, ballot):
for rank, name in enumerate(ballot.choices[:Candidate.MAX_PLACES]):
self.count[rank] += (name == self.name)
# compare two candidates standings based on their number
# of 1st votes, then 2nd votes, then 3rd votes etc...
def __lt__(self, other):
return self.count < other.count
# read all of the ballots from a CSV file
def collect_ballots(file_name):
# this function will look to see which index
# represents which item in the csv list
def index_in_header(header, key, required=False):
try:
return next(i for i, x in enumerate(header) if str(key) in str(x))
# check to see if this header index is required,
# if it is, throw an error if it is not found
# if it is not, just return -1
except StopIteration:
if required:
rcv_print_error(
f"CSV File Malformed! [header \"{key}\" not found]")
quit(-1)
else:
return -1
# open up the CSV with error checking
try:
with open(file_name) as csv:
# read the lines of the CSV
rows = [row for row in csv_reader(csv)]
header = rows[0]
lines = rows[1:]
except FileNotFoundError:
rcv_print_error(f"Could not open file \"{file_name}\"")
quit(-1)
# get the indexes of the names
name_idx = index_in_header(header, "Name", required=True)
# get the indexes of the choices
choices_finder = (index_in_header(header, str(i))
for i in range(1, len(header)))
choices_idx = [itr for itr in choices_finder if 0 < itr]
# check to see if the indexes are malformed
if len(choices_idx) <= 0:
rcv_print_error(
"CSV File Malformed! [could not identify choice columbs]")
quit(-1)
# create list of ballots
try:
return [Ballot(str(l[name_idx]), [str(l[idx]) for idx in choices_idx]) for l in lines]
except Exception:
rcv_print_error(
"CSV File Malformed! [exception risen while interpreting ballots]")
quit(-1)
# run an election based on all of the ballots
# that have been collected from the CSV file
def run_election(ballots):
# pad number with spaces so
# everything lines up correctly
def pad_number(num):
o = str(num)
return " " * max(0, 3 - len(o)) + o
# make a set of all the possible choices
choices = set()
for ballot in ballots:
for choice in ballot.choices:
choices.add(choice)
# store name length to make sure every name is lined up
name_length = max([len(c) for c in choices])
max_choices = len(choices)
# keep removing the least voted for
# person until there are none left
while 0 < len(choices):
# make a list of candidates from the choices
candidates = [Candidate(c) for c in choices]
# count all of the ballots, and record
# their votes in the candidate class
for ballot in ballots:
for candidate in candidates:
candidate.add_count(ballot)
# sort the candidates based on their votes
candidates = sorted(candidates, reverse=True)
# print out the results of this round
print(colored("\nResults:", attrs=["bold"]))
for p, c in enumerate(candidates):
place = colored(f"{p + 1}.", attrs=["bold"])
name = colored(f"{c.name}", attrs=["underline"])
name += " " * max(0, name_length - len(c.name))
votes = colored(
f"{pad_number(c.count[0])} Votes ... {c.count[0:max_choices]}", attrs=[])
print(f"{place} {name} | {votes}", end="\n")
# get rid of the least voted for candidate
last_place = candidates[-1]
choices.remove(last_place.name)
# shift the ballots who's first pick was eliminated
for ballot in ballots:
while ballot.has_pick() and not ballot.get_pick() in choices:
ballot.discard_pick()
while True:
try:
print("")
print("")
print("----= StuyPulse RCV =----")
csv_file = input(colored("Enter CSV File Location: ", attrs=["bold"]))
# collect the ballots and run the election.
ballots = collect_ballots(csv_file)
run_election(ballots)
except Exception as e:
print("An Error has Occured!")
print("Contact Sam Belliveau.")
print(e)
except:
print("An Error has Occured!")
print("Contact Sam Belliveau.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment