""" VYNDR Similarity Engine Find historically similar games for confidence adjustment. Shared by NBA and MLB. Minimum similarity threshold 0.7. """ import logging logger = logging.getLogger('vyndr') MIN_SIMILARITY = 0.7 # Similarity factors and their relative importance SIMILARITY_FACTORS = { # NBA factors 'opponent_defensive_rating': 0.15, 'pace': 0.12, 'rest_days': 0.08, 'home_away': 0.06, 'functional_role_match': 0.15, 'teammate_context': 0.10, # MLB factors 'pitcher_handedness': 0.12, 'park_factor': 0.10, 'opponent_quality': 0.12, 'weather_similarity': 0.05, 'day_night': 0.04, 'batting_order_position': 0.06, } def calculate_similarity_score(game_a, game_b, factors=None): """ Calculate similarity score between two games. Uses weighted factor comparison with normalization. Args: game_a: Dict of game context factors. game_b: Dict of game context factors. factors: Optional dict of factor weights. Defaults to SIMILARITY_FACTORS. Returns: Float similarity score between 0.0 and 1.0. """ if factors is None: factors = SIMILARITY_FACTORS total_score = 0.0 total_weight = 0.0 for factor, weight in factors.items(): val_a = game_a.get(factor) val_b = game_b.get(factor) if val_a is None or val_b is None: continue # Boolean factors if isinstance(val_a, bool) or isinstance(val_b, bool): similarity = 1.0 if val_a == val_b else 0.0 # String factors (categorical) elif isinstance(val_a, str) or isinstance(val_b, str): similarity = 1.0 if val_a == val_b else 0.0 # Numeric factors else: max_val = max(abs(val_a), abs(val_b), 1) diff = abs(val_a - val_b) / max_val similarity = max(0.0, 1.0 - diff) total_score += similarity * weight total_weight += weight if total_weight == 0: return 0.0 return min(1.0, max(0.0, total_score / total_weight)) def find_similar_games(target_game, historical_games, max_results=5, min_similarity=None): """ Find historically similar games above the minimum similarity threshold. Args: target_game: Dict of current game context factors. historical_games: List of historical game dicts. max_results: Maximum number of similar games to return. min_similarity: Minimum similarity score threshold (default 0.7). Returns: List of (similarity_score, game) tuples, sorted by similarity descending. Only games at or above min_similarity are included. """ if min_similarity is None: min_similarity = MIN_SIMILARITY scored = [] for game in historical_games: score = calculate_similarity_score(target_game, game) if score >= min_similarity: scored.append((score, game)) scored.sort(key=lambda x: x[0], reverse=True) return scored[:max_results]