|
|
import polars as pl |
|
|
import numpy as np |
|
|
import pandas as pd |
|
|
import api_scraper |
|
|
scrape = api_scraper.MLB_Scrape() |
|
|
from functions import df_update |
|
|
from functions import pitch_summary_functions |
|
|
update = df_update.df_update() |
|
|
from stuff_model import feature_engineering as fe |
|
|
from stuff_model import stuff_apply |
|
|
import requests |
|
|
import joblib |
|
|
from matplotlib.gridspec import GridSpec |
|
|
from shiny import App, reactive, ui, render |
|
|
from shiny.ui import h2, tags |
|
|
import matplotlib.pyplot as plt |
|
|
import matplotlib.gridspec as gridspec |
|
|
import seaborn as sns |
|
|
from functions.pitch_summary_functions import * |
|
|
from shiny import App, reactive, ui, render |
|
|
from shiny.ui import h2, tags |
|
|
|
|
|
import functions.PitchPlotFunctions as ppf |
|
|
import matplotlib |
|
|
ploter = ppf.PitchPlotFunctions() |
|
|
from shiny.plotutils import brushed_points |
|
|
|
|
|
|
|
|
|
|
|
colour_palette = ['#FFB000','#648FFF','#785EF0', |
|
|
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] |
|
|
cmap_sum = mcolors.LinearSegmentedColormap.from_list("", ['#648FFF', '#FFFFFF', '#FFB000']) |
|
|
|
|
|
year_list = [2025] |
|
|
|
|
|
|
|
|
type_dict = { |
|
|
'R':'Regular', |
|
|
} |
|
|
|
|
|
level_dict = {'1':'MLB',} |
|
|
|
|
|
function_dict={ |
|
|
'velocity_kdes':'Velocity Distributions', |
|
|
'break_plot':'Pitch Movement', |
|
|
'tj_stuff_roling':'Rolling tjStuff+ by Pitch', |
|
|
'tj_stuff_roling_game':'Rolling tjStuff+ by Game', |
|
|
'location_plot_lhb':'Locations vs LHB', |
|
|
'location_plot_rhb':'Locations vs RHB', |
|
|
} |
|
|
|
|
|
|
|
|
split_dict = {'all':'All', |
|
|
'left':'LHH', |
|
|
'right':'RHH'} |
|
|
|
|
|
split_dict_hand = {'all':['L','R'], |
|
|
'left':['L'], |
|
|
'right':['R']} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pitch_colours = { |
|
|
|
|
|
'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'}, |
|
|
'FA': {'colour': '#FF007D', 'name': 'Fastball'}, |
|
|
'SI': {'colour': '#98165D', 'name': 'Sinker'}, |
|
|
'FC': {'colour': '#BE5FA0', 'name': 'Cutter'}, |
|
|
|
|
|
|
|
|
'CH': {'colour': '#F79E70', 'name': 'Changeup'}, |
|
|
'FS': {'colour': '#FE6100', 'name': 'Splitter'}, |
|
|
'SC': {'colour': '#F08223', 'name': 'Screwball'}, |
|
|
'FO': {'colour': '#FFB000', 'name': 'Forkball'}, |
|
|
|
|
|
|
|
|
'SL': {'colour': '#67E18D', 'name': 'Slider'}, |
|
|
'ST': {'colour': '#1BB999', 'name': 'Sweeper'}, |
|
|
'SV': {'colour': '#376748', 'name': 'Slurve'}, |
|
|
|
|
|
|
|
|
'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'}, |
|
|
'CU': {'colour': '#3025CE', 'name': 'Curveball'}, |
|
|
'CS': {'colour': '#274BFC', 'name': 'Slow Curve'}, |
|
|
'EP': {'colour': '#648FFF', 'name': 'Eephus'}, |
|
|
|
|
|
|
|
|
'KN': {'colour': '#867A08', 'name': 'Knuckleball'}, |
|
|
'KN': {'colour': '#867A08', 'name': 'Knuckle Ball'}, |
|
|
'PO': {'colour': '#472C30', 'name': 'Pitch Out'}, |
|
|
'UN': {'colour': '#9C8975', 'name': 'Unknown'}, |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pitch_colours_tp = { |
|
|
|
|
|
'FF': {'colour': '#FF007D80', 'name': '4-Seam Fastball'}, |
|
|
'FA': {'colour': '#FF007D80', 'name': 'Fastball'}, |
|
|
'SI': {'colour': '#98165D80', 'name': 'Sinker'}, |
|
|
'FC': {'colour': '#BE5FA080', 'name': 'Cutter'}, |
|
|
|
|
|
|
|
|
'CH': {'colour': '#F79E7080', 'name': 'Changeup'}, |
|
|
'FS': {'colour': '#FE610080', 'name': 'Splitter'}, |
|
|
'SC': {'colour': '#F0822380', 'name': 'Screwball'}, |
|
|
'FO': {'colour': '#FFB00080', 'name': 'Forkball'}, |
|
|
|
|
|
|
|
|
'SL': {'colour': '#67E18D80', 'name': 'Slider'}, |
|
|
'ST': {'colour': '#1BB99980', 'name': 'Sweeper'}, |
|
|
'SV': {'colour': '#37674880', 'name': 'Slurve'}, |
|
|
|
|
|
|
|
|
'KC': {'colour': '#311D8B80', 'name': 'Knuckle Curve'}, |
|
|
'CU': {'colour': '#3025CE80', 'name': 'Curveball'}, |
|
|
'CS': {'colour': '#274BFC80', 'name': 'Slow Curve'}, |
|
|
'EP': {'colour': '#648FFF80', 'name': 'Eephus'}, |
|
|
|
|
|
|
|
|
'KN': {'colour': '#867A0880', 'name': 'Knuckleball'}, |
|
|
'KN': {'colour': '#867A0880', 'name': 'Knuckle Ball'}, |
|
|
'PO': {'colour': '#472C3080', 'name': 'Pitch Out'}, |
|
|
'UN': {'colour': '#9C897580', 'name': 'Unknown'}, |
|
|
} |
|
|
|
|
|
|
|
|
dict_colour = {key: value['colour'] for key, value in pitch_colours.items()} |
|
|
dict_pitch = {key: value['name'] for key, value in pitch_colours.items()} |
|
|
dict_pitch_desc_type = {value['name']: key for key, value in pitch_colours.items()} |
|
|
dict_pitch_desc_type.update({'Four-Seam Fastball':'FF'}) |
|
|
dict_pitch_desc_type.update({'All':'All'}) |
|
|
dict_pitch_name = {value['name']: value['colour'] for key, value in pitch_colours.items()} |
|
|
dict_pitch_name.update({'Four-Seam Fastball':'#FF007D'}) |
|
|
dict_pitch_name.update({'4-Seam':'#FF007D'}) |
|
|
dict_pitch.update({'FF':'Four-Seam Fastball'}) |
|
|
|
|
|
|
|
|
dict_pitch_name_tp = {value['name']: value['colour'] for key, value in pitch_colours_tp.items()} |
|
|
dict_pitch_name_tp.update({'Four-Seam Fastball':'#FF007D50'}) |
|
|
dict_pitch_name_tp.update({'4-Seam':'#FF007D50'}) |
|
|
|
|
|
|
|
|
dict_pitch_alpha = dict(sorted(dict_pitch.items(), key=lambda item: item[1])) |
|
|
|
|
|
from shiny import App, reactive, ui, render |
|
|
from shiny.ui import h2, tags |
|
|
|
|
|
|
|
|
app_ui = ui.page_fluid( |
|
|
ui.layout_sidebar( |
|
|
ui.panel_sidebar( |
|
|
|
|
|
ui.row( |
|
|
ui.markdown("## 2025 MLB Pitch Plots"), |
|
|
ui.markdown("This app generates a movement plot for a pitcher's pitches in Spring Training games. You can highlight and update pitch types by selecting points on the plot."), |
|
|
ui.column(4,ui.div( |
|
|
"By: ", |
|
|
ui.tags.a( |
|
|
"@TJStats", |
|
|
href="https://x.com/TJStats", |
|
|
target="_blank" |
|
|
) |
|
|
), |
|
|
ui.tags.p("Data: MLB")), |
|
|
ui.column(8, |
|
|
ui.tags.p( |
|
|
ui.tags.a( |
|
|
"Support me on Patreon for more apps", |
|
|
href="https://www.patreon.com/TJ_Stats", |
|
|
target="_blank" |
|
|
)))), |
|
|
ui.row( |
|
|
ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2025)), |
|
|
ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)), |
|
|
ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='S')) |
|
|
), |
|
|
|
|
|
ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")), |
|
|
|
|
|
ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))), |
|
|
|
|
|
ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))), |
|
|
|
|
|
ui.row( |
|
|
ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)), |
|
|
), |
|
|
ui.row( ui.column(6,ui.input_select( |
|
|
"new_pitch_type", |
|
|
"Update Pitch Type", |
|
|
dict_pitch_alpha |
|
|
)), |
|
|
ui.column(6,ui.input_action_button("update_pitch_type", "Update Pitch Type", class_="btn-secondary"))), |
|
|
|
|
|
|
|
|
|
|
|
ui.row(ui.input_action_button("generate_plot", "Generate/Reset Plot", class_="btn-primary")), |
|
|
ui.row(ui.input_action_button("generate_table", "Generate Table", class_="btn-warning")), |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
ui.panel_main( |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui.card( |
|
|
{"style": "width: 970px;"}, |
|
|
ui.head_content( |
|
|
ui.tags.script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"), |
|
|
ui.tags.script(src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"), |
|
|
ui.tags.script(""" |
|
|
async function downloadSVG() { |
|
|
const content = document.getElementById('capture-section'); |
|
|
|
|
|
// Create a new SVG element |
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); |
|
|
const bbox = content.getBoundingClientRect(); |
|
|
|
|
|
// Set SVG attributes |
|
|
svg.setAttribute('width', bbox.width); |
|
|
svg.setAttribute('height', bbox.height); |
|
|
svg.setAttribute('viewBox', `0 0 ${bbox.width} ${bbox.height}`); |
|
|
|
|
|
// Create foreignObject to contain HTML content |
|
|
const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); |
|
|
foreignObject.setAttribute('width', '100%'); |
|
|
foreignObject.setAttribute('height', '100%'); |
|
|
foreignObject.setAttribute('x', '0'); |
|
|
foreignObject.setAttribute('y', '0'); |
|
|
|
|
|
// Clone the content and its styles |
|
|
const clonedContent = content.cloneNode(true); |
|
|
|
|
|
// Add necessary style context |
|
|
const style = document.createElement('style'); |
|
|
Array.from(document.styleSheets).forEach(sheet => { |
|
|
try { |
|
|
Array.from(sheet.cssRules).forEach(rule => { |
|
|
style.innerHTML += rule.cssText + '\\n'; |
|
|
}); |
|
|
} catch (e) { |
|
|
console.warn('Could not access stylesheet rules'); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Create a wrapper div to hold styles and content |
|
|
const wrapper = document.createElement('div'); |
|
|
wrapper.appendChild(style); |
|
|
wrapper.appendChild(clonedContent); |
|
|
|
|
|
foreignObject.appendChild(wrapper); |
|
|
svg.appendChild(foreignObject); |
|
|
|
|
|
// Convert to SVG string with XML declaration and DTD |
|
|
const svgString = new XMLSerializer().serializeToString(svg); |
|
|
const svgBlob = new Blob([ |
|
|
'<?xml version="1.0" standalone="no"?>\\n', |
|
|
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\\n', |
|
|
svgString |
|
|
], {type: 'image/svg+xml;charset=utf-8'}); |
|
|
|
|
|
// Create and trigger download |
|
|
const url = URL.createObjectURL(svgBlob); |
|
|
const link = document.createElement('a'); |
|
|
link.href = url; |
|
|
link.download = 'plot_and_table.svg'; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
async function downloadPNG() { |
|
|
const content = document.getElementById('capture-section'); |
|
|
|
|
|
try { |
|
|
// Create a wrapper div with right margin only |
|
|
const wrapper = document.createElement('div'); |
|
|
wrapper.style.paddingRight = '20px'; // Only right padding |
|
|
wrapper.style.backgroundColor = 'white'; |
|
|
|
|
|
// Clone the content |
|
|
const clonedContent = content.cloneNode(true); |
|
|
wrapper.appendChild(clonedContent); |
|
|
|
|
|
// Add wrapper to document temporarily |
|
|
document.body.appendChild(wrapper); |
|
|
|
|
|
const canvas = await html2canvas(wrapper, { |
|
|
backgroundColor: 'white', |
|
|
scale: 2, |
|
|
useCORS: true, |
|
|
logging: false, |
|
|
width: content.offsetWidth + 20, // Add only right padding width |
|
|
height: content.offsetHeight // Height stays the same |
|
|
}); |
|
|
|
|
|
// Remove temporary wrapper |
|
|
document.body.removeChild(wrapper); |
|
|
|
|
|
// Convert canvas to blob |
|
|
canvas.toBlob(function(blob) { |
|
|
const url = URL.createObjectURL(blob); |
|
|
const link = document.createElement('a'); |
|
|
link.href = url; |
|
|
link.download = 'plot_and_table.png'; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
URL.revokeObjectURL(url); |
|
|
}, 'image/png'); |
|
|
} catch (error) { |
|
|
console.error('Error generating PNG:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
$(document).on('click', '#capture_svg_btn', function() { |
|
|
downloadSVG(); |
|
|
}); |
|
|
|
|
|
$(document).on('click', '#capture_png_btn', function() { |
|
|
downloadPNG(); |
|
|
}); |
|
|
""") |
|
|
), |
|
|
ui.output_text("status"), |
|
|
ui.div( |
|
|
{ |
|
|
"id": "capture-section", |
|
|
"style": "background-color: white; padding: 0; margin-left: 20px; margin-right: 20px; margin-top: 20px; margin-bottom: 20px;" |
|
|
}, |
|
|
|
|
|
ui.div( |
|
|
{"style": "position: relative;"}, |
|
|
ui.output_ui("plot_ui") |
|
|
), |
|
|
|
|
|
ui.div( |
|
|
{"style": "margin-top: 20px;"}, |
|
|
ui.row(ui.tags.b("Pitches in Selection"), ui.output_table("in_brush")), |
|
|
), |
|
|
ui.div({"style": "height: 20px;"}) |
|
|
), |
|
|
ui.div( |
|
|
{"style": "display: flex; gap: 10px;"}, |
|
|
ui.input_action_button("capture_svg_btn", "Save as SVG", class_="btn-primary"), |
|
|
ui.input_action_button("capture_png_btn", "Save as PNG", class_="btn-success"), |
|
|
), |
|
|
) |
|
|
|
|
|
|
|
|
) |
|
|
) |
|
|
) |
|
|
|
|
|
def server(input, output, session): |
|
|
|
|
|
|
|
|
|
|
|
modified_data = reactive.value(None) |
|
|
|
|
|
selection_state = reactive.value(None) |
|
|
|
|
|
|
|
|
last_pitcher_id = reactive.value(None) |
|
|
last_date_id = reactive.value(None) |
|
|
last_split_id = reactive.value(None) |
|
|
last_type_input = reactive.value(None) |
|
|
last_level_input = reactive.value(None) |
|
|
last_year_input = reactive.value(None) |
|
|
|
|
|
|
|
|
@reactive.effect |
|
|
@reactive.event(input.plot_brush) |
|
|
def _(): |
|
|
brush_data = input.plot_brush() |
|
|
selection_state.set(brush_data) |
|
|
|
|
|
|
|
|
@reactive.effect |
|
|
@reactive.event(input.pitcher_id, input.date_id, input.split_id, |
|
|
input.type_input, input.level_input, input.year_input) |
|
|
def _reset_on_data_change(): |
|
|
|
|
|
current_pitcher = input.pitcher_id() |
|
|
current_date = input.date_id() |
|
|
current_split = input.split_id() |
|
|
current_type = input.type_input() |
|
|
current_level = input.level_input() |
|
|
current_year = input.year_input() |
|
|
|
|
|
|
|
|
|
|
|
pitcher_changed = (last_pitcher_id() is not None and current_pitcher != last_pitcher_id()) |
|
|
date_changed = (last_date_id() is not None and current_date != last_date_id()) |
|
|
split_changed = (last_split_id() is not None and current_split != last_split_id()) |
|
|
type_changed = (last_type_input() is not None and current_type != last_type_input()) |
|
|
level_changed = (last_level_input() is not None and current_level != last_level_input()) |
|
|
year_changed = (last_year_input() is not None and current_year != last_year_input()) |
|
|
|
|
|
|
|
|
if (pitcher_changed or date_changed or split_changed or |
|
|
type_changed or level_changed or year_changed): |
|
|
|
|
|
modified_data.set(None) |
|
|
|
|
|
|
|
|
changed_inputs = [] |
|
|
if pitcher_changed: changed_inputs.append("pitcher") |
|
|
if date_changed: changed_inputs.append("date range") |
|
|
if split_changed: changed_inputs.append("split") |
|
|
if type_changed: changed_inputs.append("game type") |
|
|
if level_changed: changed_inputs.append("league level") |
|
|
if year_changed: changed_inputs.append("year") |
|
|
|
|
|
if changed_inputs: |
|
|
change_text = ", ".join(changed_inputs) |
|
|
ui.notification_show(f"Data filter changed ({change_text}), pitch modifications reset", type="info") |
|
|
|
|
|
|
|
|
last_pitcher_id.set(current_pitcher) |
|
|
last_date_id.set(current_date) |
|
|
last_split_id.set(current_split) |
|
|
last_type_input.set(current_type) |
|
|
last_level_input.set(current_level) |
|
|
last_year_input.set(current_year) |
|
|
@reactive.effect |
|
|
@reactive.event(input.update_pitch_type) |
|
|
def _(): |
|
|
if input.plot_brush() is None: |
|
|
ui.notification_show("Please select points first", type="warning") |
|
|
return |
|
|
|
|
|
|
|
|
if modified_data() is not None: |
|
|
|
|
|
df = modified_data().copy() |
|
|
else: |
|
|
|
|
|
year_input = int(input.year_input()) |
|
|
sport_id = int(input.level_input()) |
|
|
player_input = int(input.pitcher_id()) |
|
|
start_date = str(input.date_id()[0]) |
|
|
end_date = str(input.date_id()[1]) |
|
|
|
|
|
game_list = scrape.get_player_games_list( |
|
|
sport_id=sport_id, |
|
|
season=year_input, |
|
|
player_id=player_input, |
|
|
start_date=start_date, |
|
|
end_date=end_date, |
|
|
game_type=[input.type_input()] |
|
|
) |
|
|
|
|
|
data_list = scrape.get_data(game_list_input=game_list[:]) |
|
|
|
|
|
df = (stuff_apply.stuff_apply( |
|
|
fe.feature_engineering( |
|
|
update.update( |
|
|
scrape.get_data_df(data_list=data_list).filter( |
|
|
(pl.col("pitcher_id") == player_input) & |
|
|
(pl.col("is_pitch") == True) & |
|
|
(pl.col("start_speed") >= 50) & |
|
|
(pl.col('batter_hand').is_in(split_dict_hand[input.split_id()])) |
|
|
) |
|
|
) |
|
|
) |
|
|
)).to_pandas() |
|
|
|
|
|
|
|
|
brushed = brushed_points( |
|
|
df, |
|
|
input.plot_brush(), |
|
|
xvar="hb", |
|
|
yvar="ivb", |
|
|
all_rows=False |
|
|
) |
|
|
|
|
|
if len(brushed) == 0: |
|
|
ui.notification_show("No points selected", type="warning") |
|
|
return |
|
|
|
|
|
|
|
|
new_pitch_type = input.new_pitch_type() |
|
|
indices = brushed.index |
|
|
df.loc[indices, 'pitch_type'] = new_pitch_type |
|
|
df.loc[indices, 'pitch_description'] = dict_pitch[new_pitch_type] |
|
|
|
|
|
|
|
|
modified_data.set(df) |
|
|
|
|
|
|
|
|
pl_df = pl.from_pandas(df) |
|
|
pl_df = pl_df.with_columns( |
|
|
prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"), |
|
|
prop=pl.col('is_pitch').sum().over("pitch_type") |
|
|
) |
|
|
|
|
|
|
|
|
modified_data.set(pl_df.to_pandas()) |
|
|
|
|
|
|
|
|
ui.notification_show(f"Updated {len(indices)} pitches to {dict_pitch[new_pitch_type]}", type="success") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@render.ui |
|
|
@reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False) |
|
|
def player_select_ui(): |
|
|
|
|
|
|
|
|
if input.level_input() == '21': |
|
|
year_input = int(input.year_input()) |
|
|
sport_id = int(input.level_input()) |
|
|
|
|
|
start_date = str(input.date_id()[0]) |
|
|
end_date = str(input.date_id()[1]) |
|
|
game_type = [input.type_input()] |
|
|
|
|
|
|
|
|
|
|
|
game_list = scrape.get_schedule(year_input=[year_input], sport_id=[sport_id], game_type=game_type).filter((pl.col('date').cast(pl.Utf8)>=start_date)&(pl.col('date').cast(pl.Utf8)<=end_date))['game_id'] |
|
|
data_list = scrape.get_data(game_list_input=game_list[:]) |
|
|
df_pitcher_info = scrape.get_data_df(data_list=data_list).filter((pl.col("start_speed") >= 50)).sort('pitcher_name') |
|
|
pitcher_dict = dict(zip(df_pitcher_info['pitcher_id'], df_pitcher_info['pitcher_name'])) |
|
|
return ui.input_select("pitcher_id", "Select Pitcher",pitcher_dict, selectize=True) |
|
|
|
|
|
|
|
|
df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter( |
|
|
(pl.col("position").is_in(['P','TWP']))| |
|
|
(pl.col("player_id").is_in([686846])) |
|
|
|
|
|
).sort("name") |
|
|
|
|
|
|
|
|
pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name'])) |
|
|
|
|
|
|
|
|
return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True) |
|
|
|
|
|
@render.ui |
|
|
@reactive.event(input.player_button,input.year_input, ignore_none=False) |
|
|
def date_id(): |
|
|
|
|
|
return ui.input_date_range("date_id", "Select Date Range", |
|
|
start=f"{int(input.year_input())}-01-01", |
|
|
end=f"{int(input.year_input())}-12-31", |
|
|
min=f"{int(input.year_input())}-01-01", |
|
|
max=f"{int(input.year_input())}-12-31") |
|
|
@output |
|
|
@render.text |
|
|
def status(): |
|
|
|
|
|
if input.generate == 0: |
|
|
return "" |
|
|
return "" |
|
|
|
|
|
@render.ui |
|
|
@reactive.event(input.generate_plot) |
|
|
def plot_ui(): |
|
|
brush_opts_kwargs = { |
|
|
"direction": 'xy', |
|
|
"delay": 250, |
|
|
"delay_type": "throttle", |
|
|
"clip": True, |
|
|
"fill": "#00000050", |
|
|
"stroke": "#000000", |
|
|
} |
|
|
|
|
|
return ui.output_plot('plot', |
|
|
width='900px', |
|
|
height='900px', |
|
|
brush=ui.brush_opts(**brush_opts_kwargs)) |
|
|
|
|
|
@render.table |
|
|
@reactive.event(input.plot_brush,input.generate_plot, input.generate_table, input.update_pitch_type) |
|
|
def in_brush(): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if modified_data() is not None: |
|
|
df = pl.from_pandas(modified_data()) |
|
|
else: |
|
|
|
|
|
year_input = int(input.year_input()) |
|
|
sport_id = int(input.level_input()) |
|
|
player_input = int(input.pitcher_id()) |
|
|
start_date = str(input.date_id()[0]) |
|
|
end_date = str(input.date_id()[1]) |
|
|
print('sportid',input.type_input()) |
|
|
|
|
|
game_list = scrape.get_player_games_list(sport_id = sport_id, |
|
|
season = year_input, |
|
|
player_id = player_input, |
|
|
start_date = start_date, |
|
|
end_date = end_date, |
|
|
game_type = [input.type_input()]) |
|
|
|
|
|
data_list = scrape.get_data(game_list_input = game_list[:]) |
|
|
|
|
|
try: |
|
|
df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter( |
|
|
(pl.col("pitcher_id") == player_input)& |
|
|
(pl.col("is_pitch") == True)& |
|
|
(pl.col("start_speed") >= 50)& |
|
|
(pl.col('batter_hand').is_in(split_dict_hand[input.split_id()])) |
|
|
|
|
|
)[:]))).with_columns( |
|
|
pl.col('pitch_type').count().over('pitch_type').alias('pitch_count') |
|
|
)) |
|
|
|
|
|
df = df.with_columns( |
|
|
prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"), |
|
|
prop=pl.col('is_pitch').sum().over("pitch_type") |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except TypeError: |
|
|
print("NONE") |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if input.plot_brush() is None: |
|
|
brushed_df = df.clone() |
|
|
print('TABLE DF:',df) |
|
|
|
|
|
else: |
|
|
brushed_df = pl.DataFrame(brushed_points( |
|
|
df.to_pandas(), |
|
|
input.plot_brush(), |
|
|
xvar="hb", |
|
|
yvar="ivb", |
|
|
all_rows=False |
|
|
)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
brushed_df_final = (((brushed_df.group_by(['pitcher_id', 'pitch_description']).agg([ |
|
|
pl.col('is_pitch').drop_nans().count().alias('pitches'), |
|
|
pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'), |
|
|
pl.col('vb').drop_nans().mean().round(1).alias('vb'), |
|
|
pl.col('ivb').drop_nans().mean().round(1).alias('ivb'), |
|
|
pl.col('hb').drop_nans().mean().round(1).alias('hb'), |
|
|
pl.col('spin_rate').drop_nans().mean().alias('spin_rate'), |
|
|
pl.col('release_pos_x').drop_nans().mean().round(1).alias('x0'), |
|
|
pl.col('release_pos_z').drop_nans().mean().round(1).alias('z0'), |
|
|
pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'), |
|
|
pl.col('pitch_grade').drop_nans().mean().round(0).alias('pitch_grade'), |
|
|
]) |
|
|
.with_columns( |
|
|
(pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id')) |
|
|
|
|
|
|
|
|
.alias('proportion') |
|
|
) |
|
|
)).sort('proportion', descending=True). |
|
|
select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb", |
|
|
"spin_rate", "x0", "z0",'tj_stuff_plus','pitch_grade']) |
|
|
.with_columns( |
|
|
pl.when(pl.col("pitch_description") == "Four-Seam Fastball") |
|
|
.then(pl.lit("4-Seam")) |
|
|
.otherwise(pl.col("pitch_description")) |
|
|
.alias("pitch_description") |
|
|
) |
|
|
.rename({ |
|
|
'pitch_description': 'Pitch Type', |
|
|
'pitches': 'Pitches', |
|
|
'proportion': 'Prop', |
|
|
'start_speed': 'Velo', |
|
|
'ivb': 'iVB', |
|
|
'hb': 'HB', |
|
|
'spin_rate': 'Spin', |
|
|
'x0': 'hRel', |
|
|
'z0': 'vRel', |
|
|
'tj_stuff_plus': 'tjStuff+', |
|
|
'pitch_grade': 'Grade' |
|
|
})) |
|
|
|
|
|
brushed_df_final_pd = brushed_df_final.to_pandas() |
|
|
brushed_df_final_pd['Spin'] = brushed_df_final_pd['Spin'].fillna(0) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def change_font(val): |
|
|
if val == "Cutter": |
|
|
return "color: red; font-weight: bold;" |
|
|
else: |
|
|
'' |
|
|
return "font-weight: bold;" |
|
|
|
|
|
df_brush_style = (brushed_df_final_pd.style.set_precision(1) |
|
|
|
|
|
.set_properties(**{'border': '3 px'},overwrite=False).set_table_styles([{ |
|
|
'selector': 'caption', |
|
|
'props': [ |
|
|
('color', ''), |
|
|
('fontname', 'Century Gothic'), |
|
|
('font-size', '16px'), |
|
|
('font-style', 'italic'), |
|
|
('font-weight', ''), |
|
|
('text-align', 'centre'), |
|
|
] |
|
|
|
|
|
},{'selector' :'th', 'props':[('font-size', '16px'),('text-align', 'center'),('Height','px'),('color','black'),('border', '1px black solid !important')]},{'selector' :'td', 'props':[('text-align', 'center'),('font-size', '16px'),('color','black')]}],overwrite=False) |
|
|
.set_properties(**{'background-color':'White','index':'White','min-width':'72px'},overwrite=False) |
|
|
.set_table_styles([{'selector': 'th:first-child', 'props': [('background-color', 'white')]}],overwrite=False) |
|
|
.set_table_styles([{'selector': 'tr:first-child', 'props': [('background-color', 'white')]}],overwrite=False) |
|
|
.set_table_styles([{'selector': 'tr', 'props': [('line-height', '20px')]}],overwrite=False) |
|
|
.set_properties(**{'Height': '8px'},**{'text-align': 'center'},overwrite=False) |
|
|
.hide_index() |
|
|
.set_properties(**{'border': '1px black solid !important'}) |
|
|
.format('{:.0%}',subset=(brushed_df_final.columns[2])) |
|
|
.format('{:.0f}',subset=(brushed_df_final.columns[6])) |
|
|
.format('{:.0f}',subset=(brushed_df_final.columns[-1])) |
|
|
.format('{:.0f}',subset=(brushed_df_final.columns[-2])) |
|
|
.set_properties(subset=brushed_df_final.columns, **{'height': '30px'}) |
|
|
.set_table_styles([{'selector': 'thead th', 'props': [('height', '30px')]}], overwrite=False) |
|
|
|
|
|
.set_table_styles([{'selector': 'thead th:nth-child(1)', 'props': [('min-width', '155px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(2)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(3)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(4)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(5)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(6)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(7)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(8)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.set_table_styles([{'selector': 'thead th:nth-child(9)', 'props': [('min-width', '40px')]}], overwrite=False) |
|
|
.background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-2]),vmin=80,vmax=120) |
|
|
.background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-1]),vmin=20,vmax=80) |
|
|
.applymap(lambda x: f'background-color: {dict_pitch_name.get(x, "")}80', subset=['Pitch Type']) |
|
|
.applymap(lambda x: f'background-color: black' if x == 0 else '', subset=['Spin']) |
|
|
|
|
|
|
|
|
) |
|
|
|
|
|
print('BRUSHED:',df_brush_style) |
|
|
|
|
|
return df_brush_style |
|
|
|
|
|
|
|
|
|
|
|
@render.plot |
|
|
@reactive.event(input.generate_plot, input.update_pitch_type) |
|
|
def plot(): |
|
|
|
|
|
with ui.Progress(min=0, max=1) as p: |
|
|
year_input = int(input.year_input()) |
|
|
sport_id = int(input.level_input()) |
|
|
player_input = int(input.pitcher_id()) |
|
|
start_date = str(input.date_id()[0]) |
|
|
end_date = str(input.date_id()[1]) |
|
|
game_type = [input.type_input()] |
|
|
p.set(message="Generating plot", detail="This may take a while...") |
|
|
|
|
|
|
|
|
if modified_data() is not None: |
|
|
df = pl.from_pandas(modified_data()) |
|
|
|
|
|
if df is None: |
|
|
fig = plt.figure(figsize=(10, 10)) |
|
|
fig.text(x=0.1, y=0.9, s='No Statcast Data For This Pitcher', fontsize=24, ha='left') |
|
|
return fig |
|
|
|
|
|
else: |
|
|
|
|
|
p.set(0.3, "Gathering data...") |
|
|
|
|
|
|
|
|
|
|
|
game_list = scrape.get_player_games_list( |
|
|
sport_id=sport_id, |
|
|
season=year_input, |
|
|
player_id=player_input, |
|
|
start_date=start_date, |
|
|
end_date=end_date, |
|
|
game_type=game_type |
|
|
) |
|
|
|
|
|
data_list = scrape.get_data(game_list_input=game_list[:]) |
|
|
|
|
|
|
|
|
try: |
|
|
df = (stuff_apply.stuff_apply( |
|
|
fe.feature_engineering( |
|
|
update.update( |
|
|
scrape.get_data_df(data_list=data_list).filter( |
|
|
(pl.col("pitcher_id") == player_input) & |
|
|
(pl.col("is_pitch") == True) & |
|
|
(pl.col("start_speed") >= 50) & |
|
|
(pl.col('batter_hand').is_in(split_dict_hand[input.split_id()])) |
|
|
)[:] |
|
|
) |
|
|
) |
|
|
)).with_columns( |
|
|
pl.col('pitch_type').count().over('pitch_type').alias('pitch_count') |
|
|
) |
|
|
|
|
|
df = df.with_columns( |
|
|
prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"), |
|
|
prop=pl.col('is_pitch').sum().over("pitch_type") |
|
|
) |
|
|
|
|
|
if df is None: |
|
|
fig = plt.figure(figsize=(10, 10)) |
|
|
fig.text(x=0.1, y=0.9, s='No Statcast Data For This Pitcher', fontsize=24, ha='left') |
|
|
return fig |
|
|
|
|
|
except TypeError: |
|
|
print("NONE") |
|
|
|
|
|
fig = plt.figure(figsize=(10, 10)) |
|
|
fig.text(x=0.1, y=0.9, s='No Statcast Data For This Pitcher', fontsize=24, ha='left') |
|
|
return fig |
|
|
|
|
|
|
|
|
if df is None: |
|
|
fig = plt.figure(figsize=(10, 10)) |
|
|
fig.text(x=0.1, y=0.9, s='No Statcast Data For This Pitcher', fontsize=24, ha='left') |
|
|
return fig |
|
|
|
|
|
df = df.clone() |
|
|
|
|
|
|
|
|
p.set(0.6, "Creating plot...") |
|
|
return ploter.final_plot( |
|
|
df=df, |
|
|
pitcher_id=player_input, |
|
|
plot_picker='short_form_movement', |
|
|
sport_id=sport_id, |
|
|
game_type=[input.type_input()] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
app = App(app_ui, server) |