from shiny import App, ui, render, reactive 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 import math 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'] # df = pl.read_csv("data.csv") # df = pl.read_parquet("data_small.parquet")[:] # df = pl.read_parquet("data.parquet")[:] # print('df') season = 2024 df_mlb = pl.read_parquet("data/data_mlb_2024.parquet")[:] df_aaa = pl.read_parquet("data/data_aaa_2024.parquet")[:] df_a = pl.read_parquet("data/data_a_2024.parquet")[:] # df_mlb = pl.read_parquet("data_small.parquet")[:] # df_aaa = pl.read_parquet("data_small_aaa.parquet")[:] # df_a = pl.read_parquet("data_small_a.parquet")[:] def df_final(df:pl.dataframe,year_input:int,sport_id:int): df_schedule = scrape.get_schedule(year_input=[year_input],sport_id=[sport_id]) df = df.join(df_schedule, on='game_id', how='left') df = df.with_columns( pl.when((pl.col('batter_team_id') == pl.col('away_id'))) .then(pl.lit('Away')) .when((pl.col('batter_team_id') == pl.col('home_id'))) .then(pl.lit('Home')) .otherwise(None) .alias('home_away') ) df = df.with_columns( pl.when((pl.col('pitcher_team_id') == pl.col('away_id'))) .then(pl.lit('Away')) .when((pl.col('pitcher_team_id') == pl.col('home_id'))) .then(pl.lit('Home')) .otherwise(None) .alias('home_away_pitcher') ) print('schedule') # Calculate mound_to_release as 60.5 - extension df = df.with_columns([ (60.5 - df["extension"]).alias("release_pos_y") ]) # Calculate delta time (Δt) delta_t = (df["release_pos_y"] - df["y0"]) / df["vy0"] # Corrected back-calculation of release_pos_x and release_pos_z df = df.with_columns([ (df["x0"] + df["vx0"] * delta_t + 0.5 * df["ax"] * delta_t ** 2).alias("release_pos_x"), (df["z0"] + df["vz0"] * delta_t + 0.5 * df["az"] * delta_t ** 2).alias("release_pos_z") ]) df_stuff = stuff_apply.stuff_apply(fe.feature_engineering(df)) print('stuff') df_up = update.update(df) print('update') df_total = df_up.join(df_stuff[['play_id','tj_stuff_plus']], on='play_id', how='left') print('total') return df_total df_mlb_total = df_final(df=df_mlb,year_input=season,sport_id=1) df_aaa_total = df_final(df=df_aaa,year_input=season,sport_id=11) df_a_total = df_final(df=df_a.drop_nulls(subset=['start_speed']),year_input=season,sport_id=14) rounding_dict = { 'pa': 0, 'bip': 0, 'hits': 0, 'k': 0, 'bb': 0, 'max_launch_speed': 1, 'launch_speed_90': 1, 'launch_speed': 1, 'pitches': 0, 'tj_stuff_plus_avg': 0, 'avg': 3, 'obp': 3, 'slg': 3, 'ops': 3, 'k_percent': 3, 'bb_percent': 3, 'k_minus_bb_percent': 3, 'sweet_spot_percent': 3, 'woba_percent': 3, 'xwoba_percent': 3, 'woba_percent_contact': 3, 'xwoba_percent_contact': 3, 'hard_hit_percent': 3, 'barrel_percent': 3, 'zone_contact_percent': 3, 'zone_swing_percent': 3, 'zone_percent': 3, 'chase_percent': 3, 'chase_contact': 3, 'swing_percent': 3, 'whiff_rate': 3, 'swstr_rate': 3, 'ground_ball_percent': 3, 'line_drive_percent': 3, 'fly_ball_percent': 3, 'pop_up_percent': 3, 'pulled_fly_ball_percent': 3, 'heart_zone_swing_percent': 3, 'shadow_zone_swing_percent': 3, 'chase_zone_swing_percent': 3, 'waste_zone_swing_percent': 3, 'heart_zone_whiff_percent': 3, 'shadow_zone_whiff_percent': 3, 'chase_zone_whiff_percent': 3, 'waste_zone_whiff_percent': 3, 'start_speed_avg': 1, 'vb_avg': 1, 'ivb_avg': 1, 'hb_avg': 1, 'z0_avg': 1, 'x0_avg': 1, 'vaa_avg': 1, 'haa_avg': 1, 'spin_rate_avg': 0, 'extension_avg': 1 } columns = [ { "title": "PA", "field": "pa", "width": 150}, { "title": "BBE", "field": "bip", "width": 150 }, { "title": "H", "field": "hits", "width": 150 }, { "title": "K", "field": "k", "width": 150 }, { "title": "BB", "field": "bb", "width": 150 }, { "title": "Max EV", "field": "max_launch_speed", "width": 150 }, { "title": "90th% EV", "field": "launch_speed_90", "width": 150 }, { "title": "EV", "field": "launch_speed", "width": 150 }, { "title": "Pitches", "field": "pitches", "width": 150 }, { "title": "AVG", "field": "avg", "width": 150 }, { "title": "OBP", "field": "obp", "width": 150 }, { "title": "SLG", "field": "slg", "width": 150 }, { "title": "OPS", "field": "ops", "width": 150 }, { "title": "K%", "field": "k_percent", "width": 150,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "BB%", "field": "bb_percent", "width": 150,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "K-BB%", "field": "k_minus_bb_percent", "width": 150,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "SwSpot%", "field": "sweet_spot_percent", "width": 150,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "wOBA", "field": "woba_percent", "width": 150 }, { "title": "xwOBA", "field": "xwoba_percent", "width": 150 }, { "title": "wOBACON", "field": "woba_percent_contact", "width": 150 }, { "title": "xwOBACON", "field": "xwoba_percent_contact", "width": 150 }, { "title": "HardHit%", "field": "hard_hit_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Barrel%", "field": "barrel_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Z-Contact%", "field": "zone_contact_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Z-Swing%", "field": "zone_swing_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Zone%", "field": "zone_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "O-Swing%", "field": "chase_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "O-Contact%", "field": "chase_contact", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Swing%", "field": "swing_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Whiff%", "field": "whiff_rate", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "SwStr%", "field": "swstr_rate", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "GB%", "field": "ground_ball_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "LD%", "field": "line_drive_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "FB%", "field": "fly_ball_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "PU%", "field": "pop_up_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Pull LD+FB%", "field": "pulled_fly_ball_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Heart Swing%", "field": "heart_zone_swing_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Shadow Swing%", "field": "shadow_zone_swing_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Chase Swing%", "field": "chase_zone_swing_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Waste Swing%", "field": "waste_zone_swing_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Heart Whiff%", "field": "heart_zone_whiff_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Shadow Whiff%", "field": "shadow_zone_whiff_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Chase Whiff%", "field": "chase_zone_whiff_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "Waste Whiff%", "field": "waste_zone_whiff_percent", "width": 150 ,"formatter": "money", "formatterParams":{"decimal":".","thousand":".","symbol":"%","symbolAfter":"%","negativeSign":True,"precision":1}}, { "title": "tjStuff+", "field": "tj_stuff_plus_avg", "width": 150 }, { "title": "Velocity", "field": "start_speed_avg", "width": 150 }, { "title": "Extension", "field": "extension_avg", "width": 150 }, { "title": "VB", "field": "vb_avg", "width": 150 }, { "title": "iVB", "field": "ivb_avg", "width": 150 }, { "title": "HB", "field": "hb_avg", "width": 150 }, { "title": "vRel", "field": "z0_avg", "width": 150 }, { "title": "hRel", "field": "x0_avg", "width": 150 }, { "title": "VAA", "field": "vaa_avg", "width": 150 }, { "title": "HAA", "field": "haa_avg", "width": 150 }, { "title": "Spin Rate", "field": "spin_rate_avg", "width": 150 }, { "title": "Extension", "field": "extension_avg", "width": 150 }, ] stat_titles = dict(zip([col["field"] for col in columns],[col["title"] for col in columns])) stat_selection = [key for key in stat_titles.keys()] agg_titles = {'batter_id':'Batter ID', 'batter_name':'Batter Name', 'batter_team':'Batter Team', 'batter_hand':'Batter Hand', 'pitcher_id':'Pitcher ID', 'pitcher_name':'Pitcher Name', 'pitcher_team':'Pitcher Team', 'pitcher_hand':'Pitcher Hand', 'pitch_type':'Pitch Type', 'pitch_group':'Pitch Group', 'home_away_batter':'Home/Away Batter', 'home_away_pitcher':'Home/Away Pitcher', 'is_swing':'Is Swing?', 'is_bip':'Is BIP?', 'in_zone_final':'In Zone?', 'attack_zone_final':'Attack Zone'} columns_group = [ { "title": "Batter ID", "field": "batter_id", "width": 150, "headerFilter":"input","frozen":True,}, { "title": "Batter Name", "field": "batter_name", "width": 200,"frozen":True, "headerFilter":"input" }, { "title": "Batter Team", "field": "batter_team", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Batter Hand", "field": "batter_hand", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Pitcher ID", "field": "pitcher_id", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Pitcher Name", "field": "pitcher_name", "width": 200,"frozen":True, "headerFilter":"input" }, { "title": "Pitcher Team", "field": "pitcher_team", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Pitcher Hand", "field": "pitcher_hand", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Pitch Type", "field": "pitch_type", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Pitch Group", "field": "pitch_group", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Home/Away Batter", "field": "home_away_batter", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Home/Away Pitcher", "field": "home_away_pitcher", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Is Swing?", "field": "is_swing", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Is BIP?", "field": "is_bip", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "In Zone?", "field": "in_zone_final", "width": 150,"frozen":True, "headerFilter":"input" }, { "title": "Attack Zone", "field": "attack_zone_final", "width": 150,"frozen":True, "headerFilter":"input" } ] app_ui = ui.page_sidebar( ui.sidebar( ui.input_selectize( "level_input", "Select Level:", choices=['MLB','AAA','A'], multiple=False, selected=['MLB'] ), ui.input_selectize( "list_input", "Select Aggregation:", choices=agg_titles, multiple=True, selected=['batter_id', 'batter_name'] ), ui.input_selectize( "list_stats", "Select Stats:", choices=stat_titles, multiple=True, selected=['pa'] ), ui.input_date_range( "date_id", "Select Date Range", start=f'{season}-01-01', end=f'{season}-12-01', min=f'{season}-01-01', max=f'{season}-12-01', ), ui.hr(), ui.h4("Filters"), ui.div( {"id": "filter-container"}, ui.div( {"class": "filter-row", "id": "filter_row_1"}, # Add id for deletion ui.row( ui.column(5, # Adjusted column widths to make room for delete button ui.input_select( "filter_column_1", "Metric", choices={} ) ), ui.column(3, ui.input_select( "filter_operator_1", "Operator", choices=[">=", "<="] ), ), ui.column(3, ui.input_numeric( "filter_value_1", "Value", value=0 ) ), ui.column(1, ui.markdown(" "), ui.input_action_button( f"delete_filter_1", "", class_="btn-danger btn-sm", style="padding: 3px 6px;", icon='✖' ) ) ) ) ), ui.input_action_button( "add_filter", "Add Filter", class_="btn-secondary" ), ui.br(), ui.br(), ui.input_action_button( "generate_table", "Generate Table", class_="btn-primary" ), width="400px" ), ui.navset_tab( ui.nav_panel("Leaderboard", ui.card( #ui.card_header("Leaderboard"), output_tabulator("tabulator") ) ), ) ) def server(input, output, session): # Store the number of active filters filter_count = reactive.value(1) # Store active filter IDs active_filters = reactive.value([1]) @reactive.effect @reactive.event(input.list_stats) def _(): stat_choices = {k: k for k in input.list_stats()} filtered_stat_choices = {key: stat_titles[key] for key in stat_choices} ui.update_select("filter_column_1", choices=filtered_stat_choices) @reactive.effect @reactive.event(input.add_filter) def _(): current_count = filter_count.get() new_count = current_count + 1 stat_choices = {k: k for k in input.list_stats()} filtered_stat_choices = {key: stat_titles[key] for key in stat_choices} ui.insert_ui( selector="#filter-container", where="beforeEnd", ui=ui.div( {"class": "filter-row", "id": f"filter_row_{new_count}"}, ui.row( ui.column(5, ui.input_select( f"filter_column_{new_count}", "Metric", choices=filtered_stat_choices ), ), ui.column(3, ui.input_select( f"filter_operator_{new_count}", "Operator", choices=[">=", "<="] ), ), ui.column(3, ui.input_numeric( f"filter_value_{new_count}", "Value", value=0 ) ), ui.column(1, ui.markdown(" "), ui.input_action_button( f"delete_filter_{new_count}", "", class_="btn-danger btn-sm", style="padding: 3px 6px;", icon='✖' ) ) ) ) ) filter_count.set(new_count) current_filters = active_filters.get() current_filters.append(new_count) active_filters.set(current_filters) @reactive.effect def _(): # Monitor all possible delete buttons for i in range(1, filter_count.get() + 1): try: if getattr(input, f"delete_filter_{i}")() > 0: # Remove the filter row ui.remove_ui(f"#filter_row_{i}") # Update active filters current_filters = active_filters.get() if i in current_filters: current_filters.remove(i) active_filters.set(current_filters) except: continue @output @render_tabulator @reactive.event(input.generate_table, ignore_none=False) def tabulator(): columns_c = columns.copy() selection_list = list(input.list_input()) start_date = str(input.date_id()[0]) end_date = str(input.date_id()[1]) if input.level_input() == "MLB": df_agg = update.update_summary_select(df=df_mlb_total.filter((pl.col('game_date')>=start_date)&(pl.col('game_date')<=end_date)), selection=selection_list) elif input.level_input() == "AAA": df_agg = update.update_summary_select(df=df_aaa_total.filter((pl.col('game_date')>=start_date)&(pl.col('game_date')<=end_date)), selection=selection_list) elif input.level_input() == "A": df_agg = update.update_summary_select(df=df_a_total.filter((pl.col('game_date')>=start_date)&(pl.col('game_date')<=end_date)), selection=selection_list) df_agg = df_agg.select(selection_list + list(input.list_stats()))#.sort('pa', descending=True) # Apply filters - only for active filters for i in active_filters.get(): try: col_name = getattr(input, f"filter_column_{i}")() if col_name: # Only apply filter if column is selected operator = getattr(input, f"filter_operator_{i}")() if col_name in [col["field"] for col in columns_c if col.get("formatter") == "money"]: value = getattr(input, f"filter_value_{i}")()/100 else: value = getattr(input, f"filter_value_{i}")() if operator == ">=": df_agg = df_agg.filter(pl.col(col_name) >= value) elif operator == "<=": df_agg = df_agg.filter(pl.col(col_name) <= value) except: continue for col in df_agg.columns[len(selection_list):]: if col in rounding_dict: df_agg = df_agg.with_columns(pl.col(col).round(rounding_dict[col])) for column in columns_c: if column.get("formatter") == "money" and column.get("field") in df_agg.columns: df_agg = df_agg.with_columns(pl.col(column.get("field"))*100) col_group = [] for column in columns_group: if column.get("field") in df_agg.columns: col_group.append(column) col_group_stats = [] for column in columns_c: if column.get("field") in df_agg.columns: col_group_stats.append(column) columns_c = col_group + col_group_stats # Replace all boolean columns with 0 and 1 df_agg = df_agg.with_columns( [df_agg[col].cast(pl.Int8) for col in df_agg.columns if df_agg[col].dtype == pl.Boolean] ) return Tabulator( df_agg.to_pandas(), table_options=TableOptions( height=800, columns=columns_c, ) ) app = App(app_ui, server)