Skip to content

Commit

Permalink
Add MIP solver
Browse files Browse the repository at this point in the history
  • Loading branch information
dimakudosh authored and dimakudosh committed Jul 8, 2021
1 parent 4a766a1 commit 544d43f
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 61 deletions.
58 changes: 33 additions & 25 deletions docs/performance-and-optimization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,49 @@
Performance and Optimization
============================

Sometimes optimization process takes a lot of time to generate single lineup.
It usually happens in mlb and nfl because all teams plays in same day and each team has a lot of players and total
number of players used in optimization is >500. In this case a good approach is to remove from optimization players with
small fppg value and big salary.
Solvers
-------

By default, the optimizer uses `pulp <https://coin-or.github.io/pulp/index.html>`_ library under the hood with a default solver that free but slow.
You can change it to another solver that pulp supports.
Here is an example of how to change the default solver for GLPK solver:

.. code-block:: python
optimizer = get_optimizer(Site.DRAFTKINGS, Sport.BASEBALL)
optimizer.load_players_from_csv('dk_mlb.csv')
for player in optimizer.players:
if player.efficiency == 0:
optimizer.remove_player(player)
for lineup in optimizer.optimize(10):
print(lineup)
# install glpk: https://www.gnu.org/software/glpk/
from pulp import GLPK_CMD # You can find names of other solvers in pulp docs
from pydfs_lineup_optimizer.solvers import PuLPSolver
Optimizer parameters tuning
---------------------------
class GLPKPuLPSolver(PuLPSolver):
LP_SOLVER = GLPK_CMD(path='<path to installed glpk solver>', msg=False)
optimizer = get_optimizer(Site.DRAFTKINGS, Sport.BASEBALL, solver=GLPKPuLPSolver)
For some special cases with a lot of different constraints you can try to tune solver parameters.
`pydfs-lineup-optimizer` uses `PuLP` library for solving optimization problem, by default it uses CBC solver so you can
try to change default parameters. You can find list of available parameters `here
<https://www.gams.com/latest/docs/S_CBC.html>`_.
This is example of tuning parameters:
Also, the library supports another solver library: `mip <https://www.python-mip.com/>`_.
It can be faster in some cases, especially if you are using pypy (`benchmark <https://docs.python-mip.com/en/latest/bench.html>`_).
For you using mip you should install it via pip: pip install mip.
After that you can replace pulp:

.. code-block:: python
from pulp.solvers import PULP_CBC_CMD
from pydfs_lineup_optimizer import get_optimizer, Site, Sport
from pydfs_lineup_optimizer.solvers.pulp_solver import PuLPSolver
from pydfs_lineup_optimizer.solvers.mip_solver import MIPSolver
optimizer = get_optimizer(Site.DRAFTKINGS, Sport.BASEBALL, solver=MIPSolver)
class CustomPuLPSolver(PuLPSolver):
LP_SOLVER = PULP_CBC_CMD(threads=8, options=['preprocess off'])
Decrease solving complexity
---------------------------

Sometimes optimization process takes a lot of time to generate a single lineup.
It usually happens in mlb and nfl because all teams play on the same day and each team has a lot of players and a total
number of players used in optimization is >100. In this case, a good approach is to remove from optimization players with
small fppg value and big salary.

optimizer = get_optimizer(Site.DRAFTKINGS, Sport.BASEBALL, solver=CustomPuLPSolver)
.. code-block:: python
You can try to change solver as well for any solver that `PuLP` support: glpk, cplex, gurobi etc.
optimizer = get_optimizer(Site.DRAFTKINGS, Sport.BASEBALL)
optimizer.load_players_from_csv('dk_mlb.csv')
for player in optimizer.players:
if player.efficiency < 1: # efficiency = fppg / salary
optimizer.remove_player(player)
for lineup in optimizer.optimize(100):
print(lineup)
2 changes: 1 addition & 1 deletion pydfs_lineup_optimizer/lineup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def __repr__(self):
return 'Lineup: projection %s, budget %s' % (self.fantasy_points_projection, self.salary_costs)

def __hash__(self):
return hash(sorted(p.id for p in self))
return hash(tuple(sorted(p.id for p in self)))

def __eq__(self, other):
return sorted(p.id for p in self) == sorted(p.id for p in other)
Expand Down
9 changes: 6 additions & 3 deletions pydfs_lineup_optimizer/lineup_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pydfs_lineup_optimizer.exposure_strategy import BaseExposureStrategy, TotalExposureStrategy
from pydfs_lineup_optimizer.fantasy_points_strategy import BaseFantasyPointsStrategy, StandardFantasyPointsStrategy, \
RandomFantasyPointsStrategy
from pydfs_lineup_optimizer.solvers import get_default_solver


BASE_RULES = {TotalPlayersRule, LineupBudgetRule, PositionsRule, MaxFromOneTeamRule, LockedPlayersRule,
Expand All @@ -26,7 +27,7 @@


class LineupOptimizer:
def __init__(self, settings: Type[BaseSettings], solver: Type[Solver] = PuLPSolver):
def __init__(self, settings: Type[BaseSettings], solver: Type[Solver] = get_default_solver()):
self._settings = settings()
self._csv_importer = None # type: Optional[Type[CSVImporter]]
self._rules = BASE_RULES.copy() # type: Set[Type[OptimizerRule]]
Expand Down Expand Up @@ -380,7 +381,8 @@ def optimize(
base_solver = self._solver_class()
base_solver.setup_solver()
players_dict = OrderedDict(
[(player, base_solver.add_variable('Player_%d' % i)) for i, player in enumerate(players)])
[(player, base_solver.add_variable(base_solver.build_player_var_name(player, str(i))))
for i, player in enumerate(players)])
variables_dict = {v: k for k, v in players_dict.items()}
constraints = [constraint(self, players_dict, context) for constraint in rules]
for constraint in constraints:
Expand Down Expand Up @@ -441,7 +443,8 @@ def optimize_lineups(
base_solver = self._solver_class()
base_solver.setup_solver()
players_dict = OrderedDict(
[(player, base_solver.add_variable('Player_%d' % i)) for i, player in enumerate(players)])
[(player, base_solver.add_variable(base_solver.build_player_var_name(player, str(i))))
for i, player in enumerate(players)])
variables_dict = {v: k for k, v in players_dict.items()}
constraints = [constraint(self, players_dict, context) for constraint in rules]
for constraint in constraints:
Expand Down
25 changes: 18 additions & 7 deletions pydfs_lineup_optimizer/player.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from collections import namedtuple
from datetime import datetime
from pytz import timezone
from typing import List, Optional
from typing import List, Optional, Tuple, Sequence
from pydfs_lineup_optimizer.tz import get_timezone


Expand Down Expand Up @@ -48,7 +47,7 @@ def __init__(self,
self.id = player_id
self.first_name = first_name
self.last_name = last_name
self.positions = positions
self.positions = positions # type: ignore
self.team = team
self.salary = salary
self.fppg = fppg
Expand All @@ -64,16 +63,16 @@ def __init__(self,
self.fppg_floor = fppg_floor
self.fppg_ceil = fppg_ceil
self.progressive_scale = progressive_scale
self._original_positions = original_positions
self.original_positions = original_positions # type: ignore

def __repr__(self):
return '%s %s (%s)' % (self.full_name, '/'.join(self.positions), self.team)

def __hash__(self):
return hash((self.id, tuple(sorted(self.positions))))
return hash((self.id, self.positions))

def __eq__(self, other):
return (self.id, tuple(sorted(self.positions))) == (other.id, tuple(sorted(self.positions)))
return (self.id, self.positions) == (other.id, other.positions)

@property
def full_name(self) -> str:
Expand All @@ -95,9 +94,21 @@ def is_game_started(self) -> bool:
return False

@property
def original_positions(self) -> List[str]:
def positions(self) -> Tuple[str, ...]:
return self._positions

@positions.setter
def positions(self, value: Sequence[str]):
self._positions = tuple(sorted(value))

@property
def original_positions(self) -> Tuple[str, ...]:
return self._original_positions or self.positions

@original_positions.setter
def original_positions(self, value: Optional[Sequence[str]]):
self._original_positions = tuple(sorted(value)) if value else None


class LineupPlayer:
def __init__(self, player: Player, lineup_position: str, used_fppg: Optional[float] = None):
Expand Down
8 changes: 7 additions & 1 deletion pydfs_lineup_optimizer/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
class LineupPosition:
def __init__(self, name: str, positions: Sequence[str]):
self.name = name
self.positions = positions
self.positions = tuple(sorted(positions))

def __hash__(self):
return hash((self.name, self.positions))

def __eq__(self, other):
return self.name == other.name and self.positions == other.positions

def __repr__(self):
return '%s (%s)' % (self.name, '/'.join(self.positions))
Expand Down
4 changes: 2 additions & 2 deletions pydfs_lineup_optimizer/sites/fanduel/classic/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def import_players(self):
mvp_player = deepcopy(player)
mvp_player.fppg *= 1.5
mvp_player._original_positions = player.positions
mvp_player.positions = ['MVP']
mvp_player.positions = ('MVP', )
mvps.append(mvp_player)
players.extend(mvps)
return players
Expand All @@ -106,7 +106,7 @@ def import_players(self):
star_player = deepcopy(player)
star_player.fppg *= 1.5
star_player._original_positions = player.positions
star_player.positions = ['STAR']
star_player.positions = ('STAR', )
stars.append(star_player)
players.extend(stars)
return players
8 changes: 4 additions & 4 deletions pydfs_lineup_optimizer/sites/fanduel/single_game/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ def import_players(self):
mvp_player = deepcopy(player)
mvp_player.fppg *= 2
mvp_player._original_positions = player.positions
mvp_player.positions = ['MVP']
mvp_player.positions = ('MVP', )
extra_players.append(mvp_player)
if star:
star_player = deepcopy(player)
star_player.fppg *= 1.5
star_player._original_positions = player.positions
star_player.positions = ['STAR']
star_player.positions = ('STAR', )
extra_players.append(star_player)
if pro:
pro_player = deepcopy(player)
pro_player.fppg *= 1.2
pro_player._original_positions = player.positions
pro_player.positions = ['PRO']
pro_player.positions = ('PRO', )
extra_players.append(pro_player)
players.extend(extra_players)
return players
Expand All @@ -40,7 +40,7 @@ def import_players(self):
captain_player = deepcopy(player)
captain_player.fppg *= 1.5
captain_player._original_positions = player.positions
captain_player.positions = ['CAPTAIN']
captain_player.positions = ('CAPTAIN', )
extra_players.append(captain_player)
players.extend(extra_players)
return players
18 changes: 17 additions & 1 deletion pydfs_lineup_optimizer/solvers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import os
from typing import Type
from pydfs_lineup_optimizer.solvers.base import Solver
from pydfs_lineup_optimizer.solvers.pulp_solver import PuLPSolver
from pydfs_lineup_optimizer.solvers.constants import SolverSign
from pydfs_lineup_optimizer.solvers.exceptions import SolverException, SolverInfeasibleSolutionException


__all__ = ['Solver', 'PuLPSolver', 'SolverSign', 'SolverException', 'SolverInfeasibleSolutionException']
__all__ = ['Solver', 'PuLPSolver', 'SolverSign', 'SolverException', 'SolverInfeasibleSolutionException',
'get_default_solver']


def get_default_solver() -> Type[Solver]:
solver_backend = os.environ.get('SOLVER_BACKEND')
if solver_backend is None:
return PuLPSolver
solver_backend_name = solver_backend.lower()
if solver_backend_name == 'pulp':
return PuLPSolver
elif solver_backend_name == 'mip':
from pydfs_lineup_optimizer.solvers.mip_solver import MIPSolver
return MIPSolver
raise ValueError('Unknown solver backend: %s' % solver_backend)
13 changes: 12 additions & 1 deletion pydfs_lineup_optimizer/solvers/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from typing import TypeVar, Any, List, Iterable, Optional
from typing import TypeVar, Any, List, Iterable, Optional, TYPE_CHECKING


if TYPE_CHECKING:
from pydfs_lineup_optimizer.player import Player


Self = TypeVar('Self', bound='Solver')
Expand All @@ -23,3 +27,10 @@ def solve(self) -> List[Any]:

def copy(self) -> Self:
raise NotImplementedError

@staticmethod
def build_player_var_name(player: 'Player', postfix: Optional[str] = None):
parts = ['Player', player.full_name, *player.positions]
if postfix:
parts.append(postfix)
return '_'.join(parts).replace(' ', '_').replace('.', '_')
Loading

0 comments on commit 544d43f

Please sign in to comment.