James McCool
commited on
Commit
·
21b08b7
1
Parent(s):
bec9f9e
Adding optimization
Browse files- app.py +139 -0
- global_func/optimize_lineup.py +199 -67
- requirements.txt +2 -1
app.py
CHANGED
|
@@ -28,6 +28,7 @@ from global_func.stratification_function import stratification_function
|
|
| 28 |
from global_func.exposure_spread import exposure_spread
|
| 29 |
from global_func.reassess_edge import reassess_edge
|
| 30 |
from global_func.recalc_diversity import recalc_diversity
|
|
|
|
| 31 |
|
| 32 |
from database_queries import *
|
| 33 |
from database import *
|
|
@@ -3023,7 +3024,145 @@ if selected_tab == 'Manage Portfolio':
|
|
| 3023 |
axis=1
|
| 3024 |
)
|
| 3025 |
st.session_state['export_merge'] = st.session_state['export_base'].copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3026 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3027 |
with st.container():
|
| 3028 |
if 'export_base' not in st.session_state:
|
| 3029 |
st.session_state['export_base'] = pd.DataFrame(columns=st.session_state['working_frame'].columns)
|
|
|
|
| 28 |
from global_func.exposure_spread import exposure_spread
|
| 29 |
from global_func.reassess_edge import reassess_edge
|
| 30 |
from global_func.recalc_diversity import recalc_diversity
|
| 31 |
+
from global_func.optimize_lineup import optimize_lineup
|
| 32 |
|
| 33 |
from database_queries import *
|
| 34 |
from database import *
|
|
|
|
| 3024 |
axis=1
|
| 3025 |
)
|
| 3026 |
st.session_state['export_merge'] = st.session_state['export_base'].copy()
|
| 3027 |
+
|
| 3028 |
+
with st.expander('Lineup Reoptimization'):
|
| 3029 |
+
with st.form(key='Reoptimize'):
|
| 3030 |
+
optimize_by = st.selectbox("Optimize By", options=['median', 'ownership'], key='optimize_by')
|
| 3031 |
+
lock_teams_optimize = st.multiselect(
|
| 3032 |
+
"Locked Teams",
|
| 3033 |
+
options=sorted(list(set(st.session_state['projections_df']['team'].unique()))),
|
| 3034 |
+
default=[],
|
| 3035 |
+
key='lock_teams_optimize'
|
| 3036 |
+
)
|
| 3037 |
+
opt_submitted_col, opt_export_col = st.columns(2)
|
| 3038 |
+
st.info("Portfolio Button applies to your overall Portfolio, Export button applies to your Custom Export")
|
| 3039 |
+
with opt_submitted_col:
|
| 3040 |
+
opt_reg_submitted = st.form_submit_button("Portfolio")
|
| 3041 |
+
with opt_export_col:
|
| 3042 |
+
opt_exp_submitted = st.form_submit_button("Export")
|
| 3043 |
+
|
| 3044 |
+
if opt_reg_submitted:
|
| 3045 |
+
st.session_state['settings_base'] = False
|
| 3046 |
+
# Run optimization on working_frame
|
| 3047 |
+
optimized_frame = optimize_lineup(
|
| 3048 |
+
working_frame=st.session_state['working_frame'],
|
| 3049 |
+
projections_df=st.session_state['portfolio_inc_proj'],
|
| 3050 |
+
player_columns=st.session_state['player_columns'],
|
| 3051 |
+
map_dict=st.session_state['map_dict'],
|
| 3052 |
+
lock_teams=lock_teams_optimize,
|
| 3053 |
+
site_var=site_var,
|
| 3054 |
+
type_var=type_var,
|
| 3055 |
+
sport_var=sport_var,
|
| 3056 |
+
salary_max=salary_max,
|
| 3057 |
+
optimize_by=optimize_by
|
| 3058 |
+
)
|
| 3059 |
+
|
| 3060 |
+
# Recalculate lineup metrics
|
| 3061 |
+
optimized_frame = calculate_lineup_metrics(
|
| 3062 |
+
optimized_frame,
|
| 3063 |
+
st.session_state['player_columns'],
|
| 3064 |
+
st.session_state['map_dict'],
|
| 3065 |
+
type_var,
|
| 3066 |
+
sport_var,
|
| 3067 |
+
st.session_state['portfolio_inc_proj']
|
| 3068 |
+
)
|
| 3069 |
+
|
| 3070 |
+
st.session_state['working_frame'] = optimized_frame.reset_index(drop=True)
|
| 3071 |
+
|
| 3072 |
+
# Load Default base from compressed storage for reassess_edge
|
| 3073 |
+
default_base = load_base_frame('Default')
|
| 3074 |
+
st.session_state['working_frame'] = reassess_edge(
|
| 3075 |
+
st.session_state['working_frame'],
|
| 3076 |
+
default_base,
|
| 3077 |
+
st.session_state['map_dict'],
|
| 3078 |
+
site_var,
|
| 3079 |
+
type_var,
|
| 3080 |
+
Contest_Size,
|
| 3081 |
+
strength_var,
|
| 3082 |
+
sport_var,
|
| 3083 |
+
salary_max
|
| 3084 |
+
)
|
| 3085 |
|
| 3086 |
+
# Update Stack/Size columns if applicable
|
| 3087 |
+
team_dict = dict(zip(st.session_state['portfolio_inc_proj']['player_names'], st.session_state['portfolio_inc_proj']['team']))
|
| 3088 |
+
if 'Stack' in st.session_state['working_frame'].columns:
|
| 3089 |
+
st.session_state['working_frame']['Stack'] = st.session_state['working_frame'].apply(
|
| 3090 |
+
lambda row: Counter(
|
| 3091 |
+
team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]
|
| 3092 |
+
if team_dict.get(player, '') != ''
|
| 3093 |
+
).most_common(1)[0][0] if any(team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]) else '',
|
| 3094 |
+
axis=1
|
| 3095 |
+
)
|
| 3096 |
+
st.session_state['working_frame']['Size'] = st.session_state['working_frame'].apply(
|
| 3097 |
+
lambda row: Counter(
|
| 3098 |
+
team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]
|
| 3099 |
+
if team_dict.get(player, '') != ''
|
| 3100 |
+
).most_common(1)[0][1] if any(team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]) else 0,
|
| 3101 |
+
axis=1
|
| 3102 |
+
)
|
| 3103 |
+
st.session_state['export_merge'] = st.session_state['working_frame'].copy()
|
| 3104 |
+
|
| 3105 |
+
elif opt_exp_submitted:
|
| 3106 |
+
st.session_state['settings_base'] = False
|
| 3107 |
+
# Run optimization on export_base
|
| 3108 |
+
optimized_frame = optimize_lineup(
|
| 3109 |
+
working_frame=st.session_state['export_base'],
|
| 3110 |
+
projections_df=st.session_state['portfolio_inc_proj'],
|
| 3111 |
+
player_columns=st.session_state['player_columns'],
|
| 3112 |
+
map_dict=st.session_state['map_dict'],
|
| 3113 |
+
lock_teams=lock_teams_optimize,
|
| 3114 |
+
site_var=site_var,
|
| 3115 |
+
type_var=type_var,
|
| 3116 |
+
sport_var=sport_var,
|
| 3117 |
+
salary_max=salary_max,
|
| 3118 |
+
optimize_by=optimize_by
|
| 3119 |
+
)
|
| 3120 |
+
|
| 3121 |
+
# Recalculate lineup metrics for export
|
| 3122 |
+
optimized_frame = calculate_lineup_metrics(
|
| 3123 |
+
optimized_frame,
|
| 3124 |
+
st.session_state['player_columns'],
|
| 3125 |
+
st.session_state['map_dict'],
|
| 3126 |
+
type_var,
|
| 3127 |
+
sport_var,
|
| 3128 |
+
st.session_state['portfolio_inc_proj']
|
| 3129 |
+
)
|
| 3130 |
+
|
| 3131 |
+
st.session_state['export_base'] = optimized_frame.reset_index(drop=True)
|
| 3132 |
+
|
| 3133 |
+
# Load Default base from compressed storage for reassess_edge
|
| 3134 |
+
default_base = load_base_frame('Default')
|
| 3135 |
+
st.session_state['export_base'] = reassess_edge(
|
| 3136 |
+
st.session_state['export_base'],
|
| 3137 |
+
default_base,
|
| 3138 |
+
st.session_state['map_dict'],
|
| 3139 |
+
site_var,
|
| 3140 |
+
type_var,
|
| 3141 |
+
Contest_Size,
|
| 3142 |
+
strength_var,
|
| 3143 |
+
sport_var,
|
| 3144 |
+
salary_max
|
| 3145 |
+
)
|
| 3146 |
+
|
| 3147 |
+
# Update Stack/Size columns if applicable
|
| 3148 |
+
team_dict = dict(zip(st.session_state['portfolio_inc_proj']['player_names'], st.session_state['portfolio_inc_proj']['team']))
|
| 3149 |
+
if 'Stack' in st.session_state['export_base'].columns:
|
| 3150 |
+
st.session_state['export_base']['Stack'] = st.session_state['export_base'].apply(
|
| 3151 |
+
lambda row: Counter(
|
| 3152 |
+
team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]
|
| 3153 |
+
if team_dict.get(player, '') != ''
|
| 3154 |
+
).most_common(1)[0][0] if any(team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]) else '',
|
| 3155 |
+
axis=1
|
| 3156 |
+
)
|
| 3157 |
+
st.session_state['export_base']['Size'] = st.session_state['export_base'].apply(
|
| 3158 |
+
lambda row: Counter(
|
| 3159 |
+
team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]
|
| 3160 |
+
if team_dict.get(player, '') != ''
|
| 3161 |
+
).most_common(1)[0][1] if any(team_dict.get(player, '') for player in row[stack_column_dict[site_var][type_var][sport_var]]) else 0,
|
| 3162 |
+
axis=1
|
| 3163 |
+
)
|
| 3164 |
+
st.session_state['export_merge'] = st.session_state['export_base'].copy()
|
| 3165 |
+
|
| 3166 |
with st.container():
|
| 3167 |
if 'export_base' not in st.session_state:
|
| 3168 |
st.session_state['export_base'] = pd.DataFrame(columns=st.session_state['working_frame'].columns)
|
global_func/optimize_lineup.py
CHANGED
|
@@ -1,74 +1,206 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
import numpy as np
|
| 3 |
import pandas as pd
|
| 4 |
-
import
|
| 5 |
-
from
|
| 6 |
|
| 7 |
-
|
| 8 |
-
current_lineup = []
|
| 9 |
-
total_salary = 0
|
| 10 |
-
salary_cap = 50000
|
| 11 |
-
used_players = set()
|
| 12 |
|
| 13 |
-
# Convert row to dictionary with roster positions
|
| 14 |
-
roster = {}
|
| 15 |
-
for col, player in zip(row.index, row):
|
| 16 |
-
if col not in ['salary', 'median', 'Own', 'Finish_percentile', 'Dupes', 'Lineup Edge']:
|
| 17 |
-
roster[col] = {
|
| 18 |
-
'name': player,
|
| 19 |
-
'position': map_dict['pos_map'].get(player, '').split('/'),
|
| 20 |
-
'team': map_dict['team_map'].get(player, ''),
|
| 21 |
-
'salary': map_dict['salary_map'].get(player, 0),
|
| 22 |
-
'median': map_dict['proj_map'].get(player, 0),
|
| 23 |
-
'ownership': map_dict['own_map'].get(player, 0)
|
| 24 |
-
}
|
| 25 |
-
total_salary += roster[col]['salary']
|
| 26 |
-
used_players.add(player)
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
if current['team'] in remove_teams_var:
|
| 35 |
-
continue
|
| 36 |
-
|
| 37 |
-
valid_positions = position_rules[roster_pos]
|
| 38 |
-
better_options = []
|
| 39 |
|
| 40 |
-
# Find valid replacements for this roster position
|
| 41 |
-
for pos in valid_positions:
|
| 42 |
-
if pos in position_groups:
|
| 43 |
-
pos_options = [
|
| 44 |
-
p for p in position_groups[pos]
|
| 45 |
-
if p['median'] > current['median']
|
| 46 |
-
and (total_salary - current['salary'] + p['salary']) <= salary_cap
|
| 47 |
-
and p['player_names'] not in used_players
|
| 48 |
-
and any(valid_pos in p['positions'] for valid_pos in valid_positions)
|
| 49 |
-
and map_dict['team_map'].get(p['player_names']) not in remove_teams_var # Check team restriction
|
| 50 |
-
]
|
| 51 |
-
better_options.extend(pos_options)
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
from ortools.linear_solver import pywraplp
|
| 4 |
|
| 5 |
+
from global_func.exposure_spread import check_position_eligibility
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
def get_effective_salary(player_name: str, column_name: str, map_dict: dict, type_var: str) -> float:
|
| 9 |
+
"""Calculate the effective salary for a player in a specific column (handles CPT multiplier)"""
|
| 10 |
+
base_salary = map_dict['salary_map'].get(player_name, 0)
|
| 11 |
+
if type_var != 'Classic' and column_name == 'CPT':
|
| 12 |
+
return base_salary * 1.5
|
| 13 |
+
return base_salary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
def optimize_single_lineup(
|
| 17 |
+
row: pd.Series,
|
| 18 |
+
player_columns: list,
|
| 19 |
+
player_pool: pd.DataFrame,
|
| 20 |
+
map_dict: dict,
|
| 21 |
+
lock_teams: list,
|
| 22 |
+
type_var: str,
|
| 23 |
+
sport_var: str,
|
| 24 |
+
salary_max: int,
|
| 25 |
+
optimize_by: str = 'median'
|
| 26 |
+
) -> pd.Series:
|
| 27 |
+
"""
|
| 28 |
+
Optimize a single lineup row using linear programming.
|
| 29 |
+
|
| 30 |
+
Players from lock_teams are kept (locked), all other positions are cleared
|
| 31 |
+
and re-optimized using OR-Tools linear solver.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
row: A single lineup row from the DataFrame
|
| 35 |
+
player_columns: List of column names containing player positions
|
| 36 |
+
player_pool: DataFrame of available players (projections_df)
|
| 37 |
+
map_dict: Dictionary containing player mappings
|
| 38 |
+
lock_teams: List of team names whose players should be KEPT (locked)
|
| 39 |
+
type_var: 'Classic' or 'Showdown'
|
| 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 |
+
Optimized row with potentially upgraded players
|
| 46 |
+
"""
|
| 47 |
+
# Create a copy of the row to modify
|
| 48 |
+
optimized_row = row.copy()
|
| 49 |
+
|
| 50 |
+
# Identify locked players (from lock_teams) and open positions
|
| 51 |
+
locked_players = {} # {column: player_name}
|
| 52 |
+
open_columns = []
|
| 53 |
+
locked_salary = 0
|
| 54 |
+
locked_player_names = set()
|
| 55 |
+
|
| 56 |
+
for col in player_columns:
|
| 57 |
+
player_name = row[col]
|
| 58 |
+
player_team = map_dict['team_map'].get(player_name, '')
|
| 59 |
+
|
| 60 |
+
if player_team in lock_teams:
|
| 61 |
+
# Keep this player locked
|
| 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, nothing to optimize
|
| 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)) &
|
| 79 |
+
(~player_pool['player_names'].isin(locked_player_names))
|
| 80 |
+
].copy()
|
| 81 |
+
|
| 82 |
+
if available_players.empty:
|
| 83 |
+
return optimized_row
|
| 84 |
+
|
| 85 |
+
# Build the optimization model
|
| 86 |
+
solver = pywraplp.Solver.CreateSolver('CBC')
|
| 87 |
+
if not solver:
|
| 88 |
+
# Fallback if solver not available
|
| 89 |
+
return optimized_row
|
| 90 |
+
|
| 91 |
+
# Create decision variables: x[player_idx, col_idx] = 1 if player is assigned to column
|
| 92 |
+
player_list = available_players.to_dict('records')
|
| 93 |
+
num_players = len(player_list)
|
| 94 |
+
num_open_cols = len(open_columns)
|
| 95 |
+
|
| 96 |
+
# x[i][j] = 1 if player i is assigned to open column j
|
| 97 |
+
x = {}
|
| 98 |
+
for i in range(num_players):
|
| 99 |
+
for j in range(num_open_cols):
|
| 100 |
+
x[i, j] = solver.BoolVar(f'x_{i}_{j}')
|
| 101 |
+
|
| 102 |
+
# Constraint 1: Each open column gets exactly one player
|
| 103 |
+
for j in range(num_open_cols):
|
| 104 |
+
solver.Add(sum(x[i, j] for i in range(num_players)) == 1)
|
| 105 |
+
|
| 106 |
+
# Constraint 2: Each player can only be used once across all open columns
|
| 107 |
+
for i in range(num_players):
|
| 108 |
+
solver.Add(sum(x[i, j] for j in range(num_open_cols)) <= 1)
|
| 109 |
+
|
| 110 |
+
# Constraint 3: Position eligibility
|
| 111 |
+
for i, player in enumerate(player_list):
|
| 112 |
+
player_positions = player['position'].split('/')
|
| 113 |
+
for j, col in enumerate(open_columns):
|
| 114 |
+
if type_var == 'Classic':
|
| 115 |
+
if not check_position_eligibility(sport_var, col, player_positions):
|
| 116 |
+
solver.Add(x[i, j] == 0)
|
| 117 |
+
else:
|
| 118 |
+
# For Showdown, CPT and FLEX can take any player
|
| 119 |
+
pass
|
| 120 |
+
|
| 121 |
+
# Constraint 4: Total salary of selected players <= remaining_salary
|
| 122 |
+
salary_constraint = []
|
| 123 |
+
for i, player in enumerate(player_list):
|
| 124 |
+
for j, col in enumerate(open_columns):
|
| 125 |
+
effective_salary = get_effective_salary(player['player_names'], col, map_dict, type_var)
|
| 126 |
+
salary_constraint.append(x[i, j] * effective_salary)
|
| 127 |
+
solver.Add(sum(salary_constraint) <= remaining_salary)
|
| 128 |
+
|
| 129 |
+
# Objective: Maximize the sum of the optimization metric
|
| 130 |
+
objective_terms = []
|
| 131 |
+
for i, player in enumerate(player_list):
|
| 132 |
+
metric_value = player.get(optimize_by, player.get('median', 0))
|
| 133 |
+
for j in range(num_open_cols):
|
| 134 |
+
objective_terms.append(x[i, j] * metric_value)
|
| 135 |
+
|
| 136 |
+
solver.Maximize(sum(objective_terms))
|
| 137 |
+
|
| 138 |
+
# Solve
|
| 139 |
+
status = solver.Solve()
|
| 140 |
+
|
| 141 |
+
if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
|
| 142 |
+
# Extract solution
|
| 143 |
+
for j, col in enumerate(open_columns):
|
| 144 |
+
for i, player in enumerate(player_list):
|
| 145 |
+
if x[i, j].solution_value() > 0.5:
|
| 146 |
+
optimized_row[col] = player['player_names']
|
| 147 |
+
break
|
| 148 |
+
|
| 149 |
+
return optimized_row
|
| 150 |
+
|
| 151 |
|
| 152 |
+
def optimize_lineup(
|
| 153 |
+
working_frame: pd.DataFrame,
|
| 154 |
+
projections_df: pd.DataFrame,
|
| 155 |
+
player_columns: list,
|
| 156 |
+
map_dict: dict,
|
| 157 |
+
lock_teams: list,
|
| 158 |
+
site_var: str,
|
| 159 |
+
type_var: str,
|
| 160 |
+
sport_var: str,
|
| 161 |
+
salary_max: int,
|
| 162 |
+
optimize_by: str = 'median'
|
| 163 |
+
) -> pd.DataFrame:
|
| 164 |
+
"""
|
| 165 |
+
Optimize all lineups in a portfolio using linear programming.
|
| 166 |
+
|
| 167 |
+
Players from lock_teams are kept (locked), all other positions are cleared
|
| 168 |
+
and re-optimized to find the best combination that fits the salary cap.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
working_frame: DataFrame containing lineups to optimize
|
| 172 |
+
projections_df: DataFrame with player projections (must have columns:
|
| 173 |
+
player_names, team, position, salary, median, ownership)
|
| 174 |
+
player_columns: List of column names containing player positions
|
| 175 |
+
map_dict: Dictionary containing player mappings
|
| 176 |
+
lock_teams: List of team names whose players should be KEPT (locked).
|
| 177 |
+
All other players will be cleared and re-optimized.
|
| 178 |
+
site_var: 'Draftkings' or 'Fanduel'
|
| 179 |
+
type_var: 'Classic' or 'Showdown'
|
| 180 |
+
sport_var: Sport identifier (NFL, NBA, MLB, etc.)
|
| 181 |
+
salary_max: Maximum salary cap for lineups
|
| 182 |
+
optimize_by: 'median' or 'ownership' - which metric to optimize for (higher is better)
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
DataFrame with optimized lineups
|
| 186 |
+
"""
|
| 187 |
+
# Create a copy to avoid modifying the original
|
| 188 |
+
optimized_frame = working_frame.copy()
|
| 189 |
+
|
| 190 |
+
# Optimize each row
|
| 191 |
+
for idx in optimized_frame.index:
|
| 192 |
+
row = optimized_frame.loc[idx]
|
| 193 |
+
optimized_row = optimize_single_lineup(
|
| 194 |
+
row=row,
|
| 195 |
+
player_columns=player_columns,
|
| 196 |
+
player_pool=projections_df,
|
| 197 |
+
map_dict=map_dict,
|
| 198 |
+
lock_teams=lock_teams if lock_teams else [],
|
| 199 |
+
type_var=type_var,
|
| 200 |
+
sport_var=sport_var,
|
| 201 |
+
salary_max=salary_max,
|
| 202 |
+
optimize_by=optimize_by
|
| 203 |
+
)
|
| 204 |
+
optimized_frame.loc[idx] = optimized_row
|
| 205 |
+
|
| 206 |
+
return optimized_frame
|
requirements.txt
CHANGED
|
@@ -5,4 +5,5 @@ numpy
|
|
| 5 |
rapidfuzz
|
| 6 |
matplotlib
|
| 7 |
scipy
|
| 8 |
-
pytz
|
|
|
|
|
|
| 5 |
rapidfuzz
|
| 6 |
matplotlib
|
| 7 |
scipy
|
| 8 |
+
pytz
|
| 9 |
+
ortools
|