Choosing Temporal Cloaking Windows for Transit Feeds

The window has a floor and a ceiling, and both are measurable. The floor is the service headway, because a window narrower than one headway leaves each record matched to a single scheduled departure. The ceiling is the shortest analytical cycle the feed must preserve — usually the morning peak — because a window wider than that erases the pattern the feed exists to show.

Core Calculation permalink

Let HH be the headway on the route and Δt\Delta t the generalization window. An adversary holding the published timetable maps a released timestamp to the set of departures consistent with it. The size of that set is

m(Δt)=ΔtHm(\Delta t) = \left\lceil \frac{\Delta t}{H} \right\rceil

so the timetable-matching anonymity is log2m\log_2 m bits, and Δt<H\Delta t < H gives m=1m = 1 — no protection at all against the one auxiliary dataset that is guaranteed to be public.

The ceiling comes from the analysis. If the feed must resolve a cycle of period TT — a 2-hour morning peak, a 20-minute service disruption — the Nyquist condition on binned data requires

ΔtT2\Delta t \le \frac{T}{2}

and in practice T/4T/4 is where the binned profile still looks like the underlying curve rather than a staircase. So the admissible band is

H  Δt  T4H \ \le\ \Delta t \ \le\ \frac{T}{4}

and when that band is empty — a 20-minute headway on a route whose peak you must resolve to 30 minutes — the honest conclusion is that this feed cannot be published at stop-and-time granularity, not that one of the two constraints should be quietly bent.

Worked numeric example permalink

A city feed with three route classes, required to preserve the morning peak (T=120T = 120 min):

Route class Headway HH Floor Ceiling T/4T/4 Chosen Δt\Delta t Timetable anonymity
Trunk metro 3 min 3 min 30 min 15 min log25\log_2 5 = 2.3 bits
Urban bus 8 min 8 min 30 min 15 min log22\log_2 2 = 1.0 bit
Suburban bus 30 min 30 min 30 min 30 min log21\log_2 1 = 0 bits
Night service 60 min 60 min 30 min band empty

Two results are worth reading carefully. The suburban route sits exactly at the boundary: a 30-minute window gives one candidate departure, so the timetable resolves it completely and the cloaking has done nothing on that axis. And the night service has no admissible window at all — its headway exceeds the analytical ceiling — so it must either be suppressed, aggregated to a coarser geography, or published without the peak-resolution requirement.

Python Implementation permalink

from __future__ import annotations

import math
import pandas as pd


def window_band(
    routes: pd.DataFrame,
    analytical_cycle_min: float,
    resolution_factor: float = 4.0,
) -> pd.DataFrame:
    """Admissible cloaking window per route class, with the timetable anonymity.

    The floor is one headway: below it, the published timetable maps each released
    timestamp to exactly one departure, so the cloaking provides nothing against
    the one auxiliary dataset that is certain to be public.

    Args:
        routes: columns `route_class`, `headway_min`.
        analytical_cycle_min: shortest cycle the feed must resolve (e.g. 120 for
            a two-hour morning peak).
        resolution_factor: divisor on the cycle; 2 is the Nyquist limit and 4 is
            where a binned profile still reads as a curve.
    """
    ceiling = analytical_cycle_min / resolution_factor
    rows = []
    for _, r in routes.iterrows():
        floor = float(r["headway_min"])
        feasible = floor <= ceiling
        chosen = min(max(floor, 15.0), ceiling) if feasible else None
        rows.append({
            "route_class": r["route_class"],
            "headway_min": floor,
            "floor_min": floor,
            "ceiling_min": ceiling,
            "feasible": feasible,
            "chosen_window_min": chosen,
            "timetable_candidates": (math.ceil(chosen / floor) if chosen else 0),
            "timetable_bits": (round(math.log2(math.ceil(chosen / floor)), 2)
                               if chosen and math.ceil(chosen / floor) > 1 else 0.0),
        })
    return pd.DataFrame(rows)

Verification permalink

Two checks, and the second is the one teams skip.

import numpy as np


def peak_profile_fidelity(raw_times_min: np.ndarray, window_min: float,
                          day_min: int = 1440) -> dict:
    """Correlation between the raw hourly profile and the cloaked one.

    A window that destroys the peak has done its privacy job and broken the feed;
    this is the number that says which side of the ceiling you landed on.
    """
    raw, _ = np.histogram(raw_times_min, bins=np.arange(0, day_min + 60, 60))
    binned = (np.floor(raw_times_min / window_min) * window_min)
    cloaked, _ = np.histogram(binned, bins=np.arange(0, day_min + 60, 60))
    r = float(np.corrcoef(raw, cloaked)[0, 1])
    peak_hour_raw = int(raw.argmax())
    return {
        "profile_correlation": r,
        "peak_hour_preserved": bool(int(cloaked.argmax()) == peak_hour_raw),
        "peak_amplitude_ratio": float(cloaked.max() / max(raw.max(), 1)),
    }

peak_hour_preserved is a blunt instrument and a good gate: if the cloaked feed puts the morning peak in a different hour from the raw one, the window is past the ceiling regardless of what the correlation says.

The second check is the timetable-matching simulation. Take the published schedule, join it against the released timestamps, and count how many records match exactly one departure. That figure is the real timetable anonymity, and it is usually worse than Δt/H\lceil \Delta t / H \rceil suggests because real services bunch — three buses arriving together leave a 24-minute gap in which any released timestamp resolves uniquely.

Edge Cases & Adjustments permalink

  • Bunched services. Real headways are not uniform. A nominal 8-minute route that runs three-together-then-a-gap has an effective headway of 24 minutes in the gap, and records landing there resolve uniquely. Compute the floor from the observed 90th-percentile gap, not the scheduled headway.
  • First and last services. These are unique by definition — one departure, no neighbours — so no window protects them. Suppress them or widen the window for those bands specifically, as under preventing spatial linkage attacks in public transit data.
  • Window alignment. A window aligned to the hour and a timetable aligned to the hour interact badly: departures at :00 and :30 land at bin edges, and a record’s bin then identifies which side of the edge it fell. Offset the window origin from the timetable’s, and version the offset.
  • Multiple analytical cycles. If the feed must serve both a peak-hour model and a disruption-detection model, the ceiling is set by the shorter cycle. Publish two feeds at two windows rather than one compromise that serves neither.
  • Interaction with spatial cloaking. The k floor is evaluated on the joint space-time cell, so widening Δt\Delta t lets you narrow the spatial cell at constant k. Sweep the two together rather than fixing one and tuning the other.

FAQ permalink

Why is the headway the floor rather than something derived from k?

Because the timetable is public and exact. Every other auxiliary dataset an adversary might hold is uncertain and partial; the schedule is neither. A window that does not span at least one headway leaves the strongest available auxiliary join fully effective no matter how large the k floor is.

Can I use jitter instead of a window?

Jitter and generalization protect against different things. A window bounds the precision of the released time; jitter breaks the exact-match join while leaving precision roughly intact. Against a timetable adversary the window is what matters, because jitter of ±3 minutes on an 8-minute headway still resolves to one or two departures. Use both, and size the window against the headway.

What if the analytical requirement is not documented?

Then it is not a requirement yet, and this is a good moment to write it down. “Preserve the morning peak” is testable; “keep the data useful” is not, and a window chosen against the second is a window nobody can defend when someone later complains the feed lost something.

Does a wider window help the k floor?

Yes — it widens the space-time cell, so more distinct riders fall inside it and more cells clear the floor. That is the main reason to sit above the headway rather than exactly on it, and it is why the middle of the band is usually the right choice rather than the floor.

← Back to Temporal Cloaking & Time Obfuscation