"""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