BenBrostoff / draftfast

A tool to automate and optimize DraftKings and FanDuel lineup construction.
286 stars 112 forks source link

NBA FanDuel code won't generate teams if more than 7 players are listed in Exposures #173

Open deathdonkey-code opened 3 years ago

deathdonkey-code commented 3 years ago

I am trying to simulate a tournament field using exposure projections I can find online, however generating teams only works if my exposure CSV has 7 or fewer entries. I'll attach a couple CSVs that share player names to illustrate (I had to save them as .txt to get github to accept them, but they should be .csv). I use the output_DK.csv for the player projections, and the min_max_FD_short.csv for lineup exposures (it is "short" because ideally there would be a lineup projection for every player but I can demonstrate the problem with this shorter version). The lineup exposures CSV has 8 entries, running the run_multi function on this code will run without error but produce zero teams. If I simply go into the exposures CSV and remove one player (leaving 7), it will run correctly and generate teams that match the exposures.

8 players in exposure CSV:

Total iterations: 100 [Finished in 1.3s]

7 players in exposure CSV:

Total iterations: 100 Roster Exposure: +----------+--------------------------+------+---------+--------+--------------------+-----------+--------------------+--------------------+ | Position | Player | Team | Matchup | Salary | Projection | # Lineups | Min | Max | +----------+--------------------------+------+---------+--------+--------------------+-----------+--------------------+--------------------+ | PF | MARKIEFF MORRIS | LAL | None | 3,700 | 24.2248 | 96 | | | | SG | TALEN HORTON-TUCKER | LAL | None | 3,400 | 27.075466666666667 | 96 | | | | PG | LUKA DONCIC | DAL | None | 10,900 | 56.298333333333325 | 81 | | | | C | MONTREZL HARRELL | LAL | None | 5,500 | 34.988733333333336 | 78 | | | | SF | BRANDON INGRAM | NO | None | 8,400 | 40.814299999999996 | 59 | | | | PF | CHRIS BOUCHER | TOR | None | 6,400 | 36.212766666666674 | 39 | 26.240000000000002 | 39.360002 | | SF | EVAN FOURNIER | ORL | None | 6,700 | 33.0477 | 37 | | | | PG | DAMIAN LILLARD | POR | None | 10,600 | 54.40630000000001 | 31 | 21.200001 | 31.8 | | SG | KENTAVIOUS CALDWELL-POPE | LAL | None | 3,600 | 22.239333333333335 | 30 | | | | SG | JAMES HARDEN | BKN | None | 11,000 | 59.22056666666666 | 29 | 19.919999999999998 | 29.880000000000003 | | PF | ZION WILLIAMSON | NO | None | 9,100 | 43.96246666666667 | 29 | | | | SF | GORDON HAYWARD | CHA | None | 7,300 | 35.14796666666667 | 29 | | | | SF | NORMAN POWELL | TOR | None | 6,700 | 35.579033333333335 | 28 | 18.8 | 28.199999999999996 | | PG | DENNIS SCHRODER | LAL | None | 5,900 | 33.0496 | 27 | 26.639998 | 39.96 | | PG | STEPHEN CURRY | GS | None | 9,900 | 50.51603333333333 | 27 | | | | SF | KELLY OUBRE | GS | None | 7,000 | 34.187666666666665 | 26 | | | | PG | KYLE LOWRY | TOR | None | 8,100 | 42.484366666666666 | 23 | | | | C | ENES KANTER | POR | None | 7,500 | 36.179966666666665 | 22 | 21.68 | 32.519999999999996 | | SG | TERENCE DAVIS | TOR | None | 3,400 | 20.656266666666664 | 20 | 19.36 | 29.04 | | SF | STANLEY JOHNSON | TOR | None | 3,000 | 16.82173333333333 | 17 | | | | SG | DEANDRE BEMBRY | TOR | None | 3,700 | 22.1033 | 10 | | | | PF | NICOLAS CLAXTON | BKN | None | 3,300 | 19.030933333333333 | 9 | | | | SG | VICTOR OLADIPO | HOU | None | 7,900 | 38.8349 | 8 | | | | PF | KRISTAPS PORZINGIS | DAL | None | 8,200 | 39.137933333333336 | 7 | | | | SG | SHAI GILGEOUS-ALEXANDER | OKC | None | 8,800 | 42.60153333333333 | 6 | | | | PG | KYRIE IRVING | BKN | None | 9,400 | 46.928566666666676 | 6 | | | | PF | DOMANTAS SABONIS | IND | None | 9,700 | 45.441966666666666 | 5 | | | | PF | ROBERT COVINGTON | POR | None | 6,000 | 29.439700000000002 | 5 | | | | PG | DEAARON FOX | SAC | None | 9,000 | 44.85606666666666 | 4 | | | | PF | PATRICK WILLIAMS | CHI | None | 4,800 | 24.197699999999998 | 3 | | | | PF | DRAYMOND GREEN | GS | None | 7,400 | 34.568266666666666 | 2 | | | | PF | TOBIAS HARRIS | PHI | None | 7,900 | 35.9186 | 2 | | | | SF | DORIAN FINNEY-SMITH | DAL | None | 4,500 | 21.046566666666667 | 1 | | | | SG | ZACH LAVINE | CHI | None | 9,500 | 44.93283333333333 | 1 | | | | PF | JAVALE MCGEE | CLE | None | 3,500 | 17.976866666666666 | 1 | | | | PG | ALEX CARUSO | LAL | None | 4,000 | 22.32343333333333 | 1 | | | | PF | JOHN COLLINS | ATL | None | 6,900 | 32.214333333333336 | 1 | | | | SF | JOSH HART | NO | None | 4,900 | 23.226466666666667 | 1 | | | | SF | HARRISON BARNES | SAC | None | 7,000 | 31.9265 | 1 | | | | SF | DENZEL VALENTINE | CHI | None | 3,800 | 18.676933333333334 | 1 | | | | PF | NEMANJA BJELICA | SAC | None | 4,000 | 20.699933333333334 | 1 | | | +----------+--------------------------+------+---------+--------+--------------------+-----------+--------------------+--------------------+ [Finished in 9.5s]

output_FD.txt min_max_FD_short.txt

BenBrostoff commented 3 years ago

Can you add verbose=True on run_multi and post the output in the case it doesn't work? Also does removing any arbitrary row resolve the issue, or just a specific row?

deathdonkey-code commented 3 years ago

It appears its more complex than just "7 players are ok, 8 aren't", it does indeed depend on which players I remove. I think this is because several of the players in the example I gave here are from the same team (TOR) but unsure. Maybe its just failing to find valid configurations that use players from the same team? But then how would this work with using lineup projections for the entire slate? Here I simplified the min_max CSV and it still fails:

name,min,max
DENNIS SCHRODER,0.1,0.7
CHRIS BOUCHER,0.1,0.9
ENES KANTER,0.1,0.9
DAMIAN LILLARD,0.1,0.9
JAMES HARDEN,0.1,0.9
TERENCE DAVIS,0.1,0.9
KYLE LOWRY,0.1,0.9

Building 100 tournament teams...

No solution found. Try adjusting your query by taking away constraints.

OPTIMIZER CONSTRAINTS:

Min teams: 2

LINEUP CONSTRAINTS:

None

PLAYER POOL SETTINGS:

None

PLAYER COUNT: 192

Total iterations: 100 [Finished in 1.3s]

BenBrostoff commented 3 years ago

I'm guessing it's the locked and banned settings (which I can add to the debug output - it really should be in there). Do you mind printing locked and banned players in the failure case? What could be happening is that the optimizer is unable to satisfy required locks in addition to all the other constraints it has to satisfy. Sorry for the continued info requests - it can be tricky figuring out why the opto can't meet certain criteria unless you have all the variables.

deathdonkey-code commented 3 years ago

Not using any locked or banned players there. Will try to print those lists anyway when I get back to computer.

On Mar 7, 2021, at 3:14 PM, Ben Brostoff notifications@github.com wrote:

 I'm guessing it's the locked and banned settings (which I can add to the debug output - it really should be in there). Do you mind printing locked and banned players in the failure case? What could be happening is that the optimizer is unable to satisfy required locks in addition to all the other constraints it has to satisfy. Sorry for the continued info requests - it can be tricky figuring out why the opto can't meet certain criteria unless you have all the variables.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or unsubscribe.

BenBrostoff commented 3 years ago

Right - but run_multi will automatically lock and ban behind the scenes.

deathdonkey-code commented 3 years ago

I printed Locked and Banned lists within the Run() function like this:

LINEUP CONSTRAINTS:

{}

LOCKED LIST:

{}

BANNED LIST:

{}

PLAYER POOL SETTINGS:

{}

PLAYER COUNT: {}
        '''.format(
                optimizer_settings,
                constraints, constraints._locked, constraints._banned,
                player_settings,
                len(players or [])
            )
        )

but they are just empty sets:

No solution found. Try adjusting your query by taking away constraints.

OPTIMIZER CONSTRAINTS:

Min teams: 2

LINEUP CONSTRAINTS:

None

LOCKED LIST:

set()

BANNED LIST:

set()

PLAYER POOL SETTINGS:

None

PLAYER COUNT: 192

Total iterations: 100 [Finished in 1.4s]

BenBrostoff commented 3 years ago

Alrighty, one more test and then I can pull down and try to repro - what about exposure_dict?

Check out https://github.com/BenBrostoff/draftfast/blob/master/draftfast/optimizer.py#L38-L39 in the source. _is_banned and _is_locked both look at the values in this dictionary to determine locking and banning.

All run_multi does is just run run the number of iterations you specify with different params, so if I know the values of the params when it effectively runs, I can debug it.

deathdonkey-code commented 3 years ago

Here you go - it looks like its locking every player in the exposures CSV?

No solution found. Try adjusting your query by taking away constraints.

OPTIMIZER CONSTRAINTS:

Min teams: 2

LINEUP CONSTRAINTS:

None

EXPOSURE DICT:

{'banned': [], 'locked': ['DENNIS SCHRODER', 'CHRIS BOUCHER', 'ENES KANTER', 'DAMIAN LILLARD', 'JAMES HARDEN', 'TERENCE DAVIS', 'KYLE LOWRY']}

PLAYER POOL SETTINGS:

None

PLAYER COUNT: 192

Total iterations: 100 [Finished in 1.3s]

BenBrostoff commented 3 years ago

Yep, that's exactly the issue. run_multi doesn't look like it is selective at all with who it locks on the first iteration:

https://github.com/BenBrostoff/draftfast/blob/a067cbfea72dcc7ed5ae9269f5c06f6debf03048/draftfast/exposure.py#L53-L75

You can actually see in the TODO that this is a known issue. I think an easy-ish fix is to allow this to be settable - in your case you could max out the number of locks at 4 to be conservative. It may take me a little bit to push out a fix but in the meantime this workaround I think should do it:

MAX_LOCKS = 4 # or some number you think is reasonable
if lineups < min_lines or len(locked) < MAX_LOCKS:
  # TODO - downsize locked so solution is not impossible
   locked.append(name)
deathdonkey-code commented 3 years ago

That almost works - I think this is correct?

        MAX_LOCKS = 3 # or some number you think is reasonable
        if lineups < min_lines and len(locked) < MAX_LOCKS:
            # TODO - downsize locked so solution is not impossible
            locked.append(name)

the only problem I've noticed so far is that the code doesn't check for the locked players to make sense - so 3 centers can be locked and then it won't find a valid team and break. For FanDuel it looks like just using MAX_LOCKS = 1 is fine, for DraftKings I can use a higher MAX_LOCKS and it works because it can put those players in the UTIL / G / F slots, but it lowers the average projection a bit and makes some funny lineups. Still, seems to generate lists of teams that attempts to match the exposures and that is a lot of progress, thanks!