diff --git a/ai-engine/core/engines/player_predictor.py b/ai-engine/core/engines/player_predictor.py index e2565a8..fc45ba4 100755 --- a/ai-engine/core/engines/player_predictor.py +++ b/ai-engine/core/engines/player_predictor.py @@ -18,15 +18,20 @@ from features.sidelined_analyzer import get_sidelined_analyzer @dataclass class PlayerPrediction: - """Player engine prediction output.""" - home_squad_quality: float = 50.0 # 0-100 - away_squad_quality: float = 50.0 - squad_diff: float = 0.0 # -100 to +100 + """Player engine prediction output. + + IMPORTANT: squad_quality uses the SAME composite formula as + extract_training_data.py so that inference values match the + distribution the model was trained on (~3-36 range). + """ + home_squad_quality: float = 12.0 # training-scale composite (~3-36) + away_squad_quality: float = 12.0 + squad_diff: float = 0.0 # home - away (training scale) home_key_players: int = 0 away_key_players: int = 0 - home_missing_impact: float = 0.0 # 0-1, how much weaker due to missing players + home_missing_impact: float = 0.0 # 0-1, how much weaker due to missing players away_missing_impact: float = 0.0 - home_goals_form: int = 0 # Goals in last 5 matches + home_goals_form: int = 0 # Goals in last 5 matches away_goals_form: int = 0 lineup_available: bool = False confidence: float = 0.0 @@ -100,10 +105,12 @@ class PlayerPredictorEngine: "home_goals_last_5": home_analysis.total_goals_last_5, "home_assists_last_5": home_analysis.total_assists_last_5, "home_key_players": home_analysis.key_players_count, + "home_forwards": home_analysis.forward_count or 2, "away_starting_11": away_analysis.starting_count or 11, "away_goals_last_5": away_analysis.total_goals_last_5, "away_assists_last_5": away_analysis.total_assists_last_5, "away_key_players": away_analysis.key_players_count, + "away_forwards": away_analysis.forward_count or 2, } elif match_id: # Try to get from database @@ -131,13 +138,31 @@ class PlayerPredictorEngine: away_goals = features.get("away_goals_last_5", 0) home_key = features.get("home_key_players", 0) away_key = features.get("away_key_players", 0) + home_assists = features.get("home_assists_last_5", 0) + away_assists = features.get("away_assists_last_5", 0) + home_starting = features.get("home_starting_11", 11) + away_starting = features.get("away_starting_11", 11) + home_fwd = features.get("home_forwards", 2) + away_fwd = features.get("away_forwards", 2) - # Calculate squad quality (0-100) - # Based on: goals scored, key players, assists - home_quality = min(100, 50 + (home_goals * 3) + (home_key * 5) + - features.get("home_assists_last_5", 0) * 2) - away_quality = min(100, 50 + (away_goals * 3) + (away_key * 5) + - features.get("away_assists_last_5", 0) * 2) + # Calculate squad quality — MUST match extract_training_data.py formula + # Formula: starting_count * 0.3 + goals * 2.0 + assists * 1.0 + # + key_players * 3.0 + fwd_count * 1.5 + # Typical range: ~3 – 36 (model trained on this distribution) + home_quality = ( + home_starting * 0.3 + + home_goals * 2.0 + + home_assists * 1.0 + + home_key * 3.0 + + home_fwd * 1.5 + ) + away_quality = ( + away_starting * 0.3 + + away_goals * 2.0 + + away_assists * 1.0 + + away_key * 3.0 + + away_fwd * 1.5 + ) # Squad difference squad_diff = home_quality - away_quality @@ -186,8 +211,10 @@ class PlayerPredictorEngine: Calculate 1X2 probability modifiers based on squad analysis. Returns modifiers to apply to base probabilities. + squad_diff is in training scale (~-33 to +33), normalize to -1..+1. """ - diff = prediction.squad_diff / 100 # -1 to +1 + diff = prediction.squad_diff / 33.0 # training-scale normalisation + diff = max(-1.0, min(1.0, diff)) # clamp return { "home_modifier": 1.0 + (diff * 0.3), # Up to +/-30% diff --git a/ai-engine/services/single_match_orchestrator.py b/ai-engine/services/single_match_orchestrator.py index c0dc994..3c6ec36 100755 --- a/ai-engine/services/single_match_orchestrator.py +++ b/ai-engine/services/single_match_orchestrator.py @@ -597,7 +597,7 @@ class SingleMatchOrchestrator: the model fall back on stronger signals (odds, ELO, form, H2H). """ defaults = { - 'home_squad_quality': 0.50, 'away_squad_quality': 0.50, 'squad_diff': 0.0, + 'home_squad_quality': 12.0, 'away_squad_quality': 12.0, 'squad_diff': 0.0, 'home_key_players': 3.0, 'away_key_players': 3.0, 'home_missing_impact': 0.0, 'away_missing_impact': 0.0, 'home_goals_form': 1.3, 'away_goals_form': 1.3, @@ -612,7 +612,7 @@ class SingleMatchOrchestrator: away_lineup=data.away_lineup, sidelined_data=data.sidelined_data, ) - return { + result = { 'home_squad_quality': float(pred.home_squad_quality), 'away_squad_quality': float(pred.away_squad_quality), 'squad_diff': float(pred.squad_diff), @@ -623,6 +623,13 @@ class SingleMatchOrchestrator: 'home_goals_form': float(pred.home_goals_form), 'away_goals_form': float(pred.away_goals_form), } + # Sanity check: squad_quality must be in training range (~3-36) + for side in ('home', 'away'): + sq = result[f'{side}_squad_quality'] + if sq > 50 or sq < 0: + print(f"🚨 SCALE MISMATCH: {side}_squad_quality={sq:.1f} " + f"(expected 3-36). Check player_predictor formula!") + return result except Exception as e: print(f"⚠️ Squad features failed: {e}") return defaults