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