Syntrex commited on
Commit
b149c7a
·
verified ·
1 Parent(s): c57cbf4

Create pitcher_live_state_v2.py

Browse files
Files changed (1) hide show
  1. models/pitcher_live_state_v2.py +310 -0
models/pitcher_live_state_v2.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def _safe_float(value: Any, default: float | None = None) -> float | None:
7
+ try:
8
+ if value is None:
9
+ return default
10
+ text = str(value).strip().lower()
11
+ if text in {"", "nan", "none"}:
12
+ return default
13
+ return float(value)
14
+ except Exception:
15
+ return default
16
+
17
+
18
+ def _safe_int(value: Any, default: int = 0) -> int:
19
+ try:
20
+ if value is None:
21
+ return default
22
+ text = str(value).strip().lower()
23
+ if text in {"", "nan", "none"}:
24
+ return default
25
+ return int(float(value))
26
+ except Exception:
27
+ return default
28
+
29
+
30
+ def _clamp(value: float, low: float, high: float) -> float:
31
+ return max(low, min(high, value))
32
+
33
+
34
+ def build_pitcher_live_state_v2(
35
+ pitcher_row: dict[str, Any],
36
+ game_row: dict[str, Any],
37
+ context: dict[str, Any] | None = None,
38
+ ) -> dict[str, Any]:
39
+ """
40
+ Strongest Phase 6 live-state scorer.
41
+
42
+ Produces:
43
+ - drift metrics
44
+ - fatigue / degradation score
45
+ - trust-live score
46
+ - adaptive baseline/live weights
47
+ """
48
+
49
+ context = context or {}
50
+
51
+ # --- baseline profile ---
52
+ avg_release_speed = _safe_float(pitcher_row.get("avg_release_speed"))
53
+ avg_release_spin_rate = _safe_float(pitcher_row.get("avg_release_spin_rate"))
54
+ avg_release_extension = _safe_float(pitcher_row.get("avg_release_extension"))
55
+ avg_pfx_x = _safe_float(pitcher_row.get("avg_pfx_x"))
56
+ avg_pfx_z = _safe_float(pitcher_row.get("avg_pfx_z"))
57
+
58
+ ev_allowed = _safe_float(pitcher_row.get("ev_allowed"))
59
+ hard_hit_rate_allowed = _safe_float(pitcher_row.get("hard_hit_rate_allowed"))
60
+ barrel_rate_allowed = _safe_float(pitcher_row.get("barrel_rate_allowed"))
61
+
62
+ # --- live telemetry ---
63
+ live_velocity = _safe_float(game_row.get("pitch_velocity"))
64
+ live_spin = _safe_float(game_row.get("pitch_spin_rate"))
65
+ live_extension = _safe_float(game_row.get("pitch_extension"))
66
+ live_pfx_x = _safe_float(game_row.get("pitch_pfx_x"))
67
+ live_pfx_z = _safe_float(game_row.get("pitch_pfx_z"))
68
+
69
+ # --- live baseball context ---
70
+ outs = _safe_int(game_row.get("outs"), 0)
71
+ balls = _safe_int(game_row.get("balls"), 0)
72
+ strikes = _safe_int(game_row.get("strikes"), 0)
73
+
74
+ bullpen_entry_prob = _safe_float(context.get("bullpen_entry_prob"), 0.0) or 0.0
75
+ starter_stays_next_batter_prob = _safe_float(context.get("starter_stays_next_batter_prob"), 1.0) or 1.0
76
+ starter_stays_next_inning_prob = _safe_float(context.get("starter_stays_next_inning_prob"), 1.0) or 1.0
77
+
78
+ pitch_count = _safe_int(game_row.get("pitch_count"), 0)
79
+
80
+ # if no real pitch count is present, build a weak proxy from inning/outs/count
81
+ if pitch_count <= 0:
82
+ inning_number = _safe_int(game_row.get("inning"), 1)
83
+ pitch_count = max(0, (inning_number - 1) * 15 + outs * 5 + balls + strikes)
84
+
85
+ times_through_order = 1
86
+ if pitch_count >= 75:
87
+ times_through_order = 3
88
+ elif pitch_count >= 35:
89
+ times_through_order = 2
90
+
91
+ # --- drift calculations ---
92
+ velo_delta = None
93
+ if avg_release_speed is not None and live_velocity is not None:
94
+ velo_delta = live_velocity - avg_release_speed
95
+
96
+ spin_delta = None
97
+ if avg_release_spin_rate is not None and live_spin is not None:
98
+ spin_delta = live_spin - avg_release_spin_rate
99
+
100
+ extension_delta = None
101
+ if avg_release_extension is not None and live_extension is not None:
102
+ extension_delta = live_extension - avg_release_extension
103
+
104
+ movement_delta_x = None
105
+ if avg_pfx_x is not None and live_pfx_x is not None:
106
+ movement_delta_x = live_pfx_x - avg_pfx_x
107
+
108
+ movement_delta_z = None
109
+ if avg_pfx_z is not None and live_pfx_z is not None:
110
+ movement_delta_z = live_pfx_z - avg_pfx_z
111
+
112
+ # --- count-profile tendency proxy ---
113
+ # Current exact count is only for current batter.
114
+ # But repeated pitcher-state patterns can be approximated from live count stress.
115
+ ahead_indicator = 1.0 if strikes >= 2 and balls <= 1 else 0.0
116
+ behind_indicator = 1.0 if balls >= 2 and strikes <= 1 else 0.0
117
+ full_count_indicator = 1.0 if balls == 3 and strikes == 2 else 0.0
118
+
119
+ # --- evidence quality ---
120
+ evidence_quality_score = 0.0
121
+ reason_tags: list[str] = []
122
+
123
+ signal_count = 0
124
+
125
+ if velo_delta is not None:
126
+ signal_count += 1
127
+ if abs(velo_delta) >= 0.8:
128
+ evidence_quality_score += 0.18
129
+ if abs(velo_delta) >= 1.4:
130
+ evidence_quality_score += 0.10
131
+
132
+ if spin_delta is not None:
133
+ signal_count += 1
134
+ if abs(spin_delta) >= 100:
135
+ evidence_quality_score += 0.16
136
+ if abs(spin_delta) >= 175:
137
+ evidence_quality_score += 0.08
138
+
139
+ if extension_delta is not None:
140
+ signal_count += 1
141
+ if abs(extension_delta) >= 0.20:
142
+ evidence_quality_score += 0.12
143
+ if abs(extension_delta) >= 0.35:
144
+ evidence_quality_score += 0.06
145
+
146
+ if movement_delta_x is not None and abs(movement_delta_x) >= 2.0:
147
+ signal_count += 1
148
+ evidence_quality_score += 0.06
149
+
150
+ if movement_delta_z is not None and abs(movement_delta_z) >= 2.0:
151
+ signal_count += 1
152
+ evidence_quality_score += 0.06
153
+
154
+ if signal_count >= 3:
155
+ evidence_quality_score += 0.10
156
+
157
+ # pitch count / TTO make live evidence more trustworthy
158
+ if pitch_count >= 35:
159
+ evidence_quality_score += 0.08
160
+ if pitch_count >= 70:
161
+ evidence_quality_score += 0.10
162
+ if times_through_order >= 3:
163
+ evidence_quality_score += 0.10
164
+
165
+ evidence_quality_score = _clamp(evidence_quality_score, 0.0, 1.0)
166
+
167
+ # --- drift persistence proxy ---
168
+ # Until true rolling pitch memory exists, use multi-signal agreement + fatigue context.
169
+ drift_persistence_score = 0.0
170
+
171
+ negative_drift_signals = 0
172
+
173
+ if velo_delta is not None and velo_delta <= -0.8:
174
+ negative_drift_signals += 1
175
+ if spin_delta is not None and spin_delta <= -100:
176
+ negative_drift_signals += 1
177
+ if extension_delta is not None and extension_delta <= -0.20:
178
+ negative_drift_signals += 1
179
+ if movement_delta_z is not None and movement_delta_z <= -1.5:
180
+ negative_drift_signals += 1
181
+
182
+ drift_persistence_score += 0.15 * negative_drift_signals
183
+
184
+ if negative_drift_signals >= 2:
185
+ drift_persistence_score += 0.15
186
+ if negative_drift_signals >= 3:
187
+ drift_persistence_score += 0.10
188
+
189
+ if pitch_count >= 70:
190
+ drift_persistence_score += 0.10
191
+ if times_through_order >= 3:
192
+ drift_persistence_score += 0.10
193
+
194
+ drift_persistence_score = _clamp(drift_persistence_score, 0.0, 1.0)
195
+
196
+ # --- fatigue score ---
197
+ fatigue_score = 0.0
198
+
199
+ if pitch_count >= 35:
200
+ fatigue_score += 0.10
201
+ if pitch_count >= 60:
202
+ fatigue_score += 0.15
203
+ if pitch_count >= 85:
204
+ fatigue_score += 0.20
205
+
206
+ if times_through_order >= 2:
207
+ fatigue_score += 0.08
208
+ if times_through_order >= 3:
209
+ fatigue_score += 0.12
210
+
211
+ if velo_delta is not None and velo_delta <= -0.8:
212
+ fatigue_score += 0.12
213
+ reason_tags.append("velo_drop")
214
+ if velo_delta is not None and velo_delta <= -1.5:
215
+ fatigue_score += 0.10
216
+
217
+ if spin_delta is not None and spin_delta <= -100:
218
+ fatigue_score += 0.08
219
+ reason_tags.append("spin_drop")
220
+ if spin_delta is not None and spin_delta <= -180:
221
+ fatigue_score += 0.08
222
+
223
+ if extension_delta is not None and extension_delta <= -0.20:
224
+ fatigue_score += 0.06
225
+ reason_tags.append("extension_drop")
226
+ if extension_delta is not None and extension_delta <= -0.35:
227
+ fatigue_score += 0.05
228
+
229
+ if bullpen_entry_prob >= 0.40:
230
+ fatigue_score += 0.06
231
+ if bullpen_entry_prob >= 0.60:
232
+ fatigue_score += 0.06
233
+
234
+ fatigue_score = _clamp(fatigue_score, 0.0, 1.0)
235
+
236
+ # --- degradation score ---
237
+ degradation_score = 0.0
238
+
239
+ if ev_allowed is not None and ev_allowed >= 90.0:
240
+ degradation_score += 0.08
241
+ if ev_allowed is not None and ev_allowed >= 91.5:
242
+ degradation_score += 0.08
243
+
244
+ if hard_hit_rate_allowed is not None and hard_hit_rate_allowed >= 0.40:
245
+ degradation_score += 0.08
246
+ if hard_hit_rate_allowed is not None and hard_hit_rate_allowed >= 0.46:
247
+ degradation_score += 0.08
248
+
249
+ if barrel_rate_allowed is not None and barrel_rate_allowed >= 0.08:
250
+ degradation_score += 0.08
251
+ if barrel_rate_allowed is not None and barrel_rate_allowed >= 0.11:
252
+ degradation_score += 0.10
253
+
254
+ if behind_indicator >= 1.0:
255
+ degradation_score += 0.06
256
+ reason_tags.append("behind_in_count")
257
+ if full_count_indicator >= 1.0:
258
+ degradation_score += 0.06
259
+ reason_tags.append("full_count")
260
+
261
+ degradation_score += fatigue_score * 0.35
262
+ degradation_score = _clamp(degradation_score, 0.0, 1.0)
263
+
264
+ # --- trust-live score ---
265
+ trust_live_score = (
266
+ evidence_quality_score * 0.45
267
+ + drift_persistence_score * 0.30
268
+ + fatigue_score * 0.15
269
+ + degradation_score * 0.10
270
+ )
271
+ trust_live_score = _clamp(trust_live_score, 0.0, 1.0)
272
+
273
+ # --- adaptive blend regimes ---
274
+ if trust_live_score < 0.30:
275
+ baseline_weight = 0.80
276
+ live_weight = 0.20
277
+ elif trust_live_score < 0.55:
278
+ baseline_weight = 0.60
279
+ live_weight = 0.40
280
+ elif trust_live_score < 0.80:
281
+ baseline_weight = 0.40
282
+ live_weight = 0.60
283
+ else:
284
+ baseline_weight = 0.25
285
+ live_weight = 0.75
286
+
287
+ return {
288
+ "velo_delta": velo_delta,
289
+ "spin_delta": spin_delta,
290
+ "extension_delta": extension_delta,
291
+ "movement_delta_x": movement_delta_x,
292
+ "movement_delta_z": movement_delta_z,
293
+ "pitch_count": pitch_count,
294
+ "times_through_order": times_through_order,
295
+ "ahead_rate_live": ahead_indicator,
296
+ "behind_rate_live": behind_indicator,
297
+ "full_count_rate_live": full_count_indicator,
298
+ "first_pitch_strike_tendency_live": None,
299
+ "bullpen_entry_prob": bullpen_entry_prob,
300
+ "starter_stays_next_batter_prob": starter_stays_next_batter_prob,
301
+ "starter_stays_next_inning_prob": starter_stays_next_inning_prob,
302
+ "drift_persistence_score": drift_persistence_score,
303
+ "evidence_quality_score": evidence_quality_score,
304
+ "fatigue_score": fatigue_score,
305
+ "degradation_score": degradation_score,
306
+ "trust_live_score": trust_live_score,
307
+ "baseline_weight": baseline_weight,
308
+ "live_weight": live_weight,
309
+ "reason_tags": reason_tags[:6],
310
+ }