132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
"""裁判與天候條件量化模組。"""
|
||
|
||
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,
|
||
)
|