Files
iddaai-be/ai-engine/features/upset_engine.py
T
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

420 lines
14 KiB
Python
Executable File
Raw 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.
"""
Upset Engine - Dev Avcısı Tespit Sistemi
V9 Model için Galatasaray-Liverpool tarzı sürpriz maçları tespit eder.
Faktörler:
1. Atmosfer (Avrupa gecesi, taraftar baskısı)
2. Motivasyon asimetrisi (küme düşme vs şampiyon)
3. Yorgunluk (maç yoğunluğu, seyahat)
4. Tarihsel upset pattern
"""
import os
import sys
from typing import Dict, Any, Optional, Tuple
from dataclasses import dataclass, field
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
import psycopg2
from psycopg2.extras import RealDictCursor
except ImportError:
psycopg2 = None
@dataclass
class UpsetFactors:
"""Upset potansiyelini etkileyen faktörler"""
atmosphere_score: float = 0.0 # Atmosfer etkisi (0-1)
motivation_score: float = 0.0 # Motivasyon asimetrisi (0-1)
fatigue_score: float = 0.0 # Yorgunluk farkı (0-1)
historical_upset_rate: float = 0.0 # Tarihsel upset oranı (0-1)
total_upset_potential: float = 0.0 # Toplam upset potansiyeli (0-1)
reasoning: list = field(default_factory=list)
class UpsetEngine:
"""
Favori takımın kaybedeceği maçları tespit eder.
Galatasaray-Liverpool tarzı sürprizleri yakalar.
"""
# Yüksek atmosferli stadyumlar (manuel tanımlı + hesaplanabilir)
HIGH_ATMOSPHERE_TEAMS = {
# Türkiye
"galatasaray", "fenerbahce", "besiktas", "trabzonspor",
# İngiltere
"liverpool", "newcastle", "leeds",
# Almanya
"dortmund", "union berlin",
# Yunanistan
"olympiacos", "panathinaikos", "aek athens",
# Arjantin
"boca juniors", "river plate",
# Diğer
"celtic", "rangers", "red star belgrade"
}
# Avrupa kupaları (yüksek motivasyon)
EUROPEAN_COMPETITIONS = {
"şampiyonlar ligi", "champions league", "uefa champions league",
"avrupa ligi", "europa league", "uefa europa league",
"konferans ligi", "conference league", "uefa conference league"
}
def __init__(self):
self.conn = None
self._connect_db()
def _connect_db(self):
"""Veritabanına bağlan"""
if psycopg2 is None:
return
try:
from data.db import get_clean_dsn
self.conn = psycopg2.connect(get_clean_dsn())
except Exception as e:
print(f"[UpsetEngine] DB connection failed: {e}")
self.conn = None
def _get_conn(self):
"""Bağlantıyı kontrol et ve döndür"""
if self.conn is None or self.conn.closed:
self._connect_db()
return self.conn
def calculate_atmosphere_score(
self,
home_team_name: str,
league_name: str,
is_cup_match: bool = False
) -> Tuple[float, list]:
"""
Atmosfer skorunu hesapla.
Yüksek atmosferli stadyumlar upset potansiyelini artırır.
"""
score = 0.0
reasons = []
# Yüksek atmosferli takım mı?
home_lower = home_team_name.lower()
for team in self.HIGH_ATMOSPHERE_TEAMS:
if team in home_lower:
score += 0.25
reasons.append(f"🔥 {home_team_name} yüksek atmosferli stadyum")
break
# Avrupa kupası mı?
league_lower = league_name.lower()
for comp in self.EUROPEAN_COMPETITIONS:
if comp in league_lower:
score += 0.20
reasons.append("🌟 Avrupa gecesi - ekstra motivasyon")
break
# Kupa maçı mı? (tek maç eliminasyon)
if is_cup_match:
score += 0.10
reasons.append("🏆 Kupa maçı - her şey olabilir")
return min(score, 1.0), reasons
def calculate_motivation_score(
self,
home_position: int,
away_position: int,
home_points_to_safety: Optional[int] = None,
away_already_champion: bool = False,
total_teams: int = 20
) -> Tuple[float, list]:
"""
Motivasyon asimetrisini hesapla.
Alt sıradaki takımın üst sıradakine karşı ekstra motivasyonu.
"""
score = 0.0
reasons = []
# Pozisyon farkı
position_diff = 0
if away_position is not None and home_position is not None:
position_diff = away_position - home_position # Negatif = deplasman daha iyi sırada
# Küme düşme hattı vs üst sıra (en güçlü upset faktörü)
relegation_zone = total_teams - 3 # Son 3 takım
if home_position is not None and away_position is not None:
if home_position >= relegation_zone and away_position <= 3:
score += 0.30
reasons.append("⚔️ Hayatta kalma savaşı vs şampiyonluk adayı")
elif home_position >= relegation_zone:
score += 0.15
reasons.append("🔥 Ev sahibi küme düşme hattında - ekstra motivasyon")
elif home_position is not None and home_position >= relegation_zone:
score += 0.15
reasons.append("🔥 Ev sahibi küme düşme hattında - ekstra motivasyon")
# Deplasman takımı zaten şampiyon mu?
if away_already_champion:
score += 0.20
reasons.append("😴 Deplasman takımı zaten şampiyon - motivasyon düşük")
# Büyük pozisyon farkı (underdog evinde)
if position_diff < -10:
score += 0.15
reasons.append(f"📊 {abs(position_diff)} sıra fark - büyük maç heyecanı")
elif position_diff < -5:
score += 0.08
return min(score, 1.0), reasons
def calculate_fatigue_score(
self,
home_matches_last_14d: int = 0,
away_matches_last_14d: int = 0,
home_days_rest: int = 7,
away_days_rest: int = 7,
away_travel_km: float = 0
) -> Tuple[float, list]:
"""
Yorgunluk farkını hesapla.
Yorgun deplasman takımı = yüksek upset potansiyeli.
"""
score = 0.0
reasons = []
# Maç yoğunluğu farkı
match_diff = away_matches_last_14d - home_matches_last_14d
if match_diff >= 3:
score += 0.20
reasons.append(f"🏃 Deplasman {match_diff} maç daha fazla oynamış")
elif match_diff >= 2:
score += 0.10
# Dinlenme süresi farkı
rest_diff = home_days_rest - away_days_rest
if rest_diff >= 4:
score += 0.15
reasons.append(f"💤 Ev sahibi {rest_diff} gün daha fazla dinlenmiş")
elif rest_diff >= 2:
score += 0.08
# Uzun deplasman
if away_travel_km > 3000:
score += 0.15
reasons.append(f"✈️ Uzun deplasman ({int(away_travel_km)} km)")
elif away_travel_km > 1500:
score += 0.08
return min(score, 1.0), reasons
def get_historical_upset_rate(
self,
home_team_id: str,
before_date_ms: int,
lookback_matches: int = 20
) -> Tuple[float, list]:
"""
Ev sahibi takımın tarihsel upset oranını hesapla.
Üst sıradaki takımlara karşı galibiyetler.
"""
reasons = []
conn = self._get_conn()
if conn is None:
return 0.0, reasons
try:
cursor = conn.cursor(cursor_factory=RealDictCursor)
# Ev sahibi olarak oynadığı ve sıralamada geride olduğu maçlar
query = """
WITH home_matches AS (
SELECT
m.id,
m.score_home,
m.score_away,
m.home_team_id,
m.away_team_id
FROM matches m
WHERE m.home_team_id = %s
AND m.mst_utc < %s
AND m.score_home IS NOT NULL
AND m.score_away IS NOT NULL
ORDER BY m.mst_utc DESC
LIMIT %s
)
SELECT
COUNT(*) as total,
SUM(CASE WHEN score_home > score_away THEN 1 ELSE 0 END) as wins
FROM home_matches
"""
cursor.execute(query, (home_team_id, before_date_ms, lookback_matches))
result = cursor.fetchone()
if result and result['total'] > 0:
win_rate = result['wins'] / result['total']
# Ev sahibi kazanma oranı yüksekse, upset potansiyeli de yüksek
if win_rate > 0.5:
rate = min((win_rate - 0.4) * 0.5, 0.3)
reasons.append(f"📈 Güçlü ev sahibi performansı (%{int(win_rate*100)} kazanma)")
return rate, reasons
return 0.0, reasons
except Exception as e:
print(f"[UpsetEngine] Historical query error: {e}")
return 0.0, reasons
def calculate_upset_potential(
self,
home_team_name: str,
home_team_id: str,
away_team_name: str,
league_name: str,
home_position: int,
away_position: int,
match_date_ms: int,
is_cup_match: bool = False,
home_matches_last_14d: int = 2,
away_matches_last_14d: int = 2,
home_days_rest: int = 7,
away_days_rest: int = 7,
away_travel_km: float = 0,
total_teams: int = 20
) -> UpsetFactors:
"""
Tüm faktörleri birleştirerek upset potansiyelini hesapla.
Returns:
UpsetFactors: Tüm faktörler ve toplam skor
"""
factors = UpsetFactors()
all_reasons = []
# 1. Atmosfer
atm_score, atm_reasons = self.calculate_atmosphere_score(
home_team_name, league_name, is_cup_match
)
factors.atmosphere_score = atm_score
all_reasons.extend(atm_reasons)
# 2. Motivasyon
mot_score, mot_reasons = self.calculate_motivation_score(
home_position, away_position,
total_teams=total_teams
)
factors.motivation_score = mot_score
all_reasons.extend(mot_reasons)
# 3. Yorgunluk
fat_score, fat_reasons = self.calculate_fatigue_score(
home_matches_last_14d, away_matches_last_14d,
home_days_rest, away_days_rest,
away_travel_km
)
factors.fatigue_score = fat_score
all_reasons.extend(fat_reasons)
# 4. Tarihsel (sadece DB varsa)
hist_score, hist_reasons = self.get_historical_upset_rate(
home_team_id, match_date_ms
)
factors.historical_upset_rate = hist_score
all_reasons.extend(hist_reasons)
# Toplam skor (weighted average)
factors.total_upset_potential = min(
factors.atmosphere_score * 0.25 +
factors.motivation_score * 0.35 +
factors.fatigue_score * 0.25 +
factors.historical_upset_rate * 0.15,
1.0
)
factors.reasoning = all_reasons
return factors
def get_features(
self,
home_team_name: str,
home_team_id: str,
away_team_name: str,
league_name: str,
home_position: int,
away_position: int,
match_date_ms: int,
**kwargs
) -> Dict[str, float]:
"""
Model için feature dict döndür.
Training ve inference'da kullanılır.
"""
factors = self.calculate_upset_potential(
home_team_name=home_team_name,
home_team_id=home_team_id,
away_team_name=away_team_name,
league_name=league_name,
home_position=home_position,
away_position=away_position,
match_date_ms=match_date_ms,
**kwargs
)
return {
"upset_atmosphere": factors.atmosphere_score,
"upset_motivation": factors.motivation_score,
"upset_fatigue": factors.fatigue_score,
"upset_historical": factors.historical_upset_rate,
"upset_potential": factors.total_upset_potential,
}
# Singleton instance
_engine_instance = None
def get_upset_engine() -> UpsetEngine:
"""Singleton pattern ile engine döndür"""
global _engine_instance
if _engine_instance is None:
_engine_instance = UpsetEngine()
return _engine_instance
# Test
if __name__ == "__main__":
engine = get_upset_engine()
# Galatasaray vs Liverpool örneği
factors = engine.calculate_upset_potential(
home_team_name="Galatasaray",
home_team_id="test-gs-id",
away_team_name="Liverpool",
league_name="UEFA Champions League",
home_position=12,
away_position=1,
match_date_ms=1700000000000,
is_cup_match=False,
away_matches_last_14d=5,
home_matches_last_14d=2,
away_days_rest=3,
home_days_rest=7,
away_travel_km=2800,
total_teams=20
)
print("=" * 60)
print("GALATASARAY vs LIVERPOOL - UPSET ANALİZİ")
print("=" * 60)
print(f"🏟️ Atmosfer Skoru: {factors.atmosphere_score:.2f}")
print(f"💪 Motivasyon Skoru: {factors.motivation_score:.2f}")
print(f"😓 Yorgunluk Skoru: {factors.fatigue_score:.2f}")
print(f"📊 Tarihsel Skor: {factors.historical_upset_rate:.2f}")
print(f"\n🎯 TOPLAM UPSET POTANSİYELİ: {factors.total_upset_potential:.2f}")
print("\n📝 Sebepler:")
for reason in factors.reasoning:
print(f" {reason}")