BookmarkSubscribeRSS Feed

Fantasy Football Lineup Optimization: Part 1

Started ‎01-19-2024 by
Modified ‎01-19-2024 by
Views 1,135

 

Okay, let me set the stage for you... You're relaxing on the couch on a lazy fall Sunday afternoon. It's 12:55 pm, and only five short minutes separate you from a full afternoon of NFL football. All is right with the world. As the pregame shows conclude with predictions and final commentary, you come to the horrifying realization that you haven't set your fantasy football lineup yet!

 

Instantly you spring up from the couch and begin frantically searching for your phone to log in to your fantasy football app, knowing that with each passing second, the window of opportunity to set your starting lineup is closing. Once the clock strikes 1:00 pm, the majority of your roster will be locked. Your fantasy week, and arguably your entire fantasy football season, hangs in the balance on the roster moves you make in the next, now, 3 minutes.

 

You've managed to locate your phone and fumble your way into the fantasy football app. 2 minutes until 1:00 pm.

 

On your team's homepage, you see that your current starting lineup is projected to score 88.52 points this week (i.e., your team is represented by the Johnny Cash icon).

 

01_JL_matchup-green-optimize.png

 Select any image to see a larger version.

Mobile users: To view the images, select the "Full" version at the bottom of the page.

 

Had you have been more proactive, you would've had time to casually check your starting lineup throughout the week to move as many players into or out of the starting lineup as necessary. However, given that you're now in the 11th hour (and 59th minute), you have two options:

 

  1. Keep your starting roster as-is, with a projected score of 88.52
  2. Select the green "OPTIMIZE" button to automatically set your lineup, which, according to the display, will increase your team's projected score by +8.45 points to 96.97

 

It's not a trick question, I promise. It's like being asked, Would you rather win an $88,520 jackpot or a $96,970 jackpot? It's a no-brainer.

 

Without hesitation, you select the big, green "OPTIMIZE" button, save your lineup, breathe a momentous sigh of relief, and hop back on the couch to catch the 1:00 pm kickoff. Problem solved. Crisis averted.

 

02_JL_man-on-couch.png

 

This series will walk through the mechanics of the optimization model embedded within the green "OPTIMIZE" button, allowing you to instantly optimize your fantasy football lineup. Throughout this series, you will learn:

 

  1. How to formulate and solve this model using different solvers within the OPTMODEL procedure in SAS Optimization (and how to choose the best one!)
  2. Common programming syntax used in the OPTMODEL procedure (it's a little bit different than what you might be used to!)
  3. Automatic detection and LP relaxation strategies for pure network problems (and, what exactly is a "pure network" problem?!)
  4. What reduced costs are in linear programming (and why you should care!)
  5. Decomposition techniques for solving large optimization models (after all, Viya is built to solve problems on big data!)
  6. How to solve this problem using the OPTNETWORK procedure (and yes, believe it or not, this problem can be formulated as a network optimization model!)

 

Each subsequent post in this series will introduce or expound upon one or more of these topics in an applied, approachable way. You needn't be an expert to understand and apply the concepts discussed throughout this series. In fact, this series is specifically written for data scientists and quantitative professionals with minimal exposure to mathematical optimization, so consider this your crash course in getting up to speed with the tools and functionality packed into SAS Optimization!

 

But first thing's first. Let's break down the problem that the big green "OPTIMIZE" button is attempting to solve.

 

__________________

 

In my fantasy football league, a roster is composed of 15 players. Each player plays one of the following positions:

  • Quarterback (QB)
  • Running back (RB)
  • Wide receiver (WR)
  • Tight end (TE)
  • Kicker (K)
  • Defense (DEF)

 

You're allowed to carry as many players at each position as you wish, so long as the total number of players does not exceed 15.

 

The starting roster is composed of 9 players with the following position limits:

 

  • 1 QB
  • 2 RBs
  • 2 WRs
  • 1 TE
  • 1 flex position (W/R) containing either a RB or WR
  • 1 K
  • 1 DEF

 

My current starting lineup, which respects the position limits, is shown below:

 

03-JL_starting-roster-Copy.png

 

Highlighted in yellow to the right of each player is their projected point total for the current week. For example, my starting quarterback, D. Carr, is projected to score 16.53 points this week. One of my running backs, S. Perine, is projected to score 6.43 points this week, and so on.

 

Summing the projected points across all players in the starting lineup yields a total projected team score of 88.52, which is my team's projected point total for the week previously shown on the homepage above (next to the Johnny Cash icon).

 

The remaining 6 players are on the bench (i.e., not in the starting lineup). Similar to the players in the starting lineup, each player on the bench has a projected point total for the current week. However, because they're on the bench, their points do not count towards the team's score for the week.

 

04_JL_bench-roster.png

 

The +8.45 value shown to the left of the green "OPTIMIZE" button indicates that the projected point total for the week will increase by 8.45 points (i.e., from 88.52 currently, to 96.97) simply by clicking that button, which will automatically reallocate the players in my starting lineup to maximize the projected point total for the current week.

 

The problem is small enough that we can manually optimize the starting lineup. In other words, which players should be swapped to increased the projected point total to 96.97?

 

Scanning back and forth between the starting lineup and the bench, in no particular order, we can replace:

 

  • <RB> S. Perine with <RB> J. Mixon (+7.54)
  • <TE> D. Schultz with <TE> K. Pitts (+0.36)
  • <K> W. Lutz with <K> J. Tucker (+0.55)

 

Making these three changes will result in a total point gain of +8.45, increasing the team's projected point total to 96.97 for the current week. As you'll learn in a later post, these values (e.g., +7.54+0.36+0.55) are defined as the reduced costs in a linear programming model, but let's not get ahead of ourselves. 

 

We just did manually what clicking the green "OPTIMIZE" button will do automatically. However, the reality is there are tens of thousands of fantasy football players spread across numerous leagues. Providing the opportunity to OPTIMIZE each team's lineup every week requires an efficient, automated approach, and mathematical programming is the right tool to perform this task. 

 

To formulate this as a mathematical programming model in SAS Optimization, let's first start with the data.

 

The data set below lists each position, the maximum allowed at each position in the starting lineup, along with a binary yes/no indicator if that position is eligible for the flex position.

 

data positions;
 input pos $ max flex_pos $;
datalines;
QB 1 no
RB 3 yes
WR 3 yes
TE 1 no
K 1 no
DEF 1 no
;

 

The second data set lists each player on the roster, along with their position, projected points, opponent, current health status, and current status as either a starter or on the bench.

 

data roster;
 input player $ proj_points pos $ opp $ status $ curr_allocation $;
datalines;
Mayfield 15.22 QB @NO H bench
Mixon 13.97 RB @KC H bench
Jacobs 13.43 RB @IND Q starter
Sutton 9.43 WR LAC Q starter
Waddle 13.13 WR @BAL Q starter
Schultz 7.67 TE TEN H starter
Kirk 0.00 WR CAR IR bench
Carr 16.53 QB @TB H starter
Perine 6.43 RB LAC H starter
Boyd 7.54 WR @KC H starter
Dell 0.00 WR @NYJ IR bench
Pitts 8.03 TE @CHI H bench
Tucker 7.12 K MIA H bench
Lutz 6.57 K LAC H starter
SF49 7.79 DEF @WAS H starter
;

 

The SQL code below modifies the roster data set to flag all RBs and WRs as flex eligible.

 

proc sql;
 create table work.roster_wflexelig as 
  select *
  ,	 case when pos in(select distinct pos from positions where flex_pos='yes') then 'yes'
      else 'no'
	   end as flex_eligible
 from roster;
 drop table roster;
quit;

 

Within the OPTMODEL procedure, the first step declares a set of players, declares the parameters indexed by players, and reads the roster_wflexelig data set into the OPTMODEL procedure.

 

proc optmodel;
 set <str> PLAYERS;

 num proj_points{PLAYERS};
 num starter{PLAYERS};
 str pos{PLAYERS};
 str opp{PLAYERS};
 str status{PLAYERS};
 str curr_allocation{PLAYERS};
 str flex_eligible{PLAYERS};

 read data roster_wflexelig into PLAYERS=[player] proj_points pos opp status curr_allocation flex_eligible;

 

Next, a set of positions is declared, followed by the max and flex_pos parameters indexed by the set of positions. In other words, there is one max value for each position, and one flex_pos for each position, just like the in the data set.

 

 set <str> POSITIONS;

 num max{POSITIONS};
 str flex_pos{POSITIONS};

 read data positions into POSITIONS = [pos] max flex_pos;

 

One modeling approach to solve this problem is to declare a set of binary decision variables. In other words, for each player, is player p assigned to the starting lineup: yes (1) or no (0)?

 

var Assign{PLAYERS} binary;

 

The objective function maximizes the total projected points across all players in the starting lineup.

 

max Total_Points = sum{p in PLAYERS} proj_points[p]*Assign[p];

 

The first constraint limits the total number players at each position in the starting lineup to be less than or equal to the maximum allowed.

 

con pos_limit{po in POSITIONS}:
  sum{p in PLAYERS: pos[p] = po} Assign[p] <= max[po];

 

To accommodate the RB, WR, and flex positions, there can either be 2 RBs and 3 WRs in the starting lineup, or 3 RBs and 2 WRs. In other words, the total number of RBs and WRs in the starting lineup can't exceed five.

 

Within the curly brackets below, the colon operator : acts like a where clause, so this constraint is effectively summing across all RBs and WRs, limiting the total number assigned to the starting lineup to be less than or equal to five.

 

con flex_position: 
  sum{p in PLAYERS: flex_eligible[p] = 'yes'} Assign[p] <= 5;

 

We'll investigate several decomposition options within the solve statement in subsequent parts of this series, but for this first post, we'll simply call the default solve statement and let the OPTMODEL procedure determine and apply the most appropriate default solver.

 

solve;

 

The delta parameter calculates how many more projected points my team is expected to earn compared to my lineup as it's currently set.

 

num delta = Total_Points.sol - sum{p in PLAYERS: curr_allocation[p] = 'starter'} proj_points[p];
print delta;

 

Lastly, two output data tables are created containing my optimal starting and bench lineups.

 

 create data optimal_starting_lineup from 
  [player] = {p in PLAYERS: Assign[p] > 0.5} assign curr_allocation pos proj_points opp status;

 create data optimal_benched_lineup from
  [player] = {p in PLAYERS: Assign[p] < 0.5} assign curr_allocation pos proj_points opp status; 
quit;

 

The Solution Summary table shows the mixed-integer linear programming (MILP) solver was chosen by default, and the projected point total is maximized at 96.97. The delta parameter tells us the optimal starting lineup is projected to yield +8.45 points more than my current starting lineup.

 

05_JL_milp-results-table.png

 

The optimal_starting_lineup table, created from the OPTMODEL procedure, contains the starting lineup yielding the largest possible projected point total of 96.97. Notice the optimal solution makes the same three changes that we made manually above.

 

06_JL_milp-optimal-starting-lineup.png

 

The next post in this series will decompose and solve this same problem using Dantzig-Wolfe decomposition. Stay tuned to learn the numerous ways you can use the OPTMODEL procedure to optimize your fantasy football lineup!

 

Full OPTMODEL code below:

 

proc optmodel;
 set <str> PLAYERS;

 num proj_points{PLAYERS};
 num starter{PLAYERS};
 str pos{PLAYERS};
 str opp{PLAYERS};
 str status{PLAYERS};
 str curr_allocation{PLAYERS};
 str flex_eligible{PLAYERS};

 read data roster_wflexelig into PLAYERS=[player] proj_points pos opp status curr_allocation flex_eligible;

 set <str> POSITIONS;

 num max{POSITIONS};
 str flex_pos{POSITIONS};

 read data positions into POSITIONS = [pos] max flex_pos;

 var Assign{PLAYERS} binary;

 max Total_Points = sum{p in PLAYERS} proj_points[p]*Assign[p];

 con pos_limit{po in POSITIONS}:
   sum{p in PLAYERS: pos[p] = po} Assign[p] <= max[po];

 con flex_position: 
   sum{p in PLAYERS: flex_eligible[p] = 'yes'} Assign[p] <= 5; 

 solve; 

 num delta = Total_Points.sol - sum{p in PLAYERS: curr_allocation[p] = 'starter'} proj_points[p]; 
 print delta; 

 create data optimal_starting_lineup from 
   [player] = {p in PLAYERS: Assign[p] > 0.5} assign curr_allocation pos proj_points opp status;

 create data optimal_benched_lineup from
   [player] = {p in PLAYERS: Assign[p] < 0.5} assign curr_allocation pos proj_points opp status; 
quit;

 

 

Find more articles from SAS Global Enablement and Learning here.

Version history
Last update:
‎01-19-2024 08:04 AM
Updated by:
Contributors

sas-innovate-2024.png

Available on demand!

Missed SAS Innovate Las Vegas? Watch all the action for free! View the keynotes, general sessions and 22 breakouts on demand.

 

Register now!

Free course: Data Literacy Essentials

Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning  and boost your career prospects.

Get Started

Article Tags