This commit is contained in:
Executable
+224
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
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}")
|
||||
Reference in New Issue
Block a user