102 lines
3.0 KiB
Python
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]
|