diff --git a/Algorithms.Tests/Algorithms.Tests.csproj b/Algorithms.Tests/Algorithms.Tests.csproj index 893564d8..d6d63755 100644 --- a/Algorithms.Tests/Algorithms.Tests.csproj +++ b/Algorithms.Tests/Algorithms.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs new file mode 100644 index 00000000..208de6fb --- /dev/null +++ b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs @@ -0,0 +1,95 @@ +using Algorithms.RecommenderSystem; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; + +namespace Algorithms.Tests.RecommenderSystem +{ + [TestFixture] + public class CollaborativeFilteringTests + { + private Mock? mockSimilarityCalculator; + private CollaborativeFiltering? recommender; + private Dictionary> testRatings = null!; + + [SetUp] + public void Setup() + { + mockSimilarityCalculator = new Mock(); + recommender = new CollaborativeFiltering(mockSimilarityCalculator.Object); + + testRatings = new Dictionary> + { + ["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 }; + + var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); + + Assert.That(similarity, Is.InRange(-1.0, 1.0)); + } + + [Test] + public void CalculateSimilarity_WithNoCommonItems_ReturnsZero() + { + var user1Ratings = new Dictionary { ["item1"] = 5.0 }; + var user2Ratings = new Dictionary { ["item2"] = 4.0 }; + + var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); + + Assert.That(similarity, Is.EqualTo(0)); + } + + [Test] + public void PredictRating_WithNonexistentItem_ReturnsZero() + { + var predictedRating = recommender?.PredictRating("nonexistentItem", "user1", testRatings); + + Assert.That(predictedRating, Is.EqualTo(0)); + } + + [Test] + public void PredictRating_WithOtherUserHavingRatedTargetItem_ShouldCalculateSimilarityAndWeightedSum() + { + var targetItem = "item1"; + var targetUser = "user1"; + + mockSimilarityCalculator? + .Setup(s => s.CalculateSimilarity(It.IsAny>(), It.IsAny>())) + .Returns(0.8); + + 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)); + } + } +} diff --git a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs new file mode 100644 index 00000000..8da58b8a --- /dev/null +++ b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Algorithms.RecommenderSystem +{ + public class CollaborativeFiltering + { + private readonly ISimilarityCalculator similarityCalculator; + + public CollaborativeFiltering(ISimilarityCalculator similarityCalculator) + { + this.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) + { + 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(); + + var avgUser1 = user1Scores.Average(); + var avgUser2 = user2Scores.Average(); + + double numerator = 0; + double sumSquare1 = 0; + double sumSquare2 = 0; + double epsilon = 1e-10; + + for (var i = 0; i < commonItems.Count; i++) + { + var diff1 = user1Scores[i] - avgUser1; + var diff2 = user2Scores[i] - avgUser2; + + numerator += diff1 * diff2; + sumSquare1 += diff1 * diff1; + sumSquare2 += diff2 * diff2; + } + + 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)) + { + var otherUserRatings = ratings[otherUser]; + if (otherUserRatings.ContainsKey(targetItem)) + { + var similarity = similarityCalculator.CalculateSimilarity(targetUserRatings, otherUserRatings); + totalSimilarity += Math.Abs(similarity); + weightedSum += similarity * otherUserRatings[targetItem]; + } + } + + return Math.Abs(totalSimilarity) < epsilon ? 0 : weightedSum / totalSimilarity; + } + } +} diff --git a/Algorithms/RecommenderSystem/ISimilarityCalculator.cs b/Algorithms/RecommenderSystem/ISimilarityCalculator.cs new file mode 100644 index 00000000..e055a3c7 --- /dev/null +++ b/Algorithms/RecommenderSystem/ISimilarityCalculator.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.RecommenderSystem +{ + public interface ISimilarityCalculator + { + double CalculateSimilarity(Dictionary user1Ratings, Dictionary user2Ratings); + } +} diff --git a/README.md b/README.md index 93e048d2..f67c4356 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ find more than one implementation for the same objective but using different alg * [Josephus Problem](./Algorithms/Numeric/JosephusProblem.cs) * [Newton's Square Root Calculation](./Algorithms/NewtonSquareRoot.cs) * [SoftMax Function](./Algorithms/Numeric/SoftMax.cs) + * [RecommenderSystem](./Algorithms/RecommenderSystem) + * [CollaborativeFiltering](./Algorithms/RecommenderSystem/CollaborativeFiltering) * [Searches](./Algorithms/Search) * [A-Star](./Algorithms/Search/AStar/) * [Binary Search](./Algorithms/Search/BinarySearcher.cs)