DFS_Portfolio_Manager / global_func /exposure_spread.py
James McCool
Adding exclusions from exposure management, fixing some database query stuff
67eb3a1
import random
import numpy as np
import math
#### Goal is to choose a player and adjust the amount of lineups that have them
#### First thing you need to do is find comparable players in the projections, so any player in the projections that is within $500 of the player and within 10% of the projection
#### Take that list of players and create a list that can be accessed for random insertion into the portfolio
#### Find the player and the amount of rows that contain them and then find an exposure rate which is the percentage of total rows
#### Use the exposure target argument and try to replace the player from as many rows as necessary to be at or just under the target
#### makes sure to check if the player is eligible for the position before replacing them
def check_nba_position_eligibility(column_name, player_positions):
if 'PG' in column_name:
return 'PG' in player_positions
elif 'SG' in column_name:
return 'SG' in player_positions
elif 'SF' in column_name:
return 'SF' in player_positions
elif 'PF' in column_name:
return 'PF' in player_positions
elif 'C' in column_name:
return 'C' in player_positions
elif 'G' in column_name:
return any(pos in ['PG', 'SG'] for pos in player_positions)
elif 'F' in column_name:
return any(pos in ['SF', 'PF'] for pos in player_positions)
elif 'UTIL' in column_name:
return True # UTIL can be any position
return False
def check_lol_position_eligibility(column_name, player_positions):
if 'TOP' in column_name:
return 'TOP' in player_positions
elif 'JNG' in column_name:
return 'JNG' in player_positions
elif 'MID' in column_name:
return 'MID' in player_positions
elif 'ADC' in column_name:
return 'ADC' in player_positions
elif 'SUP' in column_name:
return 'SUP' in player_positions
elif 'Team' in column_name:
return 'Team' in player_positions
elif 'CPT' in column_name:
return any(pos in ['TOP', 'JNG', 'MID', 'ADC', 'SUP'] for pos in player_positions)
return False
def check_mlb_position_eligibility(column_name, player_positions):
if any(pos in column_name for pos in ['P', 'SP', 'RP']):
return any(pos in ['P', 'SP', 'RP'] for pos in player_positions)
elif 'C' in column_name:
return 'C' in player_positions
elif '1B' in column_name:
return '1B' in player_positions
elif '2B' in column_name:
return '2B' in player_positions
elif '3B' in column_name:
return '3B' in player_positions
elif 'SS' in column_name:
return 'SS' in player_positions
elif 'OF' in column_name:
return 'OF' in player_positions
return False
def check_nfl_position_eligibility(column_name, player_positions):
if 'QB' in column_name:
return 'QB' in player_positions
elif 'RB' in column_name:
return 'RB' in player_positions
elif 'WR' in column_name:
return 'WR' in player_positions
elif 'TE' in column_name:
return 'TE' in player_positions
elif 'DST' in column_name:
return 'DST' in player_positions
elif 'FLEX' in column_name:
return any(pos in ['RB', 'WR', 'TE'] for pos in player_positions)
elif 'UTIL' in column_name:
return any(pos in ['RB', 'WR', 'TE'] for pos in player_positions)
return False
def check_golf_position_eligibility(column_name, player_positions):
if 'FLEX' in column_name:
return any(pos in ['G'] for pos in player_positions)
return True
def check_tennis_position_eligibility(column_name, player_positions):
if 'FLEX' in column_name:
return any(pos in ['T'] for pos in player_positions)
return True
def check_mma_position_eligibility(column_name, player_positions):
if 'FLEX' in column_name:
return any(pos in ['F'] for pos in player_positions)
return True
def check_nascar_position_eligibility(column_name, player_positions):
if 'FLEX' in column_name:
return any(pos in ['D'] for pos in player_positions)
return True
def check_ncaaf_position_eligibility(column_name, player_positions):
if 'QB' in column_name:
return 'QB' in player_positions
elif 'RB' in column_name:
return 'RB' in player_positions
elif 'WR' in column_name:
return 'WR' in player_positions
elif 'FLEX' in column_name:
return any(pos in ['RB', 'WR'] for pos in player_positions)
elif 'SFLEX' in column_name:
return any(pos in ['RB', 'WR', 'QB'] for pos in player_positions)
return False
def check_nhl_position_eligibility(column_name, player_positions):
if 'C' in column_name:
return 'C' in player_positions
elif 'W' in column_name:
return 'W' in player_positions
elif 'D' in column_name:
return 'D' in player_positions
elif 'G' in column_name:
return 'G' in player_positions
elif 'FLEX' in column_name:
return any(pos in ['C', 'W', 'D'] for pos in player_positions)
elif 'UTIL' in column_name:
return any(pos in ['C', 'W', 'D'] for pos in player_positions)
return False
def check_position_eligibility(sport, column_name, player_positions):
if sport == 'NBA':
return check_nba_position_eligibility(column_name, player_positions)
elif sport == 'MLB':
return check_mlb_position_eligibility(column_name, player_positions)
elif sport == 'NFL':
return check_nfl_position_eligibility(column_name, player_positions)
elif sport == 'NHL':
return check_nhl_position_eligibility(column_name, player_positions)
elif sport == 'MMA':
return check_mma_position_eligibility(column_name, player_positions)
elif sport == 'GOLF':
return check_golf_position_eligibility(column_name, player_positions)
elif sport == 'TENNIS':
return check_tennis_position_eligibility(column_name, player_positions)
elif sport == 'LOL':
return check_lol_position_eligibility(column_name, player_positions)
else:
# Default fallback - assume exact position match
return column_name in player_positions
def get_effective_salary(player_name, column_name, projections_df, type_var):
"""Calculate the effective salary for a player in a specific column"""
base_salary = projections_df[projections_df['player_names'] == player_name]['salary'].iloc[0]
if type_var != 'Classic' and column_name == 'CPT':
return base_salary * 1.5
return base_salary
def find_player_column(player_name, row_data, working_columns):
"""Find which column contains the specified player"""
for col in working_columns:
if row_data[col] == player_name:
return col
return None
def calculate_salary_difference(current_player, new_player, column_name, projections_df, type_var):
"""Calculate the salary difference when replacing current_player with new_player in a specific column"""
current_salary = get_effective_salary(current_player, column_name, projections_df, type_var)
new_salary = get_effective_salary(new_player, column_name, projections_df, type_var)
return new_salary - current_salary
def check_salary_eligibility(current_lineup_salary, current_player, new_player, column_name, projections_df, type_var, salary_max):
"""Check if replacing current_player with new_player in the specified column stays within salary cap"""
salary_diff = calculate_salary_difference(current_player, new_player, column_name, projections_df, type_var)
return current_lineup_salary + salary_diff <= salary_max
def exposure_spread(working_frame, exposure_player, exposure_target, comp_salary_below, comp_salary_above, ignore_stacks, remove_teams, specific_replacements, specific_exclusions, specific_columns, projections_df, sport_var, type_var, salary_max, stacking_sports):
if specific_exclusions != []:
projections_df = projections_df[~projections_df['player_names'].isin(specific_exclusions)]
comparable_players = projections_df[projections_df['player_names'] == exposure_player]
comparable_players = comparable_players.reset_index(drop=True)
comp_salary_high_base = comparable_players['salary'][0]
if type_var == 'Showdown':
comp_salary_low = comparable_players['salary'][0] + comp_salary_below
comp_salary_high = comparable_players['salary'][0] + comp_salary_above
else:
comp_salary_low = comparable_players['salary'][0] + comp_salary_below
comp_salary_high = comparable_players['salary'][0] + comp_salary_above
comp_projection_high = comparable_players['median'][0]
if type_var == 'Showdown':
comp_projection_low = comparable_players['median'][0] - (comparable_players['median'][0] * .5)
else:
comp_projection_low = comparable_players['median'][0] - (comparable_players['median'][0] * .5)
# players can be eligible at multiple positions, so we need to find all the positions the player is eligible at
# the position column can have positions designated as 1B/OF which means they are eligible at 1B and OF
comp_player_position = comparable_players['position'].tolist()
comp_team = comparable_players['team'].tolist()
try:
comp_player_position = [pos.split('/') for pos in comp_player_position]
comp_player_position = [item for sublist in comp_player_position for item in sublist]
comp_player_position = list(set(comp_player_position))
except:
comp_player_position = comparable_players['position'].tolist()
def has_position_overlap(player_positions, target_positions):
player_pos_list = player_positions.split('/')
return any(pos in target_positions for pos in player_pos_list)
# find the exposure rate of the player in the working frame
if specific_columns != []:
player_mask = working_frame[specific_columns].apply(
lambda row: exposure_player in list(row), axis=1
)
else:
player_mask = working_frame[working_frame.columns].apply(
lambda row: exposure_player in list(row), axis=1
)
if specific_columns != []:
replace_mask = working_frame[specific_columns].apply(
lambda row: exposure_player not in list(row), axis=1
)
else:
replace_mask = working_frame[working_frame.columns].apply(
lambda row: exposure_player not in list(row), axis=1
)
player_exposure = player_mask.sum() / len(working_frame)
replace_exposure = replace_mask.sum() / len(working_frame)
# find the number of lineups that need to be removed to reach the target exposure
if exposure_target == 0:
lineups_to_remove = (player_exposure * len(working_frame))
else:
lineups_to_remove = ((player_exposure - exposure_target) * len(working_frame)) * 1.01
lineups_to_add = ((exposure_target - player_exposure) * len(working_frame)) * 1.10
# isolate the rows that contain the player
player_rows = working_frame[player_mask]
replace_rows = working_frame[replace_mask]
if ignore_stacks != []:
player_rows = player_rows[~player_rows['Stack'].isin(ignore_stacks)]
replace_rows = replace_rows[~replace_rows['Stack'].isin(ignore_stacks)]
change_counter = 0
random_row_indices_insert = list(player_rows.index)
random_row_indices_replace = list(replace_rows.index)
random.shuffle(random_row_indices_insert)
random.shuffle(random_row_indices_replace)
# for each row to the the number of lineups to remove, replace with random choice from comparable player list if they can be inserted
# we will need to use two separate functions here, one for an exposure player who has a lineups to remove above 0 and one for below 0
# key concept here is if they have a lineups to remove above 0 it means that we are trying to replace them with comparable players
# if the lineups to remove is below zero it means we want to find comparable players and replace them with the exposure player
if lineups_to_remove > 0:
# Keep trying until we've made enough successful replacements
while change_counter < math.ceil(lineups_to_remove) and random_row_indices_insert:
# Get the next row to try
row = random_row_indices_insert.pop(0)
if specific_replacements != []:
comparable_players = projections_df[(projections_df['player_names'].isin(specific_replacements)) &
(projections_df['salary'] <= comp_salary_high_base + (salary_max - working_frame['salary'][row]))
]
else:
comparable_players = projections_df[
(projections_df['salary'] >= comp_salary_low) &
(projections_df['salary'] <= comp_salary_high_base + (salary_max - working_frame['salary'][row])) &
(projections_df['median'] >= comp_projection_low) &
(projections_df['position'].apply(lambda x: has_position_overlap(x, comp_player_position)))
]
if exposure_target == 0:
comparable_players = comparable_players[comparable_players['player_names'] != exposure_player]
if remove_teams is not None:
remove_mask = comparable_players.apply(
lambda row: not any(team in list(row) for team in remove_teams), axis=1
)
comparable_players = comparable_players[remove_mask]
# Get the current row data to check for existing players
if specific_columns != []:
current_row_data = working_frame.iloc[row][specific_columns]
else:
current_row_data = working_frame.iloc[row]
# Filter out players that are already present in this row
existing_players = set(current_row_data.values)
try:
comparable_players = comparable_players[~comparable_players['player_names'].isin(existing_players)]
comparable_player_list = comparable_players['player_names'].tolist()
except:
comparable_player_list = []
if comparable_player_list:
# Find which column contains the exposure_player
if specific_columns != []:
row_data = working_frame.iloc[row][specific_columns]
working_columns = specific_columns
else:
row_data = working_frame.iloc[row]
working_columns = working_frame.columns
# Track if we successfully made a replacement
replacement_made = False
# For exposure_target == 0, replace ALL occurrences of exposure_player in this row
if exposure_target == 0:
for col in working_columns:
if row_data[col] == exposure_player:
# Try to find a suitable replacement for this specific column
suitable_replacements = []
for candidate in comparable_player_list:
# Get the replacement player's positions
replacement_player_positions = projections_df[projections_df['player_names'] == candidate]['position'].iloc[0].split('/')
# Check if the replacement player is eligible for this column
if type_var == 'Classic':
if check_position_eligibility(sport_var, col, replacement_player_positions):
suitable_replacements.append(candidate)
else:
# For non-Classic types, check salary eligibility using helper function
current_lineup_salary = working_frame.iloc[row]['salary']
if check_salary_eligibility(current_lineup_salary, exposure_player, candidate, col, projections_df, type_var, salary_max):
suitable_replacements.append(candidate)
if suitable_replacements:
insert_player = random.choice(suitable_replacements)
working_frame.at[row, col] = insert_player
replacement_made = True
# Remove this player from the list to avoid duplicates in the same row
comparable_player_list = [p for p in comparable_player_list if p != insert_player]
else:
for col in working_columns:
if row_data[col] == exposure_player:
insert_player = random.choice(comparable_player_list)
# Get the replacement player's positions
replacement_player_positions = projections_df[projections_df['player_names'] == insert_player]['position'].iloc[0].split('/')
# Check if the replacement player is eligible for this column
if type_var == 'Classic':
if check_position_eligibility(sport_var, col, replacement_player_positions):
working_frame.at[row, col] = insert_player
replacement_made = True
break
else:
# For non-Classic types, check salary eligibility using helper function
current_lineup_salary = working_frame.iloc[row]['salary']
if check_salary_eligibility(current_lineup_salary, exposure_player, insert_player, col, projections_df, type_var, salary_max):
working_frame.at[row, col] = insert_player
replacement_made = True
break
# Only increment counter if we actually made a replacement
if replacement_made:
change_counter += 1
# If we've run out of rows to try, break to avoid infinite loop
if not random_row_indices_insert:
break
else:
while change_counter < math.ceil(lineups_to_add) and random_row_indices_replace:
row = random_row_indices_replace.pop(0)
if specific_replacements != []:
comparable_players = projections_df[(projections_df['player_names'].isin(specific_replacements))
]
else:
if type_var == 'Showdown':
comparable_players = projections_df[
(projections_df['salary'] >= comp_salary_low) &
(projections_df['salary'] <= comp_salary_high_base + (salary_max - working_frame['salary'][row]))
]
else:
comparable_players = projections_df[
(projections_df['salary'] >= comp_salary_low) &
(projections_df['salary'] <= comp_salary_high_base + (salary_max - working_frame['salary'][row])) &
(projections_df['position'].apply(lambda x: has_position_overlap(x, comp_player_position)))
]
if sport_var in stacking_sports:
if working_frame.iloc[row]['Size'] == 5 and comp_team != working_frame.iloc[row]['Stack']:
remove_mask = comparable_players.apply(
lambda player_row: not any(team in list(player_row) for team in [working_frame.iloc[row]['Stack']]), axis=1
)
comparable_players = comparable_players[remove_mask]
if remove_teams is not None:
remove_mask = comparable_players.apply(
lambda row: not any(team in list(row) for team in remove_teams), axis=1
)
comparable_players = comparable_players[remove_mask]
comparable_players = comparable_players[comparable_players['player_names'] != exposure_player]
# Create a list of comparable players
comparable_player_list = comparable_players['player_names'].tolist()
if exposure_player in working_frame.iloc[row].values:
comparable_player_list = []
if comparable_player_list:
# Find which column contains the exposure_player
if specific_columns != []:
row_data = working_frame.iloc[row][specific_columns]
working_columns = specific_columns
else:
row_data = working_frame.iloc[row]
working_columns = working_frame.columns
for col in working_columns:
if row_data[col] in comparable_player_list:
current_lineup_salary = working_frame.iloc[row]['salary']
current_player = row_data[col]
# Check salary eligibility using helper function
if check_salary_eligibility(current_lineup_salary, current_player, exposure_player, col, projections_df, type_var, salary_max):
if type_var == 'Classic':
# For Classic types, also check position eligibility
exposure_player_positions = projections_df[projections_df['player_names'] == exposure_player]['position'].iloc[0].split('/')
if check_position_eligibility(sport_var, col, exposure_player_positions):
working_frame.at[row, col] = exposure_player
change_counter += 1
break
else:
# For non-Classic types, salary check is sufficient (position eligibility handled elsewhere)
working_frame.at[row, col] = exposure_player
change_counter += 1
break
return working_frame