From 2bb191d2f688a3876ab1dd234bc6aafa570822e4 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 6 May 2016 02:03:51 -0500 Subject: [PATCH 01/77] Add various classes to implement different types of ranking algorithms. --- TallyCore/Global/Enumerations.cs | 29 +- TallyCore/NetTally.Core.csproj | 8 + TallyCore/VoteCounting/IBaseVoteCounter.cs | 26 + .../RankVoteCounting/BaseRankVoteCounter.cs | 66 +++ .../BordaFractionRankVoteCounter.cs | 31 ++ .../RankVoteCounting/BordaRankVoteCounter.cs | 31 ++ .../RankVoteCounting/CoombsRankVoteCounter.cs | 447 ++++++++++++++++++ .../InstantRunoffRankVoteCounter.cs | 31 ++ .../SchulzeRankVoteCounter.cs | 31 ++ TallyCore/VoteCounting/VoteCounterLocator.cs | 89 ++++ 10 files changed, 788 insertions(+), 1 deletion(-) create mode 100644 TallyCore/VoteCounting/IBaseVoteCounter.cs create mode 100644 TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs create mode 100644 TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs create mode 100644 TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs create mode 100644 TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs create mode 100644 TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs create mode 100644 TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs create mode 100644 TallyCore/VoteCounting/VoteCounterLocator.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 15b42f52..00725ee8 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -14,7 +14,34 @@ public enum VoteType { Vote, Plan, - Rank + Rank, + Approval + } + + public enum RankVoteCounterMethod + { + [EnumDescription("Default (Coombs)")] + Default, + [EnumDescription("Borda")] + BordaCount, + [EnumDescription("Borda (Fraction)")] + BordaFraction, + [EnumDescription("Coombs'")] + Coombs, + [EnumDescription("Instant Runoff")] + InstantRunoff, + [EnumDescription("Schulze")] + Schulze, + } + + public enum StandardVoteCounterMethod + { + Default + } + + public enum ApprovalVoteCounterMethod + { + Default } public enum ReferenceType diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index 71ee9e72..0b930833 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -79,6 +79,14 @@ + + + + + + + + diff --git a/TallyCore/VoteCounting/IBaseVoteCounter.cs b/TallyCore/VoteCounting/IBaseVoteCounter.cs new file mode 100644 index 00000000..d2c27e00 --- /dev/null +++ b/TallyCore/VoteCounting/IBaseVoteCounter.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace NetTally.VoteCounting +{ + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + + /// + /// Base vote counter interface, that all other vote counter interfaces derive from. + /// + public interface IBaseVoteCounter + { + + } + + /// + /// Vote counter interface for ranked votes. + /// + /// + public interface IRankVoteCounter : IBaseVoteCounter + { + RankResultsByTask CountVotes(SupportedVotes votes); + } +} diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs new file mode 100644 index 00000000..e67a1b9a --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public abstract class BaseRankVoteCounter : IRankVoteCounter + { + /// + /// Counts the provided rank votes. + /// + /// The rank votes to count. + /// Returns an ordered list of ranked votes for each task in the provided votes. + /// Provided votes cannot be null. + public RankResultsByTask CountVotes(SupportedVotes votes) + { + if (votes == null) + throw new ArgumentNullException(nameof(votes)); + + RankResultsByTask preferencesByTask = new RankResultsByTask(); + + if (votes.Any()) + { + // Handle each task separately + var groupByTask = from vote in votes + group vote by VoteString.GetVoteTask(vote.Key) into g + select g; + + foreach (GroupedVotesByTask task in groupByTask) + { + if (task.Any()) + { + Debug.WriteLine($"Rank Task [{task.Key}]"); + + preferencesByTask[task.Key] = RankTask(task); + + Debug.WriteLine($"End task [{task.Key}]"); + } + } + } + + return preferencesByTask; + } + + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected abstract RankResults RankTask(GroupedVotesByTask task); + + } +} diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs new file mode 100644 index 00000000..f088dcdc --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public class BordaFractionRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + throw new NotImplementedException(); + } + } +} diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs new file mode 100644 index 00000000..eb13e6e5 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public class BordaRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + throw new NotImplementedException(); + } + } +} diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs new file mode 100644 index 00000000..2e6496a8 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -0,0 +1,447 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NetTally.Utility; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + + /// + /// Class to handle counting ranked votes, with winners determined using + /// instant runoff elimination with Coombs' method. + /// + /// + public class CoombsRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + HashSet allVotes = GetVoteList(task); + var voterChoices = ConvertVotesToVoters(task, allVotes); + var voterNonChoices = GetNonChoices(voterChoices, allVotes); + + // 1st, 2nd, 3rd, and 4th place results + List topChoices = new List(9); + + for (int i = 0; i < 9; i++) + { + System.Diagnostics.Debug.WriteLine($"- Loop [{i}]"); + + // Create copies, because the vars we pass to the calculation + // functions will be modified during the process. + var voterChoicesCopy = voterChoices.ToDictionary(a => a.Key, a => a.Value.ToList()); + var voterNonChoicesCopy = voterNonChoices.ToDictionary(a => a.Key, a => a.Value.ToList()); + + // The best result each time through the loop gets added to the result list... + string topChoice = GetTopRank(voterChoicesCopy, voterNonChoicesCopy, voterChoices, allVotes); + + if (string.IsNullOrEmpty(topChoice)) + { + break; + } + + System.Diagnostics.Debug.WriteLine($"-- Top choice: [{topChoice}]"); + + topChoices.Add(topChoice); + + // ... and removed from the active choice list, for the next time through the loop. + RemoveChoice(topChoice, voterChoices); + } + + return topChoices; + } + + + /// + /// Gets the list of all votes (as vote contents), for use in identifying + /// vote selections that were not ranked by any given voter. + /// + /// The list of all votes for a task. + /// Returns a list of all votes. + private static HashSet GetVoteList(GroupedVotesByTask task) + { + var votes = task.Select(vote => VoteString.GetVoteContent(vote.Key)); + + return new HashSet(votes, StringUtility.AgnosticStringComparer); + } + + /// + /// Convert the original grouping of voters per vote into a grouping of votes per voter, + /// ordered by voter preference for each vote. + /// + /// All votes provided for a given task. + /// Returns a grouping of votes per user. + private static Dictionary> ConvertVotesToVoters(GroupedVotesByTask task, + HashSet allVotes) + { + var ordered = task.OrderBy(a => VoteString.GetVoteMarker(a.Key)); + + Dictionary> voters = new Dictionary>(); + + var allVoters = task.SelectMany(a => a.Value); + + foreach (var voter in allVoters) + { + voters[voter] = new List(); + } + + foreach (var vote in ordered) + { + string voteContent = VoteString.GetVoteContent(vote.Key); + + string normVote = allVotes.FirstOrDefault(v => StringUtility.AgnosticStringComparer.Equals(v, voteContent)) ?? voteContent; + + foreach (var voter in vote.Value) + { + voters[voter].Add(normVote); + } + } + + return voters; + } + + /// + /// Gets a dictionary of lists of all vote options that each voter did not rank. + /// + /// The collection of all choices that each voter did rank. + /// The list of all possible vote options. + /// Returns a dictionary collection of all options each voter did not rank. + private static Dictionary> GetNonChoices(Dictionary> voterChoices, HashSet allVotes) + { + Dictionary> voterNonChoices = new Dictionary>(); + + foreach (var voter in voterChoices) + { + var nonChoices = allVotes.Except(voter.Value); + voterNonChoices.Add(voter.Key, nonChoices.ToList()); + } + + return voterNonChoices; + } + + /// + /// Select the top option out of the votes cast for a given task. + /// + /// The voter choices. + /// The voter non choices. + /// The original voters choices. + /// The vote list. + /// + /// Returns the top voter choice for the task. + /// + private static string GetTopRank(Dictionary> voterChoices, + Dictionary> voterNonChoices, + Dictionary> originalVotersChoices, + HashSet voteList) + { + // Skip processing if there's nothing to count. + if (voterChoices == null || voterChoices.Count == 0 || voterChoices.All(a => a.Value.Count == 0)) + return string.Empty; + + int loopLimit = voterChoices.Count(a => a.Value.Count > 0) + 10; + // Limit to 10 iterations, to ensure there are no infinite loops + int loop = 0; + + while (true) + { + // First see if any options has a majority of #1 votes + // Get a list of all top-choice votes, and how many votes for each option. + var firstChoices = CountFirstPlaceVotes(voterChoices); + + // Of those, get the 'best' choice, which is the one with the + // most votes, or, in the case of a tie, the one with the highest + // ranking score. + var bestChoice = GetFirstChoice(firstChoices, originalVotersChoices); + + // If we have a majority selection, that's the winner. + if (IsMajority(firstChoices[bestChoice], voterChoices.Count)) + { + return bestChoice; + } + + // If we're out of other choices, or we've gone too many loops, use what we have. + if (OnlyOneChoiceLeft(voterChoices) || ++loop >= loopLimit) + { + return bestChoice; + } + + System.Diagnostics.Debug.WriteLine("-- No majority option."); + + // If no option has an absolute majority, find the most-disliked option + // and remove it from all voters' option lists, in preparation of another + // round of checks. + string lastChoice = GetLastChoice(voterChoices, voterNonChoices, voteList); + + // Remove the last place option before running another round + RemoveLastPlaceOption(lastChoice, voterChoices, voterNonChoices); + } + } + + /// + /// + /// + /// + /// + /// + private static string GetFirstChoice(Dictionary choices, + Dictionary> originalVotersChoices) + { + foreach (var c in choices) + { + System.Diagnostics.Debug.WriteLine($"--- {c.Key} @ {c.Value}"); + } + + int highestNumberOfChoices = choices.Max(a => a.Value); + + // Get the list of all choices that have the same total (max) number of selections + var choicesWithMostVotes = choices.Where(a => a.Value == highestNumberOfChoices).Select(b => b.Key); + + if (choicesWithMostVotes.Count() == 1) + return choicesWithMostVotes.First(); + + return GetHighestScoreOption(choicesWithMostVotes, originalVotersChoices); + } + + private static string GetLastChoice(Dictionary> voterChoices, + Dictionary> voterNonChoices, HashSet voteList) + { + Dictionary votesWeight = new Dictionary(StringUtility.AgnosticStringComparer); + int distinctCount = voteList.Count(); + + int highestNumberOfChoices = voterChoices.Max(a => a.Value.Count); + int nonChoiceWeight = distinctCount > highestNumberOfChoices ? highestNumberOfChoices + 1 : distinctCount; + + HashSet lastPlaceList = new HashSet(StringUtility.AgnosticStringComparer); + + foreach (var vote in voteList) + { + votesWeight[vote] = 0; + } + + foreach (var voter in voterChoices) + { + int index = 1; + foreach (var vote in voter.Value) + { + votesWeight[vote] += index++; + } + + if (voterNonChoices[voter.Key].Count == 0 && voter.Value.Count > 0) + { + lastPlaceList.Add(voter.Value.Last()); + } + } + + foreach (var voter in voterNonChoices) + { + foreach (var vote in voter.Value) + { + votesWeight[vote] += nonChoiceWeight; + + lastPlaceList.Add(vote); + } + } + + var least = votesWeight.Where(a => lastPlaceList.Contains(a.Key)). + OrderByDescending(a => a.Value).First().Key; + + return least; + } + + + private static string GetHighestScoreOption(IEnumerable choices, + Dictionary> originalVotersChoices) + { + var scores = from a in choices + select new { Choice = a, Score = GetScore(a, originalVotersChoices) }; + + int maxScore = scores.Max(a => a.Score); + + var withMaxScore = scores.Where(a => a.Score == maxScore); + + var pick = withMaxScore.OrderBy(a => a.Choice).Last(); + + return pick.Choice; + } + + private static string GetLowestScoreOption(IEnumerable choices, + Dictionary> originalVotersChoices) + { + var scores = from a in choices + select new { Choice = a, Score = GetScore(a, originalVotersChoices) }; + + int minScore = scores.Min(a => a.Score); + + var withMinScore = scores.Where(a => a.Score == minScore); + + var pick = withMinScore.OrderBy(a => a.Choice).Last(); + + return pick.Choice; + } + + private static int GetScore(string choice, Dictionary> originalVotersChoices) + { + int score = 0; + + foreach (var voter in originalVotersChoices) + { + int index = voter.Value.IndexOf(choice); + if (index >= 0) + { + score += (10 - index); + } + } + + return score; + } + + /// + /// Count up the first choice options for all voters, and return a tally. + /// + /// The list of all voters and their ranked choices. + /// Returns the number of votes for each of the first-ranked vote options. + private static Dictionary CountFirstPlaceVotes(Dictionary> voterList) + { + Dictionary voteCount = new Dictionary(StringComparer.OrdinalIgnoreCase); + + int count = 0; + + foreach (var vote in voterList) + { + if (vote.Value.Count > 0) + { + string firstVote = vote.Value.First(); + + if (!voteCount.TryGetValue(firstVote, out count)) + { + count = 0; + } + + voteCount[firstVote] = ++count; + } + } + + return voteCount; + } + + /// + /// Count up the last choice options for all voters, and return a tally. + /// + /// The list of all voters and their ranked choices. + /// Returns the number of votes for each of the last-ranked vote options. + private static Dictionary CountLastPlaceVotes(Dictionary> voterList, + Dictionary> voterNonChoices) + { + Dictionary voteCount = new Dictionary(StringComparer.OrdinalIgnoreCase); + + int count = 0; + + foreach (var voter in voterList) + { + var nonChoices = voterNonChoices[voter.Key]; + + if (nonChoices.Count > 0) + { + foreach (var nonChoice in nonChoices) + { + if (!voteCount.TryGetValue(nonChoice, out count)) + { + count = 0; + } + + voteCount[nonChoice] = ++count; + } + } + else + { + string lastVote = voter.Value.Last(); + + if (!voteCount.TryGetValue(lastVote, out count)) + { + count = 0; + } + + voteCount[lastVote] = ++count; + } + } + + return voteCount; + } + + /// + /// Remove the specified vote option from all voters' option lists. + /// May not remove the last vote option from a voter. + /// + /// The last place option that's being removed. + /// The list of all voters. + private static void RemoveLastPlaceOption(string bottomChoice, Dictionary> voterList, + Dictionary> voterNonChoices) + { + System.Diagnostics.Debug.WriteLine($"-- Eliminate [{bottomChoice}]"); + + foreach (var voter in voterList) + { + if (voter.Value.Count > 1) + { + voter.Value.Remove(bottomChoice); + } + } + + foreach (var voter in voterNonChoices) + { + voter.Value.Remove(bottomChoice); + } + } + + /// + /// Remove an entry unconditionally from the list of voter choices. + /// + /// The choice to remove. + /// The list of all voter ranked choices. + private static void RemoveChoice(string choice, Dictionary> voterChoices) + { + foreach (var voter in voterChoices) + { + voter.Value.RemoveAll(a => a == choice); + } + } + + /// + /// Check to see if all voters only have one voting option left. + /// + /// List of all voters and their choices. + /// Returns true if all voters only have one choice left. + private static bool OnlyOneChoiceLeft(Dictionary> voterList) + { + if (voterList.All(a => a.Value.Count == 1)) + return true; + + return false; + } + + /// + /// Determine if the given number of voters qualifies as a majority out of + /// all possible voters. + /// + /// Number of voters being checked. + /// Total number of voters. + /// Returns true if the number of voters qualifies as a majority. + private static bool IsMajority(int voters, int totalVoters) => ((double)voters / totalVoters) > 0.5; + } +} diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs new file mode 100644 index 00000000..feee073a --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public class InstantRunoffRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + throw new NotImplementedException(); + } + } +} diff --git a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs new file mode 100644 index 00000000..639233f4 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public class SchulzeRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + throw new NotImplementedException(); + } + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs new file mode 100644 index 00000000..9abcfbe1 --- /dev/null +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + /// + /// Static class used to request the proper vote counter class to use for + /// any given situation. + /// + public static class VoteCounterLocator + { + /// + /// Generic function to get the default vote counter for a given vote type. + /// + /// Type of the vote. + /// Returns a base vote counter interface. + public static IBaseVoteCounter GetVoteCounter(VoteType voteType) + { + switch (voteType) + { + case VoteType.Rank: + return GetRankVoteCounter(); + case VoteType.Vote: + return GetStandardVoteCounter(); + case VoteType.Approval: + return GetApprovalVoteCounter(); + default: + throw new ArgumentOutOfRangeException(nameof(voteType)); + } + } + + /// + /// Gets a rank vote counter. + /// + /// The methodology that the requested vote rank counter should use. + /// Returns a class to handle counting rank votes using the requested methodology. + public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = RankVoteCounterMethod.Default) + { + switch (method) + { + case RankVoteCounterMethod.Coombs: + return new CoombsRankVoteCounter(); + case RankVoteCounterMethod.BordaCount: + return new BordaRankVoteCounter(); + case RankVoteCounterMethod.BordaFraction: + return new BordaFractionRankVoteCounter(); + case RankVoteCounterMethod.InstantRunoff: + return new InstantRunoffRankVoteCounter(); + case RankVoteCounterMethod.Schulze: + return new SchulzeRankVoteCounter(); + default: + return new CoombsRankVoteCounter(); + } + } + + /// + /// Gets a standard vote counter. + /// + /// The methodology that the requested vote counter should use. + /// Returns a class to handle counting standard votes using the requested methodology. + /// + public static IRankVoteCounter GetStandardVoteCounter(StandardVoteCounterMethod method = StandardVoteCounterMethod.Default) + { + switch (method) + { + default: + throw new NotImplementedException(); + } + } + + /// + /// Gets an approval vote counter. + /// + /// The methodology that the requested vote counter should use. + /// Returns a class to handle counting approval votes using the requested methodology. + /// + public static IRankVoteCounter GetApprovalVoteCounter(ApprovalVoteCounterMethod method = ApprovalVoteCounterMethod.Default) + { + switch (method) + { + default: + throw new NotImplementedException(); + } + } + } +} From 85b01aa0608c00365ca8a6d15e47b8475740e606 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 13 May 2016 15:53:23 -0500 Subject: [PATCH 02/77] Remove obsolete binding. --- NetTally/MainWindow.xaml | 1 - 1 file changed, 1 deletion(-) diff --git a/NetTally/MainWindow.xaml b/NetTally/MainWindow.xaml index a44c73e1..85ee4e8c 100644 --- a/NetTally/MainWindow.xaml +++ b/NetTally/MainWindow.xaml @@ -5,7 +5,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:NetTally" mc:Ignorable="d" - Title="{Binding MyTitle, RelativeSource={RelativeSource Mode=Self}}" Height="617.588" Width="926" MinWidth="857" Closing="Window_Closing" MinHeight="200" Icon="CheckVoteWin.ico" KeyUp="Window_KeyUp"> From 56723edd4dd141c966acac0e5f5f3ae6b795e7f6 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 13 May 2016 15:53:59 -0500 Subject: [PATCH 03/77] Implement Borda ranking. Add utility class for grouping ranked votes. --- TallyCore/NetTally.Core.csproj | 1 + .../RankVoteCounting/BordaRankVoteCounter.cs | 51 ++++++++++++++++++- .../Utility/GroupRankVotes.cs | 43 ++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index 0b930833..a3517a72 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -86,6 +86,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs index eb13e6e5..b142c1ed 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -25,7 +26,55 @@ public class BordaRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - throw new NotImplementedException(); + var groupVotes = GroupRankVotes.GroupVotesByVoteAndRank(task); + + var rankedVotes = from vote in groupVotes + select new { Vote = vote.VoteContent, Rank = RankVote(vote.Ranks) }; + + var orderedVotes = rankedVotes.OrderByDescending(a => a.Rank); + + foreach (var orderedVote in orderedVotes) + { + Debug.WriteLine($"- {orderedVote.Vote} [{orderedVote.Rank}]"); + } + + return orderedVotes.Select(a => a.Vote).ToList(); + } + + private int RankVote(IEnumerable ranks) + { + var rankVals = from r in ranks + select ValueOfRank(r.Rank); + + int voteValue = 0; + + foreach (var r in ranks) + { + int rankValue = ValueOfRank(r.Rank); + + voteValue += rankValue * r.Voters.Count(); + } + + return voteValue; } + + private int ValueOfRank(string rank) + { + if (string.IsNullOrEmpty(rank)) + throw new ArgumentNullException(nameof(rank)); + + int rankAsInt = int.Parse(rank); + + if (rankAsInt < 1 || rankAsInt > 9) + throw new ArgumentOutOfRangeException(nameof(rank)); + + // Ranks valued at 5 for #1, then -1 per rank below that, to a minimum of -3. + int rankValue = (6 - rankAsInt); + + return rankValue; + } + } + + } diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs new file mode 100644 index 00000000..0824dda6 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public class RankGroupedVoters + { + public string VoteContent { get; set; } + public IEnumerable Ranks { get; set; } + } + + public class RankedVoters + { + public string Rank { get; set; } + public IEnumerable Voters { get; set; } + } + + public static class GroupRankVotes + { + public static IEnumerable GroupVotesByVoteAndRank(GroupedVotesByTask task) + { + var res2 = from vote in task + let content = VoteString.GetVoteContent(vote.Key) + let rank = VoteString.GetVoteMarker(vote.Key) + group vote by content into votes + select new RankGroupedVoters + { + VoteContent = votes.Key, + Ranks = from v in votes + group v by VoteString.GetVoteMarker(v.Key) into vr + select new RankedVoters { Rank = vr.Key, Voters = vr.SelectMany(a => a.Value) } + }; + + return res2; + } + } +} From 8e0314a5ecc4eaef9a1bbff71f5c4e763d98351b Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 13 May 2016 15:54:08 -0500 Subject: [PATCH 04/77] Remove unneeded usings. --- TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs index e67a1b9a..c980637d 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaseRankVoteCounter.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NetTally.VoteCounting { From dad58d605a319bdb24f3c79a6437abae647e8b91 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 13 May 2016 15:59:41 -0500 Subject: [PATCH 05/77] Add comments, tidy up. --- .../RankVoteCounting/BordaRankVoteCounter.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs index b142c1ed..238354f1 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NetTally.VoteCounting { @@ -41,23 +39,30 @@ protected override RankResults RankTask(GroupedVotesByTask task) return orderedVotes.Select(a => a.Vote).ToList(); } + /// + /// Ranks the vote. + /// + /// Votes with associated ranks, for the voters who ranked the vote with a given value. + /// Returns a numeric evaluation of the overall rank of the vote. private int RankVote(IEnumerable ranks) { - var rankVals = from r in ranks - select ValueOfRank(r.Rank); - int voteValue = 0; foreach (var r in ranks) { - int rankValue = ValueOfRank(r.Rank); - - voteValue += rankValue * r.Voters.Count(); + voteValue += ValueOfRank(r.Rank) * r.Voters.Count(); } return voteValue; } + /// + /// Get the numeric value of a given rank. + /// + /// The rank being evaluated. + /// + /// + /// private int ValueOfRank(string rank) { if (string.IsNullOrEmpty(rank)) @@ -73,8 +78,5 @@ private int ValueOfRank(string rank) return rankValue; } - } - - } From 828335f5feafd773db0912c632639dd4f44f4659 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 13 May 2016 16:05:22 -0500 Subject: [PATCH 06/77] Implement Borda fractional ranking. --- .../BordaFractionRankVoteCounter.cs | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs index f088dcdc..937b8090 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -25,7 +26,59 @@ public class BordaFractionRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - throw new NotImplementedException(); + var groupVotes = GroupRankVotes.GroupVotesByVoteAndRank(task); + + var rankedVotes = from vote in groupVotes + select new { Vote = vote.VoteContent, Rank = RankVote(vote.Ranks) }; + + var orderedVotes = rankedVotes.OrderByDescending(a => a.Rank); + + foreach (var orderedVote in orderedVotes) + { + Debug.WriteLine($"- {orderedVote.Vote} [{orderedVote.Rank}]"); + } + + return orderedVotes.Select(a => a.Vote).ToList(); + } + + /// + /// Ranks the vote. + /// + /// Votes with associated ranks, for the voters who ranked the vote with a given value. + /// Returns a numeric evaluation of the overall rank of the vote. + private double RankVote(IEnumerable ranks) + { + double voteValue = 0; + + foreach (var r in ranks) + { + voteValue += ValueOfRank(r.Rank) * r.Voters.Count(); + } + + return voteValue; + } + + /// + /// Get the numeric value of a given rank. + /// + /// The rank being evaluated. + /// + /// + /// + private double ValueOfRank(string rank) + { + if (string.IsNullOrEmpty(rank)) + throw new ArgumentNullException(nameof(rank)); + + int rankAsInt = int.Parse(rank); + + if (rankAsInt < 1 || rankAsInt > 9) + throw new ArgumentOutOfRangeException(nameof(rank)); + + // Ranks valued at 1 for #1, then 1/N for each higher N. + double rankValue = 1.0 / rankAsInt; + + return rankValue; } } } From 5eca39f774b50971cb33852fb62c721bc0904f48 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 13 May 2016 16:16:58 -0500 Subject: [PATCH 07/77] Add comments, tidy up. --- .../RankVoteCounting/BordaFractionRankVoteCounter.cs | 4 ++-- .../VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs index 937b8090..1f4e0a36 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NetTally.VoteCounting { @@ -50,6 +48,8 @@ private double RankVote(IEnumerable ranks) { double voteValue = 0; + // Add up the sum of the number of voters times the value of each rank. + // If any voter didn't vote for an option, they effectively add a 0 (rank #10) for that option. foreach (var r in ranks) { voteValue += ValueOfRank(r.Rank) * r.Voters.Count(); diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs index 238354f1..6cd7dc8a 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -48,6 +48,8 @@ private int RankVote(IEnumerable ranks) { int voteValue = 0; + // Add up the sum of the number of voters times the value of each rank. + // If any voter didn't vote for an option, they effectively add a 0 (rank #6) for that option. foreach (var r in ranks) { voteValue += ValueOfRank(r.Rank) * r.Voters.Count(); From 6d0b86fb8f8bfb3fd985f0760d710a689b299553 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 15 May 2016 15:25:43 -0500 Subject: [PATCH 08/77] Rename function. --- .../RankVoteCounting/BordaFractionRankVoteCounter.cs | 2 +- TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs | 2 +- .../VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs index 1f4e0a36..28b092f8 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs @@ -24,7 +24,7 @@ public class BordaFractionRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - var groupVotes = GroupRankVotes.GroupVotesByVoteAndRank(task); + var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); var rankedVotes = from vote in groupVotes select new { Vote = vote.VoteContent, Rank = RankVote(vote.Ranks) }; diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs index 6cd7dc8a..1f79448b 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -24,7 +24,7 @@ public class BordaRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - var groupVotes = GroupRankVotes.GroupVotesByVoteAndRank(task); + var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); var rankedVotes = from vote in groupVotes select new { Vote = vote.VoteContent, Rank = RankVote(vote.Ranks) }; diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 0824dda6..19e5bc56 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -23,7 +23,7 @@ public class RankedVoters public static class GroupRankVotes { - public static IEnumerable GroupVotesByVoteAndRank(GroupedVotesByTask task) + public static IEnumerable GroupByVoteAndRank(GroupedVotesByTask task) { var res2 = from vote in task let content = VoteString.GetVoteContent(vote.Key) From 8d9c9dadf4440044b22ebe636f01b86bf1ca61c4 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 15 May 2016 16:48:06 -0500 Subject: [PATCH 09/77] Comment typo. --- TallyCore/Utility/LinqExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TallyCore/Utility/LinqExtensions.cs b/TallyCore/Utility/LinqExtensions.cs index 40c0d8a2..3bc19a8c 100644 --- a/TallyCore/Utility/LinqExtensions.cs +++ b/TallyCore/Utility/LinqExtensions.cs @@ -177,7 +177,7 @@ public static T MinObject(this IEnumerable self, Func transform, } /// - /// Extension method to get the object with the minimum value from an enumerable list. + /// Extension method to get the object with the maximum value from an enumerable list. /// /// The type of object the list contains. /// The list. From 79441ab4d2e107db2b5ecbabe3eda3698464f47f Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 15 May 2016 17:30:55 -0500 Subject: [PATCH 10/77] Add a new grouping method. Add supporting object classes. --- .../Utility/GroupRankVotes.cs | 67 +++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 19e5bc56..017c20ce 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -21,23 +21,64 @@ public class RankedVoters public IEnumerable Voters { get; set; } } + public class RankedVote + { + public int Rank { get; set; } + public string Vote { get; set; } + } + + public class VoterRankings + { + public string Voter { get; set; } + public IEnumerable RankedVotes { get; set; } + } + public static class GroupRankVotes { public static IEnumerable GroupByVoteAndRank(GroupedVotesByTask task) { - var res2 = from vote in task - let content = VoteString.GetVoteContent(vote.Key) - let rank = VoteString.GetVoteMarker(vote.Key) - group vote by content into votes - select new RankGroupedVoters - { - VoteContent = votes.Key, - Ranks = from v in votes - group v by VoteString.GetVoteMarker(v.Key) into vr - select new RankedVoters { Rank = vr.Key, Voters = vr.SelectMany(a => a.Value) } - }; - - return res2; + var res = from vote in task + let content = VoteString.GetVoteContent(vote.Key) + group vote by content into votes + select new RankGroupedVoters + { + VoteContent = votes.Key, + Ranks = from v in votes + group v by VoteString.GetVoteMarker(v.Key) into vr + select new RankedVoters { Rank = vr.Key, Voters = vr.SelectMany(a => a.Value) } + }; + + return res; + } + + public static IEnumerable GroupByVoterAndRank(GroupedVotesByTask task) + { + var res = from vote in task + from voter in vote.Value + group vote by voter into voters + select new VoterRankings + { + Voter = voters.Key, + RankedVotes = from v in voters + select new RankedVote + { + Rank = RankAsInt(VoteString.GetVoteMarker(v.Key)), + Vote = VoteString.GetVoteContent(v.Key) + } + }; + + return res; + + } + + private static int RankAsInt(string rank) + { + if (string.IsNullOrEmpty(rank)) + throw new ArgumentNullException(nameof(rank)); + + int rankAsInt = int.Parse(rank); + + return rankAsInt; } } } From e4a414e9440057415aaaf245b17dd6edde5f9382 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 15 May 2016 17:31:11 -0500 Subject: [PATCH 11/77] Implement instant runoff voting calculations. --- .../InstantRunoffRankVoteCounter.cs | 130 +++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs index feee073a..118c9abd 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; +using NetTally.Utility; namespace NetTally.VoteCounting { @@ -17,6 +19,17 @@ namespace NetTally.VoteCounting public class InstantRunoffRankVoteCounter : BaseRankVoteCounter { + /// + /// Local class to store a choice/count combo of fields for LINQ. + /// + protected class ChoiceCount + { + public string Choice { get; set; } + public int Count { get; set; } + + public override string ToString() => $"{Choice}: {Count}"; + } + /// /// Implementation to generate the ranking list for the provided set /// of votes for a specific task. @@ -25,7 +38,122 @@ public class InstantRunoffRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - throw new NotImplementedException(); + if (task == null) + throw new ArgumentNullException(nameof(task)); + + List winningChoices = new List(); + + if (task.Any()) + { + Debug.WriteLine(">>Instant Runoff<<"); + + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + + for (int i = 1; i <= 9; i++) + { + string winner = GetWinningVote(voterRankings, winningChoices); + + if (winner == null) + break; + + winningChoices.Add(winner); + Debug.WriteLine($"- {winner}"); + } + } + + return winningChoices; + } + + /// + /// Gets the winning vote, instant runoff style. + /// + /// The voters' rankings. + /// The already chosen choices that we should exclude. + /// Returns the winning vote, if any. Otherwise, null. + /// + /// + private string GetWinningVote(IEnumerable voterRankings, IEnumerable chosenChoices) + { + if (voterRankings == null) + throw new ArgumentNullException(nameof(voterRankings)); + if (chosenChoices == null) + throw new ArgumentNullException(nameof(chosenChoices)); + + var localRankings = voterRankings; + + foreach (var choice in chosenChoices) + { + localRankings = RemoveChoiceFromVotes(localRankings, choice); + } + + int voterCount = localRankings.Count(v => v.RankedVotes.Any()); + int winCount = (int)Math.Ceiling(voterCount * 0.50011); + + while (true) + { + var preferredVotes = GetPrefferredCounts(localRankings); + + if (!preferredVotes.Any()) + break; + + var best = preferredVotes.MaxObject(a => a.Count); + + if (best.Count >= winCount) + return best.Choice; + + var worst = preferredVotes.MinObject(a => a.Count); + + localRankings = RemoveChoiceFromVotes(localRankings, worst.Choice); + } + + return null; + } + + /// + /// Filter the provided list of voter rankings to remove any instances of the specified choice. + /// + /// The votes to filter. + /// The choice to remove. + /// Returns the list without the given choice in the voters' rankings. + private IEnumerable RemoveChoiceFromVotes(IEnumerable voterRankings, string choice) + { + var res = from voter in voterRankings + select new VoterRankings + { + Voter = voter.Voter, + RankedVotes = voter.RankedVotes.Where(v => v.Vote != choice) + }; + + return res; + } + + /// + /// Gets the count of the number of times a given vote is the most preferred option + /// among the provided voters. + /// + /// The list of voters and their rankings of each option. + /// Returns a collection of Choice/Count objects. + private IEnumerable GetPrefferredCounts(IEnumerable voterRankings) + { + var preferredVotes = from voter in voterRankings + let preferred = GetPreferredVote(voter.RankedVotes) + where preferred != null + group voter by preferred into preffed + select new ChoiceCount { Choice = preffed.Key, Count = preffed.Count() }; + + return preferredVotes; + } + + /// + /// Gets the preferred vote (ie: highest ranked) from a collection of ranked votes. + /// + /// A voter's rankings. + /// Returns the vote component of the most preferred vote in the list, + /// or null if none are present. + private string GetPreferredVote(IEnumerable voterRankings) + { + var choice = voterRankings.OrderBy(a => a.Rank).FirstOrDefault()?.Vote; + return choice; } } } From c40e11c37c7e2160d0a99c485fb574af6fbe13eb Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 15 May 2016 17:32:19 -0500 Subject: [PATCH 12/77] Add debug output to name the type of process being run. --- .../RankVoteCounting/BordaFractionRankVoteCounter.cs | 2 ++ .../VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs | 2 ++ .../VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs | 3 +++ 3 files changed, 7 insertions(+) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs index 28b092f8..7909646e 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs @@ -24,6 +24,8 @@ public class BordaFractionRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { + Debug.WriteLine(">>Borda Fractions<<"); + var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); var rankedVotes = from vote in groupVotes diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs index 1f79448b..c9dffd63 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -24,6 +24,8 @@ public class BordaRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { + Debug.WriteLine(">>Borda Counting<<"); + var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); var rankedVotes = from vote in groupVotes diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index 2e6496a8..b258ec04 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -32,6 +33,8 @@ public class CoombsRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { + Debug.WriteLine(">>Coombs Runoff<<"); + HashSet allVotes = GetVoteList(task); var voterChoices = ConvertVotesToVoters(task, allVotes); var voterNonChoices = GetNonChoices(voterChoices, allVotes); From 6df4b893aa866856eaec14d16f73d69d67834967 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 15 May 2016 21:22:41 -0500 Subject: [PATCH 13/77] Implement Schulze ranking algorithm. --- .../SchulzeRankVoteCounter.cs | 258 +++++++++++++++++- 1 file changed, 250 insertions(+), 8 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs index 639233f4..2331960c 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs @@ -1,17 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NetTally.VoteCounting { // List of preference results ordered by winner using RankResults = List; - // Task (string), Ordered list of ranked votes - using RankResultsByTask = Dictionary>; - // Vote (string), collection of voters - using SupportedVotes = Dictionary>; // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; @@ -19,13 +14,260 @@ public class SchulzeRankVoteCounter : BaseRankVoteCounter { /// /// Implementation to generate the ranking list for the provided set - /// of votes for a specific task. + /// of votes for a specific task, based on the Schulze algorithm. /// /// The task that the votes are grouped under. /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - throw new NotImplementedException(); + if (task == null) + throw new ArgumentNullException(nameof(task)); + + + Debug.WriteLine(">>Schulze Ranking<<"); + + List listOfChoices = GetAllChoices(task); + + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + + int[,] pairwisePreferences = GetPairwisePreferences(voterRankings, listOfChoices); + + int[,] strongestPaths = GetStrongestPaths(pairwisePreferences, listOfChoices.Count); + + int[,] winningPaths = GetWinningPaths(strongestPaths, listOfChoices.Count); + + RankResults winningChoices = GetResultsInOrder(winningPaths, listOfChoices); + + return winningChoices; + } + + #region Schulze Algorithm + /// + /// Fills the pairwise preferences. + /// This goes through each voter's ranking options and updates an array indicating + /// which options are preferred over which other options. Each higher-ranked + /// option gains one point in 'beating' a lower-ranked option. + /// + /// The voter rankings. + /// The list of choices. + /// Returns a filled-in preferences array. + private int[,] GetPairwisePreferences(IEnumerable voterRankings, List listOfChoices) + { + int[,] pairwisePreferences = new int[listOfChoices.Count, listOfChoices.Count]; + + var choiceIndexes = GetChoicesIndexes(listOfChoices); + + foreach (var voter in voterRankings) + { + var rankedChoices = voter.RankedVotes.Select(v => v.Vote); + var unrankedChoices = listOfChoices.Except(rankedChoices); + + foreach (var choice in voter.RankedVotes) + { + foreach (var otherChoice in voter.RankedVotes) + { + if ((choice.Vote != otherChoice.Vote) && (choice.Rank < otherChoice.Rank)) + { + pairwisePreferences[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]]++; + } + } + + foreach (var nonChoice in unrankedChoices) + { + pairwisePreferences[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; + } + } + } + + return pairwisePreferences; } + + /// + /// Calculate the strongest preference paths from the pairwise preferences table. + /// + /// The pairwise preferences. + /// The choices count (size of the table). + /// Returns a table with the strongest paths between each pairwise choice. + private int[,] GetStrongestPaths(int[,] pairwisePreferences, int choicesCount) + { + int[,] strongestPaths = new int[choicesCount, choicesCount]; + + for (int i = 0; i < choicesCount; i++) + { + for (int j = 0; j < choicesCount; j++) + { + if (i != j) + { + if (pairwisePreferences[i, j] >= pairwisePreferences[j, i]) + { + strongestPaths[i, j] = pairwisePreferences[i, j]; + } + else + { + strongestPaths[i, j] = 0; + } + } + } + } + + for (int i = 0; i < choicesCount; i++) + { + for (int j = 0; j < choicesCount; j++) + { + if (i != j) + { + for (int k = 0; k < choicesCount; k++) + { + if (i != k && j != k) + { + strongestPaths[j, k] = Math.Max(strongestPaths[j, k], Math.Min(strongestPaths[j, i], strongestPaths[i, k])); + } + } + } + } + } + + return strongestPaths; + } + + /// + /// Gets the winning paths - The strongest of the strongest paths, for each pair option. + /// + /// The strongest paths. + /// The choices count (size of table). + /// Returns a table with the winning choices of the strongest paths. + private int[,] GetWinningPaths(int[,] strongestPaths, int choicesCount) + { + int[,] winningPaths = new int[choicesCount, choicesCount]; + + for (int i = 0; i < choicesCount; i++) + { + for (int j = 0; j < choicesCount; j++) + { + if (i != j) + { + if (strongestPaths[i, j] >= strongestPaths[j, i]) + { + winningPaths[i, j] = strongestPaths[i, j]; + } + else + { + winningPaths[i, j] = 0; + } + } + } + } + + return winningPaths; + } + + /// + /// Gets the winning options in order of preference, based on the winning paths. + /// + /// The winning paths. + /// The list of choices. + /// Returns a list of + private List GetResultsInOrder(int[,] winningPaths, List listOfChoices) + { + int count = listOfChoices.Count; + + var availableIndexes = Enumerable.Range(0, count); + + var pathCounts = from index in availableIndexes + select new { + Index = index, + Choice = listOfChoices[index], + Count = GetPositivePathCount(winningPaths, index, count), + Sum = GetPathSum(winningPaths, index, count) + }; + + var orderPaths = pathCounts.OrderByDescending(p => p.Count).ThenByDescending(p => p.Sum).ThenBy(p => p.Choice); + + foreach (var path in orderPaths) + { + Debug.WriteLine($"- {path.Choice} [{path.Count}/{path.Sum}]"); + } + + var res = orderPaths.Select(r => listOfChoices[r.Index]).ToList(); + + return res; + } + #endregion + + #region Small Utility + /// + /// Gets all choices from all user votes. + /// + /// The collection of user votes. + /// Returns a list of all the choices in the task. + private List GetAllChoices(GroupedVotesByTask task) + { + HashSet choices = new HashSet(); + + var res = from vote in task + group vote by VoteString.GetVoteContent(vote.Key) into votes + select votes.Key; + + return res.ToList(); + } + + /// + /// Gets an indexer lookup for the list of choices, so it doesn't have to do + /// sequential lookups each time.. + /// + /// The list of choices. + /// Returns a dictionary of choices vs list index. + private Dictionary GetChoicesIndexes(RankResults listOfChoices) + { + Dictionary choiceIndexes = new Dictionary(); + int index = 0; + foreach (var choice in listOfChoices) + { + choiceIndexes[choice] = index++; + } + + return choiceIndexes; + } + + /// + /// Gets the number of paths in the table with a value greater than 0. + /// + /// The paths table. + /// The row. + /// The size of the table. + /// Returns a count of the number of positive path strength values. + private int GetPositivePathCount(int[,] paths, int row, int count) + { + int pathCount = 0; + + for (int i = 0; i < count; i++) + { + if (paths[row, i] > 0) + pathCount++; + } + + return pathCount; + } + + /// + /// Gets the sum of the path strength for a given table row. + /// + /// The paths table. + /// The row. + /// The size of the table. + /// Returns the sum of the given path. + private int GetPathSum(int[,] paths, int row, int count) + { + int pathSum = 0; + + for (int i = 0; i < count; i++) + { + pathSum += paths[row, i]; + } + + return pathSum; + } + #endregion + } } From 8f40b29c2a35835aedfc14ac4dd4c13cfe7c6a00 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 15 May 2016 22:31:23 -0500 Subject: [PATCH 14/77] Fix misspelling. --- .../RankVoteCounting/InstantRunoffRankVoteCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs index 118c9abd..6f21f0e2 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -91,7 +91,7 @@ private string GetWinningVote(IEnumerable voterRankings, IEnumera while (true) { - var preferredVotes = GetPrefferredCounts(localRankings); + var preferredVotes = GetPreferredCounts(localRankings); if (!preferredVotes.Any()) break; @@ -133,7 +133,7 @@ private IEnumerable RemoveChoiceFromVotes(IEnumerable /// The list of voters and their rankings of each option. /// Returns a collection of Choice/Count objects. - private IEnumerable GetPrefferredCounts(IEnumerable voterRankings) + private IEnumerable GetPreferredCounts(IEnumerable voterRankings) { var preferredVotes = from voter in voterRankings let preferred = GetPreferredVote(voter.RankedVotes) From 92c61bd985b77b44cccb472095b3fbce50c18324 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:01:13 -0500 Subject: [PATCH 15/77] Remove unused code. --- .../VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs index 2331960c..4aa47f8e 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs @@ -202,8 +202,6 @@ private List GetResultsInOrder(int[,] winningPaths, List listOfC /// Returns a list of all the choices in the task. private List GetAllChoices(GroupedVotesByTask task) { - HashSet choices = new HashSet(); - var res = from vote in task group vote by VoteString.GetVoteContent(vote.Key) into votes select votes.Key; From 6afe568b5832a75fe80823c4ef33c6a2c6b9c8c7 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:01:37 -0500 Subject: [PATCH 16/77] Add List<> variant on VoterRankings for in-place modifications. --- .../VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 017c20ce..8b08427a 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -33,6 +33,12 @@ public class VoterRankings public IEnumerable RankedVotes { get; set; } } + public class VoterRankingsL + { + public string Voter { get; set; } + public List RankedVotes { get; set; } + } + public static class GroupRankVotes { public static IEnumerable GroupByVoteAndRank(GroupedVotesByTask task) From 1351c35c793a41a196acf5ab040307d7ded3e194 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:02:29 -0500 Subject: [PATCH 17/77] Rebuild Coombs code using methodologies used in the other new ranking algorithms. --- .../RankVoteCounting/CoombsRankVoteCounter.cs | 463 +++++------------- 1 file changed, 122 insertions(+), 341 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index b258ec04..3252c668 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -18,13 +18,19 @@ namespace NetTally.VoteCounting using GroupedVotesByTask = IGrouping>>; - /// - /// Class to handle counting ranked votes, with winners determined using - /// instant runoff elimination with Coombs' method. - /// - /// public class CoombsRankVoteCounter : BaseRankVoteCounter { + /// + /// Local class to store a choice/count combo of fields for LINQ. + /// + protected class ChoiceCount + { + public string Choice { get; set; } + public int Count { get; set; } + + public override string ToString() => $"{Choice}: {Count}"; + } + /// /// Implementation to generate the ranking list for the provided set /// of votes for a specific task. @@ -33,418 +39,193 @@ public class CoombsRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - Debug.WriteLine(">>Coombs Runoff<<"); + if (task == null) + throw new ArgumentNullException(nameof(task)); - HashSet allVotes = GetVoteList(task); - var voterChoices = ConvertVotesToVoters(task, allVotes); - var voterNonChoices = GetNonChoices(voterChoices, allVotes); + List winningChoices = new List(); - // 1st, 2nd, 3rd, and 4th place results - List topChoices = new List(9); - - for (int i = 0; i < 9; i++) + if (task.Any()) { - System.Diagnostics.Debug.WriteLine($"- Loop [{i}]"); - - // Create copies, because the vars we pass to the calculation - // functions will be modified during the process. - var voterChoicesCopy = voterChoices.ToDictionary(a => a.Key, a => a.Value.ToList()); - var voterNonChoicesCopy = voterNonChoices.ToDictionary(a => a.Key, a => a.Value.ToList()); + Debug.WriteLine(">>Coombs Runoff<<"); - // The best result each time through the loop gets added to the result list... - string topChoice = GetTopRank(voterChoicesCopy, voterNonChoicesCopy, voterChoices, allVotes); + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + var allChoices = GetAllChoices(voterRankings); - if (string.IsNullOrEmpty(topChoice)) + for (int i = 1; i <= 9; i++) { - break; - } + string winner = GetWinningVote(voterRankings, winningChoices, allChoices); + + if (winner == null) + break; - System.Diagnostics.Debug.WriteLine($"-- Top choice: [{topChoice}]"); + winningChoices.Add(winner); + allChoices.Remove(winner); - topChoices.Add(topChoice); + Debug.WriteLine($"- {winner}"); - // ... and removed from the active choice list, for the next time through the loop. - RemoveChoice(topChoice, voterChoices); + if (!allChoices.Any()) + break; + } } - return topChoices; + return winningChoices; } - /// - /// Gets the list of all votes (as vote contents), for use in identifying - /// vote selections that were not ranked by any given voter. + /// Gets the winning vote. + /// Excludes any already chosen votes from the process. /// - /// The list of all votes for a task. - /// Returns a list of all votes. - private static HashSet GetVoteList(GroupedVotesByTask task) + /// The voter rankings. + /// The already chosen choices. + /// All remaining choices. + /// Returns the winning vote. + /// + /// + private string GetWinningVote(IEnumerable voterRankings, RankResults chosenChoices, RankResults allChoices) { - var votes = task.Select(vote => VoteString.GetVoteContent(vote.Key)); - - return new HashSet(votes, StringUtility.AgnosticStringComparer); - } + if (voterRankings == null) + throw new ArgumentNullException(nameof(voterRankings)); + if (chosenChoices == null) + throw new ArgumentNullException(nameof(chosenChoices)); - /// - /// Convert the original grouping of voters per vote into a grouping of votes per voter, - /// ordered by voter preference for each vote. - /// - /// All votes provided for a given task. - /// Returns a grouping of votes per user. - private static Dictionary> ConvertVotesToVoters(GroupedVotesByTask task, - HashSet allVotes) - { - var ordered = task.OrderBy(a => VoteString.GetVoteMarker(a.Key)); + // Initial conversion from enumerable to list + List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); - Dictionary> voters = new Dictionary>(); + AddUnselectedRankings(localRankings, allChoices); - var allVoters = task.SelectMany(a => a.Value); + int voterCount = localRankings.Count(); + int winCount = (int)Math.Ceiling(voterCount * 0.50011); - foreach (var voter in allVoters) + while (true) { - voters[voter] = new List(); - } + var preferredVotes = GetPreferredCounts(localRankings); - foreach (var vote in ordered) - { - string voteContent = VoteString.GetVoteContent(vote.Key); + if (!preferredVotes.Any()) + break; - string normVote = allVotes.FirstOrDefault(v => StringUtility.AgnosticStringComparer.Equals(v, voteContent)) ?? voteContent; + ChoiceCount best = preferredVotes.MaxObject(a => a.Count); - foreach (var voter in vote.Value) - { - voters[voter].Add(normVote); - } - } + if (best.Count >= winCount) + return best.Choice; - return voters; - } + // If no more choice removals will bump up lower prefs to higher prefs, return the best of what's left. + if (!localRankings.Any(r => r.RankedVotes.Count() > 1)) + return best.Choice; - /// - /// Gets a dictionary of lists of all vote options that each voter did not rank. - /// - /// The collection of all choices that each voter did rank. - /// The list of all possible vote options. - /// Returns a dictionary collection of all options each voter did not rank. - private static Dictionary> GetNonChoices(Dictionary> voterChoices, HashSet allVotes) - { - Dictionary> voterNonChoices = new Dictionary>(); + string leastPreferredChoice = GetLeastPreferredChoice(localRankings); - foreach (var voter in voterChoices) - { - var nonChoices = allVotes.Except(voter.Value); - voterNonChoices.Add(voter.Key, nonChoices.ToList()); + RemoveChoiceFromVotes(localRankings, leastPreferredChoice); } - return voterNonChoices; + return null; } /// - /// Select the top option out of the votes cast for a given task. + /// Removes a list of choices from voter rankings. + /// These are the choices that have already won a rank spot. /// - /// The voter choices. - /// The voter non choices. - /// The original voters choices. - /// The vote list. - /// - /// Returns the top voter choice for the task. - /// - private static string GetTopRank(Dictionary> voterChoices, - Dictionary> voterNonChoices, - Dictionary> originalVotersChoices, - HashSet voteList) + /// The voter rankings. + /// The already chosen choices. + /// Returns the results as a list. + private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) { - // Skip processing if there's nothing to count. - if (voterChoices == null || voterChoices.Count == 0 || voterChoices.All(a => a.Value.Count == 0)) - return string.Empty; - - int loopLimit = voterChoices.Count(a => a.Value.Count > 0) + 10; - // Limit to 10 iterations, to ensure there are no infinite loops - int loop = 0; - - while (true) - { - // First see if any options has a majority of #1 votes - // Get a list of all top-choice votes, and how many votes for each option. - var firstChoices = CountFirstPlaceVotes(voterChoices); - - // Of those, get the 'best' choice, which is the one with the - // most votes, or, in the case of a tie, the one with the highest - // ranking score. - var bestChoice = GetFirstChoice(firstChoices, originalVotersChoices); - - // If we have a majority selection, that's the winner. - if (IsMajority(firstChoices[bestChoice], voterChoices.Count)) - { - return bestChoice; - } - - // If we're out of other choices, or we've gone too many loops, use what we have. - if (OnlyOneChoiceLeft(voterChoices) || ++loop >= loopLimit) - { - return bestChoice; - } - - System.Diagnostics.Debug.WriteLine("-- No majority option."); - - // If no option has an absolute majority, find the most-disliked option - // and remove it from all voters' option lists, in preparation of another - // round of checks. - string lastChoice = GetLastChoice(voterChoices, voterNonChoices, voteList); - - // Remove the last place option before running another round - RemoveLastPlaceOption(lastChoice, voterChoices, voterNonChoices); - } + var res = from voter in voterRankings + select new VoterRankingsL + { + Voter = voter.Voter, + RankedVotes = voter.RankedVotes.Where(v => chosenChoices.Contains(v.Vote) == false).OrderBy(v => v.Rank).ToList() + }; + + return res.ToList(); } /// - /// + /// Adds ranking entries for any choices that users did not explictly rank. + /// Modifies the provided list. /// - /// - /// - /// - private static string GetFirstChoice(Dictionary choices, - Dictionary> originalVotersChoices) + /// The vote rankings. + /// All available choices. + private void AddUnselectedRankings(List localRankings, RankResults allChoices) { - foreach (var c in choices) + foreach (var ranker in localRankings) { - System.Diagnostics.Debug.WriteLine($"--- {c.Key} @ {c.Value}"); - } - - int highestNumberOfChoices = choices.Max(a => a.Value); + if (ranker.RankedVotes.Count == allChoices.Count) + continue; - // Get the list of all choices that have the same total (max) number of selections - var choicesWithMostVotes = choices.Where(a => a.Value == highestNumberOfChoices).Select(b => b.Key); + var extras = allChoices.Except(ranker.RankedVotes.Select(v => v.Vote)); - if (choicesWithMostVotes.Count() == 1) - return choicesWithMostVotes.First(); - - return GetHighestScoreOption(choicesWithMostVotes, originalVotersChoices); - } - - private static string GetLastChoice(Dictionary> voterChoices, - Dictionary> voterNonChoices, HashSet voteList) - { - Dictionary votesWeight = new Dictionary(StringUtility.AgnosticStringComparer); - int distinctCount = voteList.Count(); - - int highestNumberOfChoices = voterChoices.Max(a => a.Value.Count); - int nonChoiceWeight = distinctCount > highestNumberOfChoices ? highestNumberOfChoices + 1 : distinctCount; - - HashSet lastPlaceList = new HashSet(StringUtility.AgnosticStringComparer); - - foreach (var vote in voteList) - { - votesWeight[vote] = 0; - } - - foreach (var voter in voterChoices) - { - int index = 1; - foreach (var vote in voter.Value) - { - votesWeight[vote] += index++; - } - - if (voterNonChoices[voter.Key].Count == 0 && voter.Value.Count > 0) + foreach (var extra in extras) { - lastPlaceList.Add(voter.Value.Last()); + ranker.RankedVotes.Add(new RankedVote { Vote = extra, Rank = 10 }); } } - - foreach (var voter in voterNonChoices) - { - foreach (var vote in voter.Value) - { - votesWeight[vote] += nonChoiceWeight; - - lastPlaceList.Add(vote); - } - } - - var least = votesWeight.Where(a => lastPlaceList.Contains(a.Key)). - OrderByDescending(a => a.Value).First().Key; - - return least; - } - - - private static string GetHighestScoreOption(IEnumerable choices, - Dictionary> originalVotersChoices) - { - var scores = from a in choices - select new { Choice = a, Score = GetScore(a, originalVotersChoices) }; - - int maxScore = scores.Max(a => a.Score); - - var withMaxScore = scores.Where(a => a.Score == maxScore); - - var pick = withMaxScore.OrderBy(a => a.Choice).Last(); - - return pick.Choice; - } - - private static string GetLowestScoreOption(IEnumerable choices, - Dictionary> originalVotersChoices) - { - var scores = from a in choices - select new { Choice = a, Score = GetScore(a, originalVotersChoices) }; - - int minScore = scores.Min(a => a.Score); - - var withMinScore = scores.Where(a => a.Score == minScore); - - var pick = withMinScore.OrderBy(a => a.Choice).Last(); - - return pick.Choice; - } - - private static int GetScore(string choice, Dictionary> originalVotersChoices) - { - int score = 0; - - foreach (var voter in originalVotersChoices) - { - int index = voter.Value.IndexOf(choice); - if (index >= 0) - { - score += (10 - index); - } - } - - return score; } /// - /// Count up the first choice options for all voters, and return a tally. + /// Filter the provided list of voter rankings to remove any instances of the specified choice. + /// Modifies the provided list. /// - /// The list of all voters and their ranked choices. - /// Returns the number of votes for each of the first-ranked vote options. - private static Dictionary CountFirstPlaceVotes(Dictionary> voterList) + /// The votes to filter. + /// The choice to remove. + private void RemoveChoiceFromVotes(List voterRankings, string choice) { - Dictionary voteCount = new Dictionary(StringComparer.OrdinalIgnoreCase); - - int count = 0; - - foreach (var vote in voterList) + foreach (var ranker in voterRankings) { - if (vote.Value.Count > 0) - { - string firstVote = vote.Value.First(); - - if (!voteCount.TryGetValue(firstVote, out count)) - { - count = 0; - } - - voteCount[firstVote] = ++count; - } + ranker.RankedVotes.RemoveAll(v => v.Vote == choice); } - - return voteCount; } /// - /// Count up the last choice options for all voters, and return a tally. + /// Gets the least preferred choice. /// - /// The list of all voters and their ranked choices. - /// Returns the number of votes for each of the last-ranked vote options. - private static Dictionary CountLastPlaceVotes(Dictionary> voterList, - Dictionary> voterNonChoices) + /// The vote rankings. + /// Returns the vote string for the least preferred vote. + private string GetLeastPreferredChoice(List localRankings) { - Dictionary voteCount = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary rankTotals = new Dictionary(); - int count = 0; - - foreach (var voter in voterList) + foreach (var voter in localRankings) { - var nonChoices = voterNonChoices[voter.Key]; - - if (nonChoices.Count > 0) + foreach (var rank in voter.RankedVotes) { - foreach (var nonChoice in nonChoices) - { - if (!voteCount.TryGetValue(nonChoice, out count)) - { - count = 0; - } - - voteCount[nonChoice] = ++count; - } - } - else - { - string lastVote = voter.Value.Last(); + if (!rankTotals.ContainsKey(rank.Vote)) + rankTotals[rank.Vote] = 0; - if (!voteCount.TryGetValue(lastVote, out count)) - { - count = 0; - } - - voteCount[lastVote] = ++count; + rankTotals[rank.Vote] += rank.Rank; } } - return voteCount; - } - - /// - /// Remove the specified vote option from all voters' option lists. - /// May not remove the last vote option from a voter. - /// - /// The last place option that's being removed. - /// The list of all voters. - private static void RemoveLastPlaceOption(string bottomChoice, Dictionary> voterList, - Dictionary> voterNonChoices) - { - System.Diagnostics.Debug.WriteLine($"-- Eliminate [{bottomChoice}]"); - - foreach (var voter in voterList) - { - if (voter.Value.Count > 1) - { - voter.Value.Remove(bottomChoice); - } - } + var maxRank = rankTotals.MaxObject(a => a.Value); - foreach (var voter in voterNonChoices) - { - voter.Value.Remove(bottomChoice); - } + return maxRank.Key; } /// - /// Remove an entry unconditionally from the list of voter choices. + /// Gets the count of the number of times a given vote is the most preferred option + /// among the provided voters. /// - /// The choice to remove. - /// The list of all voter ranked choices. - private static void RemoveChoice(string choice, Dictionary> voterChoices) + /// The list of voters and their rankings of each option. + /// Returns a collection of Choice/Count objects. + private IEnumerable GetPreferredCounts(IEnumerable voterRankings) { - foreach (var voter in voterChoices) - { - voter.Value.RemoveAll(a => a == choice); - } + var preferredVotes = from voter in voterRankings + let preferred = voter.RankedVotes.First().Vote + group voter by preferred into preffed + select new ChoiceCount { Choice = preffed.Key, Count = preffed.Count() }; + + return preferredVotes; } /// - /// Check to see if all voters only have one voting option left. + /// Gets all choices from all user votes. /// - /// List of all voters and their choices. - /// Returns true if all voters only have one choice left. - private static bool OnlyOneChoiceLeft(Dictionary> voterList) + /// The collection of user votes. + /// Returns a list of all the choices in the task. + private List GetAllChoices(IEnumerable rankings) { - if (voterList.All(a => a.Value.Count == 1)) - return true; + var res = rankings.SelectMany(r => r.RankedVotes).Select(r => r.Vote).Distinct(); - return false; + return res.ToList(); } - - /// - /// Determine if the given number of voters qualifies as a majority out of - /// all possible voters. - /// - /// Number of voters being checked. - /// Total number of voters. - /// Returns true if the number of voters qualifies as a majority. - private static bool IsMajority(int voters, int totalVoters) => ((double)voters / totalVoters) > 0.5; } } From f2fe7e5a985735ad57eb35fd5f0888cf42ebf7b6 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:08:05 -0500 Subject: [PATCH 18/77] Remove extended VoterRankingsL class and put everything into VoterRankings, using a List<>. --- .../RankVoteCounting/CoombsRankVoteCounter.cs | 14 +++++++------- .../InstantRunoffRankVoteCounter.cs | 2 +- .../RankVoteCounting/Utility/GroupRankVotes.cs | 18 ++++++------------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index 3252c668..99083328 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -89,7 +89,7 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu throw new ArgumentNullException(nameof(chosenChoices)); // Initial conversion from enumerable to list - List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); + List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); AddUnselectedRankings(localRankings, allChoices); @@ -127,10 +127,10 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu /// The voter rankings. /// The already chosen choices. /// Returns the results as a list. - private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) + private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) { var res = from voter in voterRankings - select new VoterRankingsL + select new VoterRankings { Voter = voter.Voter, RankedVotes = voter.RankedVotes.Where(v => chosenChoices.Contains(v.Vote) == false).OrderBy(v => v.Rank).ToList() @@ -145,7 +145,7 @@ private List RemoveChoicesFromVotes(IEnumerable v /// /// The vote rankings. /// All available choices. - private void AddUnselectedRankings(List localRankings, RankResults allChoices) + private void AddUnselectedRankings(List localRankings, RankResults allChoices) { foreach (var ranker in localRankings) { @@ -167,7 +167,7 @@ private void AddUnselectedRankings(List localRankings, RankResul /// /// The votes to filter. /// The choice to remove. - private void RemoveChoiceFromVotes(List voterRankings, string choice) + private void RemoveChoiceFromVotes(List voterRankings, string choice) { foreach (var ranker in voterRankings) { @@ -180,7 +180,7 @@ private void RemoveChoiceFromVotes(List voterRankings, string ch /// /// The vote rankings. /// Returns the vote string for the least preferred vote. - private string GetLeastPreferredChoice(List localRankings) + private string GetLeastPreferredChoice(List localRankings) { Dictionary rankTotals = new Dictionary(); @@ -206,7 +206,7 @@ private string GetLeastPreferredChoice(List localRankings) /// /// The list of voters and their rankings of each option. /// Returns a collection of Choice/Count objects. - private IEnumerable GetPreferredCounts(IEnumerable voterRankings) + private IEnumerable GetPreferredCounts(IEnumerable voterRankings) { var preferredVotes = from voter in voterRankings let preferred = voter.RankedVotes.First().Vote diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs index 6f21f0e2..9b93f774 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -121,7 +121,7 @@ private IEnumerable RemoveChoiceFromVotes(IEnumerable v.Vote != choice) + RankedVotes = voter.RankedVotes.Where(v => v.Vote != choice).ToList() }; return res; diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 8b08427a..69aaac53 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -28,12 +28,6 @@ public class RankedVote } public class VoterRankings - { - public string Voter { get; set; } - public IEnumerable RankedVotes { get; set; } - } - - public class VoterRankingsL { public string Voter { get; set; } public List RankedVotes { get; set; } @@ -65,12 +59,12 @@ group vote by voter into voters select new VoterRankings { Voter = voters.Key, - RankedVotes = from v in voters - select new RankedVote - { - Rank = RankAsInt(VoteString.GetVoteMarker(v.Key)), - Vote = VoteString.GetVoteContent(v.Key) - } + RankedVotes = (from v in voters + select new RankedVote + { + Rank = RankAsInt(VoteString.GetVoteMarker(v.Key)), + Vote = VoteString.GetVoteContent(v.Key) + }).ToList() }; return res; From 48c62934030d92653194c3a4a732fec27730a2a9 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:09:09 -0500 Subject: [PATCH 19/77] Remove unnecessary usings. --- .../VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index 99083328..31f16e52 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -2,18 +2,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; using NetTally.Utility; namespace NetTally.VoteCounting { // List of preference results ordered by winner using RankResults = List; - // Task (string), Ordered list of ranked votes - using RankResultsByTask = Dictionary>; - // Vote (string), collection of voters - using SupportedVotes = Dictionary>; // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; From b43c2f52273e29601fd045daaae0474ec2d4d25f Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:18:02 -0500 Subject: [PATCH 20/77] Convert IEnumerables to Lists, using code developed to fix Coombs slow speed. --- .../InstantRunoffRankVoteCounter.cs | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs index 9b93f774..cb6faa49 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -72,19 +72,14 @@ protected override RankResults RankTask(GroupedVotesByTask task) /// Returns the winning vote, if any. Otherwise, null. /// /// - private string GetWinningVote(IEnumerable voterRankings, IEnumerable chosenChoices) + private string GetWinningVote(IEnumerable voterRankings, RankResults chosenChoices) { if (voterRankings == null) throw new ArgumentNullException(nameof(voterRankings)); if (chosenChoices == null) throw new ArgumentNullException(nameof(chosenChoices)); - var localRankings = voterRankings; - - foreach (var choice in chosenChoices) - { - localRankings = RemoveChoiceFromVotes(localRankings, choice); - } + List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); int voterCount = localRankings.Count(v => v.RankedVotes.Any()); int winCount = (int)Math.Ceiling(voterCount * 0.50011); @@ -103,28 +98,43 @@ private string GetWinningVote(IEnumerable voterRankings, IEnumera var worst = preferredVotes.MinObject(a => a.Count); - localRankings = RemoveChoiceFromVotes(localRankings, worst.Choice); + RemoveChoiceFromVotes(localRankings, worst.Choice); } return null; } /// - /// Filter the provided list of voter rankings to remove any instances of the specified choice. + /// Removes a list of choices from voter rankings. + /// These are the choices that have already won a rank spot. /// - /// The votes to filter. - /// The choice to remove. - /// Returns the list without the given choice in the voters' rankings. - private IEnumerable RemoveChoiceFromVotes(IEnumerable voterRankings, string choice) + /// The voter rankings. + /// The already chosen choices. + /// Returns the results as a list. + private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) { var res = from voter in voterRankings select new VoterRankings { Voter = voter.Voter, - RankedVotes = voter.RankedVotes.Where(v => v.Vote != choice).ToList() + RankedVotes = voter.RankedVotes.Where(v => chosenChoices.Contains(v.Vote) == false).OrderBy(v => v.Rank).ToList() }; - return res; + return res.ToList(); + } + + /// + /// Filter the provided list of voter rankings to remove any instances of the specified choice. + /// + /// The votes to filter. + /// The choice to remove. + /// Returns the list without the given choice in the voters' rankings. + private void RemoveChoiceFromVotes(IEnumerable voterRankings, string choice) + { + foreach (var ranker in voterRankings) + { + ranker.RankedVotes.RemoveAll(v => v.Vote == choice); + } } /// From 63663f9e49efec9fa0407c7998f4aa5393cafb77 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:20:50 -0500 Subject: [PATCH 21/77] Remove unnecessary usings. --- .../RankVoteCounting/InstantRunoffRankVoteCounter.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs index cb6faa49..6235cafd 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -2,18 +2,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; using NetTally.Utility; namespace NetTally.VoteCounting { // List of preference results ordered by winner using RankResults = List; - // Task (string), Ordered list of ranked votes - using RankResultsByTask = Dictionary>; - // Vote (string), collection of voters - using SupportedVotes = Dictionary>; // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; From 9566b54928f0d52fbbbd6475139815441c9e6ff9 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:51:31 -0500 Subject: [PATCH 22/77] Add a converter method for the rank algorithm enum. --- NetTally/Config/BindingConverters.cs | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/NetTally/Config/BindingConverters.cs b/NetTally/Config/BindingConverters.cs index 712728c1..49e45174 100644 --- a/NetTally/Config/BindingConverters.cs +++ b/NetTally/Config/BindingConverters.cs @@ -231,6 +231,44 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu } } + /// + /// Data binding conversion class to convert a PartitionMode enum to + /// an index value or back. + /// + [ValueConversion(typeof(RankVoteCounterMethod), typeof(int))] + public class RankCountingModeConverter : IValueConverter + { + /// + /// Convert from source (property enum) to target (control index). + /// + /// Returns whether the specified target control value should be on or off. + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is RankVoteCounterMethod) + { + return (int)value; + } + + return -1; + } + + /// + /// Convert from target (control index) to source (property enum). + /// + /// Returns what the source property value should be set to + /// based on the target value. + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int) + { + return (RankVoteCounterMethod)value; + } + + return RankVoteCounterMethod.Default; + } + } + + /// /// Data binding conversion class to return the OR state of all the objects From 7a8a029e67e006baabe3569b731dbf6a4ef3d877 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:52:23 -0500 Subject: [PATCH 23/77] Add a global option var for the rank vote algorithm method. --- TallyCore/Global/AdvancedOptions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/TallyCore/Global/AdvancedOptions.cs b/TallyCore/Global/AdvancedOptions.cs index ad5b6072..df09f9ab 100644 --- a/TallyCore/Global/AdvancedOptions.cs +++ b/TallyCore/Global/AdvancedOptions.cs @@ -36,6 +36,7 @@ protected void OnPropertyChanged([CallerMemberName] string propertyName = null) DisplayMode displayMode = DisplayMode.Normal; bool allowRankedVotes = true; + RankVoteCounterMethod rankVoteCounterMethod = RankVoteCounterMethod.Default; [Obsolete("Invert usage")] bool allowVoteLabelPlanNames = true; @@ -56,6 +57,7 @@ protected void OnPropertyChanged([CallerMemberName] string propertyName = null) #region Constants for string descriptions public const string _displayMode = "displayMode"; public const string _allowRankedVotes = "allowRankedVotes"; + public const string _rankVoteCounterMethod = "rankVoteCounterMethod"; public const string _forbidVoteLabelPlanNames = "forbidVoteLabelPlanNames"; public const string _whitespaceAndPunctuationIsSignificant = "whitespaceAndPunctuationIsSignificant"; public const string _disableProxyVotes = "disableProxyVotes"; @@ -79,6 +81,22 @@ public bool AllowRankedVotes OnPropertyChanged(); } } + + /// + /// Gets or sets the rank vote counter method. + /// + /// + /// The rank vote counter method. + /// + public RankVoteCounterMethod RankVoteCounterMethod + { + get { return rankVoteCounterMethod; } + set + { + rankVoteCounterMethod = value; + OnPropertyChanged(); + } + } #endregion #region Formatting Options From bb96e28bc145b4f06eecc068a66bb568149e8915 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:52:37 -0500 Subject: [PATCH 24/77] Adjust enum descrips. --- TallyCore/Global/Enumerations.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 00725ee8..694982bf 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -20,17 +20,17 @@ public enum VoteType public enum RankVoteCounterMethod { - [EnumDescription("Default (Coombs)")] + [EnumDescription("Default (Schulze)")] Default, [EnumDescription("Borda")] BordaCount, [EnumDescription("Borda (Fraction)")] BordaFraction, - [EnumDescription("Coombs'")] + [EnumDescription("Coombs' Runoff")] Coombs, [EnumDescription("Instant Runoff")] InstantRunoff, - [EnumDescription("Schulze")] + [EnumDescription("Schulze Method")] Schulze, } From d0799a51ac6e62a6dfa152ca7dfcdca0167f152b Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:53:26 -0500 Subject: [PATCH 25/77] Add the enum string list to the main view model. Adjust the data context for the global options window on creation. --- NetTally/MainWindow.xaml.cs | 2 +- TallyCore/ViewModels/MainViewModel.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/NetTally/MainWindow.xaml.cs b/NetTally/MainWindow.xaml.cs index 6522c7a5..3d1b740a 100644 --- a/NetTally/MainWindow.xaml.cs +++ b/NetTally/MainWindow.xaml.cs @@ -265,7 +265,7 @@ private void globalOptionsButton_Click(object sender, RoutedEventArgs e) GlobalOptionsWindow options = new GlobalOptionsWindow { Owner = Application.Current.MainWindow, - DataContext = MainViewModel.Options + DataContext = MainViewModel }; options.ShowDialog(); diff --git a/TallyCore/ViewModels/MainViewModel.cs b/TallyCore/ViewModels/MainViewModel.cs index f47a5e44..7c09fc6d 100644 --- a/TallyCore/ViewModels/MainViewModel.cs +++ b/TallyCore/ViewModels/MainViewModel.cs @@ -126,6 +126,11 @@ private void CheckForNewRelease_PropertyChanged(object sender, PropertyChangedEv /// public List PartitionModes { get; } = Enumerations.EnumDescriptionsList().ToList(); + /// + /// Gets the user-readable list of rank counting modes, for use in the view. + /// + public List RankVoteCountingModes { get; } = Enumerations.EnumDescriptionsList().ToList(); + /// /// Gets the user-readable list of valid posts per page, for use in the view. /// From 966727d2dbf4d2d8a132ccd89f127e1a6d2f0bbe Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:54:03 -0500 Subject: [PATCH 26/77] Add dropdown for selecting rank counting algorithm to global options window. Adjust paths to point to Options because of change in data context. --- NetTally/GlobalOptionsWindow.xaml | 66 +++++++++++++++++++------------ 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/NetTally/GlobalOptionsWindow.xaml b/NetTally/GlobalOptionsWindow.xaml index 620e35eb..d9f5ae90 100644 --- a/NetTally/GlobalOptionsWindow.xaml +++ b/NetTally/GlobalOptionsWindow.xaml @@ -5,10 +5,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:NetTally" mc:Ignorable="d" - Title="Global Options" Height="480" Width="640" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" Icon="CheckVoteWin.ico" ShowInTaskbar="False"> + Title="Global Options" Height="440.339" Width="640" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" Icon="CheckVoteWin.ico" ShowInTaskbar="False"> + @@ -17,39 +18,36 @@ - + - - From 4f2ce06f434781051dfd73e884ae2c710fd973a0 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:56:20 -0500 Subject: [PATCH 27/77] Adjust output to select the appropriate rank calculation algorithm class based on the global var setting. --- TallyCore/Output/TallyOutput.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/TallyCore/Output/TallyOutput.cs b/TallyCore/Output/TallyOutput.cs index 52b9f592..028a2b30 100644 --- a/TallyCore/Output/TallyOutput.cs +++ b/TallyCore/Output/TallyOutput.cs @@ -7,6 +7,9 @@ namespace NetTally.Output { + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + /// /// Class to handle generating the output of a tally run, for display in the text box. /// @@ -80,11 +83,13 @@ private void ConstructRankedOutput() { if (VoteCounter.Instance.HasRankedVotes) { - // Get ranked results, and order them by task name - var results = RankVotes.Rank().OrderBy(a => a.Key); + VoteCounting.IRankVoteCounter counter = VoteCounting.VoteCounterLocator.GetRankVoteCounter(AdvancedOptions.Instance.RankVoteCounterMethod); + RankResultsByTask results = counter.CountVotes(VoteCounter.Instance.GetVotesCollection(VoteType.Rank)); + + var orderedRes = results.OrderBy(a => a.Key); // Output the ranking results for each task - foreach (var task in results) + foreach (var task in orderedRes) { AddRankTask(task); sb.AppendLine(""); From 62d3c22bf0a1935958f098b0388f8c09edd16a70 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 01:58:49 -0500 Subject: [PATCH 28/77] Assign a reset value. --- NetTally/GlobalOptionsWindow.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/NetTally/GlobalOptionsWindow.xaml.cs b/NetTally/GlobalOptionsWindow.xaml.cs index e1147b60..9410285c 100644 --- a/NetTally/GlobalOptionsWindow.xaml.cs +++ b/NetTally/GlobalOptionsWindow.xaml.cs @@ -20,6 +20,7 @@ private void closeButton_Click(object sender, RoutedEventArgs e) private void resetAllButton_Click(object sender, RoutedEventArgs e) { allowRankedVotes.IsChecked = true; + rankedVoteAlgorithm.SelectedIndex = 0; forbidVoteLabelPlanNames.IsChecked = false; whitespaceAndPunctuationIsSignificant.IsChecked = false; From 0780e57299d81189c9d656c37ef75cfc120a7e46 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 20:28:55 -0500 Subject: [PATCH 29/77] Correct the current default description. --- TallyCore/Global/Enumerations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 694982bf..b3539779 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -20,7 +20,7 @@ public enum VoteType public enum RankVoteCounterMethod { - [EnumDescription("Default (Schulze)")] + [EnumDescription("Default (Coombs)")] Default, [EnumDescription("Borda")] BordaCount, From 32f6f73b8da7c17837aeab722701fe78e8b6984c Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 16 May 2016 20:29:44 -0500 Subject: [PATCH 30/77] Add debug output showing the elimination sequence for each winner. --- .../RankVoteCounting/CoombsRankVoteCounter.cs | 46 ++++++++++++++----- .../InstantRunoffRankVoteCounter.cs | 39 ++++++++++++---- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index 31f16e52..1a655a26 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -90,30 +90,52 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu int voterCount = localRankings.Count(); int winCount = (int)Math.Ceiling(voterCount * 0.50011); - while (true) + try { - var preferredVotes = GetPreferredCounts(localRankings); + Debug.Write("Eliminations: ["); + bool eliminateOne = false; - if (!preferredVotes.Any()) - break; + while (true) + { + var preferredVotes = GetPreferredCounts(localRankings); + + if (!preferredVotes.Any()) + break; - ChoiceCount best = preferredVotes.MaxObject(a => a.Count); + ChoiceCount best = preferredVotes.MaxObject(a => a.Count); - if (best.Count >= winCount) - return best.Choice; + if (best.Count >= winCount) + return best.Choice; - // If no more choice removals will bump up lower prefs to higher prefs, return the best of what's left. - if (!localRankings.Any(r => r.RankedVotes.Count() > 1)) - return best.Choice; + // If no more choice removals will bump up lower prefs to higher prefs, return the best of what's left. + if (!localRankings.Any(r => r.RankedVotes.Count() > 1)) + return best.Choice; - string leastPreferredChoice = GetLeastPreferredChoice(localRankings); + string leastPreferredChoice = GetLeastPreferredChoice(localRankings); - RemoveChoiceFromVotes(localRankings, leastPreferredChoice); + Debug.Write($"{Comma(eliminateOne)}{leastPreferredChoice}"); + + RemoveChoiceFromVotes(localRankings, leastPreferredChoice); + eliminateOne = true; + } + } + finally + { + Debug.WriteLine("]"); } return null; } + private string Comma(bool addComma) + { + if (addComma) + return ", "; + else + return ""; + } + + /// /// Removes a list of choices from voter rankings. /// These are the choices that have already won a rank spot. diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs index 6235cafd..7827b7e6 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -78,26 +78,47 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu int voterCount = localRankings.Count(v => v.RankedVotes.Any()); int winCount = (int)Math.Ceiling(voterCount * 0.50011); - while (true) + try { - var preferredVotes = GetPreferredCounts(localRankings); + Debug.Write("Eliminations: ["); + bool eliminateOne = false; - if (!preferredVotes.Any()) - break; + while (true) + { + var preferredVotes = GetPreferredCounts(localRankings); + + if (!preferredVotes.Any()) + break; - var best = preferredVotes.MaxObject(a => a.Count); + var best = preferredVotes.MaxObject(a => a.Count); - if (best.Count >= winCount) - return best.Choice; + if (best.Count >= winCount) + return best.Choice; - var worst = preferredVotes.MinObject(a => a.Count); + var worst = preferredVotes.MinObject(a => a.Count); - RemoveChoiceFromVotes(localRankings, worst.Choice); + Debug.Write($"{Comma(eliminateOne)}{worst.Choice}"); + + RemoveChoiceFromVotes(localRankings, worst.Choice); + eliminateOne = true; + } + } + finally + { + Debug.WriteLine("]"); } return null; } + private string Comma(bool addComma) + { + if (addComma) + return ", "; + else + return ""; + } + /// /// Removes a list of choices from voter rankings. /// These are the choices that have already won a rank spot. From 304cca1b7a9e8419865e8cfb12121f875ba45c8a Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 01:03:36 -0500 Subject: [PATCH 31/77] Initialization loop when getting strongest paths isn't needed. Can just blockcopy the source array, then modify it. This keeps data during the in-between stage that may be useful for debugging, and is much faster. --- .../SchulzeRankVoteCounter.cs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs index 4aa47f8e..a3118d40 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs @@ -92,23 +92,8 @@ protected override RankResults RankTask(GroupedVotesByTask task) { int[,] strongestPaths = new int[choicesCount, choicesCount]; - for (int i = 0; i < choicesCount; i++) - { - for (int j = 0; j < choicesCount; j++) - { - if (i != j) - { - if (pairwisePreferences[i, j] >= pairwisePreferences[j, i]) - { - strongestPaths[i, j] = pairwisePreferences[i, j]; - } - else - { - strongestPaths[i, j] = 0; - } - } - } - } + int bytesInArray = strongestPaths.Length * sizeof(Int32); + Buffer.BlockCopy(pairwisePreferences, 0, strongestPaths, 0, bytesInArray); for (int i = 0; i < choicesCount; i++) { From 83079a9550d7a0e332bb8e005a116f7554a4c309 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 14:38:36 -0500 Subject: [PATCH 32/77] Remove Borda rank counting options. --- TallyCore/Global/Enumerations.cs | 4 ---- .../RankVoteCounting/BordaFractionRankVoteCounter.cs | 7 +++++++ .../VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs | 7 +++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 4 ---- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index b3539779..c964ab82 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -22,10 +22,6 @@ public enum RankVoteCounterMethod { [EnumDescription("Default (Coombs)")] Default, - [EnumDescription("Borda")] - BordaCount, - [EnumDescription("Borda (Fraction)")] - BordaFraction, [EnumDescription("Coombs' Runoff")] Coombs, [EnumDescription("Instant Runoff")] diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs index 7909646e..27491a1a 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs @@ -14,6 +14,13 @@ namespace NetTally.VoteCounting // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; + /// + /// Borda is being removed as a valid option from the list of rank vote options. + /// Aside from systemic failures of the method itself, it cannot give proper + /// valuation to unranked options, which intrinsically makes it a bad fit + /// for handling user-entered quest voting schemes. + /// + /// public class BordaFractionRankVoteCounter : BaseRankVoteCounter { /// diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs index c9dffd63..1b4354de 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -14,6 +14,13 @@ namespace NetTally.VoteCounting // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; + /// + /// Borda is being removed as a valid option from the list of rank vote options. + /// Aside from systemic failures of the method itself, it cannot give proper + /// valuation to unranked options, which intrinsically makes it a bad fit + /// for handling user-entered quest voting schemes. + /// + /// public class BordaRankVoteCounter : BaseRankVoteCounter { /// diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index 9abcfbe1..9f9d1fc6 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -43,10 +43,6 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = { case RankVoteCounterMethod.Coombs: return new CoombsRankVoteCounter(); - case RankVoteCounterMethod.BordaCount: - return new BordaRankVoteCounter(); - case RankVoteCounterMethod.BordaFraction: - return new BordaFractionRankVoteCounter(); case RankVoteCounterMethod.InstantRunoff: return new InstantRunoffRankVoteCounter(); case RankVoteCounterMethod.Schulze: From 657fac744a04a51e28bd47b46ab4faa4e97cca8b Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 22:41:03 -0500 Subject: [PATCH 33/77] Simplify math for determining winning voter count. --- .../VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs | 2 +- .../RankVoteCounting/InstantRunoffRankVoteCounter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index 1a655a26..ee74b092 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -88,7 +88,7 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu AddUnselectedRankings(localRankings, allChoices); int voterCount = localRankings.Count(); - int winCount = (int)Math.Ceiling(voterCount * 0.50011); + int winCount = voterCount / 2 + 1; try { diff --git a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs index 7827b7e6..f10df358 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/InstantRunoffRankVoteCounter.cs @@ -76,7 +76,7 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); int voterCount = localRankings.Count(v => v.RankedVotes.Any()); - int winCount = (int)Math.Ceiling(voterCount * 0.50011); + int winCount = voterCount / 2 + 1; try { From bad8d479fdb89f8fad68f835039ac2bd983bd536 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 22:44:37 -0500 Subject: [PATCH 34/77] Current implementation of Coombs is actually Baldwin. Create class for that, so that Coombs can be corrected. --- TallyCore/Global/Enumerations.cs | 6 +- TallyCore/NetTally.Core.csproj | 1 + .../BaldwinRankVoteCounter.cs | 247 ++++++++++++++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 + 4 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index c964ab82..4da71cbd 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -22,10 +22,12 @@ public enum RankVoteCounterMethod { [EnumDescription("Default (Coombs)")] Default, - [EnumDescription("Coombs' Runoff")] - Coombs, [EnumDescription("Instant Runoff")] InstantRunoff, + [EnumDescription("Coombs' Method")] + Coombs, + [EnumDescription("Baldwin Method")] + Baldwin, [EnumDescription("Schulze Method")] Schulze, } diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index a3517a72..84af259c 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -79,6 +79,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs new file mode 100644 index 00000000..d4ba6509 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NetTally.Utility; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + + public class BaldwinRankVoteCounter : BaseRankVoteCounter + { + /// + /// Local class to store a choice/count combo of fields for LINQ. + /// + protected class ChoiceCount + { + public string Choice { get; set; } + public int Count { get; set; } + + public override string ToString() => $"{Choice}: {Count}"; + } + + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + + List winningChoices = new List(); + + if (task.Any()) + { + Debug.WriteLine(">>Coombs Runoff<<"); + + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + var allChoices = GetAllChoices(voterRankings); + + for (int i = 1; i <= 9; i++) + { + string winner = GetWinningVote(voterRankings, winningChoices, allChoices); + + if (winner == null) + break; + + winningChoices.Add(winner); + allChoices.Remove(winner); + + Debug.WriteLine($"- {winner}"); + + if (!allChoices.Any()) + break; + } + } + + return winningChoices; + } + + /// + /// Gets the winning vote. + /// Excludes any already chosen votes from the process. + /// + /// The voter rankings. + /// The already chosen choices. + /// All remaining choices. + /// Returns the winning vote. + /// + /// + private string GetWinningVote(IEnumerable voterRankings, RankResults chosenChoices, RankResults allChoices) + { + if (voterRankings == null) + throw new ArgumentNullException(nameof(voterRankings)); + if (chosenChoices == null) + throw new ArgumentNullException(nameof(chosenChoices)); + + // Initial conversion from enumerable to list + List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); + + AddUnselectedRankings(localRankings, allChoices); + + int voterCount = localRankings.Count(); + int winCount = voterCount / 2 + 1; + + try + { + Debug.Write("Eliminations: ["); + bool eliminateOne = false; + + while (true) + { + var preferredVotes = GetPreferredCounts(localRankings); + + if (!preferredVotes.Any()) + break; + + ChoiceCount best = preferredVotes.MaxObject(a => a.Count); + + if (best.Count >= winCount) + return best.Choice; + + // If no more choice removals will bump up lower prefs to higher prefs, return the best of what's left. + if (!localRankings.Any(r => r.RankedVotes.Count() > 1)) + return best.Choice; + + string leastPreferredChoice = GetLeastPreferredChoice(localRankings); + + Debug.Write($"{Comma(eliminateOne)}{leastPreferredChoice}"); + + RemoveChoiceFromVotes(localRankings, leastPreferredChoice); + eliminateOne = true; + } + } + finally + { + Debug.WriteLine("]"); + } + + return null; + } + + private string Comma(bool addComma) + { + if (addComma) + return ", "; + else + return ""; + } + + + /// + /// Removes a list of choices from voter rankings. + /// These are the choices that have already won a rank spot. + /// + /// The voter rankings. + /// The already chosen choices. + /// Returns the results as a list. + private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) + { + var res = from voter in voterRankings + select new VoterRankings + { + Voter = voter.Voter, + RankedVotes = voter.RankedVotes.Where(v => chosenChoices.Contains(v.Vote) == false).OrderBy(v => v.Rank).ToList() + }; + + return res.ToList(); + } + + /// + /// Adds ranking entries for any choices that users did not explictly rank. + /// Modifies the provided list. + /// + /// The vote rankings. + /// All available choices. + private void AddUnselectedRankings(List localRankings, RankResults allChoices) + { + foreach (var ranker in localRankings) + { + if (ranker.RankedVotes.Count == allChoices.Count) + continue; + + var extras = allChoices.Except(ranker.RankedVotes.Select(v => v.Vote)); + + foreach (var extra in extras) + { + ranker.RankedVotes.Add(new RankedVote { Vote = extra, Rank = 10 }); + } + } + } + + /// + /// Filter the provided list of voter rankings to remove any instances of the specified choice. + /// Modifies the provided list. + /// + /// The votes to filter. + /// The choice to remove. + private void RemoveChoiceFromVotes(List voterRankings, string choice) + { + foreach (var ranker in voterRankings) + { + ranker.RankedVotes.RemoveAll(v => v.Vote == choice); + } + } + + /// + /// Gets the least preferred choice. + /// + /// The vote rankings. + /// Returns the vote string for the least preferred vote. + private string GetLeastPreferredChoice(List localRankings) + { + Dictionary rankTotals = new Dictionary(); + + foreach (var voter in localRankings) + { + foreach (var rank in voter.RankedVotes) + { + if (!rankTotals.ContainsKey(rank.Vote)) + rankTotals[rank.Vote] = 0; + + rankTotals[rank.Vote] += rank.Rank; + } + } + + var maxRank = rankTotals.MaxObject(a => a.Value); + + return maxRank.Key; + } + + /// + /// Gets the count of the number of times a given vote is the most preferred option + /// among the provided voters. + /// + /// The list of voters and their rankings of each option. + /// Returns a collection of Choice/Count objects. + private IEnumerable GetPreferredCounts(IEnumerable voterRankings) + { + var preferredVotes = from voter in voterRankings + let preferred = voter.RankedVotes.First().Vote + group voter by preferred into preffed + select new ChoiceCount { Choice = preffed.Key, Count = preffed.Count() }; + + return preferredVotes; + } + + /// + /// Gets all choices from all user votes. + /// + /// The collection of user votes. + /// Returns a list of all the choices in the task. + private List GetAllChoices(IEnumerable rankings) + { + var res = rankings.SelectMany(r => r.RankedVotes).Select(r => r.Vote).Distinct(); + + return res.ToList(); + } + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index 9f9d1fc6..0bfbb9e0 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -43,6 +43,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = { case RankVoteCounterMethod.Coombs: return new CoombsRankVoteCounter(); + case RankVoteCounterMethod.Baldwin: + return new BaldwinRankVoteCounter(); case RankVoteCounterMethod.InstantRunoff: return new InstantRunoffRankVoteCounter(); case RankVoteCounterMethod.Schulze: From da6e6267d68325b5ea547c99d18553239d30f2ac Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:35:35 -0500 Subject: [PATCH 35/77] Put Borda back in options; leave out fractional version. --- TallyCore/Global/Enumerations.cs | 2 ++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 4da71cbd..8f9bbd49 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -22,6 +22,8 @@ public enum RankVoteCounterMethod { [EnumDescription("Default (Coombs)")] Default, + [EnumDescription("Borda Count")] + Borda, [EnumDescription("Instant Runoff")] InstantRunoff, [EnumDescription("Coombs' Method")] diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index 0bfbb9e0..b4a590ad 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -47,6 +47,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = return new BaldwinRankVoteCounter(); case RankVoteCounterMethod.InstantRunoff: return new InstantRunoffRankVoteCounter(); + case RankVoteCounterMethod.Borda: + return new BordaRankVoteCounter(); case RankVoteCounterMethod.Schulze: return new SchulzeRankVoteCounter(); default: From bcce60659174f6ddeeb7310a4492ad6299f788a8 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:36:04 -0500 Subject: [PATCH 36/77] Rework Baldwin's for better overall math. --- .../BaldwinRankVoteCounter.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs index d4ba6509..f1ef84f5 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -40,7 +40,7 @@ protected override RankResults RankTask(GroupedVotesByTask task) if (task.Any()) { - Debug.WriteLine(">>Coombs Runoff<<"); + Debug.WriteLine(">>Baldwin Runoff<<"); var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); var allChoices = GetAllChoices(voterRankings); @@ -193,12 +193,22 @@ private void RemoveChoiceFromVotes(List voterRankings, string cho /// /// Gets the least preferred choice. + /// This is normally determined by selecting the option with the lowest Borda count. + /// This is inverted because we don't want to convert ranks to Borda values (it gains us nothing). + /// It then needs to be averaged across the number of instances of each vote, to + /// account for unranked options. This allows apples-to-apples comparisons against options + /// that are ranked in all votes. + /// We then need to scale it relative to the number of instances of that option appearing, to deal + /// with truncated rankings (where there are more options than rankings allowed). + /// An option ranked infrequently can be scaled up relative to its rate of occurance. /// /// The vote rankings. /// Returns the vote string for the least preferred vote. private string GetLeastPreferredChoice(List localRankings) { - Dictionary rankTotals = new Dictionary(); + Dictionary rankTotals = new Dictionary(); + Dictionary rankInstances = new Dictionary(); + HashSet choices = new HashSet(); foreach (var voter in localRankings) { @@ -206,14 +216,25 @@ private string GetLeastPreferredChoice(List localRankings) { if (!rankTotals.ContainsKey(rank.Vote)) rankTotals[rank.Vote] = 0; + if (!rankInstances.ContainsKey(rank.Vote)) + rankInstances[rank.Vote] = 0; rankTotals[rank.Vote] += rank.Rank; + rankInstances[rank.Vote]++; + choices.Add(rank.Vote); } } - var maxRank = rankTotals.MaxObject(a => a.Value); + var rankScaling = from choice in choices + select new + { + Choice = choice, + Value = rankTotals[choice] / rankInstances[choice] / rankInstances[choice] + }; - return maxRank.Key; + var maxResult = rankScaling.MaxObject(a => a.Value); + + return maxResult.Choice; } /// From 250301a4ecd1ab8a16bb3b72a8815f74ee899d3f Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:36:22 -0500 Subject: [PATCH 37/77] Rework Coombs back to proper original version. --- .../RankVoteCounting/CoombsRankVoteCounter.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index ee74b092..000400f0 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -198,20 +198,22 @@ private void RemoveChoiceFromVotes(List voterRankings, string cho /// Returns the vote string for the least preferred vote. private string GetLeastPreferredChoice(List localRankings) { - Dictionary rankTotals = new Dictionary(); + Dictionary rankCount = new Dictionary(); foreach (var voter in localRankings) { - foreach (var rank in voter.RankedVotes) - { - if (!rankTotals.ContainsKey(rank.Vote)) - rankTotals[rank.Vote] = 0; + var lowestRanked = voter.RankedVotes.MaxObject(a => a.Rank); - rankTotals[rank.Vote] += rank.Rank; - } + if (lowestRanked == null) + continue; + + if (!rankCount.ContainsKey(lowestRanked.Vote)) + rankCount[lowestRanked.Vote] = 0; + + rankCount[lowestRanked.Vote]++; } - var maxRank = rankTotals.MaxObject(a => a.Value); + var maxRank = rankCount.MaxObject(a => a.Value); return maxRank.Key; } From 87bd4e1d04c77e8965e7f137bb85da7ed2839035 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:36:47 -0500 Subject: [PATCH 38/77] Comments. --- .../VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs index a3118d40..8fdaed23 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs @@ -64,6 +64,7 @@ protected override RankResults RankTask(GroupedVotesByTask task) foreach (var choice in voter.RankedVotes) { + // Each choice matching or beating the ranks of other ranked choices is marked. foreach (var otherChoice in voter.RankedVotes) { if ((choice.Vote != otherChoice.Vote) && (choice.Rank < otherChoice.Rank)) @@ -72,6 +73,7 @@ protected override RankResults RankTask(GroupedVotesByTask task) } } + // Each choice is ranked higher than all unranked choices foreach (var nonChoice in unrankedChoices) { pairwisePreferences[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; From 3108dae35e469ad4e4751a8fb4e3e12f80a4f728 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:37:24 -0500 Subject: [PATCH 39/77] Equally ranked options get a pairwise bonus on both sides. --- .../VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs index 8fdaed23..30892fe1 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/SchulzeRankVoteCounter.cs @@ -67,7 +67,7 @@ protected override RankResults RankTask(GroupedVotesByTask task) // Each choice matching or beating the ranks of other ranked choices is marked. foreach (var otherChoice in voter.RankedVotes) { - if ((choice.Vote != otherChoice.Vote) && (choice.Rank < otherChoice.Rank)) + if ((choice.Vote != otherChoice.Vote) && (choice.Rank <= otherChoice.Rank)) { pairwisePreferences[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]]++; } From 449e64d75f26b411f0d000cff0f8eaf7fc4512f0 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:45:37 -0500 Subject: [PATCH 40/77] Add class that does pure pairwise winner. --- TallyCore/Global/Enumerations.cs | 2 + TallyCore/NetTally.Core.csproj | 1 + .../PairwiseRankVoteCounter.cs | 224 ++++++++++++++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 + 4 files changed, 229 insertions(+) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/PairwiseRankVoteCounter.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 8f9bbd49..05ee5e86 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -30,6 +30,8 @@ public enum RankVoteCounterMethod Coombs, [EnumDescription("Baldwin Method")] Baldwin, + [EnumDescription("Pairwise Elimination")] + Pairwise, [EnumDescription("Schulze Method")] Schulze, } diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index 84af259c..ed107cf8 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -86,6 +86,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/PairwiseRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/PairwiseRankVoteCounter.cs new file mode 100644 index 00000000..548bcbe6 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/PairwiseRankVoteCounter.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public class PairwiseRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task, based on the Schulze algorithm. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + + + Debug.WriteLine(">>Pairwise Ranking<<"); + + List listOfChoices = GetAllChoices(task); + + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + + int[,] pairwisePreferences = GetPairwisePreferences(voterRankings, listOfChoices); + + int[,] pairwiseWinners = GetPairwiseWinners(pairwisePreferences, listOfChoices.Count); + + RankResults winningChoices = GetResultsInOrder(pairwiseWinners, listOfChoices); + + return winningChoices; + } + + #region Schulze Algorithm + /// + /// Fills the pairwise preferences. + /// This goes through each voter's ranking options and updates an array indicating + /// which options are preferred over which other options. Each higher-ranked + /// option gains one point in 'beating' a lower-ranked option. + /// + /// The voter rankings. + /// The list of choices. + /// Returns a filled-in preferences array. + private int[,] GetPairwisePreferences(IEnumerable voterRankings, List listOfChoices) + { + int[,] pairwisePreferences = new int[listOfChoices.Count, listOfChoices.Count]; + + var choiceIndexes = GetChoicesIndexes(listOfChoices); + + foreach (var voter in voterRankings) + { + var rankedChoices = voter.RankedVotes.Select(v => v.Vote); + var unrankedChoices = listOfChoices.Except(rankedChoices); + + foreach (var choice in voter.RankedVotes) + { + // Each choice matching or beating the ranks of other ranked choices is marked. + foreach (var otherChoice in voter.RankedVotes) + { + if ((choice.Vote != otherChoice.Vote) && (choice.Rank <= otherChoice.Rank)) + { + pairwisePreferences[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]]++; + } + } + + // Each choice is ranked higher than all unranked choices + foreach (var nonChoice in unrankedChoices) + { + pairwisePreferences[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; + } + } + } + + return pairwisePreferences; + } + + /// + /// Gets the winning paths - The strongest of the strongest paths, for each pair option. + /// + /// The strongest paths. + /// The choices count (size of table). + /// Returns a table with the winning choices of the strongest paths. + private int[,] GetPairwiseWinners(int[,] pairwisePreferences, int choicesCount) + { + int[,] winningPaths = new int[choicesCount, choicesCount]; + + for (int i = 0; i < choicesCount; i++) + { + for (int j = 0; j < choicesCount; j++) + { + if (i != j) + { + if (pairwisePreferences[i, j] >= pairwisePreferences[j, i]) + { + winningPaths[i, j] = pairwisePreferences[i, j]; + } + else + { + winningPaths[i, j] = 0; + } + } + } + } + + return winningPaths; + } + + /// + /// Gets the winning options in order of preference, based on the winning paths. + /// + /// The winning paths. + /// The list of choices. + /// Returns a list of + private List GetResultsInOrder(int[,] winningPaths, List listOfChoices) + { + int count = listOfChoices.Count; + + var availableIndexes = Enumerable.Range(0, count); + + var pathCounts = from index in availableIndexes + select new + { + Index = index, + Choice = listOfChoices[index], + Count = GetPositivePathCount(winningPaths, index, count), + Sum = GetPathSum(winningPaths, index, count) + }; + + var orderPaths = pathCounts.OrderByDescending(p => p.Count).ThenByDescending(p => p.Sum).ThenBy(p => p.Choice); + + foreach (var path in orderPaths) + { + Debug.WriteLine($"- {path.Choice} [{path.Count}/{path.Sum}]"); + } + + var res = orderPaths.Select(r => listOfChoices[r.Index]).ToList(); + + return res; + } + #endregion + + #region Small Utility + /// + /// Gets all choices from all user votes. + /// + /// The collection of user votes. + /// Returns a list of all the choices in the task. + private List GetAllChoices(GroupedVotesByTask task) + { + var res = from vote in task + group vote by VoteString.GetVoteContent(vote.Key) into votes + select votes.Key; + + return res.ToList(); + } + + /// + /// Gets an indexer lookup for the list of choices, so it doesn't have to do + /// sequential lookups each time.. + /// + /// The list of choices. + /// Returns a dictionary of choices vs list index. + private Dictionary GetChoicesIndexes(RankResults listOfChoices) + { + Dictionary choiceIndexes = new Dictionary(); + int index = 0; + foreach (var choice in listOfChoices) + { + choiceIndexes[choice] = index++; + } + + return choiceIndexes; + } + + /// + /// Gets the number of paths in the table with a value greater than 0. + /// + /// The paths table. + /// The row. + /// The size of the table. + /// Returns a count of the number of positive path strength values. + private int GetPositivePathCount(int[,] paths, int row, int count) + { + int pathCount = 0; + + for (int i = 0; i < count; i++) + { + if (paths[row, i] > 0) + pathCount++; + } + + return pathCount; + } + + /// + /// Gets the sum of the path strength for a given table row. + /// + /// The paths table. + /// The row. + /// The size of the table. + /// Returns the sum of the given path. + private int GetPathSum(int[,] paths, int row, int count) + { + int pathSum = 0; + + for (int i = 0; i < count; i++) + { + pathSum += paths[row, i]; + } + + return pathSum; + } + #endregion + + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index b4a590ad..faead636 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -49,6 +49,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = return new InstantRunoffRankVoteCounter(); case RankVoteCounterMethod.Borda: return new BordaRankVoteCounter(); + case RankVoteCounterMethod.Pairwise: + return new PairwiseRankVoteCounter(); case RankVoteCounterMethod.Schulze: return new SchulzeRankVoteCounter(); default: From c3d37cacb4f005c55fcd14083c6de65a016c7a8a Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:50:27 -0500 Subject: [PATCH 41/77] Update the display if the rank vote counting method is changed, as that doesn't require re-running the tally. --- TallyCore/Tally.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TallyCore/Tally.cs b/TallyCore/Tally.cs index 9aef861d..c2718207 100644 --- a/TallyCore/Tally.cs +++ b/TallyCore/Tally.cs @@ -109,7 +109,7 @@ private void PageProvider_StatusChanged(object sender, MessageEventArgs e) /// private void Options_PropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == "DisplayMode") + if (e.PropertyName == "DisplayMode" || e.PropertyName == "RankVoteCounterMethod") UpdateResults(); } From 7a8ad06d4a5a0c7d3baa0d31c6388637dd9625fe Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 17 May 2016 23:54:08 -0500 Subject: [PATCH 42/77] Update default. Baldwin is basically what NetTally has been using (but with rating calculation adjustments). --- TallyCore/Global/Enumerations.cs | 2 +- TallyCore/VoteCounting/VoteCounterLocator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 05ee5e86..a550b72f 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -20,7 +20,7 @@ public enum VoteType public enum RankVoteCounterMethod { - [EnumDescription("Default (Coombs)")] + [EnumDescription("Default (Baldwin)")] Default, [EnumDescription("Borda Count")] Borda, diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index faead636..da172e92 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -54,7 +54,7 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = case RankVoteCounterMethod.Schulze: return new SchulzeRankVoteCounter(); default: - return new CoombsRankVoteCounter(); + return new BaldwinRankVoteCounter(); } } From 2f8f6b18309463abb97e5eccd55abac48abf5763 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 01:41:51 -0500 Subject: [PATCH 43/77] Add a legacy Coombs class to verify differences from old program behavior. --- TallyCore/Global/Enumerations.cs | 2 + TallyCore/NetTally.Core.csproj | 1 + .../LegacyCoombsRankVoteCounter.cs | 247 ++++++++++++++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 + 4 files changed, 252 insertions(+) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/LegacyCoombsRankVoteCounter.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index a550b72f..e8263223 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -28,6 +28,8 @@ public enum RankVoteCounterMethod InstantRunoff, [EnumDescription("Coombs' Method")] Coombs, + [EnumDescription("Legacy Coombs")] + LegacyCoombs, [EnumDescription("Baldwin Method")] Baldwin, [EnumDescription("Pairwise Elimination")] diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index ed107cf8..ee816047 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -86,6 +86,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/LegacyCoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/LegacyCoombsRankVoteCounter.cs new file mode 100644 index 00000000..a06697b0 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/LegacyCoombsRankVoteCounter.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NetTally.Utility; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + + public class LegacyCoombsRankVoteCounter : BaseRankVoteCounter + { + /// + /// Local class to store a choice/count combo of fields for LINQ. + /// + protected class ChoiceCount + { + public string Choice { get; set; } + public int Count { get; set; } + + public override string ToString() => $"{Choice}: {Count}"; + } + + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + + List winningChoices = new List(); + + if (task.Any()) + { + Debug.WriteLine(">>Legacy Coombs Runoff<<"); + + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + var allChoices = GetAllChoices(voterRankings); + + for (int i = 1; i <= 9; i++) + { + string winner = GetWinningVote(voterRankings, winningChoices, allChoices); + + if (winner == null) + break; + + winningChoices.Add(winner); + allChoices.Remove(winner); + + Debug.WriteLine($"- {winner}"); + + if (!allChoices.Any()) + break; + } + } + + return winningChoices; + } + + /// + /// Gets the winning vote. + /// Excludes any already chosen votes from the process. + /// + /// The voter rankings. + /// The already chosen choices. + /// All remaining choices. + /// Returns the winning vote. + /// + /// + private string GetWinningVote(IEnumerable voterRankings, RankResults chosenChoices, RankResults allChoices) + { + if (voterRankings == null) + throw new ArgumentNullException(nameof(voterRankings)); + if (chosenChoices == null) + throw new ArgumentNullException(nameof(chosenChoices)); + + // Initial conversion from enumerable to list + List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); + + AddUnselectedRankings(localRankings, allChoices); + + int voterCount = localRankings.Count(); + int winCount = voterCount / 2 + 1; + + try + { + Debug.Write("Eliminations: ["); + bool eliminateOne = false; + + while (true) + { + var preferredVotes = GetPreferredCounts(localRankings); + + if (!preferredVotes.Any()) + break; + + ChoiceCount best = preferredVotes.MaxObject(a => a.Count); + + if (best.Count >= winCount) + return best.Choice; + + // If no more choice removals will bump up lower prefs to higher prefs, return the best of what's left. + if (!localRankings.Any(r => r.RankedVotes.Count() > 1)) + return best.Choice; + + string leastPreferredChoice = GetLeastPreferredChoice(localRankings); + + Debug.Write($"{Comma(eliminateOne)}{leastPreferredChoice}"); + + RemoveChoiceFromVotes(localRankings, leastPreferredChoice); + eliminateOne = true; + } + } + finally + { + Debug.WriteLine("]"); + } + + return null; + } + + private string Comma(bool addComma) + { + if (addComma) + return ", "; + else + return ""; + } + + + /// + /// Removes a list of choices from voter rankings. + /// These are the choices that have already won a rank spot. + /// + /// The voter rankings. + /// The already chosen choices. + /// Returns the results as a list. + private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) + { + var res = from voter in voterRankings + select new VoterRankings + { + Voter = voter.Voter, + RankedVotes = voter.RankedVotes.Where(v => chosenChoices.Contains(v.Vote) == false).OrderBy(v => v.Rank).ToList() + }; + + return res.ToList(); + } + + /// + /// Adds ranking entries for any choices that users did not explictly rank. + /// Modifies the provided list. + /// + /// The vote rankings. + /// All available choices. + private void AddUnselectedRankings(List localRankings, RankResults allChoices) + { + foreach (var ranker in localRankings) + { + if (ranker.RankedVotes.Count == allChoices.Count) + continue; + + var extras = allChoices.Except(ranker.RankedVotes.Select(v => v.Vote)); + + foreach (var extra in extras) + { + ranker.RankedVotes.Add(new RankedVote { Vote = extra, Rank = 10 }); + } + } + } + + /// + /// Filter the provided list of voter rankings to remove any instances of the specified choice. + /// Modifies the provided list. + /// + /// The votes to filter. + /// The choice to remove. + private void RemoveChoiceFromVotes(List voterRankings, string choice) + { + foreach (var ranker in voterRankings) + { + ranker.RankedVotes.RemoveAll(v => v.Vote == choice); + } + } + + /// + /// Gets the least preferred choice. + /// + /// The vote rankings. + /// Returns the vote string for the least preferred vote. + private string GetLeastPreferredChoice(List localRankings) + { + Dictionary rankTotals = new Dictionary(); + + foreach (var voter in localRankings) + { + foreach (var rank in voter.RankedVotes) + { + if (!rankTotals.ContainsKey(rank.Vote)) + rankTotals[rank.Vote] = 0; + + rankTotals[rank.Vote] += rank.Rank; + } + } + + var maxRank = rankTotals.MaxObject(a => a.Value); + + return maxRank.Key; + } + + /// + /// Gets the count of the number of times a given vote is the most preferred option + /// among the provided voters. + /// + /// The list of voters and their rankings of each option. + /// Returns a collection of Choice/Count objects. + private IEnumerable GetPreferredCounts(IEnumerable voterRankings) + { + var preferredVotes = from voter in voterRankings + let preferred = voter.RankedVotes.First().Vote + group voter by preferred into preffed + select new ChoiceCount { Choice = preffed.Key, Count = preffed.Count() }; + + return preferredVotes; + } + + /// + /// Gets all choices from all user votes. + /// + /// The collection of user votes. + /// Returns a list of all the choices in the task. + private List GetAllChoices(IEnumerable rankings) + { + var res = rankings.SelectMany(r => r.RankedVotes).Select(r => r.Vote).Distinct(); + + return res.ToList(); + } + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index da172e92..0ac0bf1b 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -43,6 +43,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = { case RankVoteCounterMethod.Coombs: return new CoombsRankVoteCounter(); + case RankVoteCounterMethod.LegacyCoombs: + return new LegacyCoombsRankVoteCounter(); case RankVoteCounterMethod.Baldwin: return new BaldwinRankVoteCounter(); case RankVoteCounterMethod.InstantRunoff: From 829afed3aa3fc778acff57b691f301102a184f99 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 18:03:29 -0500 Subject: [PATCH 44/77] Do not add unranked options to each user when calculating a winner, as that skews their relative values. Check for null when counting preferred votes, since vote lengths may run out during the process. --- .../RankVoteCounting/BaldwinRankVoteCounter.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs index f1ef84f5..9b67f1bd 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -47,7 +47,7 @@ protected override RankResults RankTask(GroupedVotesByTask task) for (int i = 1; i <= 9; i++) { - string winner = GetWinningVote(voterRankings, winningChoices, allChoices); + string winner = GetWinningVote(voterRankings, winningChoices); if (winner == null) break; @@ -75,7 +75,7 @@ protected override RankResults RankTask(GroupedVotesByTask task) /// Returns the winning vote. /// /// - private string GetWinningVote(IEnumerable voterRankings, RankResults chosenChoices, RankResults allChoices) + private string GetWinningVote(IEnumerable voterRankings, RankResults chosenChoices) { if (voterRankings == null) throw new ArgumentNullException(nameof(voterRankings)); @@ -85,8 +85,6 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu // Initial conversion from enumerable to list List localRankings = RemoveChoicesFromVotes(voterRankings, chosenChoices); - AddUnselectedRankings(localRankings, allChoices); - int voterCount = localRankings.Count(); int winCount = voterCount / 2 + 1; @@ -246,7 +244,8 @@ private string GetLeastPreferredChoice(List localRankings) private IEnumerable GetPreferredCounts(IEnumerable voterRankings) { var preferredVotes = from voter in voterRankings - let preferred = voter.RankedVotes.First().Vote + let preferred = voter.RankedVotes.FirstOrDefault()?.Vote + where preferred != null group voter by preferred into preffed select new ChoiceCount { Choice = preffed.Key, Count = preffed.Count() }; From f336f9bdb6d17e5d23293d667ebb8f6bced47af0 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 18:03:58 -0500 Subject: [PATCH 45/77] Display the calculated rank value when showing eliminated options. --- .../VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs index 9b67f1bd..57636467 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -109,9 +109,11 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu if (!localRankings.Any(r => r.RankedVotes.Count() > 1)) return best.Choice; + Debug.Write($"{Comma(eliminateOne)}"); + string leastPreferredChoice = GetLeastPreferredChoice(localRankings); - Debug.Write($"{Comma(eliminateOne)}{leastPreferredChoice}"); + Debug.Write($"{leastPreferredChoice}"); RemoveChoiceFromVotes(localRankings, leastPreferredChoice); eliminateOne = true; @@ -232,6 +234,8 @@ private string GetLeastPreferredChoice(List localRankings) var maxResult = rankScaling.MaxObject(a => a.Value); + Debug.Write($"({maxResult.Value:f5}) "); + return maxResult.Choice; } From ac8e0ebeeefeeb3e83ac94e4b764a5356328902c Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 18:08:46 -0500 Subject: [PATCH 46/77] Comments. --- .../VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs index 57636467..65e800bb 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -71,7 +71,6 @@ protected override RankResults RankTask(GroupedVotesByTask task) /// /// The voter rankings. /// The already chosen choices. - /// All remaining choices. /// Returns the winning vote. /// /// @@ -200,7 +199,8 @@ private void RemoveChoiceFromVotes(List voterRankings, string cho /// that are ranked in all votes. /// We then need to scale it relative to the number of instances of that option appearing, to deal /// with truncated rankings (where there are more options than rankings allowed). - /// An option ranked infrequently can be scaled up relative to its rate of occurance. + /// An option ranked infrequently can be scaled up relative to its rate of occurance for + /// a high likelihood of elimination. /// /// The vote rankings. /// Returns the vote string for the least preferred vote. From cf42f938ee12aefab4099269e20617c76577a167 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 18:54:21 -0500 Subject: [PATCH 47/77] Rework determination of least desired option. Ensure all unranked items are included in counts, and that same-ranked items are all accounted for. --- .../RankVoteCounting/CoombsRankVoteCounter.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index 000400f0..7f09b350 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -111,9 +111,11 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu if (!localRankings.Any(r => r.RankedVotes.Count() > 1)) return best.Choice; + Debug.Write($"{Comma(eliminateOne)}"); + string leastPreferredChoice = GetLeastPreferredChoice(localRankings); - Debug.Write($"{Comma(eliminateOne)}{leastPreferredChoice}"); + Debug.Write($"{leastPreferredChoice}"); RemoveChoiceFromVotes(localRankings, leastPreferredChoice); eliminateOne = true; @@ -202,19 +204,26 @@ private string GetLeastPreferredChoice(List localRankings) foreach (var voter in localRankings) { - var lowestRanked = voter.RankedVotes.MaxObject(a => a.Rank); + if (voter.RankedVotes.Any()) + { + int lowestRank = voter.RankedVotes.Max(a => a.Rank); - if (lowestRanked == null) - continue; + var lowestRankedOptions = voter.RankedVotes.Where(v => v.Rank == lowestRank); - if (!rankCount.ContainsKey(lowestRanked.Vote)) - rankCount[lowestRanked.Vote] = 0; + foreach (var lowest in lowestRankedOptions) + { + if (!rankCount.ContainsKey(lowest.Vote)) + rankCount[lowest.Vote] = 0; - rankCount[lowestRanked.Vote]++; + rankCount[lowest.Vote]++; + } + } } var maxRank = rankCount.MaxObject(a => a.Value); + Debug.Write($"({maxRank.Value}) "); + return maxRank.Key; } @@ -227,7 +236,8 @@ private string GetLeastPreferredChoice(List localRankings) private IEnumerable GetPreferredCounts(IEnumerable voterRankings) { var preferredVotes = from voter in voterRankings - let preferred = voter.RankedVotes.First().Vote + let preferred = voter.RankedVotes.FirstOrDefault()?.Vote + where preferred != null group voter by preferred into preffed select new ChoiceCount { Choice = preffed.Key, Count = preffed.Count() }; From c050b9894f7f78555c477fdf6001a4d067aaa266 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 20:40:54 -0500 Subject: [PATCH 48/77] Add a class for a normalized Borda count. --- TallyCore/Global/Enumerations.cs | 2 + TallyCore/NetTally.Core.csproj | 1 + .../BordaNormalizedRankVoteCounter.cs | 99 +++++++++++++++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 + 4 files changed, 104 insertions(+) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index e8263223..b8730773 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -24,6 +24,8 @@ public enum RankVoteCounterMethod Default, [EnumDescription("Borda Count")] Borda, + [EnumDescription("Borda Normalized")] + BordaNormalized, [EnumDescription("Instant Runoff")] InstantRunoff, [EnumDescription("Coombs' Method")] diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index ee816047..b11546d6 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -82,6 +82,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs new file mode 100644 index 00000000..1d81b395 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + /// + /// Borda is being removed as a valid option from the list of rank vote options. + /// Aside from systemic failures of the method itself, it cannot give proper + /// valuation to unranked options, which intrinsically makes it a bad fit + /// for handling user-entered quest voting schemes. + /// + /// + public class BordaNormalizedRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + Debug.WriteLine(">>Normalized Borda Counting<<"); + + var voterCount = task.SelectMany(t => t.Value).Distinct().Count(); + + var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); + + var rankedVotes = from vote in groupVotes + select new { Vote = vote.VoteContent, Rank = RankVote(vote.Ranks) }; + + var orderedVotes = rankedVotes.OrderBy(a => a.Rank); + + foreach (var orderedVote in orderedVotes) + { + Debug.WriteLine($"- {orderedVote.Vote} [{orderedVote.Rank:f5}]"); + } + + return orderedVotes.Select(a => a.Vote).ToList(); + } + + /// + /// Ranks the vote. + /// + /// Votes with associated ranks, for the voters who ranked the vote with a given value. + /// Returns a numeric evaluation of the overall rank of the vote. + private double RankVote(IEnumerable ranks) + { + double voteValue = 0; + + // Add up the sum of the number of voters times the value of each rank. + // Average the results, and then scale by the number of voters who ranked this option. + // Ranking value is Borda+1, so that ranks 1 through 9 are given values 2 through 10. + // That means first place is 5x as valuable as last place, rather than 9x as valuable. + + foreach (var r in ranks) + { + voteValue += (ValueOfRank(r.Rank) + 1.0) * r.Voters.Count(); + } + + int totalRankings = ranks.Sum(a => a.Voters.Count()); + + voteValue = voteValue / totalRankings / totalRankings; + + return voteValue; + } + + /// + /// Get the numeric value of a given rank. + /// + /// The rank being evaluated. + /// + /// + /// + private int ValueOfRank(string rank) + { + if (string.IsNullOrEmpty(rank)) + throw new ArgumentNullException(nameof(rank)); + + int rankAsInt = int.Parse(rank); + + if (rankAsInt < 1 || rankAsInt > 9) + throw new ArgumentOutOfRangeException(nameof(rank)); + + return rankAsInt; + } + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index 0ac0bf1b..b8307698 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -51,6 +51,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = return new InstantRunoffRankVoteCounter(); case RankVoteCounterMethod.Borda: return new BordaRankVoteCounter(); + case RankVoteCounterMethod.BordaNormalized: + return new BordaNormalizedRankVoteCounter(); case RankVoteCounterMethod.Pairwise: return new PairwiseRankVoteCounter(); case RankVoteCounterMethod.Schulze: From 257e7b26bca5c2c8f1cc0e6a0445eedd768382f6 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 20:42:17 -0500 Subject: [PATCH 49/77] Adjust the rank value by +1 per ballot to reduce the scaling ratio between first and last place from 9x to 5x. This helps mitigate the impact of higher vs lower ranked votes. --- .../VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs index 65e800bb..838989c3 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -219,7 +219,7 @@ private string GetLeastPreferredChoice(List localRankings) if (!rankInstances.ContainsKey(rank.Vote)) rankInstances[rank.Vote] = 0; - rankTotals[rank.Vote] += rank.Rank; + rankTotals[rank.Vote] += rank.Rank + 1; rankInstances[rank.Vote]++; choices.Add(rank.Vote); } From 70a935fd9c89def6944e9fe108bea1d111239807 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 May 2016 23:59:44 -0500 Subject: [PATCH 50/77] Implement experimental methodology based on rank distances. --- TallyCore/Global/Enumerations.cs | 2 + TallyCore/NetTally.Core.csproj | 1 + .../DistanceRankVoteCounter.cs | 328 ++++++++++++++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 + 4 files changed, 333 insertions(+) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index b8730773..658725a0 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -22,6 +22,8 @@ public enum RankVoteCounterMethod { [EnumDescription("Default (Baldwin)")] Default, + [EnumDescription("Distance Scoring")] + Distance, [EnumDescription("Borda Count")] Borda, [EnumDescription("Borda Normalized")] diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index b11546d6..b31ad83d 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -86,6 +86,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs new file mode 100644 index 00000000..7639acc6 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + public class PairwiseData + { + public int[,] PairwiseDistances { get; set; } + public int[,] UnknownDistances { get; set; } + public int MaxDistance { get; } = 8; + } + + public class StrengthData + { + public int[,] ShortestPaths { get; set; } + public int[,] ShortestUnknownPaths { get; set; } + } + + public class WinningData + { + public int[,] WinningPaths { get; set; } + public int[,] WinningUnknownPaths { get; set; } + } + + + public class DistanceRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task, based on the Schulze algorithm. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + + + Debug.WriteLine(">>Distance Scoring<<"); + + List listOfChoices = GetAllChoices(task); + + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + + PairwiseData data = GetPairwiseData(voterRankings, listOfChoices); + + StrengthData strengthData = GetStrongestPaths(data, listOfChoices.Count); + + WinningData winningPaths = GetWinningPaths(strengthData, listOfChoices.Count); + + RankResults winningChoices = GetResultsInOrder(winningPaths, listOfChoices); + + return winningChoices; + } + + #region Schulze Algorithm + /// + /// Fills the pairwise preferences. + /// This goes through each voter's ranking options and updates an array indicating + /// which options are preferred over which other options. Each higher-ranked + /// option gains one point in 'beating' a lower-ranked option. + /// + /// The voter rankings. + /// The list of choices. + /// Returns a filled-in preferences array. + private PairwiseData GetPairwiseData(IEnumerable voterRankings, List listOfChoices) + { + PairwiseData data = new PairwiseData + { + PairwiseDistances = new int[listOfChoices.Count, listOfChoices.Count], + UnknownDistances = new int[listOfChoices.Count, listOfChoices.Count], + }; + + var choiceIndexes = GetChoicesIndexes(listOfChoices); + + foreach (var voter in voterRankings) + { + var rankedChoices = voter.RankedVotes.Select(v => v.Vote); + var unrankedChoices = listOfChoices.Except(rankedChoices); + + foreach (var choice in voter.RankedVotes) + { + // Each choice matching or beating the ranks of other ranked choices is marked. + foreach (var otherChoice in voter.RankedVotes) + { + if (choice.Rank < otherChoice.Rank) + { + int distance = otherChoice.Rank - choice.Rank; + data.PairwiseDistances[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]] = distance; + } + } + + // Each choice is ranked higher than all unranked choices, but we don't know by how far. + foreach (var nonChoice in unrankedChoices) + { + data.UnknownDistances[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; + data.UnknownDistances[choiceIndexes[nonChoice], choiceIndexes[choice.Vote]]++; + } + } + + foreach (var nonChoice1 in unrankedChoices) + { + foreach (var nonChoice2 in unrankedChoices) + { + if (nonChoice1 != nonChoice2) + { + data.UnknownDistances[choiceIndexes[nonChoice1], choiceIndexes[nonChoice2]]++; + data.UnknownDistances[choiceIndexes[nonChoice2], choiceIndexes[nonChoice1]]++; + } + } + } + } + + return data; + } + + /// + /// Calculate the strongest preference paths from the pairwise preferences table. + /// + /// The pairwise data. + /// The choices count (size of the table). + /// Returns a table with the strongest paths between each pairwise choice. + private StrengthData GetStrongestPaths(PairwiseData pairwiseData, int choicesCount) + { + StrengthData data = new StrengthData() + { + ShortestPaths = new int[choicesCount, choicesCount], + ShortestUnknownPaths = new int[choicesCount, choicesCount] + }; + + int bytesInArray = data.ShortestPaths.Length * sizeof(Int32); + Buffer.BlockCopy(pairwiseData.PairwiseDistances, 0, data.ShortestPaths, 0, bytesInArray); + Buffer.BlockCopy(pairwiseData.PairwiseDistances, 0, data.ShortestUnknownPaths, 0, bytesInArray); + + for (int i = 0; i < choicesCount; i++) + { + for (int j = 0; j < choicesCount; j++) + { + if (i != j) + { + data.ShortestUnknownPaths[i, j] += pairwiseData.UnknownDistances[i, j] * pairwiseData.MaxDistance; + } + } + } + + + for (int i = 0; i < choicesCount; i++) + { + for (int j = 0; j < choicesCount; j++) + { + if (i != j) + { + for (int k = 0; k < choicesCount; k++) + { + if (i != k && j != k) + { + data.ShortestPaths[j, k] = Math.Min(data.ShortestPaths[j, k], Math.Max(data.ShortestPaths[j, i], data.ShortestPaths[i, k])); + data.ShortestUnknownPaths[j, k] = Math.Min(data.ShortestUnknownPaths[j, k], Math.Max(data.ShortestUnknownPaths[j, i], data.ShortestUnknownPaths[i, k])); + } + } + } + } + } + + return data; + } + + /// + /// Gets the winning paths - The strongest of the strongest paths, for each pair option. + /// + /// The strongest paths. + /// The choices count (size of table). + /// Returns a table with the winning choices of the strongest paths. + private WinningData GetWinningPaths(StrengthData paths, int choicesCount) + { + WinningData data = new WinningData() + { + WinningPaths = new int[choicesCount, choicesCount], + WinningUnknownPaths = new int[choicesCount, choicesCount] + }; + + for (int i = 0; i < choicesCount; i++) + { + for (int j = 0; j < choicesCount; j++) + { + if (i != j) + { + if (paths.ShortestPaths[i, j] <= paths.ShortestPaths[j, i]) + { + data.WinningPaths[i, j] = paths.ShortestPaths[i, j]; + } + else + { + data.WinningPaths[i, j] = 0; + } + + if (paths.ShortestUnknownPaths[i, j] <= paths.ShortestUnknownPaths[j, i]) + { + data.WinningUnknownPaths[i, j] = paths.ShortestUnknownPaths[i, j]; + } + else + { + data.WinningUnknownPaths[i, j] = 0; + } + } + } + } + + return data; + } + + /// + /// Gets the winning options in order of preference, based on the winning paths. + /// + /// The winning paths. + /// The list of choices. + /// Returns a list of + private List GetResultsInOrder(WinningData winningPaths, List listOfChoices) + { + int count = listOfChoices.Count; + + var availableIndexes = Enumerable.Range(0, count); + + var pathCounts = from index in availableIndexes + select new + { + Index = index, + Choice = listOfChoices[index], + Count = GetPositivePathCount(winningPaths.WinningPaths, index, count), + Sum = GetPathSum(winningPaths.WinningPaths, index, count) + }; + + var orderPaths = pathCounts.OrderByDescending(p => p.Count).ThenByDescending(p => p.Sum).ThenBy(p => p.Choice); + + foreach (var path in orderPaths) + { + Debug.WriteLine($"- {path.Choice} [{path.Count}/{path.Sum}]"); + } + + var res = orderPaths.Select(r => listOfChoices[r.Index]).ToList(); + + return res; + } + #endregion + + #region Small Utility + /// + /// Gets all choices from all user votes. + /// + /// The collection of user votes. + /// Returns a list of all the choices in the task. + private List GetAllChoices(GroupedVotesByTask task) + { + var res = from vote in task + group vote by VoteString.GetVoteContent(vote.Key) into votes + select votes.Key; + + return res.ToList(); + } + + /// + /// Gets an indexer lookup for the list of choices, so it doesn't have to do + /// sequential lookups each time.. + /// + /// The list of choices. + /// Returns a dictionary of choices vs list index. + private Dictionary GetChoicesIndexes(RankResults listOfChoices) + { + Dictionary choiceIndexes = new Dictionary(); + int index = 0; + foreach (var choice in listOfChoices) + { + choiceIndexes[choice] = index++; + } + + return choiceIndexes; + } + + /// + /// Gets the number of paths in the table with a value greater than 0. + /// + /// The paths table. + /// The row. + /// The size of the table. + /// Returns a count of the number of positive path strength values. + private int GetPositivePathCount(int[,] paths, int row, int count) + { + int pathCount = 0; + + for (int i = 0; i < count; i++) + { + if (paths[row, i] > 0) + pathCount++; + } + + return pathCount; + } + + /// + /// Gets the sum of the path strength for a given table row. + /// + /// The paths table. + /// The row. + /// The size of the table. + /// Returns the sum of the given path. + private int GetPathSum(int[,] paths, int row, int count) + { + int pathSum = 0; + + for (int i = 0; i < count; i++) + { + pathSum += paths[row, i]; + } + + return pathSum; + } + #endregion + + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index b8307698..91ae5fec 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -57,6 +57,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = return new PairwiseRankVoteCounter(); case RankVoteCounterMethod.Schulze: return new SchulzeRankVoteCounter(); + case RankVoteCounterMethod.Distance: + return new DistanceRankVoteCounter(); default: return new BaldwinRankVoteCounter(); } From ef42b71d32829ce3a0a33e8c36c996579926c2da Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 21 May 2016 01:22:28 -0500 Subject: [PATCH 51/77] When determining which option has the highest number of lowest-rank votes, unranked options per user are divided evenly, to 1/N each. --- .../RankVoteCounting/CoombsRankVoteCounter.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs index 7f09b350..8a86fa0a 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/CoombsRankVoteCounter.cs @@ -200,7 +200,7 @@ private void RemoveChoiceFromVotes(List voterRankings, string cho /// Returns the vote string for the least preferred vote. private string GetLeastPreferredChoice(List localRankings) { - Dictionary rankCount = new Dictionary(); + Dictionary rankCount = new Dictionary(); foreach (var voter in localRankings) { @@ -210,12 +210,22 @@ private string GetLeastPreferredChoice(List localRankings) var lowestRankedOptions = voter.RankedVotes.Where(v => v.Rank == lowestRank); + int lowestRankCount = lowestRankedOptions.Count(); + foreach (var lowest in lowestRankedOptions) { if (!rankCount.ContainsKey(lowest.Vote)) rankCount[lowest.Vote] = 0; - rankCount[lowest.Vote]++; + // Unranked options get fractional additions to the effective rank count. + if (lowestRank == 10) + { + rankCount[lowest.Vote] += 1.0 / lowestRankCount; + } + else + { + rankCount[lowest.Vote] += 1.0; + } } } } From 66a079be34d5a06fcf2df400316e38d1d8935068 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 21 May 2016 02:06:04 -0500 Subject: [PATCH 52/77] Lots of refinements. --- .../DistanceRankVoteCounter.cs | 140 +++++++----------- 1 file changed, 55 insertions(+), 85 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs index 7639acc6..f1c52e16 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs @@ -10,26 +10,18 @@ namespace NetTally.VoteCounting // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; - public class PairwiseData + public class DistanceData { - public int[,] PairwiseDistances { get; set; } - public int[,] UnknownDistances { get; set; } - public int MaxDistance { get; } = 8; + public int[,] NormalPaths { get; set; } + public int[,] UnknownPaths { get; set; } } - public class StrengthData + public class WinningChoices { - public int[,] ShortestPaths { get; set; } - public int[,] ShortestUnknownPaths { get; set; } + public RankResults WinningNormalChoices { get; set; } + public RankResults WinningUnknownChoices { get; set; } } - public class WinningData - { - public int[,] WinningPaths { get; set; } - public int[,] WinningUnknownPaths { get; set; } - } - - public class DistanceRankVoteCounter : BaseRankVoteCounter { /// @@ -50,18 +42,18 @@ protected override RankResults RankTask(GroupedVotesByTask task) var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); - PairwiseData data = GetPairwiseData(voterRankings, listOfChoices); + DistanceData pairwiseData = GetPairwiseData(voterRankings, listOfChoices); - StrengthData strengthData = GetStrongestPaths(data, listOfChoices.Count); + DistanceData strengthData = GetStrongestPaths(pairwiseData, listOfChoices.Count); - WinningData winningPaths = GetWinningPaths(strengthData, listOfChoices.Count); + DistanceData winningPaths = GetWinningPaths(strengthData, listOfChoices.Count); - RankResults winningChoices = GetResultsInOrder(winningPaths, listOfChoices); + WinningChoices winningChoices = GetResultsInOrder(winningPaths, listOfChoices); - return winningChoices; + return winningChoices.WinningNormalChoices; } - #region Schulze Algorithm + #region Distance Algorithm (based on Schulze+Range) /// /// Fills the pairwise preferences. /// This goes through each voter's ranking options and updates an array indicating @@ -71,12 +63,12 @@ protected override RankResults RankTask(GroupedVotesByTask task) /// The voter rankings. /// The list of choices. /// Returns a filled-in preferences array. - private PairwiseData GetPairwiseData(IEnumerable voterRankings, List listOfChoices) + private DistanceData GetPairwiseData(IEnumerable voterRankings, List listOfChoices) { - PairwiseData data = new PairwiseData + DistanceData data = new DistanceData { - PairwiseDistances = new int[listOfChoices.Count, listOfChoices.Count], - UnknownDistances = new int[listOfChoices.Count, listOfChoices.Count], + NormalPaths = new int[listOfChoices.Count, listOfChoices.Count], + UnknownPaths = new int[listOfChoices.Count, listOfChoices.Count], }; var choiceIndexes = GetChoicesIndexes(listOfChoices); @@ -88,35 +80,29 @@ private PairwiseData GetPairwiseData(IEnumerable voterRankings, L foreach (var choice in voter.RankedVotes) { - // Each choice matching or beating the ranks of other ranked choices is marked. foreach (var otherChoice in voter.RankedVotes) { - if (choice.Rank < otherChoice.Rank) + // Each ranked vote that has a higher rank (lower number) than each + // alternative has the distance between the choices added to the + // corresponding table entry. + if (choice.Vote != otherChoice.Vote && choice.Rank < otherChoice.Rank) { - int distance = otherChoice.Rank - choice.Rank; - data.PairwiseDistances[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]] = distance; + data.NormalPaths[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]] += otherChoice.Rank - choice.Rank; } } - // Each choice is ranked higher than all unranked choices, but we don't know by how far. + // All unranked options are considered to be at distance 1 from *all* ranked options. + // There is no relative preference, nor does it place unranked options 'beneath' + // ranked options, such that higher ranked options have greater distance from them. + // Unranked options are agnostic choices. foreach (var nonChoice in unrankedChoices) { - data.UnknownDistances[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; - data.UnknownDistances[choiceIndexes[nonChoice], choiceIndexes[choice.Vote]]++; + data.NormalPaths[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; } } - foreach (var nonChoice1 in unrankedChoices) - { - foreach (var nonChoice2 in unrankedChoices) - { - if (nonChoice1 != nonChoice2) - { - data.UnknownDistances[choiceIndexes[nonChoice1], choiceIndexes[nonChoice2]]++; - data.UnknownDistances[choiceIndexes[nonChoice2], choiceIndexes[nonChoice1]]++; - } - } - } + // All unranked options are at distance 0 from each other, and thus have no effect + // on the distance table. } return data; @@ -128,29 +114,16 @@ private PairwiseData GetPairwiseData(IEnumerable voterRankings, L /// The pairwise data. /// The choices count (size of the table). /// Returns a table with the strongest paths between each pairwise choice. - private StrengthData GetStrongestPaths(PairwiseData pairwiseData, int choicesCount) + private DistanceData GetStrongestPaths(DistanceData pairwiseData, int choicesCount) { - StrengthData data = new StrengthData() + DistanceData data = new DistanceData { - ShortestPaths = new int[choicesCount, choicesCount], - ShortestUnknownPaths = new int[choicesCount, choicesCount] + NormalPaths = new int[choicesCount, choicesCount], + UnknownPaths = new int[choicesCount, choicesCount] }; - int bytesInArray = data.ShortestPaths.Length * sizeof(Int32); - Buffer.BlockCopy(pairwiseData.PairwiseDistances, 0, data.ShortestPaths, 0, bytesInArray); - Buffer.BlockCopy(pairwiseData.PairwiseDistances, 0, data.ShortestUnknownPaths, 0, bytesInArray); - - for (int i = 0; i < choicesCount; i++) - { - for (int j = 0; j < choicesCount; j++) - { - if (i != j) - { - data.ShortestUnknownPaths[i, j] += pairwiseData.UnknownDistances[i, j] * pairwiseData.MaxDistance; - } - } - } - + int bytesInArray = data.NormalPaths.Length * sizeof(Int32); + Buffer.BlockCopy(pairwiseData.NormalPaths, 0, data.NormalPaths, 0, bytesInArray); for (int i = 0; i < choicesCount; i++) { @@ -162,8 +135,7 @@ private StrengthData GetStrongestPaths(PairwiseData pairwiseData, int choicesCou { if (i != k && j != k) { - data.ShortestPaths[j, k] = Math.Min(data.ShortestPaths[j, k], Math.Max(data.ShortestPaths[j, i], data.ShortestPaths[i, k])); - data.ShortestUnknownPaths[j, k] = Math.Min(data.ShortestUnknownPaths[j, k], Math.Max(data.ShortestUnknownPaths[j, i], data.ShortestUnknownPaths[i, k])); + data.NormalPaths[j, k] = Math.Max(data.NormalPaths[j, k], Math.Min(data.NormalPaths[j, i], data.NormalPaths[i, k])); } } } @@ -179,12 +151,12 @@ private StrengthData GetStrongestPaths(PairwiseData pairwiseData, int choicesCou /// The strongest paths. /// The choices count (size of table). /// Returns a table with the winning choices of the strongest paths. - private WinningData GetWinningPaths(StrengthData paths, int choicesCount) + private DistanceData GetWinningPaths(DistanceData paths, int choicesCount) { - WinningData data = new WinningData() + DistanceData winningData = new DistanceData { - WinningPaths = new int[choicesCount, choicesCount], - WinningUnknownPaths = new int[choicesCount, choicesCount] + NormalPaths = new int[choicesCount, choicesCount], + UnknownPaths = new int[choicesCount, choicesCount] }; for (int i = 0; i < choicesCount; i++) @@ -193,28 +165,23 @@ private WinningData GetWinningPaths(StrengthData paths, int choicesCount) { if (i != j) { - if (paths.ShortestPaths[i, j] <= paths.ShortestPaths[j, i]) - { - data.WinningPaths[i, j] = paths.ShortestPaths[i, j]; - } - else - { - data.WinningPaths[i, j] = 0; - } + winningData.NormalPaths[i, j] = paths.NormalPaths[i, j] - paths.NormalPaths[j, i]; - if (paths.ShortestUnknownPaths[i, j] <= paths.ShortestUnknownPaths[j, i]) + /* + if (paths.NormalPaths[i, j] > 0 && paths.NormalPaths[i, j] >= paths.NormalPaths[j, i]) { - data.WinningUnknownPaths[i, j] = paths.ShortestUnknownPaths[i, j]; + winningData.NormalPaths[i, j] = paths.NormalPaths[i, j]; } else { - data.WinningUnknownPaths[i, j] = 0; + winningData.NormalPaths[i, j] = 0; } + */ } } } - return data; + return winningData; } /// @@ -223,7 +190,7 @@ private WinningData GetWinningPaths(StrengthData paths, int choicesCount) /// The winning paths. /// The list of choices. /// Returns a list of - private List GetResultsInOrder(WinningData winningPaths, List listOfChoices) + private WinningChoices GetResultsInOrder(DistanceData winningPaths, List listOfChoices) { int count = listOfChoices.Count; @@ -234,20 +201,23 @@ private List GetResultsInOrder(WinningData winningPaths, List li { Index = index, Choice = listOfChoices[index], - Count = GetPositivePathCount(winningPaths.WinningPaths, index, count), - Sum = GetPathSum(winningPaths.WinningPaths, index, count) + Count = GetPositivePathCount(winningPaths.NormalPaths, index, count), + Sum = GetPathSum(winningPaths.NormalPaths, index, count), }; - var orderPaths = pathCounts.OrderByDescending(p => p.Count).ThenByDescending(p => p.Sum).ThenBy(p => p.Choice); + var orderPaths = pathCounts.OrderByDescending(p => p.Sum).ThenByDescending(p => p.Count).ThenBy(p => p.Choice); foreach (var path in orderPaths) { Debug.WriteLine($"- {path.Choice} [{path.Count}/{path.Sum}]"); } - var res = orderPaths.Select(r => listOfChoices[r.Index]).ToList(); + WinningChoices results = new WinningChoices() + { + WinningNormalChoices = orderPaths.Select(r => listOfChoices[r.Index]).ToList(), + }; - return res; + return results; } #endregion From b7bc1687a63812c2e8578d7b460fe738b9f39e9d Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 21 May 2016 02:34:19 -0500 Subject: [PATCH 53/77] Remove code for unknown/unranked data, and simplify. --- .../DistanceRankVoteCounter.cs | 62 ++++++------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs index f1c52e16..4a9af242 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs @@ -12,14 +12,7 @@ namespace NetTally.VoteCounting public class DistanceData { - public int[,] NormalPaths { get; set; } - public int[,] UnknownPaths { get; set; } - } - - public class WinningChoices - { - public RankResults WinningNormalChoices { get; set; } - public RankResults WinningUnknownChoices { get; set; } + public int[,] Paths { get; set; } } public class DistanceRankVoteCounter : BaseRankVoteCounter @@ -48,9 +41,9 @@ protected override RankResults RankTask(GroupedVotesByTask task) DistanceData winningPaths = GetWinningPaths(strengthData, listOfChoices.Count); - WinningChoices winningChoices = GetResultsInOrder(winningPaths, listOfChoices); + RankResults winningChoices = GetResultsInOrder(winningPaths, listOfChoices); - return winningChoices.WinningNormalChoices; + return winningChoices; } #region Distance Algorithm (based on Schulze+Range) @@ -67,8 +60,7 @@ private DistanceData GetPairwiseData(IEnumerable voterRankings, L { DistanceData data = new DistanceData { - NormalPaths = new int[listOfChoices.Count, listOfChoices.Count], - UnknownPaths = new int[listOfChoices.Count, listOfChoices.Count], + Paths = new int[listOfChoices.Count, listOfChoices.Count] }; var choiceIndexes = GetChoicesIndexes(listOfChoices); @@ -87,7 +79,7 @@ private DistanceData GetPairwiseData(IEnumerable voterRankings, L // corresponding table entry. if (choice.Vote != otherChoice.Vote && choice.Rank < otherChoice.Rank) { - data.NormalPaths[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]] += otherChoice.Rank - choice.Rank; + data.Paths[choiceIndexes[choice.Vote], choiceIndexes[otherChoice.Vote]] += otherChoice.Rank - choice.Rank; } } @@ -97,7 +89,7 @@ private DistanceData GetPairwiseData(IEnumerable voterRankings, L // Unranked options are agnostic choices. foreach (var nonChoice in unrankedChoices) { - data.NormalPaths[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; + data.Paths[choiceIndexes[choice.Vote], choiceIndexes[nonChoice]]++; } } @@ -118,12 +110,11 @@ private DistanceData GetStrongestPaths(DistanceData pairwiseData, int choicesCou { DistanceData data = new DistanceData { - NormalPaths = new int[choicesCount, choicesCount], - UnknownPaths = new int[choicesCount, choicesCount] + Paths = new int[choicesCount, choicesCount] }; - int bytesInArray = data.NormalPaths.Length * sizeof(Int32); - Buffer.BlockCopy(pairwiseData.NormalPaths, 0, data.NormalPaths, 0, bytesInArray); + int bytesInArray = data.Paths.Length * sizeof(Int32); + Buffer.BlockCopy(pairwiseData.Paths, 0, data.Paths, 0, bytesInArray); for (int i = 0; i < choicesCount; i++) { @@ -135,7 +126,7 @@ private DistanceData GetStrongestPaths(DistanceData pairwiseData, int choicesCou { if (i != k && j != k) { - data.NormalPaths[j, k] = Math.Max(data.NormalPaths[j, k], Math.Min(data.NormalPaths[j, i], data.NormalPaths[i, k])); + data.Paths[j, k] = Math.Max(data.Paths[j, k], Math.Min(data.Paths[j, i], data.Paths[i, k])); } } } @@ -148,15 +139,14 @@ private DistanceData GetStrongestPaths(DistanceData pairwiseData, int choicesCou /// /// Gets the winning paths - The strongest of the strongest paths, for each pair option. /// - /// The strongest paths. + /// The strongest paths. /// The choices count (size of table). /// Returns a table with the winning choices of the strongest paths. - private DistanceData GetWinningPaths(DistanceData paths, int choicesCount) + private DistanceData GetWinningPaths(DistanceData strengthData, int choicesCount) { DistanceData winningData = new DistanceData { - NormalPaths = new int[choicesCount, choicesCount], - UnknownPaths = new int[choicesCount, choicesCount] + Paths = new int[choicesCount, choicesCount] }; for (int i = 0; i < choicesCount; i++) @@ -165,18 +155,7 @@ private DistanceData GetWinningPaths(DistanceData paths, int choicesCount) { if (i != j) { - winningData.NormalPaths[i, j] = paths.NormalPaths[i, j] - paths.NormalPaths[j, i]; - - /* - if (paths.NormalPaths[i, j] > 0 && paths.NormalPaths[i, j] >= paths.NormalPaths[j, i]) - { - winningData.NormalPaths[i, j] = paths.NormalPaths[i, j]; - } - else - { - winningData.NormalPaths[i, j] = 0; - } - */ + winningData.Paths[i, j] = strengthData.Paths[i, j] - strengthData.Paths[j, i]; } } } @@ -190,7 +169,7 @@ private DistanceData GetWinningPaths(DistanceData paths, int choicesCount) /// The winning paths. /// The list of choices. /// Returns a list of - private WinningChoices GetResultsInOrder(DistanceData winningPaths, List listOfChoices) + private RankResults GetResultsInOrder(DistanceData winningPaths, List listOfChoices) { int count = listOfChoices.Count; @@ -201,8 +180,8 @@ private WinningChoices GetResultsInOrder(DistanceData winningPaths, List { Index = index, Choice = listOfChoices[index], - Count = GetPositivePathCount(winningPaths.NormalPaths, index, count), - Sum = GetPathSum(winningPaths.NormalPaths, index, count), + Count = GetPositivePathCount(winningPaths.Paths, index, count), + Sum = GetPathSum(winningPaths.Paths, index, count), }; var orderPaths = pathCounts.OrderByDescending(p => p.Sum).ThenByDescending(p => p.Count).ThenBy(p => p.Choice); @@ -212,12 +191,7 @@ private WinningChoices GetResultsInOrder(DistanceData winningPaths, List Debug.WriteLine($"- {path.Choice} [{path.Count}/{path.Sum}]"); } - WinningChoices results = new WinningChoices() - { - WinningNormalChoices = orderPaths.Select(r => listOfChoices[r.Index]).ToList(), - }; - - return results; + return orderPaths.Select(r => listOfChoices[r.Index]).ToList(); } #endregion From a4ceb1eaf2cc1e2d4599eed7a99cb8deeb22cc13 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 21 May 2016 15:10:53 -0500 Subject: [PATCH 54/77] Add base setup for R-IRV system. --- TallyCore/Global/Enumerations.cs | 2 ++ TallyCore/NetTally.Core.csproj | 1 + .../RankVoteCounting/RIRVRankVoteCounter.cs | 22 +++++++++++++++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 ++ 4 files changed, 27 insertions(+) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 658725a0..5b8dc556 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -36,6 +36,8 @@ public enum RankVoteCounterMethod LegacyCoombs, [EnumDescription("Baldwin Method")] Baldwin, + [EnumDescription("Rated Instant Runoff")] + RIRV, [EnumDescription("Pairwise Elimination")] Pairwise, [EnumDescription("Schulze Method")] diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index b31ad83d..bdbd661f 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -90,6 +90,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs new file mode 100644 index 00000000..0ded6cb2 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + + public class RIRVRankVoteCounter : BaseRankVoteCounter + { + protected override RankResults RankTask(GroupedVotesByTask task) + { + throw new NotImplementedException(); + } + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index 91ae5fec..4457b136 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -49,6 +49,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = return new BaldwinRankVoteCounter(); case RankVoteCounterMethod.InstantRunoff: return new InstantRunoffRankVoteCounter(); + case RankVoteCounterMethod.RIRV: + return new RIRVRankVoteCounter(); case RankVoteCounterMethod.Borda: return new BordaRankVoteCounter(); case RankVoteCounterMethod.BordaNormalized: From 22c6138ddfa4cb3c1031e321fee89b68369a519f Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 27 May 2016 14:11:57 -0500 Subject: [PATCH 55/77] Minor tweak. --- TallyCore/Utility/StringUtility.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TallyCore/Utility/StringUtility.cs b/TallyCore/Utility/StringUtility.cs index 7e5aad45..2e64f23c 100644 --- a/TallyCore/Utility/StringUtility.cs +++ b/TallyCore/Utility/StringUtility.cs @@ -31,9 +31,9 @@ public static string SafeString(string input) } /// - /// Magic character (currently ◈) to flag a user name as a base plan. + /// Magic character (currently ◈, \u25C8) to flag a user name as a base plan. /// - public static string PlanNameMarker { get; } = "\u25C8"; + public static string PlanNameMarker { get; } = "◈"; /// /// Check if the provided name starts with the plan name marker. From 97e742916743c0bf16c780ca2a3fa03e9aa9fa5a Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 27 May 2016 14:37:53 -0500 Subject: [PATCH 56/77] Fix version number back to branch line's value. --- NetTally/Properties/AssemblyInfo.cs | 4 ++-- TallyCore/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NetTally/Properties/AssemblyInfo.cs b/NetTally/Properties/AssemblyInfo.cs index 7dceb788..2c818e07 100644 --- a/NetTally/Properties/AssemblyInfo.cs +++ b/NetTally/Properties/AssemblyInfo.cs @@ -52,5 +52,5 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.7.0.0")] -[assembly: AssemblyFileVersion("1.7.3.1")] -[assembly: AssemblyInformationalVersion("1.7.3.1")] +[assembly: AssemblyFileVersion("1.7.4.0")] +[assembly: AssemblyInformationalVersion("1.7.4")] diff --git a/TallyCore/Properties/AssemblyInfo.cs b/TallyCore/Properties/AssemblyInfo.cs index 2f2dfa54..e492ac80 100644 --- a/TallyCore/Properties/AssemblyInfo.cs +++ b/TallyCore/Properties/AssemblyInfo.cs @@ -33,5 +33,5 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.7.0.0")] -[assembly: AssemblyFileVersion("1.7.3.1")] -[assembly: AssemblyInformationalVersion("1.7.3.1")] +[assembly: AssemblyFileVersion("1.7.4.0")] +[assembly: AssemblyInformationalVersion("1.7.4")] From 8ca9dc1f31af21de12d48ff5dfd13d6aeb265246 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 16:20:25 -0500 Subject: [PATCH 57/77] Change RankedVoters.Rank from string to int. --- .../BordaFractionRankVoteCounter.cs | 25 +------------------ .../BordaNormalizedRankVoteCounter.cs | 22 +--------------- .../RankVoteCounting/BordaRankVoteCounter.cs | 25 +------------------ .../Utility/GroupRankVotes.cs | 4 +-- 4 files changed, 5 insertions(+), 71 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs index 27491a1a..eaa0fd6d 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaFractionRankVoteCounter.cs @@ -61,33 +61,10 @@ private double RankVote(IEnumerable ranks) // If any voter didn't vote for an option, they effectively add a 0 (rank #10) for that option. foreach (var r in ranks) { - voteValue += ValueOfRank(r.Rank) * r.Voters.Count(); + voteValue += (1.0 / r.Rank) * r.Voters.Count(); } return voteValue; } - - /// - /// Get the numeric value of a given rank. - /// - /// The rank being evaluated. - /// - /// - /// - private double ValueOfRank(string rank) - { - if (string.IsNullOrEmpty(rank)) - throw new ArgumentNullException(nameof(rank)); - - int rankAsInt = int.Parse(rank); - - if (rankAsInt < 1 || rankAsInt > 9) - throw new ArgumentOutOfRangeException(nameof(rank)); - - // Ranks valued at 1 for #1, then 1/N for each higher N. - double rankValue = 1.0 / rankAsInt; - - return rankValue; - } } } diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs index 1d81b395..87275749 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaNormalizedRankVoteCounter.cs @@ -66,7 +66,7 @@ private double RankVote(IEnumerable ranks) foreach (var r in ranks) { - voteValue += (ValueOfRank(r.Rank) + 1.0) * r.Voters.Count(); + voteValue += (r.Rank + 1.0) * r.Voters.Count(); } int totalRankings = ranks.Sum(a => a.Voters.Count()); @@ -75,25 +75,5 @@ private double RankVote(IEnumerable ranks) return voteValue; } - - /// - /// Get the numeric value of a given rank. - /// - /// The rank being evaluated. - /// - /// - /// - private int ValueOfRank(string rank) - { - if (string.IsNullOrEmpty(rank)) - throw new ArgumentNullException(nameof(rank)); - - int rankAsInt = int.Parse(rank); - - if (rankAsInt < 1 || rankAsInt > 9) - throw new ArgumentOutOfRangeException(nameof(rank)); - - return rankAsInt; - } } } diff --git a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs index 1b4354de..766e10a3 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BordaRankVoteCounter.cs @@ -61,33 +61,10 @@ private int RankVote(IEnumerable ranks) // If any voter didn't vote for an option, they effectively add a 0 (rank #6) for that option. foreach (var r in ranks) { - voteValue += ValueOfRank(r.Rank) * r.Voters.Count(); + voteValue += (6 - r.Rank) * r.Voters.Count(); } return voteValue; } - - /// - /// Get the numeric value of a given rank. - /// - /// The rank being evaluated. - /// - /// - /// - private int ValueOfRank(string rank) - { - if (string.IsNullOrEmpty(rank)) - throw new ArgumentNullException(nameof(rank)); - - int rankAsInt = int.Parse(rank); - - if (rankAsInt < 1 || rankAsInt > 9) - throw new ArgumentOutOfRangeException(nameof(rank)); - - // Ranks valued at 5 for #1, then -1 per rank below that, to a minimum of -3. - int rankValue = (6 - rankAsInt); - - return rankValue; - } } } diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 69aaac53..9d3f1ff3 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -17,7 +17,7 @@ public class RankGroupedVoters public class RankedVoters { - public string Rank { get; set; } + public int Rank { get; set; } public IEnumerable Voters { get; set; } } @@ -45,7 +45,7 @@ group vote by content into votes VoteContent = votes.Key, Ranks = from v in votes group v by VoteString.GetVoteMarker(v.Key) into vr - select new RankedVoters { Rank = vr.Key, Voters = vr.SelectMany(a => a.Value) } + select new RankedVoters { Rank = int.Parse(vr.Key), Voters = vr.SelectMany(a => a.Value) } }; return res; From 7d76270cf74dcb8994324e543e473764a9682601 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 16:58:54 -0500 Subject: [PATCH 58/77] Add a static class for scoring ranked votes. --- TallyCore/NetTally.Core.csproj | 1 + .../RankVoteCounting/Utility/RankScoring.cs | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/Utility/RankScoring.cs diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index bdbd661f..54affac8 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -93,6 +93,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/RankScoring.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/RankScoring.cs new file mode 100644 index 00000000..b3e23fb5 --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/RankScoring.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NetTally.VoteCounting +{ + public static class RankScoring + { + /// + /// Calculates the Borda score for the provided list of voter rankings of a given vote. + /// + /// Votes with associated ranks, for the voters who ranked the vote with a given value. + /// Returns a numeric evaluation of the overall score of the vote. + public static double BordaScore(IEnumerable ranks) + { + double voteValue = 0; + + // Add up the sum of the number of voters times the value of each rank. + // Normalize to 9 point for #1, 8 points for #2, etc. + foreach (var r in ranks) + { + if (r.Rank > 0 && r.Rank < 10) + { + voteValue += (10 - r.Rank) * r.Voters.Count(); + } + } + + return voteValue; + } + + /// + /// Calculates the inverse Borda score for the provided list of voter rankings of a given vote. + /// + /// Votes with associated ranks, for the voters who ranked the vote with a given value. + /// Returns a numeric evaluation of the overall score of the vote. + public static double InverseBordaScore(IEnumerable ranks) + { + double voteValue = 0; + + // Add up the sum of the number of voters times the value of each rank. + // Value of each rank is 1/N. + foreach (var r in ranks) + { + if (r.Rank > 0 && r.Rank < 10) + { + voteValue += (1.0 / r.Rank) * r.Voters.Count(); + } + } + + return voteValue; + } + + + /// + /// Calculates the lower bound of the Wilson score for the provided list of voter rankings of a given vote. + /// Reference: http://www.evanmiller.org/how-not-to-sort-by-average-rating.html + /// + /// Votes with associated ranks, for the voters who ranked the vote with a given value. + /// Returns a numeric evaluation of the overall score of the vote. + public static double LowerWilsonScore(IEnumerable ranks) + { + int n = ranks.Sum(a => a.Voters.Count()); + + if (n == 0) + return 0; + + double positiveScore = 0.0; + double negativeScore = 0.0; + + // Add up the sum of the number of voters times the value of each rank. + // Value of each rank is 1/N. + foreach (var r in ranks) + { + double scaledPositiveScore = PositivePortionOf9RankScale(r.Rank); + + positiveScore += scaledPositiveScore * r.Voters.Count(); + negativeScore += (1.0 - scaledPositiveScore) * r.Voters.Count(); + } + + double p̂ = positiveScore / (positiveScore + negativeScore); + double z = 1.96; + double sqTerm = (p̂ * (1 - p̂) + z * z / (4 * n)) / n; + + double lowerWilson = (p̂ + (z * z / (2 * n)) - z * Math.Sqrt(sqTerm)) / (1 + z * z / n); + + return lowerWilson; + } + + private static double PositivePortionOf9RankScale(int rank) + { + // Rank 1 = 1.0 positive, 0.0 negative + // Rank 2 = 0.875 positive, 0.125 negative + // Rank 3 = 0.75 positive, 0.25 negative + // Rank 4 = 0.625 positive, 0.375 negative + // Rank 5 = 0.5 positive, 0.5 negative + // Rank 6 = 0.375 positive, 0.625 negative + // Rank 7 = 0.25 positive, 0.75 negative + // Rank 8 = 0.125 positive, 0.875 negative + // Rank 9 = 0.0 positive, 1.0 negative + + if (rank < 1) + return 0; + + if (rank > 9) + rank = 9; + + int decrement = rank - 1; + + return 1.0 - (decrement * 0.125); + } + + } +} From 31ba244af1ebc9af9e1ffaba09b054b97a153654 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 17:34:17 -0500 Subject: [PATCH 59/77] Add Wilson Scoring as an option. --- TallyCore/Global/Enumerations.cs | 2 + TallyCore/NetTally.Core.csproj | 1 + .../RankVoteCounting/WilsonRankVoteCounter.cs | 61 +++++++++++++++++++ TallyCore/VoteCounting/VoteCounterLocator.cs | 2 + 4 files changed, 66 insertions(+) create mode 100644 TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index 5b8dc556..e385a763 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -28,6 +28,8 @@ public enum RankVoteCounterMethod Borda, [EnumDescription("Borda Normalized")] BordaNormalized, + [EnumDescription("Wilson Scoring")] + Wilson, [EnumDescription("Instant Runoff")] InstantRunoff, [EnumDescription("Coombs' Method")] diff --git a/TallyCore/NetTally.Core.csproj b/TallyCore/NetTally.Core.csproj index 54affac8..02a2d5ec 100644 --- a/TallyCore/NetTally.Core.csproj +++ b/TallyCore/NetTally.Core.csproj @@ -94,6 +94,7 @@ + diff --git a/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs new file mode 100644 index 00000000..3344577f --- /dev/null +++ b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace NetTally.VoteCounting +{ + // List of preference results ordered by winner + using RankResults = List; + // Task (string), Ordered list of ranked votes + using RankResultsByTask = Dictionary>; + // Vote (string), collection of voters + using SupportedVotes = Dictionary>; + // Task (string group), collection of votes (string vote, hashset of voters) + using GroupedVotesByTask = IGrouping>>; + + /// + /// Borda is being removed as a valid option from the list of rank vote options. + /// Aside from systemic failures of the method itself, it cannot give proper + /// valuation to unranked options, which intrinsically makes it a bad fit + /// for handling user-entered quest voting schemes. + /// + /// + public class WilsonRankVoteCounter : BaseRankVoteCounter + { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. + protected override RankResults RankTask(GroupedVotesByTask task) + { + Debug.WriteLine(">>Wilson Limit<<"); + + var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); + + var rankedVotes = from vote in groupVotes + select new { Vote = vote.VoteContent, Rank = RankVote(vote.Ranks) }; + + var orderedVotes = rankedVotes.OrderByDescending(a => a.Rank); + + foreach (var orderedVote in orderedVotes) + { + Debug.WriteLine($"- {orderedVote.Vote} [{orderedVote.Rank:f5}]"); + } + + return orderedVotes.Select(a => a.Vote).ToList(); + } + + /// + /// Ranks the vote. + /// + /// Votes with associated ranks, for the voters who ranked the vote with a given value. + /// Returns a numeric evaluation of the overall rank of the vote. + private double RankVote(IEnumerable ranks) + { + return RankScoring.LowerWilsonScore(ranks); + } + } +} diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index 4457b136..c5de5dc9 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -55,6 +55,8 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = return new BordaRankVoteCounter(); case RankVoteCounterMethod.BordaNormalized: return new BordaNormalizedRankVoteCounter(); + case RankVoteCounterMethod.Wilson: + return new WilsonRankVoteCounter(); case RankVoteCounterMethod.Pairwise: return new PairwiseRankVoteCounter(); case RankVoteCounterMethod.Schulze: From 5d28f1a54ff7959145e5ae13e3b8d93e84d30583 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 17:48:12 -0500 Subject: [PATCH 60/77] Comment out methods that have been eliminated from consideration. --- TallyCore/Global/Enumerations.cs | 30 +++++++++--------- TallyCore/VoteCounting/VoteCounterLocator.cs | 32 ++++++++++---------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/TallyCore/Global/Enumerations.cs b/TallyCore/Global/Enumerations.cs index e385a763..af85b52c 100644 --- a/TallyCore/Global/Enumerations.cs +++ b/TallyCore/Global/Enumerations.cs @@ -24,26 +24,28 @@ public enum RankVoteCounterMethod Default, [EnumDescription("Distance Scoring")] Distance, - [EnumDescription("Borda Count")] - Borda, - [EnumDescription("Borda Normalized")] - BordaNormalized, [EnumDescription("Wilson Scoring")] Wilson, - [EnumDescription("Instant Runoff")] - InstantRunoff, - [EnumDescription("Coombs' Method")] - Coombs, - [EnumDescription("Legacy Coombs")] - LegacyCoombs, - [EnumDescription("Baldwin Method")] + [EnumDescription("Baldwin Runoff")] Baldwin, [EnumDescription("Rated Instant Runoff")] RIRV, - [EnumDescription("Pairwise Elimination")] - Pairwise, - [EnumDescription("Schulze Method")] + [EnumDescription("Schulze (Condorcet)")] Schulze, + //[EnumDescription("Borda Count")] + //Borda, + //[EnumDescription("Borda Normalized")] + //BordaNormalized, + //[EnumDescription("Borda Fraction")] + //BordaFraction, + //[EnumDescription("Instant Runoff")] + //InstantRunoff, + //[EnumDescription("Coombs' Method")] + //Coombs, + //[EnumDescription("Legacy Coombs")] + //LegacyCoombs, + //[EnumDescription("Pairwise Elimination")] + //Pairwise, } public enum StandardVoteCounterMethod diff --git a/TallyCore/VoteCounting/VoteCounterLocator.cs b/TallyCore/VoteCounting/VoteCounterLocator.cs index c5de5dc9..b81b693b 100644 --- a/TallyCore/VoteCounting/VoteCounterLocator.cs +++ b/TallyCore/VoteCounting/VoteCounterLocator.cs @@ -41,28 +41,28 @@ public static IRankVoteCounter GetRankVoteCounter(RankVoteCounterMethod method = { switch (method) { - case RankVoteCounterMethod.Coombs: - return new CoombsRankVoteCounter(); - case RankVoteCounterMethod.LegacyCoombs: - return new LegacyCoombsRankVoteCounter(); + //case RankVoteCounterMethod.Coombs: + // return new CoombsRankVoteCounter(); + //case RankVoteCounterMethod.LegacyCoombs: + // return new LegacyCoombsRankVoteCounter(); + //case RankVoteCounterMethod.InstantRunoff: + // return new InstantRunoffRankVoteCounter(); + //case RankVoteCounterMethod.Borda: + // return new BordaRankVoteCounter(); + //case RankVoteCounterMethod.BordaNormalized: + // return new BordaNormalizedRankVoteCounter(); + //case RankVoteCounterMethod.Pairwise: + // return new PairwiseRankVoteCounter(); case RankVoteCounterMethod.Baldwin: return new BaldwinRankVoteCounter(); - case RankVoteCounterMethod.InstantRunoff: - return new InstantRunoffRankVoteCounter(); - case RankVoteCounterMethod.RIRV: - return new RIRVRankVoteCounter(); - case RankVoteCounterMethod.Borda: - return new BordaRankVoteCounter(); - case RankVoteCounterMethod.BordaNormalized: - return new BordaNormalizedRankVoteCounter(); case RankVoteCounterMethod.Wilson: return new WilsonRankVoteCounter(); - case RankVoteCounterMethod.Pairwise: - return new PairwiseRankVoteCounter(); - case RankVoteCounterMethod.Schulze: - return new SchulzeRankVoteCounter(); case RankVoteCounterMethod.Distance: return new DistanceRankVoteCounter(); + case RankVoteCounterMethod.Schulze: + return new SchulzeRankVoteCounter(); + case RankVoteCounterMethod.RIRV: + return new RIRVRankVoteCounter(); default: return new BaldwinRankVoteCounter(); } From d82788c6a68a1c337b5cc56dba6e1ddd3a45a06e Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 17:48:25 -0500 Subject: [PATCH 61/77] Streamline code. --- .../RankVoteCounting/WilsonRankVoteCounter.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs index 3344577f..8039b68b 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs @@ -7,10 +7,6 @@ namespace NetTally.VoteCounting { // List of preference results ordered by winner using RankResults = List; - // Task (string), Ordered list of ranked votes - using RankResultsByTask = Dictionary>; - // Vote (string), collection of voters - using SupportedVotes = Dictionary>; // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; @@ -36,7 +32,7 @@ protected override RankResults RankTask(GroupedVotesByTask task) var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); var rankedVotes = from vote in groupVotes - select new { Vote = vote.VoteContent, Rank = RankVote(vote.Ranks) }; + select new { Vote = vote.VoteContent, Rank = RankScoring.LowerWilsonScore(vote.Ranks) }; var orderedVotes = rankedVotes.OrderByDescending(a => a.Rank); @@ -47,15 +43,5 @@ protected override RankResults RankTask(GroupedVotesByTask task) return orderedVotes.Select(a => a.Vote).ToList(); } - - /// - /// Ranks the vote. - /// - /// Votes with associated ranks, for the voters who ranked the vote with a given value. - /// Returns a numeric evaluation of the overall rank of the vote. - private double RankVote(IEnumerable ranks) - { - return RankScoring.LowerWilsonScore(ranks); - } } } From 8512d5862de1a05acc861ff1ef4ab6b68a504bcd Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 18:48:40 -0500 Subject: [PATCH 62/77] Add an extra function to convert from VoterRankings to RankGroupedVoters. --- .../Utility/GroupRankVotes.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 9d3f1ff3..e94eb9d7 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -51,6 +51,29 @@ group v by VoteString.GetVoteMarker(v.Key) into vr return res; } + public static IEnumerable GroupByVoteAndRank(IEnumerable rankings) + { + var q = from v in rankings + from w in v.RankedVotes + group w by w.Vote into wv + select new RankGroupedVoters + { + VoteContent = wv.Key, + Ranks = from v2 in rankings + let voter = v2.Voter + from r in v2.RankedVotes + where r.Vote == wv.Key + group voter by r.Rank into vs2 + select new RankedVoters + { + Rank = vs2.Key, + Voters = vs2.Select(g2 => g2) + } + }; + + return q; + } + public static IEnumerable GroupByVoterAndRank(GroupedVotesByTask task) { var res = from vote in task From 3c4e0ae103da8e6bd324cd6d603d5799220005c8 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 18:54:36 -0500 Subject: [PATCH 63/77] Rework least preferred choice to use Wilson scoring instead of manual pseudo-Borda scoring. Ensure that, when removing choices from player rank lists, there are no blank spaces left in the rank ordering. --- .../BaldwinRankVoteCounter.cs | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs index 838989c3..291b564f 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -112,8 +112,6 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu string leastPreferredChoice = GetLeastPreferredChoice(localRankings); - Debug.Write($"{leastPreferredChoice}"); - RemoveChoiceFromVotes(localRankings, leastPreferredChoice); eliminateOne = true; } @@ -148,7 +146,11 @@ private List RemoveChoicesFromVotes(IEnumerable vo select new VoterRankings { Voter = voter.Voter, - RankedVotes = voter.RankedVotes.Where(v => chosenChoices.Contains(v.Vote) == false).OrderBy(v => v.Rank).ToList() + RankedVotes = voter.RankedVotes + .Where(v => chosenChoices.Contains(v.Vote) == false) + .OrderBy(v => v.Rank) + .Select((a, b) => new RankedVote { Vote = a.Vote, Rank = b + 1 }) + .ToList() }; return res.ToList(); @@ -206,37 +208,16 @@ private void RemoveChoiceFromVotes(List voterRankings, string cho /// Returns the vote string for the least preferred vote. private string GetLeastPreferredChoice(List localRankings) { - Dictionary rankTotals = new Dictionary(); - Dictionary rankInstances = new Dictionary(); - HashSet choices = new HashSet(); - - foreach (var voter in localRankings) - { - foreach (var rank in voter.RankedVotes) - { - if (!rankTotals.ContainsKey(rank.Vote)) - rankTotals[rank.Vote] = 0; - if (!rankInstances.ContainsKey(rank.Vote)) - rankInstances[rank.Vote] = 0; - - rankTotals[rank.Vote] += rank.Rank + 1; - rankInstances[rank.Vote]++; - choices.Add(rank.Vote); - } - } + var groupVotes = GroupRankVotes.GroupByVoteAndRank(localRankings); - var rankScaling = from choice in choices - select new - { - Choice = choice, - Value = rankTotals[choice] / rankInstances[choice] / rankInstances[choice] - }; + var rankedVotes = from vote in groupVotes + select new { Vote = vote.VoteContent, Rank = RankScoring.LowerWilsonScore(vote.Ranks) }; - var maxResult = rankScaling.MaxObject(a => a.Value); + var worstVote = rankedVotes.MinObject(a => a.Rank); - Debug.Write($"({maxResult.Value:f5}) "); + Debug.Write($"({worstVote.Rank:f5}) {worstVote.Vote}"); - return maxResult.Choice; + return worstVote.Vote; } /// From 3a62256d4b5980624d41900f42d01e63d0fad846 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 19:43:59 -0500 Subject: [PATCH 64/77] Clarify output. --- .../VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs index 8039b68b..f8f0311f 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs @@ -27,7 +27,7 @@ public class WilsonRankVoteCounter : BaseRankVoteCounter /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { - Debug.WriteLine(">>Wilson Limit<<"); + Debug.WriteLine(">>Wilson Scoring<<"); var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); From bc91fb1e716884b2e20cda2aaf6ab065662362a6 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 8 Jun 2016 19:44:12 -0500 Subject: [PATCH 65/77] Implement RIR counting. --- .../RankVoteCounting/RIRVRankVoteCounter.cs | 179 +++++++++++++++++- 1 file changed, 176 insertions(+), 3 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs index 0ded6cb2..2faa643d 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NetTally.VoteCounting { @@ -16,7 +15,181 @@ public class RIRVRankVoteCounter : BaseRankVoteCounter { protected override RankResults RankTask(GroupedVotesByTask task) { - throw new NotImplementedException(); + Debug.WriteLine(">>Rated Instant Runoff<<"); + + List winningChoices = new List(); + + var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); + var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + var allChoices = GetAllChoices(voterRankings); + + + for (int i = 1; i <= 9; i++) + { + string winner = GetWinningVote(voterRankings, groupVotes); + + if (winner == null) + break; + + winningChoices.Add(winner); + allChoices.Remove(winner); + + Debug.WriteLine($"- {winner}"); + + if (!allChoices.Any()) + break; + + voterRankings = RemoveChoiceFromVotes(voterRankings, winner); + groupVotes = RemoveChoiceFromRanks(groupVotes, winner); + } + + return winningChoices; + } + + private string GetWinningVote(IEnumerable voterRankings, IEnumerable groupVotes) + { + var rankedVotes = from vote in groupVotes + select new { Vote = vote.VoteContent, Rank = RankScoring.LowerWilsonScore(vote.Ranks) }; + + var orderedVotes = rankedVotes.OrderByDescending(a => a.Rank); + + var topTwo = orderedVotes.Take(2); + + if (!topTwo.Any()) + return null; + + if (topTwo.Count() == 1) + return topTwo.First().Vote; + + string option1 = topTwo.First().Vote; + string option2 = topTwo.Last().Vote; + bool option1ScoreHigher = topTwo.First().Rank > topTwo.Last().Rank; + + + Debug.Write($"[{option1}, {option2}] "); + + string preferredOption = GetOptionWithHigherPrefCount(voterRankings, option1, option2, option1ScoreHigher); + + return preferredOption; } + + + private string GetOptionWithHigherPrefCount(IEnumerable voterRankings, string option1, string option2, bool option1ScoreHigher) + { + int count1 = 0; + int count2 = 0; + + foreach (var voter in voterRankings) + { + var rank1 = voter.RankedVotes.FirstOrDefault(a => a.Vote == option1); + var rank2 = voter.RankedVotes.FirstOrDefault(a => a.Vote == option2); + + if (rank1 == null && rank2 == null) + continue; + + if (rank1 == null) + { + count2++; + continue; + } + + if (rank2 == null) + { + count1++; + continue; + } + + if (rank1.Rank > rank2.Rank) + { + count2++; + } + else if (rank2.Rank > rank1.Rank) + { + count1++; + } + } + + if (count1 > count2) + { + return option1; + } + if (count2 > count1) + { + return option2; + } + if (option1ScoreHigher) + { + return option1; + } + return option2; + } + + + /// + /// Gets all choices from all user votes. + /// + /// The collection of user votes. + /// Returns a list of all the choices in the task. + private List GetAllChoices(IEnumerable rankings) + { + var res = rankings.SelectMany(r => r.RankedVotes).Select(r => r.Vote).Distinct(); + + return res.ToList(); + } + + /// + /// Removes a list of choices from voter rankings. + /// These are the choices that have already won a rank spot. + /// + /// The voter rankings. + /// The already chosen choices. + /// Returns the results as a list. + private List RemoveChoiceFromVotes(IEnumerable voterRankings, string choice) + { + var res = from voter in voterRankings + select new VoterRankings + { + Voter = voter.Voter, + RankedVotes = voter.RankedVotes + .Where(v => choice != v.Vote) + .OrderBy(v => v.Rank) + .Select((a, b) => new RankedVote { Vote = a.Vote, Rank = b + 1 }) + .ToList() + }; + + return res.ToList(); + } + + private List RemoveChoiceFromRanks(IEnumerable groupVotes, string winner) + { + var res = groupVotes.Where(a => a.VoteContent != winner); + + return res.ToList(); + } + + + /// + /// Removes a list of choices from voter rankings. + /// These are the choices that have already won a rank spot. + /// + /// The voter rankings. + /// The already chosen choices. + /// Returns the results as a list. + private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) + { + var res = from voter in voterRankings + select new VoterRankings + { + Voter = voter.Voter, + RankedVotes = voter.RankedVotes + .Where(v => chosenChoices.Contains(v.Vote) == false) + .OrderBy(v => v.Rank) + .Select((a, b) => new RankedVote { Vote = a.Vote, Rank = b + 1 }) + .ToList() + }; + + return res.ToList(); + } + } } From 8f5e98f3fb627f96642d92e965a6724276fd78f4 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 9 Jun 2016 15:11:08 -0500 Subject: [PATCH 66/77] Refactor RIR some. Add comments. --- .../RankVoteCounting/RIRVRankVoteCounter.cs | 148 +++++++++++------- .../Utility/GroupRankVotes.cs | 2 - .../RankVoteCounting/WilsonRankVoteCounter.cs | 15 +- 3 files changed, 99 insertions(+), 66 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs index 2faa643d..93f72425 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs @@ -10,23 +10,41 @@ namespace NetTally.VoteCounting // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; - + /// + /// Rated Instant Runoff voting scores all vote options, taking the top two, + /// and does an instant runoff between them. + /// This uses the Wilson score for scoring votes. + /// The scoring aspect allows relative preference to be taken into account, + /// but relative preference is ignored for the runoff phase, which merely + /// checks for which of A or B is most often preferred over the other. + /// This avoids the flaws of standard instant runoff voting by incorporating + /// score ratings into the evaluation. + /// + /// public class RIRVRankVoteCounter : BaseRankVoteCounter { + /// + /// Implementation to generate the ranking list for the provided set + /// of votes for a specific task. + /// + /// The task that the votes are grouped under. + /// Returns a ranking list of winning votes. protected override RankResults RankTask(GroupedVotesByTask task) { Debug.WriteLine(">>Rated Instant Runoff<<"); List winningChoices = new List(); - var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); + // The groupVotes are used for getting the Wilson score + var rankedVotes = GroupRankVotes.GroupByVoteAndRank(task); + // The voterRankings are used for the runoff var voterRankings = GroupRankVotes.GroupByVoterAndRank(task); + // The full choices list is just to keep track of how many we have left. var allChoices = GetAllChoices(voterRankings); - for (int i = 1; i <= 9; i++) { - string winner = GetWinningVote(voterRankings, groupVotes); + string winner = GetWinningVote(voterRankings, rankedVotes); if (winner == null) break; @@ -40,41 +58,72 @@ protected override RankResults RankTask(GroupedVotesByTask task) break; voterRankings = RemoveChoiceFromVotes(voterRankings, winner); - groupVotes = RemoveChoiceFromRanks(groupVotes, winner); + rankedVotes = RemoveChoiceFromRanks(rankedVotes, winner); } return winningChoices; } - private string GetWinningVote(IEnumerable voterRankings, IEnumerable groupVotes) + /// + /// Gets the winning vote. + /// + /// The voter rankings. + /// The votes, ranked. + /// + private string GetWinningVote(IEnumerable voterRankings, IEnumerable rankedVotes) { - var rankedVotes = from vote in groupVotes - select new { Vote = vote.VoteContent, Rank = RankScoring.LowerWilsonScore(vote.Ranks) }; - - var orderedVotes = rankedVotes.OrderByDescending(a => a.Rank); + string option1; + string option2; - var topTwo = orderedVotes.Take(2); + GetTopTwoRatedOptions(rankedVotes, out option1, out option2); - if (!topTwo.Any()) - return null; - - if (topTwo.Count() == 1) - return topTwo.First().Vote; + return GetOptionWithHigherPrefCount(voterRankings, option1, option2); + } - string option1 = topTwo.First().Vote; - string option2 = topTwo.Last().Vote; - bool option1ScoreHigher = topTwo.First().Rank > topTwo.Last().Rank; + /// + /// Gets the top two rated options. + /// + /// The group votes. + /// The top rated option. Null if there aren't any options available. + /// The second rated option. Null if there is only one option available. + private void GetTopTwoRatedOptions(IEnumerable rankedVotes, out string option1, out string option2) + { + var scoredVotes = from vote in rankedVotes + select new { Vote = vote.VoteContent, Rank = RankScoring.LowerWilsonScore(vote.Ranks) }; + var orderedVotes = scoredVotes.OrderByDescending(a => a.Rank); - Debug.Write($"[{option1}, {option2}] "); + var topTwo = orderedVotes.Take(2); - string preferredOption = GetOptionWithHigherPrefCount(voterRankings, option1, option2, option1ScoreHigher); + if (!topTwo.Any()) + { + option1 = null; + option2 = null; + } + else if (topTwo.Count() == 1) + { + option1 = topTwo.First().Vote; + option2 = null; + } + else + { + option1 = topTwo.First().Vote; + option2 = topTwo.Last().Vote; + } - return preferredOption; + Debug.Write($"[{option1 ?? ""}, {option2 ?? ""}] "); } - - private string GetOptionWithHigherPrefCount(IEnumerable voterRankings, string option1, string option2, bool option1ScoreHigher) + /// + /// Gets the option with higher preference count. + /// This is the runoff portion of the vote evaluation. Whichever option has more + /// people that prefer it over the other, wins. + /// + /// The voter rankings. This allows seeing which option each voter preferred. + /// The first option up for consideration. + /// The second option up for consideration. + /// Returns the winning option. + private string GetOptionWithHigherPrefCount(IEnumerable voterRankings, string option1, string option2) { int count1 = 0; int count2 = 0; @@ -109,19 +158,16 @@ private string GetOptionWithHigherPrefCount(IEnumerable voterRank } } - if (count1 > count2) - { - return option1; - } + // If count1==count2, we use the higher scored option, which + // will necessarily be option1. Therefore all ties will be + // in favor of option1, and the only thing we need to check + // for is if option2 wins explicitly. if (count2 > count1) { return option2; } - if (option1ScoreHigher) - { - return option1; - } - return option2; + + return option1; } @@ -138,51 +184,33 @@ private List GetAllChoices(IEnumerable rankings) } /// - /// Removes a list of choices from voter rankings. - /// These are the choices that have already won a rank spot. + /// Removes the specified vote choice from the list of ranked votes. /// - /// The voter rankings. - /// The already chosen choices. - /// Returns the results as a list. - private List RemoveChoiceFromVotes(IEnumerable voterRankings, string choice) - { - var res = from voter in voterRankings - select new VoterRankings - { - Voter = voter.Voter, - RankedVotes = voter.RankedVotes - .Where(v => choice != v.Vote) - .OrderBy(v => v.Rank) - .Select((a, b) => new RankedVote { Vote = a.Vote, Rank = b + 1 }) - .ToList() - }; - - return res.ToList(); - } - - private List RemoveChoiceFromRanks(IEnumerable groupVotes, string winner) + /// The ranked votes. + /// The choice being removed. + /// + private IEnumerable RemoveChoiceFromRanks(IEnumerable rankedVotes, string choice) { - var res = groupVotes.Where(a => a.VoteContent != winner); + var res = rankedVotes.Where(a => a.VoteContent != choice); return res.ToList(); } - /// /// Removes a list of choices from voter rankings. /// These are the choices that have already won a rank spot. /// /// The voter rankings. - /// The already chosen choices. + /// The already chosen choices. /// Returns the results as a list. - private List RemoveChoicesFromVotes(IEnumerable voterRankings, List chosenChoices) + private IEnumerable RemoveChoiceFromVotes(IEnumerable voterRankings, string choice) { var res = from voter in voterRankings select new VoterRankings { Voter = voter.Voter, RankedVotes = voter.RankedVotes - .Where(v => chosenChoices.Contains(v.Vote) == false) + .Where(v => choice != v.Vote) .OrderBy(v => v.Rank) .Select((a, b) => new RankedVote { Vote = a.Vote, Rank = b + 1 }) .ToList() diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index e94eb9d7..895d1b5a 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NetTally.VoteCounting { diff --git a/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs index f8f0311f..4d075b1b 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/WilsonRankVoteCounter.cs @@ -11,10 +11,13 @@ namespace NetTally.VoteCounting using GroupedVotesByTask = IGrouping>>; /// - /// Borda is being removed as a valid option from the list of rank vote options. - /// Aside from systemic failures of the method itself, it cannot give proper - /// valuation to unranked options, which intrinsically makes it a bad fit - /// for handling user-entered quest voting schemes. + /// Wilson vote scoring uses the lower bounds of a Bournoulli analysis of the vote + /// rankings to get the 95% minimum confidence interval. + /// This means that a voted item with only a few supporters will have a low score + /// due to a high error margin, while a score with more supporters will have a + /// higher relative confidence rating. + /// This improves on the Borda scoring, which has no means of compensating for + /// votes that are ranked by less than 100% of the voter base. /// /// public class WilsonRankVoteCounter : BaseRankVoteCounter @@ -29,6 +32,8 @@ protected override RankResults RankTask(GroupedVotesByTask task) { Debug.WriteLine(">>Wilson Scoring<<"); + // Can calculating the score easily by having all the rankings for + // each vote grouped together. var groupVotes = GroupRankVotes.GroupByVoteAndRank(task); var rankedVotes = from vote in groupVotes @@ -36,11 +41,13 @@ protected override RankResults RankTask(GroupedVotesByTask task) var orderedVotes = rankedVotes.OrderByDescending(a => a.Rank); + // Display the votes and their ratings for debugging purposes. foreach (var orderedVote in orderedVotes) { Debug.WriteLine($"- {orderedVote.Vote} [{orderedVote.Rank:f5}]"); } + // Only return the list of vote options themselves. return orderedVotes.Select(a => a.Vote).ToList(); } } From 30cfac1cd8103186b7fbe2d03232c7313eebfd0a Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 9 Jun 2016 15:16:00 -0500 Subject: [PATCH 67/77] Make sure to not try to search for nulls. --- .../VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs index 93f72425..34dd1175 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs @@ -77,7 +77,9 @@ private string GetWinningVote(IEnumerable voterRankings, IEnumera GetTopTwoRatedOptions(rankedVotes, out option1, out option2); - return GetOptionWithHigherPrefCount(voterRankings, option1, option2); + string winner = GetOptionWithHigherPrefCount(voterRankings, option1, option2); + + return winner; } /// @@ -125,6 +127,9 @@ private void GetTopTwoRatedOptions(IEnumerable rankedVotes, o /// Returns the winning option. private string GetOptionWithHigherPrefCount(IEnumerable voterRankings, string option1, string option2) { + if (string.IsNullOrEmpty(option2)) + return option1; + int count1 = 0; int count2 = 0; From 54c60bd979e9438a3872beaa06e5bb85d6e236e5 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 9 Jun 2016 15:17:02 -0500 Subject: [PATCH 68/77] Removing a choice from a player's rating list should not change the rated value. It also doesn't need to be ordered. --- .../VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs index 34dd1175..234beee6 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/RIRVRankVoteCounter.cs @@ -215,9 +215,7 @@ private IEnumerable RemoveChoiceFromVotes(IEnumerable choice != v.Vote) - .OrderBy(v => v.Rank) - .Select((a, b) => new RankedVote { Vote = a.Vote, Rank = b + 1 }) + .Where(v => v.Vote != choice) .ToList() }; From c899dbf3f7203a1d3d8ccaa2b912da68a90a8f93 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 9 Jun 2016 15:23:46 -0500 Subject: [PATCH 69/77] Add a VoteRating class. Tidy up and streamline code. --- .../Utility/GroupRankVotes.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 895d1b5a..4b36a0c0 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -7,22 +7,16 @@ namespace NetTally.VoteCounting // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; - public class RankGroupedVoters - { - public string VoteContent { get; set; } - public IEnumerable Ranks { get; set; } - } - - public class RankedVoters + public class RankedVote { + public string Vote { get; set; } public int Rank { get; set; } - public IEnumerable Voters { get; set; } } - public class RankedVote + public class RatedVote { - public int Rank { get; set; } public string Vote { get; set; } + public double Rating { get; set; } } public class VoterRankings @@ -31,6 +25,22 @@ public class VoterRankings public List RankedVotes { get; set; } } + public class RankedVoters + { + public int Rank { get; set; } + public IEnumerable Voters { get; set; } + } + + public class RankGroupedVoters + { + public string VoteContent { get; set; } + public IEnumerable Ranks { get; set; } + } + + /// + /// Static class to take known input lists and convert them to an + /// enumerable list of one of the above types. + /// public static class GroupRankVotes { public static IEnumerable GroupByVoteAndRank(GroupedVotesByTask task) @@ -83,7 +93,7 @@ group vote by voter into voters RankedVotes = (from v in voters select new RankedVote { - Rank = RankAsInt(VoteString.GetVoteMarker(v.Key)), + Rank = int.Parse(VoteString.GetVoteMarker(v.Key)), Vote = VoteString.GetVoteContent(v.Key) }).ToList() }; @@ -91,15 +101,5 @@ group vote by voter into voters return res; } - - private static int RankAsInt(string rank) - { - if (string.IsNullOrEmpty(rank)) - throw new ArgumentNullException(nameof(rank)); - - int rankAsInt = int.Parse(rank); - - return rankAsInt; - } } } From c97c80e0fd2ca333850b4fe65dda9b5bfe4a6546 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 9 Jun 2016 20:46:51 -0500 Subject: [PATCH 70/77] Tidy code. Remove unused function. Move utility class to utility class file. --- .../BaldwinRankVoteCounter.cs | 30 +------------------ .../DistanceRankVoteCounter.cs | 9 ++---- .../Utility/GroupRankVotes.cs | 6 ++++ 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs index 291b564f..4fafb027 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/BaldwinRankVoteCounter.cs @@ -124,13 +124,7 @@ private string GetWinningVote(IEnumerable voterRankings, RankResu return null; } - private string Comma(bool addComma) - { - if (addComma) - return ", "; - else - return ""; - } + private string Comma(bool addComma) => addComma ? ", " : ""; /// @@ -156,28 +150,6 @@ private List RemoveChoicesFromVotes(IEnumerable vo return res.ToList(); } - /// - /// Adds ranking entries for any choices that users did not explictly rank. - /// Modifies the provided list. - /// - /// The vote rankings. - /// All available choices. - private void AddUnselectedRankings(List localRankings, RankResults allChoices) - { - foreach (var ranker in localRankings) - { - if (ranker.RankedVotes.Count == allChoices.Count) - continue; - - var extras = allChoices.Except(ranker.RankedVotes.Select(v => v.Vote)); - - foreach (var extra in extras) - { - ranker.RankedVotes.Add(new RankedVote { Vote = extra, Rank = 10 }); - } - } - } - /// /// Filter the provided list of voter rankings to remove any instances of the specified choice. /// Modifies the provided list. diff --git a/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs index 4a9af242..1708a827 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/DistanceRankVoteCounter.cs @@ -10,11 +10,6 @@ namespace NetTally.VoteCounting // Task (string group), collection of votes (string vote, hashset of voters) using GroupedVotesByTask = IGrouping>>; - public class DistanceData - { - public int[,] Paths { get; set; } - } - public class DistanceRankVoteCounter : BaseRankVoteCounter { /// @@ -50,8 +45,8 @@ protected override RankResults RankTask(GroupedVotesByTask task) /// /// Fills the pairwise preferences. /// This goes through each voter's ranking options and updates an array indicating - /// which options are preferred over which other options. Each higher-ranked - /// option gains one point in 'beating' a lower-ranked option. + /// which options are preferred over which other options. + /// Each higher-ranked option gains the difference in ranking in 'beating' a lower-ranked option. /// /// The voter rankings. /// The list of choices. diff --git a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs index 4b36a0c0..e6a1d9a5 100644 --- a/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs +++ b/TallyCore/VoteCounting/RankVoteCounting/Utility/GroupRankVotes.cs @@ -102,4 +102,10 @@ group vote by voter into voters } } + + public class DistanceData + { + public int[,] Paths { get; set; } + } + } From 9556cb051b7178dc1abd3e2c7c7656c978a9bb4f Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 20 Jun 2016 19:20:21 -0500 Subject: [PATCH 71/77] Fix regex so that it recognizes non-word symbols at the start of a plan name. EG: Plan "World of Tomorrow" --- TallyCore/Votes/VoteString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TallyCore/Votes/VoteString.cs b/TallyCore/Votes/VoteString.cs index 8c00563b..37526174 100644 --- a/TallyCore/Votes/VoteString.cs +++ b/TallyCore/Votes/VoteString.cs @@ -23,7 +23,7 @@ private static class VoteComponents // Single line version of the vote line regex. static readonly Regex voteLineRegexSingleLine = new Regex(@"^(?[-\s]*)\[\s*(?[xX✓✔1-9])\s*\]\s*(\[\s*(?![bui]\]|color=|url=)(?[^]]*?)\])?\s*(?.*)", RegexOptions.Singleline); // Potential reference to another user's plan. - static readonly Regex referenceNameRegex = new Regex(@"^(?