Skip to content

Commit 6062765

Browse files
committed
Fix bug #543: Unstable behavior in PredictRating()
1 parent e2c20ed commit 6062765

File tree

2 files changed

+135
-136
lines changed

2 files changed

+135
-136
lines changed
Lines changed: 72 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,93 @@
11
using Algorithms.RecommenderSystem;
22
using Moq;
33

4-
namespace Algorithms.Tests.RecommenderSystem
4+
namespace Algorithms.Tests.RecommenderSystem;
5+
6+
[TestFixture]
7+
public class CollaborativeFilteringTests
58
{
6-
[TestFixture]
7-
public class CollaborativeFilteringTests
9+
private Mock<ISimilarityCalculator>? mockSimilarityCalculator;
10+
private CollaborativeFiltering? recommender;
11+
private Dictionary<string, Dictionary<string, double>> testRatings = null!;
12+
13+
[SetUp]
14+
public void Setup()
815
{
9-
private Mock<ISimilarityCalculator>? mockSimilarityCalculator;
10-
private CollaborativeFiltering? recommender;
11-
private Dictionary<string, Dictionary<string, double>> testRatings = null!;
16+
mockSimilarityCalculator = new Mock<ISimilarityCalculator>();
17+
recommender = new CollaborativeFiltering(mockSimilarityCalculator.Object);
1218

13-
[SetUp]
14-
public void Setup()
19+
testRatings = new Dictionary<string, Dictionary<string, double>>
1520
{
16-
mockSimilarityCalculator = new Mock<ISimilarityCalculator>();
17-
recommender = new CollaborativeFiltering(mockSimilarityCalculator.Object);
18-
19-
testRatings = new Dictionary<string, Dictionary<string, double>>
21+
["user1"] = new()
2022
{
21-
["user1"] = new()
22-
{
23-
["item1"] = 5.0,
24-
["item2"] = 3.0,
25-
["item3"] = 4.0
26-
},
27-
["user2"] = new()
28-
{
29-
["item1"] = 4.0,
30-
["item2"] = 2.0,
31-
["item3"] = 5.0
32-
},
33-
["user3"] = new()
34-
{
35-
["item1"] = 3.0,
36-
["item2"] = 4.0,
37-
["item4"] = 3.0
38-
}
39-
};
40-
}
41-
42-
[Test]
43-
[TestCase("item1", 4.0, 5.0)]
44-
[TestCase("item2", 2.0, 4.0)]
45-
public void CalculateSimilarity_WithValidInputs_ReturnsExpectedResults(
46-
string commonItem,
47-
double rating1,
48-
double rating2)
49-
{
50-
var user1Ratings = new Dictionary<string, double> { [commonItem] = rating1 };
51-
var user2Ratings = new Dictionary<string, double> { [commonItem] = rating2 };
23+
["item1"] = 5.0,
24+
["item2"] = 3.0,
25+
["item3"] = 4.0
26+
},
27+
["user2"] = new()
28+
{
29+
["item1"] = 4.0,
30+
["item2"] = 2.0,
31+
["item3"] = 5.0
32+
},
33+
["user3"] = new()
34+
{
35+
["item1"] = 3.0,
36+
["item2"] = 4.0,
37+
["item4"] = 3.0
38+
}
39+
};
40+
}
5241

53-
var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings);
42+
[Test]
43+
[TestCase("item1", 4.0, 5.0)]
44+
[TestCase("item2", 2.0, 4.0)]
45+
public void CalculateSimilarity_WithValidInputs_ReturnsExpectedResults(
46+
string commonItem,
47+
double rating1,
48+
double rating2)
49+
{
50+
var user1Ratings = new Dictionary<string, double> { [commonItem] = rating1 };
51+
var user2Ratings = new Dictionary<string, double> { [commonItem] = rating2 };
5452

55-
Assert.That(similarity, Is.InRange(-1.0, 1.0));
56-
}
53+
var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings);
5754

58-
[Test]
59-
public void CalculateSimilarity_WithNoCommonItems_ReturnsZero()
60-
{
61-
var user1Ratings = new Dictionary<string, double> { ["item1"] = 5.0 };
62-
var user2Ratings = new Dictionary<string, double> { ["item2"] = 4.0 };
55+
Assert.That(similarity, Is.InRange(-1.0, 1.0));
56+
}
6357

64-
var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings);
58+
[Test]
59+
public void CalculateSimilarity_WithNoCommonItems_ReturnsZero()
60+
{
61+
var user1Ratings = new Dictionary<string, double> { ["item1"] = 5.0 };
62+
var user2Ratings = new Dictionary<string, double> { ["item2"] = 4.0 };
6563

66-
Assert.That(similarity, Is.EqualTo(0));
67-
}
64+
var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings);
6865

69-
[Test]
70-
public void PredictRating_WithNonexistentItem_ReturnsZero()
71-
{
72-
var predictedRating = recommender?.PredictRating("nonexistentItem", "user1", testRatings);
66+
Assert.That(similarity, Is.EqualTo(0));
67+
}
7368

74-
Assert.That(predictedRating, Is.EqualTo(0));
75-
}
69+
[Test]
70+
public void PredictRating_WithNonexistentItem_ReturnsZero()
71+
{
72+
var predictedRating = recommender?.PredictRating("nonexistentItem", "user1", testRatings);
7673

77-
[Test]
78-
public void PredictRating_WithOtherUserHavingRatedTargetItem_ShouldCalculateSimilarityAndWeightedSum()
79-
{
80-
var targetItem = "item1";
81-
var targetUser = "user1";
74+
Assert.That(predictedRating, Is.EqualTo(0));
75+
}
76+
77+
[NonParallelizable]
78+
[Test]
79+
public void PredictRating_WithOtherUserHavingRatedTargetItem_ShouldCalculateSimilarityAndWeightedSum()
80+
{
81+
var targetItem = "item1";
82+
var targetUser = "user1";
8283

83-
mockSimilarityCalculator?
84-
.Setup(s => s.CalculateSimilarity(It.IsAny<Dictionary<string, double>>(), It.IsAny<Dictionary<string, double>>()))
85-
.Returns(0.8);
84+
mockSimilarityCalculator?
85+
.Setup(s => s.CalculateSimilarity(It.IsAny<Dictionary<string, double>>(), It.IsAny<Dictionary<string, double>>()))
86+
.Returns(0.8);
8687

87-
var predictedRating = recommender?.PredictRating(targetItem, targetUser, testRatings);
88+
var predictedRating = recommender?.PredictRating(targetItem, targetUser, testRatings);
8889

89-
Assert.That(predictedRating, Is.Not.EqualTo(0.0d));
90-
Assert.That(predictedRating, Is.EqualTo(3.5d).Within(0.01));
91-
}
90+
Assert.That(predictedRating, Is.Not.EqualTo(0.0d));
91+
Assert.That(predictedRating, Is.EqualTo(3.5d).Within(0.01));
9292
}
9393
}
Lines changed: 63 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,79 @@
1-
namespace Algorithms.RecommenderSystem
1+
namespace Algorithms.RecommenderSystem;
2+
3+
public class CollaborativeFiltering(ISimilarityCalculator similarityCalculator)
24
{
3-
public class CollaborativeFiltering(ISimilarityCalculator similarityCalculator)
4-
{
5-
private readonly ISimilarityCalculator similarityCalculator = similarityCalculator;
5+
private readonly ISimilarityCalculator similarityCalculator = similarityCalculator;
66

7-
/// <summary>
8-
/// Method to calculate similarity between two users using Pearson correlation.
9-
/// </summary>
10-
/// <param name="user1Ratings">Rating of User 1.</param>
11-
/// <param name="user2Ratings">Rating of User 2.</param>
12-
/// <returns>double value to reflect the index of similarity between two users.</returns>
13-
public double CalculateSimilarity(Dictionary<string, double> user1Ratings, Dictionary<string, double> user2Ratings)
7+
/// <summary>
8+
/// Method to calculate similarity between two users using Pearson correlation.
9+
/// </summary>
10+
/// <param name="user1Ratings">Rating of User 1.</param>
11+
/// <param name="user2Ratings">Rating of User 2.</param>
12+
/// <returns>double value to reflect the index of similarity between two users.</returns>
13+
public double CalculateSimilarity(Dictionary<string, double> user1Ratings, Dictionary<string, double> user2Ratings)
14+
{
15+
var commonItems = user1Ratings.Keys.Intersect(user2Ratings.Keys).ToList();
16+
if (commonItems.Count == 0)
1417
{
15-
var commonItems = user1Ratings.Keys.Intersect(user2Ratings.Keys).ToList();
16-
if (commonItems.Count == 0)
17-
{
18-
return 0;
19-
}
20-
21-
var user1Scores = commonItems.Select(item => user1Ratings[item]).ToArray();
22-
var user2Scores = commonItems.Select(item => user2Ratings[item]).ToArray();
18+
return 0;
19+
}
2320

24-
var avgUser1 = user1Scores.Average();
25-
var avgUser2 = user2Scores.Average();
21+
var user1Scores = commonItems.Select(item => user1Ratings[item]).ToArray();
22+
var user2Scores = commonItems.Select(item => user2Ratings[item]).ToArray();
2623

27-
double numerator = 0;
28-
double sumSquare1 = 0;
29-
double sumSquare2 = 0;
30-
double epsilon = 1e-10;
24+
var avgUser1 = user1Scores.Average();
25+
var avgUser2 = user2Scores.Average();
3126

32-
for (var i = 0; i < commonItems.Count; i++)
33-
{
34-
var diff1 = user1Scores[i] - avgUser1;
35-
var diff2 = user2Scores[i] - avgUser2;
27+
double numerator = 0;
28+
double sumSquare1 = 0;
29+
double sumSquare2 = 0;
30+
double epsilon = 1e-10;
3631

37-
numerator += diff1 * diff2;
38-
sumSquare1 += diff1 * diff1;
39-
sumSquare2 += diff2 * diff2;
40-
}
32+
for (var i = 0; i < commonItems.Count; i++)
33+
{
34+
var diff1 = user1Scores[i] - avgUser1;
35+
var diff2 = user2Scores[i] - avgUser2;
4136

42-
var denominator = Math.Sqrt(sumSquare1 * sumSquare2);
43-
return Math.Abs(denominator) < epsilon ? 0 : numerator / denominator;
37+
numerator += diff1 * diff2;
38+
sumSquare1 += diff1 * diff1;
39+
sumSquare2 += diff2 * diff2;
4440
}
4541

46-
/// <summary>
47-
/// Predict a rating for a specific item by a target user.
48-
/// </summary>
49-
/// <param name="targetItem">The item for which the rating needs to be predicted.</param>
50-
/// <param name="targetUser">The user for whom the rating is being predicted.</param>
51-
/// <param name="ratings">
52-
/// A dictionary containing user ratings where:
53-
/// - The key is the user's identifier (string).
54-
/// - The value is another dictionary where the key is the item identifier (string), and the value is the rating given by the user (double).
55-
/// </param>
56-
/// <returns>The predicted rating for the target item by the target user.
57-
/// If there is insufficient data to predict a rating, the method returns 0.
58-
/// </returns>
59-
public double PredictRating(string targetItem, string targetUser, Dictionary<string, Dictionary<string, double>> ratings)
60-
{
61-
var targetUserRatings = ratings[targetUser];
62-
double totalSimilarity = 0;
63-
double weightedSum = 0;
64-
double epsilon = 1e-10;
42+
var denominator = Math.Sqrt(sumSquare1 * sumSquare2);
43+
return Math.Abs(denominator) < epsilon ? 0 : numerator / denominator;
44+
}
45+
46+
/// <summary>
47+
/// Predict a rating for a specific item by a target user.
48+
/// </summary>
49+
/// <param name="targetItem">The item for which the rating needs to be predicted.</param>
50+
/// <param name="targetUser">The user for whom the rating is being predicted.</param>
51+
/// <param name="ratings">
52+
/// A dictionary containing user ratings where:
53+
/// - The key is the user's identifier (string).
54+
/// - The value is another dictionary where the key is the item identifier (string), and the value is the rating given by the user (double).
55+
/// </param>
56+
/// <returns>The predicted rating for the target item by the target user.
57+
/// If there is insufficient data to predict a rating, the method returns 0.
58+
/// </returns>
59+
public double PredictRating(string targetItem, string targetUser, Dictionary<string, Dictionary<string, double>> ratings)
60+
{
61+
var targetUserRatings = ratings[targetUser];
62+
double totalSimilarity = 0;
63+
double weightedSum = 0;
64+
double epsilon = 1e-10;
6565

66-
foreach (var otherUser in ratings.Keys.Where(u => u != targetUser))
66+
foreach (var otherUser in ratings.Keys.Where(u => u != targetUser))
67+
{
68+
var otherUserRatings = ratings[otherUser];
69+
if (otherUserRatings.ContainsKey(targetItem))
6770
{
68-
var otherUserRatings = ratings[otherUser];
69-
if (otherUserRatings.ContainsKey(targetItem))
70-
{
71-
var similarity = similarityCalculator.CalculateSimilarity(targetUserRatings, otherUserRatings);
72-
totalSimilarity += Math.Abs(similarity);
73-
weightedSum += similarity * otherUserRatings[targetItem];
74-
}
71+
var similarity = similarityCalculator.CalculateSimilarity(targetUserRatings, otherUserRatings);
72+
totalSimilarity += Math.Abs(similarity);
73+
weightedSum += similarity * otherUserRatings[targetItem];
7574
}
76-
77-
return Math.Abs(totalSimilarity) < epsilon ? 0 : weightedSum / totalSimilarity;
7875
}
76+
77+
return Math.Abs(totalSimilarity) < epsilon ? 0 : weightedSum / totalSimilarity;
7978
}
8079
}

0 commit comments

Comments
 (0)