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() # 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 # 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) num_player_columns = len(player_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 # (only check locked_player_names, not all players in row) for i, player in enumerate(player_list): player_name = player['player_names'] if player_name in locked_player_names: # ✅ Only check locked players 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) # 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