James McCool commited on
Commit
61123ac
·
1 Parent(s): 072cb99

Refactor Streamlit app to integrate MongoDB connection and enhance data loading features. Added user interface elements for selecting sites, slates, and stack analysis options. Implemented position validation for player combinations and added CSV export functionality for stack options.

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +305 -36
src/streamlit_app.py CHANGED
@@ -1,40 +1,309 @@
1
- import altair as alt
2
  import numpy as np
3
  import pandas as pd
4
  import streamlit as st
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import numpy as np
2
  import pandas as pd
3
  import streamlit as st
4
+ from itertools import combinations
5
+ import pymongo
6
 
7
+ st.set_page_config(layout="wide")
8
+
9
+ @st.cache_resource
10
+ def init_conn():
11
+
12
+ uri = st.secrets['mongo_uri']
13
+ client = pymongo.MongoClient(uri, retryWrites=True, serverSelectionTimeoutMS=500000)
14
+ db = client["MLB_Database"]
15
+
16
+ return db
17
+
18
+ db = init_conn()
19
+
20
+ game_format = {'Win Percentage': '{:.2%}','First Inning Lead Percentage': '{:.2%}',
21
+ 'Fifth Inning Lead Percentage': '{:.2%}', '8+ runs': '{:.2%}', 'DK LevX': '{:.2%}', 'FD LevX': '{:.2%}'}
22
+
23
+ team_roo_format = {'Top Score%': '{:.2%}','0 Runs': '{:.2%}', '1 Run': '{:.2%}', '2 Runs': '{:.2%}', '3 Runs': '{:.2%}', '4 Runs': '{:.2%}',
24
+ '5 Runs': '{:.2%}','6 Runs': '{:.2%}', '7 Runs': '{:.2%}', '8 Runs': '{:.2%}', '9 Runs': '{:.2%}', '10 Runs': '{:.2%}'}
25
+
26
+ wrong_acro = ['WSH', 'AZ', 'CHW']
27
+ right_acro = ['WAS', 'ARI', 'CWS']
28
+
29
+ @st.cache_resource(ttl = 60)
30
+ def init_stat_load():
31
+
32
+ collection = db["Player_Range_Of_Outcomes"]
33
+ cursor = collection.find()
34
+
35
+ raw_display = pd.DataFrame(list(cursor))
36
+ raw_display = raw_display[['Player', 'Position', 'Team', 'Salary', 'Floor', 'Median', 'Ceiling', 'Own%', 'Site', 'Slate']]
37
+ raw_display = raw_display.rename(columns={'Own%': 'Own'})
38
+ initial_concat = raw_display.sort_values(by='Own', ascending=False)
39
+
40
+ return initial_concat
41
+
42
+ @st.cache_data
43
+ def convert_df_to_csv(df):
44
+ return df.to_csv().encode('utf-8')
45
+
46
+ proj_raw = init_stat_load()
47
+
48
+ col1, col2 = st.columns([1, 5])
49
+
50
+ with col1:
51
+ with st.container():
52
+ if st.button("Load/Reset Data", key='reset1'):
53
+ st.cache_data.clear()
54
+ proj_raw = init_stat_load()
55
+ for key in st.session_state.keys():
56
+ del st.session_state[key]
57
+ site_var1 = st.radio("What site are you working with?", ('Draftkings', 'Fanduel'), key='site_var1')
58
+ slate_var1 = st.radio("What slate are you working with?", ('Main Slate', 'Secondary Slate'), key='slate_var1')
59
+ if site_var1 == 'Draftkings':
60
+ raw_baselines = proj_raw[proj_raw['Site'] == 'Draftkings']
61
+ if slate_var1 == 'Main Slate':
62
+ raw_baselines = raw_baselines[raw_baselines['Slate'] == 'main_slate']
63
+ elif slate_var1 == 'Secondary Slate':
64
+ raw_baselines = raw_baselines[raw_baselines['Slate'] == 'secondary_slate']
65
+ raw_baselines = raw_baselines.sort_values(by='Own', ascending=False)
66
+ elif site_var1 == 'Fanduel':
67
+ raw_baselines = proj_raw[proj_raw['Site'] == 'Fanduel']
68
+ if slate_var1 == 'Main Slate':
69
+ raw_baselines = raw_baselines[raw_baselines['Slate'] == 'main_slate']
70
+ elif slate_var1 == 'Secondary Slate':
71
+ raw_baselines = raw_baselines[raw_baselines['Slate'] == 'secondary_slate']
72
+ raw_baselines = raw_baselines.sort_values(by='Own', ascending=False)
73
+ split_var2 = st.radio("Would you like to run stack analysis for the full slate or individual teams?", ('Full Slate Run', 'Specific Teams'), key='split_var2')
74
+ if split_var2 == 'Specific Teams':
75
+ team_var2 = st.multiselect('Which teams would you like to include in the analysis?', options = raw_baselines['Team'].unique(), key='team_var2')
76
+ elif split_var2 == 'Full Slate Run':
77
+ team_var2 = raw_baselines.Team.unique().tolist()
78
+ pos_split2 = st.radio("Are you viewing all positions, specific groups, or specific positions?", ('All Positions', 'Specific Positions'), key='pos_split2')
79
+ if pos_split2 == 'Specific Positions':
80
+ pos_var2 = st.multiselect('What Positions would you like to view?', options = ['C', '1B', '2B', '3B', 'SS', 'OF'])
81
+ elif pos_split2 == 'All Positions':
82
+ pos_var2 = 'All'
83
+ if site_var1 == 'Draftkings':
84
+ max_sal2 = st.number_input('Max Salary', min_value = 5000, max_value = 50000, value = 35000, step = 100, key='max_sal2')
85
+ elif site_var1 == 'Fanduel':
86
+ max_sal2 = st.number_input('Max Salary', min_value = 5000, max_value = 35000, value = 25000, step = 100, key='max_sal2')
87
+ size_var2 = st.selectbox('What size of stacks are you analyzing?', options = ['3-man', '4-man', '5-man'])
88
+ if size_var2 == '3-man':
89
+ stack_size = 3
90
+ if size_var2 == '4-man':
91
+ stack_size = 4
92
+ if size_var2 == '5-man':
93
+ stack_size = 5
94
+
95
+ team_dict = dict(zip(raw_baselines.Player, raw_baselines.Team))
96
+ proj_dict = dict(zip(raw_baselines.Player, raw_baselines.Median))
97
+ own_dict = dict(zip(raw_baselines.Player, raw_baselines.Own))
98
+ cost_dict = dict(zip(raw_baselines.Player, raw_baselines.Salary))
99
+
100
+ with col2:
101
+
102
+ if site_var1 == 'Draftkings':
103
+ position_limits = {
104
+ 'C': 1,
105
+ '1B': 1,
106
+ '2B': 1,
107
+ '3B': 1,
108
+ 'SS': 1,
109
+ 'OF': 3,
110
+ # Add more as needed
111
+ }
112
+ max_salary = 50000
113
+ max_players = 10
114
+ else:
115
+ position_limits = {
116
+ 'C_1B': 1,
117
+ '2B': 1,
118
+ '3B': 1,
119
+ 'SS': 1,
120
+ 'OF': 3,
121
+ 'UTIL': 1,
122
+ # Add more as needed
123
+ }
124
+
125
+ stack_hold_container = st.empty()
126
+ comb_list = []
127
+ if pos_split2 == 'All Positions':
128
+ raw_baselines = raw_baselines
129
+ elif pos_split2 != 'All Positions':
130
+ raw_baselines = raw_baselines[raw_baselines['Position'].str.contains('|'.join(pos_var2))]
131
+
132
+ # Create a position dictionary mapping players to their eligible positions
133
+ pos_dict = dict(zip(raw_baselines.Player, raw_baselines.Position))
134
+
135
+ def is_valid_combination(combo):
136
+ # Count positions in this combination
137
+ position_counts = {pos: 0 for pos in position_limits.keys()}
138
+
139
+ # For each player in the combination
140
+ for player in combo:
141
+ # Get their eligible positions
142
+ player_positions = pos_dict[player].split('/')
143
+
144
+ # For each position they can play
145
+ for pos in player_positions:
146
+ # Handle special cases for FanDuel
147
+ if site_var1 == 'Fanduel':
148
+ if pos in ['C', '1B']:
149
+ position_counts['C_1B'] += 1
150
+ elif pos == 'UTIL':
151
+ # UTIL can be filled by any position
152
+ for p in position_counts:
153
+ position_counts[p] += 1
154
+ else: # DraftKings
155
+ if pos in position_counts:
156
+ position_counts[pos] += 1
157
+
158
+ # Check if any position exceeds its limit
159
+ for pos, limit in position_limits.items():
160
+ if position_counts[pos] > limit:
161
+ return False
162
+
163
+ return True
164
+
165
+ # Modify the combination generation code
166
+ comb_list = []
167
+ for cur_team in team_var2:
168
+ working_baselines = raw_baselines
169
+ working_baselines = working_baselines[working_baselines['Team'] == cur_team]
170
+ working_baselines = working_baselines[working_baselines['Position'] != 'SP']
171
+ working_baselines = working_baselines[working_baselines['Position'] != 'P']
172
+ order_list = working_baselines['Player']
173
+
174
+ comb = combinations(order_list, stack_size)
175
+
176
+ # Only add combinations that satisfy position limits
177
+ for i in list(comb):
178
+ if is_valid_combination(i):
179
+ comb_list.append(i)
180
+
181
+ comb_DF = pd.DataFrame(comb_list)
182
+
183
+ if stack_size == 3:
184
+ comb_DF['Team'] = comb_DF[0].map(team_dict)
185
+
186
+ comb_DF['Proj'] = sum([comb_DF[0].map(proj_dict),
187
+ comb_DF[1].map(proj_dict),
188
+ comb_DF[2].map(proj_dict)])
189
+
190
+ comb_DF['Salary'] = sum([comb_DF[0].map(cost_dict),
191
+ comb_DF[1].map(cost_dict),
192
+ comb_DF[2].map(cost_dict)])
193
+
194
+ comb_DF['Own%'] = sum([comb_DF[0].map(own_dict),
195
+ comb_DF[1].map(own_dict),
196
+ comb_DF[2].map(own_dict)])
197
+ elif stack_size == 4:
198
+ comb_DF['Team'] = comb_DF[0].map(team_dict)
199
+
200
+ comb_DF['Proj'] = sum([comb_DF[0].map(proj_dict),
201
+ comb_DF[1].map(proj_dict),
202
+ comb_DF[2].map(proj_dict),
203
+ comb_DF[3].map(proj_dict)])
204
+
205
+ comb_DF['Salary'] = sum([comb_DF[0].map(cost_dict),
206
+ comb_DF[1].map(cost_dict),
207
+ comb_DF[2].map(cost_dict),
208
+ comb_DF[3].map(cost_dict)])
209
+
210
+ comb_DF['Own%'] = sum([comb_DF[0].map(own_dict),
211
+ comb_DF[1].map(own_dict),
212
+ comb_DF[2].map(own_dict),
213
+ comb_DF[3].map(own_dict)])
214
+ elif stack_size == 5:
215
+ comb_DF['Team'] = comb_DF[0].map(team_dict)
216
+
217
+ comb_DF['Proj'] = sum([comb_DF[0].map(proj_dict),
218
+ comb_DF[1].map(proj_dict),
219
+ comb_DF[2].map(proj_dict),
220
+ comb_DF[3].map(proj_dict),
221
+ comb_DF[4].map(proj_dict)])
222
+
223
+ comb_DF['Salary'] = sum([comb_DF[0].map(cost_dict),
224
+ comb_DF[1].map(cost_dict),
225
+ comb_DF[2].map(cost_dict),
226
+ comb_DF[3].map(cost_dict),
227
+ comb_DF[4].map(cost_dict)])
228
+
229
+ comb_DF['Own%'] = sum([comb_DF[0].map(own_dict),
230
+ comb_DF[1].map(own_dict),
231
+ comb_DF[2].map(own_dict),
232
+ comb_DF[3].map(own_dict),
233
+ comb_DF[4].map(own_dict)])
234
+
235
+ comb_DF = comb_DF.sort_values(by='Proj', ascending=False)
236
+ comb_DF = comb_DF.loc[comb_DF['Salary'] <= max_sal2]
237
+
238
+ cut_var = 0
239
+
240
+ if stack_size == 3:
241
+ while cut_var <= int(len(comb_DF)):
242
+ try:
243
+ if int(cut_var) == 0:
244
+ cur_proj = float(comb_DF.iat[cut_var,4])
245
+ cur_own = float(comb_DF.iat[cut_var,6])
246
+ elif int(cut_var) >= 1:
247
+ check_own = float(comb_DF.iat[cut_var,6])
248
+ if check_own > cur_own:
249
+ comb_DF = comb_DF.drop([cut_var])
250
+ cur_own = cur_own
251
+ cut_var = cut_var - 1
252
+ comb_DF = comb_DF.reset_index()
253
+ comb_DF = comb_DF.drop(['index'], axis=1)
254
+ elif check_own <= cur_own:
255
+ cur_own = float(comb_DF.iat[cut_var,6])
256
+ cut_var = cut_var
257
+ cut_var += 1
258
+ except:
259
+ cut_var += 1
260
+ elif stack_size == 4:
261
+ while cut_var <= int(len(comb_DF)):
262
+ try:
263
+ if int(cut_var) == 0:
264
+ cur_proj = float(comb_DF.iat[cut_var,5])
265
+ cur_own = float(comb_DF.iat[cut_var,7])
266
+ elif int(cut_var) >= 1:
267
+ check_own = float(comb_DF.iat[cut_var,7])
268
+ if check_own > cur_own:
269
+ comb_DF = comb_DF.drop([cut_var])
270
+ cur_own = cur_own
271
+ cut_var = cut_var - 1
272
+ comb_DF = comb_DF.reset_index()
273
+ comb_DF = comb_DF.drop(['index'], axis=1)
274
+ elif check_own <= cur_own:
275
+ cur_own = float(comb_DF.iat[cut_var,7])
276
+ cut_var = cut_var
277
+ cut_var += 1
278
+ except:
279
+ cut_var += 1
280
+ elif stack_size == 5:
281
+ while cut_var <= int(len(comb_DF)):
282
+ try:
283
+ if int(cut_var) == 0:
284
+ cur_proj = float(comb_DF.iat[cut_var,6])
285
+ cur_own = float(comb_DF.iat[cut_var,8])
286
+ elif int(cut_var) >= 1:
287
+ check_own = float(comb_DF.iat[cut_var,8])
288
+ if check_own > cur_own:
289
+ comb_DF = comb_DF.drop([cut_var])
290
+ cur_own = cur_own
291
+ cut_var = cut_var - 1
292
+ comb_DF = comb_DF.reset_index()
293
+ comb_DF = comb_DF.drop(['index'], axis=1)
294
+ elif check_own <= cur_own:
295
+ cur_own = float(comb_DF.iat[cut_var,8])
296
+ cut_var = cut_var
297
+ cut_var += 1
298
+ except:
299
+ cut_var += 1
300
+
301
+ with stack_hold_container:
302
+ stack_hold_container = st.empty()
303
+ st.dataframe(comb_DF.style.background_gradient(axis=0).background_gradient(cmap='RdYlGn').format(precision=2), use_container_width = True)
304
+ st.download_button(
305
+ label="Export Tables",
306
+ data=convert_df_to_csv(comb_DF),
307
+ file_name='MLB_Stack_Options_export.csv',
308
+ mime='text/csv',
309
+ )