Syntrex commited on
Commit
cfd6eaf
·
verified ·
1 Parent(s): 7070f92

Update features/pitch_features.py

Browse files
Files changed (1) hide show
  1. features/pitch_features.py +30 -130
features/pitch_features.py CHANGED
@@ -1,138 +1,38 @@
1
  from __future__ import annotations
2
 
3
- from io import StringIO
4
- from typing import Any
5
-
6
  import pandas as pd
7
- import requests
8
-
9
- from config.settings import STATCAST_SEARCH_URL
10
-
11
-
12
- HEADERS = {
13
- "User-Agent": "Mozilla/5.0",
14
- "Accept-Language": "en-US,en;q=0.9",
15
- }
16
-
17
-
18
- def fetch_wbc_statcast_range(start_date: str, end_date: str, season: str = "2026") -> pd.DataFrame:
19
- """
20
- Pull WBC pitch/event-level Statcast-style CSV from Baseball Savant.
21
-
22
- Baseball Savant has a dedicated WBC search surface, but CSV exports are still served
23
- from the same csv backend path pattern. The key difference is using tournament filters.
24
- """
25
- params = {
26
- "all": "true",
27
- "hfPT": "",
28
- "hfAB": "",
29
- "hfBBT": "",
30
- "hfPR": "",
31
- "hfZ": "",
32
- "stadium": "",
33
- "hfBBL": "",
34
- "hfNewZones": "",
35
- "hfGT": "F|D|L|W|", # game types commonly used in savant filters
36
- "hfC": "",
37
- "hfSea": f"{season}|",
38
- "hfSit": "",
39
- "player_type": "batter",
40
- "hfOuts": "",
41
- "opponent": "",
42
- "pitcher_throws": "",
43
- "batter_stands": "",
44
- "hfSA": "",
45
- "game_date_gt": start_date,
46
- "game_date_lt": end_date,
47
- "team": "",
48
- "position": "",
49
- "hfRO": "",
50
- "home_road": "",
51
- "hfFlag": "",
52
- "metric_1": "",
53
- "hfInn": "",
54
- "min_pitches": "0",
55
- "min_results": "0",
56
- "group_by": "name",
57
- "sort_col": "pitches",
58
- "player_event_sort": "h_launch_speed",
59
- "sort_order": "desc",
60
- "min_abs": "0",
61
- "type": "details",
62
- }
63
-
64
- response = requests.get(STATCAST_SEARCH_URL, params=params, headers=HEADERS, timeout=60)
65
- response.raise_for_status()
66
 
67
- text = response.text.strip()
68
- if not text or text.startswith("<!DOCTYPE html"):
69
- return pd.DataFrame()
70
 
71
- try:
72
- df = pd.read_csv(StringIO(text))
73
- except Exception:
74
- return pd.DataFrame()
75
-
76
- return df
77
-
78
-
79
- def normalize_wbc_statcast(df: pd.DataFrame) -> pd.DataFrame:
80
  if df.empty:
81
- return df
82
-
83
- rename_map = {
84
- "player_name": "player_name",
85
- "pitch_type": "pitch_type",
86
- "release_speed": "release_speed",
87
- "release_spin_rate": "release_spin_rate",
88
- "pfx_x": "pfx_x",
89
- "pfx_z": "pfx_z",
90
- "release_pos_x": "release_pos_x",
91
- "release_pos_z": "release_pos_z",
92
- "plate_x": "plate_x",
93
- "plate_z": "plate_z",
94
- "launch_speed": "launch_speed",
95
- "launch_angle": "launch_angle",
96
- "estimated_ba_using_speedangle": "xba",
97
- "estimated_woba_using_speedangle": "xwoba",
98
- "events": "events",
99
- "description": "description",
100
- "stand": "batter_stand",
101
- "p_throws": "pitcher_hand",
102
- "game_date": "game_date",
103
- "home_team": "home_team",
104
- "away_team": "away_team",
105
- "inning": "inning",
106
- "outs_when_up": "outs_when_up",
107
- "balls": "balls",
108
- "strikes": "strikes",
109
- }
110
-
111
- keep_cols = [col for col in rename_map if col in df.columns]
112
- out = df[keep_cols].copy()
113
- out = out.rename(columns={col: rename_map[col] for col in keep_cols})
114
-
115
- numeric_cols = [
116
- "release_speed",
117
- "release_spin_rate",
118
- "pfx_x",
119
- "pfx_z",
120
- "release_pos_x",
121
- "release_pos_z",
122
- "plate_x",
123
- "plate_z",
124
- "launch_speed",
125
- "launch_angle",
126
- "xba",
127
- "xwoba",
128
- "inning",
129
- "outs_when_up",
130
- "balls",
131
- "strikes",
132
- ]
133
-
134
- for col in numeric_cols:
135
- if col in out.columns:
136
- out[col] = pd.to_numeric(out[col], errors="coerce")
137
 
138
  return out
 
1
  from __future__ import annotations
2
 
 
 
 
3
  import pandas as pd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
 
 
 
5
 
6
+ def add_pitch_features(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
 
 
 
 
 
7
  if df.empty:
8
+ return df.copy()
9
+
10
+ out = df.copy()
11
+
12
+ if {"pfx_x", "pfx_z"}.issubset(out.columns):
13
+ out["movement_magnitude"] = (
14
+ out["pfx_x"].fillna(0).astype(float) ** 2
15
+ + out["pfx_z"].fillna(0).astype(float) ** 2
16
+ ) ** 0.5
17
+
18
+ if "release_spin_rate" in out.columns:
19
+ out["spin_efficiency_proxy"] = (
20
+ pd.to_numeric(out["release_spin_rate"], errors="coerce").fillna(0.0) / 3000.0
21
+ )
22
+
23
+ if {"release_pos_x", "release_pos_z"}.issubset(out.columns):
24
+ out["release_height_proxy"] = pd.to_numeric(
25
+ out["release_pos_z"], errors="coerce"
26
+ )
27
+ out["release_side_proxy"] = pd.to_numeric(
28
+ out["release_pos_x"], errors="coerce"
29
+ )
30
+
31
+ if {"balls", "strikes"}.issubset(out.columns):
32
+ out["count_string"] = (
33
+ out["balls"].fillna(0).astype(int).astype(str)
34
+ + "-"
35
+ + out["strikes"].fillna(0).astype(int).astype(str)
36
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  return out