Files
vyndr/src/services/python/utils/similarity.py
T

102 lines
3.0 KiB
Python

"""
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]