Sizing a Deduplication Window from Report Intervals
Measure the inter-arrival distribution per device class and take a low percentile of it — the vendor’s nominal interval describes one mode of a multi-modal distribution, and a window sized from the mean is dragged above every real interval by a handful of multi-hour outage gaps.
This guide sits under Time-Windowed Deduplication for Moving Assets, within Idempotency & Spatial Deduplication. It produces the MIN_SECONDS threshold used by Deduplicating Vehicle Pings in a Sliding Window.
When to use this pattern
- The window was chosen from a configuration document, a vendor datasheet or a round number, and nobody has checked it against traffic.
- The fleet is heterogeneous — different tracker models, different firmware, different power profiles.
- The suppression rate is either surprisingly low, which means the window is doing nothing, or surprisingly high, which means it is discarding data someone downstream is waiting for.
The distribution is not a number
Complete runnable implementation
import collections
import statistics
from dataclasses import dataclass
# The window sits above the gaps to collapse and below the gaps to keep.
# A low percentile puts it there; the median puts it in the middle of the
# idle cadence, suppressing about half of all normal reporting.
WINDOW_PERCENTILE = 10
# Reconnection bursts are not reporting cadence and must not shape the window.
BURST_FLOOR_SECONDS = 0.4
# Gaps above this are outages, and they are what drags the mean off the chart.
OUTAGE_CEILING_SECONDS = 600.0
@dataclass(frozen=True, slots=True)
class WindowRecommendation:
device_class: str
samples: int
p10: float
median: float
mean: float
window_seconds: float
suppression_estimate: float
def inter_arrival_gaps(pings: list[tuple[str, float]]) -> dict[str, list[float]]:
"""Gaps per asset, then pooled per device class.
Sorting per asset matters: pooling raw timestamps across a fleet produces
gaps between different vehicles, which is not an interval at all.
"""
by_asset: dict[str, list[float]] = collections.defaultdict(list)
for asset_id, epoch in pings:
by_asset[asset_id].append(epoch)
gaps: list[float] = []
for timestamps in by_asset.values():
timestamps.sort()
gaps += [b - a for a, b in zip(timestamps, timestamps[1:])]
return gaps
def recommend(device_class: str, gaps: list[float]) -> WindowRecommendation:
"""Derive a window for one device class from its own gaps."""
usable = [g for g in gaps
if BURST_FLOOR_SECONDS <= g <= OUTAGE_CEILING_SECONDS]
if len(usable) < 1000:
raise ValueError(
f"{device_class}: {len(usable)} usable gaps — too few to size a "
"window; a recommendation from a small sample is a guess with a "
"decimal point on it"
)
usable.sort()
quantiles = statistics.quantiles(usable, n=100, method="inclusive")
p10 = quantiles[WINDOW_PERCENTILE - 1]
# What fraction of real reports this window would suppress. This is the
# number to take to the consumer, because it is the one they feel.
suppressed = sum(1 for g in usable if g < p10) / len(usable)
return WindowRecommendation(
device_class=device_class,
samples=len(usable),
p10=p10,
median=statistics.median(usable),
mean=statistics.fmean(usable),
window_seconds=round(p10, 1),
suppression_estimate=round(suppressed, 4),
)
def window_for(device_class: str, table: dict[str, float],
default: float = 60.0) -> float:
"""Look the window up per event; do not configure one number per fleet."""
return table.get(device_class, default)
The ValueError on a small sample is not defensive padding. A window derived from two hundred gaps looks exactly like one derived from two million, and the difference only shows up as an unexplained change in suppression rate weeks later.
Parameter reference
| Name | Type | Spatial constraint | Default |
|---|---|---|---|
WINDOW_PERCENTILE |
int |
Low — above the burst mode, below the idle cadence | 10 |
BURST_FLOOR_SECONDS |
float |
Excludes reconnection floods, which are not cadence | 0.4 |
OUTAGE_CEILING_SECONDS |
float |
Excludes tunnels and garages; these are what wreck the mean | 600.0 |
| Minimum sample | int |
Below this, raise rather than recommend | 1000 |
| Class table | dict[str, float] |
One entry per device class differing by more than ~2× | — |
| Re-measurement | schedule | Quarterly, and after any firmware rollout | — |
Gotchas and spatial edge cases
-
Pool gaps per asset, never across the fleet. Sorting every timestamp in the stream and differencing produces gaps between different vehicles, which for a large fleet are milliseconds apart and would recommend a window of essentially zero. The bug is easy to write and the output looks plausible.
-
A firmware rollout changes the distribution overnight. Devices that gain a motion trigger start producing a whole new mode, and the window sized last quarter now sits in the middle of it. Alert on a shift in the tenth percentile rather than waiting for someone to notice the suppression rate moved.
-
The suppression estimate is what the consumer cares about, not the window. Take “this discards 38% of reports for this class” to the team consuming the stream, because that is the number they can evaluate. A window in seconds means nothing to someone computing dwell time.
-
Devices in a depot dominate the sample. Parked vehicles report indefinitely and moving ones do not, so a naive sample is mostly stationary reporting. If the window is meant to govern moving assets, filter the sample by displacement first — which links this measurement to the distance threshold in the sliding window.
-
A class with too few devices cannot be sized, only defaulted. Rather than computing a confident-looking number from three hundred gaps, fall back to the fleet default and record that the class is unsized. The
ValueErrorabove is what makes that visible. -
The window is not the TTL. They are independent numbers and conflating them is the failure described in Expiring Deduplication Keys Without Losing Late Retries — this measurement sizes only the first.
Verification
import random
import pytest
def synthetic_gaps(n: int = 50_000) -> list[float]:
"""Four modes, matching a real tracker: burst, idle, power-save, outage."""
gaps = []
gaps += [random.gauss(1.6, 0.4) for _ in range(int(n * 0.30))]
gaps += [random.gauss(10.0, 1.2) for _ in range(int(n * 0.45))]
gaps += [random.gauss(30.0, 4.0) for _ in range(int(n * 0.20))]
gaps += [random.uniform(600, 7200) for _ in range(int(n * 0.05))]
return [g for g in gaps if g > 0]
def test_window_sits_between_the_burst_and_idle_modes():
rec = recommend("tracker-v3", synthetic_gaps())
assert 0.8 < rec.window_seconds < 8.0
def test_mean_is_outside_every_mode():
"""The reason the mean must not be used."""
rec = recommend("tracker-v3", synthetic_gaps())
assert rec.mean > 40.0 # above idle AND power-save
assert rec.window_seconds < rec.median
def test_small_sample_raises_rather_than_guessing():
with pytest.raises(ValueError, match="too few"):
recommend("new-model", [10.0] * 50)
def test_gaps_are_computed_per_asset():
"""Two vehicles reporting alternately must not produce tiny gaps."""
pings = []
for i in range(100):
pings.append(("veh-a", i * 10.0))
pings.append(("veh-b", i * 10.0 + 0.05)) # interleaved, 50 ms apart
gaps = inter_arrival_gaps(pings)
assert min(gaps) > 5.0, "gaps were pooled across assets"
The last test is the one that catches the most common implementation mistake, and it fails loudly: a fleet-pooled computation on that input returns gaps of fifty milliseconds and would recommend a window that suppresses nothing at all.
Related
- Time-Windowed Deduplication for Moving Assets — the topic this guide belongs to
- Deduplicating Vehicle Pings in a Sliding Window — where the measured window becomes a threshold
- Expiring Deduplication Keys Without Losing Late Retries — the other number, which this measurement does not produce
- Choosing an H3 Resolution from Measured Traffic — the same measure-then-choose method applied to partitioning