Variance Matters - What is your optimizer optimizing?

published on 01 October 2024

A typical lineup optimizer is not suited for building GPP winning lineups, at least not directly. If you aren’t familiar with the inner workings, there’s multiple ways to “optimize” a lineup, but most (all?) are solving a variation of the Knapsack Problem. 

Given a set of items, each with a weight and a value, determine which items to include in the collection so that the total weight is less than or equal to a given limit and the total value is as large as possible.

Or 

Given a set of players, each with a salary and fantasy point projection, determine X number of players to include in a Lineups so that the total salary is less than $50,000 and the projected point total is as large as possible. 

It’s an NP-complete optimization problem, there’s tons of walk-throughs on how to set it up, and you can solve it with any programming language, or even excel. You can even add in more complex constraints like stacking, limiting hitter vs pitcher, or any other typical lineup optimizer setting and keep the problem linear so it is easy to solve. 

The issue is that we don’t really want to just make the “projected point total as large as possible” for GPPs. That seems counterintuitive, of course we want to score the most points possible! Yes, but we know that point projections are never exact, and on their own are rarely meaningful - what is meaningful is understanding the potential range of a players fantasy score for a given game. In the same way, for GPPs, we care about the range of a lineups fantasy score for a lineup of players rather than just adding up all the single point projections and calling it “optimal”. 

The good news is we have math on our side. 

Take this simplified example: 

Lineup A comes out of the optimizer with a total projection of 165, but this lineup is made up of extremely consistent players so the range of outcomes for lineup A is 160-170. 

Lineup B comes out of the optimizer with a total projection of 150, but includes much higher variance players, maybe it’s full of deep ball receivers who either go 0-3 with 0 yards or 3-4 with 150 yards. Lineup B has a range of outcomes of 100-200. 

In cash, I’ll take Lineup A, but in GPP Lineup B gives you the best chance to win a big tournament. 

If you think about how we get to a range of outcomes for a lineup, it is essentially just pulling a bunch of projected point values for each player in that lineup. Maybe this is through a simulation or a mathematical representation of a simulation since that is all a distribution is anyway. The central limit theorem tells us that pulling a bunch of values and adding them up creates a nice normal distribution of lineup scores - this helps make the math easier. 

So we know now that we want to find lineups that have a high maximum in their range of outcomes (even if it means they have a lower mean or “optimized” value).  And we know that since their scores are normally distributed this means that we are looking for lineups with a high variance / wider bell curve. This follows with the age old DFS advice touting “barbell” style of play. 

And since a lineup’s range of outcomes are normally distributed, we can just calculate the Z-score as a quick way to compare lineups against one another - this value accounts for the mean and variance of that range of outcomes. 

Great, so we need lineups with high z-scores compared to a winning target, let’s just build them from the start. Instead of optimizing for total projection, just optimize for z-score - unfortunately this is where it gets tricky. Since z-score uses division the optimization problem becomes  non-linear, so we can’t just use the trusty knapsack approach that most (all?) lineup optimizers use, including ours at Uncertain Edge. 

Here are a few ways I have tried to tackle this and built GPP winning lineups:

Method 1. We could go the non-linear route, our simulation process uses genetic algorithms and particle swarm to quickly build a ton of lineups, but you’ll never get THE optimal solution (or at least you can’t prove it is optimal). The math is also fuzzy and setting complex stacking and other constraints gets weird. I am sure some use this, but I am not a fan.

Method 2. We can borrow inspiration from this research paper and take a branch and bound or cutting plane like approach. I have coded up the examples from this paper, it was rough, kudos to that research team for this methodology. I had a tough time with discretizing the solution space and generalizing the code so it works for MME lineup pools and ultimately found it wasn't suitable for my coding skills. Below is an attempt to generalize that process, but even that is not doing it justice as this is very simplified compared to the linked paper. This is where the math gets fun…

The process effectively cuts off or bounds the solution space to encourage picking those high z-score lineups. This involves iteratively building lineups where each solution is used as a constraint in the next iteration, going back and forth by setting new lower and upper bounds for what the best Z-score could be.

Set a Target Score, Similar to our discussion on Game Theory where we calculated TSE, decide on a reasonable winning score for the contest you are entering. This can be somewhat arbitrary and may need to be adjusted but you can take a look at a few previous contests to get a good idea. Be conservative here, you’d rather it be a little low than high. With this target score we can easily see that:

Z-score = (projection - target) / std dev

Maximize for mean projection, this is just building a lineup like normal and will give us the upper bound for what the total mean projection can be.

Minimize the variance of teams whose projection exceeds the target score. This provides the new lower bound on variance for any team that can reach the target score.

Branch and Bound the solution space - we know we want teams with relatively high mean projections, but some variance can help overcome a lower than desired mean.  If we split the solution space into variance “chunks” then we can guide the optimization process toward lineups with a better balance of mean vs variance. Of the teams that meet the target score, calculate the min and max variance to get a range of values, then split that range into N “chunks”.

Iterate Through Variance Chunks. For each variance chunk, optimize for the highest mean projection possible within that variance range. This gives us a set of lineups, each representing a different balance of mean projection and variance.

Calculate Z-Scores. For each lineup generated, calculate its Z-score. The lineup with the highest Z-score essentially has the highest upside without being overly risky.

Iteratively Update the Mean and Variance Bounds. This is where it gets a bit tricky

  • Mean Upper bound: This remains the highest achievable mean from our initial lineup.
  • Mean Lower bound: The highest mean that still allows for a variance below our current best solution's variance.
  • Variance Upper bound: The lowest variance that still allows for a mean above our current best solution's mean.
  • Variance Lower bound: This remains the lowest achievable variance from our initial optimization that meets the target score.

Instead of optimizing for a non-linear z-score, we iteratively squeeze the acceptable variance and mean ranges, going back and forth between maximizing mean for a given variance upper bound and minimizing variance for a given mean lower bound. This helps guide the optimization to lineups that either have high enough means with larger variance, or high enough variance with large means.

At each iteration you can calculate the z-score of the lineups that are created, eventually the difference between the upper and lower bounds becomes tiny and you can stop, or you can shut it off after a certain number of iterations. 

This process is fun to code, and it is pretty clever, but it also has some downsides:

  • I just typed all that out and I am not sure I even understand it. I had to reference my code and Stackoverflow (that thing we used before AI, remember that?) multiple times
  • You still don’t really get a true optimal. You will end up with a batch of good lineups, all with the same goal of winning a GPP but no true optimal
  • The process can take a while, it is computationally intensive and depending on your hardware can be inefficient
  • This process is only working off of static values for mean and std dev, it is basically a mathematical representation of a simulation but that means we are making assumptions about the normality of player distributions and not just lineup distributions. For most sports this is an incorrect assumption.

Method 3. Don’t want to mess with all of that? Instead we could just leverage lineup and contest simulations. If we know we want to optimize for z-score, we can skip the complexity and just make a large pool of lineups, then calculate the z-score for each and choose based on this. This can easily be done in our contest sims, and most other contest sims on the market. The trick here is that you need the large lineup pool to contain good lineups. Luckily there are easy heuristics here for most sports - in MLB we generally want to include 4-5 hitter stacks, do not use hitters against pitchers, maybe use total ownership constraints, etc. Going this route also makes it easier to add complex constraints - Team specific stacking with only certain players and a bring back would be rough with a genetic algo.

By reviewing previous slates, using sound game theory, and understanding lineup construction best practices, you can generally get a good idea of how winning lineups can be created. This helps create that large pool of good lineups. That still doesn’t mean the process will hit every slate, there are always one off slates that don’t materialize as they *should*, but this approach has given me the best chance of winning contests.

Using sims also allows for easily including the Sport-Team-Position-Player correlations that make up our process at Uncertain Edge. This is why I prefer to heavily leverage simulations in my process.

I am still playing around with the idea of a GPP specific optimizer that does directly use Method 2, not sure if this will make it to production but it is fun to mess around with. 

Built on Unicorn Platform