James McCool commited on
Commit
211cc48
·
1 Parent(s): b21657e

Enhance lineup optimization by adding cascading constraints and new parameters for objective value tracking. The optimize_single_lineup function now returns both the optimized row and the achieved objective value, allowing for more refined control over lineup adjustments. Additionally, the optimize_lineup function incorporates cascading optimization logic to ensure each row's objective is constrained by the previous row's performance.

Browse files
Files changed (2) hide show
  1. app.py +0 -4
  2. global_func/optimize_lineup.py +54 -16
app.py CHANGED
@@ -1855,7 +1855,6 @@ if selected_tab == 'Projections Management':
1855
  position_options = ['All Positions'] + sorted(projections_editor_df['position'].unique().tolist())
1856
  position_filter = st.selectbox("Filter by Position", options=position_options, key='proj_position_filter')
1857
 
1858
- # Apply filters
1859
  filtered_df = projections_editor_df.copy()
1860
  if player_search:
1861
  filtered_df = filtered_df[filtered_df['player_names'].str.contains(player_search, case=False, na=False)]
@@ -1864,7 +1863,6 @@ if selected_tab == 'Projections Management':
1864
  if position_filter != 'All Positions':
1865
  filtered_df = filtered_df[filtered_df['position'] == position_filter]
1866
 
1867
- # Display the editable dataframe
1868
  edited_df = st.data_editor(
1869
  filtered_df,
1870
  column_config=column_config,
@@ -1879,11 +1877,9 @@ if selected_tab == 'Projections Management':
1879
  changed_rows = edited_df[changed_mask]
1880
 
1881
  if len(changed_rows) > 0:
1882
- # Update the projections_df in session state
1883
  for idx, row in changed_rows.iterrows():
1884
  player_name = row['player_names']
1885
 
1886
- # Find and update the original projections_df
1887
  orig_idx = st.session_state['portfolio_inc_proj'][st.session_state['portfolio_inc_proj']['player_names'] == player_name].index
1888
  if len(orig_idx) > 0:
1889
  # Player exists in portfolio_inc_proj - update existing row
 
1855
  position_options = ['All Positions'] + sorted(projections_editor_df['position'].unique().tolist())
1856
  position_filter = st.selectbox("Filter by Position", options=position_options, key='proj_position_filter')
1857
 
 
1858
  filtered_df = projections_editor_df.copy()
1859
  if player_search:
1860
  filtered_df = filtered_df[filtered_df['player_names'].str.contains(player_search, case=False, na=False)]
 
1863
  if position_filter != 'All Positions':
1864
  filtered_df = filtered_df[filtered_df['position'] == position_filter]
1865
 
 
1866
  edited_df = st.data_editor(
1867
  filtered_df,
1868
  column_config=column_config,
 
1877
  changed_rows = edited_df[changed_mask]
1878
 
1879
  if len(changed_rows) > 0:
 
1880
  for idx, row in changed_rows.iterrows():
1881
  player_name = row['player_names']
1882
 
 
1883
  orig_idx = st.session_state['portfolio_inc_proj'][st.session_state['portfolio_inc_proj']['player_names'] == player_name].index
1884
  if len(orig_idx) > 0:
1885
  # Player exists in portfolio_inc_proj - update existing row
global_func/optimize_lineup.py CHANGED
@@ -22,8 +22,9 @@ def optimize_single_lineup(
22
  type_var: str,
23
  sport_var: str,
24
  salary_max: int,
25
- optimize_by: str = 'median'
26
- ) -> pd.Series:
 
27
  """
28
  Optimize a single lineup row using linear programming.
29
 
@@ -40,9 +41,10 @@ def optimize_single_lineup(
40
  sport_var: Sport identifier (NFL, NBA, MLB, etc.)
41
  salary_max: Maximum salary cap for the lineup
42
  optimize_by: 'median' or 'ownership' - which metric to optimize for
 
43
 
44
  Returns:
45
- Optimized row with potentially upgraded players
46
  """
47
  # Create a copy of the row to modify
48
  optimized_row = row.copy()
@@ -52,6 +54,7 @@ def optimize_single_lineup(
52
  open_columns = []
53
  locked_salary = 0
54
  locked_player_names = set()
 
55
 
56
  for col in player_columns:
57
  player_name = row[col]
@@ -62,17 +65,30 @@ def optimize_single_lineup(
62
  locked_players[col] = player_name
63
  locked_salary += get_effective_salary(player_name, col, map_dict, type_var)
64
  locked_player_names.add(player_name)
 
 
 
 
 
65
  else:
66
  # This position is open for optimization
67
  open_columns.append(col)
68
 
69
- # If no open columns, nothing to optimize
70
  if not open_columns:
71
- return optimized_row
72
 
73
  # Calculate remaining salary budget
74
  remaining_salary = salary_max - locked_salary
75
 
 
 
 
 
 
 
 
 
76
  # Filter player pool: exclude locked teams and already-locked players
77
  available_players = player_pool[
78
  (~player_pool['team'].isin(lock_teams)) &
@@ -83,19 +99,18 @@ def optimize_single_lineup(
83
  available_players = available_players.drop_duplicates(subset=['player_names'], keep='first')
84
 
85
  if available_players.empty:
86
- return optimized_row
87
 
88
  # Build the optimization model
89
  solver = pywraplp.Solver.CreateSolver('CBC')
90
  if not solver:
91
  # Fallback if solver not available
92
- return optimized_row
93
 
94
  # Create decision variables: x[player_idx, col_idx] = 1 if player is assigned to column
95
  player_list = available_players.to_dict('records')
96
  num_players = len(player_list)
97
  num_open_cols = len(open_columns)
98
- num_player_columns = len(player_columns)
99
 
100
  # x[i][j] = 1 if player i is assigned to open column j
101
  x = {}
@@ -112,10 +127,9 @@ def optimize_single_lineup(
112
  solver.Add(sum(x[i, j] for j in range(num_open_cols)) <= 1)
113
 
114
  # Constraint 3: Players already LOCKED in the row cannot be selected again
115
- # (only check locked_player_names, not all players in row)
116
  for i, player in enumerate(player_list):
117
  player_name = player['player_names']
118
- if player_name in locked_player_names: # ✅ Only check locked players
119
  for j in range(num_open_cols):
120
  solver.Add(x[i, j] == 0)
121
 
@@ -138,27 +152,34 @@ def optimize_single_lineup(
138
  salary_constraint.append(x[i, j] * effective_salary)
139
  solver.Add(sum(salary_constraint) <= remaining_salary)
140
 
141
- # Objective: Maximize the sum of the optimization metric
142
  objective_terms = []
143
  for i, player in enumerate(player_list):
144
  metric_value = player.get(optimize_by, player.get('median', 0))
145
  for j in range(num_open_cols):
146
  objective_terms.append(x[i, j] * metric_value)
147
 
 
 
 
 
148
  solver.Maximize(sum(objective_terms))
149
 
150
  # Solve
151
  status = solver.Solve()
152
 
 
 
153
  if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
154
  # Extract solution
155
  for j, col in enumerate(open_columns):
156
  for i, player in enumerate(player_list):
157
  if x[i, j].solution_value() > 0.5:
158
  optimized_row[col] = player['player_names']
 
159
  break
160
 
161
- return optimized_row
162
 
163
 
164
  def optimize_lineup(
@@ -171,7 +192,9 @@ def optimize_lineup(
171
  type_var: str,
172
  sport_var: str,
173
  salary_max: int,
174
- optimize_by: str = 'median'
 
 
175
  ) -> pd.DataFrame:
176
  """
177
  Optimize all lineups in a portfolio using linear programming.
@@ -192,6 +215,8 @@ def optimize_lineup(
192
  sport_var: Sport identifier (NFL, NBA, MLB, etc.)
193
  salary_max: Maximum salary cap for lineups
194
  optimize_by: 'median' or 'ownership' - which metric to optimize for (higher is better)
 
 
195
 
196
  Returns:
197
  DataFrame with optimized lineups
@@ -199,10 +224,19 @@ def optimize_lineup(
199
  # Create a copy to avoid modifying the original
200
  optimized_frame = working_frame.copy()
201
 
 
 
 
202
  # Optimize each row
203
  for idx in optimized_frame.index:
204
  row = optimized_frame.loc[idx]
205
- optimized_row = optimize_single_lineup(
 
 
 
 
 
 
206
  row=row,
207
  player_columns=player_columns,
208
  player_pool=projections_df,
@@ -211,8 +245,12 @@ def optimize_lineup(
211
  type_var=type_var,
212
  sport_var=sport_var,
213
  salary_max=salary_max,
214
- optimize_by=optimize_by
 
215
  )
216
  optimized_frame.loc[idx] = optimized_row
 
 
 
217
 
218
- return optimized_frame
 
22
  type_var: str,
23
  sport_var: str,
24
  salary_max: int,
25
+ optimize_by: str = 'median',
26
+ max_objective_value: float = None # NEW PARAMETER
27
+ ) -> tuple[pd.Series, float]: # NOW RETURNS (row, objective_value)
28
  """
29
  Optimize a single lineup row using linear programming.
30
 
 
41
  sport_var: Sport identifier (NFL, NBA, MLB, etc.)
42
  salary_max: Maximum salary cap for the lineup
43
  optimize_by: 'median' or 'ownership' - which metric to optimize for
44
+ max_objective_value: Maximum allowed objective value (for cascading optimization)
45
 
46
  Returns:
47
+ Tuple of (optimized_row, achieved_objective_value)
48
  """
49
  # Create a copy of the row to modify
50
  optimized_row = row.copy()
 
54
  open_columns = []
55
  locked_salary = 0
56
  locked_player_names = set()
57
+ locked_objective_value = 0 # Track locked player contribution to objective
58
 
59
  for col in player_columns:
60
  player_name = row[col]
 
65
  locked_players[col] = player_name
66
  locked_salary += get_effective_salary(player_name, col, map_dict, type_var)
67
  locked_player_names.add(player_name)
68
+
69
+ # Add locked player's contribution to objective
70
+ player_data = player_pool[player_pool['player_names'] == player_name]
71
+ if not player_data.empty:
72
+ locked_objective_value += player_data.iloc[0].get(optimize_by, player_data.iloc[0].get('median', 0))
73
  else:
74
  # This position is open for optimization
75
  open_columns.append(col)
76
 
77
+ # If no open columns, return with locked objective value
78
  if not open_columns:
79
+ return optimized_row, locked_objective_value
80
 
81
  # Calculate remaining salary budget
82
  remaining_salary = salary_max - locked_salary
83
 
84
+ # Calculate remaining objective budget (if max_objective_value is set)
85
+ remaining_objective_budget = None
86
+ if max_objective_value is not None:
87
+ remaining_objective_budget = max_objective_value - locked_objective_value
88
+ # If we can't meet the constraint, return original row
89
+ if remaining_objective_budget < 0:
90
+ return optimized_row, locked_objective_value
91
+
92
  # Filter player pool: exclude locked teams and already-locked players
93
  available_players = player_pool[
94
  (~player_pool['team'].isin(lock_teams)) &
 
99
  available_players = available_players.drop_duplicates(subset=['player_names'], keep='first')
100
 
101
  if available_players.empty:
102
+ return optimized_row, locked_objective_value
103
 
104
  # Build the optimization model
105
  solver = pywraplp.Solver.CreateSolver('CBC')
106
  if not solver:
107
  # Fallback if solver not available
108
+ return optimized_row, locked_objective_value
109
 
110
  # Create decision variables: x[player_idx, col_idx] = 1 if player is assigned to column
111
  player_list = available_players.to_dict('records')
112
  num_players = len(player_list)
113
  num_open_cols = len(open_columns)
 
114
 
115
  # x[i][j] = 1 if player i is assigned to open column j
116
  x = {}
 
127
  solver.Add(sum(x[i, j] for j in range(num_open_cols)) <= 1)
128
 
129
  # Constraint 3: Players already LOCKED in the row cannot be selected again
 
130
  for i, player in enumerate(player_list):
131
  player_name = player['player_names']
132
+ if player_name in locked_player_names:
133
  for j in range(num_open_cols):
134
  solver.Add(x[i, j] == 0)
135
 
 
152
  salary_constraint.append(x[i, j] * effective_salary)
153
  solver.Add(sum(salary_constraint) <= remaining_salary)
154
 
155
+ # NEW Constraint 6: Total objective value <= max_objective_value (if specified)
156
  objective_terms = []
157
  for i, player in enumerate(player_list):
158
  metric_value = player.get(optimize_by, player.get('median', 0))
159
  for j in range(num_open_cols):
160
  objective_terms.append(x[i, j] * metric_value)
161
 
162
+ if remaining_objective_budget is not None:
163
+ solver.Add(sum(objective_terms) <= remaining_objective_budget)
164
+
165
+ # Objective: Maximize the sum of the optimization metric
166
  solver.Maximize(sum(objective_terms))
167
 
168
  # Solve
169
  status = solver.Solve()
170
 
171
+ achieved_objective = locked_objective_value # Start with locked contribution
172
+
173
  if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
174
  # Extract solution
175
  for j, col in enumerate(open_columns):
176
  for i, player in enumerate(player_list):
177
  if x[i, j].solution_value() > 0.5:
178
  optimized_row[col] = player['player_names']
179
+ achieved_objective += player.get(optimize_by, player.get('median', 0))
180
  break
181
 
182
+ return optimized_row, achieved_objective
183
 
184
 
185
  def optimize_lineup(
 
192
  type_var: str,
193
  sport_var: str,
194
  salary_max: int,
195
+ optimize_by: str = 'median',
196
+ use_cascading: bool = True, # NEW PARAMETER
197
+ cascade_decrement: float = 0.01 # NEW PARAMETER
198
  ) -> pd.DataFrame:
199
  """
200
  Optimize all lineups in a portfolio using linear programming.
 
215
  sport_var: Sport identifier (NFL, NBA, MLB, etc.)
216
  salary_max: Maximum salary cap for lineups
217
  optimize_by: 'median' or 'ownership' - which metric to optimize for (higher is better)
218
+ use_cascading: If True, each row's max objective is constrained by previous row
219
+ cascade_decrement: Amount to subtract from previous row's objective (default 0.01)
220
 
221
  Returns:
222
  DataFrame with optimized lineups
 
224
  # Create a copy to avoid modifying the original
225
  optimized_frame = working_frame.copy()
226
 
227
+ # Track the previous row's objective value
228
+ previous_objective = None
229
+
230
  # Optimize each row
231
  for idx in optimized_frame.index:
232
  row = optimized_frame.loc[idx]
233
+
234
+ # Set max_objective_value based on cascading setting
235
+ max_objective = None
236
+ if use_cascading and previous_objective is not None:
237
+ max_objective = previous_objective - cascade_decrement
238
+
239
+ optimized_row, achieved_objective = optimize_single_lineup(
240
  row=row,
241
  player_columns=player_columns,
242
  player_pool=projections_df,
 
245
  type_var=type_var,
246
  sport_var=sport_var,
247
  salary_max=salary_max,
248
+ optimize_by=optimize_by,
249
+ max_objective_value=max_objective
250
  )
251
  optimized_frame.loc[idx] = optimized_row
252
+
253
+ # Update previous_objective for next iteration
254
+ previous_objective = achieved_objective
255
 
256
+ return optimized_frame