Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a9af77d
added mock use case helpers for recommendation test
raymond367 Nov 25, 2025
aea74bb
added helpers to set up testing for mocking use case helpers
raymond367 Nov 25, 2025
f37e0b4
created minimal mock player repository
raymond367 Nov 25, 2025
fb393d1
helper methods for test setup for player repository
raymond367 Nov 25, 2025
61ecdc1
added mock rooster repository
raymond367 Nov 25, 2025
fe404b2
helper methods for test setup for mock rooster repostiory
raymond367 Nov 25, 2025
49ccb19
helper functoin to create mock data for test
raymond367 Nov 25, 2025
7d79b17
feat(recommendatoin-testing) added pytest fixtures for reusasble pyth…
raymond367 Nov 25, 2025
4089ba3
feat(recommendatoin-testing) added input validation error on recommen…
raymond367 Nov 25, 2025
6c0277c
feat(recommendation-testing) added test for case less than 9 saved ba…
raymond367 Nov 25, 2025
64cd000
feat(recommendation-testing) added test for correct input and checkin…
raymond367 Nov 25, 2025
630e74c
feat(recommendation-testing) edit test for recommended players with c…
raymond367 Nov 25, 2025
121910a
chore: update commit message for previous from ssr to srp
raymond367 Nov 25, 2025
d800c76
feat(recommendation-testing) added unit test to check if validate pla…
raymond367 Nov 25, 2025
15cce2a
feat(recommendation-testing) added unit test to query error when play…
raymond367 Nov 26, 2025
ae05656
feat(recommendation-testing) remove logger from recommendation service
raymond367 Nov 26, 2025
fdb4015
feat(recommendation-testing) clean up recommendation service
raymond367 Nov 26, 2025
4363a82
feat(recommendation-testing) further cleanup of recommendation service
raymond367 Nov 26, 2025
c26f4d1
feat(recommendation-testing) finish player position unit test
raymond367 Nov 26, 2025
3781554
feat (recomendation-testing) removed boiler plate methods and functoi…
raymond367 Nov 26, 2025
1aaae1d
feat (recommendation-testing) edited integrations test with sending a…
raymond367 Nov 26, 2025
178206a
feat (recommendation-testing) remove the start time in recommendation…
raymond367 Nov 26, 2025
221bc5b
feat (recommendation-testing) removed duplicate of player id in integ…
raymond367 Nov 26, 2025
9173dba
feat(recommendation-testing) very minor clean up on recommendation us…
raymond367 Nov 26, 2025
6873141
feat (recommendation-testing) added 2 additional unit test which chec…
raymond367 Nov 26, 2025
ad845c5
feat (recommendation-testing) re-clarify the algorithm and its specif…
raymond367 Nov 26, 2025
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
3 changes: 3 additions & 0 deletions server/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ addopts =
--strict-markers
--tb=short

# allows imports without PYTHONPATH
pythonpath = .

# Asyncio configuration
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
Expand Down
70 changes: 19 additions & 51 deletions server/services/recommendation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
from useCaseHelpers.player_helper import PlayerDomain
from useCaseHelpers.errors import InputValidationError, QueryError

logger = logging.getLogger(__name__)


class RecommendationService:
"""Coordinates domain logic and data access for recommendations."""
Expand All @@ -33,6 +31,7 @@ def __init__(
async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchResult]:
"""
Return 5 recommmended players for there rooster based off of the

1. Get current team weakness vector (weakness_s) → where each stat's value > 0 means
team underperforms the league average.

Expand All @@ -41,17 +40,14 @@ async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchRes

3. Fetch all players from firebase (there mlbid) whose position matches with what we need

4. for each team hypothetiically create a ne weakness vector with this player instead of the replaced player
4. for each team hypothetiically create a new weakness vector with this player instead of the replaced player

5. Store the player id with the difference between the sum of old weakness vector - new weakness vector
5. Store the player id with the difference between the new weakness vector - old weakness vector

6. Return the top 5 mlbid with the higest difference (maybe in a hashmap with mlbid, and the difference)

Placeholder orchestration method for future recommendation flow."""

start_time = time.perf_counter()
6. Return the top 5 mlbid with the higest difference"""

logger.info("Starting recommendations for roster with %d players", len(player_ids))
if len(player_ids) < 9:
raise InputValidationError("A valid roster must contain at least 9 players.")

# 1. Get current team weakness vector (weakness_s) where each stat's value > 0 means team underperforms the league average.
self.roster_domain.validate_player_ids(player_ids)
Expand All @@ -76,7 +72,7 @@ async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchRes

seasons = roster_players_data.get(player_id)
if seasons is None:
raise QueryError(f"Player {player_id} not found or has no season data")
raise QueryError(f"Player {player_id} has no season data, could not give recommendations")

latest_stats = self.roster_domain.get_player_latest_stats(seasons) or {}

Expand All @@ -88,14 +84,6 @@ async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchRes
)[0]
player_seasons_map[player_id] = seasons

if not original_players_adjustment_scores:
raise QueryError("Unable to compute adjustment scores for roster")
logger.debug(
"Computed adjustment scores for %d players (min score %.3f)",
len(original_players_adjustment_scores),
min(original_players_adjustment_scores.values()),
)

min_adjustment_score_player_id = min(
original_players_adjustment_scores, key=original_players_adjustment_scores.get
)
Expand All @@ -119,18 +107,6 @@ async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchRes
for player in all_players
if str(player.get("position") or "").strip().upper() == primary_position_upper
]
logger.info(
"Found %d candidate players matching position %s",
len(position_matched_players),
primary_position_upper,
)

if not position_matched_players:
logger.info(
"No players found for position %s; returning empty recommendations",
primary_position_upper,
)
return []

player_contributions = {} # dictionary of key is mlbam_id and value is the difference between the sum of original weakness vector
# and sum of this vector
Expand All @@ -147,15 +123,13 @@ async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchRes
for player in position_matched_players:
candidate_player_id = player.get("mlbam_id")
if not isinstance(candidate_player_id, int):
logger.debug("Skipping candidate with invalid mlbam_id: %s", candidate_player_id)
continue

candidate_seasons = candidate_seasons_cache.get(candidate_player_id)
if candidate_seasons is None:
candidate_data_map = await self.roster_repository.get_players_seasons_data([candidate_player_id])
candidate_seasons = candidate_data_map.get(candidate_player_id)
if not candidate_seasons:
logger.debug("Skipping candidate %s due to missing season data", candidate_player_id)
continue
candidate_seasons_cache[candidate_player_id] = candidate_seasons

Expand All @@ -169,21 +143,20 @@ async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchRes
# Compute normalized weakness scores
potential_team_weakness_vector = self.roster_domain.compute_team_weakness_scores(team_avg, league_avg, league_std)

# 5. Store the player id with the difference between the sum of old weakness vector - new weakness vector
player_contributions[candidate_player_id] = original_vector_sum - sum(potential_team_weakness_vector.values())

logger.info(
"Calculated contributions for %d candidates in %.2fs",
len(player_contributions),
time.perf_counter() - start_time,
)
# 5. Store the player id with the difference between the sum of new weakness vector - old weakness vector
# NOTE: DUE TO ALGOIRTHM CHANGE, weakness veector with higher values are good and lower is bad since we are using
# z scores now so if u make the team have a higher z score for an attribute u are contributing to making the team better

top_5_id_and_score = sorted(player_contributions.items(), key=lambda x: x[1], reverse=False)[:5]
# before the "weakness vector" or in other words, the score X we give to each team regarding the baseball stats,
# were presented so that it would mean u are X percentage worst than the average mlb team at that stat

# Essentially right now, the higher number for an attribute is better which was contrary to what was before
player_contributions[candidate_player_id] = sum(potential_team_weakness_vector.values()) - original_vector_sum

top_5_id_and_score = sorted(player_contributions.items(), key=lambda x: x[1], reverse=True)[:5]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
results: List[PlayerSearchResult] = []
for mlbam_id, contribution in top_5_id_and_score:
player_data = await self.player_repository.get_player_by_id(mlbam_id)
if not player_data:
continue

player_result = self.player_domain.build_player_search_result(
player_data,
Expand All @@ -193,11 +166,6 @@ async def recommend_players(self, player_ids: List[int]) -> List[PlayerSearchRes
if player_result:
results.append(player_result)

logger.info(
"Returning %d recommendations for roster %s in %.2fs",
len(results),
player_ids,
time.perf_counter() - start_time,
)
return results

return results

87 changes: 87 additions & 0 deletions server/services/tests/mocks/mock_repositories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from repositories.player_repository import PlayerRepository
from typing import Dict, List, Optional
from unittest.mock import AsyncMock, Mock
from repositories.roster_avg_repository import RosterRepository

class MockRosterRepository(RosterRepository):
"""Mock implementation of RosterRepository for testing"""

def __init__(self):
self._players_seasons_data: Dict[int, Dict] = {}
self._league_avg: Dict[str, float] = {}
self._league_std: Dict[str, float] = {}

async def get_players_seasons_data(self, player_ids: List[int]) -> Dict[int, Dict]:
"""Get seasons data for multiple players."""
result = {}
for pid in player_ids:
if pid in self._players_seasons_data:
result[pid] = self._players_seasons_data[pid]
return result

async def get_league_unweighted_average(self) -> Dict[str, float]:
"""Fetch league-wide unweighted average stats."""
return self._league_avg.copy()

async def get_league_unweighted_std(self) -> Dict[str, float]:
"""Fetch league-wide unweighted standard deviations."""
return self._league_std.copy()

async def get_league_weighted_std(self) -> Dict[str, float]:
pass

def fetch_team_roster(self, team_id: int, season: int) -> Dict[str, any]:
pass
Comment thread
raymond367 marked this conversation as resolved.

# Helper methods for test setup and edge case creation for use case interactor unit testing
def set_players_seasons_data(self, player_id: int, seasons: Dict):
"""Set season data for a player."""
self._players_seasons_data[player_id] = seasons

def set_league_avg(self, league_avg: Dict[str, float]):
"""Set league average stats."""
self._league_avg = league_avg.copy()

def set_league_std(self, league_std: Dict[str, float]):
"""Set league standard deviations."""
self._league_std = league_std.copy()

class MockPlayerRepository(PlayerRepository):
"""Mock implementation of PlayerRepository for testing."""

def __init__(self):
self._players: List[Dict] = []
self._player_by_id: Dict[int, Dict] = {}

async def get_all_players(self) -> List[Dict]:
"""Get all players from database."""
return self._players.copy()

async def get_player_by_id(self, player_id: int) -> Optional[Dict]:
"""Get a specific player by ID."""
return self._player_by_id.get(player_id)

def upload_team(self, team, team_name, final_players):
pass

def bulk_upsert_players(self, players: List[Dict[str, any]]) -> None:
pass

def set_league_averages(self, league_doc: Dict[str, any]) -> None:
pass
Comment thread
raymond367 marked this conversation as resolved.
def build_player_image_url(self, player_id: int) -> str:
"""Return a player headshot URL."""
return f"https://example.com/players/{player_id}.jpg"

# heelper methods for test setup to add to in memory database
def add_player(self, player: Dict):
"""Add a player to the mock repository."""
mlbam_id = player.get("mlbam_id")
if mlbam_id:
self._player_by_id[mlbam_id] = player
self._players.append(player)

def set_player(self, player_id: int, player: Dict):
"""Set a specific player by ID."""
self._player_by_id[player_id] = player

Comment thread
raymond367 marked this conversation as resolved.
Loading