Choosing K for Rare Disease Point Releases

This guide, part of Spatial K-Anonymity for Health Microdata, is about the single parameter that decides both how private and how useful a masked point release is.

Problem Context & Constraints

Spatial k-anonymity requires that each released case location could plausibly belong to at least k households or people. With geomasking, that means each case is displaced far enough that at least k residential addresses lie within the area it could have come from. A higher k gives stronger protection and larger displacement; a lower k keeps points closer to where they belong and preserves more spatial detail for analysis.

Common defaults — k of 5, 10 or 20 — come from tabular disclosure practice and from studies of common conditions. They transfer poorly to rare diseases, for two opposite reasons. Rarity raises risk. For a disease with a dozen cases a year statewide, knowing that a masked point lies in a neighbourhood already narrows the possibilities sharply; a neighbour who knows someone was hospitalised with the condition needs little more. Rarity also raises cost. The analyses rare-disease programs run — cluster investigation around a suspected environmental source, for instance — depend on fine spatial detail, and a large k can displace the few cases so far that a real cluster disappears.

Choosing k is therefore a trade-off to be made explicitly for each release, informed by three things: the number of cases and their spatial spread, the attacker’s likely knowledge, and the analysis the release must support, measured by how much the masked data distort the result that matters.

Protection Rises, Detail Falls Two curves against k from five to one hundred. The protection curve, the probability that a neighbour with local knowledge cannot single out a case, rises quickly and levels off. The utility curve, the probability that a planted cluster is still detected in the masked data, falls slowly at first and then steeply above about k equals forty for a rare disease with few cases. A shaded band between about k equals fifteen and thirty marks where both remain acceptable. Find the band where both are acceptable acceptable band protection cluster still detected 5 20 40 100 k (households per released case)

Prerequisites

  • python 3.11
  • numpy 1.26.4, pandas 2.2.2, geopandas 0.14.4, scipy 1.13.1
  • Geocoded case points and a residential address point layer (or block-level household counts) in a projected metric coordinate reference system
  • A geomasking routine that accepts a target k, such as the donut method in Donut Geomasking for Patient Address Points
  • The analysis the release must support, stated in advance: a cluster test, a distance-to-source analysis or a density surface

Step-by-Step Solution

For a grid of k values, mask the data many times, measure protection as the chance a local attacker can single out a case, measure utility as the chance the target analysis still gives the right answer, and choose the smallest k that meets the protection requirement while keeping utility above a floor.

# Choose k for a rare-disease point release by simulating protection and utility.
# Pinned: numpy==1.26.4, pandas==2.2.2, geopandas==0.14.4, scipy==1.13.1
import logging

import numpy as np
import pandas as pd
from scipy.spatial import cKDTree

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("choose_k")

SEED = 20260918


def mask_to_k(cases_xy: np.ndarray, homes_xy: np.ndarray, k: int, rng) -> np.ndarray:
    """Donut mask: displace each case to a random point whose neighbourhood,
    out to the kth-nearest home, contains at least k homes."""
    tree = cKDTree(homes_xy)
    r_k = tree.query(cases_xy, k=k)[0][:, -1]           # distance to kth home
    r_min = 0.5 * r_k
    theta = rng.uniform(0, 2 * np.pi, len(cases_xy))
    dist = np.sqrt(rng.uniform(r_min ** 2, r_k ** 2))
    return cases_xy + np.column_stack([dist * np.cos(theta), dist * np.sin(theta)])


def local_attacker_success(true_xy, masked_xy, known_share: float, rng) -> float:
    """Attacker knows a random share of case households (e.g. neighbours' gossip)
    and guesses the nearest known case household to each masked point."""
    n = len(true_xy)
    known = rng.random(n) < known_share
    if not known.any():
        return 0.0
    tree = cKDTree(true_xy[known])
    _, idx = tree.query(masked_xy[known])
    return float(np.mean(idx == np.arange(known.sum())))


def cluster_detected(masked_xy, source_xy, radius_m: float, expected: float) -> bool:
    """Utility: is the excess of cases within radius of a suspected source still visible?"""
    obs = int((np.hypot(*(masked_xy - source_xy).T) <= radius_m).sum())
    return obs >= expected * 2.0 and obs >= 3


def sweep(true_xy, homes_xy, source_xy, radius_m, expected, ks=(5, 10, 15, 20, 30, 50),
          reps: int = 200, known_share: float = 0.3) -> pd.DataFrame:
    rng = np.random.default_rng(SEED)
    rows = []
    for k in ks:
        succ, det, disp = [], [], []
        for _ in range(reps):
            m = mask_to_k(true_xy, homes_xy, k, rng)
            succ.append(local_attacker_success(true_xy, m, known_share, rng))
            det.append(cluster_detected(m, source_xy, radius_m, expected))
            disp.append(np.median(np.hypot(*(m - true_xy).T)))
        rows.append({"k": k, "attacker_success": np.mean(succ),
                     "cluster_detected": np.mean(det), "median_disp_m": np.mean(disp)})
    df = pd.DataFrame(rows)
    log.info("sweep n_cases=%d:\n%s", len(true_xy), df.round(3).to_string(index=False))
    return df


def choose(df: pd.DataFrame, max_attack: float = 0.05, min_utility: float = 0.8) -> int | None:
    ok = df[(df.attacker_success <= max_attack) & (df.cluster_detected >= min_utility)]
    if ok.empty:
        log.warning("no k meets both targets; release aggregated counts instead")
        return None
    return int(ok["k"].min())

The attacker model is deliberately concrete. It assumes the attacker already knows which households have a case for some share of cases — the realistic threat for rare diseases, where neighbours and relatives often know — and asks whether the masked points let them match those known cases to released records, which would reveal the other attributes in the release. A 30 percent known share is a reasonable starting assumption for small communities; use a higher value where the disease carries visible signs or requires frequent treatment.

The utility test should mirror what the release is for. A release meant for cluster investigation around a suspected source measures whether the excess near the source survives masking. A release meant for kernel density maps would measure how far the masked density surface departs from the true one.

Reading the Sweep A table with rows for k equal to five, ten, twenty and fifty. Attacker success falls from eighteen percent at k five to seven percent at k ten, three percent at k twenty and one percent at k fifty. Cluster detection falls from ninety-six percent to ninety-three, eighty-four and fifty-one percent. Median displacement rises from ninety metres to one hundred and forty, two hundred and forty and five hundred and ten metres. With targets of at most five percent attacker success and at least eighty percent detection, k equals twenty is chosen. Smallest k meeting both targets k Attacker success Cluster detected Median shift 5 18% 96% 90 m 10 7% 93% 140 m 20 3% 84% 240 m 50 1% 51% 510 m targets: attacker success at most 5%, detection at least 80%

Validation & Edge Cases

No k satisfies both targets. With very few cases, any k large enough to protect them may destroy the analysis. That is a finding, not a failure: release aggregated counts at a geography that meets tabular rules, or provide the point data to a named researcher under a data use agreement in a secure enclave rather than as a masked file.

Rural sparsity. In rural areas the kth-nearest household can be kilometres away, so the same k produces much larger displacement than in towns. That is correct — the protection is the same — but the utility loss concentrates in rural areas. Report median displacement by urbanicity so users know where the data are least precise.

Institutional addresses. Cases in nursing homes, prisons or group quarters share one address with many residents. Counting households there by address points undercounts people, so k is overstated. Use population counts for group quarters rather than address counts.

Repeated releases. Masking the same cases independently in two releases lets an attacker average the two masked locations and approach the true one. Mask once and reuse the masked locations, or release each case only once.

Attributes that re-identify. Point location is only one quasi-identifier. Age, sex, onset date and race, released alongside a masked point, can identify a case even when k is large. Choose k together with the attribute generalisation, and test the attacker model with the attributes that will be released.

Choosing the targets. The 5 percent attacker and 80 percent utility thresholds are policy choices. Set them with the review board before running the sweep, so the chosen k follows from agreed targets rather than the targets being adjusted to fit a preferred k.

Changing case counts. A k chosen for a year with 14 cases may not hold for a year with 40, or with 6. Rerun the sweep for each release rather than carrying a k forward, since both the protection and the utility curves move with the number and spread of cases.

INFO choose_k sweep n_cases=14:
 k  attacker_success  cluster_detected  median_disp_m
 5             0.181             0.960           91.2
10             0.071             0.930          139.8
20             0.029             0.845          238.4
50             0.009             0.510          507.1
INFO choose_k chosen k=20
When No K Works A decision flow. If some k meets both the protection and utility targets, release masked points at the smallest such k. If none does, there are two alternatives: release aggregated counts at a geography that satisfies tabular rules, which sacrifices spatial detail for everyone, or provide the true points to a named researcher in a secure enclave under a data use agreement, which preserves detail for one approved analysis. If masking cannot serve both goals Any k meets both? Yes: mask at smallest such k No: aggregated counts for all No: true points in a secure enclave the review board chooses between the two alternatives

Compliance Notes

Record the case count, the household source, the masking method, the attacker model and its known share, the utility test and its parameters, the targets and who set them, the full sweep results and the chosen k, together with median displacement by urbanicity.

Rare-disease point data are among the highest-risk products a program can release. The chosen k, the sweep and the attacker assumptions belong in the residual risk statement for the release, and the review board should see the sweep, not only the chosen value, so it can judge the margin between the chosen k and the point where protection or utility fails.