import plotly.express as px
import plotly.graph_objects as go
import plotly.colors as pc
from scipy.stats import gaussian_kde
import numpy as np
import pandas as pd
import gradio as gr
from translate import max_pitch_types
from data import df, pitch_stats
# GRADIO FUNCTIONS
# location maps
def fit_pred_kde(data, X, Y):
kde = gaussian_kde(data)
return kde(np.stack((X, Y)).reshape(2, -1)).reshape(*X.shape)
plot_s = 256
sz_h = 200
sz_w = 160
h_h = 200 - 40*2
h_w = 160 - 32*2
kde_range = np.arange(-plot_s/2, plot_s/2, 1)
X, Y = np.meshgrid(
kde_range,
kde_range
)
def coordinatify(h, w):
return dict(
x0=-w/2,
y0=-h/2,
x1=w/2,
y1=h/2
)
colorscale = pc.sequential.OrRd
colorscale = [
[0, 'rgba(0, 0, 0, 0)'],
] + [
[i / len(colorscale), color] for i, color in enumerate(colorscale, start=1)
]
def plot_pitch_map(player=None, loc=None, pitch_type=None, pitch_name=None):
assert not ((loc is None and player is None) or (loc is not None and player is not None)), 'exactly one of `player` or `loc` must be specified'
if loc is None and player is not None:
assert not ((pitch_type is None and pitch_name is None) or (pitch_type is not None and pitch_name is not None)), 'exactly one of `pitch_type` or `pitch_name` must be specified'
pitch_val = pitch_type or pitch_name
pitch_col = 'pitch_type' if pitch_type else 'pitch_name'
loc = df.set_index(['name', pitch_col]).loc[(player, pitch_val), ['plate_x', 'plate_z']]
Z = fit_pred_kde(loc.to_numpy().T, X, Y)
fig = go.Figure()
fig.add_shape(
type="rect",
**coordinatify(sz_h, sz_w),
line_color='gray',
# fillcolor='rgba(220, 220, 220, 0.75)', #gainsboro
)
fig.add_shape(
type="rect",
**coordinatify(h_h, h_w),
line_color='dimgray',
)
fig.add_trace(go.Contour(
z=Z,
x=kde_range,
y=kde_range,
colorscale=colorscale,
zmin=1e-5,
zmax=Z.max(),
contours={
'start': 1e-5,
'end': Z.max(),
'size': (Z.max() - 1e-5) / 5
},
showscale=False
))
fig.update_layout(
xaxis=dict(range=[-plot_s/2, plot_s/2+1]),
yaxis=dict(range=[-plot_s/2, plot_s/2+1], scaleanchor='x', scaleratio=1),
# width=384,
# height=384
)
return fig
def plot_empty_pitch_map():
fig = go.Figure()
fig.add_annotation(
x=0,
y=0,
text='No visualization
as less than 10 pitches thrown',
showarrow=False
)
fig.update_layout(
xaxis=dict(range=[-plot_s/2, plot_s/2+1]),
yaxis=dict(range=[-plot_s/2, plot_s/2+1], scaleanchor='x', scaleratio=1),
# width=384,
# height=384
)
return fig
# velo distribution
def plot_pitch_velo(player=None, velos=None, pitch_type=None, pitch_name=None):
assert not ((velos is None and player is None) or (velos is not None and player is not None)), 'exactly one of `player` or `loc` must be specified'
if velos is None and player is not None:
assert not ((pitch_type is None and pitch_name is None) or (pitch_type is not None and pitch_name is not None)), 'exactly one of `pitch_type` or `pitch_name` must be specified'
pitch_val = pitch_type or pitch_name
pitch_col = 'pitch_type' if pitch_type else 'pitch_name'
velos = df.set_index(['name', pitch_col]).loc[(player, pitch_val), 'release_speed']
fig = go.Figure(data=go.Violin(x=velos, side='positive', hoveron='points', points=False, meanline_visible=True, name='Velocity Distribution'))
fig.update_layout(
xaxis=dict(
title='Velocity',
range=[125, 170],
scaleratio=2
),
yaxis=dict(
title='Frequency',
range=[0, 0.3],
scaleanchor='x',
scaleratio=1,
tickvals=np.linspace(0, 0.3, 3),
ticktext=np.linspace(0, 0.3, 3),
),
autosize=True,
# width=512,
# height=256,
modebar_remove=['zoom', 'autoScale', 'resetScale'],
)
return fig
def plot_empty_pitch_velo():
fig = go.Figure()
fig.add_annotation(
x=(170+125)/2,
y=0.3/2,
text='No visualization
as less than 10 pitches thrown',
showarrow=False,
)
fig.update_layout(
xaxis=dict(
title='Velocity',
range=[125, 170],
scaleratio=2
),
yaxis=dict(
title='Frequency',
range=[0, 0.3],
scaleanchor='x',
scaleratio=1,
# tickvals=np.linspace(0, 0.3, 3),
# ticktext=np.linspace(0, 0.3, 3),
tickvals=[0.15],
ticktext=[0.15]
),
autosize=True,
# width=512,
# height=256,
modebar_remove=['zoom', 'autoScale', 'resetScale'],
)
return fig
def plot_all_pitch_velo(player=None, player_df=None, pitch_counts=None, min_pitches=10):
# assert not ((player is None and player_df is None) or (player is not None and player_df is not None)), 'exactly one of `player` or `player_df` must be specified'
if player_df is None and player is not None:
assert pitch_counts is None, '`pitch_counts` must be `None` if `player_df` is None'
player_df = df.sort_values('name').set_index('name').loc[player].sort_values('pitch_name').set_index('pitch_name')
pitch_counts = player_df.index.value_counts(ascending=True)
league_df = df.set_index('pitch_name')
fig = go.Figure()
velo_center = (player_df['release_speed'].min() + player_df['release_speed'].max()) / 2
for i, (pitch_name, count) in enumerate(pitch_counts.items()):
velos = player_df.loc[pitch_name, 'release_speed']
league_velos = league_df.loc[pitch_name, 'release_speed']
fig.add_trace(go.Violin(
x=league_velos,
y=[pitch_name]*len(league_velos),
line_color='gray',
side='positive',
orientation='h',
meanline_visible=True,
points=False,
legendgroup='NPB',
legendrank=1,
# visible='legendonly',
showlegend=False,
name='NPB',
))
if count >= min_pitches:
fig.add_trace(go.Violin(
x=velos,
y=[pitch_name]*len(velos),
side='positive',
orientation='h',
meanline_visible=True,
points=False,
legendgroup=pitch_name,
legendrank=2+(len(pitch_counts) - i),
name=pitch_name
))
else:
fig.add_trace(go.Scatter(
x=[velo_center],
y=[pitch_name],
text=['No visualization as less than 10 pitches thrown'],
textposition='top center',
hovertext=False,
mode="lines+text",
legendgroup=pitch_name,
legendrank=2+(len(pitch_counts) - i),
name=pitch_name,
))
fig.add_trace(go.Violin(
x=player_df['release_speed'],
y=[player]*len(player_df),
side='positive',
orientation='h',
meanline_visible=True,
points=False,
legendrank=0,
name=player
))
fig.add_trace(go.Violin(
x=league_df['release_speed'],
y=[player]*len(league_df),
line_color='gray',
side='positive',
orientation='h',
meanline_visible=True,
points=False,
legendgroup='NPB',
legendrank=1,
# visible='legendonly',
name='NPB',
))
fig.update_xaxes(title='Velocity')
return fig
def get_data(player):
player_name = f'# {player}'
_df = df.set_index('name').loc[player]
_df.to_csv(f'files/npb.csv', index=False)
_df_by_pitch_name = _df.set_index('pitch_name')
usage_fig = px.pie(_df['pitch_name'], names='pitch_name')
usage_fig.update_traces(texttemplate='%{percent:.1%}', hovertemplate=f'{player}
' + 'threw a %{label}
%{percent:.1%} of the time (%{value} pitches)')
pitch_counts = _df['pitch_name'].value_counts()
pitch_groups = []
pitch_names = []
pitch_infos = []
pitch_velos = []
pitch_maps = []
for pitch_name, count in pitch_counts.items():
pitch_groups.append(gr.update(visible=True))
pitch_names.append(gr.update(value=f'### {pitch_name}', visible=True))
pitch_infos.append(gr.update(
value=pd.DataFrame([{
'Whiff%': pitch_stats.loc[(player, pitch_name), 'Whiff%'].item(),
'CSW%': pitch_stats.loc[(player, pitch_name), 'CSW%'].item()
}]),
visible=True
))
if count > 10:
pitch_velos.append(gr.update(
value=plot_pitch_velo(velos=_df_by_pitch_name.loc[pitch_name, 'release_speed']),
visible=True
))
pitch_maps.append(gr.update(value=plot_pitch_map(player, pitch_name=pitch_name), label='Pitch location', visible=True))
else:
pitch_velos.append(gr.update(value=plot_empty_pitch_velo(),visible=True ))
pitch_maps.append(gr.update(value=plot_empty_pitch_map(), label=pitch_name, visible=True))
for _ in range(max_pitch_types - len(pitch_names)):
pitch_groups.append(gr.update(visible=False))
pitch_names.append(gr.update(value=None, visible=False))
pitch_infos.append(gr.update(value=None, visible=False))
for _ in range(max_pitch_types - len(pitch_maps)):
pitch_velos.append(gr.update(value=None, visible=False))
pitch_maps.append(gr.update(value=None, visible=False))
pitch_velo_summary = plot_all_pitch_velo(player=player, player_df=_df_by_pitch_name, pitch_counts=pitch_counts.sort_values(ascending=True))
return player_name, 'files/npb.csv', usage_fig, *pitch_groups, *pitch_names, *pitch_infos, *pitch_velos, *pitch_maps, pitch_velo_summary