Privacy–Utility Trade-off Measurement

Every privacy control on a spatial pipeline is a trade, and almost every team makes that trade without measuring either side of it. The privacy side at least has a number — an ε\varepsilon, a kk, a cell edge in metres — but the utility side is usually defended with a screenshot of a map that “still looks right”. That asymmetry is why parameter arguments never resolve: one party quotes a guarantee and the other quotes an impression. This guide replaces the impression with a measured quantity. It sits under Core Fundamentals & Architecture for Spatial Privacy and feeds the routing thresholds set by the spatial sensitivity scoring models, the caps enforced by privacy budget management, and the architecture choice worked through in the privacy model comparison.

The concrete engineering scenario throughout is a metro mobility team publishing an hourly origin–destination matrix and a hotspot map from device pings. They need to answer one question with evidence: what does this release lose, and at which parameter setting does it stop being worth publishing?

The measurement loop that turns a parameter argument into a decision A left-to-right loop diagram. A raw extract is held out as ground truth on the left. It flows into a mechanism box configured with a parameter set: epsilon, grid resolution and the anonymity floor. The mechanism produces a protected release. Both the ground truth and the protected release feed a measurement bank containing four task-level metrics: count error, ranking agreement, spatial structure and downstream decision agreement. The measurement bank emits a utility vector, which is compared against a service level objective. A pass publishes the release and records the parameter set; a fail returns along a feedback arrow to the mechanism box to widen the grid or raise epsilon, and the loop repeats. Measuring the trade instead of arguing about it Held-out extract ground truth, never published Mechanism ε · resolution · k_min one parameter set the thing under test Protected release what analysts see Measurement bank 1 · count error (MAE, MRE) 2 · ranking agreement (top-k) 3 · spatial structure (Moran's I) 4 · decision agreement each metric names the task an analyst actually performs → utility vector Compare to the SLO every metric must pass pass → publish + log fail → coarsen, retry feedback: coarsen the grid before spending more budget

Prerequisites

  • A held-out extract you are willing to treat as truth. Measurement compares a protected release against an unprotected one, so the ground-truth extract is itself sensitive and must live inside the trust boundary, be access-controlled, and never be published. Teams that skip this because “we can’t hold raw data” end up with no measurement at all.
  • numpy, geopandas, shapely, and h3 for binning and geometry; scipy.stats for rank correlation. No modelling framework is required — every metric here is arithmetic over two arrays.
  • A named downstream task. “Utility” is not a property of a dataset; it is a property of a dataset with respect to a question. Write the question down first: “which 20 cells should get extra buses on a Tuesday evening” is measurable; “the map should look right” is not.
  • A fixed privacy unit and accounting method. Utility numbers computed under a device-day unit are not comparable with numbers computed under an event-level unit, so record the unit alongside every measurement.

Step 1: Define the utility vector, not a utility number

There is no single utility scalar, and forcing one hides the failure that matters. Use a small vector of task-aligned metrics, each with an independent threshold, and require all of them to pass.

Metric What it answers Typical SLO
Mean relative error per cell “Is this count usable as a number?” < 5% on cells above the reporting floor
Top-kk agreement “Is the hotspot list the same list?” ≥ 90% overlap at k=20k = 20
Moran’s I delta “Does the map still have its spatial structure?” `
Decision agreement “Would the operational decision change?” ≥ 95% of decisions unchanged
Suppression rate “How much of the map disappeared?” < 10% of occupied cells

Relative error is the metric everyone reaches for first and the one that most often misleads. It is dominated by small cells, where the noise is comparable to the count, and those cells are usually the ones nobody acts on. Reporting a single MRE across all cells therefore fails releases that are perfectly fit for purpose. Compute it conditioned on the reporting floor: over cells whose true count clears the threshold at which anyone would act on the number.

Decision agreement is the metric almost nobody computes and the only one that speaks the language of the people who approve the release. Encode the downstream decision as a function of the data — “deploy extra capacity where the noisy count exceeds 400” — apply it to both the truth and the release, and report the fraction of cells where the two decisions agree. A release whose counts are 20% off but whose decisions match 99% of the time is a good release; one whose counts are 3% off but whose decisions flip on a third of the borderline cells is not.

Step 2: Sweep the parameter grid, do not guess a point

Utility is not monotone in a single parameter, because resolution, ε\varepsilon and the anonymity floor interact: a finer grid raises relative error but lowers suppression bias, and a higher floor does the reverse. The only reliable approach is a sweep over the cross-product, evaluated on the held-out extract, with several noise draws per point so the numbers are not one lucky sample.

A measured sweep, with the SLO applied A five-row sweep over grid resolution and epsilon, measured on a 700-cell field. Each row reports mean relative error on actionable cells, top-twenty ranking agreement, the suppressed-cell share, and a pass or fail verdict against the service level objective. Coarser grids pass comfortably; the finest resolution fails on relative error and suppression at the same budget, showing that resolution rather than epsilon is what moves this release across the threshold. A measured sweep, with the SLO applied 15 noise draws per row; the frontier is every row marked pass rel. error top-20 agreement suppressed verdict res 7, ε = 0.5 0.6% 100% 0% pass res 7, ε = 1.0 0.3% 100% 0% pass res 8, ε = 0.5 2.6% 100% 0% pass res 8, ε = 1.0 1.3% 100% 0% pass res 9, ε = 1.0 2.5% 98% 23% fail
Read the sweep by discarding failing rows, then taking the cheapest surviving one in privacy terms. Picking the best-utility passing row is how budgets get spent on accuracy nobody asked for.
python
from __future__ import annotations

import itertools
from dataclasses import dataclass
from typing import Callable, Iterable, Sequence

import numpy as np


@dataclass(frozen=True)
class ParamSet:
    """One point on the sweep grid."""

    resolution: int          # H3 resolution used for binning
    epsilon: float           # per-release budget for the whole grid
    k_min: int               # anonymity floor; cells below it are suppressed


@dataclass(frozen=True)
class Utility:
    """The utility vector for one parameter set (means over the draws)."""

    mean_rel_error: float
    topk_agreement: float
    suppression_rate: float
    decision_agreement: float

    def passes(self, slo: "Utility") -> bool:
        return (
            self.mean_rel_error <= slo.mean_rel_error
            and self.topk_agreement >= slo.topk_agreement
            and self.suppression_rate <= slo.suppression_rate
            and self.decision_agreement >= slo.decision_agreement
        )


def evaluate(
    truth: np.ndarray,
    params: ParamSet,
    *,
    decision: Callable[[np.ndarray], np.ndarray],
    draws: int = 25,
    report_floor: float = 25.0,
    k: int = 20,
    seed: int = 0,
) -> Utility:
    """Measure one parameter set over `draws` independent noise realisations.

    `truth` is the unprotected per-cell count vector at this resolution. The noise
    is calibrated to sensitivity 1 under a device-day privacy unit, which is only
    valid because contributions were clipped upstream — without that clipping the
    scale below understates the real sensitivity.
    """
    rng = np.random.default_rng(seed)
    scale = 1.0 / params.epsilon
    actionable = truth >= report_floor
    true_top = set(np.argsort(-truth)[:k].tolist())
    true_decision = decision(truth)

    rel_errors, agreements, suppressions, decisions = [], [], [], []
    for _ in range(draws):
        noisy = truth + rng.laplace(0.0, scale, size=truth.shape)
        released = np.where(noisy >= params.k_min, np.maximum(noisy, 0.0), np.nan)

        observed = np.nan_to_num(released, nan=0.0)
        rel_errors.append(
            float(np.mean(np.abs(observed[actionable] - truth[actionable]) / truth[actionable]))
        )
        noisy_top = set(np.argsort(-observed)[:k].tolist())
        agreements.append(len(true_top & noisy_top) / k)
        suppressions.append(float(np.mean(np.isnan(released)[truth > 0])))
        decisions.append(float(np.mean(decision(observed) == true_decision)))

    return Utility(
        mean_rel_error=float(np.mean(rel_errors)),
        topk_agreement=float(np.mean(agreements)),
        suppression_rate=float(np.mean(suppressions)),
        decision_agreement=float(np.mean(decisions)),
    )


def sweep(
    truth_by_resolution: dict[int, np.ndarray],
    resolutions: Sequence[int],
    epsilons: Sequence[float],
    k_mins: Sequence[int],
    *,
    decision: Callable[[np.ndarray], np.ndarray],
    slo: Utility,
) -> list[tuple[ParamSet, Utility, bool]]:
    """Evaluate the full cross-product and mark which points clear the SLO."""
    out: list[tuple[ParamSet, Utility, bool]] = []
    for res, eps, kmin in itertools.product(resolutions, epsilons, k_mins):
        params = ParamSet(resolution=res, epsilon=eps, k_min=kmin)
        util = evaluate(truth_by_resolution[res], params, decision=decision)
        out.append((params, util, util.passes(slo)))
    return out


def _self_test() -> None:
    rng = np.random.default_rng(7)
    truth = np.clip(rng.lognormal(mean=4.4, sigma=1.1, size=800), 0, None)
    by_res = {8: truth, 9: truth / 7.0}
    decide = lambda counts: counts >= 400.0

    slo = Utility(mean_rel_error=0.05, topk_agreement=0.90,
                  suppression_rate=0.10, decision_agreement=0.95)
    results = sweep(by_res, [8, 9], [0.5, 1.0, 2.0], [5, 10], decision=decide, slo=slo)

    assert len(results) == 12, "the sweep must evaluate the full cross-product"
    # Utility is monotone in epsilon at a fixed grid: more budget, less error.
    at_res8 = {(p.epsilon, p.k_min): u for p, u, _ in results if p.resolution == 8}
    assert at_res8[(2.0, 5)].mean_rel_error < at_res8[(0.5, 5)].mean_rel_error
    # A finer grid at the same epsilon is strictly worse for relative error.
    coarse = [u for p, u, _ in results if p.resolution == 8 and p.epsilon == 1.0][0]
    fine = [u for p, u, _ in results if p.resolution == 9 and p.epsilon == 1.0][0]
    assert fine.mean_rel_error > coarse.mean_rel_error
    print("privacy-utility sweep: assertions passed")


_self_test()

The sweep returns a frontier, not a winner. Read it by discarding every point that fails the SLO, then choosing the cheapest surviving point in privacy terms — the smallest ε\varepsilon and the coarsest grid that still passes. Teams routinely do the opposite, picking the point with the best utility that is “still private enough”, which spends budget for accuracy nobody asked for.

Step 3: Separate the three sources of error

When a release fails its SLO, the fix depends entirely on which of three mechanisms caused the loss, and they are distinguishable by measurement rather than by inspection.

Three error sources that respond differently to budget Three measured curves against epsilon. Symmetric noise error falls steeply as budget rises, roughly as one over epsilon. Clamping bias, introduced by truncating negative counts to zero on a sparse field, also falls but from a much lower base. The suppression rate barely moves at all, because a cell holding three people stays below the floor no matter how little noise is added. The three demand different remedies. Three error sources that respond differently to budget measured on the same field; only one of them is fixed by more ε 1 0 5 10 ε per release counts / rate noise error (counts) clamping bias (counts/cell) suppression rate (×20)
Only the first curve is a budget problem. Clamping bias needs an estimator change and suppression needs a coarser grid — spending ε on either is money burned.
  • Noise error is symmetric, zero-mean, and scales as 1/ε1/\varepsilon independently of the count. Diagnose it by re-running the sweep at a much larger ε\varepsilon: if the metric recovers, noise was the cause, and more budget or a coarser grid will fix it.
  • Truncation and clipping bias is one-sided. Clamping negative noisy counts to zero inflates empty areas; clipping per-device contributions deflates dense ones. Diagnose it by comparing the sum of the release with the sum of the truth: noise alone leaves the total roughly unchanged, so a systematic gap is bias, and it does not shrink with more draws.
  • Suppression bias deletes cells non-randomly — always the sparse ones. Diagnose it by computing each metric twice, once over all cells and once over the surviving cells only. A metric that looks excellent on survivors and poor overall is telling you the release is fine and that a third of the map is gone, which is a different conversation with the analyst.

The three failures have opposite remedies, which is why lumping them into “the release is too noisy” leads to spending budget on a problem budget cannot solve. Suppression bias in particular gets worse with more budget at a fixed floor, because a higher ε\varepsilon does nothing about a cell that holds three people.

Threat model considerations

Measurement itself touches raw data and produces numbers that are published in release notes, so it carries its own disclosure risk.

  • The utility report is a side channel. “Suppression rate rose from 6% to 24% this week” is a statement about the underlying distribution, and an adversary tracking those reports learns about changes in sparse areas. Round reported utility metrics, publish them on a delay, or account for them under the same ledger as any other release.
  • Tuning on the production extract is adaptive data analysis. Every sweep evaluated against the same held-out data leaks a little of it into the chosen parameters, and after enough iterations the parameters themselves encode the data. Rotate the held-out extract, or budget the tuning as a release in its own right.
  • A published parameter set is an attack input. Once ε\varepsilon, the grid resolution and the floor are public — and they should be, for accountability — an adversary can model the noise distribution exactly and de-noise across repeated releases. Publishing parameters is correct; it just means the composition accounting has to be correct too.
  • Ground-truth extracts outlive their purpose. The measurement corpus is the most sensitive artefact the pipeline holds and the one least likely to appear in a retention policy. Give it the shortest lifetime that still allows a regression comparison, and delete on schedule.

Validation and compliance checklist

  1. Every metric names a task. Reject any utility metric that cannot be traced to a decision someone makes. Pass criterion: each metric in the vector has a named downstream consumer.
  2. Draws, not a draw. Every reported utility number is a mean over at least 20 independent noise realisations, with the standard deviation reported alongside. Pass criterion: no single-draw numbers appear in the release record.
  3. Conditioned error. Relative error is reported conditioned on the reporting floor, and the floor is stated. Pass criterion: the floor appears in the same table as the metric.
  4. Bias is separated from noise. The release record includes the total-sum gap and the suppression rate, not only a symmetric error metric. Pass criterion: both fields are non-null.
  5. The frontier is recorded. The chosen parameter set is justified against the sweep as the cheapest passing point, and the full sweep is archived. Pass criterion: the archived sweep contains at least one failing point on each side of the chosen one.
  6. Utility SLOs are versioned with the pipeline. A change to a threshold is a reviewed change, not a config edit. Pass criterion: the SLO file is under the same review policy as the mechanism.

Failure modes and remediation

  • The metric improves while the map gets worse. Almost always suppression: cells that would have dragged the average are gone, so the average over survivors improves. Remediate by always reporting the paired all-cells and survivors-only numbers.
  • Utility collapses at a resolution step and nothing else changed. The knee in the suppression curve was crossed. Remediate by moving one resolution coarser rather than raising ε\varepsilon — it is free in privacy terms and usually recovers the whole loss.
  • Measurements are not reproducible between runs. The noise seed is not pinned, or the held-out extract changed underneath the comparison. Remediate by pinning both and recording their hashes in the release record.
  • The SLO passes but analysts complain. The measured task is not the task they perform. This is a specification failure, not a mechanism failure: sit with the consumer, watch what they actually do with the release, and add that as a metric.
  • Tuning drifts toward ever-larger budgets. Nobody is enforcing the “cheapest passing point” rule. Remediate by making the sweep artefact a required input to release approval, so a chosen point that is not on the frontier has to be defended explicitly.

One further discipline is worth naming because it is where measurement programmes usually decay: keep the sweep artefact and the release record in the same place, versioned together. A utility number that cannot be traced back to the extract it was measured on, the seed it used and the parameter set it justified is an assertion rather than evidence, and six months later nobody will be able to reconstruct which of the three changed. The archive is small — a few kilobytes of JSON per release — and it is the difference between a parameter choice you can defend and one you have to re-derive under pressure.

Frequently Asked Questions

Is there a single number that captures privacy–utility trade-off?

No, and constructing one hides the failure that matters. A weighted scalar can stay flat while suppression doubles, because the improvement on surviving cells offsets the deleted ones. Use a small vector of task-aligned metrics with independent thresholds, and require all of them to pass.

How many noise draws are enough per parameter set?

Twenty to fifty is the practical range for count metrics. Ranking metrics are noisier and benefit from more; if the standard deviation across draws is more than a fifth of the mean, the number of draws is too low to distinguish two adjacent parameter sets, which is the comparison the sweep exists to make.

Can I measure utility without holding raw data?

Only partially. Internal-consistency checks — suppression rate, total mass, the variance of the released counts — need no ground truth and catch gross failures. Error and agreement metrics are definitionally comparisons against the truth, so they require a held-out extract inside the trust boundary. If that is impossible, measure on a synthetic corpus calibrated to the real density distribution and treat the results as indicative rather than as an SLO.

Does measuring utility consume privacy budget?

The measurement itself runs inside the trust boundary on data you already hold, so it consumes nothing. Publishing the results does: a utility report is a function of the raw data, and repeated reports compose. Treat the report as a release, round it, and log it in the ledger like any other.

Where should the trade-off be decided — engineering or policy?

Engineering produces the frontier; policy picks the point. That split is what makes the decision reviewable: the sweep is an objective artefact that says which parameter sets are feasible, and the choice among feasible points is a judgement about acceptable risk that belongs with the people accountable for it.

Up one level: Core Fundamentals & Architecture for Spatial Privacy.