Skip to content

Commit

Permalink
DimaKudosh#298 Add strict_depend parameter to conditional stack
Browse files Browse the repository at this point in the history
  • Loading branch information
dimakudosh authored and dimakudosh committed Sep 25, 2021
1 parent c223ff9 commit 5cad6f1
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 35 deletions.
43 changes: 36 additions & 7 deletions docs/rules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Rules
- Spacing for positions
- Total teams
- Minimum starters
- Teams exposures
- Stacking rules

Number of players from same team
Expand Down Expand Up @@ -140,12 +141,14 @@ For example if you want to restrict optimizer to select players within specific
Total teams
-----------

It's also possible to set exact number of teams that will be presented in generated lineups,
you can set it using `set_total_teams` method.
You can control the total number of teams used in lineups using `set_total_teams` method.

.. code-block:: python
optimizer.set_total_teams(4)
optimizer.set_total_teams(min_teams=4) # At least 4 teams should be in the lineup
optimizer.set_total_teams(max_teams=6) # Maximum 6 teams should be in the lineup
optimizer.set_total_teams(min_teams=5, max_teams=6) # 5 or 6 teams should be in the lineup
optimizer.set_total_teams(min_teams=5, max_teams=5) # Exact 5 teams should be in the lineup
Minimum starters
----------------
Expand All @@ -158,6 +161,19 @@ add `Confirmed Starter` column to csv.
optimizer.set_min_starters(4)
Teams exposures
---------------

You can set max exposures for each team it means that players from teams can be used only in a limited number of lineups.
The team counted as used in the lineup if at least one player from it is in the lineup.

.. code-block:: python
optimizer.set_teams_max_exposures(0.5) # Set 0.5 exposure for all teams
optimizer.set_teams_max_exposures(0.5, exposures_by_team={'NYY': 0.8}) # Set 0.5 exposure for all teams except NYY and 0.8 exposure for NYY
optimizer.set_teams_max_exposures(exposures_by_team={'NYY': 0.8}) # Set 0.5 exposure only for NYY
optimizer.set_teams_max_exposures(exposures_by_team={'NYY': 0.5, 'NYM': 0.5}, exposure_strategy=AfterEachExposureStrategy) # Use another exposure strategy
Stacking
========
Expand All @@ -168,7 +184,7 @@ Here is a list of available types of stacks:

Team stack
----------
You can set how many players from same team will be in lineup, for this you can use `TeamStack`.
You can set how many players from the same team will be in the lineup, for this you can use `TeamStack`.
Here are examples of using it:

.. code-block:: python
Expand All @@ -182,7 +198,7 @@ Here are examples of using it:
Positions stack
---------------
You also can add stack with known list of positions for players used in stack.
You also can add a stack with a known list of positions for players used in the stack.
Here are examples of using it:

.. code-block:: python
Expand All @@ -193,6 +209,15 @@ Here are examples of using it:
optimizer.add_stack(PositionsStack(['QB', 'WR'], max_exposure=0.5)) # stack QB and WR with 0.5 exposure for all team stacks
optimizer.add_stack(PositionsStack(['QB', 'WR'], max_exposure=0.5, max_exposure_per_team={'MIA': 0.6})) # stack QB and WR with 0.5 exposure for all team stacks and 0.6 exposure for MIA
Game stack
---------------
You can set how many players from the same game will be in the lineup, for this you can use `GameStack`.

.. code-block:: python
optimizer.add_stack(GameStack(3)) # stack 3 players from the same game
optimizer.add_stack(GameStack(5, min_from_team=2)) # stack 5 players from the same game, 3 from one team and 2 from another
Custom stack
------------
You can create your custom stacks as well. Here is example of creating custom stack so optimizer will
Expand Down Expand Up @@ -223,11 +248,15 @@ You can use this method for ungrouping players as well. In this example maximum
Also you can apply these groups conditionally based on another player selection.
In the example below one of Travis Kelce or Tyreek Hill will be added to the lineup only with Patrick Mahomes or none of them will be added to the lineup.
You can allow generating lineups with the provided group when the dependent player is not in the lineup,
for this you can set optional argument `strict_depend` to `False`.

.. code-block:: python
group = PlayersGroup(
[optimizer.get_player_by_name(name) for name in ('Tyreek Hill', 'Travis Kelce')],
depends_on=optimizer.get_player_by_name('Patrick Mahomes')
optimizer.player_pool.get_players('Tyreek Hill', 'Travis Kelce'),
max_from_group=1,
depends_on=optimizer.player_pool.get_player_by_name('Patrick Mahomes'),
strict_depend=False, # if you want to generate lineups with Hill/Kelce but without Mahomes
)
optimizer.add_players_group(group)
36 changes: 22 additions & 14 deletions pydfs_lineup_optimizer/player_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,17 +159,22 @@ def add_player(self, player: Player):
self._players_by_name[player.full_name].append(player)
self._players_by_id[player.id] = player

def get_player_by_name(self, player_name: str, position: Optional[str] = None) -> Optional[Player]:
players = self._players_by_name.get(player_name, [])
def get_player_by_name(
self, player_name: str, position: Optional[str] = None,
allowed_players: Optional[Set[Player]] = None,
) -> Optional[Player]:
players = set(self._players_by_name.get(player_name, set()))
if position:
players = [player for player in players if position in player.positions]
players = {player for player in players if position in player.positions}
if allowed_players:
players = players.intersection(allowed_players)
if players:
if len(players) > 1:
raise LineupOptimizerException('More than 1 player is found')
return players[0]
raise LineupOptimizerException('More than 1 player is found for: %s' % player_name)
return list(players)[0]
if self.search_threshold:
players = self.get_players(PlayerFilter(positions=[position])) if position else self.all_players
possibilities = [(player, ratio(player_name, player.full_name)) for player in players]
matched_players = self.get_players(PlayerFilter(positions=[position])) if position else self.all_players
possibilities = [(player, ratio(player_name, player.full_name)) for player in matched_players]
filtered_possibilities = filter(lambda pos: pos[1] >= self.search_threshold, possibilities)
if filtered_possibilities:
sorted_players = sorted(filtered_possibilities, key=lambda pos: -pos[1])
Expand Down Expand Up @@ -249,23 +254,26 @@ def get_players(self, *players_and_groups: Union[DirtyPlayer, PlayerFilter]) ->
filters.append(item)
else:
players.append(item)
allowed_players = None
if filters:
allowed_players = {
player for player in self.all_players if
all(player_filter.filter(player, False) for player_filter in filters)
}
if players:
for player in players:
cleaned_player = self._clean_player(player)
cleaned_player = self._clean_player(player, allowed_players=allowed_players)
if cleaned_player:
result.append(cleaned_player)
else:
result = self.all_players
if filters:
result = [player for player in result if all(player_filter.filter(player, False) for player_filter in filters)]
return result

def add_filters(self, *filters: BaseFilter):
self._player_filters.extend(filters)

def _clean_player(self, player: DirtyPlayer) -> Player:
def _clean_player(self, player: DirtyPlayer, allowed_players: Optional[Set[Player]] = None) -> Player:
if not isinstance(player, Player):
cleaned_player = self.get_player_by_id(player) or self.get_player_by_name(player)
cleaned_player = self.get_player_by_id(player) or self.get_player_by_name(
player, allowed_players=allowed_players)
if not cleaned_player:
raise LineupOptimizerException('%s is not found' % player)
return cleaned_player
Expand Down
9 changes: 5 additions & 4 deletions pydfs_lineup_optimizer/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,11 @@ def _create_constraints(self, solver: Solver,):
[total_players_var], None, SolverSign.GTE,
depend_var * (sub_group.min_from_group or 1)
)
solver.add_constraint(
[total_players_var], None, SolverSign.LTE,
depend_var * (sub_group.max_from_group or len(variables))
)
if group.strict_depend:
solver.add_constraint(
[total_players_var], None, SolverSign.LTE,
depend_var * (sub_group.max_from_group or len(variables))
)
if combinations_variables:
solver.add_constraint(combinations_variables.values(), None, SolverSign.GTE, 1)
for player, stacks_vars in players_in_stack.items():
Expand Down
4 changes: 4 additions & 0 deletions pydfs_lineup_optimizer/stacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ def __init__(
depends_on: Optional[Player] = None,
min_from_group: Optional[int] = None,
max_from_group: Optional[int] = None,
strict_depend: bool = True,
parent: Optional['BaseGroup'] = None,
):
self.uuid = uuid4()
self.max_exposure = max_exposure
self.parent = parent
self.depends_on = depends_on
self.strict_depend = strict_depend
self.min_from_group = min_from_group
self.max_from_group = max_from_group

Expand All @@ -55,6 +57,7 @@ def __init__(
max_from_group: Optional[int] = None,
max_exposure: Optional[float] = None,
depends_on: Optional[Player] = None,
strict_depend: bool = True,
parent: Optional[BaseGroup] = None,
):
if min_from_group is None and max_from_group is None:
Expand All @@ -65,6 +68,7 @@ def __init__(
min_from_group=min_from_group,
max_from_group=max_from_group,
depends_on=depends_on,
strict_depend=strict_depend,
)
self.players = list(set(players))

Expand Down
29 changes: 19 additions & 10 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,16 +665,25 @@ def test_group_player_max_from_group_max_exposure(self):
self.assertEqual(len([player for player in self.high_fppg_group if player in lineups[1]]), 0)

@parameterized.expand([
(True, 1, None, None, 3),
(True, 1, 1, None, 3),
(True, 1, None, 1, 2),
(True, -2000, None, None, 0),
(True, -2000, 1, None, 0),
(False, 1000, None, None, 3),
(False, 1000, None, 1, 2),
(False, 1000, 1, None, 2),
(False, True, 1, None, None, 2),
(False, True, 1, 1, None, 2),
(False, True, 1, None, 1, 1),
(False, True, -2000, None, None, 2),
(False, True, -2000, 1, None, 2),
(False, False, 1000, None, None, 3),
(False, False, 1000, None, 1, 2),
(False, False, 1000, 1, None, 2),
(False, False, -2000, 1, None, 0),
(True, True, 1, None, None, 3),
(True, True, 1, 1, None, 3),
(True, True, 1, None, 1, 2),
(True, True, -2000, None, None, 0),
(True, True, -2000, 1, None, 0),
(True, False, 1000, None, None, 3),
(True, False, 1000, None, 1, 2),
(True, False, 1000, 1, None, 2),
])
def test_group_player_with_conditional_player(self, is_high_fppg_group, fppg, min_from_group,
def test_group_player_with_conditional_player(self, strict_depend, is_high_fppg_group, fppg, min_from_group,
max_from_group, total):
if is_high_fppg_group:
group = self.high_fppg_group
Expand All @@ -684,7 +693,7 @@ def test_group_player_with_conditional_player(self, is_high_fppg_group, fppg, mi
self.player_pool.extend_players([dependant_player])
self.optimizer.add_players_group(
PlayersGroup(group, depends_on=dependant_player, min_from_group=min_from_group,
max_from_group=max_from_group)
max_from_group=max_from_group, strict_depend=strict_depend)
)
lineup = next(self.optimizer.optimize(1))
expect_players = [*group, dependant_player]
Expand Down

0 comments on commit 5cad6f1

Please sign in to comment.