I’m writing a program that takes a csv of ‘ballots’ and runs a Ranked Choice Election and then graphs the votes as they are calculated. Here is the ballots file. The problem is that when the figure is saved the tick marks are not sorted. they seem to be somewhat random. Here’s what the final product looks like: Final Product. Notice that in the first two frames wine 2 and wine 3 switch places. Any ideas on how i can fix the tick labels? I’ve searched several places on how to change the labels but they don’t seem to be affecting the actual label output.
import pandas as pd
import numpy as np
from collections import Counter, defaultdict
import matplotlib.pyplot as plt
import glob
from PIL import Image
import random
nested_dict = lambda: defaultdict(nested_dict)
global threshold
global _candidates
def make_gif(frame_folder):
frames = []
for image in glob.glob(f"{frame_folder}/*.PNG"):
frames.append(Image.open(image))
if image.endswith("0.png") or image.endswith("1.png"):
frames.append(Image.open(image))
frames.append(Image.open(image))
frames.append(Image.open(image))
frames.append(Image.open(image))
frame_one = frames[0]
frame_one.save(f"{frame_folder}/results.gif", format="GIF", append_images=frames[1:],
save_all=True, duration=200, loop=0)
# Function to read ballots from a CSV file
def read_ballots(csv_file):
global _candidates
_candidates = {}
df = pd.read_csv(csv_file)
ballots = []
candidates = Counter()
for index, row in df.iterrows():
ballot = []
for choice in row[0:]:
if not pd.isna(choice):
ballot.append(choice)
candidates[choice] += 1
ballots.append(ballot)
global threshold
threshold = len(ballots)/2
print('Eliminating non-viable candidates')
for candidate, count in candidates.items():
if count < threshold:
ballots = eliminate_candidate(ballots, candidate)
else:
_candidates[candidate] = (random.randrange(0,100)/100,random.randrange(0,100)/100,random.randrange(0,100)/100,.9 )
_candidates["Exhausted Ballots"] = (.75,.75,.75,.9)
return ballots
# Function to count votes
def count_votes(ballots):
first_choice_counts = Counter()
for ballot in ballots:
if ballot:
first_choice_counts[ballot[0]] += 1
return first_choice_counts
# Function to eliminate the candidate with the fewest votes
def eliminate_candidate(ballots, candidate_to_eliminate):
print(f"Eliminating {candidate_to_eliminate}")
for ballot in ballots:
if candidate_to_eliminate in ballot:
ballot.remove(candidate_to_eliminate)
return ballots
# Function to run the ranked choice voting process
def ranked_choice_voting(csv_file):
ballots = read_ballots(csv_file)
total_votes = len(ballots)
print(f"Total ballots: {total_votes}")
round_number = 1
rounds = nested_dict()
while True:
print(f"nRound {round_number} results:")
vote_counts = count_votes(ballots)
for candidate, count in vote_counts.items():
print(f"{candidate}: {count} votes")
# save current round counts
for ballot in ballots:
if len(ballot) == 0:
if rounds[round_number]["Exhausted Ballots"]["Exhausted Ballots"]:
rounds[round_number]["Exhausted Ballots"]["Exhausted Ballots"] += 1
else:
rounds[round_number]["Exhausted Ballots"]["Exhausted Ballots"] = 1
if len(ballot) > 0:
if rounds[round_number][ballot[0]][ballot[0]]:
rounds[round_number][ballot[0]][ballot[0]] += 1
else:
rounds[round_number][ballot[0]][ballot[0]] = 1
if len(ballot) > 1:
if rounds[round_number][ballot[0]][ballot[1]]:
rounds[round_number][ballot[0]][ballot[1]] += 1
else:
rounds[round_number][ballot[0]][ballot[1]] = 1
# Check for a winner
for candidate, count in vote_counts.items():
if count > total_votes / 2:
print(f"nWinner: {candidate} with {count} votes")
return candidate, rounds
# Eliminate candidate with the fewest votes
candidate_to_eliminate = min(vote_counts, key=vote_counts.get)
ballots = eliminate_candidate(ballots, candidate_to_eliminate)
round_number += 1
def saveplot(dictionary, round_num, i):
global threshold
df = pd.DataFrame(dictionary)
colnames = sorted(df.columns)
colnames.remove("Exhausted Ballots")
colnames.append("Exhausted Ballots")
print(colnames)
df = df.reindex(colnames, axis=1)
color = []
for key in df.keys():
color.append(_candidates[key])
df.rename(columns={"Exhausted Ballots":"ExhaustednBallots"})
df.plot(kind='bar', stacked=True, color = color)
ax = plt.gca()
plt.xticks(rotation=15)
ax.set_xlim([-1, len(df)])
ax.set_ylim([0, threshold*2])
plt.axhline(y=threshold, color="black", linestyle="dashed")
plt.savefig(fname=f'rcvresults/image_{round_num}_{i}')
plt.close()
def rounds_to_bar_chart(rounds):
for round_num in rounds:
round_for_bar_graph = nested_dict()
minkey = ''
for key in rounds[round_num].keys():
round_for_bar_graph[key][key] = rounds[round_num][key][key]
if key != "Exhausted Ballots":
if minkey:
if round_for_bar_graph[key][key] < minvalue:
minvalue = round_for_bar_graph[key][key]
minkey = key
else:
minkey = key
minvalue = round_for_bar_graph[key][key]
saveplot(round_for_bar_graph, round_num, 0)
ballots_to_exhaust = round_for_bar_graph[minkey][minkey]
if len(rounds[round_num]) > 2:
highestvotes = 0
for key in rounds[round_num][minkey].keys():
round_for_bar_graph[key][minkey] = rounds[round_num][minkey][key]
if key != minkey:
ballots_to_exhaust -= rounds[round_num][minkey][key]
round_for_bar_graph[minkey][key] = 0
if key != minkey:
highestvotes = max(highestvotes, rounds[round_num][minkey][key])
round_for_bar_graph[minkey][minkey] = 0
round_for_bar_graph["Exhausted Ballots"][minkey] = ballots_to_exhaust
saveplot(round_for_bar_graph, round_num, 1)
n = 5
modifier = round(highestvotes/n)
for i in range(0, n):
for key in round_for_bar_graph[minkey].keys():
if key != minkey:
round_for_bar_graph[key][minkey] = max(0, round_for_bar_graph[key][minkey] - modifier)
round_for_bar_graph[minkey][key] = min(rounds[round_num][minkey][key], round_for_bar_graph[minkey][key] + modifier)
saveplot(round_for_bar_graph, round_num, i + 2)
make_gif("rcvresults")
csv_file = 'rcv.csv'
winner, rounds = ranked_choice_voting(csv_file)
rounds_to_bar_chart(rounds)
I’ve tried using this link but I need “Exhausted Ballots” to appear on the far right always, so if the candidates are mixed between [A:E] and [E:Z] then “Exhausted Ballots” will be somewhere in the middle or left. Here’s what it looks like with this change: Updated Ballots Gif