Below is a synthetic dataset of 200 people. A count query asks: "How many people in the dataset satisfy some condition?" The true answer is shown in blue. The mechanism adds Laplace(0,1/ε) noise to produce a private answer.
<!-- Laplace mechanism demo: add calibrated noise to a count query. -->
<div class="controls">
<label for="eps-slider">Privacy budget ε = <strong id="eps-val">1.0</strong></label>
<input type="range" id="eps-slider" min="0.1" max="5" step="0.1" value="1.0" title="Drag to adjust epsilon (privacy budget)">
</div>
<div class="controls row">
<button id="btn-query" type="button">Run query</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="result-box" class="result-box" aria-live="polite"></div>
<div class="chart-area">
<div class="bar-row">
<span class="bar-lbl">True count</span>
<div class="bar-track"><div id="bar-true" class="bar true-bar"></div></div>
<span class="bar-num" id="num-true">—</span>
</div>
<div class="bar-row">
<span class="bar-lbl">Private answer</span>
<div class="bar-track"><div id="bar-noisy" class="bar noisy-bar"></div></div>
<span class="bar-num" id="num-noisy">—</span>
</div>
</div>
<p class="hint-text">True count = 73 out of 200. Sensitivity = 1. Noise scale = 1/ε. Lower ε → stronger privacy, wider spread.</p>
/* Base layout and control styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .6rem; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.controls.row { gap: .5rem; }
label { font-size: .9rem; font-weight: 600; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; accent-color: #1d3557; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Result message styles */
.result-box { min-height: 1.6em; font-size: .95rem; font-weight: 600;
padding: .35rem .6rem; border-radius: 6px; margin: .4rem 0; }
.result-box.ok { background: #e6f4ea; color: #0a7d33; }
.result-box.bad { background: #fdecea; color: #c92f3c; }
/* Bar-chart styles */
.chart-area { margin: .5rem 0; }
.bar-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.bar-lbl { width: 70px; font-size: .8rem; font-weight: 600; text-align: right; flex-shrink: 0; }
.bar-track { flex: 1; height: 22px; background: #e8eef3; border-radius: 4px; overflow: hidden; }
.bar { height: 100%; border-radius: 4px; transition: width .25s ease; }
.true-bar { background: #1d3557; }
.noisy-bar { background: #e63946; }
.bar-num { width: 42px; font-size: .8rem; font-weight: 700; text-align: right; flex-shrink: 0; }
.hint-text { font-size: .82rem; color: #555; margin-top: .6rem; line-height: 1.4; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Synthetic dataset: 200 people, 73 satisfy the query condition.
const DATASET_SIZE = 200;
const TRUE_COUNT = 73; // fixed true answer — in a real system this would be computed from raw data
// Global sensitivity: one person changes the count by at most 1.
const SENSITIVITY = 1;
// Sample from Lap(0, scale) using the inverse-CDF method.
function sampleLaplace(scale) {
const u = Math.random() - 0.5;
return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
}
const slider = document.getElementById('eps-slider');
const epsVal = document.getElementById('eps-val');
const btnQuery = document.getElementById('btn-query');
const btnReset = document.getElementById('btn-reset');
const resultBox = document.getElementById('result-box');
const barTrue = document.getElementById('bar-true');
const barNoisy = document.getElementById('bar-noisy');
const numTrue = document.getElementById('num-true');
const numNoisy = document.getElementById('num-noisy');
let currentEps = parseFloat(slider.value);
let queryCount = 0;
slider.addEventListener('input', () => {
currentEps = parseFloat(slider.value);
epsVal.textContent = currentEps.toFixed(1);
// Reset bars when epsilon changes so the user sees a fresh run.
resetDisplay();
});
function resetDisplay() {
resultBox.textContent = '';
resultBox.className = 'result-box';
barTrue.style.width = '0%';
barNoisy.style.width = '0%';
numTrue.textContent = '—';
numNoisy.textContent = '—';
}
function runQuery() {
queryCount++;
const scale = SENSITIVITY / currentEps;
const noise = sampleLaplace(scale);
const rawNoisy = TRUE_COUNT + noise;
// Clamp to [0, DATASET_SIZE] — counts cannot be negative or exceed the population.
const noisy = Math.max(0, Math.min(DATASET_SIZE, Math.round(rawNoisy)));
const maxBar = DATASET_SIZE;
barTrue.style.width = (TRUE_COUNT / maxBar * 100).toFixed(1) + '%';
barNoisy.style.width = (noisy / maxBar * 100).toFixed(1) + '%';
numTrue.textContent = TRUE_COUNT;
numNoisy.textContent = noisy;
const error = Math.abs(noisy - TRUE_COUNT);
if (error <= 5) {
resultBox.className = 'result-box ok';
resultBox.textContent = i18nFmt('Private answer %noisy% (true %true_val%) — within 5. Good accuracy at this ε.', { noisy: noisy, true_val: TRUE_COUNT });
} else {
resultBox.className = 'result-box bad';
resultBox.textContent = i18nFmt('Private answer %noisy% (true %true_val%) — off by %error%. Strong privacy, weak accuracy.', { noisy: noisy, true_val: TRUE_COUNT, error: error });
}
}
btnQuery.addEventListener('click', runQuery);
btnReset.addEventListener('click', () => {
queryCount = 0;
currentEps = parseFloat(slider.value);
resetDisplay();
});
// Start with empty bars; user must press Run to see the first result.
resetDisplay();
<!-- Laplace mechanism demo: add calibrated noise to a count query. -->
<div class="controls">
<label for="eps-slider">Privacy budget ε = <strong id="eps-val">1.0</strong></label>
<input type="range" id="eps-slider" min="0.1" max="5" step="0.1" value="1.0" title="Drag to adjust epsilon (privacy budget)">
</div>
<div class="controls row">
<button id="btn-query" type="button">Run query</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="result-box" class="result-box" aria-live="polite"></div>
<div class="chart-area">
<div class="bar-row">
<span class="bar-lbl">True count</span>
<div class="bar-track"><div id="bar-true" class="bar true-bar"></div></div>
<span class="bar-num" id="num-true">—</span>
</div>
<div class="bar-row">
<span class="bar-lbl">Private answer</span>
<div class="bar-track"><div id="bar-noisy" class="bar noisy-bar"></div></div>
<span class="bar-num" id="num-noisy">—</span>
</div>
</div>
<p class="hint-text">True count = 73 out of 200. Sensitivity = 1. Noise scale = 1/ε. Lower ε → stronger privacy, wider spread.</p>
/* Base layout and control styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .6rem; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.controls.row { gap: .5rem; }
label { font-size: .9rem; font-weight: 600; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; accent-color: #1d3557; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Result message styles */
.result-box { min-height: 1.6em; font-size: .95rem; font-weight: 600;
padding: .35rem .6rem; border-radius: 6px; margin: .4rem 0; }
.result-box.ok { background: #e6f4ea; color: #0a7d33; }
.result-box.bad { background: #fdecea; color: #c92f3c; }
/* Bar-chart styles */
.chart-area { margin: .5rem 0; }
.bar-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.bar-lbl { width: 70px; font-size: .8rem; font-weight: 600; text-align: right; flex-shrink: 0; }
.bar-track { flex: 1; height: 22px; background: #e8eef3; border-radius: 4px; overflow: hidden; }
.bar { height: 100%; border-radius: 4px; transition: width .25s ease; }
.true-bar { background: #1d3557; }
.noisy-bar { background: #e63946; }
.bar-num { width: 42px; font-size: .8rem; font-weight: 700; text-align: right; flex-shrink: 0; }
.hint-text { font-size: .82rem; color: #555; margin-top: .6rem; line-height: 1.4; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Synthetic dataset: 200 people, 73 satisfy the query condition.
const DATASET_SIZE = 200;
const TRUE_COUNT = 73; // fixed true answer — in a real system this would be computed from raw data
// Global sensitivity: one person changes the count by at most 1.
const SENSITIVITY = 1;
// Sample from Lap(0, scale) using the inverse-CDF method.
function sampleLaplace(scale) {
const u = Math.random() - 0.5;
return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
}
const slider = document.getElementById('eps-slider');
const epsVal = document.getElementById('eps-val');
const btnQuery = document.getElementById('btn-query');
const btnReset = document.getElementById('btn-reset');
const resultBox = document.getElementById('result-box');
const barTrue = document.getElementById('bar-true');
const barNoisy = document.getElementById('bar-noisy');
const numTrue = document.getElementById('num-true');
const numNoisy = document.getElementById('num-noisy');
let currentEps = parseFloat(slider.value);
let queryCount = 0;
slider.addEventListener('input', () => {
currentEps = parseFloat(slider.value);
epsVal.textContent = currentEps.toFixed(1);
// Reset bars when epsilon changes so the user sees a fresh run.
resetDisplay();
});
function resetDisplay() {
resultBox.textContent = '';
resultBox.className = 'result-box';
barTrue.style.width = '0%';
barNoisy.style.width = '0%';
numTrue.textContent = '—';
numNoisy.textContent = '—';
}
function runQuery() {
queryCount++;
const scale = SENSITIVITY / currentEps;
const noise = sampleLaplace(scale);
const rawNoisy = TRUE_COUNT + noise;
// Clamp to [0, DATASET_SIZE] — counts cannot be negative or exceed the population.
const noisy = Math.max(0, Math.min(DATASET_SIZE, Math.round(rawNoisy)));
const maxBar = DATASET_SIZE;
barTrue.style.width = (TRUE_COUNT / maxBar * 100).toFixed(1) + '%';
barNoisy.style.width = (noisy / maxBar * 100).toFixed(1) + '%';
numTrue.textContent = TRUE_COUNT;
numNoisy.textContent = noisy;
const error = Math.abs(noisy - TRUE_COUNT);
if (error <= 5) {
resultBox.className = 'result-box ok';
resultBox.textContent = i18nFmt('Private answer %noisy% (true %true_val%) — within 5. Good accuracy at this ε.', { noisy: noisy, true_val: TRUE_COUNT });
} else {
resultBox.className = 'result-box bad';
resultBox.textContent = i18nFmt('Private answer %noisy% (true %true_val%) — off by %error%. Strong privacy, weak accuracy.', { noisy: noisy, true_val: TRUE_COUNT, error: error });
}
}
btnQuery.addEventListener('click', runQuery);
btnReset.addEventListener('click', () => {
queryCount = 0;
currentEps = parseFloat(slider.value);
resetDisplay();
});
// Start with empty bars; user must press Run to see the first result.
resetDisplay();
Drag the ε slider and run the query repeatedly. At low ε the noisy answers scatter widely — strong privacy, weak utility. As ε grows the noise shrinks and the private answer converges on the truth. The sensitivity here is 1 (one person changes a count by at most 1), so the noise scale is exactly 1/ε.
Comments
Loading comments...