This commit is contained in:
Executable
+316
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
Head-to-Head (H2H) Feature Engine
|
||||
Takımların birbirine karşı geçmiş performansını analiz eder.
|
||||
"""
|
||||
|
||||
import os
|
||||
import psycopg2
|
||||
from typing import Dict, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from data.db import get_clean_dsn
|
||||
|
||||
|
||||
@dataclass
|
||||
class H2HProfile:
|
||||
"""Head-to-Head analiz sonucu"""
|
||||
total_matches: int
|
||||
home_wins: int
|
||||
draws: int
|
||||
away_wins: int
|
||||
home_goals_total: int
|
||||
away_goals_total: int
|
||||
btts_count: int # Both teams to score
|
||||
over25_count: int
|
||||
|
||||
@property
|
||||
def home_win_rate(self) -> float:
|
||||
return self.home_wins / self.total_matches if self.total_matches > 0 else 0.33
|
||||
|
||||
@property
|
||||
def draw_rate(self) -> float:
|
||||
return self.draws / self.total_matches if self.total_matches > 0 else 0.33
|
||||
|
||||
@property
|
||||
def away_win_rate(self) -> float:
|
||||
return self.away_wins / self.total_matches if self.total_matches > 0 else 0.33
|
||||
|
||||
@property
|
||||
def avg_total_goals(self) -> float:
|
||||
return (self.home_goals_total + self.away_goals_total) / self.total_matches if self.total_matches > 0 else 2.5
|
||||
|
||||
@property
|
||||
def btts_rate(self) -> float:
|
||||
return self.btts_count / self.total_matches if self.total_matches > 0 else 0.5
|
||||
|
||||
@property
|
||||
def over25_rate(self) -> float:
|
||||
return self.over25_count / self.total_matches if self.total_matches > 0 else 0.5
|
||||
|
||||
@property
|
||||
def home_dominance(self) -> float:
|
||||
"""Ev sahibinin üstünlük skoru (-1 ile 1 arası)"""
|
||||
if self.total_matches == 0:
|
||||
return 0
|
||||
return (self.home_wins - self.away_wins) / self.total_matches
|
||||
|
||||
def to_features(self) -> Dict[str, float]:
|
||||
"""Feature dictionary döndür"""
|
||||
return {
|
||||
'h2h_total_matches': self.total_matches,
|
||||
'h2h_home_win_rate': self.home_win_rate,
|
||||
'h2h_draw_rate': self.draw_rate,
|
||||
'h2h_away_win_rate': self.away_win_rate,
|
||||
'h2h_avg_goals': self.avg_total_goals,
|
||||
'h2h_btts_rate': self.btts_rate,
|
||||
'h2h_over25_rate': self.over25_rate,
|
||||
'h2h_home_dominance': self.home_dominance,
|
||||
}
|
||||
|
||||
|
||||
class H2HFeatureEngine:
|
||||
"""
|
||||
Head-to-Head Feature Engine
|
||||
|
||||
İki takım arasındaki geçmiş karşılaşmaları analiz eder.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self._cache: Dict[Tuple[str, str], H2HProfile] = {}
|
||||
|
||||
def get_conn(self):
|
||||
if self.conn is None or self.conn.closed:
|
||||
self.conn = psycopg2.connect(get_clean_dsn())
|
||||
return self.conn
|
||||
|
||||
def get_h2h_profile(self, home_team_id: str, away_team_id: str,
|
||||
before_date: Optional[int] = None,
|
||||
limit: int = 20) -> H2HProfile:
|
||||
"""
|
||||
İki takım arasındaki geçmiş karşılaşmaları analiz et.
|
||||
|
||||
Args:
|
||||
home_team_id: Ev sahibi takım ID
|
||||
away_team_id: Deplasman takım ID
|
||||
before_date: Bu tarihten önceki maçlar (mst_utc, milliseconds)
|
||||
limit: Kaç maç geriye bakılacak
|
||||
|
||||
Returns:
|
||||
H2HProfile: Head-to-head analiz sonucu
|
||||
"""
|
||||
cache_key = (home_team_id, away_team_id)
|
||||
|
||||
# Cache kontrolü (before_date yoksa)
|
||||
if before_date is None and cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
|
||||
conn = self.get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Her iki yöndeki karşılaşmaları al
|
||||
# (A evde B deplasman + B evde A deplasman)
|
||||
query = """
|
||||
SELECT
|
||||
home_team_id, away_team_id,
|
||||
score_home, score_away
|
||||
FROM matches
|
||||
WHERE (
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
OR
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
)
|
||||
AND score_home IS NOT NULL
|
||||
AND score_away IS NOT NULL
|
||||
"""
|
||||
|
||||
params = [home_team_id, away_team_id, away_team_id, home_team_id]
|
||||
|
||||
if before_date:
|
||||
query += " AND mst_utc < %s"
|
||||
params.append(before_date)
|
||||
|
||||
query += " ORDER BY mst_utc DESC LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
cur.execute(query, params)
|
||||
matches = cur.fetchall()
|
||||
|
||||
if not matches:
|
||||
return H2HProfile(
|
||||
total_matches=0, home_wins=0, draws=0, away_wins=0,
|
||||
home_goals_total=0, away_goals_total=0,
|
||||
btts_count=0, over25_count=0
|
||||
)
|
||||
|
||||
# İstatistikleri hesapla
|
||||
home_wins = 0
|
||||
draws = 0
|
||||
away_wins = 0
|
||||
home_goals = 0
|
||||
away_goals = 0
|
||||
btts = 0
|
||||
over25 = 0
|
||||
|
||||
for match in matches:
|
||||
m_home_id, m_away_id, score_h, score_a = match
|
||||
|
||||
# Perspektifi normalize et (istenen takım açısından)
|
||||
if m_home_id == home_team_id:
|
||||
# Normal sıralama
|
||||
h_score, a_score = score_h, score_a
|
||||
else:
|
||||
# Ters sıralama (rakip evde oynamış)
|
||||
h_score, a_score = score_a, score_h
|
||||
|
||||
# Sonuç
|
||||
if h_score > a_score:
|
||||
home_wins += 1
|
||||
elif h_score < a_score:
|
||||
away_wins += 1
|
||||
else:
|
||||
draws += 1
|
||||
|
||||
# Goller
|
||||
home_goals += h_score
|
||||
away_goals += a_score
|
||||
|
||||
# BTTS
|
||||
if h_score > 0 and a_score > 0:
|
||||
btts += 1
|
||||
|
||||
# Over 2.5
|
||||
if h_score + a_score > 2.5:
|
||||
over25 += 1
|
||||
|
||||
profile = H2HProfile(
|
||||
total_matches=len(matches),
|
||||
home_wins=home_wins,
|
||||
draws=draws,
|
||||
away_wins=away_wins,
|
||||
home_goals_total=home_goals,
|
||||
away_goals_total=away_goals,
|
||||
btts_count=btts,
|
||||
over25_count=over25
|
||||
)
|
||||
|
||||
# Cache'e kaydet
|
||||
if before_date is None:
|
||||
self._cache[cache_key] = profile
|
||||
|
||||
return profile
|
||||
|
||||
def get_features(self, home_team_id: str, away_team_id: str,
|
||||
before_date: Optional[int] = None) -> Dict[str, float]:
|
||||
"""Feature dictionary döndür"""
|
||||
profile = self.get_h2h_profile(home_team_id, away_team_id, before_date)
|
||||
return profile.to_features()
|
||||
|
||||
def get_momentum(self, home_team_id: str, away_team_id: str,
|
||||
before_date: Optional[int] = None) -> Dict[str, float]:
|
||||
"""
|
||||
Son karşılaşmalardaki momentum/trend analizi.
|
||||
Son 5 maçtaki trend'e bakar.
|
||||
"""
|
||||
profile = self.get_h2h_profile(home_team_id, away_team_id, before_date, limit=5)
|
||||
|
||||
# Streak hesapla (ardışık sonuçlar)
|
||||
conn = self.get_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT home_team_id, score_home, score_away
|
||||
FROM matches
|
||||
WHERE (
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
OR
|
||||
(home_team_id = %s AND away_team_id = %s)
|
||||
)
|
||||
AND score_home IS NOT NULL
|
||||
"""
|
||||
params = [home_team_id, away_team_id, away_team_id, home_team_id]
|
||||
if before_date:
|
||||
query += " AND mst_utc < %s"
|
||||
params.append(before_date)
|
||||
query += " ORDER BY mst_utc DESC LIMIT 5"
|
||||
|
||||
cur.execute(query, params)
|
||||
recent = cur.fetchall()
|
||||
|
||||
streak = 0
|
||||
streak_type = None # 'home', 'away', 'draw'
|
||||
|
||||
for match in recent:
|
||||
m_home_id, score_h, score_a = match
|
||||
|
||||
# Perspektifi normalize et
|
||||
if m_home_id == home_team_id:
|
||||
result = 'home' if score_h > score_a else ('away' if score_h < score_a else 'draw')
|
||||
else:
|
||||
result = 'away' if score_h > score_a else ('home' if score_h < score_a else 'draw')
|
||||
|
||||
if streak_type is None:
|
||||
streak_type = result
|
||||
streak = 1
|
||||
elif result == streak_type:
|
||||
streak += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return {
|
||||
'h2h_recent_home_dominance': profile.home_dominance,
|
||||
'h2h_streak_length': streak,
|
||||
'h2h_streak_home': 1 if streak_type == 'home' else 0,
|
||||
'h2h_streak_away': 1 if streak_type == 'away' else 0,
|
||||
'h2h_streak_draw': 1 if streak_type == 'draw' else 0,
|
||||
}
|
||||
|
||||
|
||||
# Singleton
|
||||
_engine = None
|
||||
|
||||
def get_h2h_engine() -> H2HFeatureEngine:
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = H2HFeatureEngine()
|
||||
return _engine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test
|
||||
engine = get_h2h_engine()
|
||||
|
||||
# Örnek: Fenerbahçe vs Galatasaray (ID'leri bulunmalı)
|
||||
# Test için veritabanından bir karşılaşma çekelim
|
||||
conn = engine.get_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT home_team_id, away_team_id, match_name
|
||||
FROM matches
|
||||
WHERE score_home IS NOT NULL
|
||||
LIMIT 1
|
||||
""")
|
||||
result = cur.fetchone()
|
||||
|
||||
if result:
|
||||
home_id, away_id, name = result
|
||||
print(f"\n🧪 Test: {name}")
|
||||
print(f" Home ID: {home_id}")
|
||||
print(f" Away ID: {away_id}")
|
||||
|
||||
profile = engine.get_h2h_profile(home_id, away_id)
|
||||
print(f"\n📊 H2H Profil:")
|
||||
print(f" Toplam Maç: {profile.total_matches}")
|
||||
print(f" Ev Sahibi Kazanma: {profile.home_win_rate:.1%}")
|
||||
print(f" Beraberlik: {profile.draw_rate:.1%}")
|
||||
print(f" Deplasman Kazanma: {profile.away_win_rate:.1%}")
|
||||
print(f" Ortalama Gol: {profile.avg_total_goals:.2f}")
|
||||
print(f" BTTS Oranı: {profile.btts_rate:.1%}")
|
||||
print(f" Üst 2.5 Oranı: {profile.over25_rate:.1%}")
|
||||
print(f" Ev Dominance: {profile.home_dominance:+.2f}")
|
||||
|
||||
features = engine.get_features(home_id, away_id)
|
||||
print(f"\n🔧 Features: {features}")
|
||||
Reference in New Issue
Block a user