| | 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 |
| | 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 = [2017,2018,2019,2020,2021,2022,2023,2024] |
| |
|
| |
|
| |
|
| | level_dict = {'1':'MLB', |
| | '11':'AAA', |
| | '12':'AA', |
| | '13':'A+', |
| | '14':'A', |
| | '17':'AFL', |
| | '22':'College', |
| | '21':'Prospects', |
| | '51':'International' } |
| |
|
| | 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'}, |
| | } |
| |
|
| | |
| | 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'}) |
| |
|
| |
|
| | 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.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)), |
| | ui.column(6, ui.input_select('level_input', 'Select Level', level_dict)) |
| | ), |
| | |
| | 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.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")), |
| | ui.row(ui.input_action_button("generate_table", "Generate Table", class_="btn-warning")), |
| | |
| | ), |
| |
|
| | |
| | ui.panel_main( |
| | |
| | |
| | |
| | |
| | ui.card( |
| | {"style": "width: 870px;"}, |
| | ui.head_content( |
| | ui.tags.script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.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); |
| | } |
| | |
| | $(document).on('click', '#capture_btn', function() { |
| | downloadSVG(); |
| | }); |
| | """) |
| | ), |
| | 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.input_action_button("capture_btn", "Save as SVG", class_="btn-primary"), |
| | ) |
| | |
| | |
| | ) |
| | ) |
| | ) |
| |
|
| |
|
| | def server(input, output, session): |
| |
|
| | @reactive.calc |
| | @reactive.event(input.pitcher_id, input.date_id,input.split_id) |
| | def cached_data(): |
| |
|
| | 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) |
| |
|
| | 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('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") |
| | ) |
| |
|
| | return df |
| |
|
| | @render.ui |
| | @reactive.event(input.player_button, input.level_input,input.year_input, ignore_none=False) |
| | def player_select_ui(): |
| | |
| | df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter( |
| | pl.col("position").is_in(['P'])).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.pitcher_id,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())}-03-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 = {} |
| | brush_opts_kwargs["direction"] = 'xy' |
| | brush_opts_kwargs["delay"] = 60 |
| | brush_opts_kwargs["delay_type"] = "throttle" |
| | |
| | |
| | return ui.output_plot('plot', |
| | width='800px', |
| | height='800px', |
| | brush=ui.brush_opts(**brush_opts_kwargs)) |
| |
|
| | @render.table |
| | @reactive.event(input.plot_brush, input.generate_table) |
| | def in_brush(): |
| | |
| | |
| | brushed_df = pl.DataFrame(brushed_points( |
| | cached_data().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().round(0).alias('spin_rate'), |
| | pl.col('x0').drop_nans().mean().round(1).alias('x0'), |
| | pl.col('z0').drop_nans().mean().round(1).alias('z0'), |
| | pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'), |
| | ]) |
| | .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']) |
| | .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+' |
| | })) |
| | |
| | |
| | |
| | |
| | |
| | def change_font(val): |
| | if val == "Cutter": |
| | return "color: red; font-weight: bold;" |
| | else: |
| | '' |
| | return "font-weight: bold;" |
| | df_brush_style = (brushed_df_final.to_pandas().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])) |
| | .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', '125px')]}], 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) |
| | .background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-1]),vmin=80,vmax=120) |
| | .applymap(lambda x: f'background-color: {dict_pitch_name.get(x, "")}', subset=['Pitch Type']) |
| | |
| | |
| | ) |
| |
|
| | return df_brush_style |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | @render.plot |
| | @reactive.event(input.generate_plot) |
| | def plot(): |
| | |
| | with ui.Progress(min=0, max=1) as p: |
| | p.set(message="Generating plot", detail="This may take a while...") |
| |
|
| | |
| | p.set(0.3, "Gathering data...") |
| | 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(year_input, sport_id, player_input, start_date, end_date) |
| |
|
| | df = cached_data() |
| | df = df.clone() |
| |
|
| | p.set(0.6, "Creating plot...") |
| |
|
| |
|
| | ploter.final_plot( |
| | df=df, |
| | pitcher_id=player_input, |
| | plot_picker='short_form_movement', |
| | sport_id=sport_id) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| |
|
| | |
| | |
| | |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| |
|
| | |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| |
|
| | |
| | |
| |
|
| | |
| | |
| |
|
| | |
| | |
| |
|
| | |
| |
|
| | |
| |
|
| | |
| | |
| | |
| |
|
| | app = App(app_ui, server) |