---
title: "The Phantom Variance Fallacy"
description: "Concealing random variables to perform rhetorical sleight-of-hand."
author: "Adam Fillion"
date: "2026-07-11"
categories: [statistics, fallacies, football, probability, simulation, python, plotly]
draft: false
---
I have a simple question about two football teams:
- **Team A** yields **3.3 yards per play**
- **Team B** yields **5.0 yards per play**
***Which team wins?***
In this idealized model, it looks like Team B. In reality, it's Team A, but it's due to a piece of information that was not given—the distribution of yards per play.
Football has a very clear risk of ruin during a drive: four downs without 10 yards of progress and you turn the ball over to your opponent. Thus, the shape of the distribution is key to determining the effectiveness of a play type. We can imagine that Team A always runs the ball with a very low variance result, and Team B always throws the ball with a very high variance result.
This situation is analogous to many real-world situations. Perhaps the most illustrative is to pick up nickels in front of a steamroller [[1]](#references). There are many opportunities, in financial markets, betting markets, criminal enterprise, and elsewhere where you are offered a seemingly endless supply of wins. Of course, there is invariably some hidden variance, a steamroller or otherwise, ready to ruin your day at some unexpected time.
::: {.column-margin}
Roger Lowenstein's *When Genius Failed* [[2]](#references) is an excellent book on this topic. It tells the story of how hidden risk brought down Long-Term Capital Management.
:::
**The fallacy is to present a random variable as if it were a deterministic value.** This usually happens unintentionally, but skilled rhetoricians use it frequently. You can use this bit of mathematical sleight of hand to create seemingly valid, but completely incorrect conclusions.
**A river is three feet deep and you don't know how to swim, but surely you can walk across safely.** Well, it depends on what we mean by "three feet". It sounds totally reasonable to express the depth of the river as a constant, but real rivers generally don't agree, and opt for a fluctuating depth that may simply *average* to three feet. If you aren't a strong swimmer - you will not make it across [[3]](#references).
In fact, even my original question was flawed. When I said Team A wins, I meant that Team A wins 77.9% of games in this simplified model where Team A's yards per play are determined by a normal distribution, $N(3.3, 0.8^2)$, and Team B's from a bimodal mixture: 70% $N(0.5, 0.45^2)$ and 30% $N(15.5, 2^2)$. Obviously, this verbose phrase doesn't have a "headline" quality most people look for when making strong statements.
This insight is not new—"but I arrived at it independently” [[4]](#references). Sam Savage describes the broader statistical error as “the flaw of averages”: plans based on average inputs often fail because averages hide uncertainty [[3]](#references).
--------------------
## Appendix: A simplified football model
This is deliberately a simplified model of football. Each team gets 10 drives, every drive starts at its own 25-yard line, and a drive lasts at most 14 plays. Normal downs apply: gain 10 yards within four plays to reset the downs. A touchdown scores 7 points. On fourth down at or beyond the opponent's 35-yard line, the team kicks a 3-point field goal; it also kicks if the 14-play limit ends in field-goal range. We ignore punts, field-goal misses, penalties, turnovers, clock effects, and extra-point variation.
Team A's yards per play follow a low-variance normal distribution, $N(3.3, 0.8^2)$, truncated at zero. Team B is bimodal: 70% of plays come from $N(0.5, 0.45^2)$ and 30% come from $N(15.5, 2^2)$, also truncated at zero.
```{python}
#| label: football-model
#| code-fold: true
#| code-summary: "Show simulation code"
import numpy as np
import plotly.graph_objects as go
rng = np.random.default_rng(42)
def team_a_play(size=None):
return np.maximum(rng.normal(3.3, 0.8, size), 0)
def team_b_play(size=None):
shape = () if size is None else size
boom = rng.random(shape) < .30
return np.where(boom, np.maximum(rng.normal(15.5, 2, shape), 0),
np.maximum(rng.normal(.5, .45, shape), 0))
a_plays = team_a_play(200_000)
b_plays = team_b_play(200_000)
```
```{python}
#| label: yards-per-play
#| echo: false
#| fig-cap: "Probability distributions for yards gained on one play."
#| out-width: 100%
fig = go.Figure()
fig.add_histogram(x=a_plays, histnorm="probability density", nbinsx=80,
name="Team A", marker_color="#2E86AB", opacity=.72)
fig.add_histogram(x=b_plays, histnorm="probability density", nbinsx=80,
name="Team B", marker_color="#E94F37", opacity=.62)
fig.update_layout(title="Yards per play probability functions",
xaxis_title="Yards gained", yaxis_title="Probability density",
barmode="overlay", height=420)
fig.add_vline(x=a_plays.mean(), line_dash="dash", line_color="#2E86AB")
fig.add_vline(x=b_plays.mean(), line_dash="dash", line_color="#E94F37")
fig.add_annotation(x=.98, y=.96, xref="paper", yref="paper",
text=f"Team A mean: {a_plays.mean():.1f}", showarrow=False,
xanchor="right", font=dict(color="#2E86AB"), bgcolor="white")
fig.add_annotation(x=.98, y=.88, xref="paper", yref="paper",
text=f"Team B mean: {b_plays.mean():.1f}", showarrow=False,
xanchor="right", font=dict(color="#E94F37"), bgcolor="white")
fig.update_xaxes(range=[0, 22])
fig.show()
print(f"Team A mean: {a_plays.mean():.2f}; Team B mean: {b_plays.mean():.2f}")
```
```{python}
#| label: game-simulation
#| code-fold: true
#| code-summary: "Show game simulation"
def drive(play):
position, series_yards, down = 25., 0., 1
for _ in range(14):
if down == 4 and position >= 65:
return 3
gain = float(play())
position += gain
series_yards += gain
if position >= 100:
return 7
if series_yards >= 10:
series_yards, down = 0., 1
else:
down += 1
if down > 4:
return 0
return 3 if position >= 65 else 0
def games(play, n=20_000):
return np.array([sum(drive(play) for _ in range(10)) for _ in range(n)])
a_scores, b_scores = games(team_a_play), games(team_b_play)
```
```{python}
#| label: score-distributions
#| echo: false
#| fig-cap: "The resulting probability distributions for points scored per game."
#| out-width: 100%
fig = go.Figure()
for scores, name, color in [
(a_scores, "Team A", "#2E86AB"), (b_scores, "Team B", "#E94F37")
]:
fig.add_histogram(x=scores, histnorm="probability",
xbins=dict(start=-.5, end=70.5, size=1),
name=f"{name} (mean {scores.mean():.1f})",
marker_color=color, opacity=.68)
fig.update_layout(title="Points per game probability functions",
xaxis_title="Points scored", yaxis_title="Probability",
barmode="overlay", height=420)
fig.show()
```
Finally, pair one simulated Team A score with one simulated Team B score. This assumes independent offenses and ignores defensive matchups.
```{python}
#| label: win-probability
#| echo: false
print(f"Team A win probability: {np.mean(a_scores > b_scores):.1%}")
print(f"Team B win probability: {np.mean(b_scores > a_scores):.1%}")
print(f"Tie probability: {np.mean(a_scores == b_scores):.1%}")
```
Team B gains more yards per play on average. Team A still wins more often in this model because its lower-variance plays sustain drives more reliably.
This result depends on the chosen distributions and simplified rules; it does not imply that low-variance offenses generally outperform explosive ones.
## References
[1] RidgeHaven Capital, "Picking Up Nickels In Front Of A Steam Roller" - [Seeking Alpha](https://seekingalpha.com/article/4083775-picking-up-nickels-in-front-of-a-steam-roller)
[2] Lowenstein, Roger, "When Genius Failed: The Rise and Fall of Long-Term Capital Management" - [Penguin Random House](https://www.penguinrandomhouse.com/books/103996/when-genius-failed-by-roger-lowenstein/)
[3] Savage, Sam, "The Flaw of Averages" - [Harvard Business Review](https://hbr.org/2002/11/the-flaw-of-averages)
[4] Hughes, Sarah, "Mad Men: season one, episode four" - [The Guardian](https://www.theguardian.com/tv-and-radio/tvandradioblog/2010/apr/29/mad-men-season-one-episode-four)