From e83a6486b2ab2e97d2625642aebf44f488307fc4 Mon Sep 17 00:00:00 2001 From: Armin Samii Date: Thu, 24 Aug 2023 12:44:42 -0500 Subject: [PATCH] edit text to be accurate in bottoms-up rcv --- testData/expected-video-script.txt | 2 +- visualizer/descriptors/roundDescriber.py | 23 +++++++++++++++++------ visualizer/graph/graphSummary.py | 16 ++++++++++------ visualizer/tests/testFaq.py | 22 ++++++++++++++++++++++ 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/testData/expected-video-script.txt b/testData/expected-video-script.txt index 879dca79..9a2673e4 100644 --- a/testData/expected-video-script.txt +++ b/testData/expected-video-script.txt @@ -1,5 +1,5 @@ Ranked Choice Voting Election Results\n\n\nCity of Eastpointe, Macomb County, MI -In this Proportional, Multi-Winner Ranked Choice Voting election, there were 5 rounds, after which Harvey Curley and Larry Edwards were elected. Here's what happened in each round. +In this Multi-Winner Ranked Choice Voting election, there were 5 rounds, after which Harvey Curley and Larry Edwards were elected. Here's what happened in each round. In the first round, Larry Edwards received the most votes. In the second round, People who voted for Write-In had their votes transferred to their next choice. Write-In had the fewest votes and was eliminated. Harvey Curley reached the threshold of 134 votes and was elected. In the third round, Harvey Curley received the most votes. Harvey Curley had more than enough votes to win, so to ensure no vote is wasted, 10 surplus votes were redistributed to other candidates. diff --git a/visualizer/descriptors/roundDescriber.py b/visualizer/descriptors/roundDescriber.py index 0336dfbf..61c94a95 100644 --- a/visualizer/descriptors/roundDescriber.py +++ b/visualizer/descriptors/roundDescriber.py @@ -147,7 +147,8 @@ def _describe_winners_this_round(self, roundNum): """ e.g. "Foo reached the threshold of X and was elected. " Returns empty string if there wasn't a winner. """ rounds = self.graph.summarize().rounds - winners = rounds[roundNum].winnerNames + winnerNames = rounds[roundNum].winnerNames + winnerItems = rounds[roundNum].winnerItems # Note: each event shows just one winner. If there are multiple winners, # there will be multiple sentences in the main vis. (Not true in the video...) @@ -155,12 +156,22 @@ def _describe_winners_this_round(self, roundNum): event = textForWinnerUtils.as_event(self.config, 1) if self.graph.threshold is not None: - thresholdString = intify(self.graph.threshold) - whatHappened = "{name} reached the threshold of "\ - f"{thresholdString} votes and {event}. " + # Did all candidates pass the threshold? + candidates = self.graph.summarize().candidates + thresh = self.graph.threshold + allOverThreshold = all(candidates[item].totalVotesPerRound[roundNum] > thresh + for item in winnerItems) + + if allOverThreshold: + thresholdString = intify(self.graph.threshold) + whatHappened = "{name} reached the threshold of "\ + f"{thresholdString} votes and {event}. " + else: + whatHappened = "{name} is among the top vote-getters " \ + f" and {event}. " else: whatHappened = "{name} " + event + ". " - return self._describe_list_of_names(winners, " won", whatHappened) + return self._describe_list_of_names(winnerNames, " won", whatHappened) @classmethod def _describe_first_round(cls, roundNum): @@ -215,7 +226,7 @@ def describe_initial_summary(self, isForVideo): self.summarizeAsParagraph = originalSummarizeAsParagraph if len(winners) > 1: - electionType = "Proportional, Multi-Winner Ranked Choice Voting election" + electionType = "Multi-Winner Ranked Choice Voting election" else: electionType = "Ranked Choice Voting election" diff --git a/visualizer/graph/graphSummary.py b/visualizer/graph/graphSummary.py index 95db1ffa..a9971a61 100644 --- a/visualizer/graph/graphSummary.py +++ b/visualizer/graph/graphSummary.py @@ -36,13 +36,13 @@ def __init__(self, graph): if node.isWinner: # Only count winner the first time they win if item not in alreadyWonInPreviousRound: - rounds[currRound].add_winner(item.name) + rounds[currRound].add_winner(item) alreadyWonInPreviousRound.append(item) if node.isEliminated: # Eliminate the next round: in the sankey representation, # eliminated candidates are shown on the previous round # so they don't ever show zero-vote bars. Account for that. - rounds[currRound + 1].add_eliminated(item.name) + rounds[currRound + 1].add_eliminated(item) # Create linksByNode linksByTargetNode = {} @@ -64,6 +64,8 @@ class RoundInfo: def __init__(self, round_i): self.round_i = round_i + self.eliminatedItems = [] + self.winnerItems = [] self.eliminatedNames = [] self.winnerNames = [] self.totalActiveVotes = 0 # The total number of active ballots this round @@ -72,13 +74,15 @@ def key(self): """ Returns the "key" for this round (just the round number) """ return self.round_i - def add_eliminated(self, name): + def add_eliminated(self, item): """ Adds the name to the list of names eliminated this round """ - self.eliminatedNames.append(name) + self.eliminatedItems.append(item) + self.eliminatedNames.append(item.name) - def add_winner(self, name): + def add_winner(self, item): """ Adds the name to the list of names elected this round """ - self.winnerNames.append(name) + self.winnerItems.append(item) + self.winnerNames.append(item.name) def add_votes(self, candidateItem, numVotes): """ Notes that the given Candidate received numVotes votes - unless they're diff --git a/visualizer/tests/testFaq.py b/visualizer/tests/testFaq.py index 6afcb66b..de2fe433 100644 --- a/visualizer/tests/testFaq.py +++ b/visualizer/tests/testFaq.py @@ -252,3 +252,25 @@ def test_text_for_winner_in_summaries(self): for desc in descList: assert isinstance(desc['description'], str) self.assertNotIn(searchFor, desc['description']) + + # final round shouldn't have any threshold logic + desc = allRoundsDesc[-1][-1] + self.assertNotIn("is among the top vote-getters", desc['description']) + self.assertNotIn("reached the threshold", desc['description']) + + def test_winner_didnt_meet_threshold(self): + """ + If a winner doesn't meet the threshold, don't claim they did + """ + tf = TestHelpers.modify_json_with(filenames.THREE_ROUND, + lambda d: d['config'].update({'threshold': 9999})) + with open(tf.name, 'r', encoding='utf-8') as f: + graph = make_graph_with_file(f, False) + + describer = Describer(graph, self.config, False) + allRoundsDesc = describer.describe_all_rounds() + + # final round shouldn't just say they were elected, not that they reached the threshold + desc = allRoundsDesc[-1][-1] + self.assertIn("is among the top vote-getters", desc['description']) + self.assertNotIn("reached the threshold", desc['description'])