Documenting Residual Disclosure Risk for a Release
This guide, part of Disclosure Review & Release Governance, covers the record that turns an approval into an accountable decision: a statement of the disclosure risk that remains after all protections, with evidence and a named owner.
Problem Context & Constraints
Suppression, geomasking, aggregation and noise reduce disclosure risk; none eliminates it. A tract-level rate table with cells below 11 suppressed can still be combined with local knowledge — a neighbour knows the family whose child was hospitalised — to learn something about an individual. A geomasked point file with spatial k-anonymity of 20 still places each case among 20 households. A differentially private count at epsilon 1 still leaks a bounded amount of information about each person.
When a release is approved, that remaining risk is accepted, whether or not anyone says so. The purpose of a residual risk statement is to make the acceptance explicit: what risk remains, how it was measured, why it is acceptable given the purpose, and who accepted it. Without it, an approval records only that a release happened, and when a question arises later — from a journalist, an auditor, a community — nobody can explain the reasoning.
Three constraints shape a good statement. It must be specific to the release: generic wording such as “risk is minimal” says nothing. It must be supported by numbers where numbers exist: minimum population per cell, spatial k-anonymity, privacy budget spent, differencing check results. And it must be kept current: a statement written for one release becomes wrong when later releases change what can be combined with it.
Prerequisites
python3.11pandas2.2.2,numpy1.26.4,pydantic2.7.1- The released product and the pre-protection data it was derived from, inside the secure environment
- Population counts at the release geography and stratification
- Results of the differencing check, as in Detecting Differencing Attacks Across Published Tables
- For masked point data, spatial k-anonymity results from Validating Geomasks With Spatial K-Anonymity
- For differentially private products, the privacy budget ledger
Step-by-Step Solution
Compute the metrics that apply to the product type, fill a structured statement with scenarios, metrics, combination results and justification, have the owner sign it, and register it with a review date.
# Residual disclosure risk statement with computed metrics.
# Pinned: pandas==2.2.2, numpy==1.26.4, pydantic==2.7.1
import datetime as dt
import logging
import numpy as np
import pandas as pd
from pydantic import BaseModel, Field
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("residual_risk")
class Metrics(BaseModel):
min_cell_population: float | None = None
p05_cell_population: float | None = None
share_cells_pop_under_500: float | None = None
min_spatial_k: int | None = None
median_spatial_k: float | None = None
epsilon_spent: float | None = None
epsilon_cumulative: float | None = None
determined_small_atoms: int | None = None
class Statement(BaseModel):
product_id: str
scenarios: list[str] = Field(min_length=1)
metrics: Metrics
combination: str
justification: str = Field(min_length=80)
residual_summary: str = Field(min_length=40)
owner: str
accepted_on: dt.date
review_by: dt.date
def table_metrics(released: pd.DataFrame, pop: pd.Series) -> Metrics:
"""released: one row per shown cell with geo_id and stratum; pop indexed alike."""
shown = released[released["count"].notna()]
p = pop.reindex(pd.MultiIndex.from_frame(shown[["geo_id", "stratum"]])).to_numpy(float)
return Metrics(min_cell_population=float(np.nanmin(p)),
p05_cell_population=float(np.nanpercentile(p, 5)),
share_cells_pop_under_500=float(np.nanmean(p < 500)))
def point_metrics(k_values: np.ndarray) -> Metrics:
return Metrics(min_spatial_k=int(k_values.min()), median_spatial_k=float(np.median(k_values)))
def dp_metrics(ledger: pd.DataFrame, product_id: str, population_id: str) -> Metrics:
mine = ledger[ledger["product_id"] == product_id]["epsilon"].sum()
total = ledger[ledger["population_id"] == population_id]["epsilon"].sum()
return Metrics(epsilon_spent=float(mine), epsilon_cumulative=float(total))
def register_statement(st: Statement, register: list[dict]) -> None:
if st.review_by <= st.accepted_on:
raise ValueError("review_by must be after accepted_on")
register.append(st.model_dump(mode="json"))
log.info("statement %s owner=%s review_by=%s", st.product_id, st.owner, st.review_by)
Population metrics matter more than count metrics. A table that suppresses counts below 11 passes the rule whether the smallest shown cell covers 11 people or 11,000, but the risk differs enormously. Reporting the minimum and 5th-percentile population behind shown cells, and the share of cells covering fewer than 500 people, tells the owner how close the release comes to identifying individuals by local knowledge.
The cumulative epsilon for differentially private products is the figure that matters over time. A single release at epsilon 1 is modest; the twelfth monthly release against the same population brings cumulative privacy loss to a level the program may not have intended when it approved the first. The statement for a recurring product should therefore state the ceiling the program has agreed for that population, how much of it has been used, and at which release the ceiling will be reached if nothing changes.
Validation & Edge Cases
Scenarios without an adversary model. A statement should name realistic attackers: a neighbour with local knowledge, a journalist with public records, an insurer with claims data, a researcher with a linked dataset. Each implies different risks. A table safe against a journalist may not be safe against a neighbour.
Metrics that do not apply. Not every metric fits every product. Leave inapplicable fields empty rather than filling them with values that look reassuring but mean nothing; an empty field is honest, a meaningless number is not.
Justification that restates the approval. “The release is necessary for public health” is not a justification. Say what decision or action the release supports, why coarser data would not serve, and what harm withholding it would cause.
Owners who have left. A statement owned by someone no longer with the program has no owner. When staff leave, transfer ownership of their open statements explicitly, and let the review date trigger re-evaluation by the new owner.
Statements invalidated by later releases. When a new release is registered, re-run the differencing check against every release whose statement is still open, and flag any whose combination result changes. That flag should prompt an early review.
Risk that depends on the reader’s location. The same table carries different risk for different readers. For most of the state, a tract-level count of seven is anonymous; for residents of a small town where everyone knows who was sick, it is not. Scenarios should consider the reader closest to the data — a neighbour, a co-worker, a relative — because that is the reader most able to connect a released value to a person. Where the product covers small, close-knit communities, the statement should say so and explain what was done about it.
Quantifying what cannot be measured. Some risks resist metrics: the chance that a journalist links a masked case to a news report, or that an employer infers a diagnosis from sick leave. Do not invent a number. Describe the scenario, the protections that bear on it, and a qualitative judgement of likelihood and impact, so that the owner accepts an honestly described risk rather than a precise-looking figure.
Public versions. Some programs publish a short public disclosure note with each release. It should describe the protections and the general level of residual risk without revealing which areas are close to thresholds — the detailed metrics stay internal.
Statements for withdrawn releases. Withdrawing a release does not withdraw it from the world; copies persist in caches, downloads and archives. Keep the statement open and owned after withdrawal, note the withdrawal date and reason, and continue to include the release in differencing checks.
INFO residual_risk statement od-tract-2025 owner=chief.epidemiologist review_by=2027-09-18
WARNING residual_risk open statement flu-zcta-monthly: combination result changed after release flu-district-q3
Compliance Notes
Keep every statement, its supporting metrics, the owner’s acceptance, and each review with its outcome in the release register. Statements are internal governance records; they may reference small populations and should be access-controlled accordingly.
Where law or policy requires a privacy impact assessment or a data protection impact assessment, the residual risk statement for each release is a natural attachment to it, and the assessment should reference the statements for the products it covers. Auditors will ask who accepted the risk and on what evidence; the statement answers both.
Related Topics
- Disclosure Review & Release Governance — the parent guide.
- Running a Disclosure Review Board Checklist — where statements are accepted.
- Allocating a Privacy Budget Across Nested Geographies — tracking cumulative privacy loss.