Introduction

In 2002, Latanya Sweeney showed that k-anonymity — grouping records so every person looks identical on non-sensitive fields like age, zip code, and gender — could protect medical data. If every group has at least kk members, an attacker cannot single you out.

But k-anonymity has a silent flaw. Suppose your hospital releases a table where every group of five patients has the same age range and zip code. Now suppose every one of those five has the same diagnosis: cancer. Even without knowing which of the five you are, the attacker knows your diagnosis. The group itself is the leak.

This is an attribute-disclosure attack, and it defeats k-anonymity completely. The privacy community answered with two stronger models proposed in 2006–2007:

  • l-Diversity (Machanavajjhala et al., 2007): each equivalence class must contain at least ll distinct sensitive values. Knowing which group you're in no longer tells an attacker your exact value.
  • t-Closeness (Li et al., 2007): the distribution of sensitive values inside each group must be close — within distance tt — to the global distribution across the whole table. Even if values differ, a skewed local distribution can still leak information.

Together they form a ladder: k-anonymity protects identity, l-Diversity protects sensitive values, and t-Closeness protects against inference from skewed distributions. See also k-anonymity for the foundation these models build on.

Try It

The table below shows a k-anonymous patient dataset (k = 3): every combination of quasi-identifiers appears at least three times. But look at the sensitive diagnosis column inside each group.

<!-- Demo: attribute-disclosure attack on a k-anonymous table, then l-Diversity and t-Closeness fixes -->
<div id="app">
  <div class="controls">
    <button id="btn-attack" type="button">Attack</button>
    <button id="btn-ldiv" type="button">Apply l-Diversity</button>
    <button id="btn-tclos" type="button">Apply t-Closeness</button>
    <button id="btn-reset" type="button" class="ghost">Reset</button>
  </div>
  <div class="status-bar" id="status"></div>
  <div class="legend">
    <span class="badge badge-anon">k=3 k-anonymous</span>
    <span class="badge badge-leak" id="badge-leak" style="display:none">Attribute leak!</span>
    <span class="badge badge-ldiv" id="badge-ldiv" style="display:none">l=2 l-diverse</span>
    <span class="badge badge-tclos" id="badge-tclos" style="display:none">t=0.2 t-close</span>
  </div>
  <div class="table-wrap">
    <table id="tbl">
      <thead>
        <tr>
          <th>Age</th>
          <th>ZIP</th>
          <th>Sex</th>
          <th class="sensitive">Diagnosis (sensitive)</th>
        </tr>
      </thead>
      <tbody id="tbody"></tbody>
    </table>
  </div>
  <div class="footnote" id="footnote"></div>
</div>
/* Styles for the privacy demo table */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; font-size: 14px; }
#app { padding: 0 2px; }
.controls { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.status-bar { font-size: .88rem; font-weight: 600; min-height: 1.3em;
              margin-bottom: .35rem; color: #555; }
.status-bar.attack { color: #c0392b; }
.status-bar.ok { color: #0a7d33; }
.legend { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .5rem; }
.badge { font-size: .75rem; font-weight: 700; padding: .15rem .5rem;
         border-radius: 12px; border: 1px solid transparent; }
.badge-anon { background: #dce8f7; color: #1d3557; border-color: #b8d0ef; }
.badge-leak { background: #fde8e8; color: #a31515; border-color: #f5b8b8; }
.badge-ldiv { background: #e5f5e8; color: #0a5c1e; border-color: #a8d8b4; }
.badge-tclos { background: #f0eaff; color: #4a1ea0; border-color: #c5aef5; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; min-width: 320px; }
th, td { padding: .3rem .55rem; border: 1px solid #d0d7de; text-align: left; }
th { background: #f0f4f8; font-weight: 700; font-size: .82rem; }
th.sensitive { background: #fff3cd; color: #7a5000; }
td.sensitive { font-style: italic; }
tr.group-a td { background: #f7f9fc; }
tr.group-b td { background: #fafcff; }
tr.highlight td { background: #ffe8e8 !important; }
tr.highlight td.sensitive { font-weight: 700; color: #a31515; }
tr.changed td { background: #e8f9ec !important; }
.footnote { font-size: .78rem; color: #777; margin-top: .5rem; line-height: 1.5; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));

/* Dataset: a k-anonymous (k=3) patient table with two equivalence classes */
/* Each group has identical quasi-identifiers (age, zip, sex) — k-anonymity holds */
const ORIGINAL = [
  /* Group A: age 20-30, zip 1300*, male — all diagnosed with flu (homogeneous!) */
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Flu',    group: 'a' },
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Flu',    group: 'a' },
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Flu',    group: 'a' },
  /* Group B: age 40-50, zip 2100*, female — all diagnosed with cancer (homogeneous!) */
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Cancer', group: 'b' },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Cancer', group: 'b' },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Cancer', group: 'b' },
];

/* l-Diverse version: at least 2 distinct diagnoses per group */
const LDIV = [
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Flu',        group: 'a', changed: false },
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Diabetes',   group: 'a', changed: true  },
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Hypertension',      group: 'a', changed: true  },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Cancer',      group: 'b', changed: false },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Flu',         group: 'b', changed: true  },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Diabetes',    group: 'b', changed: true  },
];

/* t-Close version: local distribution within t=0.3 of the global distribution */
/* Global distribution across all 6 records */
const GLOBAL_DIST = { 'Flu': 2/6, 'Cancer': 2/6, 'Diabetes': 1/6, 'Hypertension': 1/6 };

const TCLOS = [
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Flu',        group: 'a', changed: false },
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Cancer',      group: 'a', changed: true  },
  { age: '20-30', zip: '1300*', sex: 'M', diagnosis: 'Diabetes',   group: 'a', changed: true  },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Cancer',     group: 'b', changed: false },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Flu',        group: 'b', changed: true  },
  { age: '40-50', zip: '2100*', sex: 'F', diagnosis: 'Hypertension',     group: 'b', changed: true  },
];

/* Application state */
let currentRows = ORIGINAL.map(r => ({ ...r, highlight: false, changed: false }));
let mode = 'original';

function countDist(rows) {
  /* Count how many times each diagnosis appears in a group */
  const map = {};
  for (const r of rows) map[r.diagnosis] = (map[r.diagnosis] || 0) + 1;
  return map;
}

function tvDistance(P, Q) {
  /* Total variation distance = half the L1 distance between distributions */
  const keys = new Set([...Object.keys(P), ...Object.keys(Q)]);
  let sum = 0;
  for (const k of keys) sum += Math.abs((P[k] || 0) - (Q[k] || 0));
  return sum / 2;
}

function renderTable(rows) {
  const tbody = document.getElementById('tbody');
  tbody.innerHTML = '';
  for (const r of rows) {
    const tr = document.createElement('tr');
    tr.className = r.highlight ? 'highlight' : r.changed ? 'changed' : ('group-' + r.group);
    ['age', 'zip', 'sex', 'diagnosis'].forEach(k => {
      const td = document.createElement('td');
      td.textContent = r[k];
      if (k === 'diagnosis') td.className = 'sensitive';
      tr.appendChild(td);
    });
    tbody.appendChild(tr);
  }
}

function setStatus(msg, cls) {
  const el = document.getElementById('status');
  el.textContent = msg;
  el.className = 'status-bar ' + (cls || '');
}

function showBadge(id, show) {
  document.getElementById(id).style.display = show ? '' : 'none';
}

function attack() {
  /* Find groups where all records share the same sensitive value */
  const rows = ORIGINAL.map(r => ({ ...r }));
  const groups = {};
  for (const r of rows) { if (!groups[r.group]) groups[r.group] = []; groups[r.group].push(r); }
  let leaked = 0;
  for (const g of Object.values(groups)) {
    const dist = countDist(g);
    const vals = Object.keys(dist);
    if (vals.length === 1) {
      /* All records in this group have the same diagnosis — an attacker learns it */
      g.forEach(r => r.highlight = true);
      leaked++;
    }
  }
  currentRows = rows;
  renderTable(rows);
  showBadge('badge-leak', leaked > 0);
  showBadge('badge-ldiv', false);
  showBadge('badge-tclos', false);
  if (leaked > 0) {
    setStatus(i18nFmt('%n% group(s) have a single sensitive value — the attacker knows your diagnosis!', { n: leaked }), 'attack');
    document.getElementById('footnote').textContent = 'Red rows: every person in these groups shares the same diagnosis. k-anonymity protects identity but not the sensitive attribute.';
  } else {
    setStatus('No homogeneous groups found.', 'ok');
  }
}

function applyLDiv() {
  /* Show the l-diverse version of the table */
  const rows = LDIV.map(r => ({ ...r, highlight: false }));
  currentRows = rows;
  renderTable(rows);
  showBadge('badge-leak', false);
  showBadge('badge-ldiv', true);
  showBadge('badge-tclos', false);
  setStatus('l-Diversity applied: each group now has 2+ distinct diagnoses.', 'ok');
  document.getElementById('footnote').textContent = 'Green rows: replaced with diverse diagnoses. An attacker no longer learns your exact diagnosis from group membership.';
}

function applyTClos() {
  /* Show the t-close version and compute the max TV distance */
  const rows = TCLOS.map(r => ({ ...r, highlight: false }));
  const groups = {};
  for (const r of rows) { if (!groups[r.group]) groups[r.group] = []; groups[r.group].push(r); }
  let maxDist = 0;
  for (const g of Object.values(groups)) {
    const localDist = countDist(g);
    const total = g.length;
    /* Convert counts to probabilities for the distance calculation */
    const localP = {};
    for (const k of Object.keys(localDist)) localP[k] = localDist[k] / total;
    const d = tvDistance(localP, GLOBAL_DIST);
    if (d > maxDist) maxDist = d;
  }
  currentRows = rows;
  renderTable(rows);
  showBadge('badge-leak', false);
  showBadge('badge-ldiv', true);
  showBadge('badge-tclos', true);
  setStatus(i18nFmt('t-Closeness applied (t=0.2): max TV distance from global = %d%.', { d: maxDist.toFixed(2) }), 'ok');
  document.getElementById('footnote').textContent = 'Green rows: distributions tuned to stay within t=0.2 of the global. Even a skewed count no longer leaks more than the global base rate.';
}

function reset() {
  currentRows = ORIGINAL.map(r => ({ ...r, highlight: false, changed: false }));
  renderTable(currentRows);
  showBadge('badge-leak', false);
  showBadge('badge-ldiv', false);
  showBadge('badge-tclos', false);
  setStatus('Click Attack to see what an adversary can learn.', '');
  document.getElementById('footnote').textContent = '';
}

document.getElementById('btn-attack').addEventListener('click', attack);
document.getElementById('btn-ldiv').addEventListener('click', applyLDiv);
document.getElementById('btn-tclos').addEventListener('click', applyTClos);
document.getElementById('btn-reset').addEventListener('click', reset);

/* Initial render */
renderTable(currentRows);
setStatus('Click Attack to see what an adversary can learn.', '');
Result
Console

Click Attack to see what an adversary learns from a homogeneous group. Then click Apply l-Diversity and Apply t-Closeness to watch the guarantees get stronger. Notice how checking each property is fast, but redesigning the table to satisfy both at once is a combinatorial puzzle.

The Formal Models

Both models work on top of equivalence classes — the groups created by k-anonymity's generalization process.

l-Diversity has three flavors, each stronger than the last:

  • Distinct l-Diversity: at least ll distinct sensitive values per class. Simple but weak — one rare value plus l−1l-1 copies of a common one still leaks.
  • Entropy l-Diversity: the Shannon entropy HH of the class satisfies H≥log⁡(l)H \geq \log(l). Values must be spread, not just distinct.
  • Recursive (c,l)(c, l)-Diversity: the most common value appears less than cc times the sum of the rest. Formally, if r1≥r2≥…r_1 \geq r_2 \geq \dots are the frequency counts, then r1<c(r2+r3+… )r_1 < c(r_2 + r_3 + \dots).

t-Closeness tightens the screw further. Let PP be the distribution of a sensitive attribute in one equivalence class and QQ be the global distribution across the full table. The class satisfies t-Closeness if:

d(P,Q)≤td(P, Q) \leq t

where dd is the Earth Mover's Distance (also called Wasserstein-1 distance) — the minimum "work" needed to transform one distribution into the other. For numerical attributes like salary, this naturally captures how far the local mean has drifted from the global mean.

Why are these still active research? Both models are syntactic — they describe the shape of data, not the attacker's actual knowledge. Differential privacy (see differential privacy) takes a fundamentally different approach by bounding what any query can reveal, regardless of what the attacker already knows. For many real settings, the two approaches are complementary: k-anonymity + l-Diversity + t-Closeness for published microdata, differential privacy for interactive query systems.

Where It Matters

Whenever a dataset is published — not queried interactively but released as a file — the combination of k-anonymity, l-Diversity, and t-Closeness is the standard toolkit:

  • Medical microdata: health agencies release patient-level data for research. Diagnoses, medications, and lab results are sensitive values; age, zip, and gender are quasi-identifiers. Both the US HIPAA Safe Harbor and the EU's GDPR pseudonymization guidance lean on these models.
  • Census releases: national statistics offices publish anonymized individual records (Public Use Microdata Samples). Marital status, occupation, and income are sensitive; geography and demographics are quasi-identifiers. The Census Bureau uses a mix of generalization and suppression tuned to satisfy diversity constraints.
  • Insurance and credit data: actuarial tables shared with regulators or researchers must not let a reader infer an individual's premium or credit score from the group.
  • Location data: when trajectories are k-anonymized, the visited-location distribution inside each cluster must itself be diverse, or the home location leaks from the cluster centroid.

The models also show up in privacy audits: checking whether a proposed release satisfies l-Diversity or t-Closeness is a polynomial-time scan of the table — fast enough to run as a CI gate before every data export.

Conclusion

k-Anonymity was a landmark idea, but it left a door open: a group of identical records still shouts a single sensitive value. l-Diversity insists the group must hold at least ll meaningfully different values. t-Closeness goes further, requiring the local distribution to mirror the global one within distance tt.

Together they form a practical privacy ladder for published data. Each rung adds a guarantee and a cost — more utility is sacrificed to suppress or generalize more aggressively. Choosing the right rung is a design decision, not a solved problem.

The deeper lesson is that "anonymous" is not binary. Every privacy model makes assumptions about what the attacker knows and what they want. Understanding those assumptions — and checking them formally — is what separates real privacy engineering from a false sense of safety.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/l-diversity-t-closeness/Content licensed under CC BY-NC 4.0.