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

132 lines
3.8 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.
"""裁判與天候條件量化模組。"""
from __future__ import annotations
from dataclasses import dataclass
def calculate_referee_strictness_index(
avg_yellow_cards: float,
penalties_per_game: float,
) -> float:
"""裁判嚴厲度指標0-100"""
yellow = max(0.0, min(avg_yellow_cards, 8.0)) / 8.0
penalties = max(0.0, min(penalties_per_game, 2.5)) / 2.5
return round(yellow * 55 + penalties * 45, 4)
def detect_cards_pressure_signal(
strictness_index: float,
cards_ou_line: float,
) -> bool:
"""當裁判嚴格且莊家的卡數 O/U 開得偏低時,判斷為可能的逆風盤口。"""
return strictness_index >= 80 and cards_ou_line <= 4.5
def estimate_heat_index(ambient_temp_c: float, humidity_pct: float) -> float:
"""簡化的 Heat Index攝氏"""
t = max(-60.0, min(60.0, ambient_temp_c))
rh = max(0.0, min(100.0, humidity_pct))
hi = (
-8.784695
+ 1.61139411 * t
+ 2.338549 * rh
- 0.14611605 * t * rh
- 0.012308094 * t * t
- 0.016424828 * rh * rh
+ 0.002211732 * t * t * rh
+ 0.00072546 * t * rh * rh
- 0.000003582 * t * t * rh * rh
)
return round(max(0.0, hi), 4)
@dataclass(frozen=True)
class MatchConditionSignal:
strictness_index: float
heat_index: float
cards_pressure_alert: bool
cards_ou_line: float
second_half_home_attack: float
second_half_away_attack: float
second_half_under_recommendation: bool
attacker_direction: str
def adjust_attack_for_heat_and_altitude(
base_attack: float,
*,
heat_index: float,
is_second_half: bool,
venue_altitude_meters: float | None = None,
) -> float:
"""極端環境下的下半場攻擊效率修正。"""
if not is_second_half:
return round(float(base_attack), 6)
heat_penalty = max(0.0, heat_index - 28.0) / 120.0 # 每 1.2 度約降 1%
altitude_penalty = 0.0
if venue_altitude_meters and venue_altitude_meters > 1500:
altitude_penalty = min(0.22, (venue_altitude_meters - 1500) / 8000.0)
factor = max(0.6, 1 - heat_penalty - altitude_penalty)
return round(float(base_attack * factor), 6)
def evaluate_match_conditions(
*,
avg_yellow_cards: float,
penalties_per_game: float,
cards_ou_line: float,
temp_c: float,
humidity_pct: float,
venue_altitude_meters: int,
home_second_half_attack: float,
away_second_half_attack: float,
) -> MatchConditionSignal:
"""整合裁判與天候對下半場盤口與進攻效率的衝擊。"""
strictness_index = calculate_referee_strictness_index(avg_yellow_cards, penalties_per_game)
heat_index = estimate_heat_index(temp_c, humidity_pct)
adjusted_home = adjust_attack_for_heat_and_altitude(
home_second_half_attack,
heat_index=heat_index,
is_second_half=True,
venue_altitude_meters=venue_altitude_meters,
)
adjusted_away = adjust_attack_for_heat_and_altitude(
away_second_half_attack,
heat_index=heat_index,
is_second_half=True,
venue_altitude_meters=venue_altitude_meters,
)
cards_pressure = detect_cards_pressure_signal(strictness_index, cards_ou_line)
high_heat = heat_index >= 32.0
heat_pressure_delta = home_second_half_attack + away_second_half_attack
second_half_under = high_heat and (adjusted_home + adjusted_away) <= heat_pressure_delta * 0.95
if adjusted_home > adjusted_away:
attacker_direction = '上場勢優勢偏向主隊'
elif adjusted_home < adjusted_away:
attacker_direction = '上場勢優勢偏向客隊'
else:
attacker_direction = '攻勢對稱'
return MatchConditionSignal(
strictness_index=strictness_index,
heat_index=heat_index,
cards_pressure_alert=cards_pressure,
cards_ou_line=cards_ou_line,
second_half_home_attack=adjusted_home,
second_half_away_attack=adjusted_away,
second_half_under_recommendation=second_half_under,
attacker_direction=attacker_direction,
)