diff --git a/app/services/comparison.py b/app/services/comparison.py index d9c3882..0187a82 100644 --- a/app/services/comparison.py +++ b/app/services/comparison.py @@ -1,7 +1,5 @@ """Viewer-safe, four-domain comparison calculations (#281).""" -from math import sqrt - from sqlalchemy.orm import Session from app.services.shelves import Shelf @@ -10,16 +8,13 @@ MIN_SHARED_FOR_SCORE = 5 RESULT_LIMIT = 5 METHOD = ( - 'We adjust for different list sizes and give extra weight to favorites ' - 'near the top.' + 'We compare rank positions directly. Smaller differences mean closer agreement.' ) -def _position(rank: int, count: int) -> float: - """Top-weighted 0..1 position: zero is best, one is last.""" - if count <= 1: - return 0.0 - return sqrt((rank - 1) / (count - 1)) +def _rank_gap(your_rank: int, their_rank: int) -> int: + """Return the absolute number of places between two rankings.""" + return abs(your_rank - their_rank) def _item(catalog, **extra) -> dict: @@ -77,14 +72,6 @@ def compare_shelf( # pylint: disable=too-many-locals getattr(row, shelf.join_col): row for row in db.query(tracker).filter(tracker.user_id == viewer.pk).all() } - viewer_placed = [ - row - for row in viewer_trackers.values() - if row.on_rankings and row.rank is not None - ] - target_count = len(target_rows) - viewer_count = len(viewer_placed) - base['recommendations'] = [ _item( item, @@ -102,14 +89,12 @@ def compare_shelf( # pylint: disable=too-many-locals mine = viewer_trackers.get(item.pk) if mine is None or not mine.on_rankings or mine.rank is None: continue - my_position = _position(mine.rank, viewer_count) - their_position = _position(target_tracker.rank, target_count) shared.append( _item( item, your_rank=mine.rank, their_rank=target_tracker.rank, - gap=round(abs(my_position - their_position), 4), + gap=_rank_gap(mine.rank, target_tracker.rank), ) ) @@ -122,7 +107,7 @@ def compare_shelf( # pylint: disable=too-many-locals )[:RESULT_LIMIT] if len(shared) >= MIN_SHARED_FOR_SCORE: mean_gap = sum(item['gap'] for item in shared) / len(shared) - base['alignment_score'] = round(max(0.0, 1 - mean_gap) * 100) + base['alignment_score'] = round(max(0.0, 100 - mean_gap)) base['alignment_status'] = 'ready' if watchlist_visible: diff --git a/tests/integration/router_comparison_test.py b/tests/integration/router_comparison_test.py index db0539b..aeda991 100644 --- a/tests/integration/router_comparison_test.py +++ b/tests/integration/router_comparison_test.py @@ -112,8 +112,9 @@ def test_comparison_scores_visible_rankings_and_marks_watchlist( ] assert movies['recommendations'][0]['on_your_watchlist'] is True assert movies['recommendations'][1]['on_your_watchlist'] is False - assert movies['biggest_gaps'][0]['gap'] >= movies['biggest_gaps'][-1]['gap'] - assert movies['most_aligned'][0]['gap'] <= movies['most_aligned'][-1]['gap'] + assert [item['gap'] for item in movies['biggest_gaps']] == [4, 4, 2, 2, 0] + assert [item['gap'] for item in movies['most_aligned']] == [0, 2, 2, 4, 4] + assert movies['most_aligned'][0]['title'] == 'Movie 3' def test_hidden_watchlist_does_not_block_ranked_comparison(test_client: TestClient): diff --git a/tests/unit/services/comparison_test.py b/tests/unit/services/comparison_test.py index 4b3b609..2ab5f72 100644 --- a/tests/unit/services/comparison_test.py +++ b/tests/unit/services/comparison_test.py @@ -1,16 +1,13 @@ # pylint: disable=missing-function-docstring -"""The ranking normalization behind comparison scores.""" +"""The direct rank-distance calculation behind comparisons.""" -from app.services.comparison import _position +from app.services.comparison import _rank_gap -def test_top_weighting_expands_differences_near_the_favorites(): - # Both spans are ten places long, but the top-of-list span deliberately - # counts for more than the same raw distance near the bottom. - top_gap = _position(11, 100) - _position(1, 100) - bottom_gap = _position(100, 100) - _position(90, 100) - assert top_gap > bottom_gap +def test_equal_ranks_have_no_gap(): + assert _rank_gap(14, 14) == 0 -def test_one_item_list_has_a_stable_best_position(): - assert _position(1, 1) == 0.0 +def test_gap_is_the_absolute_rank_difference(): + assert _rank_gap(88, 10) == 78 + assert _rank_gap(10, 88) == 78