407 lines
16 KiB
Python
407 lines
16 KiB
Python
import unittest
|
|
import pytest
|
|
import numpy as np
|
|
from unittest.mock import patch
|
|
from typing import Dict, List
|
|
from pygrex.utils.aggregation_strategy import (
|
|
ScoreAggregator,
|
|
AggregationStrategy, # type: ignore
|
|
)
|
|
from enum import Enum
|
|
from typing import TypeAlias
|
|
|
|
|
|
UserID: TypeAlias = str
|
|
ItemID: TypeAlias = str
|
|
EvaluationScore: TypeAlias = float
|
|
AggregatedScore: TypeAlias = float
|
|
UserEvaluations: TypeAlias = Dict[UserID, Dict[ItemID, EvaluationScore]]
|
|
UserRankings: TypeAlias = Dict[UserID, List[ItemID]]
|
|
AggregatedScores: TypeAlias = Dict[ItemID, AggregatedScore]
|
|
|
|
|
|
class TestScoreAggregatorUnittest(unittest.TestCase):
|
|
"""Unit tests using unittest framework."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures before each test method."""
|
|
self.aggregator = ScoreAggregator(most_respected_person="user1")
|
|
|
|
self.sample_evaluations: UserEvaluations = {
|
|
"user1": {"item_A": 4.5, "item_B": 3.0, "item_C": 5.0},
|
|
"user2": {"item_A": 3.0, "item_B": 4.0, "item_C": 2.0},
|
|
"user3": {"item_A": 4.0, "item_B": 2.0, "item_C": 3.0},
|
|
}
|
|
|
|
self.sample_rankings: UserRankings = {
|
|
"user1": ["item_C", "item_A", "item_B"],
|
|
"user2": ["item_B", "item_A", "item_C"],
|
|
"user3": ["item_A", "item_C", "item_B"],
|
|
}
|
|
|
|
self.empty_evaluations: UserEvaluations = {}
|
|
|
|
self.single_user_evaluations: UserEvaluations = {
|
|
"user1": {"item_A": 3.5, "item_B": 4.0}
|
|
}
|
|
|
|
def test_init_with_mrp(self):
|
|
"""Test initialization with most respected person."""
|
|
aggregator = ScoreAggregator(most_respected_person="user1")
|
|
self.assertEqual(aggregator.most_respected_person, "user1")
|
|
|
|
def test_init_without_mrp(self):
|
|
"""Test initialization without most respected person."""
|
|
aggregator = ScoreAggregator()
|
|
self.assertIsNone(aggregator.most_respected_person)
|
|
|
|
def test_empty_evaluations(self):
|
|
"""Test aggregation with empty evaluations."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.empty_evaluations, # type: ignore
|
|
AggregationStrategy.AVG_PREDICTIONS, # type: ignore
|
|
)
|
|
self.assertEqual(result, {})
|
|
|
|
def test_avg_predictions(self):
|
|
"""Test average predictions aggregation."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.AVG_PREDICTIONS, # type: ignore
|
|
)
|
|
|
|
# Expected: item_A: (4.5+3.0+4.0)/3 = 3.833..., item_B: (3.0+4.0+2.0)/3 = 3.0, item_C: (5.0+2.0+3.0)/3 = 3.333...
|
|
self.assertAlmostEqual(result["item_A"], 3.833333333333333, places=5)
|
|
self.assertAlmostEqual(result["item_B"], 3.0, places=5)
|
|
self.assertAlmostEqual(result["item_C"], 3.333333333333333, places=5)
|
|
|
|
def test_least_misery(self):
|
|
"""Test least misery aggregation."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.LEAST_MISERY, # type: ignore
|
|
)
|
|
|
|
# Expected: item_A: min(4.5, 3.0, 4.0) = 3.0, item_B: min(3.0, 4.0, 2.0) = 2.0, item_C: min(5.0, 2.0, 3.0) = 2.0
|
|
self.assertEqual(result["item_A"], 3.0)
|
|
self.assertEqual(result["item_B"], 2.0)
|
|
self.assertEqual(result["item_C"], 2.0)
|
|
|
|
def test_most_pleasure(self):
|
|
"""Test most pleasure aggregation."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.MOST_PLEASURE, # type: ignore
|
|
)
|
|
|
|
# Expected: item_A: max(4.5, 3.0, 4.0) = 4.5, item_B: max(3.0, 4.0, 2.0) = 4.0, item_C: max(5.0, 2.0, 3.0) = 5.0
|
|
self.assertEqual(result["item_A"], 4.5)
|
|
self.assertEqual(result["item_B"], 4.0)
|
|
self.assertEqual(result["item_C"], 5.0)
|
|
|
|
def test_most_respected_person(self):
|
|
"""Test most respected person aggregation."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.MOST_RESPECTED_PERSON, # type: ignore
|
|
)
|
|
|
|
# Expected: user1's evaluations: item_A: 4.5, item_B: 3.0, item_C: 5.0
|
|
self.assertEqual(result["item_A"], 4.5)
|
|
self.assertEqual(result["item_B"], 3.0)
|
|
self.assertEqual(result["item_C"], 5.0)
|
|
|
|
def test_mrp_without_mrp_set(self):
|
|
"""Test MRP strategy without setting most respected person."""
|
|
aggregator = ScoreAggregator() # No MRP set
|
|
|
|
with self.assertRaises(ValueError) as context:
|
|
aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.MOST_RESPECTED_PERSON, # type: ignore
|
|
)
|
|
|
|
self.assertIn("Most respected person not specified", str(context.exception))
|
|
|
|
def test_mrp_user_not_in_evaluations(self):
|
|
"""Test MRP strategy when MRP user is not in evaluations."""
|
|
aggregator = ScoreAggregator(most_respected_person="nonexistent_user")
|
|
|
|
with self.assertRaises(ValueError) as context:
|
|
aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.MOST_RESPECTED_PERSON, # type: ignore
|
|
)
|
|
|
|
self.assertIn("not found in evaluations", str(context.exception))
|
|
|
|
def test_additive_utilitarian(self):
|
|
"""Test additive utilitarian aggregation."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.ADDITIVE_UTILITARIAN, # type: ignore
|
|
)
|
|
|
|
# Expected: item_A: 4.5+3.0+4.0 = 11.5, item_B: 3.0+4.0+2.0 = 9.0, item_C: 5.0+2.0+3.0 = 10.0
|
|
self.assertEqual(result["item_A"], 11.5)
|
|
self.assertEqual(result["item_B"], 9.0)
|
|
self.assertEqual(result["item_C"], 10.0)
|
|
|
|
def test_multiplicative(self):
|
|
"""Test multiplicative aggregation."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.MULTIPLICATIVE, # type: ignore
|
|
)
|
|
|
|
# Expected: item_A: 4.5*3.0*4.0 = 54.0, item_B: 3.0*4.0*2.0 = 24.0, item_C: 5.0*2.0*3.0 = 30.0
|
|
self.assertEqual(result["item_A"], 54.0)
|
|
self.assertEqual(result["item_B"], 24.0)
|
|
self.assertEqual(result["item_C"], 30.0)
|
|
|
|
def test_borda_count(self):
|
|
"""Test Borda count aggregation."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.BORDA_COUNT,
|
|
self.sample_rankings, # type: ignore
|
|
)
|
|
|
|
# Expected scores based on rankings:
|
|
# item_A: user1(1) + user2(1) + user3(2) = 4
|
|
# item_B: user1(0) + user2(2) + user3(0) = 2
|
|
# item_C: user1(2) + user2(0) + user3(1) = 3
|
|
self.assertEqual(result["item_A"], 4.0)
|
|
self.assertEqual(result["item_B"], 2.0)
|
|
self.assertEqual(result["item_C"], 3.0)
|
|
|
|
def test_borda_count_without_rankings(self):
|
|
"""Test Borda count without providing rankings."""
|
|
with self.assertRaises(ValueError) as context:
|
|
self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.BORDA_COUNT, # type: ignore
|
|
)
|
|
|
|
self.assertIn("Rankings required for Borda Count", str(context.exception))
|
|
|
|
def test_unknown_strategy(self):
|
|
"""Test with unknown aggregation strategy."""
|
|
with self.assertRaises(ValueError) as context:
|
|
self.aggregator.aggregate_scores(
|
|
self.sample_evaluations, # type: ignore
|
|
"invalid_strategy", # type: ignore
|
|
)
|
|
|
|
self.assertIn("Unknown aggregation strategy", str(context.exception))
|
|
|
|
def test_get_top_recommendation(self):
|
|
"""Test getting top recommendation."""
|
|
top_item = self.aggregator.get_top_recommendation(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.MOST_PLEASURE, # type: ignore
|
|
)
|
|
|
|
# item_C has the highest max value (5.0)
|
|
self.assertEqual(top_item, "item_C")
|
|
|
|
def test_get_top_recommendation_with_rankings(self):
|
|
"""Test getting top recommendation with Borda count."""
|
|
top_item = self.aggregator.get_top_recommendation(
|
|
self.sample_evaluations, # type: ignore
|
|
AggregationStrategy.BORDA_COUNT,
|
|
self.sample_rankings, # type: ignore
|
|
)
|
|
|
|
# item_A has the highest Borda count (4.0)
|
|
self.assertEqual(top_item, "item_A")
|
|
|
|
def test_single_user_evaluation(self):
|
|
"""Test aggregation with single user."""
|
|
result = self.aggregator.aggregate_scores(
|
|
self.single_user_evaluations, # type: ignore
|
|
AggregationStrategy.AVG_PREDICTIONS, # type: ignore
|
|
)
|
|
|
|
# With single user, average should equal the original values
|
|
self.assertEqual(result["item_A"], 3.5)
|
|
self.assertEqual(result["item_B"], 4.0)
|
|
|
|
def test_missing_items_in_evaluations(self):
|
|
"""Test with missing items in some user evaluations."""
|
|
incomplete_evaluations: UserEvaluations = {
|
|
"user1": {"item_A": 4.0, "item_B": 3.0},
|
|
"user2": {"item_A": 3.0, "item_C": 2.0}, # Missing item_B
|
|
"user3": {"item_B": 2.0, "item_C": 3.0}, # Missing item_A
|
|
}
|
|
|
|
result = self.aggregator.aggregate_scores(
|
|
incomplete_evaluations, # type: ignore
|
|
AggregationStrategy.AVG_PREDICTIONS, # type: ignore
|
|
)
|
|
|
|
# Should handle missing items gracefully
|
|
self.assertAlmostEqual(result["item_A"], 3.5, places=5) # (4.0 + 3.0) / 2
|
|
self.assertAlmostEqual(result["item_B"], 2.5, places=5) # (3.0 + 2.0) / 2
|
|
self.assertAlmostEqual(result["item_C"], 2.5, places=5) # (2.0 + 3.0) / 2
|
|
|
|
|
|
class TestScoreAggregatorPytest:
|
|
"""Unit tests using pytest framework."""
|
|
|
|
@pytest.fixture
|
|
def aggregator(self):
|
|
"""Fixture for ScoreAggregator instance."""
|
|
return ScoreAggregator(most_respected_person="user1")
|
|
|
|
@pytest.fixture
|
|
def sample_evaluations(self):
|
|
"""Fixture for sample evaluations."""
|
|
return {
|
|
"user1": {"item_A": 4.5, "item_B": 3.0, "item_C": 5.0},
|
|
"user2": {"item_A": 3.0, "item_B": 4.0, "item_C": 2.0},
|
|
"user3": {"item_A": 4.0, "item_B": 2.0, "item_C": 3.0},
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_rankings(self):
|
|
"""Fixture for sample rankings."""
|
|
return {
|
|
"user1": ["item_C", "item_A", "item_B"],
|
|
"user2": ["item_B", "item_A", "item_C"],
|
|
"user3": ["item_A", "item_C", "item_B"],
|
|
}
|
|
|
|
def test_avg_predictions_pytest(self, aggregator, sample_evaluations):
|
|
"""Test average predictions using pytest."""
|
|
result = aggregator.aggregate_scores(
|
|
sample_evaluations, AggregationStrategy.AVG_PREDICTIONS
|
|
)
|
|
|
|
assert abs(result["item_A"] - 3.833333333333333) < 1e-5
|
|
assert abs(result["item_B"] - 3.0) < 1e-5
|
|
assert abs(result["item_C"] - 3.333333333333333) < 1e-5
|
|
|
|
def test_least_misery_pytest(self, aggregator, sample_evaluations):
|
|
"""Test least misery using pytest."""
|
|
result = aggregator.aggregate_scores(
|
|
sample_evaluations, AggregationStrategy.LEAST_MISERY
|
|
)
|
|
|
|
assert result["item_A"] == 3.0
|
|
assert result["item_B"] == 2.0
|
|
assert result["item_C"] == 2.0
|
|
|
|
def test_borda_count_pytest(self, aggregator, sample_evaluations, sample_rankings):
|
|
"""Test Borda count using pytest."""
|
|
result = aggregator.aggregate_scores(
|
|
sample_evaluations, AggregationStrategy.BORDA_COUNT, sample_rankings
|
|
)
|
|
|
|
assert result["item_A"] == 4.0
|
|
assert result["item_B"] == 2.0
|
|
assert result["item_C"] == 3.0
|
|
|
|
def test_mrp_error_pytest(self, sample_evaluations):
|
|
"""Test MRP error handling using pytest."""
|
|
aggregator = ScoreAggregator() # No MRP set
|
|
|
|
with pytest.raises(ValueError, match="Most respected person not specified"):
|
|
aggregator.aggregate_scores(
|
|
sample_evaluations, AggregationStrategy.MOST_RESPECTED_PERSON
|
|
)
|
|
|
|
def test_borda_count_error_pytest(self, aggregator, sample_evaluations):
|
|
"""Test Borda count error handling using pytest."""
|
|
with pytest.raises(ValueError, match="Rankings required for Borda Count"):
|
|
aggregator.aggregate_scores(
|
|
sample_evaluations, AggregationStrategy.BORDA_COUNT
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"strategy,expected_top",
|
|
[
|
|
(AggregationStrategy.LEAST_MISERY, "item_A"),
|
|
(AggregationStrategy.MOST_PLEASURE, "item_C"),
|
|
(AggregationStrategy.ADDITIVE_UTILITARIAN, "item_A"),
|
|
],
|
|
)
|
|
def test_top_recommendation_parametrized(
|
|
self, aggregator, sample_evaluations, strategy, expected_top
|
|
):
|
|
"""Test top recommendations with parametrized testing."""
|
|
top_item = aggregator.get_top_recommendation(sample_evaluations, strategy)
|
|
assert top_item == expected_top
|
|
|
|
def test_empty_evaluations_pytest(self, aggregator):
|
|
"""Test empty evaluations using pytest."""
|
|
result = aggregator.aggregate_scores({}, AggregationStrategy.AVG_PREDICTIONS)
|
|
assert result == {}
|
|
|
|
def test_multiplicative_with_zero(self, aggregator):
|
|
"""Test multiplicative aggregation with zero values."""
|
|
evaluations_with_zero = {
|
|
"user1": {"item_A": 0.0, "item_B": 3.0},
|
|
"user2": {"item_A": 4.0, "item_B": 2.0},
|
|
}
|
|
|
|
result = aggregator.aggregate_scores(
|
|
evaluations_with_zero, AggregationStrategy.MULTIPLICATIVE
|
|
)
|
|
|
|
assert result["item_A"] == 0.0 # 0.0 * 4.0 = 0.0
|
|
assert result["item_B"] == 6.0 # 3.0 * 2.0 = 6.0
|
|
|
|
|
|
# Integration tests
|
|
class TestScoreAggregatorIntegration(unittest.TestCase):
|
|
"""Integration tests for ScoreAggregator."""
|
|
|
|
def test_all_strategies_consistency(self):
|
|
"""Test that all strategies produce consistent results."""
|
|
evaluations = {
|
|
"user1": {"item_A": 5.0, "item_B": 3.0, "item_C": 4.0},
|
|
"user2": {"item_A": 4.0, "item_B": 5.0, "item_C": 3.0},
|
|
"user3": {"item_A": 3.0, "item_B": 4.0, "item_C": 5.0},
|
|
}
|
|
|
|
rankings = {
|
|
"user1": ["item_A", "item_C", "item_B"],
|
|
"user2": ["item_B", "item_A", "item_C"],
|
|
"user3": ["item_C", "item_B", "item_A"],
|
|
}
|
|
|
|
aggregator = ScoreAggregator(most_respected_person="user1")
|
|
|
|
# Test that all strategies return valid results
|
|
strategies_without_rankings = [
|
|
AggregationStrategy.AVG_PREDICTIONS,
|
|
AggregationStrategy.LEAST_MISERY,
|
|
AggregationStrategy.MOST_PLEASURE,
|
|
AggregationStrategy.MOST_RESPECTED_PERSON,
|
|
AggregationStrategy.AVG_PREDICTIONS,
|
|
AggregationStrategy.ADDITIVE_UTILITARIAN,
|
|
AggregationStrategy.MULTIPLICATIVE,
|
|
]
|
|
|
|
for strategy in strategies_without_rankings:
|
|
result = aggregator.aggregate_scores(evaluations, strategy) # type: ignore
|
|
self.assertIsInstance(result, dict)
|
|
self.assertEqual(len(result), 3) # Should have 3 items
|
|
for score in result.values():
|
|
self.assertIsInstance(score, (int, float))
|
|
|
|
# Test Borda count separately
|
|
borda_result = aggregator.aggregate_scores(
|
|
evaluations, # type: ignore
|
|
AggregationStrategy.BORDA_COUNT,
|
|
rankings, # type: ignore
|
|
)
|
|
self.assertIsInstance(borda_result, dict)
|
|
self.assertEqual(len(borda_result), 3)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run unittest tests
|
|
unittest.main(verbosity=2)
|