DFS_Portfolio_Manager / global_func /optimize_lineup.py
James McCool
Adding optimization
21b08b7
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'
) -> pd.Series:
"""
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
Returns:
Optimized row with potentially upgraded players
"""
# 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()
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)
else:
# This position is open for optimization
open_columns.append(col)
# If no open columns, nothing to optimize
if not open_columns:
return optimized_row
# Calculate remaining salary budget
remaining_salary = salary_max - locked_salary
# 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()
if available_players.empty:
return optimized_row
# Build the optimization model
solver = pywraplp.Solver.CreateSolver('CBC')
if not solver:
# Fallback if solver not available
return optimized_row
# 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 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: 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 4: 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)
# Objective: Maximize the sum of the optimization metric
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)
solver.Maximize(sum(objective_terms))
# Solve
status = solver.Solve()
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']
break
return optimized_row
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'
) -> 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)
Returns:
DataFrame with optimized lineups
"""
# Create a copy to avoid modifying the original
optimized_frame = working_frame.copy()
# Optimize each row
for idx in optimized_frame.index:
row = optimized_frame.loc[idx]
optimized_row = 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
)
optimized_frame.loc[idx] = optimized_row
return optimized_frame