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
+582
View File
@@ -0,0 +1,582 @@
"""
Squad Analysis Engine - V9 Feature
Kadro ve oyuncu bazlı analiz.
Analiz Edilen Metrikler:
- İlk 11 kalitesi (golcü formu, key player)
- Yedek gücü
- Eksik oyuncu etkisi
- Pozisyon bazlı güç
- Takım içi golcü dağılımı
"""
import os
from typing import Dict, Optional, List, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from collections import defaultdict
try:
import psycopg2
from psycopg2.extras import RealDictCursor
except ImportError:
psycopg2 = None
@dataclass
class PlayerForm:
"""Oyuncu form bilgisi"""
player_id: str
player_name: str
goals_last_5: int = 0
assists_last_5: int = 0
minutes_last_5: int = 0
cards_last_5: int = 0
is_key_player: bool = False # Golcü veya sık oynayan
@dataclass
class SquadAnalysis:
"""Takım kadro analizi"""
team_id: str
team_name: str = ""
# İlk 11 bilgisi
starting_count: int = 0
sub_count: int = 0
total_squad: int = 0
# Pozisyon dağılımı
goalkeeper_count: int = 0
defender_count: int = 0
midfielder_count: int = 0
forward_count: int = 0
# Form metrikleri
total_goals_last_5: int = 0 # Kadrodaki oyuncuların son 5 maçtaki golleri
total_assists_last_5: int = 0
key_players_count: int = 0 # Golcü sayısı
key_player_missing: int = 0 # Eksik golcü
# Kalite metrikleri
avg_minutes_per_player: float = 0.0 # Ortalama oynama süresi
squad_experience: float = 0.0 # 0-1, takımla oynama deneyimi
rotation_rate: float = 0.0 # Kadro rotasyonu oranı
@dataclass
class SquadFeatures:
"""Model için kadro feature'ları"""
# Home team features
home_starting_11: int = 11
home_sub_count: int = 7
home_total_squad: int = 18
home_goalkeepers: int = 1
home_defenders: int = 4
home_midfielders: int = 4
home_forwards: int = 2
home_goals_last_5: int = 0
home_assists_last_5: int = 0
home_key_players: int = 0
home_squad_experience: float = 0.5
# Away team features
away_starting_11: int = 11
away_sub_count: int = 7
away_total_squad: int = 18
away_goalkeepers: int = 1
away_defenders: int = 4
away_midfielders: int = 4
away_forwards: int = 2
away_goals_last_5: int = 0
away_assists_last_5: int = 0
away_key_players: int = 0
away_squad_experience: float = 0.5
# Comparison features
squad_strength_diff: float = 0.0 # + = home stronger
goals_form_diff: float = 0.0
key_players_diff: int = 0
def to_dict(self) -> Dict[str, float]:
return {
# Home
'home_starting_11': float(self.home_starting_11),
'home_sub_count': float(self.home_sub_count),
'home_total_squad': float(self.home_total_squad),
'home_goalkeepers': float(self.home_goalkeepers),
'home_defenders': float(self.home_defenders),
'home_midfielders': float(self.home_midfielders),
'home_forwards': float(self.home_forwards),
'home_goals_last_5': float(self.home_goals_last_5),
'home_assists_last_5': float(self.home_assists_last_5),
'home_key_players': float(self.home_key_players),
'home_squad_experience': self.home_squad_experience,
# Away
'away_starting_11': float(self.away_starting_11),
'away_sub_count': float(self.away_sub_count),
'away_total_squad': float(self.away_total_squad),
'away_goalkeepers': float(self.away_goalkeepers),
'away_defenders': float(self.away_defenders),
'away_midfielders': float(self.away_midfielders),
'away_forwards': float(self.away_forwards),
'away_goals_last_5': float(self.away_goals_last_5),
'away_assists_last_5': float(self.away_assists_last_5),
'away_key_players': float(self.away_key_players),
'away_squad_experience': self.away_squad_experience,
# Diffs
'squad_strength_diff': self.squad_strength_diff,
'goals_form_diff': self.goals_form_diff,
'key_players_diff': float(self.key_players_diff),
}
class SquadAnalysisEngine:
"""
Kadro ve oyuncu analiz motoru.
Beşiktaş-Galatasaray maçı için:
- İlk 11'deki oyuncuların son 5 maçtaki gol/asist
- Key player tespiti (çok gol atan oyuncular)
- Pozisyon dağılımı (4-3-3, 4-4-2 vb.)
- Yedek kalitesi
hesaplar.
"""
# Pozisyon mapping
POSITION_MAP = {
'goalkeeper': 'GK',
'gk': 'GK',
'kaleci': 'GK',
'defender': 'DEF',
'def': 'DEF',
'defans': 'DEF',
'savunma': 'DEF',
'midfielder': 'MID',
'mid': 'MID',
'orta saha': 'MID',
'forward': 'FWD',
'fwd': 'FWD',
'forvet': 'FWD',
'striker': 'FWD',
}
def __init__(self):
self.conn = None
self._player_form_cache: Dict[str, PlayerForm] = {}
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"[SquadEngine] 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 _normalize_position(self, position: Optional[str]) -> str:
"""Pozisyonu normalize et"""
if not position:
return 'UNK'
pos_lower = position.lower().strip()
for key, val in self.POSITION_MAP.items():
if key in pos_lower:
return val
return 'UNK'
def get_player_form(self, player_id: str, before_date_ms: int = None) -> PlayerForm:
"""Oyuncunun son 5 maçtaki formunu hesapla"""
if player_id in self._player_form_cache:
return self._player_form_cache[player_id]
form = PlayerForm(player_id=player_id, player_name="")
conn = self.get_conn()
if conn is None:
return form
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Oyuncu adını al
cur.execute("SELECT name FROM players WHERE id = %s", (player_id,))
player_row = cur.fetchone()
if player_row:
form.player_name = player_row['name']
# Son 5 maçtaki gol ve asist
cur.execute("""
SELECT
COUNT(*) FILTER (WHERE event_type = 'goal' AND event_subtype NOT ILIKE '%%penaltı kaçırma%%') as goals,
COUNT(*) FILTER (WHERE event_type = 'goal' AND assist_player_id IS NOT NULL) as assists_given
FROM match_player_events
WHERE player_id = %s
AND match_id IN (
SELECT match_id FROM match_player_participation
WHERE player_id = %s
ORDER BY match_id DESC LIMIT 5
)
""", (player_id, player_id))
stats = cur.fetchone()
if stats:
form.goals_last_5 = stats['goals'] or 0
# Asist hesapla (assist_player_id olarak geçen)
cur.execute("""
SELECT COUNT(*) as assists
FROM match_player_events
WHERE assist_player_id = %s
AND match_id IN (
SELECT match_id FROM match_player_participation
WHERE player_id = %s
ORDER BY match_id DESC LIMIT 5
)
""", (player_id, player_id))
assist_row = cur.fetchone()
if assist_row:
form.assists_last_5 = assist_row['assists'] or 0
# Kart sayısı
cur.execute("""
SELECT COUNT(*) as cards
FROM match_player_events
WHERE player_id = %s AND event_type = 'card'
AND match_id IN (
SELECT match_id FROM match_player_participation
WHERE player_id = %s
ORDER BY match_id DESC LIMIT 5
)
""", (player_id, player_id))
card_row = cur.fetchone()
if card_row:
form.cards_last_5 = card_row['cards'] or 0
# Key player mi? (Son 10 maçta 3+ gol)
cur.execute("""
SELECT COUNT(*) as total_goals
FROM match_player_events
WHERE player_id = %s
AND event_type = 'goal'
AND event_subtype NOT ILIKE '%%penaltı kaçırma%%'
""", (player_id,))
total_row = cur.fetchone()
form.is_key_player = (total_row['total_goals'] or 0) >= 3
self._player_form_cache[player_id] = form
return form
except Exception as e:
import traceback
traceback.print_exc()
print(f"[SquadEngine] Error getting player form: {e}")
return form
def analyze_squad(self, match_id: str, team_id: str) -> SquadAnalysis:
"""Takımın maç kadrosunu analiz et"""
analysis = SquadAnalysis(team_id=team_id)
conn = self.get_conn()
if conn is None:
return analysis
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Takım adını al
cur.execute("SELECT name FROM teams WHERE id = %s", (team_id,))
team_row = cur.fetchone()
if team_row:
analysis.team_name = team_row['name']
# Maç kadrosunu al
cur.execute("""
SELECT player_id, position, is_starting
FROM match_player_participation
WHERE match_id = %s AND team_id = %s
""", (match_id, team_id))
players = cur.fetchall()
for p in players:
if p['is_starting']:
analysis.starting_count += 1
else:
analysis.sub_count += 1
pos = self._normalize_position(p['position'])
if pos == 'GK':
analysis.goalkeeper_count += 1
elif pos == 'DEF':
analysis.defender_count += 1
elif pos == 'MID':
analysis.midfielder_count += 1
elif pos == 'FWD':
analysis.forward_count += 1
# İlk 11'in formunu topluca hesapla
if p['is_starting']:
form = self.get_player_form(p['player_id'])
analysis.total_goals_last_5 += form.goals_last_5
analysis.total_assists_last_5 += form.assists_last_5
if form.is_key_player:
analysis.key_players_count += 1
analysis.total_squad = analysis.starting_count + analysis.sub_count
# Takım deneyimi (bu takımla kaç maç oynamışlar)
if analysis.starting_count > 0:
cur.execute("""
SELECT AVG(match_count) as avg_exp
FROM (
SELECT player_id, COUNT(*) as match_count
FROM match_player_participation
WHERE team_id = %s AND is_starting = true
GROUP BY player_id
) sub
""", (team_id,))
exp_row = cur.fetchone()
if exp_row and exp_row['avg_exp']:
# Normalize: 50+ maç = 1.0
analysis.squad_experience = min(exp_row['avg_exp'] / 50, 1.0)
return analysis
except Exception as e:
print(f"[SquadEngine] Error analyzing squad: {e}")
return analysis
def analyze_squad_from_list(self, player_ids: List[str], team_id: str) -> SquadAnalysis:
"""
Memory'deki oyuncu listesinden kadro analizi yap.
DB'de olmayan canlı maçlar için kullanılır.
"""
analysis = SquadAnalysis(team_id=team_id)
# Varsayılan: İlk 11 oyuncu (listede genellikle ilk 11 verilir)
# Eğer liste boşsa
if not player_ids:
return analysis
# Varsayımlar: Mackolik API'den gelen liste sıralıdır.
# İlk 11 genellikle as kadrodur. Ancak burada sadece 'starting' oyuncuları alıyoruz varsayalım.
# User calling uses explicit starting 11 list.
analysis.starting_count = len(player_ids)
analysis.total_squad = len(player_ids) # Subs unknown usually unless separate list
# Position tahmini zor, default dağıt? Veya oyuncu detayına git?
# Hız için: Oyuncu ID'sinden DB'ye bakıp pozisyon öğrenmeye çalışabiliriz.
conn = self.get_conn()
if conn is None:
return analysis
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Calculate stats for these specific players
for pid in player_ids:
# Get Form
form = self.get_player_form(pid)
analysis.total_goals_last_5 += form.goals_last_5
analysis.total_assists_last_5 += form.assists_last_5
if form.is_key_player:
analysis.key_players_count += 1
# Get Position/Exp history attempt
cur.execute("""
SELECT position, COUNT(*) as match_count
FROM match_player_participation
WHERE player_id = %s AND team_id = %s
GROUP BY position
ORDER BY match_count DESC LIMIT 1
""", (pid, team_id))
row = cur.fetchone()
if row:
pos = self._normalize_position(row.get('position', 'UNK'))
if pos == 'GK': analysis.goalkeeper_count += 1
elif pos == 'DEF': analysis.defender_count += 1
elif pos == 'MID': analysis.midfielder_count += 1
elif pos == 'FWD': analysis.forward_count += 1
# Experience contribution
exp = min(row['match_count'] / 50.0, 1.0)
analysis.squad_experience += exp
# Average experience
if analysis.starting_count > 0:
analysis.squad_experience /= analysis.starting_count
except Exception as e:
print(f"[SquadEngine] Live analyze error: {e}")
return analysis
def get_features(
self,
match_id: str,
home_team_id: str,
away_team_id: str
) -> Dict[str, float]:
"""
Maç için kadro feature'larını hesapla.
Args:
match_id: Maç ID'si
home_team_id: Ev sahibi takım ID
away_team_id: Deplasman takım ID
Returns:
Kadro feature'ları dict olarak
"""
features = SquadFeatures()
# Ev sahibi analizi
home = self.analyze_squad(match_id, home_team_id)
features.home_starting_11 = home.starting_count
features.home_sub_count = home.sub_count
features.home_total_squad = home.total_squad
features.home_goalkeepers = home.goalkeeper_count
features.home_defenders = home.defender_count
features.home_midfielders = home.midfielder_count
features.home_forwards = home.forward_count
features.home_goals_last_5 = home.total_goals_last_5
features.home_assists_last_5 = home.total_assists_last_5
features.home_key_players = home.key_players_count
features.home_squad_experience = home.squad_experience
# Deplasman analizi
away = self.analyze_squad(match_id, away_team_id)
features.away_starting_11 = away.starting_count
features.away_sub_count = away.sub_count
features.away_total_squad = away.total_squad
features.away_goalkeepers = away.goalkeeper_count
features.away_defenders = away.defender_count
features.away_midfielders = away.midfielder_count
features.away_forwards = away.forward_count
features.away_goals_last_5 = away.total_goals_last_5
features.away_assists_last_5 = away.total_assists_last_5
features.away_key_players = away.key_players_count
features.away_squad_experience = away.squad_experience
# Karşılaştırma feature'ları
home_strength = (
home.total_goals_last_5 * 2 +
home.total_assists_last_5 +
home.key_players_count * 3 +
home.squad_experience * 10
)
away_strength = (
away.total_goals_last_5 * 2 +
away.total_assists_last_5 +
away.key_players_count * 3 +
away.squad_experience * 10
)
features.squad_strength_diff = home_strength - away_strength
features.goals_form_diff = home.total_goals_last_5 - away.total_goals_last_5
features.key_players_diff = home.key_players_count - away.key_players_count
return features.to_dict()
def get_features_without_match(
self,
home_team_id: str,
away_team_id: str
) -> Dict[str, float]:
"""
Maç ID olmadan takım bazlı feature'ları hesapla.
Son maçtaki kadroyu referans alır.
"""
features = SquadFeatures()
conn = self.get_conn()
if conn is None:
return features.to_dict()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
for team_id, prefix in [(home_team_id, 'home'), (away_team_id, 'away')]:
# Son maçı bul
cur.execute("""
SELECT mpp.match_id
FROM match_player_participation mpp
JOIN matches m ON mpp.match_id = m.id
WHERE mpp.team_id = %s
ORDER BY m.mst_utc DESC
LIMIT 1
""", (team_id,))
row = cur.fetchone()
if row:
analysis = self.analyze_squad(row['match_id'], team_id)
if prefix == 'home':
features.home_starting_11 = analysis.starting_count
features.home_sub_count = analysis.sub_count
features.home_total_squad = analysis.total_squad
features.home_goals_last_5 = analysis.total_goals_last_5
features.home_assists_last_5 = analysis.total_assists_last_5
features.home_key_players = analysis.key_players_count
features.home_squad_experience = analysis.squad_experience
else:
features.away_starting_11 = analysis.starting_count
features.away_sub_count = analysis.sub_count
features.away_total_squad = analysis.total_squad
features.away_goals_last_5 = analysis.total_goals_last_5
features.away_assists_last_5 = analysis.total_assists_last_5
features.away_key_players = analysis.key_players_count
features.away_squad_experience = analysis.squad_experience
# Karşılaştırma
features.goals_form_diff = features.home_goals_last_5 - features.away_goals_last_5
features.key_players_diff = features.home_key_players - features.away_key_players
return features.to_dict()
except Exception as e:
print(f"[SquadEngine] Error: {e}")
return features.to_dict()
# Singleton instance
_engine: Optional[SquadAnalysisEngine] = None
def get_squad_analysis_engine() -> SquadAnalysisEngine:
"""Singleton squad analysis engine instance döndür"""
global _engine
if _engine is None:
_engine = SquadAnalysisEngine()
return _engine
if __name__ == "__main__":
# Test
engine = get_squad_analysis_engine()
print("\n🧪 Squad Analysis Engine Test")
print("=" * 50)
# Test with known team IDs (Galatasaray, Fenerbahce)
features = engine.get_features_without_match(
home_team_id="test_gs",
away_team_id="test_fb"
)
print("\n📊 Features:")
for key, value in features.items():
print(f" {key}: {value:.2f}")