Approach: compute cohort (by signup_date bucket), count active users per cohort-day, then apply small-count suppression (mask counts < 10). Show two patterns: deterministic suppression (replace with NULL or "<10") and differential/noisy release (add Laplace noise). Explain trade-offs.PySpark pattern (aggregate + suppression):python
from pyspark.sql import functions as F
# events: user_id, signup_date (date), event_date (date)
df = events.withColumn("cohort_day", F.datediff(F.col("event_date"), F.col("signup_date")))
agg = (df.groupBy("signup_date", "cohort_day")
.agg(F.countDistinct("user_id").alias("users")))
suppressed = agg.withColumn("users_suppressed",
F.when(F.col("users") < 10, None) # or F.lit("<10")
.otherwise(F.col("users")))
Equivalent SQL:sql
WITH agg AS (
SELECT signup_date, DATEDIFF(event_date, signup_date) AS cohort_day,
COUNT(DISTINCT user_id) AS users
FROM events
GROUP BY 1,2
)
SELECT signup_date, cohort_day,
CASE WHEN users < 10 THEN NULL ELSE users END AS users_suppressed
FROM agg;
Surface in dashboard:- NULL => show blank or “<10” label with tooltip: "suppressed for privacy".- Optionally show a boolean "suppressed" column to drive UI shading and accessibility.Alternative: add noise (Laplace) to enable analysis of sparsity while preserving privacy:- Add Laplace(0, b) noise where b calibrated to desired ε (differential privacy).Trade-offs:- Suppression (NULL/"<10"): simple, easy to audit, but removes signal and can bias aggregates (e.g., undercounts).- Noise: preserves utility for aggregate analysis, allows release of small cells, but requires careful DP calibration and may confuse stakeholders if negative/decimal values appear (need post-processing, clamping, and disclosure).Edge cases:- Use cohort counts across time windows (rolling) to avoid frequent suppression.- Consider reporting totals at higher granularity to reduce suppression.- Document suppression policy in dashboard.