Choosing Epsilon for a First Spatial Release

Somebody has to write a number into the manifest, there is no precedent to copy, and every source that discusses the choice explains what epsilon means without saying what to set it to.

Part of Privacy-Preserving Generation Frameworks: where that page covers the mechanisms and their guarantees, this one is the practical question of picking the parameter the first time, and of recording the choice so it can be defended and revised.

Root Cause: Epsilon Has No Absolute Meaning

The reason no source tells you what to set is that there is no answer independent of the data and the release. Epsilon bounds a ratio of probabilities; it says nothing on its own about how far a point moves, how many people are protected, or whether anything useful survives.

Three things convert it into something decidable, and all three are properties of your release rather than of the definition:

  • The sensitivity. How much one individual can change the output. For a coordinate this is a distance, and it is the parameter that turns epsilon into metres. Nothing about a chosen epsilon means anything until the sensitivity is fixed.
  • The utility floor. The worst distortion a consumer will accept, expressed in their units — a positional error, a cell-count error, a metric tolerance. This gives an upper bound on the noise and therefore a lower bound on epsilon.
  • The release schedule. How many times this population will be published against. Because the budget composes, a per-release epsilon that is fine once is not fine fifty-two times, and the schedule fixes the relationship between the two.
Three inputs to an epsilon choice, the bound each sets and who owns it Three input cards feed into a single decision. Sensitivity — how far one individual can move the output — converts epsilon into metres and is owned by the pipeline engineer; without it, a chosen epsilon means nothing at all, because the noise scale is undefined. The utility floor — the worst distortion a consumer will accept, in their units at a stated percentile — sets the lower bound on epsilon and is owned by the consumer; without it, there is no reason to prefer any epsilon to a smaller one and the choice defaults to whatever produces data somebody eyeballed. The release schedule — how many times this population will be published against, and under which composition rule — sets the upper bound and is owned by the product owner; without it, the first release looks fine and the twentieth exhausts the budget. The three converge on a feasible interval, and the diagram notes that two of the three owners sit outside the privacy team, which is why the choice is a conversation rather than a calculation. Two of the three inputs are owned outside the privacy team sensitivity how far one individual can move the output converts ε into metres owned by the pipeline engineer without it, ε means nothing at all utility floor worst acceptable distortion, at a stated percentile sets the lower bound on ε owned by the consumer without it, the choice defaults to eyeballing release schedule how often, under which composition rule sets the upper bound on ε owned by the product owner the first release looks fine; the twentieth does not a feasible interval for ε and, when it is empty, which of four things must give An empty interval is the most useful output: it says the release as specified is impossible, and names the four conversations available — tighten the sensitivity, relax the tolerance, publish less often, raise the budget.
Three inputs convert epsilon from a number with no scale into a bounded interval — and two of the three come from outside the privacy team.

Prerequisite Check: Fix the Sensitivity First

python
import math


def coordinate_sensitivity(contract: dict) -> float:
    """How far one individual can move the output, in metres.

    For a released point set this is bounded by the clamp: an individual can move
    their own coordinate anywhere within the declared envelope, so the sensitivity
    is the envelope diagonal unless the pipeline bounds it more tightly.
    """
    w, h = contract["envelope_size_m"]
    return math.hypot(w, h)


def bounded_sensitivity(contract: dict) -> float:
    """The tighter bound most pipelines can actually claim.

    Clamping each contribution to a radius R around its own aggregation cell caps the
    sensitivity at 2R regardless of the envelope, which is usually orders of magnitude
    smaller — and it is the single most effective thing you can do before choosing ε.
    """
    return 2 * contract["contribution_clamp_radius_m"]

The gap between those two functions is the first thing worth measuring, and it is usually enormous: an envelope diagonal of tens of kilometres against a contribution clamp of a few hundred metres. Since the noise scales with sensitivity, tightening it buys utility at no cost to the guarantee — which makes it strictly better than raising epsilon, and it should be exhausted first.

Fix: Bound Epsilon From Both Ends, Then Choose Inside the Interval

1 — Work backwards from the utility floor for a lower bound

python
def epsilon_floor(sensitivity_m: float, max_acceptable_error_m: float,
                  percentile: float = 0.95) -> float:
    """The smallest ε whose error stays under the consumer's tolerance at a given percentile.

    For the Laplace mechanism the radial error at percentile p is approximately
    -(Δ/ε)·ln(1-p), so inverting gives the ε that keeps the p-th percentile within
    the tolerance.
    """
    return -sensitivity_m * math.log(1 - percentile) / max_acceptable_error_m

The percentile matters more than the formula. Choosing the median gives an epsilon under which half the consumer’s data is worse than they asked for; choosing the ninety-fifth gives one under which one point in twenty is. Ask the consumer which they meant, because they will have a view and it is usually the second.

2 — Work forwards from the schedule for a ceiling

python
def epsilon_ceiling(annual_budget: float, releases_per_year: int,
                    composition: str = "basic") -> float:
    """The largest per-release ε that keeps the composed total under the annual budget."""
    if composition == "basic":
        return annual_budget / releases_per_year
    delta_prime = 1e-6
    lo, hi = 1e-4, annual_budget
    for _ in range(60):                       # invert the advanced-composition bound
        mid = (lo + hi) / 2
        k = releases_per_year
        composed = (math.sqrt(2 * k * math.log(1 / delta_prime)) * mid
                    + k * mid * (math.exp(mid) - 1))
        lo, hi = (mid, hi) if composed < annual_budget else (lo, mid)
    return lo

Two things are worth noticing. The ceiling depends on the composition rule, so the accounting decision has to be made before the parameter can be chosen. And the ceiling for a weekly schedule is far tighter than most first-time choosers expect — an annual budget of 3 over 52 releases is a per-release epsilon under 0.06 under plain addition.

3 — Check that the interval is non-empty, and act on it when it is not

python
def choose(sensitivity_m, max_error_m, annual_budget, releases_per_year) -> dict:
    lo = epsilon_floor(sensitivity_m, max_error_m)
    hi = epsilon_ceiling(annual_budget, releases_per_year)
    return {
        "sensitivity_m": sensitivity_m,
        "epsilon_floor": lo,
        "epsilon_ceiling": hi,
        "feasible": lo <= hi,
        "chosen": min(max(lo, lo), hi) if lo <= hi else None,
    }

An empty interval is the most useful output this produces, because it means the release as specified is impossible and it says which of four things has to give: tighten the sensitivity, relax the utility floor, publish less often, or raise the annual budget. Those are four different conversations with four different people, and knowing which one to have is most of the work.

The feasible epsilon interval against release cadence Release cadences run along the horizontal axis from annual to daily, on an evenly spaced grid. The vertical axis is epsilon on a logarithmic scale. A flat dashed line marks the floor: the smallest epsilon whose ninety-fifth-percentile displacement stays inside the consumer's stated tolerance, given the sensitivity. It does not depend on the cadence, so it does not move. A falling curve marks the ceiling: the largest per-release epsilon whose composed total over a year stays inside the annual budget under plain addition. The shaded region between them is the feasible interval, and it narrows steadily as the cadence rises. A marker shows where the two cross, which is the cadence at which no epsilon satisfies both — and it falls between monthly and fortnightly publication, which is a cadence teams routinely choose on operational grounds without checking whether it is affordable. The note underneath records the four levers available once the interval has closed, and observes that three of them are outside the privacy team's control. The interval closes at a cadence chosen for operational reasons 0.01 0.032 0.1 0.316 1 3.162 ε (log scale) annual 6-monthly quarterly monthly fortnightly weekly twice weekly daily feasible floor from the utility tolerance: ε ≥ 0.120 no feasible ε from 26 releases a year ceiling from the schedule (annual budget 3) floor from the utility tolerance A per-cell count release: sensitivity 1 count, consumer tolerance 25 counts at p95, annual budget 3, basic composition. Once the interval closes, four levers remain — tighten the sensitivity, relax the tolerance, publish less often, raise the budget — and three of them belong to somebody else.
The feasible interval computed across a range of release cadences: the ceiling falls with frequency while the floor does not move, and the interval closes at a cadence most teams choose without checking.

Verification Step: Record the Derivation, Not Just the Number

python
def epsilon_record(choice: dict, contract: dict) -> dict:
    return {
        "epsilon": choice["chosen"],
        "delta": contract.get("delta"),
        "sensitivity_m": choice["sensitivity_m"],
        "sensitivity_source": "contribution clamp radius × 2",
        "utility_floor": {"max_error_m": contract["max_acceptable_error_m"],
                          "percentile": 0.95,
                          "agreed_with": contract["consumer_contact"]},
        "schedule": {"releases_per_year": contract["releases_per_year"],
                     "annual_budget": contract["annual_budget"],
                     "composition": contract["composition"]},
        "feasible_interval": [choice["epsilon_floor"], choice["epsilon_ceiling"]],
    }

A manifest carrying only the epsilon is a manifest whose number cannot be revisited: a year later nobody knows what it was derived from, so nobody can tell whether a changed schedule or a changed tolerance invalidates it. Recording the derivation makes the number auditable and, more usefully, makes it recomputable when one of its inputs moves.

python
def test_epsilon_matches_its_derivation(manifest):
    rec = manifest["privacy"]
    recomputed = choose(rec["sensitivity_m"],
                        rec["utility_floor"]["max_error_m"],
                        rec["schedule"]["annual_budget"],
                        rec["schedule"]["releases_per_year"])
    assert abs(recomputed["chosen"] - rec["epsilon"]) < 1e-9, (recomputed, rec)

Edge Cases & Gotchas

Nobody will give you a utility floor. Common, and the way through it is to invert the question: generate at three epsilons, show the consumer the measured displacement distribution for each, and ask which is unacceptable. People who cannot state a tolerance in the abstract answer immediately when shown the data.

Displacement under two ways of reducing noise: raising epsilon versus tightening sensitivity Five bars give the ninety-fifth-percentile displacement in metres for the same release under five options. The baseline sits well above the consumer's tolerance, marked as a dashed line. Doubling epsilon halves the displacement and doubling it again halves it once more, and after both the release is still outside tolerance — at the cost of a guarantee four times weaker. Tightening the contribution clamp from the envelope diagonal to four hundred metres achieves a comparable reduction on its own; tightening it to one hundred metres brings the release comfortably inside tolerance. Both clamp options leave epsilon exactly where it started. Each bar is labelled with the epsilon it uses, so the two axes of the trade are visible together. The conclusion drawn underneath is the ordering rule: exhaust the sensitivity before touching the budget, because tightening it buys utility at no cost to the guarantee, and every metre of noise removed that way is a metre that does not have to be paid for in epsilon. Tighten the sensitivity before touching the budget 0 2,500 5,000 7,500 10,000 95th-percentile displacement (m) 8,039 baseline ε = 0.5 4,153 raise ε to 1.0 ε = 1 2,047 raise ε to 2.0 ε = 2 4,942 clamp to 400 m ε = 0.5 1,209 clamp to 100 m ε = 0.5 consumer tolerance 250 m spends guarantee spends nothing 4,000 draws per option, planar Laplace, fixed seed. Every metre of noise removed by tightening the clamp is a metre that does not have to be paid for in ε — which makes the ordering a rule rather than a preference.
Measured on the same release: tightening the clamp beats doubling ε twice, and it spends nothing.

The sensitivity is unbounded. If an individual can contribute an unlimited number of records, no epsilon bounds anything. Cap contributions per individual before anything else — it is a precondition for the guarantee rather than a tuning decision.

The first release is a one-off that later becomes a series. Extremely common, and the reason to compute the ceiling even when releases_per_year is one. Record the assumption explicitly, so that when somebody proposes a second release the constraint surfaces rather than being discovered after publication.

Choosing the floor exactly. The interval’s lower end is the smallest epsilon meeting the utility requirement, which means the most private feasible choice — and it is the right default. Choosing higher inside the interval buys utility nobody asked for at a cost in guarantee that somebody will eventually have to defend.

What to Do With the Number Once It Exists

Choosing epsilon is not the end of the task, and three things follow immediately that are easy to leave undone.

Publish the measured consequence, not the parameter. A release note saying “ε = 0.12” tells a consumer nothing they can act on. One saying “ε = 0.12; ninety-fifth-percentile per-cell error 24 counts; measured over the actual release” tells them exactly what they are getting. The epsilon is the guarantee and the measurement is the utility, and consumers need both.

Set a review trigger, not a review date. The number depends on three inputs, and it should be recomputed when any of them moves rather than on a calendar. A change to the contribution clamp, a change to the consumer’s tolerance, or a change to the release cadence should each fail a check that compares the recorded derivation against the current inputs — which is exactly what the derivation record makes possible.

Run the adversarial check at the chosen value. The guarantee is a worst-case bound; what your generator, your data and your schedule actually produce is an empirical question, and it is answered by running a membership-inference attack at the chosen epsilon. A measured AUC near chance is evidence a reviewer can check; an epsilon on a manifest is a promise they have to take on trust.