Spaces:
Running
Running
Commit
·
bb5e4ba
1
Parent(s):
2967b2a
Add features to pitch leaderboard
Browse files- app.py +1 -0
- pitch_leaderboard.py +50 -28
- pitcher_overview.py +13 -16
app.py
CHANGED
|
@@ -11,4 +11,5 @@ if __name__ == '__main__':
|
|
| 11 |
with gr.Tab('Pitch Leaderboard'):
|
| 12 |
create_pitch_leaderboard()
|
| 13 |
|
|
|
|
| 14 |
app.launch()
|
|
|
|
| 11 |
with gr.Tab('Pitch Leaderboard'):
|
| 12 |
create_pitch_leaderboard()
|
| 13 |
|
| 14 |
+
gr.Markdown('Last updated: 2025-07-19')
|
| 15 |
app.launch()
|
pitch_leaderboard.py
CHANGED
|
@@ -12,30 +12,51 @@ STATS = ['Count', 'Usage', 'SwStr%', 'Whiff%', 'CSW%', 'GB%', 'FB%', 'LD%']
|
|
| 12 |
PCT_STATS = ['Usage', 'SwStr%', 'Whiff%', 'CSW%', 'GB%', 'FB%', 'LD%']
|
| 13 |
STATS_WITH_PCTLS = ['SwStr%', 'Whiff%', 'CSW%', 'GB%', 'FB%', 'LD%']
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
data = data_df.filter(pl.col('ballKind_code') != '-')
|
| 17 |
|
| 18 |
data = filter_data_by_date_and_game_kind(data, start_date=start_date, end_date=end_date, game_kind='Regular Season')
|
| 19 |
-
|
| 20 |
-
(
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
pitch_stats = (
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
)
|
| 40 |
return pitch_stats
|
| 41 |
|
|
@@ -50,14 +71,16 @@ def create_pitch_leaderboard():
|
|
| 50 |
with gr.Row():
|
| 51 |
start_date = gr.DateTime(start_datetime_init, include_time=False, type='datetime', label='Start')
|
| 52 |
end_date = gr.DateTime(end_datetime_init, include_time=False, type='datetime', label='End')
|
| 53 |
-
min_pitches = gr.Number(100, label='Min. Pitches', precision=0, minimum=0)
|
| 54 |
with gr.Row():
|
| 55 |
-
include_pitches = gr.CheckboxGroup(pitch_types, value=pitch_types, label='Pitches', scale=
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
|
| 59 |
search = gr.Button('Search')
|
| 60 |
leaderboard = gr.DataFrame(
|
|
|
|
| 61 |
# gr_create_pitch_leaderboard(start_date=start_date_init, end_date=end_date_init, min_pitches=100),
|
| 62 |
column_widths=[200]*3 + [100]*(3*len(STATS)),
|
| 63 |
show_copy_button=True,
|
|
@@ -66,10 +89,9 @@ def create_pitch_leaderboard():
|
|
| 66 |
)
|
| 67 |
|
| 68 |
|
| 69 |
-
search.click(gr_create_pitch_leaderboard, inputs=[start_date, end_date, min_pitches, include_pitches], outputs=leaderboard)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
return app
|
| 74 |
|
| 75 |
if __name__ == '__main__':
|
|
|
|
| 12 |
PCT_STATS = ['Usage', 'SwStr%', 'Whiff%', 'CSW%', 'GB%', 'FB%', 'LD%']
|
| 13 |
STATS_WITH_PCTLS = ['SwStr%', 'Whiff%', 'CSW%', 'GB%', 'FB%', 'LD%']
|
| 14 |
|
| 15 |
+
todo = '''
|
| 16 |
+
**To-do**
|
| 17 |
+
- Color cells according to percentiles
|
| 18 |
+
'''
|
| 19 |
+
|
| 20 |
+
def gr_create_pitch_leaderboard(start_date, end_date, min_pitches, pitcher_lr, include_pitches):
|
| 21 |
+
assert pitcher_lr in ['Both', 'Left', 'Right']
|
| 22 |
+
|
| 23 |
data = data_df.filter(pl.col('ballKind_code') != '-')
|
| 24 |
|
| 25 |
data = filter_data_by_date_and_game_kind(data, start_date=start_date, end_date=end_date, game_kind='Regular Season')
|
| 26 |
+
if pitcher_lr != 'Both':
|
| 27 |
+
data = data.filter(pl.col('batLR') == pitcher_lr[0].lower())
|
| 28 |
+
|
| 29 |
+
# both, left, right = [
|
| 30 |
+
# (
|
| 31 |
+
# compute_pitch_stats(df, player_type='pitcher', min_pitches=min_pitches, pitch_class_type='specific')
|
| 32 |
+
# .filter(pl.col('qualified') & (pl.col('ballKind').is_in(include_pitches)))
|
| 33 |
+
# .drop('qualified')
|
| 34 |
+
# .rename({'pitcher_name': 'Pitcher', 'count': 'Count', 'usage': 'Usage', 'ballKind': 'Pitch', 'general_ballKind': 'Pitch (General)'} | {f'{stat}_pctl': f'{stat} (Pctl)' for stat in STATS_WITH_PCTLS})
|
| 35 |
+
# .with_columns(
|
| 36 |
+
# pl.col(stat).mul(100).round(1)
|
| 37 |
+
# for stat in PCT_STATS + [f'{stat} (Pctl)' for stat in STATS_WITH_PCTLS]
|
| 38 |
+
# )
|
| 39 |
+
# [['pitId', 'ballKind_code', 'Pitcher', 'Pitch', 'Pitch (General)', 'Count', 'Usage'] + STATS_WITH_PCTLS]
|
| 40 |
+
# )
|
| 41 |
+
# for df
|
| 42 |
+
# in [data, data.filter(pl.col('batLR') == 'l'), data.filter(pl.col('batLR') == 'r')]
|
| 43 |
+
# ]
|
| 44 |
+
# pitch_stats = (
|
| 45 |
+
# both
|
| 46 |
+
# .join(left, on=['pitId', 'ballKind_code'], suffix=' (LHH)', how='full')
|
| 47 |
+
# .join(right, on=['pitId', 'ballKind_code'], suffix=' (RHH)', how='full')
|
| 48 |
+
# .drop('pitId', 'ballKind_code', *list(chain.from_iterable([[f'{col} ({handedness}HH)' for col in ['pitId', 'ballKind_code', 'Pitcher', 'Pitch', 'Pitch (General)']] for handedness in ('L', 'R')])))
|
| 49 |
+
# )
|
| 50 |
pitch_stats = (
|
| 51 |
+
compute_pitch_stats(data, player_type='pitcher', min_pitches=min_pitches, pitch_class_type='specific')
|
| 52 |
+
.filter(pl.col('qualified') & (pl.col('ballKind').is_in(include_pitches)))
|
| 53 |
+
.drop('pitId', 'ballKind_code', 'qualified')
|
| 54 |
+
.rename({'pitcher_name': 'Pitcher', 'count': 'Count', 'usage': 'Usage', 'ballKind': 'Pitch', 'general_ballKind': 'Pitch (General)'} | {f'{stat}_pctl': f'{stat} (Pctl)' for stat in STATS_WITH_PCTLS})
|
| 55 |
+
.with_columns(
|
| 56 |
+
pl.col(stat).mul(100).round(1)
|
| 57 |
+
for stat in PCT_STATS + [f'{stat} (Pctl)' for stat in STATS_WITH_PCTLS]
|
| 58 |
+
)
|
| 59 |
+
[['Pitcher', 'Pitch', 'Pitch (General)', 'Count', 'Usage'] + STATS_WITH_PCTLS]
|
| 60 |
)
|
| 61 |
return pitch_stats
|
| 62 |
|
|
|
|
| 71 |
with gr.Row():
|
| 72 |
start_date = gr.DateTime(start_datetime_init, include_time=False, type='datetime', label='Start')
|
| 73 |
end_date = gr.DateTime(end_datetime_init, include_time=False, type='datetime', label='End')
|
|
|
|
| 74 |
with gr.Row():
|
| 75 |
+
include_pitches = gr.CheckboxGroup(pitch_types, value=pitch_types, label='Pitches', scale=3)
|
| 76 |
+
with gr.Column(scale=1):
|
| 77 |
+
all_pitches = gr.Button('Select/Deselect all pitches', scale=1)
|
| 78 |
+
min_pitches = gr.Number(100, label='Min. Pitches', precision=0, minimum=0)
|
| 79 |
+
pitcher_lr = gr.Radio(['Both', 'Left', 'Right'], value='Both', label='Batter handedness')
|
| 80 |
|
| 81 |
search = gr.Button('Search')
|
| 82 |
leaderboard = gr.DataFrame(
|
| 83 |
+
pl.DataFrame({'Pitcher': [], 'Pitch': []}),
|
| 84 |
# gr_create_pitch_leaderboard(start_date=start_date_init, end_date=end_date_init, min_pitches=100),
|
| 85 |
column_widths=[200]*3 + [100]*(3*len(STATS)),
|
| 86 |
show_copy_button=True,
|
|
|
|
| 89 |
)
|
| 90 |
|
| 91 |
|
| 92 |
+
search.click(gr_create_pitch_leaderboard, inputs=[start_date, end_date, min_pitches, pitcher_lr, include_pitches], outputs=leaderboard)
|
| 93 |
+
all_pitches.click(lambda _pitch_types : [] if _pitch_types == pitch_types else pitch_types, inputs=include_pitches, outputs=include_pitches)
|
| 94 |
+
gr.Markdown(todo)
|
|
|
|
| 95 |
return app
|
| 96 |
|
| 97 |
if __name__ == '__main__':
|
pitcher_overview.py
CHANGED
|
@@ -6,6 +6,17 @@ from data import SEASONS, data_df
|
|
| 6 |
|
| 7 |
from plotting import create_pitcher_overview_card
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
def dummy(*inputs):
|
| 10 |
return inputs
|
| 11 |
|
|
@@ -31,7 +42,7 @@ def gr_create_pitcher_overview_card(name, season):
|
|
| 31 |
|
| 32 |
def create_pitcher_overview(data_df):
|
| 33 |
with gr.Blocks() as app:
|
| 34 |
-
gr.Markdown('# Pitcher
|
| 35 |
|
| 36 |
with gr.Row():
|
| 37 |
with gr.Column():
|
|
@@ -43,21 +54,7 @@ def create_pitcher_overview(data_df):
|
|
| 43 |
# season_end = gr.Dropdown(SEASONS, label='Season end')
|
| 44 |
# game_type = gr.Dropdown(['Spring Training', 'Regular Season', 'Postseason'], label='Game Type'])
|
| 45 |
view = gr.Button('View')
|
| 46 |
-
gr.Markdown(
|
| 47 |
-
'''
|
| 48 |
-
**Limitations**
|
| 49 |
-
- Foreign players names are in Hebpurn romanization. Contact me if you need a card for a foreign player.
|
| 50 |
-
|
| 51 |
-
**To-do**
|
| 52 |
-
- Fix names of foreign playeres
|
| 53 |
-
- Add teams insignias
|
| 54 |
-
- Measure percentiles per pitcher handedness
|
| 55 |
-
- Allow for arbitrary date ranges
|
| 56 |
-
- Improve readability of pitch velocities
|
| 57 |
-
|
| 58 |
-
Last updated: 2025-07-19
|
| 59 |
-
'''
|
| 60 |
-
)
|
| 61 |
|
| 62 |
with gr.Column():
|
| 63 |
overview_card = gr.Image(label='Overview')
|
|
|
|
| 6 |
|
| 7 |
from plotting import create_pitcher_overview_card
|
| 8 |
|
| 9 |
+
notes = '''**Limitations**
|
| 10 |
+
- Foreign players names are in Hebpurn romanization. Contact me if you need a card for a foreign player.
|
| 11 |
+
|
| 12 |
+
**To-do**
|
| 13 |
+
- Fix names of foreign playeres
|
| 14 |
+
- Add teams insignias
|
| 15 |
+
- Measure percentiles per pitcher handedness
|
| 16 |
+
- Allow for arbitrary date ranges
|
| 17 |
+
- Improve readability of pitch velocities
|
| 18 |
+
'''
|
| 19 |
+
|
| 20 |
def dummy(*inputs):
|
| 21 |
return inputs
|
| 22 |
|
|
|
|
| 42 |
|
| 43 |
def create_pitcher_overview(data_df):
|
| 44 |
with gr.Blocks() as app:
|
| 45 |
+
gr.Markdown('# Pitcher Overview')
|
| 46 |
|
| 47 |
with gr.Row():
|
| 48 |
with gr.Column():
|
|
|
|
| 54 |
# season_end = gr.Dropdown(SEASONS, label='Season end')
|
| 55 |
# game_type = gr.Dropdown(['Spring Training', 'Regular Season', 'Postseason'], label='Game Type'])
|
| 56 |
view = gr.Button('View')
|
| 57 |
+
gr.Markdown(notes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
with gr.Column():
|
| 60 |
overview_card = gr.Image(label='Overview')
|