Files
iddaai-be/ai-engine/features/referee_engine.py
fahricansecer 2f0b85a0c7
Deploy Iddaai Backend / build-and-deploy (push) Failing after 18s
first (part 2: other directories)
2026-04-16 15:11:25 +03:00

369 lines
13 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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}")