Linkage
An interactive lab that re-identifies people in synthetic data, showing that removing names doesn't make a dataset anonymous

- re-identification attacks, each scored against ground truth
- 6
- cases pairing each defence with its attack
- 5
- synthetic records drawn at 60 fps
- 200,000
- ways to load a real dataset
- 0
On this page
The problem
Government bodies and companies publish "anonymised" data by removing names and ID numbers, and treat the job as done. It isn't. What identifies a person is the combination of ordinary details, such as district, date of birth and gender, none of which looks sensitive alone. A landmark study found 87% of the US population unique on ZIP code, gender and date of birth. A later re-run put it at 63%, and nobody has reconciled the two.
Data stewards usually meet this as a citation. They read "87%" and can't tell whether it applies to their own release. I wanted an instrument in its place: run the attack yourself, see how many people it found, apply the standard defence, then watch the next attack beat that too.
What I built
The app only works on populations it invents
A tool that takes an actual dataset and helps re-identify the people in it is a re-identification tool, whatever its copy says. So Linkage generates its own synthetic populations in the browser and keeps the truth about who every record is. That lets it tell you, for example, that you identified 340 of 500 targets, and show the 160 that failed. There is no way to load a file or paste data, and a test scans the built site and fails the build if one appears.
Checking your own data takes column descriptions only
Data stewards still need to judge their own releases. The schema assessor takes a description of each column: its name, its type and how many distinct values it holds. The input has no place to put an actual row, and a test checks that. The result is a uniqueness estimate, with its assumption stated: it treats columns as independent, so correlated columns make it look safer than it is.
Every attack reports a score
The six attacks can't claim a record is identifiable without the counts behind it: attempted, correct, incorrect and failed. For each target they also report how many candidates were left. An attack that narrows 500,000 records to three has done most of the work, even if it didn't finish, and the screen shows that.
Each case ends with the next attack
There are five scripted cases. Each one runs an attack, applies the defence that stops it, then runs the attack that gets past that defence. The chain goes from linkage to k-anonymity, homogeneity, l-diversity, skewness, differencing and finally differential privacy and its budget. For differential privacy, I derive the noise needed for each kind of query and show the working, because assuming one value for every query quietly breaks the guarantee.
200,000 people on one screen
The central view draws 200,000 synthetic records at 60 frames per second and groups them into classes of people who look identical in the data. As you generalise the data, classes merge on screen. I keep each record's position fixed unless its own class changes, so the merge reads clearly. A dissector for Indonesia's 16-digit national ID number (NIK) shows it contains that same district, birth date and gender triple, so masking its last digits hides the only harmless part.
Result
Linkage is live and public, a finished first version with no backend and no accounts. Beyond the five cases, a free sandbox holds twelve instruments, including a search for the smallest generalisation that meets a target, drawn from the search's own record of what it tested. The whole app downloads as 113.8 KB compressed. It reports what an attack recovered and never dramatises it: no "exposed", no "victim", no congratulations. I built it alone over eight days in September 2026.
Under the hoodTechnical detail for engineers
src/engine/imports nothing, not React, the DOM orDate. Every result is a function of a seed and a configuration, tested against brute-force reference implementations.- No
FileReader, file input, drag-and-drop or paste path.tests/no-file-input.test.tsgreps the built bundle for those APIs;tests/assessor-purity.test.tsasserts at type level and runtime that the assessor input has novalues,sampleorrows. - Attacks: linkage, homogeneity, background knowledge, skewness, differencing and an auxiliary-data join. Each returns a per-target
candidateCount. - DP bench: derived per-query sensitivity (count 1, sum equal to its clamp range), Laplace and Gaussian mechanisms, sequential budget meter.
dp.test.tschecks the Laplace sampler against the analytic density. - The lattice search prunes upward by monotonicity and records
prunedByand testorder;lattice.test.tsasserts it finds the same minimal set as exhaustive search. - The field uses Canvas 2D, three parallel typed arrays, a grid index for hit testing on a single listener, and deterministic packing.
tests/performance.test.tsasserts the 200,000-record path finishes under 2 s. - The Sweeney and Golle methods are both reconstructed against known populations, and the 87% versus 63% gap is shown as unresolved.
- Seed, generator parameters, generalisation vector, k target and ε live in the URL. Case progress and theme stay local.
- Two runtime dependencies, React 18.3 and React DOM. Vite 5.4 with an in-repo
linkage-metadataplugin, plain CSS, hand-rolledrequestAnimationFrame, no chart or UI library. scripts/bundle-budget.mjsfails CI over 250 KB gzipped; the current 113.8 KB includes 44.3 KB of React.- 328 tests across 20 files (about 24 s), 18 views plus 6 case screens, 30 engine modules.
- About 13,300 lines in
src/and 80 commits from 4 to 11 September 2026, paired with Claude Code.
Screenshots
Hiring, or building something similar?
I'm open to full-time roles and select freelance work.