Case Study 2: The Anonymized Extract That Named 6,312 People
"Everyone in the room reasoned about 1.9 million customers. The file we were sending had forty-one thousand rows in it, and nobody had done that arithmetic out loud."
Executive Summary
Kestrel sent a quarterly "anonymized customer extract" to a media agency for four consecutive
quarters under a data-sharing agreement. The extract carried no name, no email, and no customer_id.
A contract renewal triggered the first measurement anyone had ever run on it.
The extract held 41,208 customers — one segment, 2.2% of the 1,904,221-customer base — with three-digit ZIP, a five-year age band, and sex. Measured:
equivalence classes 13,720
smallest class (k) 1
customers alone in their class 6,312
customers in a class smaller than five 19,926 (48.4%)
Nobody had lied. The extract was pseudonymized correctly, by a competent engineer, and reviewed by two people. The word "anonymized" had simply never been checked against anything.
The resolution generalized geography to state and suppressed the residual small cells — losing 3.1% of the rows instead of 48.4% — and the agency's analysis was unaffected.
Skills applied: k-anonymity as a measurement (§31.4); quasi-identifiers; the generalization ladder and its suppression cost; composition across releases; and Chapter 30 Case Study 1's naming problem arriving in a new place.
Background
The agreement was ordinary and the extract was reasonable. The agency built lookalike audiences for Kestrel's outdoor category, and needed enough demographic and geographic detail to model against their own panel.
What was sent, quarterly:
premium_outdoor_segment_2026Q2.csv 41,208 rows
zip3 3-digit ZIP prefix
age_band five-year band
sex f / m / x
first_purchase_quarter
orders_12m
net_revenue_cents_12m
categories top 3, comma-separated
Three controls were in place, and each was real:
- No direct identifiers. No name, email, address, phone, or
customer_id. - Geography generalized. Five-digit ZIP truncated to three, deliberately, by the engineer who built it.
- A signed agreement prohibiting re-identification and onward transfer.
The review that approved it asked the right question — "can anyone be identified from this?" — and answered it by inspection. Two people looked at the columns and agreed that they could not.
The Problem
The renewal review, eleven months later, was run by someone who had read §31.4. They asked for a number instead of an opinion.
The measurement, on the Q2 file:
quasi-identifiers classes k rows<5 singletons
------------------------------------------------------------------
ZIP5 + age band + sex 38,103 1 41,127 35,435
ZIP5 + age band 35,532 1 40,690 31,145
ZIP3 + age band + sex 13,720 1 19,926 6,312 <- shipped
ZIP3 + age band 8,488 1 11,545 2,714
ZIP3 only 925 1 205 12
state + age band + sex 1,930 1 1,264 306
state + age band 780 1 204 31
state only - 332 0 0
Read the shipped row. Of 41,208 people, 6,312 were the only person in the file with their combination of ZIP3, age band, and sex, and 19,926 — 48.4% — were in a group smaller than five.
And read the whole table. Only the last row reaches k ≥ 5. Every generalization step short of throwing away geography entirely leaves someone alone.
⚠️ Failure Mode — everyone reasoned about the population; the file was a slice
This is the mistake, and it is almost universal.
The review's reasoning was: Kestrel has 1.9 million customers. In a customer base that size, a ZIP3 prefix, a five-year age band, and a sex describe thousands of people. Nobody can be picked out.
That reasoning is correct about the customer base and irrelevant to the file. The same three columns, measured both ways:
k singletons rows below k=5 400,000-row sample of the base 1 5,664 25,622 — 6.41% the 41,208-row segment extract 1 6,312 19,926 — 48.35% The extract has more people alone in their class than a sample ten times its size, and its at-risk rate is 7.5× higher.
The mechanism is simple once stated. Anonymity comes from crowding. A segment is defined by being a small, specific subset, so it spreads a small number of people across the same number of cells the whole base spreads millions across — and the cells empty out.
And the worse property: the more precisely a segment is targeted, the less anonymous it is. A broad segment is safe and useless to the agency. The segments worth sharing are exactly the ones that cannot be shared safely at fine geography — which is a structural tension, not an implementation problem.
The check is one line and nobody runs it: what is k, on the file I am actually sending? Not on the table it came from.
The Analysis
Step 1: is anybody actually identifiable? The team ran the obvious challenge on themselves.
Taking one singleton class — a ZIP3 in a low-density state, the 55–59 band, sex x — the record
also carried orders_12m, net_revenue_cents_12m, and three category names. Combined with a voter
file, a marketing list, or the agency's own panel, that is a specific person with a purchase history
attached.
Nobody at Kestrel attempted the re-identification, deliberately, and the decision not to is worth recording: attempting it would have created exactly the linkage the exercise was trying to prevent, and the measurement had already answered the question that mattered.
Step 2: composition across the four quarters. This is where it got worse.
🔐 Privacy & Governance — four safe releases are not one safe release
Even if each quarter had been k-anonymous, the four together would not be.
Roughly 24,724 customers appeared in all four extracts — the segment is stable, which is the point of it. A person in more than one release is pinned by the intersection of their equivalence classes, and an intersection is never larger than either input:
text Q1: ZIP3=802, band 45-49, sex f -> one of 340 people Q2: same, plus orders_12m 7 -> 9 -> one of 41 Q3: same, plus a category change -> one of 6 Q4: same, plus revenue movement -> one of 2Each release is a constraint, and constraints compose. The behavioural columns — order counts, revenue, category mix — are not quasi-identifiers within a single release, and become quasi- identifiers across releases, because their trajectory is nearly unique even when each value is common.
k-anonymity is a property of a release, not of a sharing relationship, and this is its most important limitation. Anyone measuring one file and declaring the program safe has measured the wrong thing.
What Kestrel does now: the composition question is asked at the agreement level, not the file level. Either the population is re-drawn each quarter (so membership does not persist), or the behavioural columns are banded coarsely enough that a trajectory is not distinguishing. Kestrel chose the second, because the agency needed the segment to be stable.
Step 3: what does the agency actually need? The question §31.4's 🎓 callout insists on, asked eleven months late.
The answer, from a forty-minute call: the agency's models used geography at DMA level (designated market area, roughly a metro), not ZIP3, and had been aggregating ZIP3 up to DMA on arrival. The finer geography was never used.
Kestrel had been shipping a re-identification risk to satisfy a requirement nobody had.
The Decision
Four changes.
One: generalize geography to state, and suppress what remains.
option k suppressed % of rows lost
────────────────────────────────────────────────────────────────
ZIP3 + age band + sex (as sent) 1 19,926 48.4%
ZIP3 + age band 1 11,545 28.0%
state + age band + sex 1 1,264 3.1% <- chosen
state + age band 1 204 0.5%
state only 332 0 0.0%
📐 Design Decision — generalize until suppression is cheap, then suppress
The instinct is to generalize until k ≥ 5 is reached without suppression. On this data, that means
state only— no age, no sex — and the file is worthless.The better move is to combine the two tools, and to choose the rung by the cost of suppression rather than by the value of k:
- At ZIP3 + age band + sex, reaching k = 5 by suppression costs 48.4% of the rows. The file is destroyed by the fix.
- At state + age band + sex, it costs 3.1%. The file survives and keeps both age and sex, which the agency's model actually used.
The suppression is not a rounding error to hide. The 1,264 removed rows are systematically the unusual people — rare age bands in small states,
sex = x— so the released file is biased in a known direction, and that bias is disclosed to the agency rather than left for them to discover. A suppressed extract that does not say what was suppressed is a dataset with an undocumented hole in it.The general rule: pick the generalization rung where suppression becomes affordable, not the rung where suppression becomes unnecessary. The second is almost always too coarse to be useful, and the arithmetic that shows this is one table.
Two: the behavioural columns are banded. orders_12m to 1, 2-3, 4-6, 7+;
net_revenue_cents_12m to quintiles. This is what closes the composition hole, and it cost the
agency nothing because their model bucketed both anyway.
Three: the check runs before every send. k_check.py in the export DAG, refusing to write a file
whose k is below 5 after suppression. The refusal is hard, not a warning.
Four: the word "anonymized" is banned from Kestrel's data-sharing documents, and replaced with one of three specific statements.
🔎 Read the Plan — three words instead of one
The root cause was not a technical failure. It was that four people used one word to mean four things, which is Chapter 30 Case Study 1 in a different department.
- The engineer meant "I removed the direct identifiers."
- The reviewer meant "I looked at it and could not identify anyone."
- The contract meant "outside the scope of the regulation."
- The agency meant "we are permitted to use it."
Only the third is a legal claim, and it was the only one nobody had checked.
Kestrel's replacement vocabulary, which now appears in every sharing document:
Term Means Who may assert it De-identified direct identifiers removed the engineer k-anonymous (k=N) measured, with the quasi-identifier set stated the engineer, with the number Anonymous outside the regulation's scope legal, in writing The middle row is the one that changed behaviour. "k-anonymous" is unusable without a number and a stated quasi-identifier set, so the claim cannot be made casually — writing it down requires running the measurement.
This is the same move as §30.8's certified metric definition: the goal is not one word, it is no two people meaning different things by the same word.
What Happened
| As sent (4 quarters) | After | |
|---|---|---|
| Rows | 41,208 | 39,944 |
| Geography | ZIP3 (925 values) | state (51) |
| Smallest class (k) | 1 | 5 |
| Customers alone in their class | 6,312 | 0 |
| Rows in a class below 5 | 19,926 (48.4%) | 0 |
| Rows suppressed | 0 | 1,264 (3.1%) |
| Behavioural columns | exact | banded |
| Check before send | none | hard refusal |
| Agency model performance | baseline | unchanged |
The agency's model performance was unchanged, which is the finding the team least expected and the one that made the change easy to agree. The ZIP3 precision had been discarded on arrival for eleven months.
And the four historical files. Kestrel notified the agency, requested deletion under the agreement, received confirmation, and recorded that it cannot verify the deletion — which is §31.5's honest position about copies you do not control, applied to itself.
Whether the four quarters constituted a reportable event was a legal determination, made by legal, on facts the platform team produced in two days. The platform team's opinion was not sought and was not relevant, which is §31.12 working as intended.
Lessons
-
"Anonymized" was never checked against anything. Nobody lied; the word had four meanings and only one of them was a legal claim.
-
⚠️ Everyone reasoned about the 1.9-million-customer base; the file was a 41,208-row slice. Same columns: 6.41% at risk over the base, 48.35% over the extract — a 7.5× difference created entirely by slicing.
-
Anonymity comes from crowding, and a segment is defined by being small and specific. The more precisely a segment is targeted, the less anonymous it is — a structural tension, not an implementation problem.
-
Only one rung of an eight-rung generalization ladder reached k ≥ 5, and it was the one with no age and no sex. Generalization does not degrade gracefully (§31.4).
-
📐 Pick the rung where suppression becomes affordable, not the rung where it becomes unnecessary. 48.4% of rows lost at ZIP3; 3.1% at state, keeping both columns the agency used.
-
Disclose what was suppressed. The 1,264 removed rows are systematically the unusual people, so the file is biased in a known direction.
-
🔐 Four safe releases are not one safe release. 24,724 customers appeared in all four quarters, and behavioural columns that are not quasi-identifiers within a release become quasi-identifiers across releases, because a trajectory is nearly unique.
-
k-anonymity is a property of a release, not of a sharing relationship. Ask the composition question at the agreement level.
-
Nobody attempted the re-identification, deliberately — it would have created the linkage the exercise existed to prevent, and the measurement had already answered the question.
-
🔎 The fix that changed behaviour was vocabulary. "k-anonymous" is unusable without a number, so the claim cannot be made without running the measurement.
-
The finer geography had been discarded on arrival for eleven months. Ask what the recipient actually uses — it took a forty-minute call, eleven months late.
Questions for Discussion
-
The original review asked the right question and answered it by inspection. What would have made "measure it" the obvious next step rather than a thing somebody eventually thought of?
-
Only
state onlyreaches k ≥ 5 without suppression, and it is useless. Is there a fourth tool beyond generalization and suppression that would help here? What does it cost? -
The suppressed rows are systematically unusual people. Is disclosing the bias sufficient, or does the systematic exclusion of
sex = xrecords raise a separate problem? -
Composition was closed by banding the behavioural columns. Construct a way the four releases could still be composed after that change.
-
Kestrel requested deletion of the historical files and cannot verify it. What is the value of a control you cannot verify — and what would you do differently in the next agreement?
-
The vocabulary table lets an engineer assert "k-anonymous (k=5)" but reserves "anonymous" for legal. Where else in your work would forcing a number into a claim change behaviour?
-
The agency's model performance was unchanged. How much of this case study's outcome depended on that luck, and what would the conversation have looked like if the model had degraded?