Choosing Epsilon for Transit Stop Data

Publishing boarding and alighting counts per transit stop under differential privacy for location data means picking an ε whose Laplace noise is negligible at busy hubs yet still meaningful at quiet stops — and pairing it with a small-count suppression threshold so low-ridership stops never leak a re-identifiable count.

Core Calculation permalink

A per-stop count is a counting query: adding or removing one rider changes any single stop’s total by at most one, so the L1 sensitivity is:

Δf=1\Delta f = 1

The Laplace mechanism therefore draws noise at scale:

b=Δfε=1εb = \frac{\Delta f}{\varepsilon} = \frac{1}{\varepsilon}

The expected absolute error at every stop is b = 1/ε, independent of how busy the stop is. That fixed error is what makes stop choice matter: the same noise is trivial against a large count and catastrophic against a small one.

Signal-to-noise at a stop permalink

For a stop with expected count λ, the relative error introduced by the mechanism is the noise magnitude over the signal:

SNR=λb=λε,relative error1λε\text{SNR} = \frac{\lambda}{b} = \lambda\,\varepsilon, \qquad \text{relative error} \approx \frac{1}{\lambda\,\varepsilon}

To hold relative error at or below a target r, a stop needs:

λ1rε\lambda \geq \frac{1}{r\,\varepsilon}

Suppression threshold permalink

Stops below that λ cannot meet the utility target and are the re-identification risk, so define a suppression threshold τ and withhold any stop whose expected count falls under it:

τ=1rε\tau = \left\lceil \frac{1}{r\,\varepsilon} \right\rceil
Parameter Symbol Typical transit value
Sensitivity Δf 1 (single count)
Privacy budget ε 0.5 – 2.0 per release
Noise scale b = 1/ε 0.5 – 2.0 riders
Target relative error r 0.05 – 0.15
Suppression threshold τ ⌈1/(rε)⌉

Worked example: busy hub vs. quiet suburban stop permalink

Take ε = 1.0, so b = 1/ε = 1 rider of expected error, and a target relative error of r = 0.10 (10%).

  • Busy hub, λ = 4000 boardings/day. Relative error ≈ 1/(4000 × 1.0) = 0.00025 — 0.025%. Effectively lossless for service planning.
  • Quiet suburban stop, λ = 6 boardings/day. Relative error ≈ 1/(6 × 1.0) = 0.167 — 17%, above target. The noisy value swings by whole riders, and a true count of 6 is easy to attribute.

The threshold is τ = ⌈1/(0.10 × 1.0)⌉ = 10, so the suburban stop (λ = 6 < 10) is suppressed while the hub is published. A rider who knows they were one of a few people at that stop cannot confirm it from a suppressed release — the same re-identification risk assessment for geospatial datasets logic that governs sparse spatial cells.

The relationship is worth internalizing: because the noise floor b = 1/ε is constant while ridership varies by three orders of magnitude across a network, a single ε produces relative errors that span from imperceptible at hubs to overwhelming at the tail. The threshold τ is simply the ridership at which those two regimes meet.

Suppression threshold against stop ridership The fixed Laplace noise band of one over epsilon is negligible relative to busy hub counts but large relative to low-ridership stops; stops below the threshold tau are suppressed, stops above are published. expected ridership per stop (lambda) count true count noise band +/- b = 1/epsilon tau suppress publish
The noise band is a fixed width while true counts scale with ridership; stops left of the threshold tau are suppressed because the band swamps the signal.

Choosing epsilon for the whole network permalink

A single ε governs the entire release, so pick it for the stops you must publish, then let suppression absorb the rest. Anchor ε to the busiest tier where planners need precision — sizing frequency, vehicle capacity, and stop infrastructure all depend on hub counts, and those tolerate ε in the 1.0–2.0 range with sub-percent error. Lowering ε toward 0.5 roughly doubles the noise and raises the threshold τ, suppressing more of the low-ridership tail; raising it toward 2.0 publishes more stops but weakens the guarantee at each. There is no ε that publishes a ten-rider stop usefully and safely at once, so resist the temptation to tune ε for the tail — that is the threshold’s job, not the budget’s.

Python Implementation permalink

The function recommends a suppression threshold from a target relative error, adds Laplace noise to each stop count in one pass, and flags the stops that must be withheld. Stop geometries are assumed pre-joined in WGS84 (EPSG:4326); only the counts are processed here.

from __future__ import annotations
import math
import numpy as np
import pandas as pd


def privatize_transit_counts(
    stops: pd.DataFrame,
    epsilon: float,
    target_relative_error: float = 0.10,
    count_col: str = "boardings",
    seed: int | None = None,
) -> pd.DataFrame:
    """Add Laplace noise to per-stop boarding counts and flag low-ridership
    stops for suppression.

    Privacy model: each stop count is a counting query with L1 sensitivity 1
    (one rider changes one stop's count by at most 1), so the Laplace scale is
    b = 1 / epsilon and the release satisfies epsilon-differential privacy
    across the stop set treated as disjoint partitions (one rider per stop).

    Args:
        stops: One row per stop; must contain `count_col` (true counts) and a
            stop identifier. Coordinates, if present, are WGS84 (EPSG:4326).
        epsilon: Privacy budget > 0. Smaller -> more noise, stronger privacy.
        target_relative_error: Acceptable noise-to-signal ratio at published
            stops; drives the suppression threshold.
        count_col: Column holding true integer counts.
        seed: Optional RNG seed for reproducible releases.

    Returns:
        Copy of `stops` with `noisy_count` and `suppress` columns added.
    """
    if epsilon <= 0:
        raise ValueError(f"epsilon must be > 0, got {epsilon!r}")
    if not 0 < target_relative_error < 1:
        raise ValueError("target_relative_error must be in (0, 1)")

    rng = np.random.default_rng(seed)
    scale = 1.0 / epsilon                       # Laplace b = sensitivity / epsilon

    # Suppression threshold: smallest lambda meeting the relative-error target.
    threshold = math.ceil(1.0 / (target_relative_error * epsilon))

    out = stops.copy()
    true_counts = out[count_col].to_numpy(dtype=float)

    # Independent Laplace draw per stop; counts are non-negative so clamp at 0.
    noise = rng.laplace(loc=0.0, scale=scale, size=len(out))
    out["noisy_count"] = np.maximum(0.0, true_counts + noise).round().astype(int)

    # Flag stops whose TRUE expected count is below the threshold. Suppression
    # decisions must not depend on the noisy value, or they would leak signal.
    out["suppress"] = out[count_col] < threshold
    out.attrs["epsilon"] = epsilon
    out.attrs["suppression_threshold"] = threshold
    return out

Verification permalink

The check confirms the fraction of published stops meeting the relative-error target and that every suppressed stop is genuinely low-ridership.

rng = np.random.default_rng(0)
# Mixed network: a few hubs, many quiet stops (WGS84 stop points elsewhere).
stops = pd.DataFrame({
    "stop_id": range(500),
    "boardings": np.concatenate([
        rng.integers(800, 5000, size=50),   # hubs
        rng.integers(1, 40, size=450),      # suburban / low ridership
    ]),
})

result = privatize_transit_counts(stops, epsilon=1.0, target_relative_error=0.10, seed=7)
threshold = result.attrs["suppression_threshold"]

published = result[~result["suppress"]]
rel_err = (published["noisy_count"] - published["boardings"]).abs() / published["boardings"]
frac_ok = (rel_err <= 0.10).mean()

# Every published stop must clear the threshold; most must meet the error target.
assert (published["boardings"] >= threshold).all()
assert frac_ok >= 0.90, f"only {frac_ok:.1%} of published stops met the target"
print(f"threshold={threshold}, published={len(published)}, "
      f"fraction within 10% error={frac_ok:.1%}")

Edge Cases & Adjustments permalink

  • Very low ridership. Stops with λ of a handful of riders can never satisfy both utility and privacy under a fixed ε; suppression is the correct answer, not a smaller ε. Report the count of suppressed stops as metadata so planners know coverage is incomplete.

  • Peak vs. off-peak. Publishing hourly counts multiplies the query load: each time bucket is a separate release. Disjoint time buckets over the same population still compose sequentially because a rider appears in many hours, so split the budget across buckets or track cumulative ε per the parent guide on privacy budget allocation for spatial queries.

  • Cross-stop correlation. If a single rider is counted at both a boarding and an alighting stop in the same release, sensitivity rises above 1 to the maximum number of stops one person touches; scale b to that value or the guarantee weakens.

  • Sequential linkage. Even suppressed and noised, stop-level releases combined with timetables can enable trajectory reconstruction — pair this control with the defenses in preventing spatial linkage attacks in public transit data.

FAQ permalink

What sensitivity should I use for transit boarding counts?

For a simple count of boardings or alightings, the L1 sensitivity is 1 — adding or removing a single rider changes any one stop’s count by at most one. If a rider can be counted at several stops in the same release (boarding plus alighting, or multiple trips), the sensitivity equals the maximum number of stops any individual contributes to, and ε must be calibrated to that larger value.

Why suppress low-ridership stops instead of just adding noise?

At a stop with an expected count of two or three, noise calibrated for a busy hub is large relative to the true value, so the published number is useless to planners and unsafe: a small true count is easy to attribute to a specific rider. Suppressing counts below the threshold removes exactly the stops where noise cannot deliver utility and protection at once.

How does epsilon trade off against planner utility?

The Laplace scale is 1/ε, so halving ε doubles expected error. Planners sizing service for a hub with thousands of boardings tolerate a small ε because relative error is negligible, while the same ε destroys a quiet suburban stop. Choose ε from the relative error you can accept at the stops you actually need to publish, then let the threshold suppress the rest.

Should the suppression decision use the noisy or the true count?

Base suppression on the true expected count, not the noisy release. If the withhold decision depended on the noisy value, the pattern of which stops are suppressed would itself leak information about the underlying counts, undermining the guarantee the noise provides.


Related

Back to Privacy Budget Allocation for Spatial Queries