Why 23 people are enough
Any two people share a birthday with probability 1/365. With 23 people there are 23 · 22 / 2 = 253 pairs, so the expected number of matching pairs is 253/365 ≈ 0.69. The pairs are not quite independent, but treating them as if they were gives a good approximation:
For 23 people and 365 days the exponent is −0.693 = −ln 2, which puts the probability at one half. Solving for even odds gives g ≈ √(2 ln 2 · d) ≈ 1.18√d, about 22.5 for a 365-day year. The number of pairs grows with the square of the group size, and that is the whole paradox.
Compare the question people usually answer in their heads: the chance that one of 22 others shares your birthday is 1 − (364/365)²² ≈ 5.9%. For even odds on that, you need 253 other people.
The exact probability
Seat people one at a time. The second avoids the first birthday with probability (d − 1)/d, the third avoids both with (d − 2)/d, and so on:
With 365 days: 30 people give 70.6%, 50 give 97.0%, 57 give 99.0% and 70 give 99.9%. When the group is larger than the number of days, a match is certain. The curve in the lab is this formula; the orange point is the simulation, with a whisker of ±1.96 standard errors.
The same arithmetic governs collisions among random identifiers and hash values. With d equally likely values, a repeat becomes more likely than not after about 1.18√d draws — the “birthday bound” behind birthday attacks on hash functions.
How the simulation works
- Draw g birthdays independently and uniformly from d days.
- Count matching pairs: each new birthday adds the number of people already on that day. A per-day stamp records which trial last used the day, so nothing needs clearing between groups.
- A group with at least one pair counts as a success. The estimate is the fraction of successes; its standard error is √(p̂(1 − p̂)/T) for T groups.
Step draws one group at a time so you can see what groups of 23 look like on the calendar grid: about half have no shared day at all, and most of the rest have a single matching pair.
Example result
With seed 42, 5,145 of 10,000 simulated groups of 23 contained a shared birthday: an estimate of 51.45% against the exact 50.73%, with a standard error of 0.50%. The groups averaged 0.701 matching pairs; the expected number is 0.693.
| Groups simulated | 10,000 |
|---|---|
| Groups with a shared birthday | 5,145 |
| Simulated probability | 51.45% |
| Exact probability | 50.73% |
| Standard error | 0.50% |
| Matching pairs per group | 0.701 |
| Expected pairs C(g,2)/d | 0.693 |
| Smallest group with P ≥ 50% | 23 |
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.
// Exact probability that at least two of g people share a birthday when each of d
// days is equally likely: one minus the chance that all g birthdays differ.
export function exactProbability(g, d) {
let allDifferent = 1;
for (let i = 0; i < g; i++) allDifferent *= (d - i) / d;
return 1 - allDifferent;
}
// One simulated group. stamp[] marks the days already used in this trial, so the
// arrays never need clearing; pairs counts every matching pair of people.
export function trial(s, rng) {
let pairs = 0;
for (let j = 0; j < s.group; j++) {
const day = rng.int(s.days);
s.birthdays[j] = day;
if (s.stamp[day] !== s.t) { s.stamp[day] = s.t; s.count[day] = 0; }
pairs += s.count[day];
s.count[day]++;
}
s.t++;
if (pairs > 0) s.hits++;
s.pairs += pairs;
s.lastPairs = pairs;
return pairs > 0;
}def exact_probability(g, d):
"""P(at least two of g people share a birthday), d equally likely days."""
all_different = 1
for i in range(g):
all_different *= (d - i) / d
return 1 - all_different
def simulate_group(g, d, rng):
"""Draw g birthdays; return the number of matching pairs of people."""
seen = {}
pairs = 0
for _ in range(g):
day = rng.int(d)
pairs += seen.get(day, 0)
seen[day] = seen.get(day, 0) + 1
return pairsRun the full model from a terminal:
node simulation.js --group 23 --trials 10000 --seed 42
python simulation.py --group 23 --trials 10000 --seed 42Download the model
| File | Contents | Size |
|---|---|---|
README.md | How to run it, parameters, example result | 2.2 KB |
model.json | Parameters with ranges and defaults, outputs, the seed-42 example | 2.1 KB |
simulation.js | The model used on this page plus a command-line runner (Node 18+) | 7.5 KB |
simulation.py | Python 3.9+ port, standard library only | 5.9 KB |
example.csv | Result table from the example run | 6.1 KB |
LICENSE.txt | MIT License | 1.0 KB |
Assumptions & limitations
- Every day equally likely. Real birth dates are not uniform: births vary by season and are fewer on weekends and holidays, when fewer deliveries are scheduled. Any departure from uniform makes a shared birthday more likely, so the uniform model gives a lower bound on the real probability.
- No February 29. The default year has 365 days; the days parameter lets you change the count, not the calendar.
- Independent people. Twins and other linked birthdays are left out; they also raise the chance of a match.
- Sampling error. The simulated estimate differs from the exact value by an amount on the order of its standard error; more groups shrink it like 1/√T.
References
- Grinstead, C. M. & Snell, J. L. (1997). Introduction to Probability, 2nd ed. American Mathematical Society. math.dartmouth.edu/~prob/prob/prob.pdf
- Feller, W. (1968). An Introduction to Probability Theory and Its Applications, Vol. 1, 3rd ed. Wiley.