Spaces:
Running
Running
| 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 | |
| # from functions.PitchPlotFunctions import * | |
| import functions.PitchPlotFunctions as ppf | |
| import matplotlib | |
| ploter = ppf.PitchPlotFunctions() | |
| from shiny.plotutils import brushed_points | |
| # from pytabulator import TableOptions, Tabulator, output_tabulator, render_tabulator, theme | |
| # theme.tabulator_site() | |
| colour_palette = ['#FFB000','#648FFF','#785EF0', | |
| '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] | |
| cmap_sum = mcolors.LinearSegmentedColormap.from_list("", ['#648FFF', '#FFFFFF', '#FFB000']) | |
| year_list = [2025,2024,2023,2022,2021,2020,2019,2018,2017] | |
| type_dict = {'R':'Regular', | |
| 'P':'Playoffs', | |
| 'S':'Spring', | |
| 'E':'Exhibition'} | |
| 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 ### | |
| # Dictionary to map pitch types to their corresponding colors and names | |
| pitch_colours = { | |
| ## Fastballs ## | |
| 'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'}, | |
| 'FA': {'colour': '#FF007D', 'name': 'Fastball'}, | |
| 'SI': {'colour': '#98165D', 'name': 'Sinker'}, | |
| 'FC': {'colour': '#BE5FA0', 'name': 'Cutter'}, | |
| ## Offspeed ## | |
| 'CH': {'colour': '#F79E70', 'name': 'Changeup'}, | |
| 'FS': {'colour': '#FE6100', 'name': 'Splitter'}, | |
| 'SC': {'colour': '#F08223', 'name': 'Screwball'}, | |
| 'FO': {'colour': '#FFB000', 'name': 'Forkball'}, | |
| ## Sliders ## | |
| 'SL': {'colour': '#67E18D', 'name': 'Slider'}, | |
| 'ST': {'colour': '#1BB999', 'name': 'Sweeper'}, | |
| 'SV': {'colour': '#376748', 'name': 'Slurve'}, | |
| ## Curveballs ## | |
| 'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'}, | |
| 'CU': {'colour': '#3025CE', 'name': 'Curveball'}, | |
| 'CS': {'colour': '#274BFC', 'name': 'Slow Curve'}, | |
| 'EP': {'colour': '#648FFF', 'name': 'Eephus'}, | |
| ## Others ## | |
| 'KN': {'colour': '#867A08', 'name': 'Knuckleball'}, | |
| 'KN': {'colour': '#867A08', 'name': 'Knuckle Ball'}, | |
| 'PO': {'colour': '#472C30', 'name': 'Pitch Out'}, | |
| 'UN': {'colour': '#9C8975', 'name': 'Unknown'}, | |
| } | |
| # Dictionary to map pitch types to their corresponding colors and names | |
| pitch_colours_tp = { | |
| ## Fastballs ## | |
| 'FF': {'colour': '#FF007D80', 'name': '4-Seam Fastball'}, | |
| 'FA': {'colour': '#FF007D80', 'name': 'Fastball'}, | |
| 'SI': {'colour': '#98165D80', 'name': 'Sinker'}, | |
| 'FC': {'colour': '#BE5FA080', 'name': 'Cutter'}, | |
| ## Offspeed ## | |
| 'CH': {'colour': '#F79E7080', 'name': 'Changeup'}, | |
| 'FS': {'colour': '#FE610080', 'name': 'Splitter'}, | |
| 'SC': {'colour': '#F0822380', 'name': 'Screwball'}, | |
| 'FO': {'colour': '#FFB00080', 'name': 'Forkball'}, | |
| ## Sliders ## | |
| 'SL': {'colour': '#67E18D80', 'name': 'Slider'}, | |
| 'ST': {'colour': '#1BB99980', 'name': 'Sweeper'}, | |
| 'SV': {'colour': '#37674880', 'name': 'Slurve'}, | |
| ## Curveballs ## | |
| 'KC': {'colour': '#311D8B80', 'name': 'Knuckle Curve'}, | |
| 'CU': {'colour': '#3025CE80', 'name': 'Curveball'}, | |
| 'CS': {'colour': '#274BFC80', 'name': 'Slow Curve'}, | |
| 'EP': {'colour': '#648FFF80', 'name': 'Eephus'}, | |
| ## Others ## | |
| 'KN': {'colour': '#867A0880', 'name': 'Knuckleball'}, | |
| 'KN': {'colour': '#867A0880', 'name': 'Knuckle Ball'}, | |
| 'PO': {'colour': '#472C3080', 'name': 'Pitch Out'}, | |
| 'UN': {'colour': '#9C897580', 'name': 'Unknown'}, | |
| } | |
| # Create dictionaries for pitch types and their attributes | |
| 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'}) | |
| # Sort dict_pitch alphabetically by pitch name | |
| dict_pitch_alpha = dict(sorted(dict_pitch.items(), key=lambda item: item[1])) | |
| import requests | |
| import os | |
| CAMPAIGN_ID = os.getenv("CAMPAIGN_ID") | |
| ACCESS_TOKEN = os.getenv("ACCESS_TOKEN") | |
| BACKUP_PW = os.getenv("BACKUP_PW") | |
| ADMIN_PW = os.getenv("ADMIN_PW") | |
| url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members" | |
| headers = { | |
| "Authorization": f"Bearer {ACCESS_TOKEN}" | |
| } | |
| # Simple parameters, requesting the member's email and currently entitled tiers | |
| params = { | |
| "fields[member]": "full_name,email", # Request the member's email | |
| "include": "currently_entitled_tiers", # Include the currently entitled tiers | |
| "page[size]": 10000 # Fetch up to 1000 patrons per request | |
| } | |
| response = requests.get(url, headers=headers, params=params) | |
| VALID_PASSWORDS = [] | |
| if response.status_code == 200: | |
| data = response.json() | |
| for patron in data['data']: | |
| try: | |
| tiers = patron['relationships']['currently_entitled_tiers']['data'] | |
| if any(tier['id'] == '9078921' for tier in tiers): | |
| full_name = patron['attributes']['email'] | |
| VALID_PASSWORDS.append(full_name) | |
| except KeyError: | |
| continue | |
| VALID_PASSWORDS.append(BACKUP_PW) | |
| VALID_PASSWORDS.append(ADMIN_PW) | |
| from shiny import App, reactive, ui, render | |
| from shiny.ui import h2, tags | |
| # Define the login UI | |
| login_ui = ui.page_fluid( | |
| ui.card( | |
| ui.h2([ | |
| "TJStats Daily Pitching Summary App ", | |
| ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank") | |
| ]), | |
| ui.p( | |
| "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ", | |
| ui.tags.a("Patreon post", href="https://www.patreon.com/posts/122860440", target="_blank"), | |
| "." | |
| ), | |
| ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="25%"), | |
| ui.tags.input( | |
| type="checkbox", | |
| id="authenticated", | |
| value=False, | |
| disabled=True | |
| ), | |
| ui.input_action_button("login", "Login", class_="btn-primary"), | |
| ui.output_text("login_message"), | |
| ) | |
| ) | |
| # Define the UI layout for the app | |
| main_ui = ui.page_fluid( | |
| ui.layout_sidebar( | |
| ui.panel_sidebar( | |
| # Row for selecting season and level | |
| ui.row( | |
| ui.markdown("## MLB & MilB Pitch Plots"), | |
| ui.markdown("This app generates a movement plot for a pitcher's pitches in MLB and MiLB 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='R')) | |
| ), | |
| # Row for the action button to get player list | |
| ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")), | |
| # Row for selecting the player | |
| ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))), | |
| # Row for selecting the date range | |
| 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.hr(), | |
| # Row for the action button to generate plot | |
| 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.navset_tab( | |
| # Tab for game summary plot | |
| # ui.nav( | |
| # "Pitching Summary", | |
| 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;" | |
| }, | |
| # Plot section with relative positioning for brush | |
| ui.div( | |
| {"style": "position: relative;"}, | |
| ui.output_ui("plot_ui") | |
| ), | |
| # Table section | |
| 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"), | |
| ), | |
| ) | |
| # ), | |
| # ) | |
| ) | |
| ) | |
| ) | |
| # Combined UI with conditional panel | |
| app_ui = ui.page_fluid( | |
| ui.tags.head( | |
| ui.tags.script(src="script.js") | |
| ), | |
| ui.panel_conditional( | |
| "!input.authenticated", | |
| login_ui | |
| ), | |
| ui.panel_conditional( | |
| "input.authenticated", | |
| main_ui | |
| ) | |
| ) | |
| def server(input, output, session): | |
| # This code should be inserted in your server function | |
| def check_password(): | |
| if input.password() in VALID_PASSWORDS: | |
| ui.update_checkbox("authenticated", value=True) | |
| ui.update_text("login_message", value="") | |
| else: | |
| ui.update_text("login_message", value="Invalid password!") | |
| ui.update_text("password", value="") | |
| def login_message(): | |
| return "" | |
| # Add this near the top of the server function | |
| modified_data = reactive.value(None) | |
| # Add a reactive value to store the current selection state | |
| selection_state = reactive.value(None) | |
| # Create reactive values to track the state of all data-dependent inputs | |
| 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) | |
| # Modify your brush handler to update the selection state | |
| def _(): | |
| brush_data = input.plot_brush() | |
| selection_state.set(brush_data) # Store the current brush data | |
| # Reset modified data when any of the key inputs change | |
| def _reset_on_data_change(): | |
| # Store the current values for comparison | |
| 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() | |
| # Check if any of the inputs have changed from their last values | |
| # and they aren't None or initial values | |
| 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 any of the inputs have changed | |
| if (pitcher_changed or date_changed or split_changed or | |
| type_changed or level_changed or year_changed): | |
| # Reset modified data | |
| modified_data.set(None) | |
| # Show notification | |
| 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") | |
| # Update the last values | |
| 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) | |
| def _(): | |
| if input.plot_brush() is None: | |
| ui.notification_show("Please select points first", type="warning") | |
| return | |
| # Get the current data - either use the previously modified data or fetch fresh data | |
| if modified_data() is not None: | |
| # Use already modified data to preserve previous changes | |
| df = modified_data().copy() | |
| else: | |
| # First time modifying, get fresh 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, | |
| 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() | |
| # Get the brushed points | |
| 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 | |
| # Update pitch types for brushed points | |
| 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] | |
| # Store the modified data for future updates | |
| modified_data.set(df) | |
| # Recalculate percentages and counts | |
| 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") | |
| ) | |
| # Convert back to pandas and update the reactive value | |
| modified_data.set(pl_df.to_pandas()) | |
| # Show success notification | |
| ui.notification_show(f"Updated {len(indices)} pitches to {dict_pitch[new_pitch_type]}", type="success") | |
| # Reset button handler - clear modified data to start fresh | |
| # @reactive.effect | |
| # @reactive.event(input.reset_changes) | |
| # def _reset_modifications(): | |
| # modified_data.set(None) | |
| # ui.notification_show("All pitch type changes have been reset", type="info") | |
| def player_select_ui(): | |
| # Get the list of pitchers for the selected level and season | |
| if input.level_input() == '21': | |
| 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()] | |
| # Get game data | |
| 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) | |
| # Get the list of pitchers for the selected level and season | |
| 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") | |
| # Create a dictionary of pitcher IDs and names | |
| pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name'])) | |
| # Return a select input for choosing a pitcher | |
| return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True) | |
| def date_id(): | |
| # Create a date range input for selecting the date range within the selected year | |
| 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") | |
| def status(): | |
| # Only show status when generating | |
| if input.generate == 0: | |
| return "" | |
| return "" | |
| def plot_ui(): | |
| brush_opts_kwargs = { | |
| "direction": 'xy', | |
| "delay": 250, | |
| "delay_type": "throttle", | |
| "clip": True, # This helps constrain the brush to the plot area | |
| "fill": "#00000050", # Optional: sets a semi-transparent fill | |
| "stroke": "#000000", # Resets brush when new data is loaded | |
| } | |
| return ui.output_plot('plot', | |
| width='900px', | |
| height='900px', | |
| brush=ui.brush_opts(**brush_opts_kwargs)) | |
| def in_brush(): | |
| # if input.plot_brush() is None: # Note: changed to match the brush ID | |
| # return None | |
| # Use modified data if available | |
| 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()) | |
| # Simulate an expensive data operation | |
| 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 | |
| # df = df.clone() | |
| # print('TABLE DF:',brushed_points()) | |
| 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('vaa').drop_nans().mean().round(1).alias('vaa'), | |
| 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')) | |
| # .round(1) | |
| # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%" | |
| .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', | |
| # 'vaa': 'VAA', | |
| '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) | |
| # brushed_df_final = brushed_df_final | |
| # print(brushed_df_final) | |
| 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': 'table', 'props': [('width', '100px')]}], 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 | |
| # @output | |
| def plot(): | |
| # Initialize progress bar+ | |
| 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...") | |
| # Use modified data if available | |
| 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: | |
| # Get input parameters | |
| p.set(0.3, "Gathering data...") | |
| # Get game 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[:]) | |
| # Process data | |
| 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") | |
| # 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 | |
| # return None | |
| 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() | |
| # Create plot | |
| 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) |