Handling Privacy Thresholds in Visit Aggregations
This page solves one exact task: enforcing minimum-count suppression on visit aggregations so that no published figure — cell, margin or derived total — can be used to infer something about a small group of people, and so that suppressed values never reappear as zeros downstream.
Vendors apply their own thresholds before data reaches you, and that protection ends the moment your pipeline starts combining, differencing and re-aggregating their tables. Suppression is not a property of a file; it is a property of everything computed from it, and preserving it is a design requirement rather than a compliance checkbox.
Prerequisites
- Vendor aggregates with a stated suppression threshold and a way to distinguish suppressed cells from genuine zeros.
- The contractual limits on publication geography and retention, which are usually stricter than the technical threshold.
- Python packages:
pandasandnumpy. Install withpip install pandas numpy. - The parent context. Mobility and foot traffic data integration covers where these tables sit and how they are used.
Configuration and execution parameters
| Parameter | Value for this task | Notes |
|---|---|---|
min_cell |
5 visitors, or the contract’s figure | Whichever is higher |
suppress_margins |
true | Row and column totals as well as cells |
complementary_suppression |
true | Suppress a second cell so the first cannot be derived |
null_semantics |
withheld ≠ zero | Two distinct states, never merged |
export_check |
mandatory | A gate on the publication path, not a convention |
aggregate_rule |
suppress if any input suppressed | Or report a bounded range |
retention |
per contract | Enforced by storage lifecycle |
complementary_suppression is the rule that is most often missed and most necessary. Suppressing one cell in a row whose total is published leaves that cell computable by subtraction; suppressing a second cell in the same row is what actually protects the first.
Annotated implementation
The suppressor operates on a table and returns a table with two states — a value or a withheld marker — and it refuses to produce an output where a withheld value is recoverable.
from __future__ import annotations
import numpy as np
import pandas as pd
MIN_CELL = 5
def suppress(table: pd.DataFrame, value: str = "visitors",
by: str = "store_id", across: str = "home_zone",
min_cell: int = MIN_CELL) -> pd.DataFrame:
"""Primary and complementary suppression, per group."""
out = table.copy()
out["withheld"] = out[value] < min_cell
out.loc[out["withheld"], value] = np.nan
for _, idx in out.groupby(by).groups.items():
group = out.loc[idx]
if group["withheld"].sum() != 1:
continue # zero suppressed cells is fine; two or more already protect
# Exactly one suppressed cell is recoverable from the published total,
# so suppress the smallest remaining cell as well.
publishable = group.loc[~group["withheld"], value]
if publishable.empty:
continue
victim = publishable.idxmin()
out.loc[victim, "withheld"] = True
out.loc[victim, value] = np.nan
return out
def safe_aggregate(table: pd.DataFrame, value: str = "visitors",
by: str | list[str] = "store_id") -> pd.DataFrame:
"""Aggregate without laundering suppressed cells into a total.
A sum over a group containing withheld values is a LOWER BOUND, and it must
be labelled as one — silently summing the non-null cells is exactly how a
suppressed figure becomes a published number."""
grouped = table.groupby(by)
return pd.DataFrame({
"value": grouped[value].sum(min_count=1),
"withheld_cells": grouped["withheld"].sum(),
"is_lower_bound": grouped["withheld"].any(),
}).reset_index()
def export_gate(frame: pd.DataFrame, value: str = "visitors",
min_cell: int = MIN_CELL) -> pd.DataFrame:
"""Last line of defence on the publication path."""
below = frame[value].dropna() < min_cell
if below.any():
raise ValueError(f"{int(below.sum())} cell(s) below the threshold in an export")
return frame
safe_aggregate carries the property that matters most downstream. A total computed over a group containing withheld cells is a lower bound rather than a total, and labelling it as such is what stops a suppressed figure being reconstructed two joins later by someone who had no idea it was ever suppressed.
Failure modes and debugging
Zero-filling on load. A single fillna(0) anywhere in the pipeline converts every withheld value into a published claim that no visits occurred. It is one line, it looks like tidiness, and it undoes the entire protection while also biasing every trade area toward dense zones.
Suppression applied at read and lost at write. A pipeline that suppresses when loading and then aggregates freely can publish a total from which individual cells are derivable. The gate belongs on the export path as well as the ingest path, and it should raise rather than warn.
Differencing two published tables. Two monthly tables, each correctly suppressed, can reveal a suppressed cell when subtracted — a change of exactly three visitors in a zone where one month was withheld. Where period-on-period differences are published, they need their own suppression pass rather than inheriting the inputs’.
Thresholds applied inconsistently across geographies. Suppressing at block-group level and publishing an unsuppressed tract total that contains only one populated block group protects nothing. The threshold has to be evaluated at every published geography, not only the finest one.
Verification
- Attempt the subtraction attack on your own published output. Take a row with one suppressed cell and a published total and check the cell cannot be recovered. If it can, complementary suppression is not running.
- Assert no export contains a value below the threshold, as a raising gate rather than a log line.
- Confirm nulls survive a round trip through storage and reload as withheld rather than as zero, which is a real risk with formats that do not distinguish them.
- Difference two consecutive periods and re-run the suppression check on the result.
Frequently Asked Questions
Is the vendor’s suppression not sufficient on its own?
It is sufficient for the file they send and says nothing about what you compute from it. Every aggregation, difference and filter creates new cells with new counts, and some of those are small even when every input cell was large. Treating the vendor’s threshold as a property to be maintained rather than a step already completed is the difference between a compliant pipeline and one that happens to have started compliant.
What should a report say about suppressed data?
That it exists, how much of the total it represents, and that the published figures are therefore a lower bound. One line does it, and its absence is what allows a reader to treat a partial total as a complete one. In markets where the suppressed share is large — rural ones, mostly — the line belongs next to the headline number rather than in a footnote.
Does aggregating to a coarser geography solve the problem?
It reduces it legitimately and does not remove the obligation. Rolling small zones up until the cells clear the threshold recovers most of the hidden visits and is usually the right analytical choice, since the finer resolution was not supportable anyway. What it does not do is remove the need to check the coarser table for cells that are still small, which happens more often than expected in sparsely populated regions.
How does this interact with data retention?
They are two halves of the same obligation and are usually specified together in the vendor contract. Suppression governs what may be published; retention governs how long the underlying data may be held. Both belong enforced by the platform — a lifecycle rule on the storage and a gate on the export — because both are the kind of policy that a person will observe carefully for a year and then forget during a busy quarter.
Should the threshold be higher than the contract requires?
Often yes, and it costs less than it appears. Contracts specify a floor rather than a target, and small cells contribute almost nothing to a trade-area analysis while carrying most of the disclosure risk. Raising the threshold from five to ten typically hides another few per cent of visits in rural markets and none of the analytical signal, which is a favourable trade in both directions — more protection and a cleaner dataset.
How does suppression interact with the calibration against transactions?
It biases the panel estimate downward for stores with many small origin zones, which are disproportionately rural. If the calibration is fitted without accounting for that, the resulting factor absorbs the suppression bias and then over-corrects urban stores. Including the suppressed share as a covariate in the calibration, or fitting separately by market density, keeps the factor describing the panel rather than describing the suppression.
Who should own the suppression rules?
The data platform, with the vendor contract as the input and a named person accountable for keeping the two aligned. Leaving suppression to individual analyses guarantees inconsistency, and leaving it to the vendor guarantees it stops at the file boundary. A single implementation used by every consumer, updated when a contract changes, is both the safest and the least effort.
Related
- Mobility & Foot Traffic Data Integration — the pipeline this protects.
- Best Practices for Securing PII in Customer Location Datasets — zoning, tokenization and retention in the data lake.
- Deriving Home Zones from Aggregated Visit Data — the attribution these thresholds protect.
← Back to Mobility & Foot Traffic Data Integration