Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
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]
|
||||
Reference in New Issue
Block a user