OS-006 · Probability

Coin Flip Simulator

A coin flip is the simplest random experiment, and it already shows the two things people most often get wrong about chance. The proportion of heads really does settle toward p, yet the coin never “evens out” a streak — and runs of heads longer than most people expect turn up in almost every long sequence.

Interactive model

OS-006 · coin-flip · seeded
running proportion of headspp ± 2 standard errors
headstails (outlined)
Parameters
Number of independent flips
0.5 is a fair coin
Same seed, same run — here, in simulation.js and in simulation.py.
Flips
1,000
Heads
537
Proportion of heads p̂
0.5370
Error |p̂ − p|
0.0370
Standard error √(p(1−p)/n)
0.0158
Longest run of heads
9
Expected longest run (approx.)
9.3
Heads right after 3+ heads
0.556
Flips that followed 3+ heads
160

Example run: default parameters, seed 42.

What the law of large numbers says — and what it does not

After n flips with H heads, the proportion p̂ = H/n has expected value p and standard error

SE(p̂) = √( p(1 − p) / n )

The dashed band in the lab is p ± 2 SE. It narrows like 1/√n, and the running proportion ends up inside it in roughly 95% of runs. That is the law of large numbers: the proportion converges to p.

The count does not. The typical gap between the number of heads and np is about √(2np(1 − p)/π): roughly 13 heads for 1,000 fair flips and 126 for 100,000. The gap grows, just more slowly than n, so dividing by n squeezes it toward zero. Nothing pulls the count back toward half.

Streaks and the gambler’s fallacy

The gambler’s fallacy is the belief that after a run of heads, tails is “due”. Each flip is independent, so the chance of heads is p whatever came before. The lab tests this on its own data: Heads right after 3+ heads is the proportion of heads among flips that immediately followed three or more heads in a row. It stays close to p, off only by sampling noise, and the gap shrinks as you add flips. The law of large numbers works by swamping early luck with new flips, not by correcting it.

Long streaks are the other surprise. A run of at least L heads starts at a given flip, just after a tail, with probability (1 − p)pL, so a sequence of n flips contains about n(1 − p)pL of them. Setting that to one gives the typical longest run; the extreme-value limit adds a constant to get its mean:

E[longest run] ≈ log1/p(n(1 − p)) + γ / ln(1/p) − ½

Here γ ≈ 0.5772 is Euler’s constant. For a fair coin this is log₂ n − 0.67: about 9.3 heads in a row somewhere in 1,000 flips, and a run of either face about one flip longer. The formula needs n(1 − p) ≥ 5; below that the readout is left blank.

How the simulation works

  1. For each flip draw u ~ U[0, 1); the flip is heads if u < p.
  2. Update the head count, the current run of heads and the longest run so far. If the current run was already 3 or more, record whether this flip was heads.
  3. Store the running proportion at checkpoints about 3% apart, so the log-scaled chart has even detail from the first flip to the last.

The standard error uses the true p, which the simulation knows. With real data you would plug in p̂ instead, as in how accurate a Monte Carlo estimate is.

Example result

With seed 42, 1,000 flips of a fair coin gave 537 heads, a proportion of 0.5370. The error of 0.0370 compares with a standard error of 0.0158. The longest run of heads was 9 against an expected 9.3, and of the 160 flips that followed three or more heads, a proportion of 0.556 were heads again.

Output of coin-flip with n=1000 p=0.5 seed=42
Flips1,000
Heads537
Proportion of heads p̂0.5370
Error |p̂ − p|0.0370
Standard error √(p(1−p)/n)0.0158
Longest run of heads9
Expected longest run (approx.)9.3
Heads right after 3+ heads0.556
Flips that followed 3+ heads160

Implementation

The page runs simulation.js from the package below; the Python file is a line-by-line port with the same random-number generator, so a given seed produces the same result in both languages. The excerpts show the core of each.

JavaScript — simulation.js
// Flip k more coins. Each flip is heads with probability p, independently of every
// earlier flip; the state only counts heads and tracks the current run of heads.
export function advance(s, k, rng, onFlip) {
  const end = Math.min(s.n, s.i + k);
  while (s.i < end) {
    const heads = rng.uniform() < s.p;
    // Gambler's fallacy check: how often does heads follow a streak of 3+ heads?
    if (s.run >= STREAK) { s.afterFlips++; if (heads) s.afterHeads++; }
    s.i++;
    if (heads) {
      s.heads++;
      s.run++;
      if (s.run > s.longest) s.longest = s.run;
    } else {
      s.run = 0;
    }
    if (onFlip) onFlip(heads);
    // Checkpoints about 3% apart in n, for a log-scaled convergence plot.
    if (s.i >= s.next || s.i === s.n) {
      s.cp.push([s.i, s.heads, s.heads / s.i]);
      s.next = Math.max(s.i + 1, Math.floor(s.i * 1.03));
    }
  }
}

// Longest run of heads in n flips: about n·q·p^L runs of length ≥ L start in the
// sequence, so the longest run sits near L = log_{1/p}(n·q). The Gumbel limit adds
// γ/ln(1/p) − 1/2 to the mean. Needs n·q ≥ 5 to be useful.
export function expectedLongestRun(n, p) {
  const q = 1 - p;
  if (n * q < 5) return null;
  const lnInv = Math.log(1 / p);
  return Math.log(n * q) / lnInv + EULER_GAMMA / lnInv - 0.5;
}
Python — simulation.py
def flip_coins(n, p, rng):
    """Flip n coins with P(heads) = p. Count heads, the longest run of heads, and
    how often heads follows a streak of STREAK or more heads."""
    heads = run = longest = after_flips = after_heads = 0
    for _ in range(n):
        h = rng.uniform() < p
        if run >= STREAK:
            after_flips += 1
            after_heads += h
        if h:
            heads += 1
            run += 1
            longest = max(longest, run)
        else:
            run = 0
    return heads, longest, after_heads / max(after_flips, 1)


def expected_longest_run(n, p):
    """About n*q*p**L runs of length >= L start in n flips, so the longest run is
    near log_{1/p}(n*q); the Gumbel limit adds gamma/ln(1/p) - 1/2 to the mean."""
    q = 1 - p
    if n * q < 5:
        return None
    ln_inv = math.log(1 / p)
    return math.log(n * q) / ln_inv + EULER_GAMMA / ln_inv - 0.5

Run the full model from a terminal:

Shell
node simulation.js --n 1000 --p 0.5 --seed 42
python simulation.py --n 1000 --p 0.5 --seed 42

Download the model

coin-flip.zip6 files · 10.2 KBDownload .zip
Files in coin-flip.zip
FileContentsSize
README.mdHow to run it, parameters, example result2.2 KB
model.jsonParameters with ranges and defaults, outputs, the seed-42 example2.0 KB
simulation.jsThe model used on this page plus a command-line runner (Node 18+)7.5 KB
simulation.pyPython 3.9+ port, standard library only6.4 KB
example.csvResult table from the example run3.9 KB
LICENSE.txtMIT License1.0 KB

Assumptions & limitations

  • Independent flips with a fixed probability. Every flip has the same p and no memory. Real tossed coins can carry a small bias from how they are thrown; this coin has exactly the bias you set.
  • Pseudo-random numbers. Flips come from a seeded generator (xoshiro128**), so a seed reproduces a run exactly; see pseudo-random numbers and seeds.
  • The ±2 SE band is a normal approximation. It is rough for the first few dozen flips and when p is close to 0 or 1.
  • The expected longest run is asymptotic. Against simulated averages it is within about 10% for short sequences and much closer for long ones.

References

  1. Grinstead, C. M. & Snell, J. L. (1997). Introduction to Probability, 2nd ed. American Mathematical Society. math.dartmouth.edu/~prob/prob/prob.pdf
  2. Feller, W. (1968). An Introduction to Probability Theory and Its Applications, Vol. 1, 3rd ed. Wiley.