Skip to content

Commit 66a5d37

Browse files
committed
DimaKudosh#53 Added constraint for restricting players from opposite teams
1 parent 60ef0ea commit 66a5d37

File tree

8 files changed

+143
-20
lines changed

8 files changed

+143
-20
lines changed

docs/constraints.rst

+19
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Constraints
1212
- Minimum salary cap.
1313
- Maximum repeating players.
1414
- Ownership projection constraint.
15+
- Team stacking
16+
- Restricting players from opposing teams
1517

1618
Number of players from same team
1719
--------------------------------
@@ -88,3 +90,20 @@ It accepts list with integers, each integer represents minimum number of players
8890
.. code-block:: python
8991
9092
optimizer.set_team_stacking([3, 3])
93+
94+
Restrict players from opposing team
95+
-----------------------------------
96+
In some cases you would want to restrict creating of lineup with players from opposing teams,
97+
for example prevent of pitchers and hitters from same game. For this you can use `restrict_positions_for_opposing_team`
98+
method of optimizer, it accepts 2 arguments with list of positions for one team and list of positions for another.
99+
100+
.. code-block:: python
101+
102+
optimizer.restrict_positions_for_opposing_team(['P'], ['1B', '2B', '3B'])
103+
104+
.. note::
105+
106+
This constraint works only when players has information about upcoming game and their opponents,
107+
in other case `LineupOptimizerException` will be raised. So it will not work in FantasyDraft
108+
(because they doesn't provide information about opponents) and if you write your custom players importer and
109+
don't pass `game_info` parameter in players constructors.

pydfs_lineup_optimizer/lineup_optimizer.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pydfs_lineup_optimizer.sites import SitesRegistry
99
from pydfs_lineup_optimizer.lineup_importer import CSVImporter
1010
from pydfs_lineup_optimizer.settings import BaseSettings, LineupPosition
11-
from pydfs_lineup_optimizer.player import LineupPlayer
11+
from pydfs_lineup_optimizer.player import LineupPlayer, GameInfo
1212
from pydfs_lineup_optimizer.utils import ratio, link_players_with_positions, get_remaining_positions
1313
from pydfs_lineup_optimizer.rules import *
1414

@@ -41,6 +41,7 @@ def __init__(self, settings, solver=PuLPSolver):
4141
self._max_projected_ownership = None # type: Optional[float]
4242
self._min_projected_ownership = None # type: Optional[float]
4343
self._team_stacks = None # type: Optional[List[int]]
44+
self._opposing_teams_position_restriction = None # type: Optional[Tuple[List[str], List[str]]]
4445

4546
@property
4647
def budget(self):
@@ -137,6 +138,16 @@ def team_stacks(self):
137138
# type: () -> Optional[List[int]]
138139
return self._team_stacks
139140

141+
@property
142+
def opposing_teams_position_restriction(self):
143+
# type: () -> Optional[Tuple[List[str], List[str]]]
144+
return self._opposing_teams_position_restriction
145+
146+
@property
147+
def games(self):
148+
# type: () -> FrozenSet[GameInfo]
149+
return frozenset(player.game_info for player in self.players if player.game_info)
150+
140151
def reset_lineup(self):
141152
self._lineup = []
142153
self._players_with_same_position = {}
@@ -377,6 +388,13 @@ def set_team_stacking(self, stacks):
377388
self.remove_rule(TeamStacksRule)
378389
self._team_stacks = stacks
379390

391+
def restrict_positions_for_opposing_team(self, first_team_positions, second_team_positions):
392+
# type: (List[str], List[str]) -> None
393+
if not self.games:
394+
raise LineupOptimizerException('Game Info isn\'t specified for players')
395+
self._opposing_teams_position_restriction = (first_team_positions, second_team_positions)
396+
self.add_new_rule(RestrictPositionsForOpposingTeams)
397+
380398
def optimize(self, n, max_exposure=None, randomness=False, with_injured=False):
381399
# type: (int, Optional[float], bool, bool) -> Generator[Lineup, None, None]
382400
params = locals()

pydfs_lineup_optimizer/lineup_printer.py

+26-12
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@ def print_lineup(self, lineup):
1212

1313

1414
class LineupPrinter(BaseLineupPrinter):
15+
OUTPUT_FORMAT = '{0:>2}. {1:<5} {2:<30}{3:<6}{4:<15}{5:<9}{6:<8}{7:<10}\n'
16+
17+
def _print_game_info(self, player):
18+
# type: ('LineupPlayer') -> str
19+
game_info = player.game_info
20+
if game_info:
21+
return '%s@%s' % (game_info.away_team, game_info.home_team)
22+
return ''
23+
1524
def _print_player(self, index, player):
1625
# type: (int, 'LineupPlayer') -> str
17-
return '{0:>2}. {1:<5} {2:<30}{3:<6}{4:<15}{5:<8}{6:<10}\n'.format(
26+
return self.OUTPUT_FORMAT.format(
1827
index,
1928
player.lineup_position,
2029
player.full_name,
2130
'/'.join(player.positions),
2231
player.team,
32+
self._print_game_info(player),
2333
round(player.fppg, 3),
2434
str(player.salary) + '$',
2535
)
@@ -33,20 +43,24 @@ def print_lineup(self, lineup):
3343
return res
3444

3545

36-
class DropLowestLineupPrinter(BaseLineupPrinter):
46+
class DropLowestLineupPrinter(LineupPrinter):
47+
def _print_player(self, index, player, is_dropped=False):
48+
return self.OUTPUT_FORMAT.format(
49+
index,
50+
player.lineup_position,
51+
'%s%s' % (player.full_name, '(DROPPED)' if is_dropped else ''),
52+
'/'.join(player.positions),
53+
player.team,
54+
self._print_game_info(player),
55+
round(player.fppg, 3),
56+
str(player.salary) + '$',
57+
)
58+
3759
def print_lineup(self, lineup):
3860
res = ''
39-
lowest_fppg_player = sorted(lineup, key=lambda player: player.fppg)[0]
61+
lowest_fppg_player = sorted(lineup, key=lambda p: p.fppg)[0]
4062
for index, player in enumerate(lineup.players, start=1):
41-
res += '{0:>2}. {1:<5} {2:<30}{3:<6}{4:<15}{5:<8}{6:<10}\n'.format(
42-
index,
43-
player.lineup_position,
44-
'%s%s' % (player.full_name, '(DROPPED)' if player == lowest_fppg_player else ''),
45-
'/'.join(player.positions),
46-
player.team,
47-
round(player.fppg, 3),
48-
str(player.salary) + '$',
49-
)
63+
res += self._print_player(index, player, is_dropped=lowest_fppg_player)
5064
res += 'Fantasy Points %.2f' % lineup.fantasy_points_projection
5165
res += '\nFantasy Points Without Dropped Player %.2f' % \
5266
(lineup.fantasy_points_projection - lowest_fppg_player.fppg)

pydfs_lineup_optimizer/rules.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import division
22
from collections import defaultdict
3-
from itertools import product, combinations, groupby
3+
from itertools import product, combinations, groupby, permutations
44
from math import ceil
55
from random import getrandbits, uniform
66
from typing import List, Dict, Any, Optional, TYPE_CHECKING
@@ -291,3 +291,22 @@ def apply(self, solver, players_dict):
291291
solver.add_constraint(variables, coefficients, SolverSign.GTE,
292292
stack * solver_variable)
293293
solver.add_constraint(combinations_variables, [1] * len(combinations_variables), SolverSign.GTE, total)
294+
295+
296+
class RestrictPositionsForOpposingTeams(OptimizerRule):
297+
def apply(self, solver, players_dict):
298+
if not self.optimizer.opposing_teams_position_restriction:
299+
return
300+
for game in self.optimizer.games:
301+
first_team_players = {player: variable for player, variable in players_dict.items()
302+
if player.team == game.home_team}
303+
second_team_players = {player: variable for player, variable in players_dict.items()
304+
if player.team == game.away_team}
305+
for first_team_positions, second_team_positions in \
306+
permutations(self.optimizer.opposing_teams_position_restriction, 2):
307+
first_team_variables = [variable for player, variable in first_team_players.items()
308+
if list_intersection(player.positions, first_team_positions)]
309+
second_team_variables = [variable for player, variable in second_team_players.items()
310+
if list_intersection(player.positions, second_team_positions)]
311+
for variables in product(first_team_variables, second_team_variables):
312+
solver.add_constraint(variables, [1, 1], SolverSign.LTE, 1)

pydfs_lineup_optimizer/sites/draftkings/classic/importer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def import_players(self): # pragma: no cover
7171
else:
7272
start_line += 1
7373

74-
def import_lineups(self, players):
74+
def import_lineups(self, players): # pragma: no cover
7575
with open(self.filename, 'r') as csv_file:
7676
lines = csv.reader(csv_file)
7777
try:

pydfs_lineup_optimizer/sites/fanduel/importer.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import csv
22
from pydfs_lineup_optimizer.exceptions import LineupOptimizerIncorrectCSV
33
from pydfs_lineup_optimizer.lineup_importer import CSVImporter
4-
from pydfs_lineup_optimizer.player import Player
4+
from pydfs_lineup_optimizer.player import Player, GameInfo
55
from pydfs_lineup_optimizer.sites.sites_registry import SitesRegistry
66
from pydfs_lineup_optimizer.constants import Site
77

@@ -15,6 +15,11 @@ def import_players(self): # pragma: no cover
1515
with open(self.filename, 'r') as csvfile:
1616
csv_data = csv.DictReader(csvfile, skipinitialspace=True)
1717
for row in csv_data:
18+
try:
19+
away_team, home_team = row.get('Game').split('@')
20+
game_info = GameInfo(home_team, away_team, None, False)
21+
except ValueError:
22+
game_info = None
1823
try:
1924
max_exposure = row.get('Max Exposure')
2025
player = Player(
@@ -26,7 +31,8 @@ def import_players(self): # pragma: no cover
2631
float(row['Salary']),
2732
float(row['FPPG']),
2833
True if row['Injury Indicator'].strip() else False,
29-
max_exposure=float(max_exposure.replace('%', '')) if max_exposure else None
34+
max_exposure=float(max_exposure.replace('%', '')) if max_exposure else None,
35+
game_info=game_info,
3036
)
3137
except KeyError:
3238
raise LineupOptimizerIncorrectCSV

pydfs_lineup_optimizer/sites/yahoo/importer.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import csv
22
from pydfs_lineup_optimizer.exceptions import LineupOptimizerIncorrectCSV
33
from pydfs_lineup_optimizer.lineup_importer import CSVImporter
4-
from pydfs_lineup_optimizer.player import Player
4+
from pydfs_lineup_optimizer.player import Player, GameInfo
55
from pydfs_lineup_optimizer.sites.sites_registry import SitesRegistry
66
from pydfs_lineup_optimizer.constants import Site
77

@@ -15,6 +15,11 @@ def import_players(self): # pragma: no cover
1515
with open(self.filename, 'r') as csvfile:
1616
csv_data = csv.DictReader(csvfile, skipinitialspace=True)
1717
for row in csv_data:
18+
try:
19+
away_team, home_team = row.get('Game', '').split('@')
20+
game_info = GameInfo(home_team, away_team, None, False)
21+
except ValueError:
22+
game_info = None
1823
try:
1924
max_exposure = row.get('Max Exposure')
2025
player = Player(
@@ -26,7 +31,8 @@ def import_players(self): # pragma: no cover
2631
float(row['Salary']),
2732
float(row['FPPG']),
2833
True if row['Injury Status'].strip() else False,
29-
max_exposure=float(max_exposure.replace('%', '')) if max_exposure else None
34+
max_exposure=float(max_exposure.replace('%', '')) if max_exposure else None,
35+
game_info=game_info,
3036
)
3137
except KeyError:
3238
raise LineupOptimizerIncorrectCSV

tests/test_rules.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import mock
44
from copy import deepcopy
55
from collections import Counter
6+
from datetime import datetime
67
from pydfs_lineup_optimizer import get_optimizer
78
from pydfs_lineup_optimizer.constants import Site, Sport
8-
from pydfs_lineup_optimizer.player import Player
9+
from pydfs_lineup_optimizer.player import Player, GameInfo
910
from pydfs_lineup_optimizer.exceptions import LineupOptimizerException
1011
from pydfs_lineup_optimizer.rules import ProjectedOwnershipRule
12+
from pydfs_lineup_optimizer.utils import list_intersection
1113
from .utils import create_players, load_players
1214

1315

@@ -303,3 +305,42 @@ def test_stack_greater_than_max_from_one_team(self):
303305
stacks = [5]
304306
with self.assertRaises(LineupOptimizerException):
305307
self.optimizer.set_team_stacking(stacks)
308+
309+
310+
class PositionsForOpposingTeamTestCase(unittest.TestCase):
311+
def setUp(self):
312+
first_game_info = GameInfo('HOU', 'BOS', datetime.now(), False)
313+
second_game_info = GameInfo('CHI', 'NY', datetime.now(), False)
314+
self.players = [
315+
Player(1, '1', '1', ['SP', 'RP'], 'HOU', 3000, 15, game_info=first_game_info),
316+
Player(2, '2', '2', ['SP', 'RP'], 'BOS', 3000, 15, game_info=first_game_info),
317+
Player(3, '3', '3', ['C'], 'HOU', 3000, 15, game_info=first_game_info),
318+
Player(4, '4', '4', ['1B'], 'BOS', 3000, 15, game_info=first_game_info),
319+
Player(5, '5', '5', ['2B'], 'HOU', 3000, 15, game_info=first_game_info),
320+
Player(6, '6', '6', ['3B'], 'BOS', 3000, 15, game_info=first_game_info),
321+
Player(7, '7', '7', ['SS'], 'HOU', 3000, 15, game_info=first_game_info),
322+
Player(8, '8', '8', ['OF'], 'BOS', 3000, 15, game_info=first_game_info),
323+
Player(9, '9', '9', ['OF'], 'HOU', 3000, 15, game_info=first_game_info),
324+
Player(10, '10', '10', ['OF'], 'BOS', 3000, 15, game_info=first_game_info),
325+
Player(11, '11', '11', ['SP', 'RP'], 'CHI', 3000, 5, game_info=second_game_info),
326+
Player(12, '12', '12', ['SP', 'RP'], 'NY', 3000, 5, game_info=second_game_info),
327+
]
328+
self.optimizer = get_optimizer(Site.DRAFTKINGS, Sport.BASEBALL)
329+
self.optimizer.load_players(self.players)
330+
331+
def test_restrict_positions_for_opposing_team_correctness(self):
332+
first_team_positions = ['SP', 'RP']
333+
second_team_positions = ['1B', '2B', '3B']
334+
self.optimizer.restrict_positions_for_opposing_team(first_team_positions, second_team_positions)
335+
lineup = next(self.optimizer.optimize(1))
336+
pitcher_games = {player.game_info for player in lineup
337+
if list_intersection(player.positions, first_team_positions)}
338+
hitters_games = {player.game_info for player in lineup
339+
if list_intersection(player.positions, second_team_positions)}
340+
self.assertFalse(pitcher_games.intersection(hitters_games))
341+
342+
def test_restrict_positions_if_game_not_specified(self):
343+
for player in self.players:
344+
player.game_info = None
345+
with self.assertRaises(LineupOptimizerException):
346+
self.optimizer.restrict_positions_for_opposing_team(['SP', 'RP'], ['1B'])

0 commit comments

Comments
 (0)