""" Referee Engine - V9 Feature Hakem profilleri ve maç etki analizi. Analiz Edilen Metrikler: - Ortalama kart sayısı (sarı/kırmızı) - Penaltı verme eğilimi - Ev sahibi lehine karar oranı - Maç başına toplam gol ortalaması """ import os from typing import Dict, Optional, List from dataclasses import dataclass, field from datetime import datetime try: import psycopg2 from psycopg2.extras import RealDictCursor except ImportError: psycopg2 = None @dataclass class RefereeProfile: """Hakem profili""" referee_name: str matches_count: int = 0 # Kart istatistikleri avg_yellow_cards: float = 0.0 avg_red_cards: float = 0.0 total_cards_per_match: float = 0.0 # Penaltı istatistikleri penalty_rate: float = 0.0 # Penaltı verdiği maç oranı # Ev sahibi eğilimi home_win_rate: float = 0.0 home_bias: float = 0.0 # -1 (away bias) to +1 (home bias) # Gol istatistikleri avg_goals_per_match: float = 0.0 over_25_rate: float = 0.0 @dataclass class RefereeFeatures: """Model için hakem feature'ları""" referee_name: str = "" referee_matches: int = 0 referee_avg_yellow: float = 0.0 referee_avg_red: float = 0.0 referee_cards_total: float = 0.0 referee_penalty_rate: float = 0.0 referee_home_bias: float = 0.0 referee_avg_goals: float = 0.0 referee_over25_rate: float = 0.0 referee_experience: float = 0.0 # 0-1 normalized def to_dict(self) -> Dict[str, float]: return { 'referee_matches': float(self.referee_matches), 'referee_avg_yellow': self.referee_avg_yellow, 'referee_avg_red': self.referee_avg_red, 'referee_cards_total': self.referee_cards_total, 'referee_penalty_rate': self.referee_penalty_rate, 'referee_home_bias': self.referee_home_bias, 'referee_avg_goals': self.referee_avg_goals, 'referee_over25_rate': self.referee_over25_rate, 'referee_experience': self.referee_experience, } class RefereeEngine: """ Hakem analiz motoru. Hakemlerin geçmiş maçlarını analiz ederek: - Kart eğilimlerini - Ev sahibi bias'ını - Gol ortalamasını hesaplar. """ # Ana hakem rolü ID'si (genellikle 1 veya "Hakem") MAIN_REFEREE_ROLE_ID = 1 def __init__(self): self.conn = None self._referee_cache: Dict[str, RefereeProfile] = {} self._cache_loaded = False def _connect_db(self): if psycopg2 is None: return None try: from data.db import get_clean_dsn self.conn = psycopg2.connect(get_clean_dsn()) return self.conn except Exception as e: print(f"[RefereeEngine] DB connection failed: {e}") return None def get_conn(self): if self.conn is None or self.conn.closed: self._connect_db() return self.conn def _get_main_referee_role_id(self) -> int: """Ana hakem rolü ID'sini bul""" conn = self.get_conn() if conn is None: return self.MAIN_REFEREE_ROLE_ID try: with conn.cursor() as cur: cur.execute(""" SELECT id FROM official_roles WHERE LOWER(name) LIKE '%%hakem%%' AND LOWER(name) NOT LIKE '%%yardımcı%%' AND LOWER(name) NOT LIKE '%%dördüncü%%' LIMIT 1 """) result = cur.fetchone() if result: return result[0] except Exception: pass return self.MAIN_REFEREE_ROLE_ID def get_referee_for_match(self, match_id: str) -> Optional[str]: """Maçın ana hakemini bul""" conn = self.get_conn() if conn is None: return None try: main_role_id = self._get_main_referee_role_id() with conn.cursor() as cur: cur.execute(""" SELECT name FROM match_officials WHERE match_id = %s AND role_id = %s LIMIT 1 """, (match_id, main_role_id)) result = cur.fetchone() return result[0] if result else None except Exception as e: print(f"[RefereeEngine] Error getting referee: {e}") return None def calculate_referee_profile(self, referee_name: str, league_id: str = None) -> RefereeProfile: """Hakemin maçlarını analiz et. league_id verilirse sadece o ligteki maçları kullanır.""" # Composite cache key — aynı isim farklı liglerde farklı profil cache_key = (referee_name, league_id) if cache_key in self._referee_cache: return self._referee_cache[cache_key] profile = RefereeProfile(referee_name=referee_name) conn = self.get_conn() if conn is None: return profile try: main_role_id = self._get_main_referee_role_id() with conn.cursor(cursor_factory=RealDictCursor) as cur: # Bu hakemin yönettiği maçları al (league_id varsa sadece o lig) if league_id: cur.execute(""" SELECT m.id, m.score_home, m.score_away, m.home_team_id, m.away_team_id FROM matches m JOIN match_officials mo ON m.id = mo.match_id WHERE mo.name = %s AND mo.role_id = %s AND m.league_id = %s AND m.score_home IS NOT NULL AND m.score_away IS NOT NULL ORDER BY m.mst_utc DESC LIMIT 100 """, (referee_name, main_role_id, league_id)) else: cur.execute(""" SELECT m.id, m.score_home, m.score_away, m.home_team_id, m.away_team_id FROM matches m JOIN match_officials mo ON m.id = mo.match_id WHERE mo.name = %s AND mo.role_id = %s AND m.score_home IS NOT NULL AND m.score_away IS NOT NULL ORDER BY m.mst_utc DESC LIMIT 100 """, (referee_name, main_role_id)) matches = cur.fetchall() profile.matches_count = len(matches) if profile.matches_count == 0: return profile match_ids = [m['id'] for m in matches] # Kart istatistikleri cur.execute(""" SELECT COUNT(*) FILTER (WHERE event_subtype ILIKE '%%yellow%%') as yellow_count, COUNT(*) FILTER (WHERE event_subtype ILIKE '%%red%%' OR event_subtype ILIKE '%%second%%') as red_count FROM match_player_events WHERE match_id = ANY(%s) AND event_type = 'card' """, (match_ids,)) card_stats = cur.fetchone() if card_stats: profile.avg_yellow_cards = (card_stats['yellow_count'] or 0) / profile.matches_count profile.avg_red_cards = (card_stats['red_count'] or 0) / profile.matches_count profile.total_cards_per_match = profile.avg_yellow_cards + profile.avg_red_cards # Penaltı istatistikleri cur.execute(""" SELECT COUNT(DISTINCT match_id) as penalty_matches FROM match_player_events WHERE match_id = ANY(%s) AND event_type = 'goal' AND event_subtype ILIKE '%%penaltı%%' """, (match_ids,)) penalty_stats = cur.fetchone() if penalty_stats: profile.penalty_rate = (penalty_stats['penalty_matches'] or 0) / profile.matches_count # Ev sahibi eğilimi ve gol ortalaması home_wins = 0 away_wins = 0 draws = 0 total_goals = 0 over_25_count = 0 for m in matches: goals = (m['score_home'] or 0) + (m['score_away'] or 0) total_goals += goals if goals > 2.5: over_25_count += 1 if m['score_home'] > m['score_away']: home_wins += 1 elif m['score_home'] < m['score_away']: away_wins += 1 else: draws += 1 profile.avg_goals_per_match = total_goals / profile.matches_count profile.over_25_rate = over_25_count / profile.matches_count profile.home_win_rate = home_wins / profile.matches_count # Home bias: -1 (away favors) to +1 (home favors) # Normal lig ortalaması ~%46 ev sahibi, buna göre normalize expected_home_rate = 0.46 profile.home_bias = (profile.home_win_rate - expected_home_rate) * 2 profile.home_bias = max(-1, min(1, profile.home_bias)) # Cache'e ekle self._referee_cache[cache_key] = profile return profile except Exception as e: print(f"[RefereeEngine] Error calculating profile: {e}") return profile def get_features(self, match_id: str, league_id: str = None) -> Dict[str, float]: """ Maç için hakem feature'larını hesapla. Args: match_id: Maç ID'si league_id: Lig ID'si (opsiyonel — isim çakışmalarını önlemek için) Returns: Hakem feature'ları dict olarak """ features = RefereeFeatures() # Hakemi bul referee_name = self.get_referee_for_match(match_id) if referee_name is None: return features.to_dict() features.referee_name = referee_name # Profili hesapla (league_id ile scope'lanmış) profile = self.calculate_referee_profile(referee_name, league_id=league_id) features.referee_matches = profile.matches_count features.referee_avg_yellow = profile.avg_yellow_cards features.referee_avg_red = profile.avg_red_cards features.referee_cards_total = profile.total_cards_per_match features.referee_penalty_rate = profile.penalty_rate features.referee_home_bias = profile.home_bias features.referee_avg_goals = profile.avg_goals_per_match features.referee_over25_rate = profile.over_25_rate # Deneyim: 50+ maç = 1.0, 0 maç = 0.0 features.referee_experience = min(profile.matches_count / 50, 1.0) return features.to_dict() def get_features_by_name(self, referee_name: str, league_id: str = None) -> Dict[str, float]: """ Hakem ismiyle feature'ları hesapla. Args: referee_name: Hakem ismi league_id: Lig ID'si (opsiyonel — isim çakışmalarını önlemek için) Returns: Hakem feature'ları dict olarak """ features = RefereeFeatures() if not referee_name: return features.to_dict() features.referee_name = referee_name profile = self.calculate_referee_profile(referee_name, league_id=league_id) features.referee_matches = profile.matches_count features.referee_avg_yellow = profile.avg_yellow_cards features.referee_avg_red = profile.avg_red_cards features.referee_cards_total = profile.total_cards_per_match features.referee_penalty_rate = profile.penalty_rate features.referee_home_bias = profile.home_bias features.referee_avg_goals = profile.avg_goals_per_match features.referee_over25_rate = profile.over_25_rate features.referee_experience = min(profile.matches_count / 50, 1.0) return features.to_dict() # Singleton instance _engine: Optional[RefereeEngine] = None def get_referee_engine() -> RefereeEngine: """Singleton referee engine instance döndür""" global _engine if _engine is None: _engine = RefereeEngine() return _engine if __name__ == "__main__": # Test engine = get_referee_engine() print("\n🧪 Referee Engine Test") print("=" * 50) # Test with a known referee name test_referee = "Cüneyt Çakır" features = engine.get_features_by_name(test_referee) print(f"\n📊 Hakem: {test_referee}") for key, value in features.items(): print(f" {key}: {value:.3f}")