first (part 2: other directories)
Deploy Iddaai Backend / build-and-deploy (push) Failing after 18s

This commit is contained in:
2026-04-16 15:11:25 +03:00
parent 7814e0bc6b
commit 2f0b85a0c7
203 changed files with 59989 additions and 0 deletions
+316
View File
@@ -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}")