File size: 10,340 Bytes
a35b524 21b08b7 a35b524 21b08b7 a35b524 21b08b7 a35b524 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 47189b9 21b08b7 211cc48 21b08b7 47189b9 21b08b7 211cc48 21b08b7 211cc48 21b08b7 47189b9 21b08b7 ad7b76c 211cc48 ad7b76c 21b08b7 ad7b76c 21b08b7 87ed105 ad7b76c 21b08b7 211cc48 21b08b7 211cc48 21b08b7 8e112a6 21b08b7 8e112a6 47189b9 8e112a6 47189b9 211cc48 47189b9 8e112a6 47189b9 21b08b7 47189b9 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 a35b524 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 21b08b7 211cc48 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
import pandas as pd
import numpy as np
from ortools.linear_solver import pywraplp
from global_func.exposure_spread import check_position_eligibility
def get_effective_salary(player_name: str, column_name: str, map_dict: dict, type_var: str) -> float:
"""Calculate the effective salary for a player in a specific column (handles CPT multiplier)"""
base_salary = map_dict['salary_map'].get(player_name, 0)
if type_var != 'Classic' and column_name == 'CPT':
return base_salary * 1.5
return base_salary
def optimize_single_lineup(
row: pd.Series,
player_columns: list,
player_pool: pd.DataFrame,
map_dict: dict,
lock_teams: list,
type_var: str,
sport_var: str,
salary_max: int,
optimize_by: str = 'median',
max_objective_value: float = None # NEW PARAMETER
) -> tuple[pd.Series, float]: # NOW RETURNS (row, objective_value)
"""
Optimize a single lineup row using linear programming.
Players from lock_teams are kept (locked), all other positions are cleared
and re-optimized using OR-Tools linear solver.
Args:
row: A single lineup row from the DataFrame
player_columns: List of column names containing player positions
player_pool: DataFrame of available players (projections_df)
map_dict: Dictionary containing player mappings
lock_teams: List of team names whose players should be KEPT (locked)
type_var: 'Classic' or 'Showdown'
sport_var: Sport identifier (NFL, NBA, MLB, etc.)
salary_max: Maximum salary cap for the lineup
optimize_by: 'median' or 'ownership' - which metric to optimize for
max_objective_value: Maximum allowed objective value (for cascading optimization)
Returns:
Tuple of (optimized_row, achieved_objective_value)
"""
# Create a copy of the row to modify
optimized_row = row.copy()
# Identify locked players (from lock_teams) and open positions
locked_players = {} # {column: player_name}
open_columns = []
locked_salary = 0
locked_player_names = set()
locked_objective_value = 0 # Track locked player contribution to objective
for col in player_columns:
player_name = row[col]
player_team = map_dict['team_map'].get(player_name, '')
if player_team in lock_teams:
# Keep this player locked
locked_players[col] = player_name
locked_salary += get_effective_salary(player_name, col, map_dict, type_var)
locked_player_names.add(player_name)
# Add locked player's contribution to objective
player_data = player_pool[player_pool['player_names'] == player_name]
if not player_data.empty:
locked_objective_value += player_data.iloc[0].get(optimize_by, player_data.iloc[0].get('median', 0))
else:
# This position is open for optimization
open_columns.append(col)
# If no open columns, return with locked objective value
if not open_columns:
return optimized_row, locked_objective_value
# Calculate remaining salary budget
remaining_salary = salary_max - locked_salary
# Calculate remaining objective budget (if max_objective_value is set)
remaining_objective_budget = None
if max_objective_value is not None:
remaining_objective_budget = max_objective_value - locked_objective_value
# If we can't meet the constraint, return original row
if remaining_objective_budget < 0:
return optimized_row, locked_objective_value
# Filter player pool: exclude locked teams and already-locked players
available_players = player_pool[
(~player_pool['team'].isin(lock_teams)) &
(~player_pool['player_names'].isin(locked_player_names))
].copy()
# CRITICAL: Remove duplicate players from available pool
available_players = available_players.drop_duplicates(subset=['player_names'], keep='first')
if available_players.empty:
return optimized_row, locked_objective_value
# Build the optimization model
solver = pywraplp.Solver.CreateSolver('CBC')
if not solver:
# Fallback if solver not available
return optimized_row, locked_objective_value
# Create decision variables: x[player_idx, col_idx] = 1 if player is assigned to column
player_list = available_players.to_dict('records')
num_players = len(player_list)
num_open_cols = len(open_columns)
# x[i][j] = 1 if player i is assigned to open column j
x = {}
for i in range(num_players):
for j in range(num_open_cols):
x[i, j] = solver.BoolVar(f'x_{i}_{j}')
# Constraint 1: Each open column gets exactly one player
for j in range(num_open_cols):
solver.Add(sum(x[i, j] for i in range(num_players)) == 1)
# Constraint 2: Each player can only be used AT MOST once across all open columns
for i in range(num_players):
solver.Add(sum(x[i, j] for j in range(num_open_cols)) <= 1)
# Constraint 3: Players already LOCKED in the row cannot be selected again
for i, player in enumerate(player_list):
player_name = player['player_names']
if player_name in locked_player_names:
for j in range(num_open_cols):
solver.Add(x[i, j] == 0)
# Constraint 4: Position eligibility
for i, player in enumerate(player_list):
player_positions = player['position'].split('/')
for j, col in enumerate(open_columns):
if type_var == 'Classic':
if not check_position_eligibility(sport_var, col, player_positions):
solver.Add(x[i, j] == 0)
else:
# For Showdown, CPT and FLEX can take any player
pass
# Constraint 5: Total salary of selected players <= remaining_salary
salary_constraint = []
for i, player in enumerate(player_list):
for j, col in enumerate(open_columns):
effective_salary = get_effective_salary(player['player_names'], col, map_dict, type_var)
salary_constraint.append(x[i, j] * effective_salary)
solver.Add(sum(salary_constraint) <= remaining_salary)
# NEW Constraint 6: Total objective value <= max_objective_value (if specified)
objective_terms = []
for i, player in enumerate(player_list):
metric_value = player.get(optimize_by, player.get('median', 0))
for j in range(num_open_cols):
objective_terms.append(x[i, j] * metric_value)
if remaining_objective_budget is not None:
solver.Add(sum(objective_terms) <= remaining_objective_budget)
# Objective: Maximize the sum of the optimization metric
solver.Maximize(sum(objective_terms))
# Solve
status = solver.Solve()
achieved_objective = locked_objective_value # Start with locked contribution
if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
# Extract solution
for j, col in enumerate(open_columns):
for i, player in enumerate(player_list):
if x[i, j].solution_value() > 0.5:
optimized_row[col] = player['player_names']
achieved_objective += player.get(optimize_by, player.get('median', 0))
break
return optimized_row, achieved_objective
def optimize_lineup(
working_frame: pd.DataFrame,
projections_df: pd.DataFrame,
player_columns: list,
map_dict: dict,
lock_teams: list,
site_var: str,
type_var: str,
sport_var: str,
salary_max: int,
optimize_by: str = 'median',
use_cascading: bool = True, # NEW PARAMETER
cascade_decrement: float = 0.01 # NEW PARAMETER
) -> pd.DataFrame:
"""
Optimize all lineups in a portfolio using linear programming.
Players from lock_teams are kept (locked), all other positions are cleared
and re-optimized to find the best combination that fits the salary cap.
Args:
working_frame: DataFrame containing lineups to optimize
projections_df: DataFrame with player projections (must have columns:
player_names, team, position, salary, median, ownership)
player_columns: List of column names containing player positions
map_dict: Dictionary containing player mappings
lock_teams: List of team names whose players should be KEPT (locked).
All other players will be cleared and re-optimized.
site_var: 'Draftkings' or 'Fanduel'
type_var: 'Classic' or 'Showdown'
sport_var: Sport identifier (NFL, NBA, MLB, etc.)
salary_max: Maximum salary cap for lineups
optimize_by: 'median' or 'ownership' - which metric to optimize for (higher is better)
use_cascading: If True, each row's max objective is constrained by previous row
cascade_decrement: Amount to subtract from previous row's objective (default 0.01)
Returns:
DataFrame with optimized lineups
"""
# Create a copy to avoid modifying the original
optimized_frame = working_frame.copy()
# Track the previous row's objective value
previous_objective = None
# Optimize each row
for idx in optimized_frame.index:
row = optimized_frame.loc[idx]
# Set max_objective_value based on cascading setting
max_objective = None
if use_cascading and previous_objective is not None:
max_objective = previous_objective - cascade_decrement
optimized_row, achieved_objective = optimize_single_lineup(
row=row,
player_columns=player_columns,
player_pool=projections_df,
map_dict=map_dict,
lock_teams=lock_teams if lock_teams else [],
type_var=type_var,
sport_var=sport_var,
salary_max=salary_max,
optimize_by=optimize_by,
max_objective_value=max_objective
)
optimized_frame.loc[idx] = optimized_row
# Update previous_objective for next iteration
previous_objective = achieved_objective
return optimized_frame |