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- app.py +0 -4
- 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 |
-
|
|
|
|
| 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 |
-
|
| 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,
|
| 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:
|
| 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 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|