Skip to content

Commit 85497af

Browse files
feat: implement final score calculation for recommendations, enhancing scoring logic with quality adjustments
1 parent ab46217 commit 85497af

File tree

3 files changed

+76
-59
lines changed

3 files changed

+76
-59
lines changed

app/services/recommendation/scoring.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from collections.abc import Callable
44
from typing import Any
55

6+
from app.core.constants import DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_MOVIE, DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_TV
7+
68

79
class RecommendationScoring:
810
"""
@@ -114,3 +116,55 @@ def apply_quality_adjustments(score: float, wr: float, vote_count: int, is_ranke
114116
q_mult *= 1.10
115117

116118
return score * q_mult
119+
120+
@staticmethod
121+
def calculate_final_score(
122+
item: dict[str, Any],
123+
profile: Any,
124+
scorer: Any,
125+
mtype: str,
126+
is_ranked: bool = False,
127+
is_fresh: bool = False,
128+
) -> float: # noqa: E501
129+
"""
130+
Calculate final recommendation score combining profile similarity and quality.
131+
132+
Args:
133+
item: Item dictionary with vote_average, vote_count, etc.
134+
profile: User taste profile
135+
scorer: ProfileScorer instance
136+
mtype: Media type (movie/tv) to determine minimum rating
137+
is_ranked: Whether item is from ranked source
138+
is_fresh: Whether item should get freshness boost
139+
minimum_rating_tv: Minimum rating constant for TV
140+
minimum_rating_movie: Minimum rating constant for movies
141+
142+
Returns:
143+
Final combined score (0-1 range)
144+
"""
145+
# Score with profile
146+
profile_score = scorer.score_item(item, profile)
147+
148+
# Calculate weighted rating
149+
C = (
150+
DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_TV
151+
if mtype in ("tv", "series")
152+
else DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_MOVIE
153+
)
154+
wr = RecommendationScoring.weighted_rating(
155+
item.get("vote_average"),
156+
item.get("vote_count"),
157+
C=C,
158+
)
159+
quality_score = RecommendationScoring.normalize(wr)
160+
161+
# Apply quality adjustments
162+
vote_count = item.get("vote_count", 0)
163+
adjusted_profile_score = RecommendationScoring.apply_quality_adjustments(
164+
profile_score, wr, vote_count, is_ranked=is_ranked, is_fresh=is_fresh
165+
)
166+
167+
# Combined score: profile similarity (with quality adjustments) + quality
168+
final_score = (adjusted_profile_score * 0.6) + (quality_score * 0.4)
169+
170+
return final_score

app/services/recommendation/theme_based.py

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33

44
from loguru import logger
55

6-
from app.core.constants import DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_MOVIE, DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_TV
76
from app.models.taste_profile import TasteProfile
87
from app.services.profile.scorer import ProfileScorer
98
from app.services.recommendation.filtering import RecommendationFiltering
109
from app.services.recommendation.metadata import RecommendationMetadata
1110
from app.services.recommendation.scoring import RecommendationScoring
12-
from app.services.recommendation.utils import filter_by_genres, filter_watched_by_imdb
11+
from app.services.recommendation.utils import content_type_to_mtype, filter_by_genres, filter_watched_by_imdb
1312

1413

1514
class ThemeBasedService:
@@ -98,31 +97,19 @@ async def get_recommendations_for_theme(
9897
# Score with profile if available
9998
if profile:
10099
scored = []
100+
mtype = content_type_to_mtype(content_type)
101101
for item in filtered:
102102
try:
103-
profile_score = self.scorer.score_item(item, profile)
104-
# Add quality score
105-
minimum_rating = (
106-
DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_TV
107-
if content_type in ("tv", "series")
108-
else DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_MOVIE
109-
)
110-
wr = RecommendationScoring.weighted_rating(
111-
item.get("vote_average"),
112-
item.get("vote_count"),
113-
C=minimum_rating,
114-
)
115-
quality_score = RecommendationScoring.normalize(wr)
116103

117-
# Apply quality adjustments (like old system)
118-
vote_count = item.get("vote_count", 0)
119-
adjusted_profile_score = RecommendationScoring.apply_quality_adjustments(
120-
profile_score, wr, vote_count, is_ranked=False, is_fresh=False
104+
final_score = RecommendationScoring.calculate_final_score(
105+
item=item,
106+
profile=profile,
107+
scorer=self.scorer,
108+
mtype=mtype,
109+
is_ranked=False,
110+
is_fresh=False,
121111
)
122112

123-
# Combined: profile similarity (with quality adjustments) + quality
124-
final_score = (adjusted_profile_score * 0.6) + (quality_score * 0.4)
125-
126113
# Apply genre multiplier (if whitelist available)
127114
genre_mult = RecommendationFiltering.get_genre_multiplier(item.get("genre_ids"), whitelist)
128115
final_score *= genre_mult

app/services/recommendation/top_picks.py

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
"""
2-
Top Picks Service - Generates personalized top picks with diversity caps.
3-
4-
Fetches from multiple sources, scores with profile, applies diversity constraints.
5-
"""
6-
71
import asyncio
82
from collections import defaultdict
93
from typing import Any
104

115
from loguru import logger
126

7+
from app.core.settings import UserSettings
138
from app.models.taste_profile import TasteProfile
149
from app.services.profile.constants import (
1510
TOP_PICKS_CREATOR_CAP,
@@ -23,24 +18,18 @@
2318
from app.services.recommendation.metadata import RecommendationMetadata
2419
from app.services.recommendation.scoring import RecommendationScoring
2520
from app.services.recommendation.utils import content_type_to_mtype, filter_watched_by_imdb, resolve_tmdb_id
21+
from app.services.tmdb.service import TMDBService
2622

2723

2824
class TopPicksService:
2925
"""
3026
Generates top picks by combining multiple sources and applying diversity caps.
3127
"""
3228

33-
def __init__(self, tmdb_service: Any, user_settings: Any = None):
34-
"""
35-
Initialize Top Picks service.
36-
37-
Args:
38-
tmdb_service: TMDB service for API calls
39-
user_settings: User settings for exclusions
40-
"""
41-
self.tmdb_service = tmdb_service
42-
self.user_settings = user_settings
43-
self.scorer = ProfileScorer()
29+
def __init__(self, tmdb_service: TMDBService, user_settings: UserSettings | None = None):
30+
self.tmdb_service: TMDBService = tmdb_service
31+
self.user_settings: UserSettings | None = user_settings
32+
self.scorer: ProfileScorer = ProfileScorer()
4433

4534
async def get_top_picks(
4635
self,
@@ -104,24 +93,16 @@ async def get_top_picks(
10493
scored_candidates = []
10594
for item in filtered_candidates:
10695
try:
107-
profile_score = self.scorer.score_item(item, profile)
108-
# Add quality score for final ranking
109-
wr = RecommendationScoring.weighted_rating(
110-
item.get("vote_average"), item.get("vote_count"), C=7.2 if mtype == "tv" else 6.8
111-
)
112-
quality_score = RecommendationScoring.normalize(wr)
113-
114-
# Apply quality adjustments
115-
vote_count = item.get("vote_count", 0)
11696
is_ranked = item.get("_ranked_candidate", False)
11797
is_fresh = item.get("_fresh_boost", False)
118-
adjusted_profile_score = RecommendationScoring.apply_quality_adjustments(
119-
profile_score, wr, vote_count, is_ranked=is_ranked, is_fresh=is_fresh
98+
final_score = RecommendationScoring.calculate_final_score(
99+
item=item,
100+
profile=profile,
101+
scorer=self.scorer,
102+
mtype=mtype,
103+
is_ranked=is_ranked,
104+
is_fresh=is_fresh,
120105
)
121-
122-
# Combined score: profile similarity (with quality adjustments) + quality
123-
final_score = (adjusted_profile_score * 0.6) + (quality_score * 0.4)
124-
125106
scored_candidates.append((final_score, item))
126107
except Exception as e:
127108
logger.debug(f"Failed to score item {item.get('id')}: {e}")
@@ -373,11 +354,6 @@ def _apply_diversity_caps(
373354
if genre_counts[top_genre] >= max_per_genre:
374355
continue
375356

376-
# Check creator cap (2 max per creator)
377-
# Extract directors and cast from item (if available in metadata)
378-
# For now, we'll skip this check as it requires full metadata
379-
# Can be added after enrichment
380-
381357
# Check era cap (40% max per era)
382358
year = self._extract_year(item)
383359
if year:

0 commit comments

Comments
 (0)