<!-- Chernoff / Hoeffding concentration demo: flip n fair coins, count how often the fraction of heads deviates from 0.5 by more than delta. -->
<div class="controls">
<label>
<span class="lbl">Flips (n)</span>
<input type="range" id="nSlider" min="10" max="500" value="50" step="10">
<span id="nVal">50</span>
</label>
<label>
<span class="lbl">Deviation (δ %)</span>
<input type="range" id="dSlider" min="1" max="30" value="10" step="1">
<span id="dVal">0.10</span>
</label>
<label>
<span class="lbl">Trials</span>
<input type="range" id="tSlider" min="100" max="2000" value="500" step="100">
<span id="tVal">500</span>
</label>
</div>
<div class="hint">Each trial flips n fair coins and checks whether the fraction of heads is outside [0.5 − δ, 0.5 + δ]. The red dashed line is the Hoeffding upper bound 2·exp(−2nδ²); the blue line is the measured frequency.</div>
<button id="runBtn" type="button">Run trials</button>
<div id="canvas-wrap"><canvas id="chart" width="480" height="200"></canvas></div>
<div class="results" id="results"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .5rem; }
label { display: flex; align-items: center; gap: .4rem; font-size: .88rem; }
.lbl { width: 9rem; flex-shrink: 0; font-weight: 600; }
input[type=range] { flex: 1; }
.hint { font-size: .85rem; color: #555; margin: .4rem 0; line-height: 1.45; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-bottom: .5rem; }
#canvas-wrap { overflow-x: auto; }
canvas { display: block; max-width: 100%; border: 1px solid #dde3e9; border-radius: 6px; background: #f9fbfd; }
.results { font-size: .9rem; margin-top: .5rem; line-height: 1.6; }
.results strong { color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Chernoff/Hoeffding demo: empirical tail frequency vs Hoeffding bound 2*exp(-2*n*delta^2).
// Read slider values.
const nSlider = document.getElementById('nSlider');
const dSlider = document.getElementById('dSlider');
const tSlider = document.getElementById('tSlider');
const nVal = document.getElementById('nVal');
const dVal = document.getElementById('dVal');
const tVal = document.getElementById('tVal');
// Keep display values in sync with sliders.
function syncLabels() {
nVal.textContent = nSlider.value;
dVal.textContent = (parseInt(dSlider.value, 10) / 100).toFixed(2);
tVal.textContent = tSlider.value;
}
nSlider.addEventListener('input', syncLabels);
dSlider.addEventListener('input', syncLabels);
tSlider.addEventListener('input', syncLabels);
syncLabels();
// Hoeffding bound: Pr[|mean - 0.5| > delta] <= 2*exp(-2*n*delta^2).
function hoeffdingBound(n, delta) {
return 2 * Math.exp(-2 * n * delta * delta);
}
// Run numTrials coin-flip experiments; return fraction where |frac_heads - 0.5| > delta.
function runTrials(n, delta, numTrials) {
let outside = 0;
for (let t = 0; t < numTrials; t++) {
let heads = 0;
for (let i = 0; i < n; i++) heads += Math.random() < 0.5 ? 1 : 0;
const frac = heads / n;
if (Math.abs(frac - 0.5) > delta) outside++;
}
return outside / numTrials;
}
// Draw the chart showing bound vs empirical across a range of n values.
function drawChart(n, delta, numTrials) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 24, right: 20, bottom: 36, left: 48 };
ctx.clearRect(0, 0, W, H);
// Build a series of n values from 10 to nMax for the x-axis.
const steps = 20;
const nMax = Math.max(n * 2, 100);
const ns = Array.from({ length: steps + 1 }, (_, i) => Math.max(10, Math.round((nMax / steps) * i)));
const empirical = ns.map(ni => runTrials(ni, delta, Math.min(numTrials, 400)));
const bounds = ns.map(ni => hoeffdingBound(ni, delta));
const innerW = W - PAD.left - PAD.right;
const innerH = H - PAD.top - PAD.bottom;
// Draw x and y axes.
ctx.strokeStyle = '#bbc5ce';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + innerH);
ctx.lineTo(PAD.left + innerW, PAD.top + innerH);
ctx.stroke();
// Y-axis labels (probability 0..1) with horizontal grid lines.
ctx.fillStyle = '#555';
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'right';
for (let p = 0; p <= 100; p += 25) {
const y = PAD.top + innerH - (p / 100) * innerH;
ctx.fillText((p / 100).toFixed(2), PAD.left - 4, y + 4);
ctx.strokeStyle = '#e5eaee';
ctx.lineWidth = 0.7;
ctx.beginPath(); ctx.moveTo(PAD.left, y); ctx.lineTo(PAD.left + innerW, y); ctx.stroke();
}
// X-axis labels: show selected n values.
ctx.textAlign = 'center';
ctx.fillStyle = '#555';
[0, 0.5, 1].forEach(frac => {
const ni = ns[Math.round(frac * steps)];
const x = PAD.left + frac * innerW;
ctx.fillText(String(ni), x, PAD.top + innerH + 16);
});
const xOf = (i) => PAD.left + (i / steps) * innerW;
const yOf = (v) => PAD.top + innerH - Math.min(v, 1) * innerH;
// Plot the Hoeffding bound curve (dashed red).
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2;
ctx.setLineDash([4, 3]);
ctx.beginPath();
bounds.forEach((b, i) => { i === 0 ? ctx.moveTo(xOf(i), yOf(b)) : ctx.lineTo(xOf(i), yOf(b)); });
ctx.stroke();
ctx.setLineDash([]);
// Plot the empirical tail frequency curve (solid blue).
ctx.strokeStyle = '#1d6fc4';
ctx.lineWidth = 2;
ctx.beginPath();
empirical.forEach((e, i) => { i === 0 ? ctx.moveTo(xOf(i), yOf(e)) : ctx.lineTo(xOf(i), yOf(e)); });
ctx.stroke();
// Legend labels.
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#e63946'; ctx.fillRect(PAD.left + 4, PAD.top + 4, 16, 3);
ctx.fillStyle = '#333'; ctx.textAlign = 'left'; ctx.fillText('Hoeffding bound', PAD.left + 24, PAD.top + 8);
ctx.fillStyle = '#1d6fc4'; ctx.fillRect(PAD.left + 4, PAD.top + 18, 16, 3);
ctx.fillStyle = '#333'; ctx.fillText('Empirical frequency', PAD.left + 24, PAD.top + 22);
// X-axis label.
ctx.fillStyle = '#555'; ctx.textAlign = 'center';
ctx.fillText('n', PAD.left + innerW / 2, H - 4);
}
// Run everything and update the results text.
function run() {
const n = parseInt(nSlider.value, 10);
const delta = parseInt(dSlider.value, 10) / 100;
const numTrials = parseInt(tSlider.value, 10);
const empRate = runTrials(n, delta, numTrials);
const bound = hoeffdingBound(n, delta);
drawChart(n, delta, numTrials);
const pct = (empRate * 100).toFixed(1);
const bpct = (bound * 100).toFixed(1);
document.getElementById('results').innerHTML =
i18nFmt('For <strong>n = %n%</strong>, δ = %delta%: empirical tail = <strong>%emp%%</strong> of %trials% trials | Hoeffding bound = <strong>%bound%%</strong>.', { n: n, delta: delta.toFixed(2), trials: numTrials,
emp: pct, bound: bpct });
}
document.getElementById('runBtn').addEventListener('click', run);
run();
<!-- Chernoff / Hoeffding concentration demo: flip n fair coins, count how often the fraction of heads deviates from 0.5 by more than delta. -->
<div class="controls">
<label>
<span class="lbl">Flips (n)</span>
<input type="range" id="nSlider" min="10" max="500" value="50" step="10">
<span id="nVal">50</span>
</label>
<label>
<span class="lbl">Deviation (δ %)</span>
<input type="range" id="dSlider" min="1" max="30" value="10" step="1">
<span id="dVal">0.10</span>
</label>
<label>
<span class="lbl">Trials</span>
<input type="range" id="tSlider" min="100" max="2000" value="500" step="100">
<span id="tVal">500</span>
</label>
</div>
<div class="hint">Each trial flips n fair coins and checks whether the fraction of heads is outside [0.5 − δ, 0.5 + δ]. The red dashed line is the Hoeffding upper bound 2·exp(−2nδ²); the blue line is the measured frequency.</div>
<button id="runBtn" type="button">Run trials</button>
<div id="canvas-wrap"><canvas id="chart" width="480" height="200"></canvas></div>
<div class="results" id="results"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .5rem; }
label { display: flex; align-items: center; gap: .4rem; font-size: .88rem; }
.lbl { width: 9rem; flex-shrink: 0; font-weight: 600; }
input[type=range] { flex: 1; }
.hint { font-size: .85rem; color: #555; margin: .4rem 0; line-height: 1.45; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-bottom: .5rem; }
#canvas-wrap { overflow-x: auto; }
canvas { display: block; max-width: 100%; border: 1px solid #dde3e9; border-radius: 6px; background: #f9fbfd; }
.results { font-size: .9rem; margin-top: .5rem; line-height: 1.6; }
.results strong { color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Chernoff/Hoeffding demo: empirical tail frequency vs Hoeffding bound 2*exp(-2*n*delta^2).
// Read slider values.
const nSlider = document.getElementById('nSlider');
const dSlider = document.getElementById('dSlider');
const tSlider = document.getElementById('tSlider');
const nVal = document.getElementById('nVal');
const dVal = document.getElementById('dVal');
const tVal = document.getElementById('tVal');
// Keep display values in sync with sliders.
function syncLabels() {
nVal.textContent = nSlider.value;
dVal.textContent = (parseInt(dSlider.value, 10) / 100).toFixed(2);
tVal.textContent = tSlider.value;
}
nSlider.addEventListener('input', syncLabels);
dSlider.addEventListener('input', syncLabels);
tSlider.addEventListener('input', syncLabels);
syncLabels();
// Hoeffding bound: Pr[|mean - 0.5| > delta] <= 2*exp(-2*n*delta^2).
function hoeffdingBound(n, delta) {
return 2 * Math.exp(-2 * n * delta * delta);
}
// Run numTrials coin-flip experiments; return fraction where |frac_heads - 0.5| > delta.
function runTrials(n, delta, numTrials) {
let outside = 0;
for (let t = 0; t < numTrials; t++) {
let heads = 0;
for (let i = 0; i < n; i++) heads += Math.random() < 0.5 ? 1 : 0;
const frac = heads / n;
if (Math.abs(frac - 0.5) > delta) outside++;
}
return outside / numTrials;
}
// Draw the chart showing bound vs empirical across a range of n values.
function drawChart(n, delta, numTrials) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 24, right: 20, bottom: 36, left: 48 };
ctx.clearRect(0, 0, W, H);
// Build a series of n values from 10 to nMax for the x-axis.
const steps = 20;
const nMax = Math.max(n * 2, 100);
const ns = Array.from({ length: steps + 1 }, (_, i) => Math.max(10, Math.round((nMax / steps) * i)));
const empirical = ns.map(ni => runTrials(ni, delta, Math.min(numTrials, 400)));
const bounds = ns.map(ni => hoeffdingBound(ni, delta));
const innerW = W - PAD.left - PAD.right;
const innerH = H - PAD.top - PAD.bottom;
// Draw x and y axes.
ctx.strokeStyle = '#bbc5ce';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + innerH);
ctx.lineTo(PAD.left + innerW, PAD.top + innerH);
ctx.stroke();
// Y-axis labels (probability 0..1) with horizontal grid lines.
ctx.fillStyle = '#555';
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'right';
for (let p = 0; p <= 100; p += 25) {
const y = PAD.top + innerH - (p / 100) * innerH;
ctx.fillText((p / 100).toFixed(2), PAD.left - 4, y + 4);
ctx.strokeStyle = '#e5eaee';
ctx.lineWidth = 0.7;
ctx.beginPath(); ctx.moveTo(PAD.left, y); ctx.lineTo(PAD.left + innerW, y); ctx.stroke();
}
// X-axis labels: show selected n values.
ctx.textAlign = 'center';
ctx.fillStyle = '#555';
[0, 0.5, 1].forEach(frac => {
const ni = ns[Math.round(frac * steps)];
const x = PAD.left + frac * innerW;
ctx.fillText(String(ni), x, PAD.top + innerH + 16);
});
const xOf = (i) => PAD.left + (i / steps) * innerW;
const yOf = (v) => PAD.top + innerH - Math.min(v, 1) * innerH;
// Plot the Hoeffding bound curve (dashed red).
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2;
ctx.setLineDash([4, 3]);
ctx.beginPath();
bounds.forEach((b, i) => { i === 0 ? ctx.moveTo(xOf(i), yOf(b)) : ctx.lineTo(xOf(i), yOf(b)); });
ctx.stroke();
ctx.setLineDash([]);
// Plot the empirical tail frequency curve (solid blue).
ctx.strokeStyle = '#1d6fc4';
ctx.lineWidth = 2;
ctx.beginPath();
empirical.forEach((e, i) => { i === 0 ? ctx.moveTo(xOf(i), yOf(e)) : ctx.lineTo(xOf(i), yOf(e)); });
ctx.stroke();
// Legend labels.
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#e63946'; ctx.fillRect(PAD.left + 4, PAD.top + 4, 16, 3);
ctx.fillStyle = '#333'; ctx.textAlign = 'left'; ctx.fillText('Hoeffding bound', PAD.left + 24, PAD.top + 8);
ctx.fillStyle = '#1d6fc4'; ctx.fillRect(PAD.left + 4, PAD.top + 18, 16, 3);
ctx.fillStyle = '#333'; ctx.fillText('Empirical frequency', PAD.left + 24, PAD.top + 22);
// X-axis label.
ctx.fillStyle = '#555'; ctx.textAlign = 'center';
ctx.fillText('n', PAD.left + innerW / 2, H - 4);
}
// Run everything and update the results text.
function run() {
const n = parseInt(nSlider.value, 10);
const delta = parseInt(dSlider.value, 10) / 100;
const numTrials = parseInt(tSlider.value, 10);
const empRate = runTrials(n, delta, numTrials);
const bound = hoeffdingBound(n, delta);
drawChart(n, delta, numTrials);
const pct = (empRate * 100).toFixed(1);
const bpct = (bound * 100).toFixed(1);
document.getElementById('results').innerHTML =
i18nFmt('For <strong>n = %n%</strong>, δ = %delta%: empirical tail = <strong>%emp%%</strong> of %trials% trials | Hoeffding bound = <strong>%bound%%</strong>.', { n: n, delta: delta.toFixed(2), trials: numTrials,
emp: pct, bound: bpct });
}
document.getElementById('runBtn').addEventListener('click', run);
run();
Comments
Loading comments...