Skip to main content
pcloadletter

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!