Spaces:
Running
Running
Upload 28 files
Browse files- .dockerignore +27 -0
- Dockerfile +11 -15
- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/app.cpython-39.pyc +0 -0
- __pycache__/batter_scatter.cpython-39.pyc +0 -0
- __pycache__/configure.cpython-39.pyc +0 -0
- __pycache__/damage.cpython-39.pyc +0 -0
- __pycache__/decision_value.cpython-39.pyc +0 -0
- __pycache__/ev_angle.cpython-39.pyc +0 -0
- __pycache__/home.cpython-39.pyc +0 -0
- __pycache__/spray.cpython-39.pyc +0 -0
- angle_ev_list_df.csv +44 -0
- app.py +10 -2
- batter_scatter.py +499 -0
- damage.py +610 -0
- decision_value.py +683 -0
- ev_angle.py +270 -0
- home.py +22 -9
- manifest.json +33 -0
- no_swing.joblib +3 -0
- requirements.txt +306 -14
- runtime.txt +1 -0
- spray.py +437 -0
- summary_batter.csv +0 -0
- summary_batter_level.csv +0 -0
- swing.joblib +3 -0
- xtb_model.joblib +3 -0
.dockerignore
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
**/__pycache__
|
2 |
+
**/.venv
|
3 |
+
**/.classpath
|
4 |
+
**/.dockerignore
|
5 |
+
**/.env
|
6 |
+
**/.git
|
7 |
+
**/.gitignore
|
8 |
+
**/.project
|
9 |
+
**/.settings
|
10 |
+
**/.toolstarget
|
11 |
+
**/.vs
|
12 |
+
**/.vscode
|
13 |
+
**/*.*proj.user
|
14 |
+
**/*.dbmdl
|
15 |
+
**/*.jfm
|
16 |
+
**/bin
|
17 |
+
**/charts
|
18 |
+
**/docker-compose*
|
19 |
+
**/compose*
|
20 |
+
**/Dockerfile*
|
21 |
+
**/node_modules
|
22 |
+
**/npm-debug.log
|
23 |
+
**/obj
|
24 |
+
**/secrets.dev.yaml
|
25 |
+
**/values.dev.yaml
|
26 |
+
LICENSE
|
27 |
+
README.md
|
Dockerfile
CHANGED
@@ -1,20 +1,16 @@
|
|
1 |
FROM python:3.9
|
2 |
|
3 |
-
|
|
|
|
|
4 |
|
5 |
-
|
|
|
|
|
|
|
6 |
|
7 |
-
|
|
|
8 |
|
9 |
-
#
|
10 |
-
|
11 |
-
USER user
|
12 |
-
ENV HOME=/home/user \
|
13 |
-
PATH=/home/user/.local/bin:$PATH
|
14 |
-
|
15 |
-
|
16 |
-
COPY . .
|
17 |
-
|
18 |
-
EXPOSE 7860
|
19 |
-
|
20 |
-
CMD ["shiny", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
1 |
FROM python:3.9
|
2 |
|
3 |
+
# Install dependencies
|
4 |
+
COPY requirements.txt /app/
|
5 |
+
RUN pip install -r /app/requirements.txt
|
6 |
|
7 |
+
# Copy app files
|
8 |
+
COPY app_name /app/app_name/
|
9 |
+
COPY static/ /app/static/
|
10 |
+
COPY templates/ /app/templates/
|
11 |
|
12 |
+
# Set working directory
|
13 |
+
WORKDIR /app/app_name
|
14 |
|
15 |
+
# Set the command to run the app
|
16 |
+
CMD ["python", "app.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__pycache__/app.cpython-311.pyc
ADDED
Binary file (27.8 kB). View file
|
|
__pycache__/app.cpython-39.pyc
ADDED
Binary file (767 Bytes). View file
|
|
__pycache__/batter_scatter.cpython-39.pyc
ADDED
Binary file (11.4 kB). View file
|
|
__pycache__/configure.cpython-39.pyc
ADDED
Binary file (215 Bytes). View file
|
|
__pycache__/damage.cpython-39.pyc
ADDED
Binary file (13.8 kB). View file
|
|
__pycache__/decision_value.cpython-39.pyc
ADDED
Binary file (15.8 kB). View file
|
|
__pycache__/ev_angle.cpython-39.pyc
ADDED
Binary file (6.64 kB). View file
|
|
__pycache__/home.cpython-39.pyc
ADDED
Binary file (1.69 kB). View file
|
|
__pycache__/spray.cpython-39.pyc
ADDED
Binary file (7.73 kB). View file
|
|
angle_ev_list_df.csv
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
,launch_angle,launch_speed
|
2 |
+
0,8,116.09999999999894
|
3 |
+
1,9,115.099999999999
|
4 |
+
2,10,114.09999999999906
|
5 |
+
3,11,113.09999999999911
|
6 |
+
4,12,112.09999999999917
|
7 |
+
5,13,111.09999999999923
|
8 |
+
6,14,110.09999999999928
|
9 |
+
7,15,109.09999999999934
|
10 |
+
8,16,108.0999999999994
|
11 |
+
9,17,107.09999999999945
|
12 |
+
10,18,106.09999999999951
|
13 |
+
11,19,105.09999999999957
|
14 |
+
12,20,104.09999999999962
|
15 |
+
13,21,103.09999999999968
|
16 |
+
14,22,102.09999999999974
|
17 |
+
15,23,101.0999999999998
|
18 |
+
16,24,100.09999999999985
|
19 |
+
17,25,99.09999999999991
|
20 |
+
18,26,98.09999999999997
|
21 |
+
19,27,97.5
|
22 |
+
20,28,97.5
|
23 |
+
21,29,97.5
|
24 |
+
22,30,98.09999999999997
|
25 |
+
23,31,98.69999999999993
|
26 |
+
24,32,99.39999999999989
|
27 |
+
25,33,100.09999999999985
|
28 |
+
26,34,100.69999999999982
|
29 |
+
27,35,101.39999999999978
|
30 |
+
28,36,102.09999999999974
|
31 |
+
29,37,102.6999999999997
|
32 |
+
30,38,103.39999999999966
|
33 |
+
31,39,104.09999999999962
|
34 |
+
32,40,104.69999999999959
|
35 |
+
33,41,105.39999999999955
|
36 |
+
34,42,106.09999999999951
|
37 |
+
35,43,106.69999999999948
|
38 |
+
36,44,107.39999999999944
|
39 |
+
37,45,108.0999999999994
|
40 |
+
38,46,108.69999999999936
|
41 |
+
39,47,109.39999999999932
|
42 |
+
40,48,110.09999999999928
|
43 |
+
41,49,110.69999999999925
|
44 |
+
42,50,111.39999999999921
|
app.py
CHANGED
@@ -17,7 +17,11 @@ from home import home
|
|
17 |
# from team_xg_rates import team_xg_rates
|
18 |
# from gsax_comparison import gsax_comparison
|
19 |
# from game import game
|
20 |
-
|
|
|
|
|
|
|
|
|
21 |
# from articles import articles
|
22 |
# from xg_model import xg_model
|
23 |
|
@@ -31,7 +35,11 @@ routes = [
|
|
31 |
# Mount('/skater-xg-percentages', app=on_ice_xgfp),
|
32 |
# Mount('/team-xg-rates', app=team_xg_rates),
|
33 |
# Mount('/gsax-comparison',app=gsax_comparison),
|
34 |
-
|
|
|
|
|
|
|
|
|
35 |
# Mount('/game/{game_id}',app=game),
|
36 |
# Mount('/articles',app=articles),
|
37 |
# Mount('/xg-model',app=xg_model)
|
|
|
17 |
# from team_xg_rates import team_xg_rates
|
18 |
# from gsax_comparison import gsax_comparison
|
19 |
# from game import game
|
20 |
+
from spray import spray
|
21 |
+
from decision_value import decision_value
|
22 |
+
from damage import damage
|
23 |
+
from batter_scatter import batter_scatter
|
24 |
+
from ev_angle import ev_angle
|
25 |
# from articles import articles
|
26 |
# from xg_model import xg_model
|
27 |
|
|
|
35 |
# Mount('/skater-xg-percentages', app=on_ice_xgfp),
|
36 |
# Mount('/team-xg-rates', app=team_xg_rates),
|
37 |
# Mount('/gsax-comparison',app=gsax_comparison),
|
38 |
+
Mount('/spray',app=spray),
|
39 |
+
Mount('/decision_value',app=decision_value),
|
40 |
+
Mount('/damage_model',app=damage),
|
41 |
+
Mount('/batter_scatter',app=batter_scatter),
|
42 |
+
Mount('/ev_angle',app=ev_angle),
|
43 |
# Mount('/game/{game_id}',app=game),
|
44 |
# Mount('/articles',app=articles),
|
45 |
# Mount('/xg-model',app=xg_model)
|
batter_scatter.py
ADDED
@@ -0,0 +1,499 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
|
2 |
+
import datasets
|
3 |
+
from datasets import load_dataset
|
4 |
+
import pandas as pd
|
5 |
+
import numpy as np
|
6 |
+
import matplotlib.pyplot as plt
|
7 |
+
import seaborn as sns
|
8 |
+
import numpy as np
|
9 |
+
from scipy.stats import gaussian_kde
|
10 |
+
import matplotlib
|
11 |
+
from matplotlib.ticker import MaxNLocator
|
12 |
+
from matplotlib.gridspec import GridSpec
|
13 |
+
from scipy.stats import zscore
|
14 |
+
import math
|
15 |
+
import matplotlib
|
16 |
+
from adjustText import adjust_text
|
17 |
+
import matplotlib.ticker as mtick
|
18 |
+
from shinywidgets import output_widget, render_widget
|
19 |
+
import pandas as pd
|
20 |
+
from configure import base_url
|
21 |
+
import shinyswatch
|
22 |
+
|
23 |
+
|
24 |
+
exit_velo_df_codes_summ_batter = pd.read_csv('summary_batter.csv',index_col=[0])
|
25 |
+
#exit_velo_df_codes_summ = pd.read_csv('summary_pitcher.csv',index_col=[0])
|
26 |
+
|
27 |
+
exit_velo_df_codes_summ_non_level = pd.read_csv('summary_batter_level.csv',index_col=[0]).reset_index(drop=True)
|
28 |
+
|
29 |
+
exit_velo_df_codes_summ_non_level['levels'] = exit_velo_df_codes_summ_non_level.levels.str.split(', ')
|
30 |
+
|
31 |
+
exit_velo_df_codes_summ_non_level = exit_velo_df_codes_summ_non_level.rename(columns={'levels':'level'})
|
32 |
+
|
33 |
+
|
34 |
+
|
35 |
+
print(exit_velo_df_codes_summ_batter.bb_minus_k_percent)
|
36 |
+
|
37 |
+
batter_dict_stat = { 'sweet_spot_percent':{'x_axis':'SweetSpot%','title':'SweetSpot%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
38 |
+
'max_launch_speed':{'x_axis':'Max Exit Velocity','title':'Max Exit Velocity','flip_p':False,'decimal_format':'string_0','percent_adjust':1},
|
39 |
+
'launch_speed_90':{'x_axis':'90th Percentile EV','title':'90th Percentile EV','flip_p':False,'decimal_format':'string_0','percent_adjust':1},
|
40 |
+
'launch_speed':{'x_axis':'Exit Velocity','title':'Exit Velocity','flip_p':False,'decimal_format':'string_0','percent_adjust':1},
|
41 |
+
'launch_angle':{'x_axis':'Launch Angle','title':'Launch Angle','flip_p':False,'decimal_format':'string_0','percent_adjust':100},
|
42 |
+
'avg':{'x_axis':'AVG','title':'AVG','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
|
43 |
+
'obp':{'x_axis':'OBP','title':'OBP','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
|
44 |
+
'slg':{'x_axis':'SLG','title':'SLG','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
|
45 |
+
'ops':{'x_axis':'OPS','title':'OPS','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
|
46 |
+
'k_percent':{'x_axis':'K%','title':'K%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
|
47 |
+
'bb_percent':{'x_axis':'BB%','title':'BB%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
48 |
+
'bb_over_k_percent':{'x_axis':'BB/K','title':'BB/K','flip_p':False,'decimal_format':'string_1','percent_adjust':100},
|
49 |
+
'bb_minus_k_percent':{'x_axis':'BB%-K%','title':'BB%-K%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
50 |
+
'csw_percent':{'x_axis':'CSW%','title':'CSW%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
|
51 |
+
'woba_percent':{'x_axis':'wOBA','title':'wOBA','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
|
52 |
+
'hard_hit_percent':{'x_axis':'HardHit%','title':'HardHit%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
53 |
+
'barrel_percent':{'x_axis':'Barrel%','title':'Barrel%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
54 |
+
'zone_contact_percent':{'x_axis':'Z-Contact%','title':'Z-Contact%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
55 |
+
'zone_swing_percent':{'x_axis':'Z-Swing%','title':'Z-Swing%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
56 |
+
'zone_percent':{'x_axis':'Zone%','title':'Zone%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
57 |
+
'chase_percent':{'x_axis':'O-Swing%','title':'O-Swing%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
|
58 |
+
'chase_contact':{'x_axis':'O-Contact%','title':'O-Contact%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
|
59 |
+
'swing_percent':{'x_axis':'Swing%','title':'Swing%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
|
60 |
+
'whiff_rate':{'x_axis':'Whiff%','title':'Whiff%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
|
61 |
+
'swstr_rate':{'x_axis':'SwStr%','title':'SwStr%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
|
62 |
+
}
|
63 |
+
|
64 |
+
batter_dict_stat_small = { 'sweet_spot_percent':'SweetSpot%',
|
65 |
+
'max_launch_speed':'Max Exit Velocity',
|
66 |
+
'launch_speed_90':'90th Percentile EV',
|
67 |
+
'launch_speed':'Exit Velocity',
|
68 |
+
'launch_angle':'Launch Angle',
|
69 |
+
'avg':'AVG',
|
70 |
+
'obp':'OBP',
|
71 |
+
'slg':'SLG',
|
72 |
+
'ops':'OPS',
|
73 |
+
'k_percent':'K%',
|
74 |
+
'bb_percent':'BB%',
|
75 |
+
'bb_over_k_percent':'BB/K',
|
76 |
+
'bb_minus_k_percent':'BB%-K%',
|
77 |
+
'csw_percent':'CSW%',
|
78 |
+
'woba_percent':'wOBA',
|
79 |
+
'hard_hit_percent':'HardHit%',
|
80 |
+
'barrel_percent':'Barrel%',
|
81 |
+
'zone_contact_percent':'Z-Contact%',
|
82 |
+
'zone_swing_percent':'Z-Swing%',
|
83 |
+
'zone_percent':'Zone%',
|
84 |
+
'chase_percent':'O-Swing%',
|
85 |
+
'chase_contact':'O-Contact%',
|
86 |
+
'swing_percent':'Swing%',
|
87 |
+
'whiff_rate':'Whiff%',
|
88 |
+
'swstr_rate':'SwStr%',
|
89 |
+
}
|
90 |
+
|
91 |
+
|
92 |
+
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
93 |
+
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
94 |
+
|
95 |
+
level_dict = {'MLB':'MLB','AAA':'AAA','AA':'AA','A+':'A+','A':'A','ROK':'ROK'}
|
96 |
+
|
97 |
+
batter_test_df = exit_velo_df_codes_summ_batter.sort_values(by='batter').drop_duplicates(subset='batter_id').reset_index(drop=True)[['batter_id','batter']]#['pitcher'].to_dict()
|
98 |
+
batter_test_df = batter_test_df.set_index('batter_id')
|
99 |
+
|
100 |
+
|
101 |
+
def decimal_format_assign(x):
|
102 |
+
if x['decimal_format'] == 'percent_1':
|
103 |
+
return mtick.PercentFormatter(1,decimals=1)
|
104 |
+
if x['decimal_format'] == 'string_3':
|
105 |
+
return mtick.FormatStrFormatter('%.3f')
|
106 |
+
if x['decimal_format'] == 'string_0':
|
107 |
+
return mtick.FormatStrFormatter('%.0f')
|
108 |
+
if x['decimal_format'] == 'string_1':
|
109 |
+
return mtick.FormatStrFormatter('%.1f')
|
110 |
+
|
111 |
+
|
112 |
+
#test_df = test_df[test_df.pitcher == 'Chris Bassitt'].append(test_df[test_df.pitcher != 'Chris Bassitt'])
|
113 |
+
|
114 |
+
batter_dict = batter_test_df['batter'].to_dict()
|
115 |
+
|
116 |
+
exit_velo_df_codes_summ_batter.position = exit_velo_df_codes_summ_batter.position.replace(['LF','RF','CF','TWP'],['OF','OF','OF','DH'])
|
117 |
+
exit_velo_df_codes_summ_non_level.position = exit_velo_df_codes_summ_non_level.position.replace(['LF','RF','CF','TWP'],['OF','OF','OF','DH'])
|
118 |
+
|
119 |
+
position_list = ['All'] + list(exit_velo_df_codes_summ_batter.position.unique())
|
120 |
+
team_list = ['All'] + sorted(list(exit_velo_df_codes_summ_batter.parent_org_abb.unique()))
|
121 |
+
|
122 |
+
|
123 |
+
|
124 |
+
def server(input,output,session):
|
125 |
+
|
126 |
+
|
127 |
+
@output
|
128 |
+
@render.plot(alt="A histogram")
|
129 |
+
def plot():
|
130 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
131 |
+
print(input.level_id())
|
132 |
+
print(input.n())
|
133 |
+
print('we made it here',input.team_id(),input.position_id())
|
134 |
+
if input.group_level():
|
135 |
+
data_df = exit_velo_df_codes_summ_non_level.copy()
|
136 |
+
|
137 |
+
turth_list = []
|
138 |
+
#turth_list_2 = []
|
139 |
+
for x in range(0,len(data_df.level)):
|
140 |
+
turth_list_2 = []
|
141 |
+
for y in range(0,len(data_df.level[x])):
|
142 |
+
#print(level_list[x][y])
|
143 |
+
turth_list_2.append(data_df.level[x][y] in input.level_id())
|
144 |
+
turth_list.append(turth_list_2)
|
145 |
+
|
146 |
+
final_check_list = [True if True in x else False for x in turth_list]
|
147 |
+
|
148 |
+
|
149 |
+
data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())&(final_check_list)]
|
150 |
+
|
151 |
+
|
152 |
+
else:
|
153 |
+
|
154 |
+
|
155 |
+
data_df = exit_velo_df_codes_summ_batter.copy()
|
156 |
+
data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())&(data_df.level.isin(input.level_id()))]
|
157 |
+
print(data_df)
|
158 |
+
if 'All' in input.team_id():
|
159 |
+
print('nice')#data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())].reset_index(drop=True)
|
160 |
+
|
161 |
+
else:
|
162 |
+
data_df = data_df[(data_df.parent_org_abb.isin(input.team_id()))].reset_index(drop=True)
|
163 |
+
|
164 |
+
if 'All' in input.position_id():
|
165 |
+
print('nice')#data_df = data_df[(data_df.level.isin(input.level_id()))&(data_df.pa >= input.n())&(data_df.age <= input.n_age())].reset_index(drop=True)
|
166 |
+
|
167 |
+
else:
|
168 |
+
data_df = data_df[(data_df.position.isin(input.position_id()))].reset_index(drop=True)
|
169 |
+
|
170 |
+
|
171 |
+
#print('we made it here')
|
172 |
+
print(data_df)
|
173 |
+
data_df = data_df.sort_values(by='level').reset_index(drop=True)
|
174 |
+
print(batter_dict_stat[input.stat_x()]['flip_p'])
|
175 |
+
|
176 |
+
|
177 |
+
|
178 |
+
x_flip = batter_dict_stat[input.stat_x()]['flip_p']
|
179 |
+
y_flip = batter_dict_stat[input.stat_y()]['flip_p']
|
180 |
+
cbr_flip = batter_dict_stat[input.stat_z()]['flip_p']
|
181 |
+
|
182 |
+
|
183 |
+
|
184 |
+
data_df[input.stat_x()+'_percent'] = data_df[input.stat_x()].rank(pct=True,ascending=abs(x_flip-1))
|
185 |
+
|
186 |
+
data_df[input.stat_y()+'_percent'] = data_df[input.stat_y()].rank(pct=True,ascending=abs(y_flip-1))
|
187 |
+
|
188 |
+
data_df[input.stat_z()+'_percent'] = data_df[input.stat_z()].rank(pct=True,ascending=abs(cbr_flip-1))
|
189 |
+
|
190 |
+
|
191 |
+
|
192 |
+
fig, ax = plt.subplots(1, 1, figsize=(9, 9))
|
193 |
+
|
194 |
+
#data_df['bb_over_obp'] = data_df['bb']/data_df['k']
|
195 |
+
|
196 |
+
#data_df[input.stat_z()]= data_df[input.stat_z()].fillna(-100000)
|
197 |
+
|
198 |
+
|
199 |
+
if cbr_flip:
|
200 |
+
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[0],colour_palette[3],colour_palette[1]])
|
201 |
+
norm = plt.Normalize(data_df[input.stat_z()].min(), data_df[input.stat_z()].max())
|
202 |
+
|
203 |
+
else:
|
204 |
+
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],colour_palette[3],colour_palette[0]])
|
205 |
+
norm = plt.Normalize(data_df[input.stat_z()].min(), data_df[input.stat_z()].max())
|
206 |
+
|
207 |
+
sm = plt.cm.ScalarMappable(cmap=cmap_hue, norm=norm)
|
208 |
+
print('we made it here')
|
209 |
+
|
210 |
+
# sns.regplot(x = stat_x, y = stat_y, data=data_df, color = colour_palette[6],ax=ax,scatter=False,
|
211 |
+
# line_kws=dict(alpha=0.3,linewidth=2,zorder=1))
|
212 |
+
# scatter_plot = sns.scatterplot(x = stat_x, y = stat_y, data=data_df, color = colour_palette[0],ax=ax,hue=stat_z,palette=cmap_hue)
|
213 |
+
|
214 |
+
|
215 |
+
|
216 |
+
# r, p = sp.stats.pearsonr(data_df[input.stat_x()], data_df[input.stat_y()])
|
217 |
+
# ax = plt.gca()
|
218 |
+
# # ax.text(.25, 0.3, 'r={:.2f}, p={:.2g}'.format(r, p),
|
219 |
+
# # transform=ax.transAxes, fontsize=12)
|
220 |
+
|
221 |
+
# ax.annotate('R²={:.2f}'.format(r, p), ( math.ceil(data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust']/5)*5/batter_dict_stat[input.stat_x()]['percent_adjust']*(1-batter_dict_stat[input.stat_x()]['flip_p']),
|
222 |
+
# math.floor(data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust']/5)*5/batter_dict_stat[input.stat_y()]['percent_adjust']*(1-batter_dict_stat[input.stat_y()]['flip_p'])),
|
223 |
+
# fontsize=18,fontname='Century Gothic',ha='right')
|
224 |
+
|
225 |
+
if input.group_level():
|
226 |
+
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = '#b3b3b3')
|
227 |
+
#ax.get_legend().remove()
|
228 |
+
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = colour_palette[0],ax=ax,hue=input.stat_z(),palette=cmap_hue)
|
229 |
+
else:
|
230 |
+
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = '#b3b3b3',style='level')
|
231 |
+
#ax.get_legend().remove()
|
232 |
+
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = colour_palette[0],ax=ax,hue=input.stat_z(),palette=cmap_hue,style='level')
|
233 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
234 |
+
|
235 |
+
fig.set_facecolor('#F0F0F0')
|
236 |
+
ax.set_facecolor('white')
|
237 |
+
|
238 |
+
print('we made it here')
|
239 |
+
# for i in range(0,len(pitch_group_unique)):
|
240 |
+
# data_df = elly_zone_df[elly_zone_df.pitch_group==pitch_group_unique[i]]
|
241 |
+
# len_df.append(len(data_df))
|
242 |
+
# sns.lineplot(x=range(1,len(data_df)+1),y=data_df.swings.rolling(window=rolling_window_input).sum()/data_df.pitches.rolling(window=rolling_window_input).sum(),color=colour_palette[i],linewidth=3,ax=ax,
|
243 |
+
# label=f'{pitch_group_unique[i]} (Season Average {float(data_df.swings.sum()/data_df.pitches.sum()):.1%})',zorder=i+10)
|
244 |
+
# ax.hlines(xmin=0,xmax=len(elly_zone_df),y=data_df.swings.sum()/data_df.pitches.sum(),color=colour_palette[i],linewidth=3,linestyle='-.',alpha=0.4,zorder=i)
|
245 |
+
|
246 |
+
ts=[]
|
247 |
+
print(input.player_id())
|
248 |
+
|
249 |
+
print(len(data_df))
|
250 |
+
if input.names():
|
251 |
+
for i in range(len(data_df)):
|
252 |
+
if (data_df[input.stat_x()+'_percent'][i] < input.n_percent_bot_x() or data_df[input.stat_x()+'_percent'][i] > 1 - input.n_percent_top_x() ) \
|
253 |
+
or (data_df[input.stat_y()+'_percent'][i] < input.n_percent_bot_y() or data_df[input.stat_y()+'_percent'][i] > 1 -input.n_percent_top_y()) \
|
254 |
+
or (data_df[input.stat_z()+'_percent'][i] < input.n_percent_bot_z() or data_df[input.stat_z()+'_percent'][i] > 1 -input.n_percent_top_z() )\
|
255 |
+
or (str(data_df.batter_id[i]) in (input.player_id())):
|
256 |
+
# print(data_df.batter[i])
|
257 |
+
# ax.annotate(data_df.batter[i], xy=((data_df[input.stat_x()][i])+0.025/batter_dict_stat[input.stat_x()]['percent_adjust'], data_df[input.stat_y()][i]+0.01/batter_dict_stat[input.stat_x()]['percent_adjust']), xytext=(-20,20),
|
258 |
+
# textcoords='offset points', ha='center', va='bottom',fontsize=7,
|
259 |
+
# bbox=dict(boxstyle='round,pad=0', fc=colour_palette[6], alpha=0.0),
|
260 |
+
# arrowprops=dict(arrowstyle='->', connectionstyle="angle,angleA=-90,angleB=-10,rad=2",
|
261 |
+
# color=colour_palette[8]))
|
262 |
+
|
263 |
+
#if data_df['batter'][i] != 'Jo Adell':
|
264 |
+
# ax.annotate(data_df.batter[i], (data_df[input.stat_x()][i]-len(data_df.batter[i])*0.00025, data_df[input.stat_y()][i]+0.001),fontsize=8)
|
265 |
+
ts.append(ax.text(data_df[input.stat_x()][i], data_df[input.stat_y()][i], data_df.batter[i],fontsize=8))
|
266 |
+
|
267 |
+
|
268 |
+
|
269 |
+
ax.hlines(xmin=(math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'],
|
270 |
+
xmax= (math.ceil((data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust']+0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'],
|
271 |
+
y=data_df[input.stat_y()].mean(),color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
|
272 |
+
|
273 |
+
print('we made it here')
|
274 |
+
|
275 |
+
ax.vlines(ymin=(math.floor((data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'],
|
276 |
+
ymax= (math.ceil((data_df[input.stat_y()].max()*batter_dict_stat[input.stat_y()]['percent_adjust']+0.01)/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'],
|
277 |
+
x=data_df[input.stat_x()].mean(),color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
|
278 |
+
|
279 |
+
print(data_df[input.stat_x()].min())
|
280 |
+
print(batter_dict_stat[input.stat_x()]['percent_adjust'])
|
281 |
+
print((math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'])
|
282 |
+
|
283 |
+
|
284 |
+
ax.set_xlim((math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'],
|
285 |
+
(math.ceil((data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'])
|
286 |
+
|
287 |
+
|
288 |
+
ax.set_ylim((math.floor((data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'],
|
289 |
+
(math.ceil((data_df[input.stat_y()].max()*batter_dict_stat[input.stat_y()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'])
|
290 |
+
|
291 |
+
|
292 |
+
|
293 |
+
title_level = str([x .strip("\'")for x in input.level_id()]).strip('[').strip(']').replace("'",'')
|
294 |
+
|
295 |
+
if title_level == 'AAA, AA, A+, A':
|
296 |
+
title_level='MiLB'
|
297 |
+
#title_level = input.level_id()[0]
|
298 |
+
if input.n_age() >= 50:
|
299 |
+
title_spot = f'{title_level} Batter {batter_dict_stat[input.stat_y()]["title"]} vs {batter_dict_stat[input.stat_x()]["title"]} (min. {input.n()} PA)'
|
300 |
+
|
301 |
+
else:
|
302 |
+
title_spot = f'{title_level} Batter {batter_dict_stat[input.stat_y()]["title"]} vs {batter_dict_stat[input.stat_x()]["title"]} (min. {input.n()} PA, Max Age {input.n_age()})'
|
303 |
+
|
304 |
+
ax.set_title(title_spot, fontsize=24/(len(title_spot)*0.03),fontname='Century Gothic')
|
305 |
+
# #vals = ax.get_yticks()
|
306 |
+
ax.set_xlabel(batter_dict_stat[input.stat_x()]['x_axis'], fontsize=16,fontname='Century Gothic')
|
307 |
+
ax.set_ylabel(batter_dict_stat[input.stat_y()]['x_axis'], fontsize=16,fontname='Century Gothic')
|
308 |
+
|
309 |
+
|
310 |
+
if input.group_level():
|
311 |
+
ax.get_legend().remove()
|
312 |
+
|
313 |
+
if not input.group_level():
|
314 |
+
if len(input.level_id()) > 1:
|
315 |
+
h,l = scatter.get_legend_handles_labels()
|
316 |
+
l[-(len(input.level_id())+1)] = 'Level'
|
317 |
+
ax.legend(h[-(len(input.level_id())+1):],l[-(len(input.level_id())+1):], borderaxespad=0.1,loc=0)
|
318 |
+
|
319 |
+
else:
|
320 |
+
ax.get_legend().remove()
|
321 |
+
|
322 |
+
#plt.show(g)
|
323 |
+
# ax.figure.colorbar(sm, ax=ax)
|
324 |
+
|
325 |
+
cbar = ax.figure.colorbar(sm, ax=ax,format=decimal_format_assign(x=batter_dict_stat[input.stat_z()]),orientation='vertical',aspect=30)
|
326 |
+
cbar.set_label(batter_dict_stat[input.stat_z()]['x_axis'])
|
327 |
+
#fig.axes[0].invert_yaxis()
|
328 |
+
print('we made it here5')
|
329 |
+
fig.subplots_adjust(wspace=.02, hspace=.02)
|
330 |
+
# ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
|
331 |
+
#ax.set_yticks([0,0.1,0.2,0.3,0.4,0.5])
|
332 |
+
# fig.colorbar(plot_dist, ax=ax)
|
333 |
+
# fig.colorbar(plot_dist)
|
334 |
+
|
335 |
+
if batter_dict_stat[input.stat_x()]['flip_p']:
|
336 |
+
fig.axes[0].invert_xaxis()
|
337 |
+
|
338 |
+
if batter_dict_stat[input.stat_y()]['flip_p']:
|
339 |
+
fig.axes[0].invert_yaxis()
|
340 |
+
|
341 |
+
|
342 |
+
# ax.xaxis.set_major_formatter(mtick.PercentFormatter(1,decimals=0))
|
343 |
+
# ax.yaxis.set_major_formatter(mtick.PercentFormatter(1))
|
344 |
+
|
345 |
+
|
346 |
+
|
347 |
+
|
348 |
+
|
349 |
+
print('we made it here6')
|
350 |
+
|
351 |
+
ax.xaxis.set_major_formatter(decimal_format_assign(x=batter_dict_stat[input.stat_x()]))
|
352 |
+
ax.yaxis.set_major_formatter(decimal_format_assign(x=batter_dict_stat[input.stat_y()]))
|
353 |
+
|
354 |
+
|
355 |
+
print('we made it here7')
|
356 |
+
# ax.text(0.5, 0.5, '/u/tomstoms', transform=ax.transAxes,
|
357 |
+
# fontsize=60, color='gray', alpha=0.075,
|
358 |
+
# ha='center', va='center', rotation=45)
|
359 |
+
|
360 |
+
print(ts)
|
361 |
+
adjust_text(ts,
|
362 |
+
arrowprops=dict(arrowstyle="-", color=colour_palette[4], lw=1),ax=ax)
|
363 |
+
|
364 |
+
#ax.legend(fontsize='16')
|
365 |
+
fig.text(x=0.03,y=0.02,s='By: @TJStats',fontname='Century Gothic')
|
366 |
+
fig.text(x=1-0.03,y=0.02,s='Data: MLB',ha='right',fontname='Century Gothic')
|
367 |
+
fig.tight_layout()
|
368 |
+
#matplotlib.rcParams["figure.dpi"] = 600
|
369 |
+
#plt.show()
|
370 |
+
|
371 |
+
|
372 |
+
batter_scatter = App(ui.page_fluid(
|
373 |
+
ui.tags.base(href=base_url),
|
374 |
+
ui.tags.div(
|
375 |
+
{"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
|
376 |
+
ui.tags.style(
|
377 |
+
"""
|
378 |
+
h4 {
|
379 |
+
margin-top: 1em;font-size:35px;
|
380 |
+
}
|
381 |
+
h2{
|
382 |
+
font-size:25px;
|
383 |
+
}
|
384 |
+
"""
|
385 |
+
),
|
386 |
+
shinyswatch.theme.simplex(),
|
387 |
+
ui.tags.h4("TJStats"),
|
388 |
+
ui.tags.i("Baseball Analytics and Visualizations"),
|
389 |
+
ui.navset_tab(
|
390 |
+
ui.nav_control(
|
391 |
+
ui.a(
|
392 |
+
"Home",
|
393 |
+
href="home/"
|
394 |
+
),
|
395 |
+
),
|
396 |
+
ui.nav_menu(
|
397 |
+
"Batter Charts",
|
398 |
+
ui.nav_control(
|
399 |
+
ui.a(
|
400 |
+
"Spray",
|
401 |
+
href="spray/"
|
402 |
+
),
|
403 |
+
ui.a(
|
404 |
+
"Decision Value",
|
405 |
+
href="decision_value/"
|
406 |
+
),
|
407 |
+
ui.a(
|
408 |
+
"Damage Model",
|
409 |
+
href="damage_model/"
|
410 |
+
),
|
411 |
+
ui.a(
|
412 |
+
"Batter Scatter",
|
413 |
+
href="batter_scatter/"
|
414 |
+
),
|
415 |
+
ui.a(
|
416 |
+
"EV vs LA Plot",
|
417 |
+
href="ev_angle/"
|
418 |
+
)
|
419 |
+
),
|
420 |
+
),
|
421 |
+
ui.nav_menu(
|
422 |
+
"Goalie Charts",
|
423 |
+
ui.nav_control(
|
424 |
+
ui.a(
|
425 |
+
"GSAx Timeline",
|
426 |
+
href="gsax-timeline/"
|
427 |
+
),
|
428 |
+
ui.a(
|
429 |
+
"GSAx Leaderboard",
|
430 |
+
href="gsax-leaderboard/"
|
431 |
+
),
|
432 |
+
ui.a(
|
433 |
+
"GSAx Comparison",
|
434 |
+
href="gsax-comparison/"
|
435 |
+
)
|
436 |
+
),
|
437 |
+
),ui.nav_menu(
|
438 |
+
"Team Charts",
|
439 |
+
ui.nav_control(
|
440 |
+
ui.a(
|
441 |
+
"Team xG Rates",
|
442 |
+
href="team-xg-rates/"
|
443 |
+
),
|
444 |
+
),
|
445 |
+
),ui.nav_control(
|
446 |
+
ui.a(
|
447 |
+
"Games",
|
448 |
+
href="games/"
|
449 |
+
),
|
450 |
+
),ui.nav_control(
|
451 |
+
ui.a(
|
452 |
+
"About",
|
453 |
+
href="about/"
|
454 |
+
),
|
455 |
+
),ui.nav_control(
|
456 |
+
ui.a(
|
457 |
+
"Articles",
|
458 |
+
href="articles/"
|
459 |
+
),
|
460 |
+
)),ui.row(
|
461 |
+
ui.layout_sidebar(
|
462 |
+
|
463 |
+
|
464 |
+
|
465 |
+
ui.panel_sidebar(
|
466 |
+
#ui.input_select("id", "Select Batter",batter_dict,selected=675911,width=1,size=1),
|
467 |
+
ui.row(
|
468 |
+
ui.column(4,ui.input_select("level_id", "Select Level",level_dict,width=1,size=1,multiple=True,selected='MLB',selectize=True),),
|
469 |
+
ui.column(4,ui.input_select("team_id", "Select Team",team_list,width=1,size=1,multiple=True,selected='All',selectize=True),),
|
470 |
+
ui.column(4,ui.input_select("position_id", "Select Position",position_list,width=1,size=1,selected='All',multiple=True,selectize=True))),
|
471 |
+
ui.row(
|
472 |
+
ui.column(6,ui.input_numeric("n", "Minimum PA", value=100)),
|
473 |
+
ui.column(6,ui.input_numeric("n_age", "Maximum Age", value=50))),
|
474 |
+
ui.row(
|
475 |
+
ui.column(4,ui.input_select("stat_x", "X-Axis",batter_dict_stat_small,selected='k_percent',width=1,size=1)),
|
476 |
+
ui.column(4,ui.input_select("stat_y", "Y-Axis",batter_dict_stat_small,selected='bb_percent',width=1,size=1)),
|
477 |
+
ui.column(4,ui.input_select("stat_z", "Colour-Bar Axis",batter_dict_stat_small,selected='bb_over_k_percent',width=1,size=1))),
|
478 |
+
|
479 |
+
ui.row(
|
480 |
+
ui.column(6,ui.input_numeric("n_percent_top_x", "Top 'n' Percentile X-Labels", value=0.01)),
|
481 |
+
ui.column(6,ui.input_numeric("n_percent_bot_x", "Bottom 'n' Percentile X-Labels", value=0.01))),
|
482 |
+
ui.row(
|
483 |
+
ui.column(6,ui.input_numeric("n_percent_top_y", "Top 'n' Percentile Y-Labels", value=0.01)),
|
484 |
+
ui.column(6,ui.input_numeric("n_percent_bot_y", "Bottom 'n' Percentile Y-Labels", value=0.01))),
|
485 |
+
ui.row(
|
486 |
+
ui.column(6,ui.input_numeric("n_percent_top_z", "Top 'n' Percentile Z-Labels", value=0.01)),
|
487 |
+
ui.column(6,ui.input_numeric("n_percent_bot_z", "Bottom 'n' Percentile Z-Labels", value=0.01))),
|
488 |
+
|
489 |
+
ui.input_select("player_id", "Label Player",batter_dict,width=1,size=1,multiple=True,selectize=True),
|
490 |
+
ui.row(
|
491 |
+
ui.input_switch("names", "Toggle Names"),
|
492 |
+
ui.input_switch("group_level", "Group Levels")),
|
493 |
+
),
|
494 |
+
|
495 |
+
ui.panel_main(
|
496 |
+
ui.output_plot("plot",height = "1000px",width="1000px")
|
497 |
+
,
|
498 |
+
),
|
499 |
+
)),)),server)
|
damage.py
ADDED
@@ -0,0 +1,610 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
|
2 |
+
import datasets
|
3 |
+
from datasets import load_dataset
|
4 |
+
import pandas as pd
|
5 |
+
import numpy as np
|
6 |
+
import matplotlib.pyplot as plt
|
7 |
+
import seaborn as sns
|
8 |
+
import numpy as np
|
9 |
+
from scipy.stats import gaussian_kde
|
10 |
+
import matplotlib
|
11 |
+
from matplotlib.ticker import MaxNLocator
|
12 |
+
from matplotlib.gridspec import GridSpec
|
13 |
+
from scipy.stats import zscore
|
14 |
+
import math
|
15 |
+
import matplotlib
|
16 |
+
from adjustText import adjust_text
|
17 |
+
import matplotlib.ticker as mtick
|
18 |
+
from shinywidgets import output_widget, render_widget
|
19 |
+
import pandas as pd
|
20 |
+
from configure import base_url
|
21 |
+
import shinyswatch
|
22 |
+
|
23 |
+
### Import Datasets
|
24 |
+
dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv' ])
|
25 |
+
dataset_train = dataset['train']
|
26 |
+
df_2023 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
|
27 |
+
print(df_2023)
|
28 |
+
### Normalize Hit Locations
|
29 |
+
|
30 |
+
df_2023['season'] = df_2023['game_date'].str[0:4].astype(int)
|
31 |
+
# df_2023['hit_x'] = df_2023['hit_x'] - df_2023['hit_x'].median()
|
32 |
+
# df_2023['hit_y'] = -df_2023['hit_y']+df_2023['hit_y'].quantile(0.9999)
|
33 |
+
|
34 |
+
df_2023['hit_x'] = df_2023['hit_x'] - 126#df_2023['hit_x'].median()
|
35 |
+
df_2023['hit_y'] = -df_2023['hit_y']+204.5#df_2023['hit_y'].quantile(0.9999)
|
36 |
+
|
37 |
+
df_2023['hit_x_og'] = df_2023['hit_x']
|
38 |
+
df_2023.loc[df_2023['batter_hand'] == 'R','hit_x'] = -1*df_2023.loc[df_2023['batter_hand'] == 'R','hit_x']
|
39 |
+
df_2023['h_la'] = np.arctan(df_2023['hit_x'] / df_2023['hit_y'])*180/np.pi
|
40 |
+
conditions_ss = [
|
41 |
+
(df_2023['h_la']<-15),
|
42 |
+
(df_2023['h_la']<15)&(df_2023['h_la']>=-15),
|
43 |
+
(df_2023['h_la']>=15)
|
44 |
+
]
|
45 |
+
|
46 |
+
choices_ss = ['Oppo','Straight','Pull']
|
47 |
+
df_2023['traj'] = np.select(conditions_ss, choices_ss, default=np.nan)
|
48 |
+
df_2023['bip'] = [1 if x > 0 else np.nan for x in df_2023['launch_speed']]
|
49 |
+
|
50 |
+
conditions_woba = [
|
51 |
+
(df_2023['event_type']=='walk'),
|
52 |
+
(df_2023['event_type']=='hit_by_pitch'),
|
53 |
+
(df_2023['event_type']=='single'),
|
54 |
+
(df_2023['event_type']=='double'),
|
55 |
+
(df_2023['event_type']=='triple'),
|
56 |
+
(df_2023['event_type']=='home_run'),
|
57 |
+
]
|
58 |
+
|
59 |
+
|
60 |
+
choices_woba = [1,
|
61 |
+
1,
|
62 |
+
1,
|
63 |
+
2,
|
64 |
+
3,
|
65 |
+
4]
|
66 |
+
|
67 |
+
|
68 |
+
# choices_woba = [0.698,
|
69 |
+
# 0.728,
|
70 |
+
# 0.887,
|
71 |
+
# 1.253,
|
72 |
+
# 1.583,
|
73 |
+
# 2.027]
|
74 |
+
|
75 |
+
df_2023['woba'] = np.select(conditions_woba, choices_woba, default=0)
|
76 |
+
|
77 |
+
choices_woba_train = [1,
|
78 |
+
1,
|
79 |
+
1,
|
80 |
+
2,
|
81 |
+
3,
|
82 |
+
4]
|
83 |
+
|
84 |
+
df_2023['woba_train'] = np.select(conditions_woba, choices_woba_train, default=0)
|
85 |
+
|
86 |
+
|
87 |
+
df_2023_bip = df_2023[~df_2023['bip'].isnull()].dropna(subset=['h_la','launch_angle'])
|
88 |
+
df_2023_bip['h_la'] = df_2023_bip['h_la'].round(0)
|
89 |
+
|
90 |
+
|
91 |
+
df_2023_bip['season'] = df_2023_bip['game_date'].str[0:4].astype(int)
|
92 |
+
|
93 |
+
df_2023_bip = df_2023[~df_2023['bip'].isnull()].dropna(subset=['launch_angle','bip'])
|
94 |
+
df_2023_bip_train = df_2023_bip[df_2023_bip['season'] == 2023]
|
95 |
+
|
96 |
+
batter_dict = df_2023_bip.sort_values('batter_name').set_index('batter_id')['batter_name'].to_dict()
|
97 |
+
|
98 |
+
features = ['launch_angle','launch_speed','h_la']
|
99 |
+
target = ['woba_train']
|
100 |
+
|
101 |
+
df_2023_bip_train = df_2023_bip_train.dropna(subset=features)
|
102 |
+
|
103 |
+
import joblib
|
104 |
+
# # Dump the model to a file named 'model.joblib'
|
105 |
+
model = joblib.load('xtb_model.joblib')
|
106 |
+
|
107 |
+
|
108 |
+
df_2023_bip_train['y_pred'] = [sum(x) for x in model.predict_proba(df_2023_bip_train[features]) * ([0,1,2,3,4])]
|
109 |
+
# df_2023_bip_train['y_pred_noh'] = [sum(x) for x in model_noh.predict_proba(df_2023_bip_train[['launch_angle','launch_speed']]) * ([0,0.887,1.253,1.583,2.027])]
|
110 |
+
|
111 |
+
df_2023_output = df_2023_bip_train.groupby(['batter_id','batter_name']).agg(
|
112 |
+
bip = ('y_pred','count'),
|
113 |
+
y_pred = ('y_pred','sum'),
|
114 |
+
slgcon = ('woba','mean'),
|
115 |
+
xslgcon = ('y_pred','mean'),
|
116 |
+
launch_speed = ('launch_speed','mean'),
|
117 |
+
launch_angle_std = ('launch_angle','median'),
|
118 |
+
h_la_std = ('h_la','mean'))
|
119 |
+
|
120 |
+
df_2023_output_copy = df_2023_output.copy()
|
121 |
+
# df_2023_output = df_2023_output[df_2023_output['bip'] > 100]
|
122 |
+
# df_2023_output[df_2023_output['bip'] > 100].sort_values(by='h_la_std',ascending=True).head(20)
|
123 |
+
|
124 |
+
import pandas as pd
|
125 |
+
import numpy as np
|
126 |
+
|
127 |
+
|
128 |
+
# Create grid coordinates
|
129 |
+
x = np.arange(30, 121,1 )
|
130 |
+
y = np.arange(-30, 61,1 )
|
131 |
+
z = np.arange(-45, 46,1 )
|
132 |
+
|
133 |
+
# Create a meshgrid
|
134 |
+
X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
|
135 |
+
# Flatten the meshgrid to get x and y coordinates
|
136 |
+
x_flat = X.flatten()
|
137 |
+
y_flat = Y.flatten()
|
138 |
+
z_flat = Z.flatten()
|
139 |
+
|
140 |
+
# Create a DataFrame
|
141 |
+
df = pd.DataFrame({'launch_speed': x_flat, 'launch_angle': y_flat,'h_la':z_flat})
|
142 |
+
|
143 |
+
df['y_pred'] = [sum(x) for x in model.predict_proba(df[features]) * ([0,1,2,3,4])]
|
144 |
+
|
145 |
+
|
146 |
+
import matplotlib
|
147 |
+
|
148 |
+
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
149 |
+
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
150 |
+
|
151 |
+
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]])
|
152 |
+
cmap_hue2 = matplotlib.colors.LinearSegmentedColormap.from_list("",['#ffffff',colour_palette[0]])
|
153 |
+
|
154 |
+
|
155 |
+
from matplotlib.pyplot import text
|
156 |
+
import inflect
|
157 |
+
from scipy.stats import percentileofscore
|
158 |
+
p = inflect.engine()
|
159 |
+
|
160 |
+
|
161 |
+
|
162 |
+
|
163 |
+
def server(input,output,session):
|
164 |
+
|
165 |
+
|
166 |
+
@output
|
167 |
+
@render.plot(alt="hex_plot")
|
168 |
+
def hex_plot():
|
169 |
+
|
170 |
+
if input.batter_id() is "":
|
171 |
+
fig = plt.figure(figsize=(12, 12))
|
172 |
+
fig.text(s='Please Select a Batter',x=0.5,y=0.5)
|
173 |
+
return
|
174 |
+
|
175 |
+
batter_select_id = int(input.batter_id())
|
176 |
+
# batter_select_name = 'Edouard Julien'
|
177 |
+
quant = int(input.quant())/100
|
178 |
+
df_batter_og = df_2023_bip_train[df_2023_bip_train['batter_id']==batter_select_id]
|
179 |
+
# df_batter_og = df_2023_bip_train[df_2023_bip_train['batter_name']==batter_select_name]
|
180 |
+
df_batter = df_batter_og[df_batter_og['launch_speed'] >= df_batter_og['launch_speed'].quantile(quant)]
|
181 |
+
# df_batter_best_speed = df_batter['launch_speed'].mean().round()
|
182 |
+
|
183 |
+
# df_bip_league = df_2023_bip_train[df_2023_bip_train['launch_speed'] >= df_2023_bip_train['launch_speed'].quantile(quant)]
|
184 |
+
|
185 |
+
import pandas as pd
|
186 |
+
import numpy as np
|
187 |
+
|
188 |
+
|
189 |
+
# Create grid coordinates
|
190 |
+
#x = np.arange(30, 121,1 )
|
191 |
+
y_b = np.arange(df_batter['launch_angle'].median()-df_batter['launch_angle'].std(),
|
192 |
+
df_batter['launch_angle'].median()+df_batter['launch_angle'].std(),1 )
|
193 |
+
|
194 |
+
z_b = np.arange(df_batter['h_la'].median()-df_batter['h_la'].std(),
|
195 |
+
df_batter['h_la'].median()+df_batter['h_la'].std(),1 )
|
196 |
+
|
197 |
+
# Create a meshgrid
|
198 |
+
Y_b, Z_b = np.meshgrid( y_b,z_b, indexing='ij')
|
199 |
+
# Flatten the meshgrid to get x and y coordinates
|
200 |
+
|
201 |
+
y_flat_b = Y_b.flatten()
|
202 |
+
z_flat_b = Z_b.flatten()
|
203 |
+
|
204 |
+
# Create a DataFrame
|
205 |
+
df_batter_base = pd.DataFrame({'launch_angle': y_flat_b,'h_la':z_flat_b,'c':[0]*len(y_flat_b)})
|
206 |
+
|
207 |
+
# df_batter_base['y_pred'] = [sum(x) for x in model.predict_proba(df_batter_base[features]) * ([0,1,2,3,4])]
|
208 |
+
|
209 |
+
from matplotlib.gridspec import GridSpec
|
210 |
+
# fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
|
211 |
+
fig = plt.figure(figsize=(12,12))
|
212 |
+
gs = GridSpec(4, 3, height_ratios=[0.5,10,1.5,0.2], width_ratios=[0.05,0.9,0.05])
|
213 |
+
|
214 |
+
axheader = fig.add_subplot(gs[0, :])
|
215 |
+
ax10 = fig.add_subplot(gs[1, 0])
|
216 |
+
ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
|
217 |
+
ax12 = fig.add_subplot(gs[1, 2])
|
218 |
+
ax2_ = fig.add_subplot(gs[2, :])
|
219 |
+
axfooter1 = fig.add_subplot(gs[-1, :])
|
220 |
+
|
221 |
+
axheader.axis('off')
|
222 |
+
ax10.axis('off')
|
223 |
+
ax12.axis('off')
|
224 |
+
ax2_.axis('off')
|
225 |
+
axfooter1.axis('off')
|
226 |
+
|
227 |
+
|
228 |
+
|
229 |
+
extents = [-45,45,-30,60]
|
230 |
+
|
231 |
+
def hexLines(a=None,i=None,off=[0,0]):
|
232 |
+
'''regular hexagon segment lines as `(xy1,xy2)` in clockwise
|
233 |
+
order with points in line sorted top to bottom
|
234 |
+
for irregular hexagon pass both `a` (vertical) and `i` (horizontal)'''
|
235 |
+
if a is None: a = 2 / np.sqrt(3) * i;
|
236 |
+
if i is None: i = np.sqrt(3) / 2 * a;
|
237 |
+
h = a / 2
|
238 |
+
xy = np.array([ [ [ 0, a], [ i, h] ],
|
239 |
+
[ [ i, h], [ i,-h] ],
|
240 |
+
[ [ i,-h], [ 0,-a] ],
|
241 |
+
[ [-i,-h], [ 0,-a] ], #flipped
|
242 |
+
[ [-i, h], [-i,-h] ], #flipped
|
243 |
+
[ [ 0, a], [-i, h] ] #flipped
|
244 |
+
])
|
245 |
+
return xy+off;
|
246 |
+
|
247 |
+
|
248 |
+
h = ax.hexbin(x=df_batter_base['h_la'],
|
249 |
+
y=df_batter_base['launch_angle'],
|
250 |
+
gridsize=25,
|
251 |
+
edgecolors='k',
|
252 |
+
extent=extents,mincnt=1,lw=2,zorder=-3,)
|
253 |
+
|
254 |
+
# cfg = {**cfg,'vmin':h.get_clim()[0], 'vmax':h.get_clim()[1]}
|
255 |
+
# plt.hexbin( ec="black" ,lw=6,zorder=4,mincnt=2,**cfg,alpha=0.1)
|
256 |
+
# plt.hexbin( ec="#ffffff",lw=1,zorder=5,mincnt=2,**cfg,alpha=0.1)
|
257 |
+
|
258 |
+
|
259 |
+
ax.hexbin(x=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['h_la'],
|
260 |
+
y=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['launch_angle'],
|
261 |
+
C=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['y_pred'],
|
262 |
+
gridsize=25,
|
263 |
+
vmin=0,
|
264 |
+
vmax=4,
|
265 |
+
cmap=cmap_hue2,
|
266 |
+
extent=extents,zorder=-3)
|
267 |
+
|
268 |
+
|
269 |
+
# Get the counts and centers of the hexagons
|
270 |
+
counts = ax.hexbin(x=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['h_la'],
|
271 |
+
y=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['launch_angle'],
|
272 |
+
C=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['y_pred'],
|
273 |
+
gridsize=25,
|
274 |
+
vmin=0,
|
275 |
+
vmax=4,
|
276 |
+
cmap=cmap_hue2,
|
277 |
+
extent=extents).get_array()
|
278 |
+
|
279 |
+
bin_centers = ax.hexbin(x=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['h_la'],
|
280 |
+
y=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['launch_angle'],
|
281 |
+
C=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['y_pred'],
|
282 |
+
gridsize=25,
|
283 |
+
vmin=0,
|
284 |
+
vmax=4,
|
285 |
+
cmap=cmap_hue2,
|
286 |
+
extent=extents).get_offsets()
|
287 |
+
|
288 |
+
# Add text with the values of "C" to each hexagon
|
289 |
+
for count, (x, y) in zip(counts, bin_centers):
|
290 |
+
if count >= 1:
|
291 |
+
ax.text(x, y, f'{count:.1f}', color='black', ha='center', va='center',fontsize=7)
|
292 |
+
|
293 |
+
|
294 |
+
|
295 |
+
#get hexagon centers that should be highlighted
|
296 |
+
verts = h.get_offsets()
|
297 |
+
cnts = h.get_array()
|
298 |
+
highl = verts[cnts > .5*cnts.max()]
|
299 |
+
|
300 |
+
#create hexagon lines
|
301 |
+
a = ((verts[0,1]-verts[1,1])/3).round(6)
|
302 |
+
i = ((verts[1:,0]-verts[:-1,0])/2).round(6)
|
303 |
+
i = i[i>0][0]
|
304 |
+
lines = np.concatenate([hexLines(a,i,off) for off in highl])
|
305 |
+
|
306 |
+
#select contour lines and draw
|
307 |
+
uls,c = np.unique(lines.round(4),axis=0,return_counts=True)
|
308 |
+
for l in uls[c==1]: ax.plot(*l.transpose(),'w-',lw=2,scalex=False,scaley=False,color=colour_palette[1],zorder=100)
|
309 |
+
|
310 |
+
|
311 |
+
# Plot filled hexagons
|
312 |
+
for hc in highl:
|
313 |
+
hx = hc[0] + np.array([0, i, i, 0, -i, -i])
|
314 |
+
hy = hc[1] + np.array([a, a/2, -a/2, -a, -a/2, a/2])
|
315 |
+
ax.fill(hx, hy, color=colour_palette[1], alpha=0.15, edgecolor=None) # Adjust color and alpha as needed
|
316 |
+
|
317 |
+
# # Create grid coordinates
|
318 |
+
# #x = np.arange(30, 121,1 )
|
319 |
+
# y_b = np.arange(df_bip_league['launch_angle'].median()-df_bip_league['launch_angle'].std(),
|
320 |
+
# df_bip_league['launch_angle'].median()+df_bip_league['launch_angle'].std(),1 )
|
321 |
+
|
322 |
+
# z_b = np.arange(df_bip_league['h_la'].median()-df_bip_league['h_la'].std(),
|
323 |
+
# df_bip_league['h_la'].median()+df_bip_league['h_la'].std(),1 )
|
324 |
+
|
325 |
+
# # Create a meshgrid
|
326 |
+
# Y_b, Z_b = np.meshgrid( y_b,z_b, indexing='ij')
|
327 |
+
# # Flatten the meshgrid to get x and y coordinates
|
328 |
+
|
329 |
+
# y_flat_b = Y_b.flatten()
|
330 |
+
# z_flat_b = Z_b.flatten()
|
331 |
+
|
332 |
+
# # Create a DataFrame
|
333 |
+
# df_league_base = pd.DataFrame({'launch_angle': y_flat_b,'h_la':z_flat_b,'c':[0]*len(y_flat_b)})
|
334 |
+
|
335 |
+
# h_league = ax.hexbin(x=df_league_base['h_la'],
|
336 |
+
# y=df_league_base['launch_angle'],
|
337 |
+
# gridsize=25,
|
338 |
+
# edgecolors=colour_palette[1],
|
339 |
+
# extent=extents,mincnt=1,lw=2,zorder=-3,)
|
340 |
+
|
341 |
+
# #get hexagon centers that should be highlighted
|
342 |
+
# verts = h_league.get_offsets()
|
343 |
+
# cnts = h_league.get_array()
|
344 |
+
# highl = verts[cnts > .5*cnts.max()]
|
345 |
+
|
346 |
+
# #create hexagon lines
|
347 |
+
# a = ((verts[0,1]-verts[1,1])/3).round(6)
|
348 |
+
# i = ((verts[1:,0]-verts[:-1,0])/2).round(6)
|
349 |
+
# i = i[i>0][0]
|
350 |
+
# lines = np.concatenate([hexLines(a,i,off) for off in highl])
|
351 |
+
|
352 |
+
# #select contour lines and draw
|
353 |
+
# uls,c = np.unique(lines.round(4),axis=0,return_counts=True)
|
354 |
+
# for l in uls[c==1]: ax.plot(*l.transpose(),'w-',lw=2,scalex=False,scaley=False,color=colour_palette[3],zorder=99)
|
355 |
+
|
356 |
+
|
357 |
+
axheader.text(s=f"{df_batter['batter_name'].values[0]} - {int(quant*100)}th% EV and Greater Batted Ball Tendencies",x=0.5,y=0.2,fontsize=20,ha='center',va='bottom')
|
358 |
+
axheader.text(s=f"2023 Season",x=0.5,y=-0.1,fontsize=14,ha='center',va='top')
|
359 |
+
|
360 |
+
ax.set_xlabel(f"Horizontal Spray Angle (°)",fontsize=12)
|
361 |
+
ax.set_ylabel(f"Vertical Launch Angle (°)",fontsize=12)
|
362 |
+
|
363 |
+
ax2_.text(x=0.5,
|
364 |
+
y=0.0,
|
365 |
+
|
366 |
+
s="Notes:\n" \
|
367 |
+
f"- {int(quant*100)}th% EV and Greater BBE is defined as a batter's top {100 - int(quant*100)}% hardest hit BBE\n" \
|
368 |
+
f"- Colour Scale and Number Labels Represents the Expected Total Bases for a batter's range of Best Speeds\n" \
|
369 |
+
f"- Shaded Area Represents the 2-D Region bounded by ±1σ Launch Angle and Horizontal Spray Angle on batter's Best Speed BBE\n"\
|
370 |
+
f"- {df_batter['batter_name'].values[0]} {int(quant*100)}th% EV and Greater BBE Range from {df_batter['launch_speed'].min():.0f} to {df_batter['launch_speed'].max():.0f} mph ({len(df_batter)} BBE)\n"\
|
371 |
+
f"- Positive Horizontal Spray Angle Represents a BBE hit in same direction as batter handedness (i.e. Pulled)" ,
|
372 |
+
|
373 |
+
fontsize=11,
|
374 |
+
fontstyle='oblique',
|
375 |
+
va='bottom',
|
376 |
+
ha='center',
|
377 |
+
bbox=dict(facecolor='white', edgecolor='black'),ma='left')
|
378 |
+
|
379 |
+
axfooter1.text(0.05, 0.5, "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
|
380 |
+
axfooter1.text(0.95, 0.5, "Data: MLB",ha='right', va='bottom',fontsize=12)
|
381 |
+
|
382 |
+
if df_batter['batter_hand'].values[0] == 'R':
|
383 |
+
ax.invert_xaxis()
|
384 |
+
ax.grid(False)
|
385 |
+
ax.axis('equal')
|
386 |
+
# Adjusting subplot to center it within the figure
|
387 |
+
fig.subplots_adjust(left=0.01, right=0.99, top=0.975, bottom=0.025)
|
388 |
+
|
389 |
+
#ax.text(f"Vertical Spray Angle (°)")
|
390 |
+
|
391 |
+
|
392 |
+
@output
|
393 |
+
@render.plot(alt="roll_plot")
|
394 |
+
def roll_plot():
|
395 |
+
# player_select = 'Nolan Gorman'
|
396 |
+
# player_select_full =player_select
|
397 |
+
|
398 |
+
if input.batter_id() is "":
|
399 |
+
fig = plt.figure(figsize=(12, 12))
|
400 |
+
fig.text(s='Please Select a Batter',x=0.5,y=0.5)
|
401 |
+
return
|
402 |
+
|
403 |
+
# df_will = df_model_2023[df_model_2023.batter_name == player_select].sort_values(by=['game_date','start_time'])
|
404 |
+
# df_will = df_will[df_will['is_swing'] != 1]
|
405 |
+
batter_select_id = int(input.batter_id())
|
406 |
+
# batter_select_name = 'Edouard Julien'
|
407 |
+
df_batter_og = df_2023_bip_train[df_2023_bip_train['batter_id']==batter_select_id]
|
408 |
+
batter_select_name = df_batter_og['batter_name'].values[0]
|
409 |
+
win = min(int(input.rolling_window()),len(df_batter_og))
|
410 |
+
df_2023_output = df_2023_output_copy[df_2023_output_copy['bip'] >= win]
|
411 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
412 |
+
#fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
|
413 |
+
|
414 |
+
from matplotlib.gridspec import GridSpec
|
415 |
+
# fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
|
416 |
+
fig = plt.figure(figsize=(12,12))
|
417 |
+
gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
|
418 |
+
|
419 |
+
axheader = fig.add_subplot(gs[0, :])
|
420 |
+
ax10 = fig.add_subplot(gs[1, 0])
|
421 |
+
ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
|
422 |
+
ax12 = fig.add_subplot(gs[1, 2])
|
423 |
+
axfooter1 = fig.add_subplot(gs[-1, :])
|
424 |
+
|
425 |
+
axheader.axis('off')
|
426 |
+
ax10.axis('off')
|
427 |
+
ax12.axis('off')
|
428 |
+
axfooter1.axis('off')
|
429 |
+
|
430 |
+
|
431 |
+
sns.lineplot( x= range(win,len(df_batter_og.y_pred.rolling(window=win).mean())+1),
|
432 |
+
y= df_batter_og.y_pred.rolling(window=win).mean().dropna(),
|
433 |
+
color=colour_palette[0],linewidth=2,ax=ax)
|
434 |
+
|
435 |
+
ax.hlines(y=df_batter_og.y_pred.mean(),xmin=win,xmax=len(df_batter_og),color=colour_palette[0],linestyle='--',
|
436 |
+
label=f'{batter_select_name} Average: {df_batter_og.y_pred.mean():.3f} xSLGCON ({p.ordinal(int(np.around(percentileofscore(df_2023_output["xslgcon"],df_batter_og.y_pred.mean(), kind="strict"))))} Percentile)')
|
437 |
+
|
438 |
+
# ax.hlines(y=df_model_2023.y_pred_no_swing.std()*100,xmin=win,xmax=len(df_will))
|
439 |
+
|
440 |
+
# sns.scatterplot( x= [976],
|
441 |
+
# y= df_will.y_pred.rolling(window=win).mean().min()*100,
|
442 |
+
# color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
|
443 |
+
|
444 |
+
|
445 |
+
ax.hlines(y=df_2023_bip_train['y_pred'].mean(),xmin=win,xmax=len(df_batter_og),color=colour_palette[1],linestyle='-.',alpha=1,
|
446 |
+
label = f'MLB Average: {df_2023_bip_train["y_pred"].mean():.3f} xSLGCON')
|
447 |
+
|
448 |
+
ax.legend()
|
449 |
+
|
450 |
+
hard_hit_dates = [df_2023_output['xslgcon'].quantile(0.9),
|
451 |
+
df_2023_output['xslgcon'].quantile(0.75),
|
452 |
+
df_2023_output['xslgcon'].quantile(0.25),
|
453 |
+
df_2023_output['xslgcon'].quantile(0.1)]
|
454 |
+
|
455 |
+
|
456 |
+
|
457 |
+
ax.hlines(y=df_2023_output['xslgcon'].quantile(0.9),xmin=win,xmax=len(df_batter_og),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)
|
458 |
+
ax.hlines(y=df_2023_output['xslgcon'].quantile(0.75),xmin=win,xmax=len(df_batter_og),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)
|
459 |
+
ax.hlines(y=df_2023_output['xslgcon'].quantile(0.25),xmin=win,xmax=len(df_batter_og),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)
|
460 |
+
ax.hlines(y=df_2023_output['xslgcon'].quantile(0.1),xmin=win,xmax=len(df_batter_og),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)
|
461 |
+
|
462 |
+
hard_hit_text = ['90th %','75th %','25th %','10th %']
|
463 |
+
for i, x in enumerate(hard_hit_dates):
|
464 |
+
ax.text(min(win+win/50,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
|
465 |
+
bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
|
466 |
+
|
467 |
+
# # Annotate with an arrow
|
468 |
+
# ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
|
469 |
+
# xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
|
470 |
+
# arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
|
471 |
+
# bbox=dict(facecolor='white', edgecolor='black'),va='top')
|
472 |
+
|
473 |
+
ax.set_xlim(win,len(df_batter_og))
|
474 |
+
# ax.set_ylim(0.2,max(1,))
|
475 |
+
|
476 |
+
ax.set_yticks([0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1])
|
477 |
+
|
478 |
+
ax.set_xlabel('Balls In Play')
|
479 |
+
ax.set_ylabel('Expected Total Bases per Ball In Play (xSLGCON)')
|
480 |
+
|
481 |
+
from matplotlib.ticker import FormatStrFormatter
|
482 |
+
|
483 |
+
ax.yaxis.set_major_formatter(FormatStrFormatter('%.3f'))
|
484 |
+
|
485 |
+
axheader.text(s=f'{batter_select_name} - MLB - {win} Rolling BIP Expected Slugging on Contact (xSLGCON)',x=0.5,y=-0.5,ha='center',va='bottom',fontsize=14)
|
486 |
+
axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
|
487 |
+
axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
|
488 |
+
|
489 |
+
fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
|
490 |
+
|
491 |
+
damage = App(ui.page_fluid(
|
492 |
+
ui.tags.base(href=base_url),
|
493 |
+
ui.tags.div(
|
494 |
+
{"style": "width:95%;margin: 0 auto;max-width: 1600px;"},
|
495 |
+
ui.tags.style(
|
496 |
+
"""
|
497 |
+
h4 {
|
498 |
+
margin-top: 1em;font-size:35px;
|
499 |
+
}
|
500 |
+
h2{
|
501 |
+
font-size:25px;
|
502 |
+
}
|
503 |
+
"""
|
504 |
+
),
|
505 |
+
shinyswatch.theme.simplex(),
|
506 |
+
ui.tags.h4("TJStats"),
|
507 |
+
ui.tags.i("Baseball Analytics and Visualizations"),
|
508 |
+
ui.navset_tab(
|
509 |
+
ui.nav_control(
|
510 |
+
ui.a(
|
511 |
+
"Home",
|
512 |
+
href="home/"
|
513 |
+
),
|
514 |
+
),
|
515 |
+
ui.nav_menu(
|
516 |
+
"Batter Charts",
|
517 |
+
ui.nav_control(
|
518 |
+
ui.a(
|
519 |
+
"Spray",
|
520 |
+
href="spray/"
|
521 |
+
),
|
522 |
+
ui.a(
|
523 |
+
"Decision Value",
|
524 |
+
href="decision_value/"
|
525 |
+
),
|
526 |
+
ui.a(
|
527 |
+
"Damage Model",
|
528 |
+
href="damage_model/"
|
529 |
+
),
|
530 |
+
ui.a(
|
531 |
+
"Batter Scatter",
|
532 |
+
href="batter_scatter/"
|
533 |
+
),
|
534 |
+
ui.a(
|
535 |
+
"EV vs LA Plot",
|
536 |
+
href="ev_angle/"
|
537 |
+
)
|
538 |
+
),
|
539 |
+
),
|
540 |
+
ui.nav_menu(
|
541 |
+
"Goalie Charts",
|
542 |
+
ui.nav_control(
|
543 |
+
ui.a(
|
544 |
+
"GSAx Timeline",
|
545 |
+
href="gsax-timeline/"
|
546 |
+
),
|
547 |
+
ui.a(
|
548 |
+
"GSAx Leaderboard",
|
549 |
+
href="gsax-leaderboard/"
|
550 |
+
),
|
551 |
+
ui.a(
|
552 |
+
"GSAx Comparison",
|
553 |
+
href="gsax-comparison/"
|
554 |
+
)
|
555 |
+
),
|
556 |
+
),ui.nav_menu(
|
557 |
+
"Team Charts",
|
558 |
+
ui.nav_control(
|
559 |
+
ui.a(
|
560 |
+
"Team xG Rates",
|
561 |
+
href="team-xg-rates/"
|
562 |
+
),
|
563 |
+
),
|
564 |
+
),ui.nav_control(
|
565 |
+
ui.a(
|
566 |
+
"Games",
|
567 |
+
href="games/"
|
568 |
+
),
|
569 |
+
),ui.nav_control(
|
570 |
+
ui.a(
|
571 |
+
"About",
|
572 |
+
href="about/"
|
573 |
+
),
|
574 |
+
),ui.nav_control(
|
575 |
+
ui.a(
|
576 |
+
"Articles",
|
577 |
+
href="articles/"
|
578 |
+
),
|
579 |
+
)),ui.row(
|
580 |
+
ui.layout_sidebar(
|
581 |
+
|
582 |
+
ui.panel_sidebar(
|
583 |
+
ui.input_select("batter_id",
|
584 |
+
"Select Batter",
|
585 |
+
batter_dict,
|
586 |
+
width=1,
|
587 |
+
size=1,
|
588 |
+
selectize=True),
|
589 |
+
ui.input_numeric("quant",
|
590 |
+
"Select Percentile",
|
591 |
+
value=50,
|
592 |
+
min=0,max=100),
|
593 |
+
ui.input_numeric("rolling_window",
|
594 |
+
"Select Rolling Window",
|
595 |
+
value=50,
|
596 |
+
min=1)),
|
597 |
+
|
598 |
+
ui.panel_main(
|
599 |
+
ui.navset_tab(
|
600 |
+
|
601 |
+
ui.nav("Damage Hex",
|
602 |
+
ui.output_plot('hex_plot',
|
603 |
+
width='1200px',
|
604 |
+
height='1200px')),
|
605 |
+
ui.nav("Damage Roll",
|
606 |
+
ui.output_plot('roll_plot',
|
607 |
+
width='1200px',
|
608 |
+
height='1200px'))
|
609 |
+
))
|
610 |
+
)),)),server)
|
decision_value.py
ADDED
@@ -0,0 +1,683 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
|
2 |
+
import datasets
|
3 |
+
from datasets import load_dataset
|
4 |
+
import pandas as pd
|
5 |
+
import numpy as np
|
6 |
+
import matplotlib.pyplot as plt
|
7 |
+
import seaborn as sns
|
8 |
+
import numpy as np
|
9 |
+
from scipy.stats import gaussian_kde
|
10 |
+
import matplotlib
|
11 |
+
from matplotlib.ticker import MaxNLocator
|
12 |
+
from matplotlib.gridspec import GridSpec
|
13 |
+
from scipy.stats import zscore
|
14 |
+
import math
|
15 |
+
import matplotlib
|
16 |
+
from adjustText import adjust_text
|
17 |
+
import matplotlib.ticker as mtick
|
18 |
+
from shinywidgets import output_widget, render_widget
|
19 |
+
import pandas as pd
|
20 |
+
from configure import base_url
|
21 |
+
import shinyswatch
|
22 |
+
|
23 |
+
### Import Datasets
|
24 |
+
dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv' ])
|
25 |
+
dataset_train = dataset['train']
|
26 |
+
df_2023_mlb = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
|
27 |
+
|
28 |
+
### Import Datasets
|
29 |
+
dataset = load_dataset('nesticot/mlb_data', data_files=['aaa_pitch_data_2023.csv' ])
|
30 |
+
dataset_train = dataset['train']
|
31 |
+
df_2023_aaa = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
|
32 |
+
|
33 |
+
df_2023_mlb['level'] = 'MLB'
|
34 |
+
df_2023_aaa['level'] = 'AAA'
|
35 |
+
|
36 |
+
df_2023 = pd.concat([df_2023_mlb,df_2023_aaa])
|
37 |
+
|
38 |
+
#print(df_2023)
|
39 |
+
### Normalize Hit Locations
|
40 |
+
import joblib
|
41 |
+
swing_model = joblib.load('swing.joblib')
|
42 |
+
|
43 |
+
no_swing_model = joblib.load('no_swing.joblib')
|
44 |
+
|
45 |
+
# Now you can use the loaded model for prediction or any other task
|
46 |
+
|
47 |
+
|
48 |
+
batter_dict = df_2023.sort_values('batter_name').set_index('batter_id')['batter_name'].to_dict()
|
49 |
+
|
50 |
+
## Make Predictions
|
51 |
+
## Define Features and Target
|
52 |
+
features = ['px','pz','strikes','balls']
|
53 |
+
## Set up 2023 Data for Prediction of Run Expectancy
|
54 |
+
df_model_2023_no_swing = df_2023[df_2023.is_swing != 1].dropna(subset=features)
|
55 |
+
df_model_2023_swing = df_2023[df_2023.is_swing == 1].dropna(subset=features)
|
56 |
+
|
57 |
+
|
58 |
+
import xgboost as xgb
|
59 |
+
df_model_2023_no_swing['y_pred'] = no_swing_model.predict(xgb.DMatrix(df_model_2023_no_swing[features]))
|
60 |
+
df_model_2023_swing['y_pred'] = swing_model.predict(xgb.DMatrix(df_model_2023_swing[features]))
|
61 |
+
|
62 |
+
df_model_2023 = pd.concat([df_model_2023_no_swing,df_model_2023_swing])
|
63 |
+
import joblib
|
64 |
+
# # Dump the model to a file named 'model.joblib'
|
65 |
+
# model = joblib.load('xtb_model.joblib')
|
66 |
+
|
67 |
+
# ## Create a Dataset to calculate xRV/100 Pitches
|
68 |
+
# df_model_2023['pitcher_name'] = df_model_2023.pitcher.map(pitcher_dict)
|
69 |
+
# df_model_2023['player_team'] = df_model_2023.batter.map(team_player_dict)
|
70 |
+
df_model_2023_group = df_model_2023.groupby(['batter_id','batter_name','level']).agg(
|
71 |
+
pitches = ('start_speed','count'),
|
72 |
+
y_pred = ('y_pred','mean'),
|
73 |
+
)
|
74 |
+
|
75 |
+
## Minimum 500 pitches faced
|
76 |
+
#min_pitches = 300
|
77 |
+
#df_model_2023_group = df_model_2023_group[df_model_2023_group.pitches >= min_pitches]
|
78 |
+
## Calculate 20-80 Scale
|
79 |
+
df_model_2023_group['decision_value'] = zscore(df_model_2023_group['y_pred'])
|
80 |
+
df_model_2023_group['decision_value'] = (50+df_model_2023_group['decision_value']*10)
|
81 |
+
|
82 |
+
## Create a Dataset to calculate xRV/100 for Pitches Taken
|
83 |
+
df_model_2023_group_no_swing = df_model_2023[df_model_2023.is_swing!=1].groupby(['batter_id','batter_name','level']).agg(
|
84 |
+
pitches = ('start_speed','count'),
|
85 |
+
y_pred = ('y_pred','mean')
|
86 |
+
)
|
87 |
+
|
88 |
+
# Select Pitches with 500 total pitches
|
89 |
+
df_model_2023_group_no_swing = df_model_2023_group_no_swing[df_model_2023_group_no_swing.index.get_level_values(1).isin(df_model_2023_group.index.get_level_values(1))]
|
90 |
+
## Calculate 20-80 Scale
|
91 |
+
df_model_2023_group_no_swing['iz_awareness'] = zscore(df_model_2023_group_no_swing['y_pred'])
|
92 |
+
df_model_2023_group_no_swing['iz_awareness'] = (((50+df_model_2023_group_no_swing['iz_awareness']*10)))
|
93 |
+
|
94 |
+
## Create a Dataset for xRV/100 Pitches Swung At
|
95 |
+
df_model_2023_group_swing = df_model_2023[df_model_2023.is_swing==1].groupby(['batter_id','batter_name','level']).agg(
|
96 |
+
pitches = ('start_speed','count'),
|
97 |
+
y_pred = ('y_pred','mean')
|
98 |
+
)
|
99 |
+
|
100 |
+
# Select Pitches with 500 total pitches
|
101 |
+
df_model_2023_group_swing = df_model_2023_group_swing[df_model_2023_group_swing.index.get_level_values(1).isin(df_model_2023_group.index.get_level_values(1))]
|
102 |
+
## Calculate 20-80 Scale
|
103 |
+
df_model_2023_group_swing['oz_awareness'] = zscore(df_model_2023_group_swing['y_pred'])
|
104 |
+
df_model_2023_group_swing['oz_awareness'] = (((50+df_model_2023_group_swing['oz_awareness']*10)))
|
105 |
+
|
106 |
+
## Create df for plotting
|
107 |
+
# Merge Datasets
|
108 |
+
df_model_2023_group_swing_plus_no = df_model_2023_group_swing.merge(df_model_2023_group_no_swing,left_index=True,right_index=True,suffixes=['_swing','_no_swing'])
|
109 |
+
df_model_2023_group_swing_plus_no['pitches'] = df_model_2023_group_swing_plus_no.pitches_swing + df_model_2023_group_swing_plus_no.pitches_no_swing
|
110 |
+
|
111 |
+
# Calculate xRV/100 Pitches
|
112 |
+
df_model_2023_group_swing_plus_no['y_pred'] = (df_model_2023_group_swing_plus_no.y_pred_swing*df_model_2023_group_swing_plus_no.pitches_swing + \
|
113 |
+
df_model_2023_group_swing_plus_no.y_pred_no_swing*df_model_2023_group_swing_plus_no.pitches_no_swing) / \
|
114 |
+
df_model_2023_group_swing_plus_no.pitches
|
115 |
+
|
116 |
+
df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no.merge(right=df_model_2023_group,
|
117 |
+
left_index=True,
|
118 |
+
right_index=True,
|
119 |
+
suffixes=['','_y'])
|
120 |
+
|
121 |
+
df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no.reset_index()
|
122 |
+
team_dict = df_2023.groupby(['batter_name'])[['batter_id','batter_team']].tail().set_index('batter_id')['batter_team'].to_dict()
|
123 |
+
df_model_2023_group_swing_plus_no['team'] = df_model_2023_group_swing_plus_no['batter_id'].map(team_dict)
|
124 |
+
df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no.set_index(['batter_id','batter_name','level','team'])
|
125 |
+
|
126 |
+
df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no[df_model_2023_group_swing_plus_no['pitches']>=250]
|
127 |
+
df_model_2023_group_swing_plus_no_copy = df_model_2023_group_swing_plus_no.copy()
|
128 |
+
import matplotlib
|
129 |
+
|
130 |
+
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
131 |
+
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
132 |
+
|
133 |
+
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]])
|
134 |
+
cmap_hue2 = matplotlib.colors.LinearSegmentedColormap.from_list("",['#ffffff',colour_palette[0]])
|
135 |
+
|
136 |
+
|
137 |
+
from matplotlib.pyplot import text
|
138 |
+
import inflect
|
139 |
+
from scipy.stats import percentileofscore
|
140 |
+
p = inflect.engine()
|
141 |
+
|
142 |
+
|
143 |
+
|
144 |
+
|
145 |
+
def server(input,output,session):
|
146 |
+
|
147 |
+
@output
|
148 |
+
@render.plot(alt="hex_plot")
|
149 |
+
def scatter_plot():
|
150 |
+
|
151 |
+
if input.batter_id() is "":
|
152 |
+
fig = plt.figure(figsize=(12, 12))
|
153 |
+
fig.text(s='Please Select a Batter',x=0.5,y=0.5)
|
154 |
+
return
|
155 |
+
print(df_model_2023_group_swing_plus_no_copy)
|
156 |
+
print(input.level_list())
|
157 |
+
df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no_copy[df_model_2023_group_swing_plus_no_copy.index.get_level_values(2) == input.level_list()]
|
158 |
+
print('this one')
|
159 |
+
print(df_model_2023_group_swing_plus_no)
|
160 |
+
batter_select_id = int(input.batter_id())
|
161 |
+
# batter_select_name = 'Edouard Julien'
|
162 |
+
#max(1,int(input.pitch_min()))
|
163 |
+
plot_min = max(250,int(input.pitch_min()))
|
164 |
+
df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no[df_model_2023_group_swing_plus_no.pitches >= plot_min]
|
165 |
+
## Plot In-Zone vs Out-of-Zone Awareness
|
166 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
167 |
+
# fig, ax = plt.subplots(1,1,figsize=(12,12))
|
168 |
+
fig = plt.figure(figsize=(12,12))
|
169 |
+
gs = GridSpec(3, 3, height_ratios=[0.6,10,0.2], width_ratios=[0.25,0.50,0.25])
|
170 |
+
|
171 |
+
axheader = fig.add_subplot(gs[0, :])
|
172 |
+
#ax10 = fig.add_subplot(gs[1, 0])
|
173 |
+
ax = fig.add_subplot(gs[1, :]) # Subplot at the top-right position
|
174 |
+
#ax12 = fig.add_subplot(gs[1, 2])
|
175 |
+
axfooter1 = fig.add_subplot(gs[-1, 0])
|
176 |
+
axfooter2 = fig.add_subplot(gs[-1, 1])
|
177 |
+
axfooter3 = fig.add_subplot(gs[-1, 2])
|
178 |
+
|
179 |
+
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],colour_palette[3],colour_palette[0]])
|
180 |
+
norm = plt.Normalize(df_model_2023_group_swing_plus_no['y_pred'].min()*100, df_model_2023_group_swing_plus_no['y_pred'].max()*100)
|
181 |
+
|
182 |
+
sns.scatterplot(
|
183 |
+
x=df_model_2023_group_swing_plus_no['y_pred_swing']*100,
|
184 |
+
y=df_model_2023_group_swing_plus_no['y_pred_no_swing']*100,
|
185 |
+
hue=df_model_2023_group_swing_plus_no['y_pred']*100,
|
186 |
+
size=df_model_2023_group_swing_plus_no['pitches_swing']/df_model_2023_group_swing_plus_no['pitches'],
|
187 |
+
palette=cmap_hue,ax=ax)
|
188 |
+
|
189 |
+
sm = plt.cm.ScalarMappable(cmap=cmap_hue, norm=norm)
|
190 |
+
cbar = plt.colorbar(sm, cax=axfooter2, orientation='horizontal',shrink=1)
|
191 |
+
cbar.set_label('Decision Value xRV/100 Pitches',fontsize=12)
|
192 |
+
|
193 |
+
ax.hlines(xmin=(math.floor((df_model_2023_group_swing_plus_no['y_pred_swing'].min()*100*100-0.01)/5))*5/100,
|
194 |
+
xmax= (math.ceil((df_model_2023_group_swing_plus_no['y_pred_swing'].max()**100100+0.01)/5))*5/100,
|
195 |
+
y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].mean()*100,color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
|
196 |
+
|
197 |
+
ax.vlines(ymin=(math.floor((df_model_2023_group_swing_plus_no['y_pred_no_swing'].min()*100*100-0.01)/5))*5/100,
|
198 |
+
ymax= (math.ceil((df_model_2023_group_swing_plus_no['y_pred_no_swing'].max()*100*100+0.01)/5))*5/100,
|
199 |
+
x=df_model_2023_group_swing_plus_no['y_pred_swing'].mean()*100,color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
|
200 |
+
|
201 |
+
x_lim_min = (math.floor((df_model_2023_group_swing_plus_no['y_pred_swing'].min()*100*100)/5))*5/100
|
202 |
+
x_lim_max = (math.ceil((df_model_2023_group_swing_plus_no['y_pred_swing'].max()*100*100)/5))*5/100
|
203 |
+
|
204 |
+
y_lim_min = (math.floor((df_model_2023_group_swing_plus_no['y_pred_no_swing'].min()*100*100)/5))*5/100
|
205 |
+
y_lim_max = (math.ceil((df_model_2023_group_swing_plus_no['y_pred_no_swing'].max()*100*100)/5))*5/100
|
206 |
+
|
207 |
+
ax.set_xlim(x_lim_min,x_lim_max)
|
208 |
+
ax.set_ylim(y_lim_min,y_lim_max)
|
209 |
+
|
210 |
+
ax.tick_params(axis='both', which='major', labelsize=12)
|
211 |
+
|
212 |
+
ax.set_xlabel('Out-of-Zone Awareness Value xRV/100 Swings',fontsize=16)
|
213 |
+
ax.set_ylabel('In-Zone Awareness Value xRV/100 Takes',fontsize=16)
|
214 |
+
ax.get_legend().remove()
|
215 |
+
|
216 |
+
|
217 |
+
ts=[]
|
218 |
+
|
219 |
+
|
220 |
+
# thresh = 0.5
|
221 |
+
# thresh_2 = -0.9
|
222 |
+
# for i in range(len(df_model_2023_group_swing_plus_no)):
|
223 |
+
# if (df_model_2023_group_swing_plus_no['y_pred'].values[i]*100) >= thresh or \
|
224 |
+
# (df_model_2023_group_swing_plus_no['y_pred'].values[i]*100) <= thresh_2 or \
|
225 |
+
# (str(df_model_2023_group_swing_plus_no.index.get_level_values(0).values[i]) in (input.name_list())) :
|
226 |
+
# ts.append(ax.text(x=df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]*100,
|
227 |
+
# y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]*100,
|
228 |
+
# s=df_model_2023_group_swing_plus_no.index.get_level_values(1).values[i],
|
229 |
+
# fontsize=8))
|
230 |
+
thresh = 0.5
|
231 |
+
thresh_2 = -0.9
|
232 |
+
for i in range(len(df_model_2023_group_swing_plus_no)):
|
233 |
+
if (df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]) >= df_model_2023_group_swing_plus_no['y_pred_swing'].quantile(0.98) or \
|
234 |
+
(df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]) <= df_model_2023_group_swing_plus_no['y_pred_swing'].quantile(0.02) or \
|
235 |
+
(df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]) >= df_model_2023_group_swing_plus_no['y_pred_no_swing'].quantile(0.98) or \
|
236 |
+
(df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]) <= df_model_2023_group_swing_plus_no['y_pred_no_swing'].quantile(0.02) or \
|
237 |
+
(df_model_2023_group_swing_plus_no['y_pred'].values[i]) >= df_model_2023_group_swing_plus_no['y_pred'].quantile(0.98) or \
|
238 |
+
(df_model_2023_group_swing_plus_no['y_pred'].values[i]) <= df_model_2023_group_swing_plus_no['y_pred'].quantile(0.02) or \
|
239 |
+
(str(df_model_2023_group_swing_plus_no.index.get_level_values(0).values[i]) in (input.name_list())) :
|
240 |
+
ts.append(ax.text(x=df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]*100,
|
241 |
+
y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]*100,
|
242 |
+
s=df_model_2023_group_swing_plus_no.index.get_level_values(1).values[i],
|
243 |
+
fontsize=8))
|
244 |
+
|
245 |
+
ax.text(x=x_lim_min+abs(x_lim_min)*0.02,y=y_lim_max-abs(y_lim_max-y_lim_min)*0.02,s=f'Min. {plot_min} Pitches',fontsize='10',fontstyle='oblique',va='top',
|
246 |
+
bbox=dict(facecolor='white', edgecolor='black'))
|
247 |
+
# ax.text(x=x_lim_min+abs(x_lim_min)*0.02,y=y_lim_max-abs(y_lim_max-y_lim_min)*0.06,s=f'Labels for Batters with\nDescion Value xRV/100 > {thresh:.2f}\nDescion Value xRV/100 < {thresh_2:.2f}',fontsize='10',fontstyle='oblique',va='top',
|
248 |
+
# bbox=dict(facecolor='white', edgecolor='black'))
|
249 |
+
ax.text(x=x_lim_min+abs(x_lim_min)*0.02,y=y_lim_max-abs(y_lim_max-y_lim_min)*0.06,s=f'Point Size Represents Swing%',fontsize='10',fontstyle='oblique',va='top',
|
250 |
+
bbox=dict(facecolor='white', edgecolor='black'))
|
251 |
+
|
252 |
+
adjust_text(ts,
|
253 |
+
arrowprops=dict(arrowstyle="-", color=colour_palette[4], lw=1),ax=ax)
|
254 |
+
|
255 |
+
axfooter1.axis('off')
|
256 |
+
axfooter3.axis('off')
|
257 |
+
axheader.axis('off')
|
258 |
+
|
259 |
+
axheader.text(s=f'{input.level_list()} In-Zone vs Out-of-Zone Awareness Value',fontsize=24,x=0.5,y=0,va='top',ha='center')
|
260 |
+
|
261 |
+
axfooter1.text(0.05, -0.5,"By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
|
262 |
+
axfooter3.text(0.95, -0.5, "Data: MLB",ha='right', va='bottom',fontsize=12)
|
263 |
+
fig.subplots_adjust(left=0.01, right=0.99, top=0.975, bottom=0.025)
|
264 |
+
|
265 |
+
@output
|
266 |
+
@render.plot(alt="hex_plot")
|
267 |
+
def dv_plot():
|
268 |
+
|
269 |
+
if input.batter_id() is "":
|
270 |
+
fig = plt.figure(figsize=(12, 12))
|
271 |
+
fig.text(s='Please Select a Batter',x=0.5,y=0.5)
|
272 |
+
return
|
273 |
+
|
274 |
+
player_select = int(input.batter_id())
|
275 |
+
player_select_full = batter_dict[player_select]
|
276 |
+
|
277 |
+
|
278 |
+
df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])
|
279 |
+
df_will = df_will[df_will['level']==input.level_list()]
|
280 |
+
# df_will['y_pred'] = df_will['y_pred'] - df_will['y_pred'].mean()
|
281 |
+
|
282 |
+
win = max(1,int(input.rolling_window()))
|
283 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
284 |
+
#fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
|
285 |
+
|
286 |
+
from matplotlib.gridspec import GridSpec
|
287 |
+
# fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
|
288 |
+
fig = plt.figure(figsize=(12,12))
|
289 |
+
gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
|
290 |
+
|
291 |
+
axheader = fig.add_subplot(gs[0, :])
|
292 |
+
ax10 = fig.add_subplot(gs[1, 0])
|
293 |
+
ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
|
294 |
+
ax12 = fig.add_subplot(gs[1, 2])
|
295 |
+
axfooter1 = fig.add_subplot(gs[-1, :])
|
296 |
+
|
297 |
+
axheader.axis('off')
|
298 |
+
ax10.axis('off')
|
299 |
+
ax12.axis('off')
|
300 |
+
axfooter1.axis('off')
|
301 |
+
|
302 |
+
|
303 |
+
sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),
|
304 |
+
y= df_will.y_pred.rolling(window=win).mean().dropna()*100,
|
305 |
+
color=colour_palette[0],linewidth=2,ax=ax,zorder=100)
|
306 |
+
|
307 |
+
ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',
|
308 |
+
label=f'{player_select_full} Average: {df_will.y_pred.mean()*100:.2} xRV/100 ({p.ordinal(int(np.around(percentileofscore(df_model_2023_group_swing_plus_no.y_pred,df_will.y_pred.mean(), kind="strict"))))} Percentile)')
|
309 |
+
|
310 |
+
# ax.hlines(y=df_model_2023.y_pred.std()*100,xmin=win,xmax=len(df_will))
|
311 |
+
|
312 |
+
# sns.scatterplot( x= [976],
|
313 |
+
# y= df_will.y_pred.rolling(window=win).mean().min()*100,
|
314 |
+
# color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
|
315 |
+
|
316 |
+
|
317 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[1],linestyle='-.',alpha=1,
|
318 |
+
label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred.mean()*100:.2f} xRV/100')
|
319 |
+
|
320 |
+
ax.legend()
|
321 |
+
|
322 |
+
hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred.quantile(0.9)*100,
|
323 |
+
df_model_2023_group_swing_plus_no.y_pred.quantile(0.75)*100,
|
324 |
+
df_model_2023_group_swing_plus_no.y_pred.quantile(0.25)*100,
|
325 |
+
df_model_2023_group_swing_plus_no.y_pred.quantile(0.1)*100]
|
326 |
+
|
327 |
+
|
328 |
+
|
329 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.9)*100,xmin=win,xmax=len(df_will),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)
|
330 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.75)*100,xmin=win,xmax=len(df_will),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)
|
331 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.25)*100,xmin=win,xmax=len(df_will),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)
|
332 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.1)*100,xmin=win,xmax=len(df_will),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)
|
333 |
+
|
334 |
+
hard_hit_text = ['90th %','75th %','25th %','10th %']
|
335 |
+
for i, x in enumerate(hard_hit_dates):
|
336 |
+
ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
|
337 |
+
bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
|
338 |
+
|
339 |
+
# # Annotate with an arrow
|
340 |
+
# ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
|
341 |
+
# xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
|
342 |
+
# arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
|
343 |
+
# bbox=dict(facecolor='white', edgecolor='black'),va='top')
|
344 |
+
|
345 |
+
ax.set_xlim(win,len(df_will))
|
346 |
+
#ax.set_ylim(-1.5,1.5)
|
347 |
+
ax.set_yticks([-1.5,-1,-0.5,0,0.5,1,1.5])
|
348 |
+
ax.set_xlabel('Pitch')
|
349 |
+
ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')
|
350 |
+
|
351 |
+
axheader.text(s=f'{player_select_full} - {input.level_list()} - {win} Pitch Rolling Swing Decision Expected Run Value Added',x=0.5,y=-0.5,ha='center',va='bottom',fontsize=14)
|
352 |
+
axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
|
353 |
+
axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
|
354 |
+
|
355 |
+
fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
|
356 |
+
#fig.set_facecolor(colour_palette[5])
|
357 |
+
|
358 |
+
@output
|
359 |
+
@render.plot(alt="hex_plot")
|
360 |
+
def iz_plot():
|
361 |
+
|
362 |
+
if input.batter_id() is "":
|
363 |
+
fig = plt.figure(figsize=(12, 12))
|
364 |
+
fig.text(s='Please Select a Batter',x=0.5,y=0.5)
|
365 |
+
return
|
366 |
+
|
367 |
+
player_select = int(input.batter_id())
|
368 |
+
player_select_full = batter_dict[player_select]
|
369 |
+
|
370 |
+
|
371 |
+
df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])
|
372 |
+
df_will = df_will[df_will['level']==input.level_list()]
|
373 |
+
df_will = df_will[df_will['is_swing'] != 1]
|
374 |
+
|
375 |
+
win = max(1,int(input.rolling_window()))
|
376 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
377 |
+
#fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
|
378 |
+
|
379 |
+
from matplotlib.gridspec import GridSpec
|
380 |
+
# fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
|
381 |
+
fig = plt.figure(figsize=(12,12))
|
382 |
+
gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
|
383 |
+
|
384 |
+
axheader = fig.add_subplot(gs[0, :])
|
385 |
+
ax10 = fig.add_subplot(gs[1, 0])
|
386 |
+
ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
|
387 |
+
ax12 = fig.add_subplot(gs[1, 2])
|
388 |
+
axfooter1 = fig.add_subplot(gs[-1, :])
|
389 |
+
|
390 |
+
axheader.axis('off')
|
391 |
+
ax10.axis('off')
|
392 |
+
ax12.axis('off')
|
393 |
+
axfooter1.axis('off')
|
394 |
+
|
395 |
+
|
396 |
+
sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),
|
397 |
+
y= df_will.y_pred.rolling(window=win).mean().dropna()*100,
|
398 |
+
color=colour_palette[0],linewidth=2,ax=ax,zorder=100)
|
399 |
+
|
400 |
+
ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',
|
401 |
+
label=f'{player_select_full} Average: {df_will.y_pred.mean()*100:.2} xRV/100 ({p.ordinal(int(np.around(percentileofscore(df_model_2023_group_swing_plus_no.y_pred_no_swing,df_will.y_pred.mean(), kind="strict"))))} Percentile)')
|
402 |
+
|
403 |
+
# ax.hlines(y=df_model_2023.y_pred_no_swing.std()*100,xmin=win,xmax=len(df_will))
|
404 |
+
|
405 |
+
# sns.scatterplot( x= [976],
|
406 |
+
# y= df_will.y_pred.rolling(window=win).mean().min()*100,
|
407 |
+
# color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
|
408 |
+
|
409 |
+
|
410 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[1],linestyle='-.',alpha=1,
|
411 |
+
label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred_no_swing.mean()*100:.2} xRV/100')
|
412 |
+
|
413 |
+
ax.legend()
|
414 |
+
|
415 |
+
hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.9)*100,
|
416 |
+
df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.75)*100,
|
417 |
+
df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.25)*100,
|
418 |
+
df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.1)*100]
|
419 |
+
|
420 |
+
|
421 |
+
|
422 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.9)*100,xmin=win,xmax=len(df_will),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)
|
423 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.75)*100,xmin=win,xmax=len(df_will),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)
|
424 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.25)*100,xmin=win,xmax=len(df_will),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)
|
425 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.1)*100,xmin=win,xmax=len(df_will),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)
|
426 |
+
|
427 |
+
hard_hit_text = ['90th %','75th %','25th %','10th %']
|
428 |
+
for i, x in enumerate(hard_hit_dates):
|
429 |
+
ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
|
430 |
+
bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
|
431 |
+
|
432 |
+
# # Annotate with an arrow
|
433 |
+
# ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
|
434 |
+
# xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
|
435 |
+
# arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
|
436 |
+
# bbox=dict(facecolor='white', edgecolor='black'),va='top')
|
437 |
+
|
438 |
+
ax.set_xlim(win,len(df_will))
|
439 |
+
ax.set_yticks([1.0,1.5,2.0,2.5,3.0])
|
440 |
+
# ax.set_ylim(1,3)
|
441 |
+
|
442 |
+
ax.set_xlabel('Takes')
|
443 |
+
ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')
|
444 |
+
|
445 |
+
axheader.text(s=f'{player_select_full} - {input.level_list()} - {win} Pitch Rolling In-Zone Awareness Expected Run Value Added',x=0.5,y=-0.5,ha='center',va='bottom',fontsize=14)
|
446 |
+
axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
|
447 |
+
axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
|
448 |
+
|
449 |
+
fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
|
450 |
+
|
451 |
+
@output
|
452 |
+
@render.plot(alt="hex_plot")
|
453 |
+
def oz_plot():
|
454 |
+
if input.batter_id() is "":
|
455 |
+
fig = plt.figure(figsize=(12, 12))
|
456 |
+
fig.text(s='Please Select a Batter',x=0.5,y=0.5)
|
457 |
+
return
|
458 |
+
|
459 |
+
player_select = int(input.batter_id())
|
460 |
+
player_select_full = batter_dict[player_select]
|
461 |
+
|
462 |
+
|
463 |
+
|
464 |
+
df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])
|
465 |
+
df_will = df_will[df_will['level']==input.level_list()]
|
466 |
+
df_will = df_will[df_will['is_swing'] == 1]
|
467 |
+
|
468 |
+
win = max(1,int(input.rolling_window()))
|
469 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
470 |
+
#fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
|
471 |
+
|
472 |
+
from matplotlib.gridspec import GridSpec
|
473 |
+
# fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
|
474 |
+
fig = plt.figure(figsize=(12,12))
|
475 |
+
gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
|
476 |
+
|
477 |
+
axheader = fig.add_subplot(gs[0, :])
|
478 |
+
ax10 = fig.add_subplot(gs[1, 0])
|
479 |
+
ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
|
480 |
+
ax12 = fig.add_subplot(gs[1, 2])
|
481 |
+
axfooter1 = fig.add_subplot(gs[-1, :])
|
482 |
+
|
483 |
+
axheader.axis('off')
|
484 |
+
ax10.axis('off')
|
485 |
+
ax12.axis('off')
|
486 |
+
axfooter1.axis('off')
|
487 |
+
|
488 |
+
|
489 |
+
sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),
|
490 |
+
y= df_will.y_pred.rolling(window=win).mean().dropna()*100,
|
491 |
+
color=colour_palette[0],linewidth=2,ax=ax,zorder=100)
|
492 |
+
|
493 |
+
ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',
|
494 |
+
label=f'{player_select_full} Average: {df_will.y_pred.mean()*100:.2} xRV/100 ({p.ordinal(int(np.around(percentileofscore(df_model_2023_group_swing_plus_no.y_pred_swing,df_will.y_pred.mean(), kind="strict"))))} Percentile)')
|
495 |
+
|
496 |
+
# ax.hlines(y=df_model_2023.y_pred_swing.std()*100,xmin=win,xmax=len(df_will))
|
497 |
+
|
498 |
+
# sns.scatterplot( x= [976],
|
499 |
+
# y= df_will.y_pred.rolling(window=win).mean().min()*100,
|
500 |
+
# color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
|
501 |
+
|
502 |
+
|
503 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[1],linestyle='-.',alpha=1,
|
504 |
+
label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred_swing.mean()*100:.2} xRV/100')
|
505 |
+
|
506 |
+
ax.legend()
|
507 |
+
|
508 |
+
hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.9)*100,
|
509 |
+
df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.75)*100,
|
510 |
+
df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.25)*100,
|
511 |
+
df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.1)*100]
|
512 |
+
|
513 |
+
|
514 |
+
|
515 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.9)*100,xmin=win,xmax=len(df_will),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)
|
516 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.75)*100,xmin=win,xmax=len(df_will),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)
|
517 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.25)*100,xmin=win,xmax=len(df_will),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)
|
518 |
+
ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.1)*100,xmin=win,xmax=len(df_will),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)
|
519 |
+
|
520 |
+
hard_hit_text = ['90th %','75th %','25th %','10th %']
|
521 |
+
for i, x in enumerate(hard_hit_dates):
|
522 |
+
ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
|
523 |
+
bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
|
524 |
+
|
525 |
+
# # Annotate with an arrow
|
526 |
+
# ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
|
527 |
+
# xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
|
528 |
+
# arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
|
529 |
+
# bbox=dict(facecolor='white', edgecolor='black'),va='top')
|
530 |
+
|
531 |
+
ax.set_xlim(win,len(df_will))
|
532 |
+
#ax.set_ylim(-3.25,-1.25)
|
533 |
+
ax.set_yticks([-3.25,-2.75,-2.25,-1.75,-1.25])
|
534 |
+
ax.set_xlabel('Swing')
|
535 |
+
ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')
|
536 |
+
|
537 |
+
axheader.text(s=f'{player_select_full} - {input.level_list()} - {win} Pitch Rolling Out of Zone Awareness Expected Run Value Added',x=0.5,y=-0.5,ha='center',va='bottom',fontsize=14)
|
538 |
+
axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
|
539 |
+
axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
|
540 |
+
|
541 |
+
fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
|
542 |
+
|
543 |
+
decision_value = App(ui.page_fluid(
|
544 |
+
ui.tags.base(href=base_url),
|
545 |
+
ui.tags.div(
|
546 |
+
{"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
|
547 |
+
ui.tags.style(
|
548 |
+
"""
|
549 |
+
h4 {
|
550 |
+
margin-top: 1em;font-size:35px;
|
551 |
+
}
|
552 |
+
h2{
|
553 |
+
font-size:25px;
|
554 |
+
}
|
555 |
+
"""
|
556 |
+
),
|
557 |
+
shinyswatch.theme.simplex(),
|
558 |
+
ui.tags.h4("TJStats"),
|
559 |
+
ui.tags.i("Baseball Analytics and Visualizations"),
|
560 |
+
ui.navset_tab(
|
561 |
+
ui.nav_control(
|
562 |
+
ui.a(
|
563 |
+
"Home",
|
564 |
+
href="home/"
|
565 |
+
),
|
566 |
+
),
|
567 |
+
ui.nav_menu(
|
568 |
+
"Batter Charts",
|
569 |
+
ui.nav_control(
|
570 |
+
ui.a(
|
571 |
+
"Spray",
|
572 |
+
href="spray/"
|
573 |
+
),
|
574 |
+
ui.a(
|
575 |
+
"Decision Value",
|
576 |
+
href="decision_value/"
|
577 |
+
),
|
578 |
+
ui.a(
|
579 |
+
"Damage Model",
|
580 |
+
href="damage_model/"
|
581 |
+
),
|
582 |
+
ui.a(
|
583 |
+
"Batter Scatter",
|
584 |
+
href="batter_scatter/"
|
585 |
+
),
|
586 |
+
ui.a(
|
587 |
+
"EV vs LA Plot",
|
588 |
+
href="ev_angle/"
|
589 |
+
)
|
590 |
+
),
|
591 |
+
),
|
592 |
+
ui.nav_menu(
|
593 |
+
"Goalie Charts",
|
594 |
+
ui.nav_control(
|
595 |
+
ui.a(
|
596 |
+
"GSAx Timeline",
|
597 |
+
href="gsax-timeline/"
|
598 |
+
),
|
599 |
+
ui.a(
|
600 |
+
"GSAx Leaderboard",
|
601 |
+
href="gsax-leaderboard/"
|
602 |
+
),
|
603 |
+
ui.a(
|
604 |
+
"GSAx Comparison",
|
605 |
+
href="gsax-comparison/"
|
606 |
+
)
|
607 |
+
),
|
608 |
+
),ui.nav_menu(
|
609 |
+
"Team Charts",
|
610 |
+
ui.nav_control(
|
611 |
+
ui.a(
|
612 |
+
"Team xG Rates",
|
613 |
+
href="team-xg-rates/"
|
614 |
+
),
|
615 |
+
),
|
616 |
+
),ui.nav_control(
|
617 |
+
ui.a(
|
618 |
+
"Games",
|
619 |
+
href="games/"
|
620 |
+
),
|
621 |
+
),ui.nav_control(
|
622 |
+
ui.a(
|
623 |
+
"About",
|
624 |
+
href="about/"
|
625 |
+
),
|
626 |
+
),ui.nav_control(
|
627 |
+
ui.a(
|
628 |
+
"Articles",
|
629 |
+
href="articles/"
|
630 |
+
),
|
631 |
+
)),ui.row(
|
632 |
+
ui.layout_sidebar(
|
633 |
+
|
634 |
+
ui.panel_sidebar(
|
635 |
+
|
636 |
+
|
637 |
+
ui.input_numeric("pitch_min",
|
638 |
+
"Select Pitch Minimum [min. 250] (Scatter)",
|
639 |
+
value=500,
|
640 |
+
min=250),
|
641 |
+
|
642 |
+
ui.input_select("name_list",
|
643 |
+
"Select Players to List (Scatter)",
|
644 |
+
batter_dict,
|
645 |
+
selectize=True,
|
646 |
+
multiple=True),
|
647 |
+
ui.input_select("batter_id",
|
648 |
+
"Select Batter (Rolling)",
|
649 |
+
batter_dict,
|
650 |
+
width=1,
|
651 |
+
size=1,
|
652 |
+
selectize=True),
|
653 |
+
ui.input_numeric("rolling_window",
|
654 |
+
"Select Rolling Window (Rolling)",
|
655 |
+
value=100,
|
656 |
+
min=1),
|
657 |
+
|
658 |
+
ui.input_select("level_list",
|
659 |
+
"Select Level",
|
660 |
+
['MLB','AAA'],
|
661 |
+
selected='MLB')),
|
662 |
+
|
663 |
+
ui.panel_main(
|
664 |
+
ui.navset_tab(
|
665 |
+
|
666 |
+
ui.nav("Scatter Plot",
|
667 |
+
ui.output_plot('scatter_plot',
|
668 |
+
width='1000px',
|
669 |
+
height='1000px')),
|
670 |
+
ui.nav("Rolling DV",
|
671 |
+
ui.output_plot('dv_plot',
|
672 |
+
width='1000px',
|
673 |
+
height='1000px')),
|
674 |
+
ui.nav("Rolling In-Zone",
|
675 |
+
ui.output_plot('iz_plot',
|
676 |
+
width='1000px',
|
677 |
+
height='1000px')),
|
678 |
+
ui.nav("Rolling Out-of-Zone",
|
679 |
+
ui.output_plot('oz_plot',
|
680 |
+
width='1000px',
|
681 |
+
height='1000px'))
|
682 |
+
))
|
683 |
+
)),)),server)
|
ev_angle.py
ADDED
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
|
2 |
+
import datasets
|
3 |
+
from datasets import load_dataset
|
4 |
+
import pandas as pd
|
5 |
+
import numpy as np
|
6 |
+
import matplotlib.pyplot as plt
|
7 |
+
import seaborn as sns
|
8 |
+
import numpy as np
|
9 |
+
from scipy.stats import gaussian_kde
|
10 |
+
import matplotlib
|
11 |
+
from matplotlib.ticker import MaxNLocator
|
12 |
+
from matplotlib.gridspec import GridSpec
|
13 |
+
from scipy.stats import zscore
|
14 |
+
import math
|
15 |
+
import matplotlib
|
16 |
+
from adjustText import adjust_text
|
17 |
+
import matplotlib.ticker as mtick
|
18 |
+
from shinywidgets import output_widget, render_widget
|
19 |
+
import pandas as pd
|
20 |
+
from configure import base_url
|
21 |
+
import shinyswatch
|
22 |
+
from matplotlib.pyplot import text
|
23 |
+
|
24 |
+
|
25 |
+
### Import Datasets
|
26 |
+
dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv' ])
|
27 |
+
dataset_train = dataset['train']
|
28 |
+
exit_velo_df = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
|
29 |
+
|
30 |
+
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
31 |
+
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
32 |
+
|
33 |
+
|
34 |
+
#exit_velo_df = pd.read_csv('exit_velo_df.csv',index_col=[0])
|
35 |
+
|
36 |
+
conditions = [
|
37 |
+
(exit_velo_df['launch_speed'].isna()),
|
38 |
+
(exit_velo_df['launch_speed']*1.5 - exit_velo_df['launch_angle'] >= 117 ) & (exit_velo_df['launch_speed'] + exit_velo_df['launch_angle'] >= 124) & (exit_velo_df['launch_speed'] > 98) & (exit_velo_df['launch_angle'] >= 8) & (exit_velo_df['launch_angle'] <= 50)
|
39 |
+
]
|
40 |
+
|
41 |
+
choices = [False,True]
|
42 |
+
exit_velo_df['barrel'] = np.select(conditions, choices, default=np.nan)
|
43 |
+
|
44 |
+
test_df = exit_velo_df.sort_values(by='batter_name').drop_duplicates(subset='batter_id').reset_index(drop=True)[['batter_id','batter_name']]#['pitcher'].to_dict()
|
45 |
+
test_df = test_df.set_index('batter_id')
|
46 |
+
|
47 |
+
#test_df = test_df[test_df.pitcher == 'Chris Bassitt'].append(test_df[test_df.pitcher != 'Chris Bassitt'])
|
48 |
+
|
49 |
+
batter_dict = test_df['batter_name'].to_dict()
|
50 |
+
|
51 |
+
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
52 |
+
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
53 |
+
|
54 |
+
angle_ev_list_df = pd.read_csv('angle_ev_list_df.csv')
|
55 |
+
ev_ranges = list(np.arange(97.5,130,0.1))
|
56 |
+
angle_ranges = list(range(8,51))
|
57 |
+
#print
|
58 |
+
|
59 |
+
|
60 |
+
def server(input,output,session):
|
61 |
+
@output
|
62 |
+
@render.plot(alt="A histogram")
|
63 |
+
def plot():
|
64 |
+
data_df = exit_velo_df[exit_velo_df.batter_id==int(input.id())]
|
65 |
+
#pitch_list = exit_velo_df_small.pitch_type.unique()
|
66 |
+
sns.set_theme(style="whitegrid", palette="pastel")
|
67 |
+
fig, ax = plt.subplots(1, 1, figsize=(10, 10))
|
68 |
+
|
69 |
+
|
70 |
+
|
71 |
+
if input.plot_id() == 'dist':
|
72 |
+
sns.histplot(x=data_df.launch_angle,y=data_df.launch_speed,cbar=colour_palette,binwidth=(5,2.5),ax=ax,cbar_kws=dict(shrink=.75,label='Count'),binrange=(
|
73 |
+
(math.floor((min(data_df.launch_angle.dropna())/5))*5,math.ceil((max(data_df.launch_angle.dropna())/5))*5),(math.floor((min(data_df.launch_speed.dropna())/5))*5,math.ceil((max(data_df.launch_speed.dropna())/5))*5)))
|
74 |
+
if input.plot_id() == 'scatter':
|
75 |
+
sns.scatterplot(x=data_df.launch_angle,y=data_df.launch_speed,color=colour_palette[1])
|
76 |
+
ax.set_xlim(math.floor((min(data_df.launch_angle.dropna())/10))*10,math.ceil((max(data_df.launch_angle.dropna())/10))*10)
|
77 |
+
#ticks=np.arange(revels.values.min(),revels.values.max()+1 )
|
78 |
+
sns.lineplot(x=angle_ev_list_df.launch_angle,y=angle_ev_list_df.launch_speed,color=colour_palette[0])
|
79 |
+
ax.vlines(x=angle_ev_list_df.launch_angle[0],ymin=angle_ev_list_df.launch_speed[0],ymax=ev_ranges[-1],color=colour_palette[0])
|
80 |
+
ax.vlines(x=angle_ev_list_df.launch_angle[len(angle_ev_list_df)-1],ymin=angle_ev_list_df.launch_speed[len(angle_ev_list_df)-1],ymax=ev_ranges[-1],color=colour_palette[0])
|
81 |
+
|
82 |
+
groundball = f'{sum(data_df.launch_angle.dropna()<=10)/len(data_df.launch_angle.dropna()):.1%}'
|
83 |
+
linedrive = f'{sum((data_df.launch_angle.dropna()<=25) & (data_df.launch_angle.dropna()>10))/len(data_df.launch_angle.dropna()):.1%}'
|
84 |
+
flyball = f'{sum((data_df.launch_angle.dropna()<=50) & (data_df.launch_angle.dropna()>25))/len(data_df.launch_angle.dropna()):.1%}'
|
85 |
+
popup = f'{sum(data_df.launch_angle.dropna()>50)/len(data_df.launch_angle.dropna()):.1%}'
|
86 |
+
percentages_list = [groundball,linedrive,flyball,popup]
|
87 |
+
|
88 |
+
hard_hit_percent = f'{sum(data_df.launch_speed.dropna()>=95)/len(data_df.launch_speed.dropna()):.1%}'
|
89 |
+
|
90 |
+
barrel_percentage = f'{data_df.barrel.dropna().sum()/len(data_df.launch_angle.dropna()):.1%}'
|
91 |
+
|
92 |
+
plt.text(x=27, y=math.ceil((max(data_df.launch_speed.dropna())/5))*5+5-3, s=f'Barrel% {barrel_percentage}',ha='left',bbox=dict(facecolor='white',alpha=0.8, edgecolor=colour_palette[4], pad=5))
|
93 |
+
|
94 |
+
|
95 |
+
sample_dates = np.array([math.floor((min(data_df.launch_angle.dropna())/10))*10,10,25,50])
|
96 |
+
sample_text = [f'Groundball ({groundball})',f'Line Drive ({linedrive})',f'Fly Ball ({flyball})',f'Pop-up ({popup})']
|
97 |
+
|
98 |
+
hard_hit_dates = [95]
|
99 |
+
hard_hit_text = [f'Hard Hit% ({hard_hit_percent})']
|
100 |
+
|
101 |
+
|
102 |
+
|
103 |
+
#sample_dates = mdates.date2num(sample_dates)
|
104 |
+
plt.hlines(y=hard_hit_dates,xmin=math.floor((min(data_df.launch_angle.dropna())/10))*10, xmax=math.ceil((max(data_df.launch_angle.dropna())/10))*10, color = colour_palette[4],linestyles='--')
|
105 |
+
plt.vlines(x=sample_dates, ymin=0, ymax=130, color = colour_palette[3],linestyles='--')
|
106 |
+
|
107 |
+
|
108 |
+
# ax.vlines(x=10,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
|
109 |
+
# ax.vlines(x=25,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
|
110 |
+
# ax.vlines(x=50,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
|
111 |
+
|
112 |
+
|
113 |
+
|
114 |
+
for i, x in enumerate(hard_hit_dates):
|
115 |
+
text(math.ceil((max(data_df.launch_angle.dropna())/10))*10-2.5, x+1.25,hard_hit_text[i], rotation=0, ha='right',
|
116 |
+
bbox=dict(facecolor='white',alpha=0.5, edgecolor=colour_palette[4], pad=5))
|
117 |
+
|
118 |
+
|
119 |
+
for i, x in enumerate(sample_dates):
|
120 |
+
text(x+0.75, (math.floor((min(data_df.launch_speed.dropna())/5))*5)+1,sample_text[i], rotation=90, verticalalignment='bottom',
|
121 |
+
bbox=dict(facecolor='white',alpha=0.5, edgecolor=colour_palette[3], pad=5))
|
122 |
+
#ax.vlines(x=math.floor((min(data_df.launch_angle.dropna())/10))*10+1,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
|
123 |
+
|
124 |
+
ax.set_xlim((math.floor((min(data_df.launch_angle.dropna())/10))*10,math.ceil((max(data_df.launch_angle.dropna())/10))*10))
|
125 |
+
ax.set_ylim((math.floor((min(data_df.launch_speed.dropna())/5))*5,math.ceil((max(data_df.launch_speed.dropna())/5))*5+5))
|
126 |
+
# ax.set_xlim(-90,90)
|
127 |
+
# ax.set_ylim(0,125)
|
128 |
+
ax.set_title(f'MLB - {data_df.batter_name.unique()[0]} Launch Angle vs EV Plot', fontsize=18,fontname='Century Gothic',)
|
129 |
+
#vals = ax.get_yticks()
|
130 |
+
ax.set_xlabel('Launch Angle', fontsize=16,fontname='Century Gothic')
|
131 |
+
ax.set_ylabel('Exit Velocity', fontsize=16,fontname='Century Gothic')
|
132 |
+
ax.fill_between(angle_ev_list_df.launch_angle, 130, angle_ev_list_df.launch_speed, interpolate=True, color=colour_palette[3],alpha=0.1,label='Barrel')
|
133 |
+
#fig.colorbar(plot_dist, ax=ax)
|
134 |
+
#fig.colorbar(plot_dist)
|
135 |
+
#fig.axes[0].invert_yaxis()
|
136 |
+
ax.legend(fontsize='16',loc='upper left')
|
137 |
+
fig.text(x=0.03,y=0.02,s='By: @TJStats')
|
138 |
+
fig.text(x=1-0.03,y=0.02,s='Data: MLB',ha='right')
|
139 |
+
|
140 |
+
# fig.text(x=0.25,y=0.02,s='Data: MLB',ha='right')
|
141 |
+
# fig.text(x=0.25,y=0.02,s='Data: MLB',ha='right')
|
142 |
+
# fig.text(x=0.25,y=0.02,s='Data: MLB',ha='right')
|
143 |
+
#cbar = plt.colorbar()
|
144 |
+
#fig.subplots_adjust(wspace=.02, hspace=.02)
|
145 |
+
#ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
|
146 |
+
fig.set_facecolor('white')
|
147 |
+
fig.tight_layout()
|
148 |
+
#matplotlib.rcParams["figure.dpi"] = 300
|
149 |
+
|
150 |
+
# ax.set_xlim(input.n(),exit_velo_df_small.pitch.max())
|
151 |
+
#ax.axis('off')
|
152 |
+
|
153 |
+
#fig.set_facecolor('white')
|
154 |
+
#fig.tight_layout()
|
155 |
+
#ax.hist(exit_velo_df[exit_velo_df.pitcher_id==int(input.id())]['pitch_velocity'],input.n(),density=True)
|
156 |
+
#plt.show()
|
157 |
+
#return g
|
158 |
+
|
159 |
+
# This is a shiny.App object. It must be named `app`.
|
160 |
+
|
161 |
+
# fig, ax = plt.subplots()
|
162 |
+
#print(input.pitcher_id())
|
163 |
+
# print(input)
|
164 |
+
# plt.hist(x=exit_velo_df[exit_velo_df.pitcher_id==input.x()]['pitch_velocity'])
|
165 |
+
# plt.show()
|
166 |
+
|
167 |
+
|
168 |
+
ev_angle = App(ui.page_fluid(
|
169 |
+
ui.tags.base(href=base_url),
|
170 |
+
ui.tags.div(
|
171 |
+
{"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
|
172 |
+
ui.tags.style(
|
173 |
+
"""
|
174 |
+
h4 {
|
175 |
+
margin-top: 1em;font-size:35px;
|
176 |
+
}
|
177 |
+
h2{
|
178 |
+
font-size:25px;
|
179 |
+
}
|
180 |
+
"""
|
181 |
+
),
|
182 |
+
shinyswatch.theme.simplex(),
|
183 |
+
ui.tags.h4("TJStats"),
|
184 |
+
ui.tags.i("Baseball Analytics and Visualizations"),
|
185 |
+
ui.navset_tab(
|
186 |
+
ui.nav_control(
|
187 |
+
ui.a(
|
188 |
+
"Home",
|
189 |
+
href="home/"
|
190 |
+
),
|
191 |
+
),
|
192 |
+
ui.nav_menu(
|
193 |
+
"Batter Charts",
|
194 |
+
ui.nav_control(
|
195 |
+
ui.a(
|
196 |
+
"Spray",
|
197 |
+
href="spray/"
|
198 |
+
),
|
199 |
+
ui.a(
|
200 |
+
"Decision Value",
|
201 |
+
href="decision_value/"
|
202 |
+
),
|
203 |
+
ui.a(
|
204 |
+
"Damage Model",
|
205 |
+
href="damage_model/"
|
206 |
+
),
|
207 |
+
ui.a(
|
208 |
+
"Batter Scatter",
|
209 |
+
href="batter_scatter/"
|
210 |
+
),
|
211 |
+
ui.a(
|
212 |
+
"EV vs LA Plot",
|
213 |
+
href="ev_angle/"
|
214 |
+
)
|
215 |
+
),
|
216 |
+
),
|
217 |
+
ui.nav_menu(
|
218 |
+
"Goalie Charts",
|
219 |
+
ui.nav_control(
|
220 |
+
ui.a(
|
221 |
+
"GSAx Timeline",
|
222 |
+
href="gsax-timeline/"
|
223 |
+
),
|
224 |
+
ui.a(
|
225 |
+
"GSAx Leaderboard",
|
226 |
+
href="gsax-leaderboard/"
|
227 |
+
),
|
228 |
+
ui.a(
|
229 |
+
"GSAx Comparison",
|
230 |
+
href="gsax-comparison/"
|
231 |
+
)
|
232 |
+
),
|
233 |
+
),ui.nav_menu(
|
234 |
+
"Team Charts",
|
235 |
+
ui.nav_control(
|
236 |
+
ui.a(
|
237 |
+
"Team xG Rates",
|
238 |
+
href="team-xg-rates/"
|
239 |
+
),
|
240 |
+
),
|
241 |
+
),ui.nav_control(
|
242 |
+
ui.a(
|
243 |
+
"Games",
|
244 |
+
href="games/"
|
245 |
+
),
|
246 |
+
),ui.nav_control(
|
247 |
+
ui.a(
|
248 |
+
"About",
|
249 |
+
href="about/"
|
250 |
+
),
|
251 |
+
),ui.nav_control(
|
252 |
+
ui.a(
|
253 |
+
"Articles",
|
254 |
+
href="articles/"
|
255 |
+
),
|
256 |
+
)),ui.row(
|
257 |
+
ui.layout_sidebar(
|
258 |
+
|
259 |
+
|
260 |
+
|
261 |
+
|
262 |
+
ui.panel_sidebar(
|
263 |
+
ui.input_select("id", "Select Batter",batter_dict,width=1),
|
264 |
+
ui.input_select("plot_id", "Select Plot",{'scatter':'Scatter Plot','dist':'Distribution Plot'},width=1)
|
265 |
+
),
|
266 |
+
|
267 |
+
ui.panel_main(
|
268 |
+
ui.output_plot("plot",height = "1000px",width="1000px")
|
269 |
+
),
|
270 |
+
)),)),server)
|
home.py
CHANGED
@@ -14,7 +14,7 @@ from configure import base_url
|
|
14 |
home = App(ui.page_fluid(
|
15 |
ui.tags.base(href=base_url),
|
16 |
ui.tags.div(
|
17 |
-
{"style": "width:
|
18 |
ui.tags.style(
|
19 |
"""
|
20 |
h4 {
|
@@ -25,8 +25,9 @@ home = App(ui.page_fluid(
|
|
25 |
}
|
26 |
"""
|
27 |
),
|
28 |
-
shinyswatch.theme.
|
29 |
-
ui.tags.
|
|
|
30 |
ui.navset_tab(
|
31 |
ui.nav_control(
|
32 |
ui.a(
|
@@ -35,16 +36,28 @@ home = App(ui.page_fluid(
|
|
35 |
),
|
36 |
),
|
37 |
ui.nav_menu(
|
38 |
-
"
|
39 |
ui.nav_control(
|
40 |
ui.a(
|
41 |
-
"
|
42 |
-
href="
|
43 |
),
|
44 |
ui.a(
|
45 |
-
"
|
46 |
-
href="
|
47 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
),
|
49 |
),
|
50 |
ui.nav_menu(
|
@@ -86,4 +99,4 @@ home = App(ui.page_fluid(
|
|
86 |
"Articles",
|
87 |
href="articles/"
|
88 |
),
|
89 |
-
)),ui.tags.br(),ui.tags.h5("Welcome to
|
|
|
14 |
home = App(ui.page_fluid(
|
15 |
ui.tags.base(href=base_url),
|
16 |
ui.tags.div(
|
17 |
+
{"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
|
18 |
ui.tags.style(
|
19 |
"""
|
20 |
h4 {
|
|
|
25 |
}
|
26 |
"""
|
27 |
),
|
28 |
+
shinyswatch.theme.simplex(),
|
29 |
+
ui.tags.h4("TJStats"),
|
30 |
+
ui.tags.i("Baseball Analytics and Visualizations"),
|
31 |
ui.navset_tab(
|
32 |
ui.nav_control(
|
33 |
ui.a(
|
|
|
36 |
),
|
37 |
),
|
38 |
ui.nav_menu(
|
39 |
+
"Batter Charts",
|
40 |
ui.nav_control(
|
41 |
ui.a(
|
42 |
+
"Spray",
|
43 |
+
href="spray/"
|
44 |
),
|
45 |
ui.a(
|
46 |
+
"Decision Value",
|
47 |
+
href="decision_value/"
|
48 |
),
|
49 |
+
ui.a(
|
50 |
+
"Damage Model",
|
51 |
+
href="damage_model/"
|
52 |
+
),
|
53 |
+
ui.a(
|
54 |
+
"Batter Scatter",
|
55 |
+
href="batter_scatter/"
|
56 |
+
),
|
57 |
+
ui.a(
|
58 |
+
"EV vs LA Plot",
|
59 |
+
href="ev_angle/"
|
60 |
+
)
|
61 |
),
|
62 |
),
|
63 |
ui.nav_menu(
|
|
|
99 |
"Articles",
|
100 |
href="articles/"
|
101 |
),
|
102 |
+
)),ui.tags.br(),ui.tags.h5("Welcome to TJStats!"),ui.tags.h6(""))), None)
|
manifest.json
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"version": 1,
|
3 |
+
"locale": "en_CA.cp1252",
|
4 |
+
"metadata": {
|
5 |
+
"appmode": "python-shiny",
|
6 |
+
"entrypoint": "app"
|
7 |
+
},
|
8 |
+
"python": {
|
9 |
+
"version": "3.9.13",
|
10 |
+
"package_manager": {
|
11 |
+
"name": "pip",
|
12 |
+
"version": "23.1",
|
13 |
+
"package_file": "requirements.txt"
|
14 |
+
}
|
15 |
+
},
|
16 |
+
"files": {
|
17 |
+
"requirements.txt": {
|
18 |
+
"checksum": "d9ed48c97a63d10c292a5a380e83b577"
|
19 |
+
},
|
20 |
+
".dockerignore": {
|
21 |
+
"checksum": "abd9778fec883c042a294e6f8ec4d95f"
|
22 |
+
},
|
23 |
+
"Dockerfile": {
|
24 |
+
"checksum": "ad90b7a7fd00daa424a233ac74027c3f"
|
25 |
+
},
|
26 |
+
"app.py": {
|
27 |
+
"checksum": "9ede9917d07c102f70acc73a105debdf"
|
28 |
+
},
|
29 |
+
"runtime.txt": {
|
30 |
+
"checksum": "d500af0d9f1bed234ef59fa020da5bf1"
|
31 |
+
}
|
32 |
+
}
|
33 |
+
}
|
no_swing.joblib
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:c3da3e7ab2b513b87d05e90ae30c788ac819dfcaa7cc1cd9943fc13d2958a00f
|
3 |
+
size 279409
|
requirements.txt
CHANGED
@@ -1,18 +1,310 @@
|
|
1 |
# requirements.txt generated by rsconnect-python on 2023-04-20 21:38:50.254957
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
matplotlib==3.5.1
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
Pillow==9.0.1
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
seaborn==0.11.1
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# requirements.txt generated by rsconnect-python on 2023-04-20 21:38:50.254957
|
2 |
+
adjustText==0.7.3
|
3 |
+
aiohttp==3.8.1
|
4 |
+
aiosignal==1.2.0
|
5 |
+
altair==4.2.0
|
6 |
+
ansicolors==1.1.8
|
7 |
+
anyio==3.6.2
|
8 |
+
appdirs==1.4.4
|
9 |
+
argon2-cffi==21.1.0
|
10 |
+
asgiref==3.5.2
|
11 |
+
aspose-words==22.10.0
|
12 |
+
asttokens==2.2.1
|
13 |
+
async-timeout==4.0.2
|
14 |
+
atomicwrites==1.4.0
|
15 |
+
attrs==20.3.0
|
16 |
+
backcall==0.2.0
|
17 |
+
bandit==1.7.4
|
18 |
+
baseball-scraper==0.4.10
|
19 |
+
beautifulsoup4==4.9.3
|
20 |
+
bleach==4.1.0
|
21 |
+
branca==0.4.2
|
22 |
+
bs4==0.0.1
|
23 |
+
cachetools==5.2.0
|
24 |
+
cairocffi==1.2.0
|
25 |
+
CairoSVG==2.6.0
|
26 |
+
calfem-python==3.5.9
|
27 |
+
certifi==2020.12.5
|
28 |
+
cffi==1.14.5
|
29 |
+
chardet==4.0.0
|
30 |
+
charset-normalizer==2.0.11
|
31 |
+
chart-studio==1.1.0
|
32 |
+
click==8.1.3
|
33 |
+
click-plugins==1.1.1
|
34 |
+
cligj==0.7.2
|
35 |
+
colorama==0.4.4
|
36 |
+
colour==0.1.5
|
37 |
+
commonmark==0.9.1
|
38 |
+
contextvars==2.4
|
39 |
+
coverage==6.5.0
|
40 |
+
cryptography==38.0.1
|
41 |
+
cssselect==1.1.0
|
42 |
+
cssselect2==0.4.1
|
43 |
+
cssutils==2.2.0
|
44 |
+
cycler==0.10.0
|
45 |
+
dash==2.9.3
|
46 |
+
dash-core-components==2.0.0
|
47 |
+
dash-html-components==2.0.0
|
48 |
+
dash-table==5.0.0
|
49 |
+
dataframe-image==0.1.7
|
50 |
+
DateTime==4.3
|
51 |
+
decorator==4.4.2
|
52 |
+
decorest==0.0.6
|
53 |
+
defusedxml==0.7.1
|
54 |
+
Deprecated==1.2.13
|
55 |
+
deserialize==1.8.1
|
56 |
+
df2img==0.2.9
|
57 |
+
Django==4.1.1
|
58 |
+
dmsh==0.2.19
|
59 |
+
docopt==0.6.2
|
60 |
+
entrypoints==0.3
|
61 |
+
et-xmlfile==1.0.1
|
62 |
+
executing==1.2.0
|
63 |
+
ezdxf==0.17.2
|
64 |
+
fastapi==0.95.0
|
65 |
+
fastjsonschema==2.16.3
|
66 |
+
flake8==5.0.4
|
67 |
+
Flask==2.2.3
|
68 |
+
folium==0.12.1.post1
|
69 |
+
fonttools==4.31.2
|
70 |
+
frozenlist==1.3.0
|
71 |
+
fsspec==2023.4.0
|
72 |
+
fuzzywuzzy==0.18.0
|
73 |
+
geomdl==5.3.1
|
74 |
+
gitdb==4.0.9
|
75 |
+
GitPython==3.1.29
|
76 |
+
gmsh==4.9.5
|
77 |
+
google-api-core==2.10.1
|
78 |
+
google-api-python-client==2.63.0
|
79 |
+
google-auth==2.12.0
|
80 |
+
google-auth-httplib2==0.1.0
|
81 |
+
google-auth-oauthlib==0.5.3
|
82 |
+
google-spreadsheet==0.0.6
|
83 |
+
googleapis-common-protos==1.56.4
|
84 |
+
graphviz==0.19.1
|
85 |
+
greenlet==1.1.3.post0
|
86 |
+
gspread==5.5.0
|
87 |
+
h11==0.12.0
|
88 |
+
h5py==3.6.0
|
89 |
+
hockey-rink==0.1.1
|
90 |
+
hockey-scraper==1.37.1
|
91 |
+
hockeyjockey==1.2
|
92 |
+
html2image==2.0.1
|
93 |
+
html5lib==1.1
|
94 |
+
htmltools==0.2.1
|
95 |
+
httpcore==0.15.0
|
96 |
+
httplib2==0.20.4
|
97 |
+
httpx==0.23.0
|
98 |
+
humanize==4.6.0
|
99 |
+
idna==2.10
|
100 |
+
image==1.5.33
|
101 |
+
imageio==2.15.0
|
102 |
+
imageio-ffmpeg==0.4.8
|
103 |
+
imgkit==1.2.2
|
104 |
+
immutables==0.19
|
105 |
+
importlib-metadata==6.0.0
|
106 |
+
importlib-resources==5.12.0
|
107 |
+
iniconfig==1.1.1
|
108 |
+
ipykernel==5.5.0
|
109 |
+
ipython==8.11.0
|
110 |
+
ipython-genutils==0.2.0
|
111 |
+
itsdangerous==2.1.2
|
112 |
+
jdcal==1.4.1
|
113 |
+
jedi==0.18.0
|
114 |
+
Jinja2==3.0.1
|
115 |
+
joblib==1.1.0
|
116 |
+
jsonschema==3.2.0
|
117 |
+
jupyter-client==6.1.11
|
118 |
+
jupyter-core==4.7.1
|
119 |
+
jupyterlab-pygments==0.1.2
|
120 |
+
kaleido==0.2.1
|
121 |
+
kiwisolver==1.3.1
|
122 |
+
libhockey==0.23.0
|
123 |
+
linkify-it-py==2.0.0
|
124 |
+
lxml==4.6.2
|
125 |
+
markdown-it-py==2.2.0
|
126 |
+
MarkupSafe==2.1.2
|
127 |
matplotlib==3.5.1
|
128 |
+
matplotlib-inline==0.1.6
|
129 |
+
mccabe==0.7.0
|
130 |
+
mdit-py-plugins==0.3.5
|
131 |
+
mdurl==0.1.2
|
132 |
+
menyou==1.0
|
133 |
+
meshio==5.3.4
|
134 |
+
meshplex==0.17.2
|
135 |
+
meshzoo==0.9.4
|
136 |
+
mistune==0.8.4
|
137 |
+
MLB-StatsAPI==1.4.2
|
138 |
+
more-itertools==8.12.0
|
139 |
+
moviepy==1.0.3
|
140 |
+
mpmath==1.2.1
|
141 |
+
multidict==6.0.2
|
142 |
+
munch==2.5.0
|
143 |
+
mysql-connector-python==8.0.24
|
144 |
+
natsort==7.1.1
|
145 |
+
nbclient==0.5.4
|
146 |
+
nbconvert==6.4.0
|
147 |
+
nbformat==5.1.3
|
148 |
+
ndim==0.1.6
|
149 |
+
nest-asyncio==1.5.1
|
150 |
+
networkx==2.6.3
|
151 |
+
nhl-logo-scraper==1.1.0
|
152 |
+
nhlpy==0.3.0
|
153 |
+
nibabel==3.2.2
|
154 |
+
notebook==6.4.4
|
155 |
+
npx==0.1.1
|
156 |
+
numpy==1.22.3
|
157 |
+
oauth2client==4.1.3
|
158 |
+
oauthlib==3.2.1
|
159 |
+
openpyxl==3.0.5
|
160 |
+
optimesh==0.8.7
|
161 |
+
orthopy==0.9.5
|
162 |
+
packaging==21.3
|
163 |
+
pandas==1.2.0
|
164 |
+
pandocfilters==1.5.0
|
165 |
+
parso==0.8.1
|
166 |
+
patsy==0.5.1
|
167 |
+
pbr==5.11.0
|
168 |
+
pdfkit==1.0.0
|
169 |
+
pickleshare==0.7.5
|
170 |
Pillow==9.0.1
|
171 |
+
pins==0.8.0
|
172 |
+
pip==23.1
|
173 |
+
platformdirs==2.5.1
|
174 |
+
plotly==4.1.1
|
175 |
+
pluggy==0.13.1
|
176 |
+
praw==7.7.0
|
177 |
+
prawcore==2.3.0
|
178 |
+
proglog==0.1.10
|
179 |
+
prometheus-client==0.11.0
|
180 |
+
prompt-toolkit==3.0.38
|
181 |
+
protobuf==4.21.6
|
182 |
+
psutil==5.9.3
|
183 |
+
pure-eval==0.2.2
|
184 |
+
py==1.10.0
|
185 |
+
pyaml==20.4.0
|
186 |
+
pyarrow==5.0.0
|
187 |
+
pyasn1==0.4.8
|
188 |
+
pyasn1-modules==0.2.8
|
189 |
+
pybaseball==2.2.1
|
190 |
+
pybind11==2.9.1
|
191 |
+
pycairo==1.23.0
|
192 |
+
pycodestyle==2.9.1
|
193 |
+
pycparser==2.20
|
194 |
+
pydantic==1.10.7
|
195 |
+
pyfiglet==0.8.post1
|
196 |
+
pyflakes==2.5.0
|
197 |
+
PyGithub==1.55
|
198 |
+
Pygments==2.8.0
|
199 |
+
pygsheets==2.0.5
|
200 |
+
PyHockeyStats==0.5.2
|
201 |
+
PyJWT==2.6.0
|
202 |
+
pymesh==1.0.2
|
203 |
+
PyNaCl==1.4.0
|
204 |
+
PyOpenGL==3.1.6
|
205 |
+
pyOpenSSL==22.1.0
|
206 |
+
pyparsing==3.0.7
|
207 |
+
pyperclip==1.8.2
|
208 |
+
pyproj==3.2.1
|
209 |
+
PyQt5==5.15.6
|
210 |
+
PyQt5-Qt5==5.15.2
|
211 |
+
PyQt5-sip==12.9.1
|
212 |
+
PyQtWebEngine==5.15.5
|
213 |
+
PyQtWebEngine-Qt5==5.15.2
|
214 |
+
pyrsistent==0.17.3
|
215 |
+
pytest==7.1.3
|
216 |
+
pytest_check==1.0.5
|
217 |
+
python-dateutil==2.8.2
|
218 |
+
python-dotenv==0.21.0
|
219 |
+
python-mlb-statsapi==0.3.9
|
220 |
+
python-multipart==0.0.6
|
221 |
+
pytools==2022.1.1
|
222 |
+
pytz==2022.7.1
|
223 |
+
PyVTK==0.5.18
|
224 |
+
PyWavelets==1.2.0
|
225 |
+
pywin32==305
|
226 |
+
pywinpty==1.1.4
|
227 |
+
PyYAML==5.3.1
|
228 |
+
pyzmq==25.0.1
|
229 |
+
quadpy==0.16.10
|
230 |
+
rauth==0.7.3
|
231 |
+
requests==2.28.1
|
232 |
+
requests-mock==1.10.0
|
233 |
+
requests-oauthlib==1.3.1
|
234 |
+
retrying==1.3.3
|
235 |
+
rfc3986==1.5.0
|
236 |
+
rhino-shapley-interop==0.0.4
|
237 |
+
rhino3dm==7.15.0
|
238 |
+
rich==12.0.0
|
239 |
+
riotwatcher==3.2.4
|
240 |
+
rsa==4.9
|
241 |
+
scikit-image==0.19.1
|
242 |
+
scikit-learn==1.0.1
|
243 |
+
scipy==1.6.0
|
244 |
seaborn==0.11.1
|
245 |
+
selenium==3.141.0
|
246 |
+
semver==2.13.0
|
247 |
+
Send2Trash==1.8.0
|
248 |
+
Shapely==1.7.1
|
249 |
+
shiny==0.3.0
|
250 |
+
six==1.16.0
|
251 |
+
sklearn==0.0
|
252 |
+
smmap==5.0.0
|
253 |
+
sniffio==1.3.0
|
254 |
+
soupsieve==2.4
|
255 |
+
spotipy==2.18.0
|
256 |
+
SQLAlchemy==1.4.42
|
257 |
+
sqlparse==0.4.2
|
258 |
+
stack-data==0.6.2
|
259 |
+
starlette==0.26.1
|
260 |
+
statsmodels==0.12.2
|
261 |
+
stevedore==4.1.1
|
262 |
+
stringcase==1.2.0
|
263 |
+
sympy==1.10
|
264 |
+
tabulate==0.9.0
|
265 |
+
tekore==4.5.0
|
266 |
+
tenacity==8.2.2
|
267 |
+
terminado==0.12.1
|
268 |
+
termplotlib==0.3.9
|
269 |
+
testpath==0.5.0
|
270 |
+
threadpoolctl==3.0.0
|
271 |
+
tifffile==2022.2.2
|
272 |
+
tinycss2==1.2.1
|
273 |
+
toml==0.10.2
|
274 |
+
tomli==2.0.1
|
275 |
+
toolz==0.11.2
|
276 |
+
TopDownHockey-Scraper==2.0.1
|
277 |
+
tornado==6.2
|
278 |
+
tqdm==4.62.3
|
279 |
+
traitlets==5.9.0
|
280 |
+
triangle==20220202
|
281 |
+
trimeshpy==0.0.2
|
282 |
+
tweepy==4.13.0
|
283 |
+
typing_extensions==4.5.0
|
284 |
+
tzdata==2022.2
|
285 |
+
uc-micro-py==1.0.1
|
286 |
+
Unidecode==1.3.4
|
287 |
+
update-checker==0.18.0
|
288 |
+
uritemplate==4.1.1
|
289 |
+
urllib3==1.26.15
|
290 |
+
uvicorn==0.21.1
|
291 |
+
vega-datasets==0.9.0
|
292 |
+
visvis==1.13.0
|
293 |
+
vtk==9.1.0
|
294 |
+
wcwidth==0.2.6
|
295 |
+
webdriver-manager==3.8.5
|
296 |
+
webencodings==0.5.1
|
297 |
+
websocket-client==1.5.1
|
298 |
+
websockets==11.0.1
|
299 |
+
Werkzeug==2.2.3
|
300 |
+
wget==3.2
|
301 |
+
wrapt==1.12.1
|
302 |
+
wslink==1.4.3
|
303 |
+
xmltodict==0.12.0
|
304 |
+
xxhash==3.2.0
|
305 |
+
yahoo-oauth==2.0
|
306 |
+
yarl==1.7.2
|
307 |
+
yffpy==2.11.0
|
308 |
+
yfpy==9.1.0
|
309 |
+
zipp==3.15.0
|
310 |
+
zope.interface==5.4.0
|
runtime.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
python-3.9.13
|
spray.py
ADDED
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
##### games.,py #####
|
2 |
+
|
3 |
+
# Import modules
|
4 |
+
from shiny import *
|
5 |
+
import shinyswatch
|
6 |
+
import plotly.express as px
|
7 |
+
from shinywidgets import output_widget, render_widget
|
8 |
+
import pandas as pd
|
9 |
+
from configure import base_url
|
10 |
+
import math
|
11 |
+
import datetime
|
12 |
+
import datasets
|
13 |
+
from datasets import load_dataset
|
14 |
+
import numpy as np
|
15 |
+
import matplotlib
|
16 |
+
from matplotlib.ticker import MaxNLocator
|
17 |
+
from matplotlib.gridspec import GridSpec
|
18 |
+
import matplotlib.pyplot as plt
|
19 |
+
from scipy.stats import gaussian_kde
|
20 |
+
|
21 |
+
### Import Datasets
|
22 |
+
dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv',
|
23 |
+
'mlb_pitch_data_2022.csv'])
|
24 |
+
dataset_train = dataset['train']
|
25 |
+
df_2023 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
|
26 |
+
# Paths to data
|
27 |
+
### Normalize Hit Locations
|
28 |
+
df_2023['hit_x'] = df_2023['hit_x'] - 126#df_2023['hit_x'].median()
|
29 |
+
df_2023['hit_y'] = -df_2023['hit_y']+204.5#df_2023['hit_y'].quantile(0.9999)
|
30 |
+
|
31 |
+
df_2023['hit_x_og'] = df_2023['hit_x']
|
32 |
+
df_2023.loc[df_2023['batter_hand'] == 'R','hit_x'] = -1*df_2023.loc[df_2023['batter_hand'] == 'R','hit_x']
|
33 |
+
|
34 |
+
### Calculate Horizontal Launch Angles
|
35 |
+
df_2023['h_la'] = np.arctan(df_2023['hit_x'] / df_2023['hit_y'])*180/np.pi
|
36 |
+
conditions_ss = [
|
37 |
+
(df_2023['h_la']<-16+5/6),
|
38 |
+
(df_2023['h_la']<16+5/6)&(df_2023['h_la']>=-16+5/6),
|
39 |
+
(df_2023['h_la']>=16+5/6)
|
40 |
+
]
|
41 |
+
|
42 |
+
choices_ss = ['Oppo','Straight','Pull']
|
43 |
+
df_2023['traj'] = np.select(conditions_ss, choices_ss, default=np.nan)
|
44 |
+
df_2023['bip'] = [1 if x > 0 else np.nan for x in df_2023['launch_speed']]
|
45 |
+
|
46 |
+
conditions_woba = [
|
47 |
+
(df_2023['event_type']=='walk'),
|
48 |
+
(df_2023['event_type']=='hit_by_pitch'),
|
49 |
+
(df_2023['event_type']=='single'),
|
50 |
+
(df_2023['event_type']=='double'),
|
51 |
+
(df_2023['event_type']=='triple'),
|
52 |
+
(df_2023['event_type']=='home_run'),
|
53 |
+
]
|
54 |
+
|
55 |
+
choices_woba = [0.698,
|
56 |
+
0.728,
|
57 |
+
0.887,
|
58 |
+
1.253,
|
59 |
+
1.583,
|
60 |
+
2.027]
|
61 |
+
|
62 |
+
df_2023['woba'] = np.select(conditions_woba, choices_woba, default=0)
|
63 |
+
|
64 |
+
|
65 |
+
|
66 |
+
df_2023_bip = df_2023[~df_2023['bip'].isnull()].dropna(subset=['h_la','launch_angle'])
|
67 |
+
df_2023_bip['h_la'] = df_2023_bip['h_la'].round(0)
|
68 |
+
|
69 |
+
|
70 |
+
df_2023_bip['season'] = df_2023_bip['game_date'].str[0:4].astype(int)
|
71 |
+
|
72 |
+
df_2023_bip = df_2023_bip[df_2023_bip['season'] == 2023]
|
73 |
+
df_2022_bip = df_2023_bip[df_2023_bip['season'] == 2022]
|
74 |
+
|
75 |
+
batter_dict = df_2023_bip.sort_values('batter_name').set_index('batter_id')['batter_name'].to_dict()
|
76 |
+
|
77 |
+
|
78 |
+
|
79 |
+
|
80 |
+
|
81 |
+
def server(input,output,session):
|
82 |
+
@output
|
83 |
+
@render.plot(alt="plot")
|
84 |
+
def plot():
|
85 |
+
|
86 |
+
batter_id_select = int(input.batter_id())
|
87 |
+
df_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2023)]
|
88 |
+
df_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2022)]
|
89 |
+
|
90 |
+
df_non_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2023)]
|
91 |
+
df_non_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2022)]
|
92 |
+
|
93 |
+
traj_df = df_batter_2023.groupby(['traj'])['launch_speed'].count() / len(df_batter_2023)
|
94 |
+
trajectory_df = df_batter_2023.groupby(['trajectory'])['launch_speed'].count() / len(df_batter_2023)#.loc['Oppo']
|
95 |
+
|
96 |
+
|
97 |
+
|
98 |
+
|
99 |
+
colour_palette = ['#FFB000','#648FFF','#785EF0',
|
100 |
+
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
|
101 |
+
|
102 |
+
fig = plt.figure(figsize=(10, 10))
|
103 |
+
|
104 |
+
|
105 |
+
|
106 |
+
# Create a 2x2 grid of subplots using GridSpec
|
107 |
+
gs = GridSpec(3, 3, width_ratios=[0.1,0.8,0.1], height_ratios=[0.1,0.8,0.1])
|
108 |
+
|
109 |
+
# ax00 = fig.add_subplot(gs[0, 0])
|
110 |
+
ax01 = fig.add_subplot(gs[0, :]) # Subplot at the top-right position
|
111 |
+
# ax02 = fig.add_subplot(gs[0, 2])
|
112 |
+
# Subplot spanning the entire bottom row
|
113 |
+
ax10 = fig.add_subplot(gs[1, 0])
|
114 |
+
ax11 = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
|
115 |
+
ax12 = fig.add_subplot(gs[1, 2])
|
116 |
+
# ax20 = fig.add_subplot(gs[2, 0])
|
117 |
+
ax21 = fig.add_subplot(gs[2, :]) # Subplot at the top-right position
|
118 |
+
# ax22 = fig.add_subplot(gs[2, 2])
|
119 |
+
|
120 |
+
initial_position = ax12.get_position()
|
121 |
+
|
122 |
+
# Change the size of the axis
|
123 |
+
# new_width = 0.06 # Set your desired width
|
124 |
+
# new_height = 0.4 # Set your desired height
|
125 |
+
# new_position = [initial_position.x0-0.01, initial_position.y0+0.065, new_width, new_height]
|
126 |
+
# ax12.set_position(new_position)
|
127 |
+
|
128 |
+
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]])
|
129 |
+
# Generate two sets of two-dimensional data
|
130 |
+
# data1 = np.random.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]], 1000)
|
131 |
+
# data2 = np.random.multivariate_normal([3, 3], [[1, -0.5], [-0.5, 1]], 1000)
|
132 |
+
bat_hand = df_batter_2023.groupby('batter_hand')['launch_speed'].count().sort_values(ascending=False).index[0]
|
133 |
+
|
134 |
+
bat_hand_value = 1
|
135 |
+
|
136 |
+
if bat_hand == 'R':
|
137 |
+
bat_hand_value = -1
|
138 |
+
|
139 |
+
kde1_df = df_batter_2023[['h_la','launch_angle']]
|
140 |
+
kde1_df['h_la'] = kde1_df['h_la'] * bat_hand_value
|
141 |
+
kde2_df = df_non_batter_2023[['h_la','launch_angle']].sample(n=50000, random_state=42)
|
142 |
+
kde2_df['h_la'] = kde2_df['h_la'] * bat_hand_value
|
143 |
+
|
144 |
+
|
145 |
+
# Calculate 2D KDE for each dataset
|
146 |
+
kde1 = gaussian_kde(kde1_df.values.T)
|
147 |
+
kde2 = gaussian_kde(kde2_df.values.T)
|
148 |
+
|
149 |
+
# Generate a grid of points for evaluation
|
150 |
+
x, y = np.meshgrid(np.arange(-45, 46,1 ), np.arange(-30, 61,1 ))
|
151 |
+
positions = np.vstack([x.ravel(), y.ravel()])
|
152 |
+
|
153 |
+
# Evaluate the KDEs on the grid
|
154 |
+
kde1_values = np.reshape(kde1(positions).T, x.shape)
|
155 |
+
kde2_values = np.reshape(kde2(positions).T, x.shape)
|
156 |
+
|
157 |
+
# Subtract one KDE from the other
|
158 |
+
result_kde_values = kde1_values - kde2_values
|
159 |
+
|
160 |
+
# Normalize the array to the range [0, 1]
|
161 |
+
# result_kde_values = (result_kde_values - np.min(result_kde_values)) / (np.max(result_kde_values) - np.min(result_kde_values))
|
162 |
+
result_kde_values = (result_kde_values - np.mean(result_kde_values)) / (np.std(result_kde_values))
|
163 |
+
|
164 |
+
result_kde_values = np.clip(result_kde_values, -3, 3)
|
165 |
+
# # Plot the original KDEs
|
166 |
+
# plt.contourf(x, y, kde1_values, cmap='Blues', alpha=0.5, levels=20)
|
167 |
+
# plt.contourf(x, y, kde2_values, cmap='Reds', alpha=0.5, levels=20)
|
168 |
+
|
169 |
+
# Plot the subtracted KDE
|
170 |
+
# Set the number of levels and midrange value
|
171 |
+
# Set the number of levels and midrange value
|
172 |
+
num_levels = 14
|
173 |
+
midrange_value = 0
|
174 |
+
|
175 |
+
# Create a filled contour plot with specified levels
|
176 |
+
levels = np.linspace(-3, 3, num_levels)
|
177 |
+
|
178 |
+
batter_plot = ax11.contourf(x, y, result_kde_values, cmap=cmap_hue, levels=levels, vmin=-3, vmax=3)
|
179 |
+
|
180 |
+
|
181 |
+
ax11.hlines(y=10,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
|
182 |
+
ax11.hlines(y=25,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
|
183 |
+
ax11.hlines(y=50,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
|
184 |
+
|
185 |
+
ax11.vlines(x=-15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1)
|
186 |
+
ax11.vlines(x=15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1)
|
187 |
+
#ax11.axis('square')
|
188 |
+
#ax11.axis('off')
|
189 |
+
#ax.hlines(y=10,xmin=-45,xmax=-45)
|
190 |
+
# Add labels and legend
|
191 |
+
#plt.xlabel('X-axis')
|
192 |
+
#plt.ylabel('Y-axis')
|
193 |
+
#ax.plot('equal')
|
194 |
+
#plt.gca().set_aspect('equal')
|
195 |
+
|
196 |
+
#Choose a mappable (can be any plot or image)
|
197 |
+
ax12.set_ylim(0,1)
|
198 |
+
cbar = plt.colorbar(batter_plot, cax=ax12, orientation='vertical',shrink=1)
|
199 |
+
cbar.set_ticks([])
|
200 |
+
# Set the colorbar to have 13 levels
|
201 |
+
cbar_locator = MaxNLocator(nbins=13)
|
202 |
+
cbar.locator = cbar_locator
|
203 |
+
cbar.update_ticks()
|
204 |
+
#cbar.set_clim(vmin=-3, vmax=)
|
205 |
+
# Set ticks and tick labels
|
206 |
+
# cbar.set_ticks(np.linspace(-3, 3, 13))
|
207 |
+
# cbar.set_ticklabels(np.linspace(0, 3, 13))
|
208 |
+
cbar.set_ticks([])
|
209 |
+
|
210 |
+
|
211 |
+
|
212 |
+
|
213 |
+
ax10.text(s=f"Pop Up\n({trajectory_df.loc['popup']:.1%})",
|
214 |
+
x=1,
|
215 |
+
y=0.95,va='center',ha='right',fontsize=16)
|
216 |
+
# Choose a mappable (can be any plot or image)
|
217 |
+
ax10.text(s=f"Fly Ball\n({trajectory_df.loc['fly_ball']:.1%})",
|
218 |
+
x=1,
|
219 |
+
y=0.75,va='center',ha='right',fontsize=16)
|
220 |
+
|
221 |
+
ax10.text(s=f"Line\nDrive\n({trajectory_df.loc['line_drive']:.1%})",
|
222 |
+
x=1,
|
223 |
+
y=0.53,va='center',ha='right',fontsize=16)
|
224 |
+
|
225 |
+
|
226 |
+
ax10.text(s=f"Ground\nBall\n({trajectory_df.loc['ground_ball']:.1%})",
|
227 |
+
x=1,
|
228 |
+
y=0.23,va='center',ha='right',fontsize=16)
|
229 |
+
#ax12.axis(True)
|
230 |
+
# Set equal aspect ratio for the contour plot
|
231 |
+
|
232 |
+
if bat_hand == 'R':
|
233 |
+
|
234 |
+
|
235 |
+
ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})",
|
236 |
+
x=0.2+1/16*0.8,
|
237 |
+
y=1,va='top',ha='center',fontsize=16)
|
238 |
+
|
239 |
+
ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})",
|
240 |
+
x=0.5,
|
241 |
+
y=1,va='top',ha='center',fontsize=16)
|
242 |
+
|
243 |
+
ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})",
|
244 |
+
x=0.8-1/16*0.8,
|
245 |
+
y=1,va='top',ha='center',fontsize=16)
|
246 |
+
|
247 |
+
else:
|
248 |
+
|
249 |
+
ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})",
|
250 |
+
x=0.8-1/16*0.8,
|
251 |
+
y=1,va='top',ha='center',fontsize=16)
|
252 |
+
|
253 |
+
ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})",
|
254 |
+
x=0.5,
|
255 |
+
y=1,va='top',ha='center',fontsize=16)
|
256 |
+
|
257 |
+
ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})",
|
258 |
+
x=0.2+1/16*0.8,
|
259 |
+
y=1,va='top',ha='center',fontsize=16)
|
260 |
+
|
261 |
+
# Define the initial position of the axis
|
262 |
+
|
263 |
+
# Customize colorbar properties
|
264 |
+
# cbar = fig.colorbar(orientation='vertical', pad=0.1,ax=ax12)
|
265 |
+
#cbar.set_label('Difference', rotation=270, labelpad=15)
|
266 |
+
# Show the plot
|
267 |
+
# ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
|
268 |
+
# ax21.text(1, 0., "Data: MLB",ha='right', va='bottom',fontsize=12)
|
269 |
+
# ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12)
|
270 |
+
|
271 |
+
# ax00.axis('off')
|
272 |
+
ax01.axis('off')
|
273 |
+
# ax02.axis('off')
|
274 |
+
ax10.axis('off')
|
275 |
+
#ax11.axis('off')
|
276 |
+
#ax12.axis('off')
|
277 |
+
# ax20.axis('off')
|
278 |
+
ax21.axis('off')
|
279 |
+
# ax22.axis('off')
|
280 |
+
|
281 |
+
ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
|
282 |
+
ax21.text(0.98, 0., "Data: MLB",ha='right', va='bottom',fontsize=12)
|
283 |
+
ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12)
|
284 |
+
|
285 |
+
|
286 |
+
ax11.set_xticks([])
|
287 |
+
ax11.set_yticks([])
|
288 |
+
|
289 |
+
# ax12.text(s='Same',x=np.mean([x for x in ax12.get_xlim()]),y=np.median([x for x in ax12.get_ylim()]),
|
290 |
+
# va='center',ha='center',fontsize=12)
|
291 |
+
|
292 |
+
# ax12.text(s='More\nOften',x=0.5,y=0.74,
|
293 |
+
# va='top',ha='center',fontsize=12)
|
294 |
+
|
295 |
+
ax12.text(s='+3σ',x=0.5,y=3-1/14*3,
|
296 |
+
va='center',ha='center',fontsize=12)
|
297 |
+
|
298 |
+
ax12.text(s='+2σ',x=0.5,y=2-1/14*2,
|
299 |
+
va='center',ha='center',fontsize=12)
|
300 |
+
|
301 |
+
ax12.text(s='+1σ',x=0.5,y=1-1/14*1,
|
302 |
+
va='center',ha='center',fontsize=12)
|
303 |
+
|
304 |
+
|
305 |
+
ax12.text(s='±0σ',x=0.5,y=0,
|
306 |
+
va='center',ha='center',fontsize=12)
|
307 |
+
|
308 |
+
ax12.text(s='-1σ',x=0.5,y=-1-1/14*-1,
|
309 |
+
va='center',ha='center',fontsize=12)
|
310 |
+
|
311 |
+
ax12.text(s='-2σ',x=0.5,y=-2-1/14*-2,
|
312 |
+
va='center',ha='center',fontsize=12)
|
313 |
+
|
314 |
+
ax12.text(s='-3σ',x=0.5,y=-3-1/14*-3,
|
315 |
+
va='center',ha='center',fontsize=12)
|
316 |
+
|
317 |
+
# # ax12.text(s='Less\nOften',x=0.5,y=0.26,
|
318 |
+
# # va='bottom',ha='center',fontsize=12)
|
319 |
+
|
320 |
+
ax01.text(s=f"{df_batter_2023['batter_name'].values[0]}'s 2023 Batted Ball Tendencies",
|
321 |
+
x=0.5,
|
322 |
+
y=0.8,va='top',ha='center',fontsize=20)
|
323 |
+
|
324 |
+
ax01.text(s=f"(Compared to rest of MLB)",
|
325 |
+
x=0.5,
|
326 |
+
y=0.3,va='top',ha='center',fontsize=16)
|
327 |
+
|
328 |
+
#plt.show()
|
329 |
+
|
330 |
+
spray = App(ui.page_fluid(
|
331 |
+
ui.tags.base(href=base_url),
|
332 |
+
ui.tags.div(
|
333 |
+
{"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
|
334 |
+
ui.tags.style(
|
335 |
+
"""
|
336 |
+
h4 {
|
337 |
+
margin-top: 1em;font-size:35px;
|
338 |
+
}
|
339 |
+
h2{
|
340 |
+
font-size:25px;
|
341 |
+
}
|
342 |
+
"""
|
343 |
+
),
|
344 |
+
shinyswatch.theme.simplex(),
|
345 |
+
ui.tags.h4("Stats By Zach"),
|
346 |
+
ui.tags.i("A website for hockey analytics"),
|
347 |
+
ui.navset_tab(
|
348 |
+
ui.nav_control(
|
349 |
+
ui.a(
|
350 |
+
"Home",
|
351 |
+
href="home/"
|
352 |
+
),
|
353 |
+
),
|
354 |
+
ui.nav_menu(
|
355 |
+
"Batter Charts",
|
356 |
+
ui.nav_control(
|
357 |
+
ui.a(
|
358 |
+
"Spray",
|
359 |
+
href="spray/"
|
360 |
+
),
|
361 |
+
ui.a(
|
362 |
+
"Decision Value",
|
363 |
+
href="decision_value/"
|
364 |
+
),
|
365 |
+
ui.a(
|
366 |
+
"Damage Model",
|
367 |
+
href="damage_model/"
|
368 |
+
),
|
369 |
+
ui.a(
|
370 |
+
"Batter Scatter",
|
371 |
+
href="batter_scatter/"
|
372 |
+
),
|
373 |
+
ui.a(
|
374 |
+
"EV vs LA Plot",
|
375 |
+
href="ev_angle/"
|
376 |
+
)
|
377 |
+
),
|
378 |
+
),
|
379 |
+
ui.nav_menu(
|
380 |
+
"Goalie Charts",
|
381 |
+
ui.nav_control(
|
382 |
+
ui.a(
|
383 |
+
"GSAx Timeline",
|
384 |
+
href="gsax-timeline/"
|
385 |
+
),
|
386 |
+
ui.a(
|
387 |
+
"GSAx Leaderboard",
|
388 |
+
href="gsax-leaderboard/"
|
389 |
+
),
|
390 |
+
ui.a(
|
391 |
+
"GSAx Comparison",
|
392 |
+
href="gsax-comparison/"
|
393 |
+
)
|
394 |
+
),
|
395 |
+
),ui.nav_menu(
|
396 |
+
"Team Charts",
|
397 |
+
ui.nav_control(
|
398 |
+
ui.a(
|
399 |
+
"Team xG Rates",
|
400 |
+
href="team-xg-rates/"
|
401 |
+
),
|
402 |
+
),
|
403 |
+
),ui.nav_control(
|
404 |
+
ui.a(
|
405 |
+
"Games",
|
406 |
+
href="games/"
|
407 |
+
),
|
408 |
+
),ui.nav_control(
|
409 |
+
ui.a(
|
410 |
+
"About",
|
411 |
+
href="about/"
|
412 |
+
),
|
413 |
+
),ui.nav_control(
|
414 |
+
ui.a(
|
415 |
+
"Articles",
|
416 |
+
href="articles/"
|
417 |
+
),
|
418 |
+
)),ui.row(
|
419 |
+
ui.layout_sidebar(
|
420 |
+
|
421 |
+
ui.panel_sidebar(
|
422 |
+
ui.input_select("batter_id",
|
423 |
+
"Select Batter",
|
424 |
+
batter_dict,
|
425 |
+
width=1,
|
426 |
+
size=1,
|
427 |
+
selectize=True)),
|
428 |
+
|
429 |
+
ui.panel_main(
|
430 |
+
ui.navset_tab(
|
431 |
+
|
432 |
+
ui.nav("2023 vs MLB",
|
433 |
+
ui.output_plot('plot',
|
434 |
+
width='1000px',
|
435 |
+
height='1000px')),
|
436 |
+
))
|
437 |
+
)),)),server)
|
summary_batter.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
summary_batter_level.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
swing.joblib
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:4fef4a66363e5f3fdc70ae45c5382bd986c800ff8bf9296a1f9b334461e70fd4
|
3 |
+
size 262137
|
xtb_model.joblib
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:a0b08278fdbb8c3bc2af79a65e9563e8b8d2251072ae90fc772ce7b4d2937b7d
|
3 |
+
size 14497085
|