James McCool commited on
Commit
f4fa784
Β·
1 Parent(s): 14be3da

Enhance baseline initialization by adding 'Opp', 'Team_Total', and 'Opp_Total' columns for both hitters and pitchers. Update player display logic to include new metrics and improve data handling for lineup building. Introduce a new 'Handbuilder' tab in the UI for streamlined player selection and lineup management, incorporating position limits and quick fill options.

Browse files
Files changed (1) hide show
  1. app.py +418 -8
app.py CHANGED
@@ -3,6 +3,7 @@ import numpy as np
3
  import pandas as pd
4
  import gspread
5
  import pymongo
 
6
 
7
  st.set_page_config(layout="wide")
8
 
@@ -27,7 +28,7 @@ fd_columns = ['P', 'C_1B', '2B', '3B', 'SS', 'OF1', 'OF2', 'OF3', 'UTIL', 'salar
27
  dk_sd_columns = ['CPT', 'FLEX1', 'FLEX2', 'FLEX3', 'FLEX4', 'FLEX5', 'salary', 'proj', 'Team', 'Team_count', 'Secondary', 'Secondary_count', 'Own']
28
  fd_sd_columns = ['CPT', 'FLEX1', 'FLEX2', 'FLEX3', 'FLEX4', 'salary', 'proj', 'Team', 'Team_count', 'Secondary', 'Secondary_count', 'Own']
29
 
30
- @st.cache_resource(ttl = 60)
31
  def init_baselines():
32
 
33
  collection = db["Hitter_Info"]
@@ -53,9 +54,27 @@ def init_baselines():
53
 
54
  hold_frame['Order'] = np.where(hold_frame['pos_group'] == 'Hitters', hold_frame['Player'].map(RHP_Info.set_index('Player')['Order']), 0)
55
  hold_frame['Hand'] = np.where(hold_frame['pos_group'] == 'Hitters', hold_frame['Player'].map(RHP_Info.set_index('Player')['bats']), hold_frame['Player'].map(RHH_Info.set_index('Player')['Hand']))
56
-
57
- roo_data.insert(3, 'Hand', hold_frame['Hand'])
58
- roo_data.insert(4, 'Order', hold_frame['Order'].astype(int))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  dk_roo = roo_data[roo_data['Site'] == 'Draftkings']
61
  dk_id_map = dict(zip(dk_roo['Player'], dk_roo['player_ID']))
@@ -68,7 +87,31 @@ def init_baselines():
68
 
69
  sd_roo_data = player_frame.drop(columns=['_id'])
70
  sd_roo_data['Salary'] = sd_roo_data['Salary'].astype(int)
71
- sd_roo_data = sd_roo_data.rename(columns={'Own': 'Own%'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  collection = db["Scoring_Percentages"]
74
  cursor = collection.find()
@@ -258,7 +301,7 @@ with col2:
258
  site_var = st.selectbox("What site do you want to view?", ('Draftkings', 'Fanduel'), key='site_var')
259
 
260
 
261
- tab1, tab2, tab3 = st.tabs(["Scoring Percentages", "Player ROO", "Optimals"])
262
 
263
  roo_data, sd_roo_data, scoring_percentages, dk_roo, fd_roo, dk_id_map, fd_id_map = init_baselines()
264
  hold_display = roo_data
@@ -448,7 +491,7 @@ with tab2:
448
  player_roo_disp = player_roo_disp.set_index('Player', drop=True)
449
  st.dataframe(player_roo_disp.style.background_gradient(axis=0).background_gradient(cmap='RdYlGn').background_gradient(cmap='RdYlGn_r', subset=['Order', 'Salary', 'Own%', 'Small Field Own%', 'Large Field Own%', 'Cash Own%']).format(player_roo_format, precision=2), height=750, use_container_width = True)
450
  except:
451
- player_roo_disp = player_roo_disp.set_index('Player', drop=True)
452
  st.dataframe(player_roo_disp.style.background_gradient(axis=0).background_gradient(cmap='RdYlGn').background_gradient(cmap='RdYlGn_r', subset=['Order', 'Salary', 'Own%', 'Small Field Own%', 'Large Field Own%', 'Cash Own%']).format(player_roo_format, precision=2), height=750, use_container_width = True)
453
 
454
  with tab3:
@@ -763,4 +806,371 @@ with tab3:
763
  data=convert_df_to_csv(summary_df),
764
  file_name='MLB_seed_frame_frequency.csv',
765
  mime='text/csv',
766
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import pandas as pd
4
  import gspread
5
  import pymongo
6
+ import re
7
 
8
  st.set_page_config(layout="wide")
9
 
 
28
  dk_sd_columns = ['CPT', 'FLEX1', 'FLEX2', 'FLEX3', 'FLEX4', 'FLEX5', 'salary', 'proj', 'Team', 'Team_count', 'Secondary', 'Secondary_count', 'Own']
29
  fd_sd_columns = ['CPT', 'FLEX1', 'FLEX2', 'FLEX3', 'FLEX4', 'salary', 'proj', 'Team', 'Team_count', 'Secondary', 'Secondary_count', 'Own']
30
 
31
+ @st.cache_resource(ttl = 61)
32
  def init_baselines():
33
 
34
  collection = db["Hitter_Info"]
 
54
 
55
  hold_frame['Order'] = np.where(hold_frame['pos_group'] == 'Hitters', hold_frame['Player'].map(RHP_Info.set_index('Player')['Order']), 0)
56
  hold_frame['Hand'] = np.where(hold_frame['pos_group'] == 'Hitters', hold_frame['Player'].map(RHP_Info.set_index('Player')['bats']), hold_frame['Player'].map(RHH_Info.set_index('Player')['Hand']))
57
+ try:
58
+ hold_frame['Opp'] = hold_frame['Team'].map(RHH_Info.drop_duplicates(subset='Team').set_index('Team')['Opp'])
59
+ except:
60
+ hold_frame['Opp'] = np.nan
61
+ try:
62
+ hold_frame['Team_Total'] = hold_frame['Team'].map(RHH_Info.drop_duplicates(subset='Team').set_index('Opp')['Opp_TT'])
63
+ except:
64
+ hold_frame['Team_Total'] = np.nan
65
+ try:
66
+ hold_frame['Opp_Total'] = hold_frame['Team'].map(RHH_Info.drop_duplicates(subset='Team').set_index('Team')['Opp_TT'])
67
+ except:
68
+ hold_frame['Opp_Total'] = np.nan
69
+
70
+ roo_data.insert(3, 'Opp', hold_frame['Opp'])
71
+ roo_data.insert(4, 'Hand', hold_frame['Hand'])
72
+ try:
73
+ roo_data.insert(5, 'Order', hold_frame['Order'].astype(int))
74
+ except:
75
+ roo_data.insert(5, 'Order', hold_frame['Order'])
76
+ roo_data.insert(6, 'Team_Total', hold_frame['Team_Total'])
77
+ roo_data.insert(7, 'Opp_Total', hold_frame['Opp_Total'])
78
 
79
  dk_roo = roo_data[roo_data['Site'] == 'Draftkings']
80
  dk_id_map = dict(zip(dk_roo['Player'], dk_roo['player_ID']))
 
87
 
88
  sd_roo_data = player_frame.drop(columns=['_id'])
89
  sd_roo_data['Salary'] = sd_roo_data['Salary'].astype(int)
90
+ sd_roo_data = sd_roo_data.rename(columns={'Own': 'Own%', 'Small_Own': 'Small Field Own%', 'Large_Own': 'Large Field Own%', 'Cash_Own': 'Cash Own%'})
91
+ sd_hold_frame = sd_roo_data.copy()
92
+
93
+ sd_hold_frame['Order'] = np.where(sd_hold_frame['Position'] != 'SP', sd_hold_frame['Player'].map(RHP_Info.set_index('Player')['Order']), 0)
94
+ sd_hold_frame['Hand'] = np.where(sd_hold_frame['Position'] != 'SP', sd_hold_frame['Player'].map(RHP_Info.set_index('Player')['bats']), sd_hold_frame['Player'].map(RHH_Info.set_index('Player')['Hand']))
95
+ try:
96
+ sd_hold_frame['Opp'] = sd_hold_frame['Team'].map(RHH_Info.drop_duplicates(subset='Team').set_index('Team')['Opp'])
97
+ except:
98
+ sd_hold_frame['Opp'] = np.nan
99
+ try:
100
+ sd_hold_frame['Team_Total'] = sd_hold_frame['Team'].map(RHH_Info.drop_duplicates(subset='Team').set_index('Opp')['Opp_TT'])
101
+ except:
102
+ sd_hold_frame['Team_Total'] = np.nan
103
+ try:
104
+ sd_hold_frame['Opp_Total'] = sd_hold_frame['Team'].map(RHH_Info.drop_duplicates(subset='Team').set_index('Team')['Opp_TT'])
105
+ except:
106
+ sd_hold_frame['Opp_Total'] = np.nan
107
+ sd_roo_data.insert(3, 'Opp', sd_hold_frame['Opp'])
108
+ sd_roo_data.insert(4, 'Hand', sd_hold_frame['Hand'])
109
+ try:
110
+ sd_roo_data.insert(5, 'Order', sd_hold_frame['Order'].astype(int))
111
+ except:
112
+ sd_roo_data.insert(5, 'Order', sd_hold_frame['Order'])
113
+ sd_roo_data.insert(6, 'Team_Total', sd_hold_frame['Team_Total'])
114
+ sd_roo_data.insert(7, 'Opp_Total', sd_hold_frame['Opp_Total'])
115
 
116
  collection = db["Scoring_Percentages"]
117
  cursor = collection.find()
 
301
  site_var = st.selectbox("What site do you want to view?", ('Draftkings', 'Fanduel'), key='site_var')
302
 
303
 
304
+ tab1, tab2, tab3, tab4 = st.tabs(["Scoring Percentages", "Player ROO", "Optimals", "Handbuilder"])
305
 
306
  roo_data, sd_roo_data, scoring_percentages, dk_roo, fd_roo, dk_id_map, fd_id_map = init_baselines()
307
  hold_display = roo_data
 
491
  player_roo_disp = player_roo_disp.set_index('Player', drop=True)
492
  st.dataframe(player_roo_disp.style.background_gradient(axis=0).background_gradient(cmap='RdYlGn').background_gradient(cmap='RdYlGn_r', subset=['Order', 'Salary', 'Own%', 'Small Field Own%', 'Large Field Own%', 'Cash Own%']).format(player_roo_format, precision=2), height=750, use_container_width = True)
493
  except:
494
+ # player_roo_disp = player_roo_disp.set_index('Player', drop=True)
495
  st.dataframe(player_roo_disp.style.background_gradient(axis=0).background_gradient(cmap='RdYlGn').background_gradient(cmap='RdYlGn_r', subset=['Order', 'Salary', 'Own%', 'Small Field Own%', 'Large Field Own%', 'Cash Own%']).format(player_roo_format, precision=2), height=750, use_container_width = True)
496
 
497
  with tab3:
 
806
  data=convert_df_to_csv(summary_df),
807
  file_name='MLB_seed_frame_frequency.csv',
808
  mime='text/csv',
809
+ )
810
+
811
+ with tab4:
812
+ col1, col2 = st.columns(2)
813
+ with col1:
814
+ st.header("Handbuilder")
815
+ with col2:
816
+ slate_var3 = st.selectbox("Slate Selection", options=['Main', 'Secondary', 'Auxiliary'])
817
+ if site_var == 'Draftkings':
818
+ if slate_var3 == 'Main':
819
+ handbuild_roo = dk_roo[dk_roo['Slate'] == 'main_slate']
820
+ elif slate_var3 == 'Secondary':
821
+ handbuild_roo = dk_roo[dk_roo['Slate'] == 'secondary_slate']
822
+ elif slate_var3 == 'Auxiliary':
823
+ handbuild_roo = dk_roo[dk_roo['Slate'] == 'turbo_slate']
824
+ else:
825
+ if slate_var3 == 'Main':
826
+ handbuild_roo = fd_roo[fd_roo['Slate'] == 'main_slate']
827
+ elif slate_var3 == 'Secondary':
828
+ handbuild_roo = fd_roo[fd_roo['Slate'] == 'secondary_slate']
829
+ elif slate_var3 == 'Auxiliary':
830
+ handbuild_roo = fd_roo[fd_roo['Slate'] == 'turbo_slate']
831
+
832
+ # --- POSITION LIMITS ---
833
+ if site_var == 'Draftkings':
834
+ position_limits = {
835
+ 'SP': 2,
836
+ 'C': 1,
837
+ '1B': 1,
838
+ '2B': 1,
839
+ '3B': 1,
840
+ 'SS': 1,
841
+ 'OF': 3,
842
+ # Add more as needed
843
+ }
844
+ max_salary = 50000
845
+ max_players = 10
846
+ else:
847
+ position_limits = {
848
+ 'P': 1,
849
+ 'C_1B': 1,
850
+ '2B': 1,
851
+ '3B': 1,
852
+ 'SS': 1,
853
+ 'OF': 3,
854
+ 'UTIL': 1,
855
+ # Add more as needed
856
+ }
857
+ max_salary = 35000
858
+ max_players = 9
859
+
860
+ # --- LINEUP STATE ---
861
+ if 'handbuilder_lineup' not in st.session_state:
862
+ st.session_state['handbuilder_lineup'] = pd.DataFrame(columns=['Player', 'Order', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Own%'])
863
+ if 'handbuilder_select_key' not in st.session_state:
864
+ st.session_state['handbuilder_select_key'] = 0
865
+
866
+ # Count positions in the current lineup
867
+ lineup = st.session_state['handbuilder_lineup']
868
+ slot_counts = lineup['Slot'].value_counts() if not lineup.empty else {}
869
+
870
+ # --- PLAYER FILTERS ---
871
+ with st.expander("Player Filters"):
872
+ col1, col2 = st.columns(2)
873
+ with col1:
874
+ pos_select3 = st.multiselect("Select your position(s)", options=['P', 'C', '1B', '2B', '3B', 'SS', 'OF'], key='pos_select3')
875
+ with col2:
876
+ salary_var = st.number_input("Salary Max", min_value = 0, max_value = 20000, value = 20000, step = 100)
877
+
878
+ # --- TEAM FILTER UI ---
879
+ with st.expander("Team Filters"):
880
+ all_teams = sorted(handbuild_roo['Team'].unique())
881
+ st.markdown("**Toggle teams to include:**")
882
+ team_cols = st.columns(len(all_teams) // 2 + 1)
883
+
884
+ selected_teams = []
885
+ for idx, team in enumerate(all_teams):
886
+ col = team_cols[idx % len(team_cols)]
887
+ if f"handbuilder_team_{team}" not in st.session_state:
888
+ st.session_state[f"handbuilder_team_{team}"] = False
889
+ checked = col.toggle(team, value=st.session_state[f"handbuilder_team_{team}"], key=f"handbuilder_team_{team}")
890
+ if checked:
891
+ selected_teams.append(team)
892
+
893
+ # If no teams selected, show all teams
894
+ if selected_teams:
895
+ player_select_df = handbuild_roo[
896
+ handbuild_roo['Team'].isin(selected_teams)
897
+ ][['Player', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Order', 'Hand', 'Own%']].drop_duplicates(subset=['Player', 'Team']).sort_values(by='Order', ascending=True).copy()
898
+ else:
899
+ player_select_df = handbuild_roo[['Player', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Order', 'Hand', 'Own%']].drop_duplicates(subset=['Player', 'Team']).copy()
900
+
901
+ # If no teams selected, show all teams
902
+ if pos_select3:
903
+ position_mask_2 = handbuild_roo['Position'].apply(lambda x: any(pos in x for pos in pos_select3))
904
+ player_select_df = player_select_df[position_mask_2][['Player', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Order', 'Hand', 'Own%']].drop_duplicates(subset=['Player', 'Team']).sort_values(by='Order', ascending=True).copy()
905
+ else:
906
+ player_select_df = player_select_df[['Player', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Order', 'Hand', 'Own%']].drop_duplicates(subset=['Player', 'Team']).copy()
907
+
908
+ player_select_df = player_select_df[player_select_df['Salary'] <= salary_var]
909
+
910
+
911
+ with st.expander("Quick Fill Options"):
912
+ auto_team_var = st.selectbox("Auto Fill Team", options=all_teams)
913
+ auto_size_var = st.selectbox("Auto Fill Size", options=[3, 4, 5])
914
+ auto_range_var = st.selectbox("Auto Fill Order", options=['Top (1-5)', 'Mid (4-8)', 'Wrap (7-2)'])
915
+ # --- QUICK FILL LOGIC ---
916
+ if st.button("Quick Fill", key="quick_fill"):
917
+ # 1. Get all eligible players from the selected team, not already in the lineup
918
+ current_players = set(st.session_state['handbuilder_lineup']['Player'])
919
+ team_players = player_select_df[
920
+ (player_select_df['Team'] == auto_team_var) &
921
+ (~player_select_df['Player'].isin(current_players))
922
+ ].copy()
923
+
924
+ # 2. Sort by Order
925
+ team_players = team_players.sort_values(by='Order')
926
+
927
+ # 3. Select the order range
928
+ if auto_range_var == 'Top (1-5)':
929
+ selected_players = team_players[team_players['Order'] > 0].head(auto_size_var)
930
+ elif auto_range_var == 'Mid (4-8)':
931
+ selected_players = team_players[team_players['Order'] > 0].iloc[3:3 + auto_size_var - 1]
932
+ elif auto_range_var == 'Wrap (7-2)':
933
+ first_three = team_players[team_players['Order'] > 0].head(2)
934
+ last_two = team_players[team_players['Order'] > 0].tail(3)
935
+ selected_players = pd.concat([first_three, last_two])
936
+ else:
937
+ selected_players = team_players[team_players['Order'] > 0].head(auto_size_var)
938
+
939
+ # 4. Add each player to the lineup, filling the first available eligible slot
940
+ for _, player_row in selected_players.iterrows():
941
+ eligible_positions = re.split(r'[/, ]+', player_row['Position'])
942
+ slot_to_fill = None
943
+
944
+ if site_var == 'Fanduel':
945
+ # Logic for handling Fanduel Positions (with C/1B and UTIL)
946
+ for slot in ['P', 'C_1B', '2B', '3B', 'SS', 'OF', 'UTIL']:
947
+ if slot_counts.get(slot, 0) < position_limits.get(slot, 0):
948
+ if slot == 'C_1B':
949
+ if any(pos in eligible_positions for pos in ['C', '1B']):
950
+ slot_to_fill = slot
951
+ break
952
+ elif slot == 'UTIL':
953
+ if 'P' not in eligible_positions:
954
+ slot_to_fill = slot
955
+ break
956
+ elif slot in eligible_positions:
957
+ slot_to_fill = slot
958
+ break
959
+ else:
960
+ # General logic for handling Draftkings Positions
961
+ for pos in eligible_positions:
962
+ if slot_counts.get(pos, 0) < position_limits.get(pos, 0):
963
+ slot_to_fill = pos
964
+ break
965
+
966
+ if slot_to_fill is not None:
967
+ # Avoid duplicates
968
+ if player_row['Player'] not in st.session_state['handbuilder_lineup']['Player'].values:
969
+ add_row = player_row.copy()
970
+ add_row['Slot'] = slot_to_fill
971
+ st.session_state['handbuilder_lineup'] = pd.concat(
972
+ [st.session_state['handbuilder_lineup'], pd.DataFrame([add_row[[
973
+ 'Player', 'Order', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Own%', 'Slot'
974
+ ]]])],
975
+ ignore_index=True
976
+ )
977
+ # Update slot_counts for next player
978
+ slot_counts[slot_to_fill] = slot_counts.get(slot_to_fill, 0) + 1
979
+ st.rerun()
980
+
981
+ # --- FILTER OUT PLAYERS WHOSE ALL ELIGIBLE POSITIONS ARE FILLED ---
982
+ def is_player_eligible(row):
983
+ eligible_positions = re.split(r'[/, ]+', row['Position'])
984
+ # Player is eligible if at least one of their positions is not at max
985
+ for pos in eligible_positions:
986
+ if slot_counts.get(pos, 0) < position_limits.get(pos, 0):
987
+ return True
988
+ return False
989
+
990
+ # player_select_df = player_select_df[player_select_df.apply(is_player_eligible, axis=1)]
991
+
992
+ col1, col2 = st.columns([1, 2])
993
+ with col2:
994
+ st.subheader("Player Select")
995
+ event = st.dataframe(
996
+ player_select_df.style.background_gradient(axis=0).background_gradient(cmap='RdYlGn').background_gradient(cmap='RdYlGn_r', subset=['Order', 'Salary', 'Own%']).format(precision=2),
997
+ on_select="rerun",
998
+ selection_mode=["single-row"],
999
+ key=f"handbuilder_select_{st.session_state['handbuilder_select_key']}",
1000
+ height=500,
1001
+ hide_index=True
1002
+ )
1003
+ # If a row is selected, add that player to the lineup and reset selection
1004
+ if event and "rows" in event.selection and len(event.selection["rows"]) > 0:
1005
+ idx = event.selection["rows"][0]
1006
+ player_row = player_select_df.iloc[[idx]]
1007
+ eligible_positions = re.split(r'[/, ]+', player_row['Position'].iloc[0])
1008
+ # Find the first eligible slot that is not full
1009
+ slot_to_fill = None
1010
+
1011
+ if site_var == 'Fanduel':
1012
+ # Logic for handling Fanduel Positions (with C/1B and UTIL)
1013
+ for slot in ['P', 'C_1B', '2B', '3B', 'SS', 'OF', 'UTIL']:
1014
+ if slot_counts.get(slot, 0) < position_limits.get(slot, 0):
1015
+ if slot == 'C_1B':
1016
+ if any(pos in eligible_positions for pos in ['C', '1B']):
1017
+ slot_to_fill = slot
1018
+ break
1019
+ elif slot == 'UTIL':
1020
+ if 'P' not in eligible_positions:
1021
+ slot_to_fill = slot
1022
+ break
1023
+ elif slot in eligible_positions:
1024
+ slot_to_fill = slot
1025
+ break
1026
+ else:
1027
+ # General logic for handling Draftkings Positions
1028
+ for pos in eligible_positions:
1029
+ if slot_counts.get(pos, 0) < position_limits.get(pos, 0):
1030
+ slot_to_fill = pos
1031
+ break
1032
+
1033
+ if slot_to_fill is not None:
1034
+ # Avoid duplicates
1035
+ if not player_row['Player'].iloc[0] in st.session_state['handbuilder_lineup']['Player'].values:
1036
+ # Add the slot info
1037
+ player_row = player_row.assign(Slot=slot_to_fill)
1038
+ st.session_state['handbuilder_lineup'] = pd.concat(
1039
+ [st.session_state['handbuilder_lineup'], player_row[['Player', 'Order', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Own%', 'Slot']]],
1040
+ ignore_index=True
1041
+ )
1042
+ st.session_state['handbuilder_select_key'] += 1
1043
+ st.rerun()
1044
+
1045
+
1046
+ with col1:
1047
+ st.subheader("Lineup Build")
1048
+
1049
+ # --- EXPLICIT LINEUP ORDER ---
1050
+ if site_var == 'Draftkings':
1051
+ lineup_slots = ['SP', 'SP', 'C', '1B', '2B', '3B', 'SS', 'OF', 'OF', 'OF']
1052
+ else:
1053
+ lineup_slots = ['P', 'C_1B', '2B', '3B', 'SS', 'OF', 'OF', 'OF', 'UTIL']
1054
+ display_columns = ['Slot', 'Player', 'Order', 'Team', 'Salary', 'Median', 'Own%']
1055
+
1056
+ filled_lineup = st.session_state['handbuilder_lineup']
1057
+ display_rows = []
1058
+ used_indices = set()
1059
+ if not filled_lineup.empty:
1060
+ for slot in lineup_slots:
1061
+ match = filled_lineup[(filled_lineup['Slot'] == slot) & (~filled_lineup.index.isin(used_indices))]
1062
+ if not match.empty:
1063
+ row = match.iloc[0]
1064
+ used_indices.add(match.index[0])
1065
+ display_rows.append({
1066
+ 'Slot': slot,
1067
+ 'Player': row['Player'],
1068
+ 'Order': row['Order'],
1069
+ 'Position': row['Position'],
1070
+ 'Team': row['Team'],
1071
+ 'Salary': row['Salary'],
1072
+ 'Median': row['Median'],
1073
+ '2x%': row['2x%'],
1074
+ 'Own%': row['Own%']
1075
+ })
1076
+ else:
1077
+ display_rows.append({
1078
+ 'Slot': slot,
1079
+ 'Player': '',
1080
+ 'Order': np.nan,
1081
+ 'Position': '',
1082
+ 'Team': '',
1083
+ 'Salary': np.nan,
1084
+ 'Median': np.nan,
1085
+ '2x%': np.nan,
1086
+ 'Own%': np.nan
1087
+ })
1088
+
1089
+ lineup_display_df = pd.DataFrame(display_rows, columns=display_columns)
1090
+
1091
+ # Show the lineup table with single-row selection for removal
1092
+ event_remove = st.dataframe(
1093
+ lineup_display_df.style.background_gradient(axis=0).background_gradient(cmap='RdYlGn', subset=['Median']).background_gradient(cmap='RdYlGn_r', subset=['Order', 'Salary', 'Own%']).format(precision=2),
1094
+ on_select="rerun",
1095
+ selection_mode=["single-row"],
1096
+ key="lineup_remove",
1097
+ height=445,
1098
+ hide_index=True
1099
+ )
1100
+
1101
+ # If a row is selected and not blank, remove that player from the lineup
1102
+ if event_remove and "rows" in event_remove.selection and len(event_remove.selection["rows"]) > 0:
1103
+ idx = event_remove.selection["rows"][0]
1104
+ player_to_remove = lineup_display_df.iloc[idx]['Player']
1105
+ slot_to_remove = lineup_display_df.iloc[idx]['Slot']
1106
+ if player_to_remove: # Only remove if not blank
1107
+ st.session_state['handbuilder_lineup'] = filled_lineup[
1108
+ ~((filled_lineup['Player'] == player_to_remove) & (filled_lineup['Slot'] == slot_to_remove))
1109
+ ]
1110
+ st.rerun()
1111
+
1112
+ # --- SUMMARY ROW ---
1113
+ if not filled_lineup.empty:
1114
+ total_salary = filled_lineup['Salary'].sum()
1115
+ total_median = filled_lineup['Median'].sum()
1116
+ avg_2x = filled_lineup['2x%'].mean()
1117
+ total_own = filled_lineup['Own%'].sum()
1118
+ most_common_team = filled_lineup['Team'].mode()[0] if not filled_lineup['Team'].mode().empty else ""
1119
+
1120
+ summary_row = pd.DataFrame({
1121
+ 'Slot': [''],
1122
+ 'Player': ['TOTAL'],
1123
+ 'Order': [''],
1124
+ 'Position': [''],
1125
+ 'Team': [most_common_team],
1126
+ 'Salary': [total_salary],
1127
+ 'Median': [total_median],
1128
+ '2x%': [avg_2x],
1129
+ 'Own%': [total_own]
1130
+ })
1131
+ summary_row = summary_row[['Salary', 'Median', 'Own%']].head(max_players)
1132
+
1133
+ col1, col3 = st.columns([2, 3])
1134
+
1135
+ with col1:
1136
+ if (max_players - len(filled_lineup)) > 0:
1137
+ st.markdown(f"""
1138
+ <div style='text-align: left; vertical-align: top; margin-top: 0; padding-top: 0;''>
1139
+ <b>πŸ’° Per Player:</b> ${round((max_salary - total_salary) / (max_players - len(filled_lineup)), 0)} &nbsp;
1140
+ </div>
1141
+ """,
1142
+ unsafe_allow_html=True)
1143
+ else:
1144
+ st.markdown(f"""
1145
+ <div style='text-align: left; vertical-align: top; margin-top: 0; padding-top: 0;''>
1146
+ <b>πŸ’° Leftover:</b> ${round(max_salary - total_salary, 0)} &nbsp;
1147
+ </div>
1148
+ """,
1149
+ unsafe_allow_html=True)
1150
+
1151
+ with col3:
1152
+ if total_salary <= max_salary:
1153
+ st.markdown(
1154
+ f"""
1155
+ <div style='text-align: right; vertical-align: top; margin-top: 0; padding-top: 0;''>
1156
+ <b>πŸ’° Salary:</b> ${round(total_salary, 0)} &nbsp;
1157
+ <b>πŸ”₯ Median:</b> {round(total_median, 2)} &nbsp;
1158
+ </div>
1159
+ """,
1160
+ unsafe_allow_html=True
1161
+ )
1162
+ else:
1163
+ st.markdown(
1164
+ f"""
1165
+ <div style='text-align: right; vertical-align: top; margin-top: 0; padding-top: 0;''>
1166
+ <b>❌ Salary:</b> ${round(total_salary, 0)} &nbsp;
1167
+ <b>πŸ”₯ Median:</b> {round(total_median, 2)} &nbsp;
1168
+ </div>
1169
+ """,
1170
+ unsafe_allow_html=True
1171
+ )
1172
+
1173
+ # Optionally, add a button to clear the lineup
1174
+ if st.button("Clear Lineup", key='clear_lineup'):
1175
+ st.session_state['handbuilder_lineup'] = pd.DataFrame(columns=['Player', 'Position', 'Team', 'Team_Total', 'Opp_Total', 'Salary', 'Median', '2x%', 'Own%', 'Slot', 'Order'])
1176
+ st.rerun()