Defending Against Attribute Inference on Synthetic Geodata

A release passes its membership-inference test comfortably and still leaks. An adversary who knows where somebody lives and works can predict a withheld attribute — income band, household composition, a health indicator — far better than chance, using only the synthetic data.

Part of Adversarial Privacy Testing: membership inference asks whether a record was present, and attribute inference asks what a known individual’s hidden value is. They are different attacks with different defences, and passing one says nothing about the other.

Root Cause: The Correlation Was the Requirement

The uncomfortable part of this attack is that the thing it exploits is the thing the release was asked to preserve.

A consumer wants synthetic data whose attributes relate to each other the way the real ones do — that is the entire point of attribute correlation modelling. An adversary wants to predict a sensitive attribute from quasi-identifiers they already hold. Both are asking the generator to carry the same information: the conditional distribution of the sensitive attribute given the quasi-identifiers.

That means the defence cannot be “remove the correlation”, because removing it removes the utility. It has to be a bound on how much is carried, chosen deliberately, with the residual measured rather than assumed.

Spatial data makes this sharper than it is elsewhere, for two reasons. Location is an unusually strong quasi-identifier — a home and work pair is close to unique over a metropolitan area — and spatial autocorrelation means neighbouring records carry information about each other, so suppressing an individual’s own record does not suppress the signal about them.

Attribute-inference advantage against carried mutual information, across five configurations The horizontal axis is the mutual information the release carries between the quasi-identifiers an adversary holds and the sensitive attribute, in bits normalised to the attribute's entropy. The vertical axis is the measured inference advantage — how much better than the marginal baseline an adversary does. The five configurations fall close to a straight line, which is the figure's main claim: the attack consumes carried information and very little else. Three of the five sit near the top right and are barely distinguishable: the baseline, the same generator with two hundred and fifty metres of coordinate noise, and the same again with a kilometre. Adding a kilometre of positional noise — far more than any release would tolerate — moves the advantage by about a tenth, because the attack needs the neighbourhood's conditional distribution rather than the individual's exact position. The two configurations that move substantially are the ones that operate on the conditional distribution itself: coarsening the quasi-identifiers, and coarsening plus smoothing plus sparse suppression. A shaded band marks an advantage ceiling of 0.15, which only the last configuration reaches. The attack consumes carried information — not positional precision 0 0.25 0.5 0.75 1 0 0.2 0.4 0.6 0.8 mutual information carried I(Q; S), normalised attribute-inference advantage advantage ceiling 0.15 full correlation + 250 m coordinate noise + 1 km coordinate noise coarsened quasi-identifiers coarsened + smoothed + k≥12 coordinate noise only operates on the conditional distribution A kilometre of positional noise — far more than any release would tolerate — moves the advantage by about a tenth, because the attack needs the neighbourhood's conditional distribution rather than anybody's exact position.
Measured across five generator configurations: the inference advantage tracks the carried mutual information almost exactly, and more coordinate noise barely moves it.

Minimal Reproducer: Measure the Advantage, Not the Accuracy

Raw accuracy is meaningless here, because a sensitive attribute with a skewed marginal can be predicted well by always guessing the majority class. The number that matters is the advantage over that baseline.

python
def attribute_advantage(synth, targets, quasi: list[str], sensitive: str) -> float:
    """How much better than the marginal an adversary does, using the synthetic data.

    0.0 means the release adds nothing beyond what the marginal already gives away;
    1.0 means the sensitive value is fully determined by the quasi-identifiers.
    """
    baseline = majority_rate(synth[sensitive])
    model = fit_predictor(synth, features=quasi, target=sensitive)
    accuracy = evaluate(model, targets, features=quasi, target=sensitive)
    return max(0.0, (accuracy - baseline) / (1.0 - baseline))

Two details in that function decide whether the number means anything. The predictor is fitted on the synthetic data and evaluated on the real targets, because the question is what an adversary learns from the release about real people. And the quasi-identifier set has to be the one an adversary plausibly holds — which for spatial data almost always includes location, and is worth agreeing with whoever signs off the release rather than choosing quietly.

Fix: Cap the Carried Information, Then Verify the Residual

1 — Measure what the generator is carrying

python
import math
from collections import Counter


def mutual_information(pairs) -> float:
    """I(Q; S) in bits, over binned quasi-identifiers and the sensitive attribute."""
    joint = Counter(pairs)
    n = sum(joint.values())
    pq = Counter(q for q, _ in pairs)
    ps = Counter(s for _, s in pairs)
    return sum((c / n) * math.log2((c / n) / ((pq[q] / n) * (ps[s] / n)))
               for (q, s), c in joint.items() if c)

Mutual information is the right quantity because it is what the attack consumes, and because it is bounded by the entropy of the sensitive attribute — which gives the cap a natural scale. A release carrying half the sensitive attribute’s entropy is telling an adversary half of what they want to know, regardless of which model they use.

2 — Cap it in the generator, not afterwards

The effective defences all operate on the conditional distribution during generation, and there are three of them.

python
def coarsen_quasi(df, columns: list[str], cell_m: float, age_band: int = 10):
    """Generalise the quasi-identifiers so fewer individuals are uniquely described."""
    df = df.copy()
    for col in columns:
        if col in ("home_x", "home_y", "work_x", "work_y"):
            df[col] = (df[col] // cell_m) * cell_m
        elif col == "age":
            df[col] = (df[col] // age_band) * age_band
    return df


def smooth_conditional(counts: dict, floor: float = 0.05) -> dict:
    """Blend each conditional toward the marginal, capping how sharp any cell can be."""
    marginal = normalise(aggregate(counts))
    return {key: {k: (1 - floor) * v + floor * marginal[k] for k, v in dist.items()}
            for key, dist in normalise_each(counts).items()}


def suppress_sparse(counts: dict, k: int = 12) -> dict:
    """Drop conditionals estimated from fewer than k individuals; fall back to the marginal."""
    marginal = normalise(aggregate(counts))
    return {key: (dist if sum(dist.values()) >= k else marginal)
            for key, dist in counts.items()}

Coarsening reduces how uniquely the quasi-identifiers describe anybody, smoothing bounds how sharp any single conditional can be, and sparse suppression removes the conditionals that are effectively about one person. All three cost utility, and all three cost it in a different place, which is why they are worth applying together rather than pushing any one of them hard.

3 — Do not reach for more coordinate noise

The instinct on discovering an inference leak is to perturb coordinates further. It does very little, and the reason is worth stating: the attack does not need the individual’s exact position. It needs the neighbourhood’s conditional distribution, and moving a point a hundred metres leaves it in the same neighbourhood. Coordinate noise defends against re-identification and barely touches attribute inference.

Four attribute-inference defences by advantage reduction and utility cost Four defences run along the horizontal axis, each with two bars. The first is more coordinate noise: it reduces the inference advantage by a few per cent and costs a substantial amount of positional utility, which is the worst combination available and is also the defence teams reach for first. The second is coarsening the quasi-identifiers to a larger cell and wider bands: it reduces the advantage substantially at a moderate utility cost, because it reduces how uniquely the quasi-identifiers describe anybody. The third is smoothing each conditional distribution toward the marginal, which caps how sharp any single conditional can be: a large reduction, and the utility cost falls entirely on the conditional relationships rather than on positions. The fourth is suppressing conditionals estimated from fewer than a dozen individuals and falling back to the marginal: a large reduction at a small utility cost, because the suppressed cells were estimated from too little data to be reliable anyway. The note underneath draws the practical conclusion: the last three compose, they cost utility in different places, and applying all three moderately beats pushing any one of them hard. The defence everybody reaches for first is the only one that does not work 0% 20% 40% 60% per cent 8 55 62 47 34 22 18 7 more coordinate noise coarsen the quasi-identifiers smooth the conditionals suppress sparse conditionals reduction in inference advantage utility cost The last three compose and they cost utility in different places — positions, conditional sharpness, and sparse-cell fidelity — so applying all three moderately beats pushing any one of them hard.
Four defences against the same attack: the three that operate on the conditional distribution work, and the one everybody reaches for first does not.

Verification Step: Gate the Advantage With the Utility Beside It

python
ADVANTAGE_CEILING = 0.15


def test_attribute_inference_advantage(release, real_targets, contract):
    adv = attribute_advantage(release, real_targets,
                              quasi=contract["quasi_identifiers"],
                              sensitive=contract["sensitive_attribute"])
    assert adv <= ADVANTAGE_CEILING, f"attribute advantage {adv:.2f}"


def test_utility_survived_the_cap(release, reference, contract):
    """The defence must not have removed the correlation the release exists to carry."""
    got = rank_correlation(release, contract["utility_pair"])
    want = rank_correlation(reference, contract["utility_pair"])
    assert abs(got - want) < contract["correlation_tolerance"], (got, want)


def test_quasi_identifier_set_is_declared(contract):
    assert contract.get("quasi_identifiers"), (
        "an undeclared quasi-identifier set means the attack was run against a guess"
    )

Running the two assertions together is the point. Either one alone is trivially satisfiable — destroy the correlation and the advantage goes to zero; carry it fully and the utility is perfect — and it is only the pair that describes a release worth shipping.

Edge Cases & Gotchas

The sensitive attribute is the one consumers want to model. Then the release is being asked to carry the exact information the attack extracts, and no parameter resolves it. The resolution is structural: publish aggregates of the sensitive attribute rather than per-record values, or restrict the release to an environment where the quasi-identifiers are not available.

Share of individuals uniquely described, as quasi-identifiers accumulate Quasi-identifiers are added left to right along the horizontal axis, and the vertical axis is the share of individuals in the population that the accumulated set describes uniquely. A home location coarsened to a five-hundred-metre cell identifies almost nobody uniquely on its own. Adding a work location at the same resolution changes that sharply: the pair of home and work cells is close to unique across a metropolitan population, and the curve jumps. Adding an age band and then a household size pushes it further, close to complete uniqueness. Markers give the value at each step. The point the chart makes is that spatial quasi-identifiers combine much more aggressively than tabular ones, because two locations are jointly far more distinctive than any two demographic attributes, and that a defence sized against a single location will be badly under-specified against a pair. The note underneath records the practical consequence: the quasi-identifier set is an assumption about what an adversary holds, it belongs in the contract as a versioned declaration rather than an implicit choice, and it has to be revisited whenever a new public dataset overlaps the release. Two locations are jointly far more distinctive than any two attributes 0% 25% 50% 75% 100% uniquely described (%) 3% home cell 500 m 61% + work cell 500 m 84% + age band 10 y 93% + household size the jump is the pair The quasi-identifier set is an assumption about what an adversary holds. It belongs in the contract as a versioned declaration rather than an implicit choice, and it has to be revisited whenever a new public dataset overlapping the release appears — because what counts as a quasi-identifier depends on what else exists in the world.
Spatial quasi-identifiers combine aggressively: a home cell alone identifies almost nobody, and a home-and-work pair identifies most people.

Neighbours leak about each other. Spatial autocorrelation means an individual’s sensitive value is partly predictable from the neighbours in the release even if their own record is suppressed. Suppression at the individual level is therefore weaker here than in tabular data, and the cell-level defences — coarsening and sparse suppression — do more.

The adversary’s quasi-identifier set grows. What is a quasi-identifier depends on what else exists in the world, and that changes. Re-run the attack when a new public dataset appears that overlaps the release, and treat the declared set as a version rather than a constant.

The advantage is high on a small subgroup. An aggregate advantage inside the ceiling can hide a subgroup where it is near one — a rare category, a sparsely populated area. Report the advantage per stratum as well as overall; the worst stratum is the one an adversary will use.

Reporting the Result

Attribute inference is the attack whose result is hardest to communicate, because a single number does not carry it. Three fields do.

The advantage, with the quasi-identifier set beside it. An advantage of 0.11 means nothing without knowing what the adversary was assumed to hold. The same release scores very differently against a home cell alone and against a home-and-work pair, and quoting the number without the assumption is quoting half a result.

The worst stratum, not only the aggregate. An overall advantage inside the ceiling routinely hides a subgroup — a rare category, a sparsely populated district — where it approaches one. An adversary will use the worst stratum, so that is the number that describes the release’s actual exposure, and reporting only the mean understates it in exactly the cases that matter.

The utility that was preserved. Because the defence and the requirement pull on the same information, an advantage number alone is uninterpretable: it could describe a well-defended release or one whose correlations were destroyed. Publishing the rank correlation the release still carries, beside the advantage, is what shows the trade was made deliberately.

Together those three make the result reviewable by somebody who was not in the room — which is the standard every other privacy artifact in this area is held to, and there is no reason this one should be exempt.