Twelve people answered a sensitive yes/no question. Below you see the exact count of "yes" answers and a differentially private count that has calibrated noise added. Flip any single person and compare how the two numbers react.
<p class="hint">A survey asks 12 people a sensitive yes/no question. The true count of "yes" answers is shown. Toggle <b>one person's</b> answer and watch how the result reacts — first with the <b>exact</b> count, then with <b>differentially private</b> noise added. Noise hides the individual while keeping the overall total useful.</p>
<div class="people" id="people"></div>
<div class="readout">
<div class="card"><div class="lbl">Exact count</div><div class="val" id="exact">7</div></div>
<div class="card priv"><div class="lbl">Private answer (with noise)</div><div class="val" id="noisy">7</div></div>
</div>
<div class="status" id="status">Flip a person and compare the two answers.</div>
<div class="btns">
<label class="eps">Privacy budget ε
<input id="eps" type="range" min="0.1" max="4" step="0.1" value="0.5">
<span id="epsv">0.5</span>
</label>
<button id="requery" type="button">Re-run private query</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.people { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin: .4rem 0 .8rem; }
.person { height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 16px system-ui, sans-serif; border-radius: 8px; cursor: pointer;
user-select: none; border: 1px solid #cdd9e3; background: #e8eef3; color: #1d3557;
transition: all .12s; }
.person.yes { background: #1d3557; color: #fff; border-color: #16294a; }
.person.flagged { outline: 3px solid #f4a259; outline-offset: 1px; }
.readout { display: flex; gap: 12px; margin: .5rem 0; flex-wrap: wrap; }
.card { flex: 1; min-width: 140px; border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem .8rem; background: #fafcfe; }
.card.priv { border-color: #2a9d8f; background: #eef8f6; }
.lbl { font-size: .78rem; color: #556; text-transform: uppercase; letter-spacing: .03em; }
.val { font: 800 30px ui-monospace, monospace; color: #1d3557; }
.card.priv .val { color: #1d7d6e; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #444; }
.btns { display: flex; gap: .7rem; flex-wrap: wrap; align-items: center; }
.eps { font-size: .85rem; color: #333; display: flex; align-items: center; gap: .4rem; }
.eps input { vertical-align: middle; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Differential privacy via the Laplace mechanism. A counting query has "sensitivity" 1: one person flipping their answer changes the true count by 1. To hide that one person, we add noise drawn from a Laplace distribution with scale b = sensitivity / epsilon. Smaller epsilon => more noise => more privacy, but a noisier (less accurate) answer. The published number reveals the trend of the crowd while making any single person's contribution statistically deniable.
const N = 12;
// Initial answers: 7 "yes". The first person (index 0) is the one we'll flip.
const answers = [1,1,0,1,0,1,1,0,1,0,0,1];
let flagged = 0; // the highlighted "target" individual
let epsilon = 0.5;
function trueCount(){ return answers.reduce((a,b)=>a+b,0); }
// Sample from a Laplace(0, b) distribution using inverse transform sampling.
function laplace(b){
const u = Math.random() - 0.5; // u in (-0.5, 0.5)
return -b * Math.sign(u) * Math.log(1 - 2*Math.abs(u));
}
// Differentially private count: true count + Laplace noise, scale = 1/epsilon.
function privateCount(){
const noisy = trueCount() + laplace(1/epsilon);
return Math.max(0, Math.round(noisy)); // counts are non-negative integers
}
function renderPeople(){
const box = document.getElementById('people'); box.innerHTML='';
for(let i=0;i<N;i++){
const d=document.createElement('div');
d.className='person'+(answers[i]?' yes':'')+(i===flagged?' flagged':'');
d.textContent=answers[i]?'Yes':'No';
d.addEventListener('click',()=>{
answers[i]=answers[i]?0:1; flagged=i; update(true);
});
box.appendChild(d);
}
}
function setExact(){ document.getElementById('exact').textContent=trueCount(); }
let lastNoisy=privateCount();
function setNoisy(reroll){ if(reroll) lastNoisy=privateCount();
document.getElementById('noisy').textContent=lastNoisy; }
function status(t){ document.getElementById('status').textContent=t; }
function update(flipped){
renderPeople(); setExact(); setNoisy(true);
if(flipped){
status(fmt('You flipped person %flagged%%v%. The EXACT count jumped by 1 and leaks their answer. The PRIVATE answer barely reacts — it could have changed for many reasons.', { flagged: flagged, v: 1 }));
}
}
document.getElementById('eps').addEventListener('input',e=>{
epsilon=parseFloat(e.target.value); document.getElementById('epsv').textContent=epsilon.toFixed(1);
setNoisy(true);
status('ε = '+epsilon.toFixed(1)+(epsilon<1?' → lots of noise, strong privacy.':epsilon>2?' → little noise, weak privacy.':' → a balanced trade-off.'));
});
document.getElementById('requery').addEventListener('click',()=>{ setNoisy(true);
status('Re-ran the private query: same data, fresh noise, a different published number.'); });
document.getElementById('reset').addEventListener('click',()=>{
const init=[1,1,0,1,0,1,1,0,1,0,0,1];
for(let i=0;i<N;i++) answers[i]=init[i];
flagged=0; epsilon=0.5; document.getElementById('eps').value=0.5;
document.getElementById('epsv').textContent='0.5';
update(false); status('Reset. Flip a person and compare the two answers.');
});
update(false);
<p class="hint">A survey asks 12 people a sensitive yes/no question. The true count of "yes" answers is shown. Toggle <b>one person's</b> answer and watch how the result reacts — first with the <b>exact</b> count, then with <b>differentially private</b> noise added. Noise hides the individual while keeping the overall total useful.</p>
<div class="people" id="people"></div>
<div class="readout">
<div class="card"><div class="lbl">Exact count</div><div class="val" id="exact">7</div></div>
<div class="card priv"><div class="lbl">Private answer (with noise)</div><div class="val" id="noisy">7</div></div>
</div>
<div class="status" id="status">Flip a person and compare the two answers.</div>
<div class="btns">
<label class="eps">Privacy budget ε
<input id="eps" type="range" min="0.1" max="4" step="0.1" value="0.5">
<span id="epsv">0.5</span>
</label>
<button id="requery" type="button">Re-run private query</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.people { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin: .4rem 0 .8rem; }
.person { height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 16px system-ui, sans-serif; border-radius: 8px; cursor: pointer;
user-select: none; border: 1px solid #cdd9e3; background: #e8eef3; color: #1d3557;
transition: all .12s; }
.person.yes { background: #1d3557; color: #fff; border-color: #16294a; }
.person.flagged { outline: 3px solid #f4a259; outline-offset: 1px; }
.readout { display: flex; gap: 12px; margin: .5rem 0; flex-wrap: wrap; }
.card { flex: 1; min-width: 140px; border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem .8rem; background: #fafcfe; }
.card.priv { border-color: #2a9d8f; background: #eef8f6; }
.lbl { font-size: .78rem; color: #556; text-transform: uppercase; letter-spacing: .03em; }
.val { font: 800 30px ui-monospace, monospace; color: #1d3557; }
.card.priv .val { color: #1d7d6e; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #444; }
.btns { display: flex; gap: .7rem; flex-wrap: wrap; align-items: center; }
.eps { font-size: .85rem; color: #333; display: flex; align-items: center; gap: .4rem; }
.eps input { vertical-align: middle; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Differential privacy via the Laplace mechanism. A counting query has "sensitivity" 1: one person flipping their answer changes the true count by 1. To hide that one person, we add noise drawn from a Laplace distribution with scale b = sensitivity / epsilon. Smaller epsilon => more noise => more privacy, but a noisier (less accurate) answer. The published number reveals the trend of the crowd while making any single person's contribution statistically deniable.
const N = 12;
// Initial answers: 7 "yes". The first person (index 0) is the one we'll flip.
const answers = [1,1,0,1,0,1,1,0,1,0,0,1];
let flagged = 0; // the highlighted "target" individual
let epsilon = 0.5;
function trueCount(){ return answers.reduce((a,b)=>a+b,0); }
// Sample from a Laplace(0, b) distribution using inverse transform sampling.
function laplace(b){
const u = Math.random() - 0.5; // u in (-0.5, 0.5)
return -b * Math.sign(u) * Math.log(1 - 2*Math.abs(u));
}
// Differentially private count: true count + Laplace noise, scale = 1/epsilon.
function privateCount(){
const noisy = trueCount() + laplace(1/epsilon);
return Math.max(0, Math.round(noisy)); // counts are non-negative integers
}
function renderPeople(){
const box = document.getElementById('people'); box.innerHTML='';
for(let i=0;i<N;i++){
const d=document.createElement('div');
d.className='person'+(answers[i]?' yes':'')+(i===flagged?' flagged':'');
d.textContent=answers[i]?'Yes':'No';
d.addEventListener('click',()=>{
answers[i]=answers[i]?0:1; flagged=i; update(true);
});
box.appendChild(d);
}
}
function setExact(){ document.getElementById('exact').textContent=trueCount(); }
let lastNoisy=privateCount();
function setNoisy(reroll){ if(reroll) lastNoisy=privateCount();
document.getElementById('noisy').textContent=lastNoisy; }
function status(t){ document.getElementById('status').textContent=t; }
function update(flipped){
renderPeople(); setExact(); setNoisy(true);
if(flipped){
status(fmt('You flipped person %flagged%%v%. The EXACT count jumped by 1 and leaks their answer. The PRIVATE answer barely reacts — it could have changed for many reasons.', { flagged: flagged, v: 1 }));
}
}
document.getElementById('eps').addEventListener('input',e=>{
epsilon=parseFloat(e.target.value); document.getElementById('epsv').textContent=epsilon.toFixed(1);
setNoisy(true);
status('ε = '+epsilon.toFixed(1)+(epsilon<1?' → lots of noise, strong privacy.':epsilon>2?' → little noise, weak privacy.':' → a balanced trade-off.'));
});
document.getElementById('requery').addEventListener('click',()=>{ setNoisy(true);
status('Re-ran the private query: same data, fresh noise, a different published number.'); });
document.getElementById('reset').addEventListener('click',()=>{
const init=[1,1,0,1,0,1,1,0,1,0,0,1];
for(let i=0;i<N;i++) answers[i]=init[i];
flagged=0; epsilon=0.5; document.getElementById('eps').value=0.5;
document.getElementById('epsv').textContent='0.5';
update(false); status('Reset. Flip a person and compare the two answers.');
});
update(false);
The exact count is a snitch: flip one person and it moves by exactly one, betraying that individual. The private count barely flinches — and because the noise is random, that same shift could have happened with or without them. Slide the privacy budget ε down for more noise and stronger privacy, up for a sharper but leakier answer. This tug-of-war between accuracy and privacy is the whole game, and it echoes the trade-offs behind cryptographic hashing.
Comments
Loading comments...