Files
py-grex/test/recommender/test_group_recommender.py
T
2026-05-22 10:02:10 +02:00

459 lines
17 KiB
Python

import itertools
import pytest
import numpy as np
from unittest.mock import MagicMock, patch
from typing import Dict, List, Union
from pygrex.data_reader.data_reader import DataReader
from pygrex.models.recommender_model import RecommenderModel
from pygrex.recommender.group_recommender import GroupRecommender
from pygrex.utils.aggregation_strategy import AggregationStrategy, ScoreAggregator
from pygrex.utils.scale import Scale
class TestGroupRecommender:
"""Test suite for the GroupRecommender class."""
@pytest.fixture
def mock_data_reader(self):
"""Create a mock DataReader."""
mock_data = MagicMock(spec=DataReader)
# Setup mock dataset
mock_data.dataset = MagicMock()
return mock_data
@pytest.fixture
def mock_model(self):
"""Create a mock RecommenderModel."""
return MagicMock(spec=RecommenderModel)
@pytest.fixture
def group_recommender(self, mock_data_reader):
"""Create a GroupRecommender instance with mock data."""
return GroupRecommender(mock_data_reader)
def test_init(self, mock_data_reader):
"""Test the initialization of GroupRecommender."""
recommender = GroupRecommender(mock_data_reader)
assert recommender.data == mock_data_reader
assert recommender._group_predictions is None
assert recommender._members is None
assert recommender._item_pool is None
assert recommender._model is None
assert recommender._aggregation_strategy is None
assert recommender._score_aggregator is None
assert recommender._aggregated_scores is None
def test_setup_recommendation(
self, group_recommender, mock_model, mock_data_reader
):
"""Test setup_recommendation method."""
# Arrange
members = [1, 2, 3]
item_ids = [101, 102, 103, 104, 105]
mock_item_pool = np.array([101, 103, 105])
aggregation_strategy = AggregationStrategy.AVG_PREDICTIONS
mock_data_reader.dataset.__getitem__().unique.return_value = item_ids
# Mock methods
group_recommender.get_non_interacted_items_for_recommendation = MagicMock(
return_value=mock_item_pool
)
group_recommender._generate_group_predictions = MagicMock(
return_value={1: {101: 4.5}, 2: {103: 3.2}, 3: {105: 4.0}}
)
group_recommender._aggregate_group_scores = MagicMock(
return_value={101: 4.5, 103: 3.2, 105: 4.0}
)
# Act
with patch.object(GroupRecommender, "_get_max_valid_item_id", return_value=100000):
group_recommender.setup_recommendation(
mock_model, members, mock_data_reader, aggregation_strategy
)
# Assert
assert group_recommender._members == members
assert group_recommender._model == mock_model
assert group_recommender._aggregation_strategy == aggregation_strategy
assert isinstance(group_recommender._score_aggregator, ScoreAggregator)
assert np.array_equal(group_recommender._item_pool, mock_item_pool)
group_recommender.get_non_interacted_items_for_recommendation.assert_called_once_with(
group_recommender.data, item_ids, members
)
group_recommender._generate_group_predictions.assert_called_once()
group_recommender._aggregate_group_scores.assert_called_once()
def test_setup_recommendation_with_most_respected_person(
self, group_recommender, mock_model, mock_data_reader
):
"""Test setup_recommendation method with most respected person."""
# Arrange
members = [1, 2, 3]
item_ids = [101, 102, 103, 104, 105]
mock_item_pool = np.array([101, 103, 105])
aggregation_strategy = AggregationStrategy.MOST_RESPECTED_PERSON
most_respected_person = 1
mock_data_reader.dataset.__getitem__().unique.return_value = item_ids
# Mock methods
group_recommender.get_non_interacted_items_for_recommendation = MagicMock(
return_value=mock_item_pool
)
group_recommender._generate_group_predictions = MagicMock(
return_value={1: {101: 4.5}, 2: {103: 3.2}, 3: {105: 4.0}}
)
group_recommender._aggregate_group_scores = MagicMock(
return_value={101: 4.5, 103: 3.2, 105: 4.0}
)
# Act
with patch.object(GroupRecommender, "_get_max_valid_item_id", return_value=100000):
group_recommender.setup_recommendation(
mock_model,
members,
mock_data_reader,
aggregation_strategy,
most_respected_person,
)
# Assert
assert (
group_recommender._score_aggregator.most_respected_person
== most_respected_person
)
def test_generate_group_predictions(self, group_recommender, mock_model):
"""Test _generate_group_predictions method."""
# Arrange
members = [1, 2]
item_pool = np.array([101, 103])
group_recommender._members = members
group_recommender._model = mock_model
group_recommender._item_pool = item_pool
# Mock generate_recommendation to return different predictions for each user
group_recommender.generate_recommendation = MagicMock(
side_effect=[
{101: 4.5, 103: 3.8}, # User 1's predictions
{101: 3.2, 103: 4.7}, # User 2's predictions
]
)
# Act
result = group_recommender._generate_group_predictions()
# Assert
expected = {1: {101: 4.5, 103: 3.8}, 2: {101: 3.2, 103: 4.7}}
assert result == expected
assert group_recommender.generate_recommendation.call_count == 2
def test_generate_group_predictions_error(self, group_recommender):
"""Test _generate_group_predictions method raises error when setup is incomplete."""
# Arrange - incomplete setup
group_recommender._members = [1, 2]
group_recommender._model = None # Missing model
group_recommender._item_pool = np.array([101, 103])
# Act & Assert
with pytest.raises(
ValueError,
match="You must call setup_recommendation before generating predictions",
):
group_recommender._generate_group_predictions()
def test_aggregate_group_scores(self, group_recommender):
"""Test _aggregate_group_scores method."""
# Arrange
group_recommender._group_predictions = {
1: {101: 4.5, 102: 3.8},
2: {101: 3.2, 102: 4.7},
}
group_recommender._aggregation_strategy = AggregationStrategy.AVG_PREDICTIONS
mock_score_aggregator = MagicMock(spec=ScoreAggregator)
mock_score_aggregator.aggregate_scores.return_value = {102: 4.25, 101: 3.85}
group_recommender._score_aggregator = mock_score_aggregator
# Act
result = group_recommender._aggregate_group_scores()
# Assert
expected = {102: 4.25, 101: 3.85} # Already sorted by score descending
assert result == expected
mock_score_aggregator.aggregate_scores.assert_called_once_with(
evaluations=group_recommender._group_predictions,
strategy=AggregationStrategy.AVG_PREDICTIONS,
rankings=None,
)
def test_aggregate_group_scores_borda_count(self, group_recommender):
"""Test _aggregate_group_scores method with Borda Count strategy."""
# Arrange
group_recommender._group_predictions = {
1: {101: 4.5, 102: 3.8},
2: {101: 3.2, 102: 4.7},
}
group_recommender._aggregation_strategy = AggregationStrategy.BORDA_COUNT
mock_score_aggregator = MagicMock(spec=ScoreAggregator)
mock_score_aggregator.aggregate_scores.return_value = {102: 2.0, 101: 1.0}
group_recommender._score_aggregator = mock_score_aggregator
# Mock _create_rankings_from_predictions
mock_rankings = {1: [101, 102], 2: [102, 101]}
group_recommender._create_rankings_from_predictions = MagicMock(
return_value=mock_rankings
)
# Act
result = group_recommender._aggregate_group_scores()
# Assert
expected = {102: 2.0, 101: 1.0} # Already sorted by score descending
assert result == expected
group_recommender._create_rankings_from_predictions.assert_called_once()
mock_score_aggregator.aggregate_scores.assert_called_once_with(
evaluations=group_recommender._group_predictions,
strategy=AggregationStrategy.BORDA_COUNT,
rankings=mock_rankings,
)
def test_aggregate_group_scores_error(self, group_recommender):
"""Test _aggregate_group_scores method raises error when setup is incomplete."""
# Arrange - incomplete setup
group_recommender._group_predictions = None
# Act & Assert
with pytest.raises(
ValueError,
match="You must call setup_recommendation before aggregating scores",
):
group_recommender._aggregate_group_scores()
def test_create_rankings_from_predictions(self, group_recommender):
"""Test _create_rankings_from_predictions method."""
# Arrange
group_recommender._group_predictions = {
1: {101: 4.5, 102: 3.8, 103: 2.1},
2: {101: 3.2, 102: 4.7, 103: 3.9},
}
# Act
result = group_recommender._create_rankings_from_predictions()
# Assert
expected = {
1: [101, 102, 103], # Sorted by score descending: 4.5, 3.8, 2.1
2: [102, 103, 101], # Sorted by score descending: 4.7, 3.9, 3.2
}
assert result == expected
def test_create_rankings_from_predictions_error(self, group_recommender):
"""Test _create_rankings_from_predictions method raises error when predictions are not available."""
# Arrange - no predictions
group_recommender._group_predictions = None
# Act & Assert
with pytest.raises(ValueError, match="Group predictions not available"):
group_recommender._create_rankings_from_predictions()
def test_get_non_interacted_items(self, group_recommender, mock_data_reader):
"""Test get_non_interacted_items_for_recommendation method."""
# Arrange
members = [1, 2]
all_items = [101, 102, 103, 104, 105]
# Setup mock data
interacted_items = np.array([102, 104])
mock_data_reader.dataset.loc = MagicMock()
mock_data_reader.dataset.loc.__getitem__.return_value.unique.return_value = (
interacted_items
)
# Act
with patch(
"numpy.setdiff1d", return_value=np.array([101, 103, 105])
) as mock_setdiff:
result = group_recommender.get_non_interacted_items_for_recommendation(
mock_data_reader, all_items, members
)
# Assert
assert np.array_equal(result, np.array([101, 103, 105]))
mock_setdiff.assert_called_once_with(
all_items, interacted_items, assume_unique=True
)
def test_generate_recommendation(
self, group_recommender, mock_data_reader, mock_model
):
"""Test generate_recommendation method."""
# Arrange
member = "1" # Test string conversion
member_id_int = 1
new_member_id = 101 # Mapped internal ID
item_pool = [201, 202]
# Setup mocks
mock_data_reader.get_new_user_id.return_value = new_member_id
mock_data_reader.get_original_item_id.side_effect = (
lambda x: x + 1000
) # Simple mapping function
mock_model.predict.return_value = [3.5, 4.2] # Predictions for the two items
# Mock Scale.linear
with patch(
"pygrex.utils.scale.Scale.linear", return_value=np.array([3.0, 4.0])
) as mock_scale:
# Act
with patch.object(GroupRecommender, "_get_max_valid_item_id", return_value=100000):
result = group_recommender.generate_recommendation(
mock_model, member, item_pool, mock_data_reader
)
# Assert
mock_data_reader.get_new_user_id.assert_called_once_with(member_id_int)
assert mock_model.predict.call_count == 1
mock_scale.assert_called_once()
# Check if the result dict has the expected structure: {original_item_id: scaled_score}
expected = {1202: 4.0, 1201: 3.0} # Sorted by score descending
assert result == expected
def test_get_group_recommendations_all(self, group_recommender):
"""Test get_group_recommendations method for returning all items."""
# Arrange
group_recommender._aggregated_scores = {102: 4.25, 101: 3.85}
# Act
result = group_recommender.get_group_recommendations()
# Assert
expected = [102, 101] # All item IDs from the aggregated scores
assert result == expected
def test_get_group_recommendations_top_k(self, group_recommender):
"""Test get_group_recommendations method for returning top k items."""
# Arrange
group_recommender._aggregated_scores = {102: 4.25, 101: 3.85, 103: 3.2}
# Act
result = group_recommender.get_group_recommendations(top_k=2)
# Assert
expected = [102, 101] # Top 2 items from the aggregated scores
assert result == expected
def test_get_group_recommendations_top_one(self, group_recommender):
"""Test get_group_recommendations method for returning only the top item."""
# Arrange
group_recommender._aggregated_scores = {102: 4.25, 101: 3.85}
# Act
result = group_recommender.get_group_recommendations(top_k=1)
# Assert
expected = 102 # The top item ID
assert result == expected
def test_get_group_recommendations_error(self, group_recommender):
"""Test get_group_recommendations method raises error when setup is incomplete."""
# Arrange - incomplete setup
group_recommender._aggregated_scores = None
# Act & Assert
with pytest.raises(
ValueError,
match="You must call setup_recommendation before getting recommendations",
):
group_recommender.get_group_recommendations()
def test_get_top_recommendation(self, group_recommender):
"""Test get_top_recommendation method."""
# Arrange
top_item = 102
group_recommender.get_group_recommendations = MagicMock(return_value=top_item)
# Act
result = group_recommender.get_top_recommendation()
# Assert
assert result == top_item
group_recommender.get_group_recommendations.assert_called_once_with(top_k=1)
def test_get_recommendation_scores(self, group_recommender):
"""Test get_recommendation_scores method."""
# Arrange
group_recommender._aggregated_scores = {103: 3.5, 101: 3.5, 102: 3.5}
# Act
result = group_recommender.get_recommendation_scores()
# Assert
expected = {103: 3.5, 101: 3.5, 102: 3.5}
assert result == expected
def test_get_recommendation_scores_error(self, group_recommender):
"""Test get_recommendation_scores method raises error when setup is incomplete."""
# Arrange - incomplete setup
group_recommender._aggregated_scores = None
# Act & Assert
with pytest.raises(
ValueError,
match="You must call setup_recommendation before getting recommendation scores",
):
group_recommender.get_recommendation_scores()
def test_get_aggregation_strategy(self, group_recommender):
"""Test get_aggregation_strategy method."""
# Arrange
strategy = AggregationStrategy.LEAST_MISERY
group_recommender._aggregation_strategy = strategy
# Act
result = group_recommender.get_aggregation_strategy()
# Assert
assert result == strategy
def test_get_group_members(self, group_recommender):
"""Test get_group_members method."""
# Arrange
members = [1, 2, 3]
group_recommender._members = members
# Act
result = group_recommender.get_group_members()
# Assert
assert result == members
# Ensure it returns a copy, not the original
assert result is not members
def test_get_individual_predictions(self, group_recommender):
"""Test get_individual_predictions method."""
# Arrange
predictions = {1: {101: 4.5, 102: 3.8}, 2: {101: 3.2, 102: 4.7}}
group_recommender._group_predictions = predictions
# Act
result = group_recommender.get_individual_predictions()
# Assert
assert result == predictions
# Ensure it returns a copy, not the original
assert result is not predictions
def test_get_individual_predictions_none(self, group_recommender):
"""Test get_individual_predictions method when predictions are None."""
# Arrange
group_recommender._group_predictions = None
# Act
result = group_recommender.get_individual_predictions()
# Assert
assert result is None