From 60627651bf349f995369d78d1d64397a76bdb7a5 Mon Sep 17 00:00:00 2001 From: Duc Nguyen Date: Sun, 5 Oct 2025 23:23:35 +0700 Subject: [PATCH] Fix bug #543: Unstable behavior in PredictRating() --- .../CollaborativeFilteringTests.cs | 144 +++++++++--------- .../CollaborativeFiltering.cs | 127 ++++++++------- 2 files changed, 135 insertions(+), 136 deletions(-) diff --git a/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs index 038b5596..673b4f30 100644 --- a/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs +++ b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs @@ -1,93 +1,93 @@ using Algorithms.RecommenderSystem; using Moq; -namespace Algorithms.Tests.RecommenderSystem +namespace Algorithms.Tests.RecommenderSystem; + +[TestFixture] +public class CollaborativeFilteringTests { - [TestFixture] - public class CollaborativeFilteringTests + private Mock? mockSimilarityCalculator; + private CollaborativeFiltering? recommender; + private Dictionary> testRatings = null!; + + [SetUp] + public void Setup() { - private Mock? mockSimilarityCalculator; - private CollaborativeFiltering? recommender; - private Dictionary> testRatings = null!; + mockSimilarityCalculator = new Mock(); + recommender = new CollaborativeFiltering(mockSimilarityCalculator.Object); - [SetUp] - public void Setup() + testRatings = new Dictionary> { - mockSimilarityCalculator = new Mock(); - recommender = new CollaborativeFiltering(mockSimilarityCalculator.Object); - - testRatings = new Dictionary> + ["user1"] = new() { - ["user1"] = new() - { - ["item1"] = 5.0, - ["item2"] = 3.0, - ["item3"] = 4.0 - }, - ["user2"] = new() - { - ["item1"] = 4.0, - ["item2"] = 2.0, - ["item3"] = 5.0 - }, - ["user3"] = new() - { - ["item1"] = 3.0, - ["item2"] = 4.0, - ["item4"] = 3.0 - } - }; - } - - [Test] - [TestCase("item1", 4.0, 5.0)] - [TestCase("item2", 2.0, 4.0)] - public void CalculateSimilarity_WithValidInputs_ReturnsExpectedResults( - string commonItem, - double rating1, - double rating2) - { - var user1Ratings = new Dictionary { [commonItem] = rating1 }; - var user2Ratings = new Dictionary { [commonItem] = rating2 }; + ["item1"] = 5.0, + ["item2"] = 3.0, + ["item3"] = 4.0 + }, + ["user2"] = new() + { + ["item1"] = 4.0, + ["item2"] = 2.0, + ["item3"] = 5.0 + }, + ["user3"] = new() + { + ["item1"] = 3.0, + ["item2"] = 4.0, + ["item4"] = 3.0 + } + }; + } - var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); + [Test] + [TestCase("item1", 4.0, 5.0)] + [TestCase("item2", 2.0, 4.0)] + public void CalculateSimilarity_WithValidInputs_ReturnsExpectedResults( + string commonItem, + double rating1, + double rating2) + { + var user1Ratings = new Dictionary { [commonItem] = rating1 }; + var user2Ratings = new Dictionary { [commonItem] = rating2 }; - Assert.That(similarity, Is.InRange(-1.0, 1.0)); - } + var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); - [Test] - public void CalculateSimilarity_WithNoCommonItems_ReturnsZero() - { - var user1Ratings = new Dictionary { ["item1"] = 5.0 }; - var user2Ratings = new Dictionary { ["item2"] = 4.0 }; + Assert.That(similarity, Is.InRange(-1.0, 1.0)); + } - var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); + [Test] + public void CalculateSimilarity_WithNoCommonItems_ReturnsZero() + { + var user1Ratings = new Dictionary { ["item1"] = 5.0 }; + var user2Ratings = new Dictionary { ["item2"] = 4.0 }; - Assert.That(similarity, Is.EqualTo(0)); - } + var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); - [Test] - public void PredictRating_WithNonexistentItem_ReturnsZero() - { - var predictedRating = recommender?.PredictRating("nonexistentItem", "user1", testRatings); + Assert.That(similarity, Is.EqualTo(0)); + } - Assert.That(predictedRating, Is.EqualTo(0)); - } + [Test] + public void PredictRating_WithNonexistentItem_ReturnsZero() + { + var predictedRating = recommender?.PredictRating("nonexistentItem", "user1", testRatings); - [Test] - public void PredictRating_WithOtherUserHavingRatedTargetItem_ShouldCalculateSimilarityAndWeightedSum() - { - var targetItem = "item1"; - var targetUser = "user1"; + Assert.That(predictedRating, Is.EqualTo(0)); + } + + [NonParallelizable] + [Test] + public void PredictRating_WithOtherUserHavingRatedTargetItem_ShouldCalculateSimilarityAndWeightedSum() + { + var targetItem = "item1"; + var targetUser = "user1"; - mockSimilarityCalculator? - .Setup(s => s.CalculateSimilarity(It.IsAny>(), It.IsAny>())) - .Returns(0.8); + mockSimilarityCalculator? + .Setup(s => s.CalculateSimilarity(It.IsAny>(), It.IsAny>())) + .Returns(0.8); - var predictedRating = recommender?.PredictRating(targetItem, targetUser, testRatings); + var predictedRating = recommender?.PredictRating(targetItem, targetUser, testRatings); - Assert.That(predictedRating, Is.Not.EqualTo(0.0d)); - Assert.That(predictedRating, Is.EqualTo(3.5d).Within(0.01)); - } + Assert.That(predictedRating, Is.Not.EqualTo(0.0d)); + Assert.That(predictedRating, Is.EqualTo(3.5d).Within(0.01)); } } diff --git a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs index 4e5d8529..653791c1 100644 --- a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs +++ b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs @@ -1,80 +1,79 @@ -namespace Algorithms.RecommenderSystem +namespace Algorithms.RecommenderSystem; + +public class CollaborativeFiltering(ISimilarityCalculator similarityCalculator) { - public class CollaborativeFiltering(ISimilarityCalculator similarityCalculator) - { - private readonly ISimilarityCalculator similarityCalculator = similarityCalculator; + private readonly ISimilarityCalculator similarityCalculator = similarityCalculator; - /// - /// Method to calculate similarity between two users using Pearson correlation. - /// - /// Rating of User 1. - /// Rating of User 2. - /// double value to reflect the index of similarity between two users. - public double CalculateSimilarity(Dictionary user1Ratings, Dictionary user2Ratings) + /// + /// Method to calculate similarity between two users using Pearson correlation. + /// + /// Rating of User 1. + /// Rating of User 2. + /// double value to reflect the index of similarity between two users. + public double CalculateSimilarity(Dictionary user1Ratings, Dictionary user2Ratings) + { + var commonItems = user1Ratings.Keys.Intersect(user2Ratings.Keys).ToList(); + if (commonItems.Count == 0) { - var commonItems = user1Ratings.Keys.Intersect(user2Ratings.Keys).ToList(); - if (commonItems.Count == 0) - { - return 0; - } - - var user1Scores = commonItems.Select(item => user1Ratings[item]).ToArray(); - var user2Scores = commonItems.Select(item => user2Ratings[item]).ToArray(); + return 0; + } - var avgUser1 = user1Scores.Average(); - var avgUser2 = user2Scores.Average(); + var user1Scores = commonItems.Select(item => user1Ratings[item]).ToArray(); + var user2Scores = commonItems.Select(item => user2Ratings[item]).ToArray(); - double numerator = 0; - double sumSquare1 = 0; - double sumSquare2 = 0; - double epsilon = 1e-10; + var avgUser1 = user1Scores.Average(); + var avgUser2 = user2Scores.Average(); - for (var i = 0; i < commonItems.Count; i++) - { - var diff1 = user1Scores[i] - avgUser1; - var diff2 = user2Scores[i] - avgUser2; + double numerator = 0; + double sumSquare1 = 0; + double sumSquare2 = 0; + double epsilon = 1e-10; - numerator += diff1 * diff2; - sumSquare1 += diff1 * diff1; - sumSquare2 += diff2 * diff2; - } + for (var i = 0; i < commonItems.Count; i++) + { + var diff1 = user1Scores[i] - avgUser1; + var diff2 = user2Scores[i] - avgUser2; - var denominator = Math.Sqrt(sumSquare1 * sumSquare2); - return Math.Abs(denominator) < epsilon ? 0 : numerator / denominator; + numerator += diff1 * diff2; + sumSquare1 += diff1 * diff1; + sumSquare2 += diff2 * diff2; } - /// - /// Predict a rating for a specific item by a target user. - /// - /// The item for which the rating needs to be predicted. - /// The user for whom the rating is being predicted. - /// - /// A dictionary containing user ratings where: - /// - The key is the user's identifier (string). - /// - The value is another dictionary where the key is the item identifier (string), and the value is the rating given by the user (double). - /// - /// The predicted rating for the target item by the target user. - /// If there is insufficient data to predict a rating, the method returns 0. - /// - public double PredictRating(string targetItem, string targetUser, Dictionary> ratings) - { - var targetUserRatings = ratings[targetUser]; - double totalSimilarity = 0; - double weightedSum = 0; - double epsilon = 1e-10; + var denominator = Math.Sqrt(sumSquare1 * sumSquare2); + return Math.Abs(denominator) < epsilon ? 0 : numerator / denominator; + } + + /// + /// Predict a rating for a specific item by a target user. + /// + /// The item for which the rating needs to be predicted. + /// The user for whom the rating is being predicted. + /// + /// A dictionary containing user ratings where: + /// - The key is the user's identifier (string). + /// - The value is another dictionary where the key is the item identifier (string), and the value is the rating given by the user (double). + /// + /// The predicted rating for the target item by the target user. + /// If there is insufficient data to predict a rating, the method returns 0. + /// + public double PredictRating(string targetItem, string targetUser, Dictionary> ratings) + { + var targetUserRatings = ratings[targetUser]; + double totalSimilarity = 0; + double weightedSum = 0; + double epsilon = 1e-10; - foreach (var otherUser in ratings.Keys.Where(u => u != targetUser)) + foreach (var otherUser in ratings.Keys.Where(u => u != targetUser)) + { + var otherUserRatings = ratings[otherUser]; + if (otherUserRatings.ContainsKey(targetItem)) { - var otherUserRatings = ratings[otherUser]; - if (otherUserRatings.ContainsKey(targetItem)) - { - var similarity = similarityCalculator.CalculateSimilarity(targetUserRatings, otherUserRatings); - totalSimilarity += Math.Abs(similarity); - weightedSum += similarity * otherUserRatings[targetItem]; - } + var similarity = similarityCalculator.CalculateSimilarity(targetUserRatings, otherUserRatings); + totalSimilarity += Math.Abs(similarity); + weightedSum += similarity * otherUserRatings[targetItem]; } - - return Math.Abs(totalSimilarity) < epsilon ? 0 : weightedSum / totalSimilarity; } + + return Math.Abs(totalSimilarity) < epsilon ? 0 : weightedSum / totalSimilarity; } }