Files
2026FIFAWorldCup/platform/backend/app/analytics/poisson_model.py

114 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Poisson 分佈賽果預測模組。"""
from __future__ import annotations
import numpy as np
from scipy.stats import poisson
class PoissonMatchPredictor:
"""基於攻守強度的雙方進球機率預測器。"""
def __init__(
self,
home_attack_strength: float,
home_defense_strength: float,
away_attack_strength: float,
away_defense_strength: float,
league_avg_home_goals: float,
) -> None:
for value, name in [
(home_attack_strength, 'home_attack_strength'),
(home_defense_strength, 'home_defense_strength'),
(away_attack_strength, 'away_attack_strength'),
(away_defense_strength, 'away_defense_strength'),
(league_avg_home_goals, 'league_avg_home_goals'),
]:
if value <= 0:
raise ValueError(f'{name} 必須大於 0')
self.home_attack_strength = float(home_attack_strength)
self.home_defense_strength = float(home_defense_strength)
self.away_attack_strength = float(away_attack_strength)
self.away_defense_strength = float(away_defense_strength)
self.league_avg_home_goals = float(league_avg_home_goals)
def calculate_expected_goals(self) -> tuple[float, float]:
"""根據攻守強度與聯盟均值估算預期進球數(λ 值)。
使用比值校正避免極端值放大風險:
- 主隊 λ = 聯盟主場均值 × (主攻 / 客守)
- 客隊 λ = 聯盟客場均值 × (客攻 / 主守)
"""
league_avg_away_goals = self.league_avg_home_goals * 0.95
home_lambda = self.league_avg_home_goals * (self.home_attack_strength / self.away_defense_strength)
away_lambda = league_avg_away_goals * (self.away_attack_strength / self.home_defense_strength)
home_lambda = max(0.01, min(home_lambda, 8.0))
away_lambda = max(0.01, min(away_lambda, 8.0))
return home_lambda, away_lambda
def predict_exact_score_matrix(self, max_goals: int = 5) -> np.ndarray:
"""輸出 0~max_goals 間所有比分組合的機率矩陣。
回傳 shape = (max_goals+1, max_goals+1)
index [i,j] 代表主隊 i 球、客隊 j 球的機率。
"""
if max_goals < 0:
raise ValueError('max_goals 必須大於等於 0')
home_lambda, away_lambda = self.calculate_expected_goals()
goals = np.arange(max_goals + 1)
home_prob = poisson.pmf(goals, home_lambda)
away_prob = poisson.pmf(goals, away_lambda)
matrix = np.outer(home_prob, away_prob)
matrix = matrix.astype(float)
matrix /= matrix.sum() if matrix.sum() > 0 else 1.0
return matrix
def predict_1x2_probabilities(self) -> dict[str, float]:
"""由波膽矩陣匯總 1x2主勝/平/客勝)機率。"""
matrix = self.predict_exact_score_matrix(max_goals=8)
draw = float(np.trace(matrix))
home_win = float(np.tril(matrix, -1).sum())
away_win = float(np.triu(matrix, 1).sum())
total = home_win + draw + away_win
if total <= 0:
return {'home_win': 0.0, 'draw': 0.0, 'away_win': 0.0}
return {
'home_win': home_win / total,
'draw': draw / total,
'away_win': away_win / total,
}
def predict_over_under_prob(self, line: float = 2.5, max_goals: int = 8) -> tuple[float, float]:
"""回傳Under 機率, Over 機率)。"""
if line < 0:
raise ValueError('line 必須大於等於 0')
matrix = self.predict_exact_score_matrix(max_goals=max_goals)
goals = np.arange(max_goals + 1)
home, away = np.meshgrid(goals, goals)
total = home + away
under_mask = total <= line
under = float(matrix[under_mask].sum())
over = float(matrix[~under_mask].sum())
normalizer = under + over
if normalizer <= 0:
return 0.0, 0.0
return under / normalizer, over / normalizer