#!/usr/bin/env python # coding: utf-8 # In[1]: import pandas as pd np=pd.np from sdd_api.api import Api from credentials import * import matplotlib.pyplot as plt get_ipython().run_line_magic('matplotlib', 'inline') api = Api(username=username, password=password, client_id=client_id, client_secret=client_secret) # ### What We're Going to Do # 1. Get salary information # 2. Get projected points # 3. Run lineup optimizer # 4. Tweak player pool or select highest rated lineups # 5. Generate lineup csv # In[2]: salaries=api.get_dataframe("dfs_salaries",season_start=2017) salaries.sample(5) # We're working on creating our own projections but for this example we'll use the ones published by [Fantasy Football Analytics](fantasyfootballanalytics.net). # For your convenience I've already downloaded week 1 for draft kings. The whole process is as follows # # Go to http://apps.fantasyfootballanalytics.net # - create a free account # - update your scoring settings to reflect the league your interested in. # -A handy guide to dfs scoring is available here https://rotogrinders.com/pages/nfl-site-scoring-comparison-357433 # - Set the general settings to week 1 and only the positions you have in your league. # - Download custom rankings # # In[3]: salaries=salaries[salaries['week_num']==1] # In[4]: dk_projections=pd.read_csv("data/FFA-projs-2017-week1-dk.csv",index_col=False)[['player','position','points','risk','team','lower','upper']] dk_projections.rename(columns={'player':'player_name','points':'predicted_pts', 'team':'team_name'},inplace=True) dk_projections['team_name']=dk_projections['team_name'].replace("SD","LAC").replace("LA","LAR") dk_projections['week_num']=1 dk_projections.head() # In[5]: def cleanNames(row): name=row['player_name'] name=name.upper() if row['position'] in ['Def','DST']: #print(row) return row['team_name'] else: name=name.replace(" JR","").replace(".","").replace(" III","").replace("'","") #fix a few nicknames name=name.replace("PHILLY BROWN","COREY BROWN").replace("KEITH SMITH", "ROD SMITH").replace("ROBERT KELLEY", "ROB KELLEY") return name salaries['merge_name']=salaries.apply(cleanNames,axis=1) dk_projections['merge_name']=dk_projections.apply(cleanNames,axis=1) merged=salaries[salaries['week_num']==1].merge(dk_projections, how="left", on=['merge_name','week_num'], suffixes=["","_ffa"]) merged=merged.drop_duplicates(subset=["player_name", "position","team_name"]) merged[(pd.isnull(merged['predicted_pts']))&(merged['position']!='K')]#no kickers in draft kings/dk predictions # Eric Tomlinson doesn't have a prediction. Given his dk salary is 2500 we will just ignore him # ### Lineup Optimizer # In[14]: from itertools import combinations,product from collections import defaultdict from operator import itemgetter, attrgetter def mapPos(row): pos=row['position'] args=[] #Vectorize to make position groups easier to handle if pos=='QB': args.append(np.array((row['salary'], 1,0,0,0,0,0))) elif pos=='RB': args.append(np.array((row['salary'], 0,1,0,0,0,0))) args.append(np.array((row['salary'], 0,0,0,0,1,0))) elif pos=='WR': args.append(np.array((row['salary'], 0,0,1,0,0,0))) args.append(np.array((row['salary'], 0,0,0,0,1,0))) elif pos=='TE': args.append(np.array((row['salary'], 0,0,0,1,0,0))) args.append(np.array((row['salary'], 0,0,0,0,1,0))) elif pos=='DST': args.append(np.array((row['salary'], 0,0,0,0,0,1))) return args class Lineup:#'W/R/T':1 def __init__(self,players=[]): self.rem_lookup={'QB':1,'WR':3,'RB':2,'TE':1,'DST':1}#draft kings settings self.score=0. self.predictedScore=0. self.players=players self.salary=50000 for player in players: self.addFull(player) def isValid(self): if len(self.rem_lookup.keys())<=0: return True else: return False def nextPos(self): return list(self.rem_lookup.keys())[0] def addFull(self,player): pos=player['position'] self.salary-=player['salary'] try: self.score+=player['score'] except: pass try: self.predictedScore+=player['predictedScore'] except: pass def add(self,player): self.players.append(player) pos=player['position'] self.rem_lookup[pos]-=1 if self.rem_lookup[pos]<=0: self.rem_lookup.pop(pos) self.salary-=player['salary'] print(player['predictedScore']) self.score+=player['predictedScore'] def __repr__(self): names=', '.join([p['player_name'] for p in self.players]) return str(self.score)+' '+str(self.predictedScore)+" "+str(self.salary)+' '+names def __hash__(self): l=sorted([p['player_name'] for p in self.players]) strings=','.join(l) return hash(strings) # In[15]: def hashPlayer(p): return p['player_name']+p['position'] class lineupGenerator: def __init__(self,pool,locks=[],goodDef=[]): self.rem_lookup={'QB':1,'WR':3,'RB':2,'TE':1,'DST':1} self.score=0. keys=['QB','WR','RB','TE','DST'] self.players=[] self.pool=set() self.lineups=[] self.goodDef=goodDef pDict={} self.locks=pool[pool['player_name'].isin(locks)].to_dict(orient="records") for lock in self.locks: self.rem_lookup[lock['position']]-=1 pool=pool[~pool['player_name'].isin(locks)] for pos,group in pool.groupby("position"): pDict[pos]=group.to_dict(orient="records") for p in pDict[pos]: self.pool.add(hashPlayer(p)) options={pos:combinations(pDict[pos],self.rem_lookup[pos]) for pos in self.rem_lookup} self.pDict=pDict self.salary=50000 lineups=[] lineups=options[keys[0]] for idx in range(1,len(keys)): lineups=product(lineups,options[keys[idx]]) self.lineups=lineups def genLineups(self): for l in self.lineups: fromTeam=defaultdict(int) fromSkill=defaultdict(int) fromPos=defaultdict(int) oppDST=defaultdict(int) attempt=list(l[1]+l[0][1]+l[0][0][1]+l[0][0][0][1]+l[0][0][0][0]) for lock in self.locks: attempt.append(lock) sal=0 valid=True for p in attempt: if p['position']!='DST': fromTeam[p['team_name']]+=1 fromPos[hashPlayer(p)]+=1 oppDST[p['opp_name']]+=1 if p['position']!='QB': fromSkill[p['team_name']]+=1 else: if oppDST[p['team_name']]>5:#more than 1 player from team facing defense in lineup valid=False sal+=p['salary'] #add flex generator that checks if in pool #unique player unique_player=all(fromPos[pos]<=1 for pos in fromPos) #3 from same team from_same=all(oppDST[t]<=4 for t in oppDST) if sal<=48000\ and unique_player\ and from_same\ and all(p['opp_name'] not in self.goodDef for p in attempt) and valid: yield attempt # In[16]: def prune(this): ''' Prunes player pool to generate maximum expected value follows: if a player has a higher predicted score and lower salary, remove all players with a higher salary than the player Additionally if all position spots are unused, keep up to the remaining number of slots left with a lower score than the player we are on but higher predicted point total than the next best option Here's an example: Odell beckam salary 8000 predicted 19 Alshon Jeffrey salary 7900 predicted 20 Mohammed sanu salary 5100 predicted 15 Because there are 3+1 spots for WR (including FLEX), we can't remove odell beckham because he would contribute more to our point total than removing him. If there was only 1 WR slot we'd be safe to remove him. ''' FLEX=['WR', 'RB', 'TE'] best=pd.DataFrame() this=this.sort_values(by="predictedScore",ascending=False) for pos, group in this.groupby('position'): group=group.to_dict(orient='records') bad=[] for idx,best_player in enumerate(group): salary=best_player['salary'] rem_lookup={'QB':1,'WR':3,'RB':2,'W/R/T':1,'TE':1,'DST':1} rem_lookup['W/R/T']+=rem_lookup[pos] for player in group[idx+1:]: if player['salary']>=salary: remove_player=False if rem_lookup[pos]<=1: remove_player=True if pos in FLEX : if rem_lookup['W/R/T']<1: remove_player=True else: remove_player=False rem_lookup['W/R/T']-=1 else: rem_lookup[pos]-=1 if remove_player: group.remove(player) best=pd.concat([best,pd.DataFrame(group)]) best=best[this.columns]#maintain passed col order return best.sort_values(by='predictedScore', ascending=False) # In[17]: dfs=merged.copy() dfs=dfs[dfs['position']!="K"][['player_name', 'team_name', 'position', 'opp_name', 'dk_salary', 'player_id', 'predicted_pts', 'lower', 'upper']] dfs['predictedScore']=dfs['predicted_pts']#you could change this to upper or lower depending on the type of lineup your looking for dfs['salary']=dfs['dk_salary'] dfs=dfs[['team_name', 'player_name', 'position', 'opp_name', 'salary','predictedScore']] dfs.head(1) # ### Prune # In order to speed up our lineup generator we can prune out overly expensive players. Read the comments in the prune function to get the exact description # In[18]: pruned=prune(dfs) pruned # I like to use this pruned set as a starting point and then tweak my final player pool using a custom web app I've developed -- not ready for release as of now # In[19]: #example def toIncl(s): return s.split(", ") inclusions=toIncl("Le'Veon Bell, David Johnson, Antonio Brown, Pittsburgh Steelers, Julio Jones, AJ Green, Dez Bryant, Amari Cooper, Kirk Cousins, Sammy Watkins, Terrelle Pryor, Martavis Bryant, Michael Crabtree, Todd Gurley, Larry Fitzgerald, DeAndre Hopkins, Jordan Reed, Alshon Jeffery, Pierre Garcon, Allen Robinson, Lamar Miller, Jimmy Graham, Jeremy Maclin, Eric Decker, Matt Forte, Bilal Powell, John Brown, Jared Goff, Kevin White, Rishard Matthews, Marvin Jones, Sterling Shepard, Robert Woods, Eli Rogers, Jason Witten, Sammie Coates, Will Fuller, Tavon Austin, Terrance West, Jordan Matthews, Mohamed Sanu, JJ Nelson, Torrey Smith, Jaguars, Zay Jones, Zach Ertz, Jared Cook, Phillip Dorsett, Los Angeles Rams, Brian Cook, Marquess Wilson, Austin Hooper, Vernon Davis, Lance Dunbar, Evan Engram, Zach Miller, Vance McDonald, Austin Seferian-Jenkins, Jermaine Gresham, Detroit Lions, Dion Sims, Adam Shaheen, San Francisco 49ers") player_pool=dfs[dfs['player_name'].isin(inclusions)] print(len(player_pool)) player_pool[player_pool['position']=='WR'] # ### Constraints # Next, we can impose constrains on our lineups. I'll walk through the types of supported constraints: # - ONE_OF -- All lineups must contain only 1 of the players in each added list # - ONLY_ONE -- All lineups can contain no more than 1 of the players in each added list # - AT_LEAST -- All lineups must have at least 1 of the players in each added list # # Last season this was great for dealing with position groups like the Falcons Offense or the Redskins Receiving core. It was tough to say who will score but you can be quite confident they will have a touchdown. Constraints help you take advantage of that # In[35]: #last years example tweaked=pruned ONE_OF=[] ONE_OF.append(['Ty Montgomery'])#forces Ty Montgomery in each lineup ONE_OF.append(["LeGarrette Blount","Dion Lewis"]) ONE_OF.append(["Devonta Freeman","Tevin Coleman"]) ONLY_ONE=[] ONLY_ONE.append(['Devonta Freeman', 'Tevin Coleman']) ONLY_ONE.append(['Antonio Brown','Eli Rogers'])#never more than one of these guys ONLY_ONE.append(['Julio Jones','Mohamed Sanu']) AT_LEAST=[] #AT_LEAST.append() locks=[]#["Julien Edelman"] tweaked[tweaked['position']=='DST'] # We'll just use locks for this example. Besides that we have some standard GPP constraints like: # - don't face your starting defense # - don't have too many players from one team # - too many players from the same skill group on a team # - our max expected value pruned player pool. # In[39]: #gen lineups without flex tweaked=prune(dfs) locks=["Matt Ryan","David Johnson","Baltimore Ravens", "Rob Gronkowski"] lineGen=lineupGenerator(tweaked, locks, []) lineups=lineGen.genLineups() lineups=list(lineups) HIGH_RISK_SWAPS=[] ONE_OF=[] AT_LEAST=[] ONLY_ONE=[] len(lineups) # Add flex player # In[40]: finals=[] flex=tweaked[tweaked['position'].isin(["WR","TE","RB"])] for run,l in enumerate(lineups): if run%1000==0: print(float(run)/len(lineups),len(finals)) names=[player['player_name'] for player in l] for f in flex[~flex['player_name'].isin(names)].to_dict(orient="records"): final=l[:] originalPos=f['position'] f['position']='FLEX' final.append(f) fromTeam=defaultdict(int) fromSkill=defaultdict(int) fromPos=defaultdict(int) fromTeamTE=defaultdict(int) oppDST=defaultdict(int) sal=0 valid=True swap=0 one_of=[0]*len(ONE_OF) at_least=[0]*len(AT_LEAST) only_one=[0]*len(ONLY_ONE) #ADD SWAPS. ONE_OF_COPY=ONE_OF.copy() AT_LEAST_COPY=AT_LEAST.copy() ONLY_ONE_COPY=ONLY_ONE.copy() dst=None for p in final: if p['position']=='FLEX': p["position"]=originalPos if p['position']!='DST': fromTeam[p['team_name']]+=1 if p['position']=='TE': fromTeamTE[p['team_name']]+=1 fromPos[hashPlayer(p)]+=1 oppDST[p['opp_name'].replace('@','')]+=1 if p['position']!='QB': fromSkill[p['team_name']]+=1 else: dst=p['team_name'] sal+=p['salary'] if p['player_name'] in HIGH_RISK_SWAPS: swap+=1 for idx, players in enumerate(ONE_OF_COPY): if p['player_name'] in players: one_of[idx]+=1 #ONE_OF_COPY.remove(players) for idx, players in enumerate(AT_LEAST_COPY): if p['player_name'] in players: at_least[idx]+=1 #AT_LEAST_COPY.remove(players) for idx, players in enumerate(ONLY_ONE_COPY): if p['player_name'] in players: only_one[idx]+=1 #ONLY_ONE_COPY.remove(players) #add flex generator that checks if in pool final[-1]["position"]="FLEX" if sal<=50000 and all(fromSkill[pos]<=2 for pos in fromSkill) \ and all(fromTeamTE[team]<=1 for team in fromTeamTE)\ and all(fromPos[pos]<=1 for pos in fromPos)\ and sal>=40000 and valid and swap<=1 \ and all(one_of==1 for one_of in one_of)\ and all(at>=1 for at in at_least)\ and all(one<=1 for one in only_one): if oppDST[dst]<=1: #print(oppDST) #print(lineup) lineup=Lineup(final) finals.append(lineup) # In[42]: allHash=set() for f in finals: phash=set() for p in f.players: phash.add(p["player_name"]+","+p["team_name"]+","+str(p["predictedScore"])) allHash.add(frozenset(phash)) len(allHash) # ### Inspect Lineups Generated # 500,000 lineups! The lower number is without duplicates caused by flex vs normal spot # In[43]: #Grab top 10 lineups finals=sorted(list(allHash)[:10],key=lambda x: sum([float(xi.split(",")[-1]) for xi in x]),reverse=True) # In[44]: new=[] for idx, f in enumerate(finals): if idx%1000==0: print(idx,len(finals), idx/len(finals)) l=[] posis={"WR": 3,"TE":1,"RB":2} for i,n in enumerate(list(f)): p={} p["player_name"]=n.split(",")[0] p["team_name"]=n.split(",")[1] p['predictedScore']=float(n.split(",")[2]) filtered=pruned[(pruned["player_name"]==p["player_name"])&(pruned["team_name"]==p["team_name"])].iloc[0] p["position"]=filtered["position"] p["salary"]=filtered["salary"] if p["position"] in posis.keys(): posis[p["position"]]-=1 if p["position"] in posis.keys() and posis[p["position"]]<0: p["position"]="FLEX" l.append(p) new.append(l) new_lineups= [Lineup(l) for l in new] sort=sorted(new_lineups,key=attrgetter("predictedScore"),reverse=True) playerCounts=defaultdict(int) for l in new: for p in l: playerCounts[p["player_name"]]+=1 print("Player count", len(playerCounts.keys())) print("Players used",sorted(tweaked['player_name'].unique())) print(len(tweaked['player_name'].unique())) {key: playerCounts[key]/float(len(new)) for key in playerCounts} # Our constraints we're honored. Matt Ryan, Rob Gronkowski and the Baltimore Ravens are in all of our lineups # In[45]: new_lineups # Our highest expected value with our constraints is 127.838 points # ### Output Lineup CSV # Download the draft kings lineup template by going to the lineups tab on Draft Kings.com. We've included the all game template for week 1 for your convenience # In[53]: id_table=pd.read_csv("data/dk-lineup-template-week1.csv",skiprows=7) id_table.index=range(len(id_table)) id_table.columns=range(len(id_table.columns)) id_table=id_table[[9,10,11,12,13,14,15]] id_table.columns=["position","Name + ID","player_name","ID","Salary","Game Info","team_name"] def cleanName(name): #sites handle surnames/nicknames differently. We will remove them and have verified this is ok at the time of writing return name.replace(' Jr.', '').replace("'","").replace(" Sr.","") id_table['player_name']=id_table['player_name'].apply(cleanName) id_table.head(3) # In[54]: rows=[] POS_START={"QB":0,"RB":1,"WR":3,"TE":6,"FLEX":7,"DST":8} for l in new_lineups: row=[0]*9 for p in l.players[:]: idx=POS_START[p['position']] if(idx==8): name=p["player_name"].split(" ")[-1]+" "#Draft kings DST's have a space after them for some reason. else: name=p["player_name"] player=id_table[(id_table['player_name']==name)&(id_table['team_name']==p['team_name'])].iloc[0] if row[idx]<1: row[idx]=player['ID'] else: idx+=1 if row[idx]<1: row[idx]=player['ID'] else: idx+=1 row[idx]=player['ID'] rows.append(row) output_lineups=pd.DataFrame(rows, columns=["QB","RB","RB","WR","WR","WR","TE","FLEX","DST"]) output_lineups # In[55]: output_lineups.to_csv("data/lineupsweek1-example.csv",index=False) # There you have it. All you have to do now is go to the lineups tab on draft kings and upload this sheet. # I hope this served as a good example. The code needs serious refactoring, probably should just be scrapped, but this should give you a great starting point for your own lineup optimizer # In[ ]: