The Birthday Paradox, simulated
I'm a fan of simulating counterintuitive statistics. I recently did this with the Monty Hall problem and I really enjoyed how it turned out.
A similarly interesting statistical puzzle is the birthday paradox: you only need to get 23 people in a room a room to end up with a 50% chance of at least one birthday match amongst the group.
This can, of course, be demonstrated using math. But still I think this is a fun simulation problem!
Controls #
Start or stop the simulations and adjust the speed to change how fast simulations are run.
x3
Score #
Cumulative results as we run more simulations.
| Result | Trials | Count | Percentage |
|---|---|---|---|
| Match found | 0 | 0 | - |
| No match | 0 | 0 | - |
Simulation #
Each simulation creates 23 people below and deals out random birthdays.
Code #
If you'd like to run this simulation or just want to peek at the code, you can find it below:
html
section class="board" id="birthday-board">
<div class="people">
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
<div class="person"></div>
</div>
<div class="status"> </div>
</section>
js
let running = false;
const playBtn = document.querySelector("#play-btn");
const speedControl = document.querySelector("#speed-control");
const speedDisplay = document.querySelector("#speed-display");
let speed = parseInt(speedControl.value);
const randi = (n) => Math.floor(Math.random() * n);
const sleep = (s) =>
new Promise((res) => setTimeout(res, (s * 1000) / Math.max(1, speed)));
const dayToMonthDay = (dayNum) => {
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let day = dayNum;
for (let m = 0; m < 12; m++) {
if (day < daysPerMonth[m]) {
return months[m] + " " + (day + 1);
}
day -= daysPerMonth[m];
}
};
let trialsWithMatch = 0;
let trialsWithoutMatch = 0;
const trialsEl = document.querySelector("#trials");
const matchesEl = document.querySelector("#matches");
const matchPctEl = document.querySelector("#match-pct");
const trialsNmEl = document.querySelector("#trials-nm");
const noMatchesEl = document.querySelector("#no-matches");
const noMatchPctEl = document.querySelector("#no-match-pct");
const pct = (count, total) =>
total ? ((100 * count) / total).toFixed(1) + "%" : "-";
const updateScoreboard = () => {
const totalTrials = trialsWithMatch + trialsWithoutMatch;
trialsEl.textContent = totalTrials;
matchesEl.textContent = trialsWithMatch;
matchPctEl.textContent = pct(trialsWithMatch, totalTrials);
trialsNmEl.textContent = totalTrials;
noMatchesEl.textContent = trialsWithoutMatch;
noMatchPctEl.textContent = pct(trialsWithoutMatch, totalTrials);
};
class BirthdayBoard {
constructor() {
this.root = document.getElementById("birthday-board");
this.personEls = Array.from(this.root.querySelectorAll(".person"));
this.statusEl = this.root.querySelector(".status");
}
clear() {
this.personEls.forEach((el) => {
el.textContent = "";
el.classList.remove("match");
});
}
status(t) {
this.statusEl.textContent = t;
}
hasMatchingBirthdays(birthdays) {
const seen = new Set();
for (let bd of birthdays) {
if (seen.has(bd)) {
return true;
}
seen.add(bd);
}
return false;
}
async playOnce() {
if (!running) return;
this.status("\u00A0");
this.clear();
const birthdays = [];
for (let i = 0; i < 23; i++) {
birthdays.push(randi(365));
}
await sleep(0.3);
this.status("Assigning birthdays...");
for (let i = 0; i < 23; i++) {
this.personEls[i].textContent = dayToMonthDay(birthdays[i]);
await sleep(0.1);
}
await sleep(0.5);
this.status("Checking for matches...");
await sleep(0.5);
const hasMatch = this.hasMatchingBirthdays(birthdays);
if (hasMatch) {
const seen = new Map();
for (let i = 0; i < 23; i++) {
if (seen.has(birthdays[i])) {
const j = seen.get(birthdays[i]);
this.personEls[i].classList.add("match");
this.personEls[j].classList.add("match");
this.status("✓ Match found! (" + dayToMonthDay(birthdays[i]) + ")");
trialsWithMatch++;
break;
}
seen.set(birthdays[i], i);
}
} else {
this.status("✗ No matching birthdays");
trialsWithoutMatch++;
}
updateScoreboard();
await sleep(2);
}
start() {
const loop = async () => {
while (running) {
await this.playOnce();
}
};
loop();
}
}
const board = new BirthdayBoard();
playBtn.addEventListener("click", () => {
running = !running;
playBtn.textContent = running ? "Stop simulation" : "Start simulation";
if (running) board.start();
});
speedControl.addEventListener("input", (e) => {
speed = parseInt(e.target.value) || 1;
speedDisplay.textContent = "x" + speed;
});
updateScoreboard();
Enjoy this post? Please subscribe to my RSS feed on Feedly or add my RSS XML file to another reader!
- Previous: Pre-agent nostalgia