Spaces:
Sleeping
Sleeping
| 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 | |