Skip to content

Structured privacy and re-identification risk lab

The structured privacy lab is an offline evidence workflow for tabular clinical data. It profiles explicitly declared direct identifiers, quasi-identifiers, sensitive attributes, missingness, uniqueness, rare combinations, and coded population assumptions. It then measures k-anonymity, l-diversity, and t-closeness before and after deterministic generalization and whole-privacy-unit suppression.

The output is evidence for qualified review. It is not a legal safe-harbor certification, an Expert Determination, a universal anonymity guarantee, or a single risk score that authorizes release.

Python workflow

All policy choices are explicit. The lab does not infer a quasi-identifier set or choose an acceptable threshold.

from openmed.structured import (
    StructuredPrivacyPolicy,
    run_structured_privacy_lab,
)

policy = StructuredPrivacyPolicy(
    quasi_identifiers=("age", "postal_prefix"),
    sensitive_attributes=("diagnosis",),
    direct_identifiers=("synthetic_record_id",),
    target_k=3,
    target_l=2,
    target_t=0.5,
    suppression_limit=2,
    membership_max_inference_rate=0.0,
)

result = run_structured_privacy_lab(
    rows,
    policy,
    population_assumptions={
        "scope": "reviewed_synthetic_cohort",
        "population_kind": "synthetic_fixture",
    },
    membership_candidates=local_candidate_rows,
)

if result.meets_policy:
    release_rows = result.records  # Keep this separate from evidence.
evidence_json = result.evidence.to_json()

result.evidence contains schema and dataset hashes, coded parameters, population assumptions, before/after k/l/t measurements, transformation and utility deltas, bounded membership-test results, and limitations. It contains no raw cell values, record identifiers, equivalence-class keys, or source paths. Transformed rows are retained only in the local result object and are not serialized by the evidence methods.

CLI workflow

The equivalent local workflow is:

openmed risk lab input.jsonl \
  --evidence structured-risk.json \
  --output release.jsonl \
  --qi age,postal_prefix \
  --sensitive diagnosis \
  --direct-id synthetic_record_id \
  --k 3 --l 2 --t 0.5 \
  --suppression-limit 2 \
  --population-scope reviewed_synthetic_cohort \
  --overwrite

The release output is written only when the configured release and optional membership gates pass. A failed policy still produces aggregate evidence and returns a non-zero status. Use --membership-candidates to run the bounded local self-test; set --membership-max-inference-rate explicitly when that test is part of the release policy.

Aggregate differential privacy

Differential privacy is a separate aggregate-release mechanism. The ledger composes named epsilon/delta spends deterministically and rejects a query that would exceed the declared budget. The Laplace API accepts only a scalar or a mapping of numeric aggregates and rejects row-shaped input:

from openmed.risk import AggregateDPBudgetLedger, release_aggregate

ledger = AggregateDPBudgetLedger(max_epsilon=1.0, max_delta=0.0)
aggregate = release_aggregate(
    {"count": 120},
    ledger=ledger,
    epsilon=0.25,
    seed="synthetic-test-seed",
)

The resulting ledger and mechanism output say aggregate_only and row_level_anonymization: false. A differential-privacy aggregate budget does not transform, anonymize, or authorize row-level release. The CLI equivalent is openmed risk dp-aggregate with a local JSON object of named numeric aggregates and explicit --epsilon, --budget-epsilon, and --budget-delta choices.

Limitations

The lab is intentionally bounded and local. k/l/t metrics depend on the declared population unit and published representations; the membership probe tests only the supplied candidate population and exact declared QIs; and aggregate differential privacy does not cover row-level release. Reviewers must document auxiliary data, recipients, release context, utility needs, and any additional attack models before deciding whether to release data.