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

113 lines
3.6 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.
"""量化投注引擎EV、泊松預測、海拔修正"""
from __future__ import annotations
import math
import numpy as np
import pandas as pd
from scipy.stats import poisson
def calculate_value_bet(true_prob: float, decimal_odds: float, *, stake: float = 1.0) -> tuple[float, bool]:
"""計算期望值EV並判斷是否屬於 Value Bet。
EV 計算EV = (勝率 * 利潤) - (敗率 * 本金)
其中利潤 = decimal_odds - 1。
Returns
-------
ev_pct: float
以本金為基底的 EV 百分比EV / stake
is_value_bet: bool
當 EV > 0.033%)回傳 True。
"""
prob = float(true_prob)
odds = float(decimal_odds)
if not 0 <= prob <= 1 or odds <= 1 or stake <= 0:
return 0.0, False
profit = odds - 1
ev = prob * profit - (1 - prob) * stake
ev_pct = ev / stake
return round(ev_pct, 6), ev_pct > 0.03
class PoissonPredictor:
"""球員進球分佈預測器2x2 進球建模)。"""
def __init__(
self,
home_attack: float,
home_defense: float,
away_attack: float,
away_defense: float,
league_avg_goals: float,
) -> None:
self.home_attack = float(home_attack)
self.home_defense = float(home_defense)
self.away_attack = float(away_attack)
self.away_defense = float(away_defense)
self.league_avg_goals = float(league_avg_goals)
# 以攻守乘積估算 λ,並限制在合理範圍避免極端值發散。
home_lambda = league_avg_goals * (self.home_attack / max(self.away_defense, 0.01))
away_lambda = league_avg_goals * (self.away_attack / max(self.home_defense, 0.01))
self.home_lambda = float(np.clip(home_lambda, 0.02, 6.5))
self.away_lambda = float(np.clip(away_lambda, 0.02, 6.5))
def predict_exact_score(self, home_goals: int, away_goals: int) -> float:
"""回傳指定波膽home_goals, away_goals發生機率。"""
p_home = poisson.pmf(home_goals, self.home_lambda)
p_away = poisson.pmf(away_goals, self.away_lambda)
return float(p_home * p_away)
def predict_over_under_prob(self, line: float = 2.5, max_goals: int = 10) -> tuple[float, float]:
"""回傳under, over機率。"""
goals = pd.MultiIndex.from_product(
[range(max_goals + 1), range(max_goals + 1)],
names=['home', 'away'],
).to_frame(index=False)
def joint_prob(r: pd.Series) -> float:
return float(poisson.pmf(r['home'], self.home_lambda) * poisson.pmf(r['away'], self.away_lambda))
probs = goals.apply(joint_prob, axis=1)
total_goals = goals['home'] + goals['away']
under = float(probs[total_goals <= line].sum())
over = float(probs[total_goals > line].sum())
return under, over
def adjust_away_defense_for_altitude(
base_defense_rating: float,
venue_altitude_meters: float,
*,
is_second_half: bool,
penalty_factor: float = 0.35,
) -> float:
"""高海拔下修正客隊防守能力。
當場地海拔高於 1500m 且處於下半場,套用對數懲罰,
代表客隊在氧氣濃度降低下體能下降導致防守效率衰退。
"""
base = float(base_defense_rating)
if venue_altitude_meters <= 1500 or not is_second_half:
return base
# 以 log(1 + altitude/1000) 做平滑遞增函式,避免低海拔時劇烈改變。
altitude_penalty = penalty_factor * math.log1p(venue_altitude_meters / 1000)
return base * (1 - min(max(altitude_penalty, 0), 0.45))
__all__ = [
'calculate_value_bet',
'PoissonPredictor',
'adjust_away_defense_for_altitude',
]