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

80 lines
2.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.
"""球員道具盤Props蒙地卡羅模擬模組。"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class PlayerPropsDistribution:
shots: np.ndarray
shots_on_target: np.ndarray
passes: np.ndarray
def simulate_player_stats(
player_metrics: dict,
opponent_defense_metrics: dict,
iterations: int = 10_000,
) -> PlayerPropsDistribution:
"""快速模擬球員事件次數分佈。"""
if iterations <= 0:
raise ValueError('iterations 必須大於 0')
avg_touches = float(player_metrics.get('avg_touches', 45) or 0.0)
base_shot_rate = float(player_metrics.get('shots_per_touch', 0.08) or 0.0)
base_target_rate = float(player_metrics.get('shot_on_target_rate', 0.35) or 0.0)
base_pass_rate = float(player_metrics.get('passes_per_touch', 0.65) or 0.0)
opp_pressure = float(opponent_defense_metrics.get('pressing_index', 1.0) or 1.0)
opp_tackling = float(opponent_defense_metrics.get('marking_index', 1.0) or 1.0)
adj_touches = max(1.0, avg_touches * max(0.6, 1.0 / max(0.5, opp_pressure)))
shot_lambda = adj_touches * base_shot_rate
pass_lambda = adj_touches * base_pass_rate
rng = np.random.default_rng()
shots = rng.poisson(lam=shot_lambda, size=iterations)
passes = rng.poisson(lam=pass_lambda, size=iterations)
# 對方壓迫會降低射正率
effective_target_rate = max(0.02, base_target_rate / max(opp_tackling, 0.3))
shots_on_target = rng.binomial(shots, p=min(effective_target_rate, 0.99), size=iterations)
return PlayerPropsDistribution(shots=shots.astype(int), shots_on_target=shots_on_target.astype(int), passes=passes.astype(int))
def evaluate_prop_bet(
simulated_distribution: PlayerPropsDistribution,
line: float,
odds: float,
) -> dict[str, float | bool]:
"""從 10,000 次模擬結果計算超過盤口機率與 EV。"""
if odds <= 1:
raise ValueError('odds 必須大於 1')
if line < 0:
raise ValueError('line 必須大於等於 0')
shots = simulated_distribution.shots
if shots.size == 0:
raise ValueError('distribution 為空')
probability_over = float((shots > line).mean())
from .ev_calculator import calculate_expected_value
ev = calculate_expected_value(probability_over, odds)
return {
'metric': 'shots',
'line': line,
'over_probability': round(probability_over, 6),
'under_probability': round(1.0 - probability_over, 6),
'implied_ev': ev['ev_value'],
'ev_percentage': ev['ev_percentage'],
'is_value_bet': bool(ev['is_value_bet']),
}