Lineup Optimizer Part 1

Fanduel Player List

Hey Everyone!

Today's this tutorial is a step-by-step walkthrough to create your own daily fantasy sports lineup optimizer! Whether you want a single lineup, or are maxing out a 150 entry tournament, this video is for you!

***NOTE**** This video is going to be a fairly high level view of the code itself. Going forward my tutorial videos will be much more granular explaining what is happening, when, and why with demonstrations and examples for learning purposes. This video is just a preview of the end game to give everyone something to work toward!

Packages needed: pandas, pulp, openpyxl, and re as shown being imported below.

import pandas as pd
from pulp import *
import openpyxl
import re

Next up we will open a temporary excel workbook in the background to write lineups to -

wb = openpyxl.Workbook()
ws = wb.active

Then, we will be loading our fantasy player list csv into a pandas dataframe for manipulation. -

players = pd.read_csv(r"C:\Users\nfwya\Fanduel\playerLists\20200807.csv",
                     usecols= ['Id', 'Position', 'FPPG', 'Salary'])

After our player data is loaded, it’s time to start working with it. Regrouping dataframe by position and resetting the dataframe index off of positions — This allows us to use the grouped series as a new dataframe.

availables = players.groupby(["Position", "Id", "FPPG", "Salary", 'Team']).agg('count')
availables = availables.reset_index()

Next, we need to define empty salary and point dictionaries in order to track the metrics for our lineup optimizer, by storing each player’s data by position -

salaries = {}
points = {}
teams = {}

Next, we will need to loop through each position in our grouped dataframe to create a dictionary with key value pair formatting of:

{player id: salary} and {player id: points}

Then, we will update empty dictionaries with a new dictionary entries with the Position being the Key, and all the player IDs as the value, with dual roles as the Key for the nested dictionary. ------ This results in nested dictionary with the following logic {K1:V1}, {K2:V2} --- {K1:{K2:V2}} Where K2=V1 ---- K1=Position, V1=K2=Player ID, V2= Points/Salary

for pos in availables.Position.unique():
    available_pos = availables[availables.Position == pos]
    salary = list(available_pos[['Id', 'Salary']].set_index("Id").to_dict().values())[0]
    point = list(available_pos[['Id', 'FPPG']].set_index("Id").to_dict().values())[0]

    salaries[pos] = salary
    points[pos] = point

After this, we need to define dictionary of positional constraints (how many player we need from each position) and define the Salary Cap we want to use.

pos_num_available = {
    "PG": 2,
    "SG": 2,
    "SF": 2,
    "PF": 2,
    "C": 1
                    }

salary_cap = 60000

Next, we will be establishing our lineup creation loop:

for lineup in range(1,151):

To do this, we will need to create a pulp variable to track whether player is chosen or not, (Binary = yes/no)

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

define our ‘problem’, maximize function because we want max fantasy points

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

    prob = LpProblem("Fantasy", LpMaximize)

Create empty lists to track rewards, costs and positional constraints

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

    prob = LpProblem("Fantasy", LpMaximize)

    rewards = []
    costs = []
    position_constraints = []

Iterate over each player to populate previously created tracking lists for solving.

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

    prob = LpProblem("Fantasy", LpMaximize)

    rewards = []
    costs = []
    position_constraints = []

    for k, v in _vars.items():
        costs += lpSum([salaries[k][i] * _vars[k][i] for i in v])
        rewards += lpSum([points[k][i] * _vars[k][i] for i in v])
        prob += lpSum([_vars[k][i] for i in v]) == pos_num_available[k]

Next, we will add the costs(Salary) and rewards(Points) into the optimization calculation, add in our logic check for slightly lowering the salary cap for every lineup after the first iteration to ensure varied lineups, and then solve.

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

    prob = LpProblem("Fantasy", LpMaximize)
    rewards = []
    costs = []
    position_constraints = []

    for k, v in _vars.items():
        costs += lpSum([salaries[k][i] * _vars[k][i] for i in v])
        rewards += lpSum([points[k][i] * _vars[k][i] for i in v])
        prob += lpSum([_vars[k][i] for i in v]) == pos_num_available[k]

    prob += lpSum(rewards)
    prob += lpSum(costs) <= salary_cap
    if not lineup == 1:
        prob += (lpSum(rewards) <= total_score-0.01)
    prob.solve()

Now that we have solved the lineup function, we need to evaluate it to return the total score, then reset our column variable for writing to excel back to column 1 to start the new lineup.

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

    prob = LpProblem("Fantasy", LpMaximize)
    rewards = []
    costs = []
    position_constraints = []

    for k, v in _vars.items():
        costs += lpSum([salaries[k][i] * _vars[k][i] for i in v])
        rewards += lpSum([points[k][i] * _vars[k][i] for i in v])
        prob += lpSum([_vars[k][i] for i in v]) == pos_num_available[k]

    prob += lpSum(rewards)
    prob += lpSum(costs) <= salary_cap
    if not lineup == 1:
        prob += (lpSum(rewards) <= total_score-0.01)
    prob.solve()

    score= str(prob.objective)
    colnum = 1

Now, the default output of this function is going to be a list of every single player in our dataset, with a variable of either a 1 or 0 assigned to it, depending on whether or not that player was chosen for the lineup. So we need to loop through this list and single out the players chosen, and write them to excel.

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

    prob = LpProblem("Fantasy", LpMaximize)
    rewards = []
    costs = []
    position_constraints = []

    for k, v in _vars.items():
        costs += lpSum([salaries[k][i] * _vars[k][i] for i in v])
        rewards += lpSum([points[k][i] * _vars[k][i] for i in v])
        prob += lpSum([_vars[k][i] for i in v]) == pos_num_available[k]

    prob += lpSum(rewards)
    prob += lpSum(costs) <= salary_cap
    if not lineup == 1:
        prob += (lpSum(rewards) <= total_score-0.01)
    prob.solve()

    score= str(prob.objective)
    constraints = [str(const) for const in prob.constraints.values()]
    colnum = 1

    for v in prob.variables():
        score = score.replace(v.name, str(v.varValue))
        if v.varValue !=0:
            ws.cell(row=lineup, column=colnum).value = v.name
            colnum +=1

Next, we just need to save our total_score variable to reference in all lineup creations after the first run through, write our final score into the excel spreadsheet for reference, and print out the lineup number and score if you would like to see the results as it runs. That finishes up our for loop creating the lineups, so we decrease the indent level back outside of the for loop and save the workbook. And we’re done!

for lineup in range(1,151):
    _vars = {k: LpVariable.dict(k, v, cat='Binary') for k, v in points.items()}

    prob = LpProblem("Fantasy", LpMaximize)
    rewards = []
    costs = []
    position_constraints = []

    for k, v in _vars.items():
        costs += lpSum([salaries[k][i] * _vars[k][i] for i in v])
        rewards += lpSum([points[k][i] * _vars[k][i] for i in v])
        prob += lpSum([_vars[k][i] for i in v]) == pos_num_available[k]

    prob += lpSum(rewards)
    prob += lpSum(costs) <= salary_cap
    if not lineup == 1:
        prob += (lpSum(rewards) <= total_score-0.01)
    prob.solve()

    score= str(prob.objective)
    constraints = [str(const) for const in prob.constraints.values()]
    colnum = 1

    for v in prob.variables():
        score = score.replace(v.name, str(v.varValue))
        if v.varValue !=0:
            ws.cell(row=lineup, column=colnum).value = v.name
            colnum +=1
    total_score = eval(score)
    ws.cell(row=lineup, column=colnum).value = total_score
    print(lineup, total_score)

wb.save(r"\playerLists\Lineups20200807_12111.xlsx")

With this export format a little cleanup will be required prior to uploading to fanduel: -Remove Position and following Underscore from player ID ----- PF_PLAYERID == PLAYERID Then find and replace interior underscore with hyphen to match format of fanduel download. Can write a simple vlookup function in excel to return the player name from the fanduel download if you want to review lineups prior to uploading.

Previous
Previous

Lineup Optimizer Part 2