In Baseball You Stack a Team. In Football You Stack a Throw.

I have had a baseball lineup optimizer running on this site since June. Pointing it at football looked like a small job — swap ten roster slots for nine, swap a $50,000 cap for a $60,000 one, done. That part took an afternoon. The part that took actual thought was NFL DFS stacking: baseball and football are correlated in completely different ways, and correlation is the whole game.

In baseball, the correlation is sequential. Batters score in a chain: a single sets up the double that drives him in. That is why my MLB optimizer has a constraint forcing four hitters from one team into consecutive batting-order spots — 3-4-5-6, not 2-5-7-9. In football there is no batting order. The correlation is simultaneous, and it is much sharper.

NFL Lineup Optimizer site and player pool screen showing DraftKings salary CSV upload

One throw, counted twice

When a quarterback throws a 40-yard touchdown to his receiver, that is a single physical event. Daily fantasy scores it twice: four points and 40 passing yards for the quarterback, six points and 40 receiving yards for the receiver. Roster both and you have not diversified anything — you have doubled down on one event.

That sounds like a reason to avoid it. In a cash game, where you only need to beat half the field, it partly is. In a large tournament it is exactly what you want, because you are not trying to be reliably good, you are trying to be occasionally enormous. Doubling down on one event is how you get a score nobody else has.

So “stack the quarterback” is not advice you follow by hand after the optimizer runs. It is a constraint you write into the model, because an optimizer that only maximizes projected points will never build a stack on purpose. Projections are additive and independent; the model has no idea two of its players share a football.

Writing it down without a big-M

Here is the constraint I actually wanted: if quarterback q is in the lineup, then at least two of his own pass-catchers or backs must be too.

Conditional constraints normally mean an indicator variable and a big-M term, which is where mixed-integer models go to get slow and numerically ugly. This one has a shortcut, and the shortcut comes from the roster itself: a classic NFL lineup has exactly one quarterback slot. So for every quarterback in the pool I can write

sum(pick[j] for j in teammates_of(q)) >= 2 * pick[q]

If q is not selected, pick[q] is zero and the whole thing reads “sum ≥ 0” — true for free, no effect on the solve. If q is selected it reads “sum ≥ 2” and bites. No indicator variable, no big constant to tune, and one constraint per quarterback instead of one per quarterback-receiver pair. The same shape handles “at least one WR specifically” or “at least one TE specifically” — just change the set you sum over and the number on the right.

This is the sort of thing that makes people think optimization is a dark art. It is not. It is that a model is a piece of writing, and there is usually a better sentence.

The bring-back

A quarterback stack only pays if his team scores a lot. What actually makes a team score a lot is usually the other team scoring a lot too — a shootout forces both offenses to keep throwing. If you have correctly guessed which game explodes, the receivers on the losing side are just as valuable as the ones on the winning side, and nobody in your contest has them.

So there is a second constraint, hung off the same variable, pointing at the opposing team:

sum(pick[j] for j in opponents_of(q)) >= 1 * pick[q]

That is the “bring-back,” and in my old Excel model it lived in a named range called team_plus_oppo. It is the single most football-specific thing in the whole optimizer. Nothing in the baseball version resembles it, because in baseball the opposing pitcher’s success is the direct negation of your hitters’ success — the correlation across the matchup is negative, not positive.

And the one place the correlation is negative

Defense. A DST scores points by sacking a quarterback, intercepting him, and holding his team under 20. If you roster a defense and also roster the offense it is facing, you have built a lineup that argues with itself: every sack you cheer for costs you passing yards somewhere else on the same nine-player card.

That one is easy to write — a plain pairwise exclusion for every defense and every opposing player you want to block:

pick[dst] + pick[j] <= 1

I made it two separate switches, because they are not equally obvious. Blocking a defense against your own quarterback is close to non-negotiable and defaults to on. Blocking it against any of your players is a stricter rule that costs you real lineup equity on a small slate, so it defaults to off and is there when you want it.

Why this belongs in the solver and not in your head

You could build one of these lineups by hand in five minutes. The reason to write it as a mixed-integer program is that you are not building one lineup — you are building twenty that all have to satisfy every rule above and be meaningfully different from each other, which is the part that is genuinely hard. Once "stack the quarterback with two receivers and bring one back" is a constraint rather than a habit, the solver enforces it perfectly across all twenty while it hunts for points somewhere you would not have looked.

Underneath, it is still the same knapsack problem the baseball version solves. The cap is the bag, the players are the items. Football just has opinions about which items belong together.

The model this came from was an Excel workbook I built in 2014 and last touched in 2021 — here is what was actually inside it. The finished tool is here: a free NFL lineup optimizer for DraftKings and FanDuel.

Tradeline Supply
Things that I use, like, and am affiliated with:
Mint Mobile offers great cell phone service for $15 flat, get $15 off using the link. Get discounted phones with service activation and no contract.
I never spend money before I check Mr Rebates or Rakuten to get cashbacks, rebates, discounts, coupons or cheaper gift cards.

Leave a Reply