Upload 27 files
Browse files- app.py +279 -283
- app_streamlit.py +253 -0
- functions/__pycache__/pitch_summary_functions.cpython-39.pyc +0 -0
- functions/pitch_summary_functions.py +147 -1
- pitching_summary_api.ipynb +0 -0
- requirements.txt +12 -14
app.py
CHANGED
|
@@ -1,284 +1,280 @@
|
|
| 1 |
-
import polars as pl
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
-
import api_scraper
|
| 5 |
-
scrape = api_scraper.MLB_Scrape()
|
| 6 |
-
from functions import df_update
|
| 7 |
-
from functions import pitch_summary_functions
|
| 8 |
-
update = df_update.df_update()
|
| 9 |
-
from stuff_model import feature_engineering as fe
|
| 10 |
-
from stuff_model import stuff_apply
|
| 11 |
-
import requests
|
| 12 |
-
import joblib
|
| 13 |
-
from matplotlib.gridspec import GridSpec
|
| 14 |
-
from shiny import App, reactive, ui, render
|
| 15 |
-
from shiny.ui import h2, tags
|
| 16 |
-
import matplotlib.pyplot as plt
|
| 17 |
-
import matplotlib.gridspec as gridspec
|
| 18 |
-
import seaborn as sns
|
| 19 |
-
from functions.pitch_summary_functions import *
|
| 20 |
-
from shiny import App, reactive, ui, render
|
| 21 |
-
from shiny.ui import h2, tags
|
| 22 |
-
|
| 23 |
-
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
| 24 |
-
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
level_dict = {'1':'MLB',
|
| 32 |
-
'11':'AAA',
|
| 33 |
-
'12':'AA',
|
| 34 |
-
'13':'A+',
|
| 35 |
-
'14':'A',
|
| 36 |
-
'17':'AFL',
|
| 37 |
-
'22':'College',
|
| 38 |
-
'21':'Prospects',
|
| 39 |
-
'51':'International' }
|
| 40 |
-
|
| 41 |
-
function_dict={
|
| 42 |
-
'velocity_kdes':'Velocity Distributions',
|
| 43 |
-
'break_plot':'Pitch Movement',
|
| 44 |
-
'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
|
| 45 |
-
'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
|
| 46 |
-
'location_plot_lhb':'Locations vs LHB',
|
| 47 |
-
'location_plot_rhb':'Locations vs RHB',
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
split_dict = {'all':'All',
|
| 52 |
-
'left':'LHH',
|
| 53 |
-
'right':'RHH'}
|
| 54 |
-
|
| 55 |
-
split_dict_hand = {'all':['L','R'],
|
| 56 |
-
'left':['L'],
|
| 57 |
-
'right':['R']}
|
| 58 |
-
|
| 59 |
-
from shiny import App, reactive, ui, render
|
| 60 |
-
from shiny.ui import h2, tags
|
| 61 |
-
|
| 62 |
-
# Define the UI layout for the app
|
| 63 |
-
app_ui = ui.page_fluid(
|
| 64 |
-
ui.layout_sidebar(
|
| 65 |
-
ui.panel_sidebar(
|
| 66 |
-
# Row for selecting season and level
|
| 67 |
-
ui.row(
|
| 68 |
-
|
| 69 |
-
ui.column(6, ui.
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
ui.row(ui.input_action_button("
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
ui.column(4, ui.input_select('
|
| 82 |
-
ui.column(4, ui.input_select('
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
ui.column(6, ui.
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
ui.
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
@
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
print(
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
fig.
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
app = App(app_ui, server)
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
app = App(app_ui, server)
|
|
|
|
| 1 |
+
import polars as pl
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import api_scraper
|
| 5 |
+
scrape = api_scraper.MLB_Scrape()
|
| 6 |
+
from functions import df_update
|
| 7 |
+
from functions import pitch_summary_functions
|
| 8 |
+
update = df_update.df_update()
|
| 9 |
+
from stuff_model import feature_engineering as fe
|
| 10 |
+
from stuff_model import stuff_apply
|
| 11 |
+
import requests
|
| 12 |
+
import joblib
|
| 13 |
+
from matplotlib.gridspec import GridSpec
|
| 14 |
+
from shiny import App, reactive, ui, render
|
| 15 |
+
from shiny.ui import h2, tags
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
import matplotlib.gridspec as gridspec
|
| 18 |
+
import seaborn as sns
|
| 19 |
+
from functions.pitch_summary_functions import *
|
| 20 |
+
from shiny import App, reactive, ui, render
|
| 21 |
+
from shiny.ui import h2, tags
|
| 22 |
+
|
| 23 |
+
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
| 24 |
+
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
level_dict = {'1':'MLB',
|
| 32 |
+
'11':'AAA',
|
| 33 |
+
'12':'AA',
|
| 34 |
+
'13':'A+',
|
| 35 |
+
'14':'A',
|
| 36 |
+
'17':'AFL',
|
| 37 |
+
'22':'College',
|
| 38 |
+
'21':'Prospects',
|
| 39 |
+
'51':'International' }
|
| 40 |
+
|
| 41 |
+
function_dict={
|
| 42 |
+
'velocity_kdes':'Velocity Distributions',
|
| 43 |
+
'break_plot':'Pitch Movement',
|
| 44 |
+
'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
|
| 45 |
+
'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
|
| 46 |
+
'location_plot_lhb':'Locations vs LHB',
|
| 47 |
+
'location_plot_rhb':'Locations vs RHB',
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
split_dict = {'all':'All',
|
| 52 |
+
'left':'LHH',
|
| 53 |
+
'right':'RHH'}
|
| 54 |
+
|
| 55 |
+
split_dict_hand = {'all':['L','R'],
|
| 56 |
+
'left':['L'],
|
| 57 |
+
'right':['R']}
|
| 58 |
+
|
| 59 |
+
from shiny import App, reactive, ui, render
|
| 60 |
+
from shiny.ui import h2, tags
|
| 61 |
+
|
| 62 |
+
# Define the UI layout for the app
|
| 63 |
+
app_ui = ui.page_fluid(
|
| 64 |
+
ui.layout_sidebar(
|
| 65 |
+
ui.panel_sidebar(
|
| 66 |
+
# Row for selecting season and level
|
| 67 |
+
ui.row(
|
| 68 |
+
|
| 69 |
+
ui.column(6, ui.input_date('date_input', 'Select Date')),
|
| 70 |
+
ui.column(6, ui.input_select('level_input', 'Select Level', level_dict))
|
| 71 |
+
),
|
| 72 |
+
ui.row(ui.input_action_button("game_button", "Get Games", class_="btn-primary")),
|
| 73 |
+
ui.row(
|
| 74 |
+
|
| 75 |
+
ui.row(ui.column(12, ui.output_ui('game_select_ui', 'Select Game'))),
|
| 76 |
+
ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
|
| 77 |
+
),
|
| 78 |
+
|
| 79 |
+
# Rows for selecting plots and split options
|
| 80 |
+
ui.row(
|
| 81 |
+
ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')),
|
| 82 |
+
ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='tj_stuff_roling')),
|
| 83 |
+
ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='break_plot'))
|
| 84 |
+
),
|
| 85 |
+
ui.row(
|
| 86 |
+
ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
|
| 87 |
+
ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
|
| 88 |
+
),
|
| 89 |
+
|
| 90 |
+
# Row for the action button to generate plot
|
| 91 |
+
ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary"))
|
| 92 |
+
),
|
| 93 |
+
|
| 94 |
+
ui.panel_main(
|
| 95 |
+
ui.navset_tab(
|
| 96 |
+
# Tab for game summary plot
|
| 97 |
+
ui.nav("Pitching Summary",
|
| 98 |
+
ui.output_text("status"),
|
| 99 |
+
ui.output_plot('plot', width='2100px', height='2100px')
|
| 100 |
+
),
|
| 101 |
+
)
|
| 102 |
+
)
|
| 103 |
+
)
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def server(input, output, session):
|
| 108 |
+
|
| 109 |
+
@render.ui
|
| 110 |
+
@reactive.event(input.game_button, ignore_none=False)
|
| 111 |
+
def game_select_ui():
|
| 112 |
+
df = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
|
| 113 |
+
sport_id=[int(input.level_input())],
|
| 114 |
+
game_type=['S','R','P','E','A']).with_columns(pl.col('date').cast(pl.Utf8)).
|
| 115 |
+
filter(pl.col('date') == str(input.date_input()))).with_columns(
|
| 116 |
+
(pl.col('away')+' @ '+pl.col('home')).alias('matchup'))
|
| 117 |
+
game_dict = dict(zip(df['game_id'], df['matchup']))
|
| 118 |
+
|
| 119 |
+
return ui.input_select("game_id", "Select Game", game_dict, selectize=True)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@render.ui
|
| 123 |
+
@reactive.event(input.game_id)
|
| 124 |
+
def player_select_ui():
|
| 125 |
+
try:
|
| 126 |
+
# Get the list of pitchers for the selected level and season
|
| 127 |
+
data_list = scrape.get_data(game_list_input = [int(input.game_id())])
|
| 128 |
+
df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
|
| 129 |
+
(pl.col("is_pitch") == True)&
|
| 130 |
+
(pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
|
| 131 |
+
|
| 132 |
+
)))).with_columns(
|
| 133 |
+
pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
|
| 134 |
+
).with_columns(
|
| 135 |
+
(pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
|
| 136 |
+
)
|
| 137 |
+
pitcher_dict = dict(zip(df['pitcher_id'], df['pitcher_name']))
|
| 138 |
+
return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
|
| 139 |
+
except Exception as e:
|
| 140 |
+
print(e)
|
| 141 |
+
return ui.output_text('pitcher_id',"No pitchers available for this game")
|
| 142 |
+
|
| 143 |
+
@output
|
| 144 |
+
@render.text
|
| 145 |
+
def status():
|
| 146 |
+
# Only show status when generating
|
| 147 |
+
if input.generate == 0:
|
| 148 |
+
return ""
|
| 149 |
+
return ""
|
| 150 |
+
|
| 151 |
+
@output
|
| 152 |
+
@render.plot
|
| 153 |
+
@reactive.event(input.generate_plot, ignore_none=False)
|
| 154 |
+
def plot():
|
| 155 |
+
# Show progress/loading notification
|
| 156 |
+
with ui.Progress(min=0, max=1) as p:
|
| 157 |
+
p.set(message="Generating plot", detail="This may take a while...")
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
p.set(0.3, "Gathering data...")
|
| 161 |
+
|
| 162 |
+
data_list = scrape.get_data(game_list_input = [int(input.game_id())])
|
| 163 |
+
df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
|
| 164 |
+
(pl.col("pitcher_id") == int(input.pitcher_id()))&
|
| 165 |
+
(pl.col("is_pitch") == True)&
|
| 166 |
+
(pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
|
| 167 |
+
|
| 168 |
+
)))).with_columns(
|
| 169 |
+
pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
|
| 170 |
+
))
|
| 171 |
+
df = df.clone()
|
| 172 |
+
|
| 173 |
+
p.set(0.6, "Creating plot...")
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
#plt.rcParams["figure.figsize"] = [10,10]
|
| 177 |
+
fig = plt.figure(figsize=(26,26))
|
| 178 |
+
plt.rcParams.update({'figure.autolayout': True})
|
| 179 |
+
fig.set_facecolor('white')
|
| 180 |
+
sns.set_theme(style="whitegrid", palette=colour_palette)
|
| 181 |
+
print('this is the one plot')
|
| 182 |
+
|
| 183 |
+
gs = gridspec.GridSpec(6, 8,
|
| 184 |
+
height_ratios=[5,20,12,36,36,7],
|
| 185 |
+
width_ratios=[4,18,18,18,18,18,18,4])
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
gs.update(hspace=0.2, wspace=0.5)
|
| 189 |
+
|
| 190 |
+
# Define the positions of each subplot in the grid
|
| 191 |
+
ax_headshot = fig.add_subplot(gs[1,1:3])
|
| 192 |
+
ax_bio = fig.add_subplot(gs[1,3:5])
|
| 193 |
+
ax_logo = fig.add_subplot(gs[1,5:7])
|
| 194 |
+
|
| 195 |
+
ax_season_table = fig.add_subplot(gs[2,1:7])
|
| 196 |
+
|
| 197 |
+
ax_plot_1 = fig.add_subplot(gs[3,1:3])
|
| 198 |
+
ax_plot_2 = fig.add_subplot(gs[3,3:5])
|
| 199 |
+
ax_plot_3 = fig.add_subplot(gs[3,5:7])
|
| 200 |
+
|
| 201 |
+
ax_table = fig.add_subplot(gs[4,1:7])
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
ax_footer = fig.add_subplot(gs[-1,1:7])
|
| 205 |
+
ax_header = fig.add_subplot(gs[0,1:7])
|
| 206 |
+
ax_left = fig.add_subplot(gs[:,0])
|
| 207 |
+
ax_right = fig.add_subplot(gs[:,-1])
|
| 208 |
+
|
| 209 |
+
# Hide axes for footer, header, left, and right
|
| 210 |
+
ax_footer.axis('off')
|
| 211 |
+
ax_header.axis('off')
|
| 212 |
+
ax_left.axis('off')
|
| 213 |
+
ax_right.axis('off')
|
| 214 |
+
|
| 215 |
+
sns.set_theme(style="whitegrid", palette=colour_palette)
|
| 216 |
+
fig.set_facecolor('white')
|
| 217 |
+
|
| 218 |
+
df_teams = scrape.get_teams()
|
| 219 |
+
|
| 220 |
+
year_input = int(str(input.date_input())[:4])
|
| 221 |
+
sport_id = int(input.level_input())
|
| 222 |
+
player_input = int(input.pitcher_id())
|
| 223 |
+
|
| 224 |
+
player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
|
| 225 |
+
player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
|
| 226 |
+
plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
|
| 227 |
+
|
| 228 |
+
# stat_summary_table(df=df,
|
| 229 |
+
# ax=ax_season_table,
|
| 230 |
+
# player_input=player_input,
|
| 231 |
+
# split=input.split_id(),
|
| 232 |
+
# sport_id=sport_id)
|
| 233 |
+
|
| 234 |
+
stat_daily_summary(df=df,
|
| 235 |
+
data=data_list,
|
| 236 |
+
player_input=int(input.pitcher_id()),
|
| 237 |
+
sport_id=int(input.level_input()),
|
| 238 |
+
ax=ax_season_table)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# break_plot(df=df_plot,ax=ax2)
|
| 242 |
+
for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
|
| 243 |
+
if x == 'velocity_kdes':
|
| 244 |
+
velocity_kdes(df,
|
| 245 |
+
ax=y,
|
| 246 |
+
gs=gs,
|
| 247 |
+
gs_x=[3,4],
|
| 248 |
+
gs_y=[z,z+2],
|
| 249 |
+
fig=fig)
|
| 250 |
+
if x == 'tj_stuff_roling':
|
| 251 |
+
tj_stuff_roling(df=df,
|
| 252 |
+
window=int(input.rolling_window()),
|
| 253 |
+
ax=y)
|
| 254 |
+
|
| 255 |
+
if x == 'tj_stuff_roling_game':
|
| 256 |
+
tj_stuff_roling_game(df=df,
|
| 257 |
+
window=int(input.rolling_window()),
|
| 258 |
+
ax=y)
|
| 259 |
+
|
| 260 |
+
if x == 'break_plot':
|
| 261 |
+
break_plot(df = df,ax=y)
|
| 262 |
+
|
| 263 |
+
if x == 'location_plot_lhb':
|
| 264 |
+
location_plot(df = df,ax=y,hand='L')
|
| 265 |
+
|
| 266 |
+
if x == 'location_plot_rhb':
|
| 267 |
+
location_plot(df = df,ax=y,hand='R')
|
| 268 |
+
|
| 269 |
+
summary_table(df=df,
|
| 270 |
+
ax=ax_table)
|
| 271 |
+
|
| 272 |
+
plot_footer(ax_footer)
|
| 273 |
+
|
| 274 |
+
fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
app = App(app_ui, server)
|
app_streamlit.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import seaborn as sns
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode
|
| 4 |
+
import PitchPlotFunctions as ppf
|
| 5 |
+
import requests
|
| 6 |
+
import polars as pl
|
| 7 |
+
from datetime import date
|
| 8 |
+
import api_scraper
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Display the app title and description
|
| 13 |
+
st.markdown("""
|
| 14 |
+
## MLB & AAA Pitch Plots App
|
| 15 |
+
|
| 16 |
+
##### By: Thomas Nestico ([@TJStats](https://x.com/TJStats))
|
| 17 |
+
##### Code: [GitHub Repo](https://github.com/tnestico/streamlit_pitch_plots)
|
| 18 |
+
##### Data: [MLB](https://baseballsavant.mlb.com/)
|
| 19 |
+
|
| 20 |
+
#### About
|
| 21 |
+
This Streamlit app retrieves MLB and AAA Pitching Data for a selected pitcher from the MLB Stats API and is accessed using my [MLB Stats API Scraper](https://github.com/tnestico/mlb_scraper).
|
| 22 |
+
|
| 23 |
+
The app outputs the pitcher's data into both a plot and table to illustrate and summarize the data.
|
| 24 |
+
It can also display data for games currently in progress.
|
| 25 |
+
|
| 26 |
+
*More information about the data and plots is shown at the bottom of this page.*
|
| 27 |
+
|
| 28 |
+
"""
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Initialize the plotter object from PitchPlotFunctions
|
| 32 |
+
ploter = ppf.PitchPlotFunctions()
|
| 33 |
+
# Initialize the scraper object
|
| 34 |
+
scraper = api_scraper.MLB_Scrape()
|
| 35 |
+
|
| 36 |
+
# Dictionary mapping league names to sport IDs
|
| 37 |
+
sport_id_dict = {'MLB': 1, 'AAA': 11}
|
| 38 |
+
|
| 39 |
+
# Create two columns for league and pitcher selection
|
| 40 |
+
st.write("#### Plot")
|
| 41 |
+
col_1, col_2 = st.columns(2)
|
| 42 |
+
with col_1:
|
| 43 |
+
# Select league
|
| 44 |
+
selected_league = st.selectbox('##### Select League', list(sport_id_dict.keys()))
|
| 45 |
+
selected_sport_id = sport_id_dict[selected_league]
|
| 46 |
+
|
| 47 |
+
with col_2:
|
| 48 |
+
# Get player data and filter for pitchers
|
| 49 |
+
df_player = scraper.get_players(sport_id=selected_sport_id)
|
| 50 |
+
df_player = df_player.filter(pl.col('position').str.contains('P'))
|
| 51 |
+
df_player = df_player.with_columns(
|
| 52 |
+
(pl.concat_str(["name", "player_id"], separator=" - ").alias("pitcher_name_id"))
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Select specific columns and convert to dictionary
|
| 56 |
+
pitcher_name_id_dict = dict(df_player.select(['pitcher_name_id', 'player_id']).iter_rows())
|
| 57 |
+
|
| 58 |
+
# Initialize session state for previous selection
|
| 59 |
+
if 'prev_pitcher_id' not in st.session_state:
|
| 60 |
+
st.session_state.prev_pitcher_id = None
|
| 61 |
+
|
| 62 |
+
# Display a selectbox for pitcher selection
|
| 63 |
+
selected_pitcher = st.selectbox("##### Select Pitcher", list(pitcher_name_id_dict.keys()))
|
| 64 |
+
pitcher_id = pitcher_name_id_dict[selected_pitcher]
|
| 65 |
+
|
| 66 |
+
# Clear cache if selection changes
|
| 67 |
+
if pitcher_id != st.session_state.prev_pitcher_id:
|
| 68 |
+
st.cache_data.clear()
|
| 69 |
+
st.session_state.prev_pitcher_id = pitcher_id
|
| 70 |
+
st.session_state.cache_cleared = False
|
| 71 |
+
st.write('Cache cleared!')
|
| 72 |
+
|
| 73 |
+
# Initialize session state for cache status
|
| 74 |
+
if 'cache_cleared' not in st.session_state:
|
| 75 |
+
st.session_state.cache_cleared = False
|
| 76 |
+
|
| 77 |
+
# Dictionary for batter hand selection
|
| 78 |
+
batter_hand_picker = {
|
| 79 |
+
'All': ['L', 'R'],
|
| 80 |
+
'LHH': ['L'],
|
| 81 |
+
'RHH': ['R']
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# Define date range for the season
|
| 85 |
+
min_date = date(2024, 3, 20)
|
| 86 |
+
max_date = date(2024, 11, 30)
|
| 87 |
+
|
| 88 |
+
# Create columns for input widgets
|
| 89 |
+
st.write("##### Filters")
|
| 90 |
+
col1, col2, col3 = st.columns(3)
|
| 91 |
+
with col1:
|
| 92 |
+
# Selectbox for batter handedness
|
| 93 |
+
batter_hand_select = st.selectbox('Batter Handedness:', list(batter_hand_picker.keys()))
|
| 94 |
+
batter_hand = batter_hand_picker[batter_hand_select]
|
| 95 |
+
with col2:
|
| 96 |
+
# Date input for start date
|
| 97 |
+
start_date = st.date_input('Start Date:',
|
| 98 |
+
value=min_date,
|
| 99 |
+
min_value=min_date,
|
| 100 |
+
max_value=max_date,
|
| 101 |
+
format="YYYY-MM-DD")
|
| 102 |
+
with col3:
|
| 103 |
+
# Date input for end date
|
| 104 |
+
end_date = st.date_input('End Date:',
|
| 105 |
+
value="default_value_today",
|
| 106 |
+
min_value=min_date,
|
| 107 |
+
max_value=max_date,
|
| 108 |
+
format="YYYY-MM-DD")
|
| 109 |
+
|
| 110 |
+
# Dictionary for plot type selection
|
| 111 |
+
plot_picker_dict = {
|
| 112 |
+
'Short Form Movement': 'short_form_movement',
|
| 113 |
+
'Long Form Movement': 'long_form_movement',
|
| 114 |
+
'Release Points': 'release_point'
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
# Selectbox for plot type
|
| 118 |
+
plot_picker_select = st.selectbox('Select Plot Type:', list(plot_picker_dict.keys()))
|
| 119 |
+
plot_picker = plot_picker_dict[plot_picker_select]
|
| 120 |
+
|
| 121 |
+
# Extract season from start date
|
| 122 |
+
season = str(start_date)[0:4]
|
| 123 |
+
|
| 124 |
+
# Get list of games for the selected player and date range
|
| 125 |
+
player_games = scraper.get_player_games_list(player_id=pitcher_id, season=season,
|
| 126 |
+
start_date=str(start_date), end_date=str(end_date),
|
| 127 |
+
sport_id=selected_sport_id,
|
| 128 |
+
game_type = ['R','P'])
|
| 129 |
+
|
| 130 |
+
# Function to fetch data and cache it
|
| 131 |
+
@st.cache_data
|
| 132 |
+
def fetch_data():
|
| 133 |
+
data = scraper.get_data(game_list_input=player_games)
|
| 134 |
+
df = scraper.get_data_df(data_list=data)
|
| 135 |
+
return df
|
| 136 |
+
|
| 137 |
+
# Fetch data and manage cache status
|
| 138 |
+
if not st.session_state.cache_cleared:
|
| 139 |
+
df_original = fetch_data()
|
| 140 |
+
st.session_state.cache_cleared = True
|
| 141 |
+
else:
|
| 142 |
+
df_original = fetch_data()
|
| 143 |
+
|
| 144 |
+
# Button to generate plot
|
| 145 |
+
if st.button('Generate Plot'):
|
| 146 |
+
try:
|
| 147 |
+
# Convert dataframe to polars and filter based on inputs
|
| 148 |
+
df = ploter.df_to_polars(df_original=df_original,
|
| 149 |
+
pitcher_id=pitcher_id,
|
| 150 |
+
start_date=str(start_date),
|
| 151 |
+
end_date=str(end_date),
|
| 152 |
+
batter_hand=batter_hand)
|
| 153 |
+
print(df)
|
| 154 |
+
if len(df) == 0:
|
| 155 |
+
st.write('Please select different parameters.')
|
| 156 |
+
else:
|
| 157 |
+
# Generate the final plot
|
| 158 |
+
ploter.final_plot(
|
| 159 |
+
df=df,
|
| 160 |
+
pitcher_id=pitcher_id,
|
| 161 |
+
plot_picker=plot_picker,
|
| 162 |
+
sport_id=selected_sport_id)
|
| 163 |
+
|
| 164 |
+
# Use a container to control the width of the AgGrid display
|
| 165 |
+
with st.container():
|
| 166 |
+
# Group the data by pitch type
|
| 167 |
+
grouped_df = (
|
| 168 |
+
df.group_by(['pitcher_id', 'pitch_description'])
|
| 169 |
+
.agg([
|
| 170 |
+
pl.col('is_pitch').drop_nans().count().alias('pitches'),
|
| 171 |
+
pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
|
| 172 |
+
pl.col('vb').drop_nans().mean().round(1).alias('vb'),
|
| 173 |
+
pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
|
| 174 |
+
pl.col('hb').drop_nans().mean().round(1).alias('hb'),
|
| 175 |
+
pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
|
| 176 |
+
pl.col('x0').drop_nans().mean().round(1).alias('x0'),
|
| 177 |
+
pl.col('z0').drop_nans().mean().round(1).alias('z0'),
|
| 178 |
+
])
|
| 179 |
+
.with_columns(
|
| 180 |
+
(pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100).round(3).alias('proportion')
|
| 181 |
+
)).sort('proportion', descending=True).select(["pitch_description", "pitches", "proportion", "start_speed", "vb", "ivb", "hb",
|
| 182 |
+
"spin_rate", "x0", "z0"])
|
| 183 |
+
|
| 184 |
+
st.write("#### Pitching Data")
|
| 185 |
+
column_config_dict = {
|
| 186 |
+
'pitcher_id': 'Pitcher ID',
|
| 187 |
+
'pitch_description': 'Pitch Type',
|
| 188 |
+
'pitches': 'Pitches',
|
| 189 |
+
'start_speed': 'Velocity',
|
| 190 |
+
'vb': 'VB',
|
| 191 |
+
'ivb': 'iVB',
|
| 192 |
+
'hb': 'HB',
|
| 193 |
+
'spin_rate': 'Spin Rate',
|
| 194 |
+
'proportion': st.column_config.NumberColumn("Pitch%", format="%.1f%%"),
|
| 195 |
+
'x0': 'hRel',
|
| 196 |
+
'z0': 'vRel',
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
st.markdown(f"""##### {selected_pitcher.split('-')[0]} {selected_league} Pitch Data""")
|
| 200 |
+
st.dataframe(grouped_df,
|
| 201 |
+
hide_index=True,
|
| 202 |
+
column_config=column_config_dict,
|
| 203 |
+
width=1500)
|
| 204 |
+
|
| 205 |
+
# Configure the AgGrid options
|
| 206 |
+
# gb = GridOptionsBuilder.from_dataframe(grouped_df)
|
| 207 |
+
# # Set display names for columns
|
| 208 |
+
# for col, display_name in zip(grouped_df.columns, grouped_df.columns):
|
| 209 |
+
# gb.configure_column(col, headerName=display_name)
|
| 210 |
+
|
| 211 |
+
# grid_options = gb.build()
|
| 212 |
+
|
| 213 |
+
# # Display the dataframe using AgGrid
|
| 214 |
+
# grid_response = AgGrid(
|
| 215 |
+
# grouped_df,
|
| 216 |
+
# gridOptions=grid_options,
|
| 217 |
+
# height=300,
|
| 218 |
+
# allow_unsafe_jscode=True,
|
| 219 |
+
# )
|
| 220 |
+
|
| 221 |
+
except IndexError:
|
| 222 |
+
st.write('Please select different parameters.')
|
| 223 |
+
|
| 224 |
+
# Display column and plot descriptions
|
| 225 |
+
st.markdown("""
|
| 226 |
+
#### Column Descriptions
|
| 227 |
+
|
| 228 |
+
- **`Pitch Type`**: Describes the type of pitch thrown (e.g., 4-Seam Fastball, Curveball, Slider).
|
| 229 |
+
- **`Pitches`**: The total number of pitches thrown by the pitcher.
|
| 230 |
+
- **`Pitch%`**: Proportion of pitch thrown.
|
| 231 |
+
- **`Velocity`**: The initial velocity of the pitch as it leaves the pitcher's hand, measured in miles per hour (mph).
|
| 232 |
+
- **`VB`**: Vertical Break (VB), representing the amount movement of a pitch due to spin and gravity, measured in inches (in).
|
| 233 |
+
- **`iVB`**: Induced Vertical Break (iVB), representing the amount movement of a pitch strictly due to the spin imparted on the ball, measured in inches (in).
|
| 234 |
+
- **`HB`**: Horizontal Break (HB), indicating the amount of horizontal movement of a pitch, measured in inches (in).
|
| 235 |
+
- **`Spin Rate`**: The rate of spin of the pitch as it is released, measured in revolutions per minute (rpm).
|
| 236 |
+
- **`hRel`**: The horizontal release point of the pitch, measured in feet from the center of the pitcher's mound (ft).
|
| 237 |
+
- **`vRel`**: The vertical release point of the pitch, measured in feet above the ground (ft).
|
| 238 |
+
|
| 239 |
+
#### Plot Descriptions
|
| 240 |
+
|
| 241 |
+
- **`Short Form Movement`**: Illustrates the movement of the pitch due to spin, where (0,0) indicates a pitch with perfect gyro-spin (e.g. Like a Football).
|
| 242 |
+
- **`Long Form Movement`**: Illustrates the movement of the pitch due to spin and gravity.
|
| 243 |
+
- **`Release Points`**: Illustrates a pitchers release points from the catcher's perspective.
|
| 244 |
+
|
| 245 |
+
#### Acknowledgements
|
| 246 |
+
|
| 247 |
+
Big thanks to [Michael Rosen](https://twitter.com/bymichaelrosen) and [Jeremy Maschino](https://twitter.com/pitchprofiler) for inspiration for this project
|
| 248 |
+
|
| 249 |
+
Check Out Michael's [Pitch Plotting App](https://pitchplotgenerator.streamlit.app/)
|
| 250 |
+
|
| 251 |
+
Check Out Jeremy's Website [Pitch Profiler](http://www.mlbpitchprofiler.com/)
|
| 252 |
+
"""
|
| 253 |
+
)
|
functions/__pycache__/pitch_summary_functions.cpython-39.pyc
CHANGED
|
Binary files a/functions/__pycache__/pitch_summary_functions.cpython-39.pyc and b/functions/__pycache__/pitch_summary_functions.cpython-39.pyc differ
|
|
|
functions/pitch_summary_functions.py
CHANGED
|
@@ -866,7 +866,7 @@ def plot_logo(pitcher_id: str, ax: plt.Axes, df_team: pl.DataFrame, df_players:
|
|
| 866 |
|
| 867 |
# Turn off the axis
|
| 868 |
ax.axis('off')
|
| 869 |
-
except KeyError:
|
| 870 |
ax.axis('off')
|
| 871 |
return
|
| 872 |
|
|
@@ -1121,3 +1121,149 @@ def stat_summary_table(df: pl.DataFrame,
|
|
| 1121 |
# Add title to the plot
|
| 1122 |
ax.text(0.5, 0.9, title, va='bottom', ha='center', fontsize=36, fontstyle='italic')
|
| 1123 |
ax.axis('off')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
|
| 867 |
# Turn off the axis
|
| 868 |
ax.axis('off')
|
| 869 |
+
except (KeyError,IndexError) as e:
|
| 870 |
ax.axis('off')
|
| 871 |
return
|
| 872 |
|
|
|
|
| 1121 |
# Add title to the plot
|
| 1122 |
ax.text(0.5, 0.9, title, va='bottom', ha='center', fontsize=36, fontstyle='italic')
|
| 1123 |
ax.axis('off')
|
| 1124 |
+
|
| 1125 |
+
|
| 1126 |
+
|
| 1127 |
+
def stat_daily_summary(df: pl.DataFrame,
|
| 1128 |
+
data: list,
|
| 1129 |
+
player_input: int,
|
| 1130 |
+
sport_id: int,
|
| 1131 |
+
ax: plt.Axes):
|
| 1132 |
+
|
| 1133 |
+
|
| 1134 |
+
pk_list = []
|
| 1135 |
+
pitcher_id_list = []
|
| 1136 |
+
summary_list = []
|
| 1137 |
+
ip_list = []
|
| 1138 |
+
pa_list = []
|
| 1139 |
+
er_list = []
|
| 1140 |
+
hit_list = []
|
| 1141 |
+
k_list = []
|
| 1142 |
+
bb_list = []
|
| 1143 |
+
hbp_list = []
|
| 1144 |
+
strikes_list = []
|
| 1145 |
+
hr_list = []
|
| 1146 |
+
test_list = []
|
| 1147 |
+
game_pk_list = []
|
| 1148 |
+
pitches_list = []
|
| 1149 |
+
|
| 1150 |
+
|
| 1151 |
+
# 'inningsPitched','battersFaced','earnedRuns','hits','strikeOuts','baseOnBalls','hitByPitch'
|
| 1152 |
+
|
| 1153 |
+
for y in range(0,len(data)):
|
| 1154 |
+
|
| 1155 |
+
pk_list.append([data[y]['gameData']['game']['pk'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1156 |
+
pk_list.append([data[y]['gameData']['game']['pk'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1157 |
+
|
| 1158 |
+
pitcher_id_list.append([x for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1159 |
+
pitcher_id_list.append([x for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1160 |
+
|
| 1161 |
+
|
| 1162 |
+
ip_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['inningsPitched'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1163 |
+
ip_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['inningsPitched'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1164 |
+
|
| 1165 |
+
pa_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['battersFaced'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1166 |
+
pa_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['battersFaced'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1167 |
+
|
| 1168 |
+
er_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['earnedRuns'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1169 |
+
er_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['earnedRuns'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1170 |
+
|
| 1171 |
+
hit_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['hits'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1172 |
+
hit_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['hits'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1173 |
+
|
| 1174 |
+
k_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['strikeOuts'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1175 |
+
k_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['strikeOuts'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1176 |
+
|
| 1177 |
+
bb_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['baseOnBalls'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1178 |
+
bb_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['baseOnBalls'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1179 |
+
|
| 1180 |
+
hbp_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['hitByPitch'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1181 |
+
hbp_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['hitByPitch'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1182 |
+
|
| 1183 |
+
strikes_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['strikes'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1184 |
+
strikes_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['strikes'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1185 |
+
|
| 1186 |
+
pitches_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['pitchesThrown'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1187 |
+
pitches_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['pitchesThrown'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1188 |
+
|
| 1189 |
+
|
| 1190 |
+
hr_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['homeRuns'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1191 |
+
hr_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['homeRuns'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1192 |
+
|
| 1193 |
+
summary_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['summary'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
|
| 1194 |
+
summary_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['summary'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
|
| 1195 |
+
|
| 1196 |
+
test_list.append([x for x in data[y]['liveData']['plays']['allPlays']])
|
| 1197 |
+
game_pk_list.append([data[y]['gameData']['game']['pk'] for x in data[y]['liveData']['plays']['allPlays']])
|
| 1198 |
+
|
| 1199 |
+
flat_list_pk = [item for sublist in pk_list for item in sublist]
|
| 1200 |
+
flat_list_pitcher_id = [item for sublist in pitcher_id_list for item in sublist]
|
| 1201 |
+
flat_list_summary = [item for sublist in summary_list for item in sublist]
|
| 1202 |
+
flat_list_hits = [item for sublist in hit_list for item in sublist]
|
| 1203 |
+
flat_list_k = [item for sublist in k_list for item in sublist]
|
| 1204 |
+
flat_list_bb = [item for sublist in bb_list for item in sublist]
|
| 1205 |
+
flat_list_pa = [item for sublist in pa_list for item in sublist]
|
| 1206 |
+
flat_list_ip = [item for sublist in ip_list for item in sublist]
|
| 1207 |
+
flat_list_hbp= [item for sublist in hbp_list for item in sublist]
|
| 1208 |
+
flat_list_strikes = [item for sublist in strikes_list for item in sublist]
|
| 1209 |
+
flat_list_hr= [item for sublist in hr_list for item in sublist]
|
| 1210 |
+
flat_list_er= [item for sublist in er_list for item in sublist]
|
| 1211 |
+
flat_list_pitches= [item for sublist in pitches_list for item in sublist]
|
| 1212 |
+
|
| 1213 |
+
|
| 1214 |
+
|
| 1215 |
+
pitcher_summary_df = pl.DataFrame(data={'game_id':flat_list_pk,
|
| 1216 |
+
'pitcher_id':flat_list_pitcher_id,
|
| 1217 |
+
'summary':flat_list_summary,
|
| 1218 |
+
'hits':flat_list_hits,
|
| 1219 |
+
'k':flat_list_k,
|
| 1220 |
+
'bb':flat_list_bb,
|
| 1221 |
+
'pa':flat_list_pa,
|
| 1222 |
+
'hbp':flat_list_hbp,
|
| 1223 |
+
'strikes':flat_list_strikes,
|
| 1224 |
+
'hr':flat_list_hr,
|
| 1225 |
+
'ip':flat_list_ip,
|
| 1226 |
+
'er':flat_list_er,
|
| 1227 |
+
'pitches':flat_list_pitches})
|
| 1228 |
+
|
| 1229 |
+
|
| 1230 |
+
|
| 1231 |
+
# Add additional calculated columns
|
| 1232 |
+
pitcher_summary_df = pitcher_summary_df.filter(pl.col('pitcher_id')==player_input).with_columns(
|
| 1233 |
+
pl.lit(df['is_whiff'].sum()).alias('whiffs'),
|
| 1234 |
+
((pl.col('strikes'))/(pl.col('pitches'))*100).round(1).cast(pl.Utf8).str.concat('%').alias('strikePercentage')
|
| 1235 |
+
)
|
| 1236 |
+
|
| 1237 |
+
# Determine columns and title based on game count and sport ID
|
| 1238 |
+
|
| 1239 |
+
pitcher_stats_call_df_small = pitcher_summary_df.select(['ip',
|
| 1240 |
+
'pa',
|
| 1241 |
+
'er',
|
| 1242 |
+
'hits',
|
| 1243 |
+
'k',
|
| 1244 |
+
'bb',
|
| 1245 |
+
'hbp',
|
| 1246 |
+
'hr',
|
| 1247 |
+
'strikePercentage',
|
| 1248 |
+
'whiffs'])
|
| 1249 |
+
|
| 1250 |
+
new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{ER}$', '$\\bf{H}$', '$\\bf{K}$', '$\\bf{BB}$', '$\\bf{HBP}$', '$\\bf{HR}$', '$\\bf{Strike\%}$', '$\\bf{Whiffs}$']
|
| 1251 |
+
title = f'{df["game_date"][0]} vs {df["batter_team"][0]}'
|
| 1252 |
+
|
| 1253 |
+
table_fg = ax.table(cellText=pitcher_stats_call_df_small.to_numpy(), colLabels=pitcher_stats_call_df_small.columns, cellLoc='center',
|
| 1254 |
+
bbox=[0.0, 0.1, 1, 0.7])
|
| 1255 |
+
|
| 1256 |
+
min_font_size = 20
|
| 1257 |
+
table_fg.set_fontsize(min_font_size)
|
| 1258 |
+
|
| 1259 |
+
|
| 1260 |
+
new_column_names = ['$\\bf{IP}$','$\\bf{PA}$','$\\bf{ER}$','$\\bf{H}$','$\\bf{K}$','$\\bf{BB}$','$\\bf{HBP}$','$\\bf{HR}$','$\\bf{Strike\%}$','$\\bf{Whiffs}$']
|
| 1261 |
+
# #new_column_names = ['Pitch Name', 'Pitch%', 'Velocity', 'Spin Rate','Exit Velocity', 'Whiff%', 'CSW%']
|
| 1262 |
+
for i, col_name in enumerate(new_column_names):
|
| 1263 |
+
table_fg.get_celld()[(0, i)].get_text().set_text(col_name)
|
| 1264 |
+
|
| 1265 |
+
ax.axis('off')
|
| 1266 |
+
|
| 1267 |
+
# Add title to the plot
|
| 1268 |
+
ax.text(0.5, 0.9, title, va='bottom', ha='center', fontsize=36, fontstyle='italic')
|
| 1269 |
+
ax.axis('off')
|
pitching_summary_api.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
CHANGED
|
@@ -1,14 +1,12 @@
|
|
| 1 |
-
joblib==1.
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
polars==1.12.0
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
seaborn==0.11.1
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
tqdm==4.62.3
|
| 14 |
-
|
|
|
|
| 1 |
+
joblib==1.4.2
|
| 2 |
+
matplotlib==3.5.1
|
| 3 |
+
numpy==1.22.1
|
| 4 |
+
pandas==2.0.3
|
| 5 |
+
Pillow==11.0.0
|
| 6 |
+
polars==1.12.0
|
| 7 |
+
pytz==2022.7.1
|
| 8 |
+
Requests==2.32.3
|
| 9 |
+
seaborn==0.11.1
|
| 10 |
+
shiny==0.7.1
|
| 11 |
+
streamlit==1.37.1
|
| 12 |
+
tqdm==4.62.3
|
|
|
|
|
|