189 lines
6.4 KiB
Python
Executable File
189 lines
6.4 KiB
Python
Executable File
"""
|
||
Referee Predictor Engine - V20 Ensemble Component
|
||
Analyzes referee patterns for cards, goals, and home bias.
|
||
|
||
Weight: 15% in ensemble
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
from typing import Dict, Optional
|
||
from dataclasses import dataclass
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
|
||
from features.referee_engine import get_referee_engine
|
||
|
||
|
||
@dataclass
|
||
class RefereePrediction:
|
||
"""Referee engine prediction output."""
|
||
referee_name: str = ""
|
||
matches_officiated: int = 0
|
||
|
||
# Card tendencies
|
||
avg_yellow_cards: float = 4.0
|
||
avg_red_cards: float = 0.2
|
||
is_card_heavy: bool = False # Above average cards
|
||
|
||
# Goal tendencies
|
||
avg_goals_per_match: float = 2.5
|
||
over_25_rate: float = 0.50
|
||
is_high_scoring: bool = False # Above average goals
|
||
|
||
# Home bias
|
||
home_win_rate: float = 0.45
|
||
home_bias: float = 0.0 # -1 to +1, positive = favors home
|
||
|
||
# Penalty tendency
|
||
penalty_rate: float = 0.15
|
||
|
||
confidence: float = 0.0
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"referee_name": self.referee_name,
|
||
"matches_officiated": self.matches_officiated,
|
||
"avg_yellow_cards": round(self.avg_yellow_cards, 1),
|
||
"avg_red_cards": round(self.avg_red_cards, 2),
|
||
"is_card_heavy": self.is_card_heavy,
|
||
"avg_goals_per_match": round(self.avg_goals_per_match, 2),
|
||
"over_25_rate": round(self.over_25_rate * 100, 1),
|
||
"is_high_scoring": self.is_high_scoring,
|
||
"home_win_rate": round(self.home_win_rate * 100, 1),
|
||
"home_bias": round(self.home_bias, 2),
|
||
"penalty_rate": round(self.penalty_rate * 100, 1),
|
||
"confidence": round(self.confidence, 1)
|
||
}
|
||
|
||
|
||
class RefereePredictorEngine:
|
||
"""
|
||
Referee-based prediction engine.
|
||
|
||
Analyzes:
|
||
- Card tendency (sarı/kırmızı kart ortalaması)
|
||
- Goal tendency (maç başına gol, 2.5 üst oranı)
|
||
- Home bias (ev sahibi lehine karar oranı)
|
||
- Penalty tendency (penaltı verme oranı)
|
||
"""
|
||
|
||
# League average benchmarks
|
||
LEAGUE_AVG_GOALS = 2.65
|
||
LEAGUE_AVG_YELLOW = 4.0
|
||
LEAGUE_HOME_WIN_RATE = 0.45
|
||
|
||
def __init__(self):
|
||
self.referee_engine = get_referee_engine()
|
||
print("✅ RefereePredictorEngine initialized")
|
||
|
||
def predict(self,
|
||
match_id: str = None,
|
||
referee_name: str = None,
|
||
league_id: str = None) -> RefereePrediction:
|
||
"""
|
||
Generate referee-based prediction.
|
||
|
||
Args:
|
||
match_id: Match ID to find referee
|
||
referee_name: Or provide referee name directly
|
||
league_id: League ID to scope stats (prevents name collisions)
|
||
|
||
Returns:
|
||
RefereePrediction with referee analysis
|
||
"""
|
||
|
||
# Get referee features
|
||
if match_id:
|
||
features = self.referee_engine.get_features(match_id, league_id=league_id)
|
||
# Live flows may already have referee_name while match_officials table is sparse.
|
||
# Prefer the richer profile if direct-name lookup has more history.
|
||
if referee_name:
|
||
name_features = self.referee_engine.get_features_by_name(referee_name, league_id=league_id)
|
||
if (name_features.get("referee_matches", 0) or 0) > (features.get("referee_matches", 0) or 0):
|
||
features = name_features
|
||
elif referee_name:
|
||
features = self.referee_engine.get_features_by_name(referee_name, league_id=league_id)
|
||
else:
|
||
# Return default
|
||
return RefereePrediction(confidence=10.0)
|
||
|
||
ref_name = features.get("referee_name", "Unknown")
|
||
matches = features.get("referee_matches", 0)
|
||
|
||
if matches < 5:
|
||
# Not enough data
|
||
return RefereePrediction(
|
||
referee_name=ref_name,
|
||
matches_officiated=matches,
|
||
confidence=20.0
|
||
)
|
||
|
||
# Extract features
|
||
avg_yellow = features.get("referee_avg_yellow", 4.0)
|
||
avg_red = features.get("referee_avg_red", 0.2)
|
||
avg_goals = features.get("referee_avg_goals", 2.5)
|
||
over25_rate = features.get("referee_over25_rate", 0.5)
|
||
home_win_rate = features.get("referee_home_win_rate", 0.45) if "referee_home_win_rate" in features else 0.45
|
||
home_bias = features.get("referee_home_bias", 0.0)
|
||
penalty_rate = features.get("referee_penalty_rate", 0.15)
|
||
|
||
# Determine tendencies
|
||
is_card_heavy = (avg_yellow + avg_red * 4) > (self.LEAGUE_AVG_YELLOW + 1)
|
||
is_high_scoring = avg_goals > self.LEAGUE_AVG_GOALS
|
||
|
||
# Confidence based on matches officiated
|
||
confidence = min(90.0, 30.0 + matches * 2)
|
||
|
||
return RefereePrediction(
|
||
referee_name=ref_name,
|
||
matches_officiated=matches,
|
||
avg_yellow_cards=avg_yellow,
|
||
avg_red_cards=avg_red,
|
||
is_card_heavy=is_card_heavy,
|
||
avg_goals_per_match=avg_goals,
|
||
over_25_rate=over25_rate,
|
||
is_high_scoring=is_high_scoring,
|
||
home_win_rate=home_win_rate,
|
||
home_bias=home_bias,
|
||
penalty_rate=penalty_rate,
|
||
confidence=confidence
|
||
)
|
||
|
||
def get_modifiers(self, prediction: RefereePrediction) -> Dict[str, float]:
|
||
"""
|
||
Get modifiers to apply to other predictions based on referee profile.
|
||
"""
|
||
return {
|
||
# Home team gets slight boost if referee has home bias
|
||
"home_modifier": 1.0 + (prediction.home_bias * 0.05),
|
||
# O/U modifier
|
||
"over_25_modifier": 1.0 + (prediction.avg_goals_per_match - self.LEAGUE_AVG_GOALS) * 0.1,
|
||
# Card modifier for card markets
|
||
"cards_modifier": 1.0 + (prediction.avg_yellow_cards - self.LEAGUE_AVG_YELLOW) * 0.05
|
||
}
|
||
|
||
|
||
# Singleton
|
||
_engine: Optional[RefereePredictorEngine] = None
|
||
|
||
|
||
def get_referee_predictor() -> RefereePredictorEngine:
|
||
global _engine
|
||
if _engine is None:
|
||
_engine = RefereePredictorEngine()
|
||
return _engine
|
||
|
||
|
||
if __name__ == "__main__":
|
||
engine = get_referee_predictor()
|
||
|
||
print("\n🧪 Referee Predictor Engine Test")
|
||
print("=" * 50)
|
||
|
||
pred = engine.predict(referee_name="Cüneyt Çakır")
|
||
|
||
print(f"\n📊 Prediction:")
|
||
for k, v in pred.to_dict().items():
|
||
print(f" {k}: {v}")
|