This commit is contained in:
Executable
+655
@@ -0,0 +1,655 @@
|
||||
"""
|
||||
ELO Rating System V2 - Venue-Adjusted & League-Weighted
|
||||
V9 Model için geliştirilmiş ELO sistemi.
|
||||
|
||||
V1'den Farklar:
|
||||
- Lig kalitesi faktörü (Premier League vs küçük lig)
|
||||
- Form decay (son maçlar daha etkili)
|
||||
- Venue-adjusted ELO (ev/deplasman ayrı)
|
||||
- Win probability hesaplama
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Optional, Tuple
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
MODELS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'models')
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeamELO:
|
||||
"""Takım ELO profili - Geliştirilmiş"""
|
||||
team_id: str
|
||||
team_name: str = ""
|
||||
|
||||
# Ana ELO'lar
|
||||
overall_elo: float = 1500.0
|
||||
home_elo: float = 1500.0
|
||||
away_elo: float = 1500.0
|
||||
|
||||
# Form ELO (son 5 maça göre)
|
||||
form_elo: float = 1500.0
|
||||
|
||||
# Meta
|
||||
matches_played: int = 0
|
||||
home_matches: int = 0
|
||||
away_matches: int = 0
|
||||
wins: int = 0
|
||||
draws: int = 0
|
||||
losses: int = 0
|
||||
last_updated: Optional[str] = None
|
||||
|
||||
# Son 5 maç formu (W/D/L sequence)
|
||||
recent_form: str = ""
|
||||
|
||||
def win_rate(self) -> float:
|
||||
if self.matches_played == 0:
|
||||
return 0.0
|
||||
return self.wins / self.matches_played
|
||||
|
||||
def to_features(self) -> Dict[str, float]:
|
||||
return {
|
||||
'elo_overall': self.overall_elo,
|
||||
'elo_home': self.home_elo,
|
||||
'elo_away': self.away_elo,
|
||||
'elo_form': self.form_elo,
|
||||
'elo_matches': self.matches_played,
|
||||
'elo_win_rate': self.win_rate(),
|
||||
}
|
||||
|
||||
|
||||
# Lig kalitesi faktörleri (1.0 = ortalama)
|
||||
LEAGUE_QUALITY = {
|
||||
# Top 5 Avrupa Ligleri
|
||||
"premier league": 1.15,
|
||||
"premier lig": 1.15,
|
||||
"la liga": 1.12,
|
||||
"bundesliga": 1.10,
|
||||
"serie a": 1.08,
|
||||
"ligue 1": 1.05,
|
||||
|
||||
# Güçlü ligler
|
||||
"eredivisie": 1.02,
|
||||
"primeira liga": 1.02,
|
||||
"süper lig": 1.00,
|
||||
|
||||
# Avrupa kupaları
|
||||
"champions league": 1.20,
|
||||
"şampiyonlar ligi": 1.20,
|
||||
"europa league": 1.10,
|
||||
"avrupa ligi": 1.10,
|
||||
"conference league": 1.00,
|
||||
|
||||
# Orta ligler
|
||||
"championship": 0.95,
|
||||
"2. bundesliga": 0.92,
|
||||
"serie b": 0.90,
|
||||
"la liga 2": 0.90,
|
||||
|
||||
# Küçük ligler
|
||||
"default": 0.85,
|
||||
}
|
||||
|
||||
|
||||
class ELORatingSystem:
|
||||
"""
|
||||
ELO Rating System V2 - Venue-Adjusted & League-Weighted
|
||||
|
||||
Yenilikler:
|
||||
- Ev/Deplasman ayrı ELO takibi
|
||||
- Lig kalitesi faktörü
|
||||
- Form ELO (son 5 maç ağırlıklı)
|
||||
- Gol farkına göre K-faktör ayarı
|
||||
"""
|
||||
|
||||
# ELO parametreleri
|
||||
K_FACTOR_BASE = 32 # Temel K faktörü
|
||||
K_FACTOR_NEW_TEAM = 48 # Yeni takımlar için daha yüksek (ilk 20 maç)
|
||||
HOME_ADVANTAGE = 65 # Ev sahibi avantajı (ELO cinsinden)
|
||||
INITIAL_ELO = 1500
|
||||
FORM_WEIGHT = 0.7 # Form ELO için son maç ağırlığı
|
||||
|
||||
def __init__(self):
|
||||
self.ratings: Dict[str, TeamELO] = {}
|
||||
self.league_cache: Dict[str, str] = {} # team_id -> league_name
|
||||
self.conn = None
|
||||
self._load_ratings()
|
||||
|
||||
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"[ELO] 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 _load_ratings(self):
|
||||
"""Rating'leri yükle — önce DB, sonra JSON fallback"""
|
||||
if self._load_ratings_from_db():
|
||||
return
|
||||
self._load_ratings_from_json()
|
||||
|
||||
def _load_ratings_from_db(self) -> bool:
|
||||
"""team_elo_ratings tablosundan rating'leri yükle"""
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
return False
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT ter.team_id, t.name,
|
||||
ter.overall_elo, ter.home_elo, ter.away_elo,
|
||||
ter.form_elo, ter.matches_played, ter.recent_form
|
||||
FROM team_elo_ratings ter
|
||||
LEFT JOIN teams t ON ter.team_id = t.id
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
if not rows:
|
||||
return False
|
||||
for row in rows:
|
||||
tid, name, overall, home, away, form, played, recent = row
|
||||
self.ratings[str(tid)] = TeamELO(
|
||||
team_id=str(tid),
|
||||
team_name=name or "",
|
||||
overall_elo=float(overall),
|
||||
home_elo=float(home),
|
||||
away_elo=float(away),
|
||||
form_elo=float(form),
|
||||
matches_played=int(played),
|
||||
recent_form=recent or [],
|
||||
)
|
||||
print(f"[OK] ELO V2 ratings DB'den yuklendi ({len(self.ratings)} takim)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[WARN] ELO DB yuklenemedi, JSON'a dusuyuyor: {e}")
|
||||
return False
|
||||
|
||||
def _load_ratings_from_json(self):
|
||||
"""JSON dosyasından rating'leri yükle (fallback)"""
|
||||
ratings_path = os.path.join(MODELS_DIR, 'elo_ratings_v2.json')
|
||||
if os.path.exists(ratings_path):
|
||||
try:
|
||||
with open(ratings_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for team_id, rating_data in data.items():
|
||||
self.ratings[team_id] = TeamELO(**rating_data)
|
||||
print(f"[OK] ELO V2 ratings JSON'dan yuklendi ({len(self.ratings)} takim)")
|
||||
except Exception as e:
|
||||
print(f"[WARN] ELO V2 ratings yuklenemedi: {e}")
|
||||
|
||||
def save_ratings(self):
|
||||
"""Rating'leri kaydet"""
|
||||
ratings_path = os.path.join(MODELS_DIR, 'elo_ratings_v2.json')
|
||||
os.makedirs(MODELS_DIR, exist_ok=True)
|
||||
|
||||
data = {team_id: asdict(elo) for team_id, elo in self.ratings.items()}
|
||||
with open(ratings_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
print(f"💾 ELO V2 ratings kaydedildi ({len(self.ratings)} takım)")
|
||||
|
||||
def get_or_create_rating(self, team_id: str, team_name: str = "") -> TeamELO:
|
||||
"""Takımın ELO'sunu getir veya oluştur"""
|
||||
if team_id not in self.ratings:
|
||||
self.ratings[team_id] = TeamELO(team_id=team_id, team_name=team_name)
|
||||
return self.ratings[team_id]
|
||||
|
||||
def get_league_quality(self, league_name: str) -> float:
|
||||
"""Lig kalitesi faktörünü döndür"""
|
||||
if not league_name:
|
||||
return LEAGUE_QUALITY["default"]
|
||||
|
||||
league_lower = league_name.lower()
|
||||
for key, quality in LEAGUE_QUALITY.items():
|
||||
if key in league_lower:
|
||||
return quality
|
||||
return LEAGUE_QUALITY["default"]
|
||||
|
||||
def expected_score(self, rating_a: float, rating_b: float) -> float:
|
||||
"""
|
||||
A'nın B'ye karşı beklenen skoru (0-1 arası).
|
||||
1 = kesin kazanır, 0.5 = eşit, 0 = kesin kaybeder
|
||||
"""
|
||||
return 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
|
||||
|
||||
def get_k_factor(self, team_elo: TeamELO, goal_diff: int,
|
||||
league_quality: float = 1.0) -> float:
|
||||
"""
|
||||
Dinamik K-faktörü hesapla.
|
||||
- Yeni takımlar için yüksek (hızlı adaptasyon)
|
||||
- Gol farkı yüksekse yüksek
|
||||
- Kaliteli liglerde yüksek
|
||||
"""
|
||||
# Temel K
|
||||
if team_elo.matches_played < 20:
|
||||
k = self.K_FACTOR_NEW_TEAM
|
||||
else:
|
||||
k = self.K_FACTOR_BASE
|
||||
|
||||
# Gol farkı çarpanı
|
||||
if goal_diff == 1:
|
||||
goal_mult = 1.0
|
||||
elif goal_diff == 2:
|
||||
goal_mult = 1.25
|
||||
elif goal_diff == 3:
|
||||
goal_mult = 1.5
|
||||
else:
|
||||
goal_mult = 1.75 + (goal_diff - 3) * 0.1
|
||||
|
||||
# Lig kalitesi çarpanı
|
||||
return k * goal_mult * league_quality
|
||||
|
||||
def update_after_match(
|
||||
self,
|
||||
home_id: str,
|
||||
away_id: str,
|
||||
home_goals: int,
|
||||
away_goals: int,
|
||||
home_name: str = "",
|
||||
away_name: str = "",
|
||||
league_name: str = ""
|
||||
):
|
||||
"""Maç sonrası ELO güncelle"""
|
||||
home_elo = self.get_or_create_rating(home_id, home_name)
|
||||
away_elo = self.get_or_create_rating(away_id, away_name)
|
||||
|
||||
# Gerçek skor
|
||||
if home_goals > away_goals:
|
||||
actual_home, actual_away = 1.0, 0.0
|
||||
home_elo.wins += 1
|
||||
away_elo.losses += 1
|
||||
result_home, result_away = 'W', 'L'
|
||||
elif home_goals < away_goals:
|
||||
actual_home, actual_away = 0.0, 1.0
|
||||
home_elo.losses += 1
|
||||
away_elo.wins += 1
|
||||
result_home, result_away = 'L', 'W'
|
||||
else:
|
||||
actual_home, actual_away = 0.5, 0.5
|
||||
home_elo.draws += 1
|
||||
away_elo.draws += 1
|
||||
result_home, result_away = 'D', 'D'
|
||||
|
||||
goal_diff = abs(home_goals - away_goals)
|
||||
league_quality = self.get_league_quality(league_name)
|
||||
|
||||
# K faktörleri
|
||||
k_home = self.get_k_factor(home_elo, goal_diff, league_quality)
|
||||
k_away = self.get_k_factor(away_elo, goal_diff, league_quality)
|
||||
|
||||
# -- Overall ELO --
|
||||
expected_home = self.expected_score(
|
||||
home_elo.overall_elo + self.HOME_ADVANTAGE,
|
||||
away_elo.overall_elo
|
||||
)
|
||||
home_elo.overall_elo += k_home * (actual_home - expected_home)
|
||||
away_elo.overall_elo += k_away * (actual_away - (1 - expected_home))
|
||||
|
||||
# -- Venue-Specific ELO --
|
||||
expected_home_venue = self.expected_score(home_elo.home_elo, away_elo.away_elo)
|
||||
home_elo.home_elo += k_home * (actual_home - expected_home_venue)
|
||||
away_elo.away_elo += k_away * (actual_away - (1 - expected_home_venue))
|
||||
|
||||
# -- Form ELO (son maçlar daha ağırlıklı) --
|
||||
home_elo.form_elo = (
|
||||
home_elo.form_elo * (1 - self.FORM_WEIGHT) +
|
||||
(1500 + (actual_home - 0.5) * 100) * self.FORM_WEIGHT
|
||||
)
|
||||
away_elo.form_elo = (
|
||||
away_elo.form_elo * (1 - self.FORM_WEIGHT) +
|
||||
(1500 + (actual_away - 0.5) * 100) * self.FORM_WEIGHT
|
||||
)
|
||||
|
||||
# Meta güncelle
|
||||
home_elo.matches_played += 1
|
||||
away_elo.matches_played += 1
|
||||
home_elo.home_matches += 1
|
||||
away_elo.away_matches += 1
|
||||
|
||||
# Son 5 form güncelle
|
||||
home_elo.recent_form = (result_home + home_elo.recent_form)[:5]
|
||||
away_elo.recent_form = (result_away + away_elo.recent_form)[:5]
|
||||
|
||||
home_elo.last_updated = datetime.now().isoformat()
|
||||
away_elo.last_updated = datetime.now().isoformat()
|
||||
|
||||
def predict_match(self, home_id: str, away_id: str) -> Dict[str, float]:
|
||||
"""
|
||||
Maç için kazanma olasılıklarını tahmin et.
|
||||
"""
|
||||
home_elo = self.get_or_create_rating(home_id)
|
||||
away_elo = self.get_or_create_rating(away_id)
|
||||
|
||||
# Overall bazlı
|
||||
exp_home_overall = self.expected_score(
|
||||
home_elo.overall_elo + self.HOME_ADVANTAGE,
|
||||
away_elo.overall_elo
|
||||
)
|
||||
|
||||
# Venue bazlı
|
||||
exp_home_venue = self.expected_score(
|
||||
home_elo.home_elo,
|
||||
away_elo.away_elo
|
||||
)
|
||||
|
||||
# Kombine (ortama)
|
||||
home_prob = (exp_home_overall + exp_home_venue) / 2
|
||||
|
||||
# Draw tahmini (ELO farkı küçükse daha yüksek)
|
||||
elo_diff = abs(home_elo.overall_elo - away_elo.overall_elo)
|
||||
draw_base = 0.25 # Temel beraberlik oranı
|
||||
draw_prob = draw_base * (1 - elo_diff / 800) # Fark arttıkça beraberlik azalır
|
||||
draw_prob = max(0.15, min(draw_prob, 0.35))
|
||||
|
||||
# Normalize
|
||||
remaining = 1 - draw_prob
|
||||
home_win = home_prob * remaining
|
||||
away_win = (1 - home_prob) * remaining
|
||||
|
||||
return {
|
||||
"home_win": round(home_win, 3),
|
||||
"draw": round(draw_prob, 3),
|
||||
"away_win": round(away_win, 3),
|
||||
}
|
||||
|
||||
def get_match_features(self, home_id: str, away_id: str) -> Dict[str, float]:
|
||||
"""Model için ELO feature'larını döndür"""
|
||||
home_elo = self.get_or_create_rating(home_id)
|
||||
away_elo = self.get_or_create_rating(away_id)
|
||||
|
||||
probs = self.predict_match(home_id, away_id)
|
||||
|
||||
# Form encode (WWWDL -> sayısal)
|
||||
def form_to_score(form: str) -> float:
|
||||
if not form:
|
||||
return 0.5
|
||||
score = 0
|
||||
for char in form:
|
||||
if char == 'W':
|
||||
score += 1
|
||||
elif char == 'D':
|
||||
score += 0.5
|
||||
return score / max(len(form), 1)
|
||||
|
||||
return {
|
||||
# Overall ELO
|
||||
'elo_home_overall': home_elo.overall_elo,
|
||||
'elo_away_overall': away_elo.overall_elo,
|
||||
'elo_diff_overall': home_elo.overall_elo - away_elo.overall_elo,
|
||||
|
||||
# Venue-Specific ELO
|
||||
'elo_home_venue': home_elo.home_elo,
|
||||
'elo_away_venue': away_elo.away_elo,
|
||||
'elo_diff_venue': home_elo.home_elo - away_elo.away_elo,
|
||||
|
||||
# Form ELO
|
||||
'elo_home_form': home_elo.form_elo,
|
||||
'elo_away_form': away_elo.form_elo,
|
||||
'elo_diff_form': home_elo.form_elo - away_elo.form_elo,
|
||||
|
||||
# Win probabilities
|
||||
'elo_prob_home': probs['home_win'],
|
||||
'elo_prob_draw': probs['draw'],
|
||||
'elo_prob_away': probs['away_win'],
|
||||
|
||||
# Experience
|
||||
'elo_home_matches': min(home_elo.matches_played, 100),
|
||||
'elo_away_matches': min(away_elo.matches_played, 100),
|
||||
|
||||
# Form score
|
||||
'elo_home_form_score': form_to_score(home_elo.recent_form),
|
||||
'elo_away_form_score': form_to_score(away_elo.recent_form),
|
||||
|
||||
# Win rates
|
||||
'elo_home_win_rate': home_elo.win_rate(),
|
||||
'elo_away_win_rate': away_elo.win_rate(),
|
||||
}
|
||||
|
||||
def save_ratings_to_db(self):
|
||||
"""Rating'leri team_elo_ratings tablosuna yaz (upsert)"""
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
print("❌ DB bağlantısı yok, DB'ye yazılamadı!")
|
||||
return
|
||||
|
||||
cur = conn.cursor()
|
||||
batch_size = 500
|
||||
teams = list(self.ratings.values())
|
||||
written = 0
|
||||
|
||||
for i in range(0, len(teams), batch_size):
|
||||
batch = teams[i:i + batch_size]
|
||||
values = []
|
||||
for elo in batch:
|
||||
values.append(cur.mogrify(
|
||||
"(%s, %s, %s, %s, %s, %s, %s, NOW())",
|
||||
(
|
||||
elo.team_id,
|
||||
round(elo.overall_elo, 2),
|
||||
round(elo.home_elo, 2),
|
||||
round(elo.away_elo, 2),
|
||||
round(elo.form_elo, 2),
|
||||
elo.matches_played,
|
||||
elo.recent_form[:5],
|
||||
)
|
||||
).decode('utf-8'))
|
||||
|
||||
sql = """
|
||||
INSERT INTO team_elo_ratings
|
||||
(team_id, overall_elo, home_elo, away_elo, form_elo, matches_played, recent_form, updated_at)
|
||||
VALUES {}
|
||||
ON CONFLICT (team_id) DO UPDATE SET
|
||||
overall_elo = EXCLUDED.overall_elo,
|
||||
home_elo = EXCLUDED.home_elo,
|
||||
away_elo = EXCLUDED.away_elo,
|
||||
form_elo = EXCLUDED.form_elo,
|
||||
matches_played = EXCLUDED.matches_played,
|
||||
recent_form = EXCLUDED.recent_form,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
""".format(", ".join(values))
|
||||
|
||||
cur.execute(sql)
|
||||
written += len(batch)
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
print(f"💾 DB'ye {written} takım ELO yazıldı (team_elo_ratings)")
|
||||
|
||||
def _load_top_league_ids(self) -> set:
|
||||
"""top_leagues.json'dan lig ID'lerini oku"""
|
||||
paths = [
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', 'top_leagues.json'),
|
||||
os.path.join(os.path.dirname(__file__), '..', 'top_leagues.json'),
|
||||
]
|
||||
for p in paths:
|
||||
if os.path.exists(p):
|
||||
with open(p) as f:
|
||||
ids = set(json.load(f))
|
||||
print(f"📋 {len(ids)} top lig yüklendi ({os.path.basename(p)})")
|
||||
return ids
|
||||
print("⚠️ top_leagues.json bulunamadı — tüm maçlar yazılacak")
|
||||
return set()
|
||||
|
||||
def calculate_all_from_history(self, sport: str = 'football'):
|
||||
"""Tüm tarihsel maçlardan ELO hesapla, top ligleri match_ai_features'a yaz"""
|
||||
print(f"\n🔄 {sport.upper()} için ELO V2 hesaplanıyor...")
|
||||
|
||||
conn = self.get_conn()
|
||||
if conn is None:
|
||||
print("❌ DB bağlantısı yok!")
|
||||
return
|
||||
|
||||
top_league_ids = self._load_top_league_ids()
|
||||
|
||||
cur = conn.cursor()
|
||||
|
||||
# Tüm bitmiş maçları tarih sırasına göre al (m.id ve league_id dahil)
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_name, t2.name as away_name,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.sport = %s
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc ASC
|
||||
""", (sport,))
|
||||
|
||||
matches = cur.fetchall()
|
||||
print(f"📊 {len(matches):,} maç işlenecek...")
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
batch: list = []
|
||||
processed = 0
|
||||
written = 0
|
||||
|
||||
for match in matches:
|
||||
(match_id, home_id, away_id, score_h, score_a,
|
||||
league_id, home_name, away_name, league) = match
|
||||
|
||||
if not (home_id and away_id):
|
||||
continue
|
||||
|
||||
# Sadece top ligler için pre-match ELO kaydet
|
||||
if not top_league_ids or league_id in top_league_ids:
|
||||
home_elo_obj = self.get_or_create_rating(home_id, home_name or "")
|
||||
away_elo_obj = self.get_or_create_rating(away_id, away_name or "")
|
||||
batch.append((
|
||||
match_id,
|
||||
home_elo_obj.overall_elo,
|
||||
away_elo_obj.overall_elo,
|
||||
home_elo_obj.home_elo,
|
||||
away_elo_obj.away_elo,
|
||||
home_elo_obj.form_elo,
|
||||
away_elo_obj.form_elo,
|
||||
))
|
||||
|
||||
# Tüm maçlar için ELO güncelle
|
||||
self.update_after_match(
|
||||
home_id, away_id, score_h, score_a,
|
||||
home_name or "", away_name or "", league or ""
|
||||
)
|
||||
processed += 1
|
||||
|
||||
if len(batch) >= BATCH_SIZE:
|
||||
self._flush_elo_batch(cur, batch, sport)
|
||||
conn.commit()
|
||||
written += len(batch)
|
||||
batch.clear()
|
||||
|
||||
if processed % 10000 == 0:
|
||||
print(f" İşlenen: {processed:,} / {len(matches):,}")
|
||||
|
||||
# Kalan batch'i yaz
|
||||
if batch:
|
||||
self._flush_elo_batch(cur, batch, sport)
|
||||
conn.commit()
|
||||
written += len(batch)
|
||||
|
||||
cur.close()
|
||||
print(f"✅ {processed:,} maç işlendi, {len(self.ratings)} takım")
|
||||
print(f"📝 {written:,} maç match_ai_features'a yazıldı")
|
||||
|
||||
# JSON'a kaydet
|
||||
self.save_ratings()
|
||||
|
||||
# DB'ye kaydet
|
||||
self.save_ratings_to_db()
|
||||
|
||||
# Top 20 takımı göster
|
||||
self._show_top_teams()
|
||||
|
||||
@staticmethod
|
||||
def _flush_elo_batch(cur, batch: list, sport: str = 'football') -> None:
|
||||
"""Batch upsert pre-match ELO values into sport-partitioned ai_features table."""
|
||||
from psycopg2.extras import execute_values
|
||||
|
||||
table_name = 'football_ai_features' if sport == 'football' else 'basketball_ai_features'
|
||||
sql = f"""
|
||||
INSERT INTO {table_name}
|
||||
(match_id, home_elo, away_elo,
|
||||
home_home_elo, away_away_elo,
|
||||
home_form_elo, away_form_elo,
|
||||
calculator_ver, updated_at)
|
||||
VALUES %s
|
||||
ON CONFLICT (match_id) DO UPDATE SET
|
||||
home_elo = EXCLUDED.home_elo,
|
||||
away_elo = EXCLUDED.away_elo,
|
||||
home_home_elo = EXCLUDED.home_home_elo,
|
||||
away_away_elo = EXCLUDED.away_away_elo,
|
||||
home_form_elo = EXCLUDED.home_form_elo,
|
||||
away_form_elo = EXCLUDED.away_form_elo,
|
||||
calculator_ver = EXCLUDED.calculator_ver,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
"""
|
||||
now = datetime.now().isoformat()
|
||||
values = [
|
||||
(mid, h_elo, a_elo, hh_elo, aa_elo, hf_elo, af_elo,
|
||||
'elo_v2_backfill', now)
|
||||
for mid, h_elo, a_elo, hh_elo, aa_elo, hf_elo, af_elo in batch
|
||||
]
|
||||
execute_values(cur, sql, values, page_size=500)
|
||||
|
||||
def _show_top_teams(self, n: int = 20):
|
||||
"""En güçlü takımları göster"""
|
||||
sorted_teams = sorted(
|
||||
self.ratings.items(),
|
||||
key=lambda x: x[1].overall_elo,
|
||||
reverse=True
|
||||
)[:n]
|
||||
|
||||
print(f"\n🏆 Top {n} Takım (ELO V2):")
|
||||
for i, (team_id, elo) in enumerate(sorted_teams, 1):
|
||||
name = elo.team_name[:25] if elo.team_name else team_id[:25]
|
||||
print(f" {i:2}. {name:25} → {elo.overall_elo:.0f} (H:{elo.home_elo:.0f} A:{elo.away_elo:.0f})")
|
||||
|
||||
|
||||
# Singleton
|
||||
_system = None
|
||||
|
||||
def get_elo_system() -> ELORatingSystem:
|
||||
global _system
|
||||
if _system is None:
|
||||
_system = ELORatingSystem()
|
||||
return _system
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure ai-engine root is on sys.path (for `from data.db import ...`)
|
||||
_AI_ENGINE_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_AI_ENGINE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_AI_ENGINE_ROOT))
|
||||
|
||||
system = get_elo_system()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'calculate':
|
||||
system.calculate_all_from_history('football')
|
||||
else:
|
||||
print("\n🧪 ELO V2 Test")
|
||||
print("Kullanım: python elo_system.py calculate")
|
||||
print(f"\n📊 Yüklü takım sayısı: {len(system.ratings)}")
|
||||
|
||||
if len(system.ratings) > 0:
|
||||
system._show_top_teams(10)
|
||||
Reference in New Issue
Block a user