195 lines
6.3 KiB
Python
Executable File
195 lines
6.3 KiB
Python
Executable File
"""
|
||
Team Stats Engine
|
||
Takımların oyun tarzı istatistiklerini analiz eder.
|
||
football_team_stats tablosundaki kayıtlardan possession, şut, korner verilerini kullanır.
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import psycopg2
|
||
from typing import Dict
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from data.db import get_clean_dsn
|
||
|
||
|
||
class TeamStatsEngine:
|
||
"""
|
||
Takım istatistikleri için feature engine.
|
||
|
||
Analiz edilen metrikler:
|
||
- Ortalama top hakimiyeti (possession)
|
||
- Ortalama isabetli şut
|
||
- Ortalama korner
|
||
- Şut/Gol dönüşüm oranı (xG benzeri)
|
||
- Savunma gücü
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.conn = None
|
||
|
||
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_features(self, team_id: str, before_date: int,
|
||
limit: int = 10, max_days: int = 180) -> Dict[str, float]:
|
||
"""
|
||
Takımın oyun tarzı feature'larını hesapla.
|
||
|
||
Args:
|
||
team_id: Takım ID
|
||
before_date: Bu tarihten önceki maçlara bak (ms timestamp)
|
||
limit: Kaç maç analiz edilecek
|
||
max_days: Maksimum kaç gün geriye gidilecek
|
||
|
||
Returns:
|
||
Dict: Team stats feature'ları
|
||
"""
|
||
if not team_id or len(team_id) < 5:
|
||
return self._default_features()
|
||
|
||
try:
|
||
conn = self.get_conn()
|
||
cur = conn.cursor()
|
||
|
||
min_date = before_date - (max_days * 24 * 60 * 60 * 1000)
|
||
|
||
# Bu takımın son N maçındaki istatistikleri çek
|
||
cur.execute("""
|
||
SELECT
|
||
mts.possession_percentage,
|
||
mts.shots_on_target,
|
||
mts.shots_off_target,
|
||
mts.total_shots,
|
||
mts.corners,
|
||
mts.fouls,
|
||
m.score_home,
|
||
m.score_away,
|
||
m.home_team_id
|
||
FROM football_team_stats mts
|
||
JOIN matches m ON mts.match_id = m.id
|
||
WHERE mts.team_id = %s
|
||
AND m.mst_utc < %s
|
||
AND m.mst_utc > %s
|
||
AND m.score_home IS NOT NULL
|
||
AND m.sport = 'football'
|
||
ORDER BY m.mst_utc DESC
|
||
LIMIT %s
|
||
""", (team_id, before_date, min_date, limit))
|
||
|
||
stats = cur.fetchall()
|
||
|
||
if not stats:
|
||
return self._default_features()
|
||
|
||
# İstatistikleri hesapla
|
||
total_matches = len(stats)
|
||
|
||
possession_sum = 0
|
||
shots_on_target_sum = 0
|
||
shots_total_sum = 0
|
||
corners_sum = 0
|
||
fouls_sum = 0
|
||
goals_scored = 0
|
||
valid_possession_count = 0
|
||
|
||
for stat in stats:
|
||
poss, sot, soff, total_shots, corners, fouls, sh, sa, home_id = stat
|
||
|
||
if poss and poss > 0:
|
||
possession_sum += poss
|
||
valid_possession_count += 1
|
||
|
||
if sot:
|
||
shots_on_target_sum += sot
|
||
if total_shots:
|
||
shots_total_sum += total_shots
|
||
if corners:
|
||
corners_sum += corners
|
||
if fouls:
|
||
fouls_sum += fouls
|
||
|
||
# Gol hesaplama
|
||
is_home = (home_id == team_id)
|
||
goals_scored += sh if is_home else sa
|
||
|
||
avg_possession = possession_sum / valid_possession_count if valid_possession_count > 0 else 50.0
|
||
avg_shots_on_target = shots_on_target_sum / total_matches if total_matches > 0 else 3.0
|
||
avg_shots_total = shots_total_sum / total_matches if total_matches > 0 else 10.0
|
||
avg_corners = corners_sum / total_matches if total_matches > 0 else 4.0
|
||
avg_fouls = fouls_sum / total_matches if total_matches > 0 else 12.0
|
||
|
||
# Shot conversion rate (xG benzeri)
|
||
shot_conversion = goals_scored / shots_total_sum if shots_total_sum > 0 else 0.1
|
||
|
||
# Shot accuracy
|
||
shot_accuracy = shots_on_target_sum / shots_total_sum if shots_total_sum > 0 else 0.35
|
||
|
||
return {
|
||
'avg_possession': avg_possession / 100, # Normalize to 0-1
|
||
'avg_shots_on_target': avg_shots_on_target,
|
||
'avg_shots_total': avg_shots_total,
|
||
'avg_corners': avg_corners,
|
||
'avg_fouls': avg_fouls,
|
||
'shot_conversion_rate': shot_conversion,
|
||
'shot_accuracy': shot_accuracy,
|
||
'attacking_intensity': (avg_shots_total + avg_corners) / 2
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"[TeamStatsEngine] Error: {e}")
|
||
return self._default_features()
|
||
|
||
def _default_features(self) -> Dict[str, float]:
|
||
return {
|
||
'avg_possession': 0.50,
|
||
'avg_shots_on_target': 3.5,
|
||
'avg_shots_total': 11.0,
|
||
'avg_corners': 4.5,
|
||
'avg_fouls': 12.0,
|
||
'shot_conversion_rate': 0.10,
|
||
'shot_accuracy': 0.35,
|
||
'attacking_intensity': 7.5
|
||
}
|
||
|
||
|
||
# Singleton
|
||
_engine = None
|
||
|
||
def get_team_stats_engine() -> TeamStatsEngine:
|
||
global _engine
|
||
if _engine is None:
|
||
_engine = TeamStatsEngine()
|
||
return _engine
|
||
|
||
|
||
if __name__ == "__main__":
|
||
engine = get_team_stats_engine()
|
||
|
||
print("\n🧪 Team Stats Engine Test")
|
||
print("=" * 50)
|
||
|
||
# Test için örnek takım ID'si al
|
||
conn = engine.get_conn()
|
||
cur = conn.cursor()
|
||
cur.execute("""
|
||
SELECT DISTINCT mts.team_id, t.name
|
||
FROM match_team_stats mts
|
||
JOIN teams t ON mts.team_id = t.id
|
||
LIMIT 1
|
||
""")
|
||
result = cur.fetchone()
|
||
|
||
if result:
|
||
team_id, team_name = result
|
||
print(f"Test Takımı: {team_name}")
|
||
|
||
import time
|
||
features = engine.get_features(team_id, int(time.time() * 1000))
|
||
|
||
print(f"\n📊 Feature'lar:")
|
||
for k, v in features.items():
|
||
print(f" {k}: {v:.3f}")
|