Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from typing import Any | |
| def _safe_float(value: Any, default: float | None = None) -> float | None: | |
| try: | |
| if value is None: | |
| return default | |
| text = str(value).strip().lower() | |
| if text in {"", "nan", "none"}: | |
| return default | |
| return float(value) | |
| except Exception: | |
| return default | |
| def _clamp(value: float, lo: float, hi: float) -> float: | |
| return max(lo, min(hi, value)) | |
| def _reliability(sample_size: Any, k: float = 120.0) -> float: | |
| sample = max(0.0, float(_safe_float(sample_size, 0.0) or 0.0)) | |
| return _clamp(sample / (sample + max(1.0, k)), 0.0, 1.0) | |
| def compute_zone_matchup_adjustment( | |
| batter_zone_row: dict[str, Any], | |
| pitcher_zone_row: dict[str, Any], | |
| *, | |
| pitch_zone_weights: dict[str, float] | None = None, | |
| handedness_context: dict[str, Any] | None = None, | |
| ) -> dict[str, float]: | |
| adjustment = { | |
| "hr_zone_boost": 0.0, | |
| "hit_zone_boost": 0.0, | |
| "tb2p_zone_boost": 0.0, | |
| "damage_zone_boost": 0.0, | |
| "whiff_zone_risk": 0.0, | |
| } | |
| pitch_families = ["fastball", "breaking", "offspeed"] | |
| zones = ["heart", "shadow", "chase", "waste"] | |
| total_weight = 0.0 | |
| weighted_samples = 0.0 | |
| context_weight_used = False | |
| handedness_context = handedness_context or {} | |
| handedness_bonus = 1.0 | |
| if str(handedness_context.get("batter_stand") or "").strip().upper() == str(handedness_context.get("pitcher_hand") or "").strip().upper(): | |
| handedness_bonus = 0.98 | |
| elif handedness_context: | |
| handedness_bonus = 1.02 | |
| for family in pitch_families: | |
| for zone in zones: | |
| base_attack_rate = _safe_float(pitcher_zone_row.get(f"{family}_{zone}_rate")) | |
| context_attack_rate = _safe_float((pitch_zone_weights or {}).get(f"{family}_{zone}")) | |
| sample_size = _safe_float(pitcher_zone_row.get(f"sample_size_{family}_{zone}"), 0.0) or 0.0 | |
| sample_reliability = _reliability(sample_size, k=80.0) | |
| if context_attack_rate is not None: | |
| context_weight_used = True | |
| if base_attack_rate is None: | |
| attack_rate = context_attack_rate | |
| else: | |
| attack_rate = (context_attack_rate * 0.72) + (base_attack_rate * 0.28 * sample_reliability) | |
| else: | |
| attack_rate = base_attack_rate | |
| if attack_rate is None or attack_rate <= 0: | |
| continue | |
| attack_rate *= handedness_bonus | |
| hr_prob = _safe_float(batter_zone_row.get(f"hr_prob_{family}_{zone}")) | |
| hit_prob = _safe_float(batter_zone_row.get(f"hit_prob_{family}_{zone}")) | |
| tb2p_prob = _safe_float(batter_zone_row.get(f"tb2p_prob_{family}_{zone}")) | |
| whiff_prob = _safe_float(batter_zone_row.get(f"whiff_prob_{family}_{zone}")) | |
| damage_prob = _safe_float(batter_zone_row.get(f"damage_prob_{family}_{zone}")) | |
| batter_sample = _safe_float(batter_zone_row.get(f"sample_size_{family}_{zone}"), 0.0) or 0.0 | |
| batter_reliability = _reliability(batter_sample, k=70.0) | |
| combined_reliability = sample_reliability * batter_reliability | |
| if hr_prob is not None: | |
| adjustment["hr_zone_boost"] += attack_rate * hr_prob * combined_reliability | |
| if hit_prob is not None: | |
| adjustment["hit_zone_boost"] += attack_rate * hit_prob * combined_reliability | |
| if tb2p_prob is not None: | |
| adjustment["tb2p_zone_boost"] += attack_rate * tb2p_prob * combined_reliability | |
| if damage_prob is not None: | |
| adjustment["damage_zone_boost"] += attack_rate * damage_prob * combined_reliability | |
| if whiff_prob is not None: | |
| adjustment["whiff_zone_risk"] += attack_rate * whiff_prob * combined_reliability | |
| total_weight += attack_rate | |
| weighted_samples += sample_size | |
| if total_weight > 0: | |
| for k in adjustment: | |
| adjustment[k] = adjustment[k] / total_weight | |
| adjustment["sample_size"] = weighted_samples if weighted_samples > 0 else total_weight | |
| adjustment["reliability"] = round(_reliability(weighted_samples, k=320.0), 4) | |
| adjustment["context_weight_used"] = context_weight_used | |
| return adjustment | |