nesticot's picture
Update app.py
785d83c verified
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("&nbsp;"),
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("&nbsp;"),
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)