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