/// <summary> /// 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 /// </summary> /// <param name="votes">Votes with associated ranks, for the voters who ranked the vote with a given value.</param> /// <returns>Returns a numeric evaluation of the overall score of the vote.</returns> public static (double score, int count) LowerWilsonRankingScore(VoteStorageEntry votes) { int n = votes.Value.Count(v => v.Value.MarkerType == MarkerType.Rank); if (n == 0) { return(0, 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 vote in votes.Value) { if (vote.Value.MarkerType == MarkerType.Rank && vote.Value.MarkerValue > 0 && vote.Value.MarkerValue < 10) { double scaledPositiveScore = PositivePortionOf9RankScale(vote.Value.MarkerValue); positiveScore += scaledPositiveScore; negativeScore += (1.0 - scaledPositiveScore); } } 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, n); }
/// <summary> /// Construct vote output per task for scored votes. /// </summary> /// <param name="votesInTask">The group of votes falling under a task.</param> /// <param name="token">Cancellation token.</param> private void ConstructScoredOutput(VotesGroupedByTask votesInTask, IEnumerable <CompactVote> compactVotesInTask) { bool multiline = votesInTask.Any(a => a.Key.Lines.Count > 1); if (displayMode == DisplayMode.Compact || displayMode == DisplayMode.CompactNoVoters) { var orderedResults = compactVotesInTask .OrderByDescending(a => a.Voters.GetScore().lowerMargin) .ThenBy(a => a.CurrentLine.CleanContent); foreach (var result in orderedResults) { var flattened = result.GetFlattenedCompactVote(); foreach (var vote in flattened) { var(score, average, lowerMargin) = vote.Voters.GetScore(); sb.AppendLine(vote.ToOutputString($"{score}%")); if (displayMode != DisplayMode.CompactNoVoters) { AddCompactNormalVoteVoters(vote); } } if (!(quest.PartitionMode == PartitionMode.ByLine || quest.PartitionMode == PartitionMode.ByLineTask)) { sb.AppendLine(); } } } else { var voteResults = votesInTask.Select(v => new { vote = v, score = v.Value.GetScore() }); var orderedResults = voteResults .OrderByDescending(a => a.score.lowerMargin) .ThenByDescending(a => a.score.average) .ThenBy(a => a.vote.Key.First().CleanContent); foreach (var result in orderedResults) { VoteStorageEntry resultVote = result.vote; var resultScore = result.score; var(entryVote, entryStorage) = resultVote; AddScoreVoteSupport(resultScore); AddScoreVoteDisplay(resultVote, resultScore); AddVoterCount(entryStorage.GetNonRankUserCount()); AddNonRankVoters(entryStorage); sb.AppendLine(); } } }
/// <summary> /// Construct vote output per task for standard votes. /// </summary> /// <param name="votesInTask">The group of votes falling under a task.</param> /// <param name="token">Cancellation token.</param> private void ConstructNormalOutput(VotesGroupedByTask votesInTask, IEnumerable <CompactVote> compactVotesInTask) { bool multiline = votesInTask.Any(a => a.Key.Lines.Count > 1); if (displayMode == DisplayMode.Compact || displayMode == DisplayMode.CompactNoVoters) { var orderedResults = compactVotesInTask .OrderByDescending(a => a.Voters.GetSupportCount()) .ThenBy(a => a.CurrentLine.CleanContent); foreach (var result in orderedResults) { var flattened = result.GetFlattenedCompactVote(); foreach (var vote in flattened) { sb.AppendLine(vote.ToOutputString(vote.Voters.GetSupportCount().ToString(CultureInfo.InvariantCulture))); if (displayMode != DisplayMode.CompactNoVoters) { AddCompactNormalVoteVoters(vote); } } if (!(quest.PartitionMode == PartitionMode.ByLine || quest.PartitionMode == PartitionMode.ByLineTask)) { sb.AppendLine(); } } } else { var voteResults = votesInTask.Select(v => new { vote = v, supportCount = v.Value.GetSupportCount() }); var orderedResults = voteResults.OrderByDescending(a => a.supportCount).ThenBy(a => a.vote.Key.First().CleanContent); foreach (var result in orderedResults) { VoteStorageEntry resultVote = result.vote; int resultSupport = result.supportCount; var(entryVote, entryStorage) = resultVote; int voterCount = entryStorage.GetNonRankUserCount(); if (voterCount != resultSupport) { AddStandardVoteSupport(resultSupport); } AddStandardVoteDisplay(resultVote, resultSupport); AddVoterCount(voterCount); AddNonRankVoters(entryStorage); sb.AppendLine(); } } }
GetWinningVote(VoteStorage votes) { var options = GetTopTwoRatedOptions(votes); if (options.Count == 1) { return(options[0]); } VoteStorageEntry winner = GetOptionWithHigherPrefCount(options[0].option, options[1].option); return(winner, winner.Key == options[0].option.Key ? options[0].score : options[1].score); }
/// <summary> /// Calculates the inverse Borda score for the provided list of voter rankings of a given vote. /// </summary> /// <param name="votes">Votes with rank information included.</param> /// <returns>Returns a numeric evaluation of the overall score of the vote.</returns> public static double InverseBordaScore(VoteStorageEntry votes) { double voteValue = 0; // Value of each rank is 1/N. foreach (var vote in votes.Value) { if (vote.Value.MarkerType == MarkerType.Rank && vote.Value.MarkerValue > 0 && vote.Value.MarkerValue < 10) { voteValue += (1.0 / vote.Value.MarkerValue); } } return(voteValue); }
/// <summary> /// Calculates the Borda score for the provided list of voter rankings of a given vote. /// </summary> /// <param name="votes">Votes with rank information included.</param> /// <returns>Returns a numeric evaluation of the overall score of the vote.</returns> public static double BordaScore(VoteStorageEntry votes) { double voteValue = 0; // Normalize to 9 points for #1, 8 points for #2, etc. foreach (var vote in votes.Value) { if (vote.Value.MarkerType == MarkerType.Rank && vote.Value.MarkerValue > 0 && vote.Value.MarkerValue < 10) { voteValue += (10 - vote.Value.MarkerValue); } } return(voteValue); }
/// <summary> /// 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. /// </summary> /// <param name="voterRankings">The voter rankings. This allows seeing which option each voter preferred.</param> /// <param name="option1">The first option up for consideration.</param> /// <param name="option2">The second option up for consideration.</param> /// <returns>Returns the winning option.</returns> private VoteStorageEntry GetOptionWithHigherPrefCount( VoteStorageEntry option1, VoteStorageEntry option2) { var voters1 = option1.Value; var voters2 = option2.Value; var allVoters = voters1.Keys.Concat(voters2.Keys).Distinct().ToList(); int count1 = 0; int count2 = 0; foreach (var voter in allVoters) { if (!voters1.TryGetValue(voter, out var support1)) { if (voters2.ContainsKey(voter)) { count2++; } continue; } if (!voters2.TryGetValue(voter, out var support2)) { if (voters1.ContainsKey(voter)) { count1++; } continue; } if (support1.MarkerValue < support2.MarkerValue) { count1++; } else if (support2.MarkerValue > support1.MarkerValue) { count2++; } } // 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. return(count2 > count1 ? option2 : option1); }
/// <summary> /// Rank the vote using Borda math. Ranks closer to 1 have higher value. /// To handle scaling, rank 6 is considered to be worth 0 points, which /// means rank 1 is 5 points, and rank 9 is -3 points. /// </summary> /// <param name="vote">The vote being scored.</param> /// <returns>Returns the Borda score based on the voters for the vote.</returns> private double GetBordaScore(VoteStorageEntry vote) { double voteValue = 0; int count = 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 voter in vote.Value) { if (voter.Key.AuthorType == IdentityType.User && voter.Value.MarkerType == MarkerType.Rank) { voteValue += (6 - voter.Value.MarkerValue); count++; } } return(voteValue / count); }
/// <summary> /// 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. /// </summary> /// <param name="vote">The vote being scored.</param> /// <returns>Returns the Borda score based on the voters for the vote.</returns> private double GetBordaScore(VoteStorageEntry vote) { double voteValue = 0; int count = 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 voter in vote.Value) { if (voter.Key.AuthorType == IdentityType.User && voter.Value.MarkerType == MarkerType.Rank) { voteValue += (1.0 + voter.Value.MarkerValue); count++; } } voteValue = voteValue / count / count; return(voteValue); }