Python PuLP Optimization problem for Fantasy Football, how to add Certain Conditional Constraints?

It is currently my first time using the PuLP library in python. The purpose of diving into this library was to make a fantasy football solver in python. I have successfully made the solver but cant figure out how to add a few constraints I need.

I have an excel sheet of 400 players and how I project them to play, and I want to find the optimal combination of 9 players given specific constraints. The excel sheet holds, player name, player projection, team player is on, opponent player is facing, and position. Below is what the head of the panda dataframe looks like.

              Name  Projection Position Team  Salary Opponent
0             Jets    3.528576      DST  NYJ    2000      IND
1           Texans    7.936528      DST  HOU    2100      PIT
2         Panthers    4.219883      DST  CAR    2200      LAC
3          Raiders    0.904948      DST  LVR    2300       NE

The constraints i have done successfully: limit a max of 9 players are selected, only 1 person of position QB, 3-4 of position WR, 1-2 of position TE, 1 position DST, and 2-3 position RB.

raw_data = pd.read_csv(file_name,engine="python",index_col=False, header=0, delimiter=",", quoting = 3)

#create new columns that has binary numbers if player == a specific position
raw_data["RB"] = (raw_data["Position"] == 'RB').astype(float)
raw_data["WR"] = (raw_data["Position"] == 'WR').astype(float)
raw_data["QB"] = (raw_data["Position"] == 'QB').astype(float)
raw_data["TE"] = (raw_data["Position"] == 'TE').astype(float)
raw_data["DST"] = (raw_data["Position"] == 'DST').astype(float)
raw_data["Salary"] = raw_data["Salary"].astype(float)

total_points = {}
cost = {}
QBs = {}
RBs = {}
WRs = {}
TEs = {}
DST = {}
number_of_players = {}

# i = row index, player = player attributes
for i, player in raw_data.iterrows():
    var_name = 'x' + str(i) # Create variable name
    decision_var = pulp.LpVariable(var_name, cat='Binary') # Initialize Variables

    total_points[decision_var] = player["Projection"] # Create Projection Dictionary
    cost[decision_var] = player["Salary"] # Create Cost Dictionary
    
    # Create Dictionary for Player Types
    QBs[decision_var] = player["QB"]
    RBs[decision_var] = player["RB"]
    WRs[decision_var] = player["WR"]
    TEs[decision_var] = player["TE"]
    DST[decision_var] = player["DST"]
    number_of_players[decision_var] = 1.0

QB_constraint = pulp.LpAffineExpression(QBs)
RB_constraint = pulp.LpAffineExpression(RBs)
WR_constraint = pulp.LpAffineExpression(WRs)
TE_constraint = pulp.LpAffineExpression(TEs)
DST_constraint = pulp.LpAffineExpression(DST)
total_players = pulp.LpAffineExpression(number_of_players)

model += (QB_constraint == 1)
model += (RB_constraint <= 3)
model += (RB_constraint >= 2)
model += (WR_constraint <= 4)
model += (WR_constraint >= 3)
model += (TE_constraint <= 2)
model += (TE_constraint >= 1)
model += (DST_constraint == 1)
model += (total_players == 9)

The constraints I am trying to add and cant figure out how: have 2 players of the 9 selected be on the same team as the QB, the opponent of the DST cant be anyone of the 9’s team, have 1 players opponent be the QB’s team. Any idea how I would do this? This data is in my excel file but I’m not sure how to add these constraints to the model?

Ive been looking through the cases in the documentation and i cant find any examples where the optimal output is changed based off what the model picks. Example: if picks a quarterback, it affects the rest of the 8 players being selected.

Appreciate any help anyone can provide me

Answer

This is my exact specialty! In general, if you want a constraint to depend on the choice for particular variables (e.x. which QB variable is picked), you’ll need to set up a new constraint for each possible choice, in a somewhat clever way to ensure that that constraint only does anything when that variable was chosen.

  1. Stack at least n players with your QB: You’re going to have a new constraint for each QB in your player pool. The constraint will look like this:
[sum of other players on the same team as QB] + -n*[QB] >= 0

This way, if the optimizer selects the QB, it will also have to select n other players on that QB’s team, to satisfy the requirement that when you subtract n from the number of other players from that team, the result is non-negative. When the QB is not selected, this equation does nothing, since the QB variable has the only negative coeofficient. Note that this methodology also lets you stack a spesific position (Ex. a QB-WR stack) by manipulating what players appear on the left side of that equation. You can also adapt this to force a DST-RB stack.

  1. Don’t stack any players against your DST: This is similar to the above in that we have an equation for each team, but saying “none of this list of players” rather than “at least n of these players” changes the math a little.
[sum of players facing DST] + 8*[DST] <= 8

In this equation, if the optimizer selects the DST the left side is already at 8, so adding any players on the opposing team puts the equation over the limit. This equation has no effect if the DST is not selected, since we aren’t picking more than 8 non-DST players anyway.

  1. Stack your QB with at least one player on the opposing team: This is essentially the same as 1., but we’re choosing n=1, and filling the rest of the equation with the QB’s opponents, rather than teammates:
[sum of players on the team facing QB] + -1*[QB] >= 0

Again, if the QB is selected, we must also select one of the other players in this equation to balance it out and keep the total non-negative. If the QB is not selected, this equation does nothing because all the other players involved have positive coefficients.

In terms of implementing these with pulp, I’ve found that it’s extremely helpful to create your variables using LpVariables.dicts, so that you can iterate through your player list multiple times, and access the same variables each time:

player_ids = raw_data.index
player_vars = pulp.LpVariable.dicts('player', player_ids, cat = 'Binary')

Then it’s really easy to create your roster constraints and objective using list comprehension, ex:

prob = pulp.LpProblem("DFS Optimizer", pulp.LpMaximize)
#Objective
prob += pulp.lpSum([raw_data['Projection'][i]*player_vars[i] for i in player_ids]),
##Total Salary:
prib += pulp.lpSum([raw_data['Salary'][i]*player_vars[i] for i in player_ids]) <= 50000,
##Exactly 9 players:
prob += pulp.lpSum([player_vars[i] for i in player_ids]) == 9,
##2-3 RBs:
prob += pulp.lpSum([player_vars[i] for i in player_ids if raw_data['Position'] == 'RB']) >= 2,
prob += pulp.lpSum([player_vars[i] for i in player_ids if raw_data['Position'] == 'RB']) <= 3,

You can probably extrapolate from there how to do all the stuff you’ve already done in that style. Now for QB stacking:

###Stack QB with 2 teammates
for qbid in player_ids:
    if raw_data['Position'][qbid] == 'QB':
        prob += pulp.lpSum([player_vars[i] for i in player_ids if 
                           (raw_data['Team'][i] == raw_data['Team'][qbid] and 
                            raw_data['Position'][i] in ('WR', 'RB', 'TE'))] + 
                           [-2*player_vars[qbid]]) >= 0,
###Don't stack with opposing DST:
for dstid in player_ids:
    if raw_data['Position'][dstid] == 'DST':
        prob += pulp.lpSum([player_vars[i] for i in player_ids if
                            raw_data['Team'][i] == raw_data['Opponent'][dstid]] +
                           [8*player_vars[dstid]]) <= 8,
###Stack QB with 1 opposing player:
for qbid in player_ids:
    if raw_data['Position'][qbid] == 'QB':
        prob += pulp.lpSum([player_vars[i] for i in player_ids if
                            raw_data['Team'][i] == raw_data['Opponent'][qbid]] +
                           [-1*player_vars[qbid]]) >= 0,

Once you get this down and are able to generate a single lineup with any stacking rule you want, it gets real fun when you start trying to generate several lineups to enter in a GPP. How do you make sure they’re all different? What if you want any 2 of your lineup to have at least 3 players different? How do you set minimum/maximum exposure for your players? I hope this all helps, I know it’s kind of a long read.