""" Player Predictor Engine - V20 Ensemble Component Analyzes squad quality, key players, and missing player impact. Weight: 25% in ensemble """ import os import sys from typing import Dict, Optional, List from dataclasses import dataclass sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from features.squad_analysis_engine import get_squad_analysis_engine 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 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 away_missing_impact: float = 0.0 home_goals_form: int = 0 # Goals in last 5 matches away_goals_form: int = 0 lineup_available: bool = False confidence: float = 0.0 def to_dict(self) -> dict: return { "home_squad_quality": round(self.home_squad_quality, 1), "away_squad_quality": round(self.away_squad_quality, 1), "squad_diff": round(self.squad_diff, 1), "home_key_players": self.home_key_players, "away_key_players": self.away_key_players, "home_missing_impact": round(self.home_missing_impact, 2), "away_missing_impact": round(self.away_missing_impact, 2), "home_goals_form": self.home_goals_form, "away_goals_form": self.away_goals_form, "lineup_available": self.lineup_available, "confidence": round(self.confidence, 1) } class PlayerPredictorEngine: """ Player/Squad-based prediction engine. Analyzes: - Starting 11 quality - Key player availability (top scorers) - Missing player impact - Recent goalscoring form per player """ def __init__(self): self.squad_engine = get_squad_analysis_engine() self.sidelined_analyzer = get_sidelined_analyzer() print("โœ… PlayerPredictorEngine initialized") def predict(self, match_id: str, home_team_id: str, away_team_id: str, home_lineup: List[str] = None, away_lineup: List[str] = None, sidelined_data: Dict = None) -> PlayerPrediction: """ Generate player-based prediction. Args: match_id: Match ID for lineup lookup home_team_id: Home team ID away_team_id: Away team ID home_lineup: Optional list of home player IDs away_lineup: Optional list of away player IDs Returns: PlayerPrediction with squad analysis """ # Get squad features if home_lineup and away_lineup: # Use provided lineups (for live matches) home_analysis = self.squad_engine.analyze_squad_from_list( home_lineup, home_team_id ) away_analysis = self.squad_engine.analyze_squad_from_list( away_lineup, away_team_id ) lineup_available = True # Build features dict from analysis objects features = { "home_starting_11": home_analysis.starting_count or 11, "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, "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, } elif match_id: # Try to get from database try: features = self.squad_engine.get_features( match_id, home_team_id, away_team_id ) lineup_available = ( features.get("home_starting_11", 0) >= 11 and features.get("away_starting_11", 0) >= 11 ) except Exception: features = self.squad_engine.get_features_without_match( home_team_id, away_team_id ) lineup_available = False else: features = self.squad_engine.get_features_without_match( home_team_id, away_team_id ) lineup_available = False # Extract features home_goals = features.get("home_goals_last_5", 0) 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) # 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) # Squad difference squad_diff = home_quality - away_quality # Missing player impact # Priority: sidelined data (position-weighted) > lineup count (basic) if sidelined_data: home_impact, away_impact = self.sidelined_analyzer.analyze_match(sidelined_data) home_missing = home_impact.impact_score away_missing = away_impact.impact_score sidelined_available = True else: # Fallback: basic lineup count method expected_xi = 11 actual_home_xi = features.get("home_starting_11", 11) actual_away_xi = features.get("away_starting_11", 11) home_missing = (expected_xi - actual_home_xi) / expected_xi if actual_home_xi < expected_xi else 0 away_missing = (expected_xi - actual_away_xi) / expected_xi if actual_away_xi < expected_xi else 0 sidelined_available = False # Confidence: more data sources = higher confidence confidence = 70.0 if lineup_available else 35.0 if home_goals + away_goals > 10: confidence += 15 if sidelined_available: confidence += self.sidelined_analyzer.config.get("sidelined.confidence_boost", 10) if not lineup_available: confidence -= 5.0 return PlayerPrediction( home_squad_quality=home_quality, away_squad_quality=away_quality, squad_diff=squad_diff, home_key_players=home_key, away_key_players=away_key, home_missing_impact=home_missing, away_missing_impact=away_missing, home_goals_form=home_goals, away_goals_form=away_goals, lineup_available=lineup_available, confidence=max(5.0, confidence) ) def get_1x2_modifier(self, prediction: PlayerPrediction) -> Dict[str, float]: """ Calculate 1X2 probability modifiers based on squad analysis. Returns modifiers to apply to base probabilities. """ diff = prediction.squad_diff / 100 # -1 to +1 return { "home_modifier": 1.0 + (diff * 0.3), # Up to +/-30% "away_modifier": 1.0 - (diff * 0.3), "draw_modifier": 1.0 - abs(diff) * 0.2 # Less draw if big diff } # Singleton _engine: Optional[PlayerPredictorEngine] = None def get_player_predictor() -> PlayerPredictorEngine: global _engine if _engine is None: _engine = PlayerPredictorEngine() return _engine if __name__ == "__main__": engine = get_player_predictor() print("\n๐Ÿงช Player Predictor Engine Test") print("=" * 50) pred = engine.predict( match_id=None, home_team_id="test_home", away_team_id="test_away" ) print(f"\n๐Ÿ“Š Prediction:") for k, v in pred.to_dict().items(): print(f" {k}: {v}")