<!-- k-anonymity demo — generalize a patient table until every equivalence class has >= k rows -->
<div class="controls">
<label for="kSlider">k = <strong id="kVal">2</strong></label>
<input type="range" id="kSlider" min="1" max="5" value="2" />
<button id="btnGeneralize" type="button">Generalize</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
<div class="table-wrap">
<table id="dataTable">
<thead>
<tr>
<th>Age</th>
<th>ZIP</th>
<th>Sex</th>
<th class="sensitive">Condition</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div class="legend">
<span class="legend-item suppressed-swatch"></span> Suppressed row
<span class="legend-item generalized-swatch"></span> Generalized value
</div>
/* styles for the k-anonymity demo table */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .6rem; }
label { font-weight: 600; white-space: nowrap; }
input[type=range] { width: 120px; cursor: pointer; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.warn { color: #b36200; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #cdd9e3; padding: .32rem .6rem; text-align: left; white-space: nowrap; }
th { background: #e8eef3; font-weight: 700; font-size: .82rem; }
th.sensitive { background: #f5e6c8; }
td.sensitive { background: #fdf6e8; }
tr.suppressed td { background: #fce4e4; color: #888; text-decoration: line-through; }
td.gen { background: #ddefff; font-style: italic; }
.legend { display: flex; align-items: center; font-size: .8rem; color: #555; margin-top: .5rem; flex-wrap: wrap; gap: .3rem; }
.legend-item { display: inline-block; width: 14px; height: 14px; border-radius: 3px; vertical-align: middle; }
.suppressed-swatch { background: #fce4e4; border: 1px solid #c92f3c; }
.generalized-swatch { background: #ddefff; border: 1px solid #5a9fd4; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Small patient dataset with quasi-identifiers (age, zip, sex) and one sensitive attribute (condition).
const ORIGINAL = [
{ age: 29, zip: '13053', sex: 'F', cond: 'Flu' },
{ age: 22, zip: '13068', sex: 'M', cond: 'Hypertension' },
{ age: 27, zip: '13053', sex: 'F', cond: 'Flu' },
{ age: 43, zip: '13068', sex: 'M', cond: 'Diabetes' },
{ age: 52, zip: '13068', sex: 'F', cond: 'Hypertension' },
{ age: 47, zip: '13076', sex: 'M', cond: 'Diabetes' },
{ age: 36, zip: '13076', sex: 'F', cond: 'Flu' },
{ age: 32, zip: '13053', sex: 'M', cond: 'Asthma' }
];
// Age generalization: level 0 = exact, level 1 = decade range, level 2 = any.
function generalizeAge(age, level) {
if (level === 0) return String(age);
if (level === 1) {
const lo = Math.floor(age / 10) * 10;
return lo + '–' + (lo + 9);
}
return 'Any';
}
// ZIP generalization: level 0 = exact, level 1 = mask last digit, level 2 = mask last two, level 3 = all masked.
function generalizeZip(zip, level) {
if (level === 0) return zip;
if (level === 1) return zip.slice(0, 4) + '*';
if (level === 2) return zip.slice(0, 3) + '**';
return '*****';
}
// Sex generalization: level 0 = exact (M/F), level 1 = any.
function generalizeSex(sex, level) {
if (level === 0) return sex;
return 'Any';
}
// Equivalence-class key from the three generalized quasi-identifiers.
function qiKey(row) {
return row.genAge + '|' + row.genZip + '|' + row.genSex;
}
// Greedy k-anonymization — repeatedly generalize the least-generalized attribute until all groups reach size k, then suppress remaining violators.
function greedyAnonymize(k) {
let rows = ORIGINAL.map(r => ({
...r, genAge: String(r.age), genZip: r.zip, genSex: r.sex,
ageLevel: 0, zipLevel: 0, sexLevel: 0, suppressed: false
}));
let changed = true;
while (changed) {
changed = false;
// Count equivalence-class sizes.
const counts = {};
rows.forEach(r => { if (!r.suppressed) { const key = qiKey(r); counts[key] = (counts[key] || 0) + 1; } });
// Find rows whose equivalence class is still below k.
const violations = rows.filter(r => !r.suppressed && counts[qiKey(r)] < k);
if (violations.length === 0) break;
// Generalize the least-generalized quasi-identifier globally (greedy).
const r0 = violations[0];
if (r0.ageLevel < 2) {
const newLevel = r0.ageLevel + 1;
rows.forEach(r => { if (!r.suppressed) { r.ageLevel = Math.max(r.ageLevel, newLevel); r.genAge = generalizeAge(r.age, r.ageLevel); } });
changed = true;
} else if (r0.zipLevel < 3) {
const newLevel = r0.zipLevel + 1;
rows.forEach(r => { if (!r.suppressed) { r.zipLevel = Math.max(r.zipLevel, newLevel); r.genZip = generalizeZip(r.zip, r.zipLevel); } });
changed = true;
} else if (r0.sexLevel < 1) {
rows.forEach(r => { if (!r.suppressed) { r.sexLevel = 1; r.genSex = generalizeSex(r.sex, 1); } });
changed = true;
} else {
// All attributes are fully generalized; suppress remaining violators.
violations.forEach(r => { r.suppressed = true; });
changed = true;
}
}
return rows;
}
const tbody = document.getElementById('tableBody');
const statusEl = document.getElementById('status');
const kVal = document.getElementById('kVal');
const kSlider = document.getElementById('kSlider');
function renderOriginal() {
tbody.innerHTML = '';
ORIGINAL.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML =
'<td>' + row.age + '</td>' +
'<td>' + row.zip + '</td>' +
'<td>' + row.sex + '</td>' +
'<td class="sensitive">' + row.cond + '</td>';
tbody.appendChild(tr);
});
statusEl.textContent = 'Original table (not yet anonymized).';
statusEl.className = 'status';
}
function renderAnonymized(rows, k) {
tbody.innerHTML = '';
const counts = {};
rows.forEach(r => { if (!r.suppressed) { const key = qiKey(r); counts[key] = (counts[key] || 0) + 1; } });
rows.forEach(row => {
const tr = document.createElement('tr');
if (row.suppressed) { tr.className = 'suppressed'; }
const ageGen = row.ageLevel > 0;
const zipGen = row.zipLevel > 0;
const sexGen = row.sexLevel > 0;
tr.innerHTML =
'<td' + (ageGen && !row.suppressed ? ' class="gen"' : '') + '>' + row.genAge + '</td>' +
'<td' + (zipGen && !row.suppressed ? ' class="gen"' : '') + '>' + row.genZip + '</td>' +
'<td' + (sexGen && !row.suppressed ? ' class="gen"' : '') + '>' + row.genSex + '</td>' +
'<td class="sensitive">' + row.cond + '</td>';
tbody.appendChild(tr);
});
const suppressed = rows.filter(r => r.suppressed).length;
const remaining = rows.length - suppressed;
if (suppressed === 0) {
statusEl.textContent = i18nFmt('%n% rows — every equivalence class has ≥ %k% members. ✓ %k%-anonymous.', { k: k, n: remaining });
statusEl.className = 'status ok';
} else {
statusEl.textContent = i18nFmt('%s% row(s) suppressed. %n% rows remain — %k%-anonymous.', { k: k, s: suppressed, n: remaining });
statusEl.className = 'status warn';
}
}
kSlider.addEventListener('input', () => {
kVal.textContent = kSlider.value;
renderOriginal();
});
document.getElementById('btnGeneralize').addEventListener('click', () => {
const k = parseInt(kSlider.value, 10);
const rows = greedyAnonymize(k);
renderAnonymized(rows, k);
});
document.getElementById('btnReset').addEventListener('click', () => {
kSlider.value = 2;
kVal.textContent = '2';
renderOriginal();
});
renderOriginal();
<!-- k-anonymity demo — generalize a patient table until every equivalence class has >= k rows -->
<div class="controls">
<label for="kSlider">k = <strong id="kVal">2</strong></label>
<input type="range" id="kSlider" min="1" max="5" value="2" />
<button id="btnGeneralize" type="button">Generalize</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
<div class="table-wrap">
<table id="dataTable">
<thead>
<tr>
<th>Age</th>
<th>ZIP</th>
<th>Sex</th>
<th class="sensitive">Condition</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div class="legend">
<span class="legend-item suppressed-swatch"></span> Suppressed row
<span class="legend-item generalized-swatch"></span> Generalized value
</div>
/* styles for the k-anonymity demo table */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .6rem; }
label { font-weight: 600; white-space: nowrap; }
input[type=range] { width: 120px; cursor: pointer; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.warn { color: #b36200; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #cdd9e3; padding: .32rem .6rem; text-align: left; white-space: nowrap; }
th { background: #e8eef3; font-weight: 700; font-size: .82rem; }
th.sensitive { background: #f5e6c8; }
td.sensitive { background: #fdf6e8; }
tr.suppressed td { background: #fce4e4; color: #888; text-decoration: line-through; }
td.gen { background: #ddefff; font-style: italic; }
.legend { display: flex; align-items: center; font-size: .8rem; color: #555; margin-top: .5rem; flex-wrap: wrap; gap: .3rem; }
.legend-item { display: inline-block; width: 14px; height: 14px; border-radius: 3px; vertical-align: middle; }
.suppressed-swatch { background: #fce4e4; border: 1px solid #c92f3c; }
.generalized-swatch { background: #ddefff; border: 1px solid #5a9fd4; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Small patient dataset with quasi-identifiers (age, zip, sex) and one sensitive attribute (condition).
const ORIGINAL = [
{ age: 29, zip: '13053', sex: 'F', cond: 'Flu' },
{ age: 22, zip: '13068', sex: 'M', cond: 'Hypertension' },
{ age: 27, zip: '13053', sex: 'F', cond: 'Flu' },
{ age: 43, zip: '13068', sex: 'M', cond: 'Diabetes' },
{ age: 52, zip: '13068', sex: 'F', cond: 'Hypertension' },
{ age: 47, zip: '13076', sex: 'M', cond: 'Diabetes' },
{ age: 36, zip: '13076', sex: 'F', cond: 'Flu' },
{ age: 32, zip: '13053', sex: 'M', cond: 'Asthma' }
];
// Age generalization: level 0 = exact, level 1 = decade range, level 2 = any.
function generalizeAge(age, level) {
if (level === 0) return String(age);
if (level === 1) {
const lo = Math.floor(age / 10) * 10;
return lo + '–' + (lo + 9);
}
return 'Any';
}
// ZIP generalization: level 0 = exact, level 1 = mask last digit, level 2 = mask last two, level 3 = all masked.
function generalizeZip(zip, level) {
if (level === 0) return zip;
if (level === 1) return zip.slice(0, 4) + '*';
if (level === 2) return zip.slice(0, 3) + '**';
return '*****';
}
// Sex generalization: level 0 = exact (M/F), level 1 = any.
function generalizeSex(sex, level) {
if (level === 0) return sex;
return 'Any';
}
// Equivalence-class key from the three generalized quasi-identifiers.
function qiKey(row) {
return row.genAge + '|' + row.genZip + '|' + row.genSex;
}
// Greedy k-anonymization — repeatedly generalize the least-generalized attribute until all groups reach size k, then suppress remaining violators.
function greedyAnonymize(k) {
let rows = ORIGINAL.map(r => ({
...r, genAge: String(r.age), genZip: r.zip, genSex: r.sex,
ageLevel: 0, zipLevel: 0, sexLevel: 0, suppressed: false
}));
let changed = true;
while (changed) {
changed = false;
// Count equivalence-class sizes.
const counts = {};
rows.forEach(r => { if (!r.suppressed) { const key = qiKey(r); counts[key] = (counts[key] || 0) + 1; } });
// Find rows whose equivalence class is still below k.
const violations = rows.filter(r => !r.suppressed && counts[qiKey(r)] < k);
if (violations.length === 0) break;
// Generalize the least-generalized quasi-identifier globally (greedy).
const r0 = violations[0];
if (r0.ageLevel < 2) {
const newLevel = r0.ageLevel + 1;
rows.forEach(r => { if (!r.suppressed) { r.ageLevel = Math.max(r.ageLevel, newLevel); r.genAge = generalizeAge(r.age, r.ageLevel); } });
changed = true;
} else if (r0.zipLevel < 3) {
const newLevel = r0.zipLevel + 1;
rows.forEach(r => { if (!r.suppressed) { r.zipLevel = Math.max(r.zipLevel, newLevel); r.genZip = generalizeZip(r.zip, r.zipLevel); } });
changed = true;
} else if (r0.sexLevel < 1) {
rows.forEach(r => { if (!r.suppressed) { r.sexLevel = 1; r.genSex = generalizeSex(r.sex, 1); } });
changed = true;
} else {
// All attributes are fully generalized; suppress remaining violators.
violations.forEach(r => { r.suppressed = true; });
changed = true;
}
}
return rows;
}
const tbody = document.getElementById('tableBody');
const statusEl = document.getElementById('status');
const kVal = document.getElementById('kVal');
const kSlider = document.getElementById('kSlider');
function renderOriginal() {
tbody.innerHTML = '';
ORIGINAL.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML =
'<td>' + row.age + '</td>' +
'<td>' + row.zip + '</td>' +
'<td>' + row.sex + '</td>' +
'<td class="sensitive">' + row.cond + '</td>';
tbody.appendChild(tr);
});
statusEl.textContent = 'Original table (not yet anonymized).';
statusEl.className = 'status';
}
function renderAnonymized(rows, k) {
tbody.innerHTML = '';
const counts = {};
rows.forEach(r => { if (!r.suppressed) { const key = qiKey(r); counts[key] = (counts[key] || 0) + 1; } });
rows.forEach(row => {
const tr = document.createElement('tr');
if (row.suppressed) { tr.className = 'suppressed'; }
const ageGen = row.ageLevel > 0;
const zipGen = row.zipLevel > 0;
const sexGen = row.sexLevel > 0;
tr.innerHTML =
'<td' + (ageGen && !row.suppressed ? ' class="gen"' : '') + '>' + row.genAge + '</td>' +
'<td' + (zipGen && !row.suppressed ? ' class="gen"' : '') + '>' + row.genZip + '</td>' +
'<td' + (sexGen && !row.suppressed ? ' class="gen"' : '') + '>' + row.genSex + '</td>' +
'<td class="sensitive">' + row.cond + '</td>';
tbody.appendChild(tr);
});
const suppressed = rows.filter(r => r.suppressed).length;
const remaining = rows.length - suppressed;
if (suppressed === 0) {
statusEl.textContent = i18nFmt('%n% rows — every equivalence class has ≥ %k% members. ✓ %k%-anonymous.', { k: k, n: remaining });
statusEl.className = 'status ok';
} else {
statusEl.textContent = i18nFmt('%s% row(s) suppressed. %n% rows remain — %k%-anonymous.', { k: k, s: suppressed, n: remaining });
statusEl.className = 'status warn';
}
}
kSlider.addEventListener('input', () => {
kVal.textContent = kSlider.value;
renderOriginal();
});
document.getElementById('btnGeneralize').addEventListener('click', () => {
const k = parseInt(kSlider.value, 10);
const rows = greedyAnonymize(k);
renderAnonymized(rows, k);
});
document.getElementById('btnReset').addEventListener('click', () => {
kSlider.value = 2;
kVal.textContent = '2';
renderOriginal();
});
renderOriginal();
Comments
Loading comments...