372 lines
11 KiB
Python
Executable File
372 lines
11 KiB
Python
Executable File
"""
|
||
Poisson Engine - Matematiksel Gol Modeli
|
||
V9 Model için Poisson dağılımı ile gol olasılıkları hesaplar.
|
||
|
||
Özellikler:
|
||
1. Exact score olasılıkları (0-0, 1-0, 1-1, 2-1, vb.)
|
||
2. Over/Under olasılıkları (matematiksel)
|
||
3. BTTS (Karşılıklı Gol) olasılıkları
|
||
4. Expected Goals (xG) tahmini
|
||
"""
|
||
|
||
import math
|
||
from typing import Dict, Tuple, Optional
|
||
from dataclasses import dataclass, field
|
||
|
||
|
||
def poisson_prob(lam: float, k: int) -> float:
|
||
"""
|
||
Poisson olasılık formülü.
|
||
P(X = k) = (λ^k * e^(-λ)) / k!
|
||
"""
|
||
if lam <= 0:
|
||
return 1.0 if k == 0 else 0.0
|
||
return (math.pow(lam, k) * math.exp(-lam)) / math.factorial(k)
|
||
|
||
|
||
@dataclass
|
||
class PoissonPrediction:
|
||
"""Poisson tahmin sonuçları"""
|
||
home_xg: float = 0.0 # Ev sahibi beklenen gol
|
||
away_xg: float = 0.0 # Deplasman beklenen gol
|
||
total_xg: float = 0.0 # Toplam beklenen gol
|
||
|
||
# Maç sonucu olasılıkları
|
||
home_win_prob: float = 0.0
|
||
draw_prob: float = 0.0
|
||
away_win_prob: float = 0.0
|
||
|
||
# Alt/Üst olasılıkları
|
||
over_15_prob: float = 0.0
|
||
over_25_prob: float = 0.0
|
||
over_35_prob: float = 0.0
|
||
under_15_prob: float = 0.0
|
||
under_25_prob: float = 0.0
|
||
under_35_prob: float = 0.0
|
||
|
||
# BTTS
|
||
btts_yes_prob: float = 0.0
|
||
btts_no_prob: float = 0.0
|
||
|
||
# En olası skorlar
|
||
most_likely_scores: list = field(default_factory=list)
|
||
|
||
|
||
class PoissonEngine:
|
||
"""
|
||
Poisson dağılımı ile gol olasılıkları hesaplar.
|
||
İstatistiksel bir yaklaşım - machine learning'den bağımsız.
|
||
"""
|
||
|
||
# Lig bazlı ortalama gol verileri (varsayılan değerler)
|
||
DEFAULT_HOME_XG = 1.45
|
||
DEFAULT_AWAY_XG = 1.15
|
||
DEFAULT_LEAGUE_AVG = 2.60
|
||
|
||
def __init__(self):
|
||
self.max_goals = 7 # Hesaplama için maksimum gol sayısı
|
||
|
||
def calculate_xg(
|
||
self,
|
||
home_goals_avg: float,
|
||
home_conceded_avg: float,
|
||
away_goals_avg: float,
|
||
away_conceded_avg: float,
|
||
league_home_avg: float = None,
|
||
league_away_avg: float = None,
|
||
league_total_avg: float = None
|
||
) -> Tuple[float, float]:
|
||
"""
|
||
Beklenen gol (xG) hesapla.
|
||
|
||
Attack strength * Defense weakness * League average
|
||
"""
|
||
# Varsayılan lig ortalamaları
|
||
if league_home_avg is None:
|
||
league_home_avg = self.DEFAULT_HOME_XG
|
||
if league_away_avg is None:
|
||
league_away_avg = self.DEFAULT_AWAY_XG
|
||
if league_total_avg is None:
|
||
league_total_avg = self.DEFAULT_LEAGUE_AVG
|
||
|
||
# Güç hesaplamaları
|
||
# Ev sahibi saldırı gücü = Ev gol ortalaması / Lig ev gol ortalaması
|
||
home_attack = home_goals_avg / league_home_avg if league_home_avg > 0 else 1.0
|
||
# Deplasman savunma zayıflığı = Deplasman yenilen gol / Lig deplasman yenilen
|
||
away_defense = away_conceded_avg / league_away_avg if league_away_avg > 0 else 1.0
|
||
|
||
# Deplasman saldırı gücü
|
||
away_attack = away_goals_avg / league_away_avg if league_away_avg > 0 else 1.0
|
||
# Ev sahibi savunma zayıflığı
|
||
home_defense = home_conceded_avg / league_home_avg if league_home_avg > 0 else 1.0
|
||
|
||
# Expected Goals
|
||
home_xg = home_attack * away_defense * league_home_avg
|
||
away_xg = away_attack * home_defense * league_away_avg
|
||
|
||
# Aşırı değerleri sınırla
|
||
home_xg = max(0.3, min(home_xg, 4.0))
|
||
away_xg = max(0.2, min(away_xg, 3.5))
|
||
|
||
return home_xg, away_xg
|
||
|
||
def calculate_score_matrix(
|
||
self,
|
||
home_xg: float,
|
||
away_xg: float
|
||
) -> Dict[Tuple[int, int], float]:
|
||
"""
|
||
Tüm skor kombinasyonlarının olasılıklarını hesapla.
|
||
|
||
Returns:
|
||
Dict[(home_goals, away_goals)] = probability
|
||
"""
|
||
matrix = {}
|
||
|
||
for home_goals in range(self.max_goals + 1):
|
||
for away_goals in range(self.max_goals + 1):
|
||
prob = poisson_prob(home_xg, home_goals) * poisson_prob(away_xg, away_goals)
|
||
matrix[(home_goals, away_goals)] = prob
|
||
|
||
return matrix
|
||
|
||
def calculate_match_odds(
|
||
self,
|
||
home_xg: float,
|
||
away_xg: float
|
||
) -> Tuple[float, float, float]:
|
||
"""
|
||
1X2 olasılıklarını hesapla.
|
||
|
||
Returns:
|
||
(home_win, draw, away_win) probabilities
|
||
"""
|
||
matrix = self.calculate_score_matrix(home_xg, away_xg)
|
||
|
||
home_win = 0.0
|
||
draw = 0.0
|
||
away_win = 0.0
|
||
|
||
for (h, a), prob in matrix.items():
|
||
if h > a:
|
||
home_win += prob
|
||
elif h == a:
|
||
draw += prob
|
||
else:
|
||
away_win += prob
|
||
|
||
# Normalize (toplam 1 olmalı)
|
||
total = home_win + draw + away_win
|
||
if total > 0:
|
||
home_win /= total
|
||
draw /= total
|
||
away_win /= total
|
||
|
||
return home_win, draw, away_win
|
||
|
||
def calculate_over_under(
|
||
self,
|
||
home_xg: float,
|
||
away_xg: float
|
||
) -> Dict[str, float]:
|
||
"""
|
||
Alt/Üst olasılıklarını hesapla.
|
||
"""
|
||
matrix = self.calculate_score_matrix(home_xg, away_xg)
|
||
|
||
over_15 = 0.0
|
||
over_25 = 0.0
|
||
over_35 = 0.0
|
||
|
||
for (h, a), prob in matrix.items():
|
||
total = h + a
|
||
if total > 1.5:
|
||
over_15 += prob
|
||
if total > 2.5:
|
||
over_25 += prob
|
||
if total > 3.5:
|
||
over_35 += prob
|
||
|
||
return {
|
||
"over_15": over_15,
|
||
"over_25": over_25,
|
||
"over_35": over_35,
|
||
"under_15": 1 - over_15,
|
||
"under_25": 1 - over_25,
|
||
"under_35": 1 - over_35,
|
||
}
|
||
|
||
def calculate_btts(
|
||
self,
|
||
home_xg: float,
|
||
away_xg: float
|
||
) -> Tuple[float, float]:
|
||
"""
|
||
Karşılıklı Gol (Both Teams To Score) olasılığı.
|
||
"""
|
||
# P(Home scores at least 1) = 1 - P(Home scores 0)
|
||
home_scores = 1 - poisson_prob(home_xg, 0)
|
||
# P(Away scores at least 1) = 1 - P(Away scores 0)
|
||
away_scores = 1 - poisson_prob(away_xg, 0)
|
||
|
||
# P(BTTS) = P(Home scores) * P(Away scores)
|
||
btts_yes = home_scores * away_scores
|
||
btts_no = 1 - btts_yes
|
||
|
||
return btts_yes, btts_no
|
||
|
||
def get_most_likely_scores(
|
||
self,
|
||
home_xg: float,
|
||
away_xg: float,
|
||
top_n: int = 5
|
||
) -> list:
|
||
"""
|
||
En olası skorları getir.
|
||
"""
|
||
matrix = self.calculate_score_matrix(home_xg, away_xg)
|
||
|
||
# Olasılığa göre sırala
|
||
sorted_scores = sorted(matrix.items(), key=lambda x: x[1], reverse=True)
|
||
|
||
return [
|
||
{"score": f"{h}-{a}", "probability": round(prob * 100, 1)}
|
||
for (h, a), prob in sorted_scores[:top_n]
|
||
]
|
||
|
||
def predict(
|
||
self,
|
||
home_goals_avg: float,
|
||
home_conceded_avg: float,
|
||
away_goals_avg: float,
|
||
away_conceded_avg: float,
|
||
league_home_avg: float = None,
|
||
league_away_avg: float = None,
|
||
league_total_avg: float = None
|
||
) -> PoissonPrediction:
|
||
"""
|
||
Tam Poisson tahmini.
|
||
"""
|
||
prediction = PoissonPrediction()
|
||
|
||
# 1. xG hesapla
|
||
home_xg, away_xg = self.calculate_xg(
|
||
home_goals_avg, home_conceded_avg,
|
||
away_goals_avg, away_conceded_avg,
|
||
league_home_avg, league_away_avg, league_total_avg
|
||
)
|
||
|
||
prediction.home_xg = round(home_xg, 2)
|
||
prediction.away_xg = round(away_xg, 2)
|
||
prediction.total_xg = round(home_xg + away_xg, 2)
|
||
|
||
# 2. Maç sonucu
|
||
hw, d, aw = self.calculate_match_odds(home_xg, away_xg)
|
||
prediction.home_win_prob = round(hw, 3)
|
||
prediction.draw_prob = round(d, 3)
|
||
prediction.away_win_prob = round(aw, 3)
|
||
|
||
# 3. Alt/Üst
|
||
ou = self.calculate_over_under(home_xg, away_xg)
|
||
prediction.over_15_prob = round(ou["over_15"], 3)
|
||
prediction.over_25_prob = round(ou["over_25"], 3)
|
||
prediction.over_35_prob = round(ou["over_35"], 3)
|
||
prediction.under_15_prob = round(ou["under_15"], 3)
|
||
prediction.under_25_prob = round(ou["under_25"], 3)
|
||
prediction.under_35_prob = round(ou["under_35"], 3)
|
||
|
||
# 4. BTTS
|
||
btts_yes, btts_no = self.calculate_btts(home_xg, away_xg)
|
||
prediction.btts_yes_prob = round(btts_yes, 3)
|
||
prediction.btts_no_prob = round(btts_no, 3)
|
||
|
||
# 5. En olası skorlar
|
||
prediction.most_likely_scores = self.get_most_likely_scores(home_xg, away_xg)
|
||
|
||
return prediction
|
||
|
||
def get_features(
|
||
self,
|
||
home_goals_avg: float,
|
||
home_conceded_avg: float,
|
||
away_goals_avg: float,
|
||
away_conceded_avg: float,
|
||
league_home_avg: float = None,
|
||
league_away_avg: float = None,
|
||
league_total_avg: float = None
|
||
) -> Dict[str, float]:
|
||
"""
|
||
Model için feature dict.
|
||
"""
|
||
pred = self.predict(
|
||
home_goals_avg, home_conceded_avg,
|
||
away_goals_avg, away_conceded_avg,
|
||
league_home_avg, league_away_avg, league_total_avg
|
||
)
|
||
|
||
return {
|
||
"poisson_home_xg": pred.home_xg,
|
||
"poisson_away_xg": pred.away_xg,
|
||
"poisson_total_xg": pred.total_xg,
|
||
"poisson_home_win": pred.home_win_prob,
|
||
"poisson_draw": pred.draw_prob,
|
||
"poisson_away_win": pred.away_win_prob,
|
||
"poisson_over_15": pred.over_15_prob,
|
||
"poisson_over_25": pred.over_25_prob,
|
||
"poisson_over_35": pred.over_35_prob,
|
||
"poisson_btts_yes": pred.btts_yes_prob,
|
||
}
|
||
|
||
|
||
# Singleton
|
||
_engine_instance = None
|
||
|
||
def get_poisson_engine() -> PoissonEngine:
|
||
"""Singleton pattern"""
|
||
global _engine_instance
|
||
if _engine_instance is None:
|
||
_engine_instance = PoissonEngine()
|
||
return _engine_instance
|
||
|
||
|
||
# Test
|
||
if __name__ == "__main__":
|
||
engine = get_poisson_engine()
|
||
|
||
# Örnek: Güçlü ev sahibi vs zayıf deplasman
|
||
print("=" * 60)
|
||
print("POISSON ENGINE TEST")
|
||
print("Galatasaray (ev) vs Antalyaspor (deplasman)")
|
||
print("=" * 60)
|
||
|
||
pred = engine.predict(
|
||
home_goals_avg=2.1, # GS ev ortalaması
|
||
home_conceded_avg=0.8, # GS ev yenilen
|
||
away_goals_avg=0.9, # Antalya deplasman gol
|
||
away_conceded_avg=1.8, # Antalya deplasman yenilen
|
||
league_home_avg=1.5,
|
||
league_away_avg=1.1
|
||
)
|
||
|
||
print(f"\n📊 Expected Goals:")
|
||
print(f" Ev Sahibi xG: {pred.home_xg}")
|
||
print(f" Deplasman xG: {pred.away_xg}")
|
||
print(f" Toplam xG: {pred.total_xg}")
|
||
|
||
print(f"\n🎯 Maç Sonucu:")
|
||
print(f" 1 (Ev): {pred.home_win_prob*100:.1f}%")
|
||
print(f" X (Beraberlik): {pred.draw_prob*100:.1f}%")
|
||
print(f" 2 (Deplasman): {pred.away_win_prob*100:.1f}%")
|
||
|
||
print(f"\n⚽ Alt/Üst:")
|
||
print(f" 2.5 Üst: {pred.over_25_prob*100:.1f}%")
|
||
print(f" 2.5 Alt: {pred.under_25_prob*100:.1f}%")
|
||
|
||
print(f"\n🤝 Karşılıklı Gol:")
|
||
print(f" KG Var: {pred.btts_yes_prob*100:.1f}%")
|
||
print(f" KG Yok: {pred.btts_no_prob*100:.1f}%")
|
||
|
||
print(f"\n📈 En Olası Skorlar:")
|
||
for score_data in pred.most_likely_scores:
|
||
print(f" {score_data['score']}: {score_data['probability']}%")
|