Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 6 additions & 21 deletions app/services/comparison.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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),
)
)

Expand All @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions tests/integration/router_comparison_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
17 changes: 7 additions & 10 deletions tests/unit/services/comparison_test.py
Original file line number Diff line number Diff line change
@@ -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