Differentially private publication pipeline for aggregate group statistics with k-anonymity gating and budget accounting
Project description
dp-group-stats
A lightweight Python library for publishing differentially private aggregate statistics over small groups, with k-anonymity gating and privacy budget accounting.
What this solves
When publishing aggregate statistics (means, sums) for small professional groups on a recurring schedule, you need:
- Differential privacy — calibrated noise so individual contributions can't be reverse-engineered
- K-anonymity gating — suppress groups that are too small to publish safely
- Budget accounting — track cumulative privacy spend per user across groups and time
- Graceful degradation — when budget runs low, statistics get noisier instead of disappearing
Existing DP libraries are either massive frameworks (OpenDP, TensorFlow Privacy) or bare mechanism implementations. dp-group-stats is the pipeline layer in between: configuration, noise, publication policy, and budget tracking in one focused package with zero dependencies.
Install
pip install dp-group-stats
Requirements: Python 3.10+ — no dependencies beyond the standard library.
Quick start
from datetime import date
from dp_group_stats import (
DPGroupStatsConfig,
InMemoryPrivacyLedger,
PublicationStatus,
compute_adaptive_epsilon,
get_period_bounds,
get_publication_status,
laplace_noise,
laplace_ci_half_width,
)
# 1. Configure the pipeline
config = DPGroupStatsConfig(
period_type="weekly",
annual_epsilon_cap=150.0,
)
# 2. Check publication eligibility
status = get_publication_status(
was_active=False,
consecutive_eligible=3,
consecutive_ineligible=0,
activation_weeks=config.release_policy.activation_weeks,
deactivation_grace_weeks=config.release_policy.deactivation_grace_weeks,
)
assert status == PublicationStatus.published
# 3. Clip contributions and add noise
raw_actual_hours = [42.0, 55.0, 38.0, 61.0, 200.0, 44.0]
clipped = [config.bounds.clip_actual(h) for h in raw_actual_hours]
true_sum = sum(clipped)
noisy_sum = true_sum + laplace_noise(
epsilon=config.epsilon_split.actual_sum,
sensitivity=config.bounds.actual_sensitivity,
)
noisy_mean = noisy_sum / len(clipped)
# 4. Compute confidence intervals
ci_half, n_display = laplace_ci_half_width(
epsilon=config.epsilon_split.actual_sum,
sensitivity=config.bounds.actual_sensitivity,
n_users=len(clipped),
)
print(f"Mean: {noisy_mean:.1f}h [±{ci_half:.1f}h 90% CI] (~{n_display} reporters)")
# 5. Track budget
ledger = InMemoryPrivacyLedger()
for user_id in ["u1", "u2", "u3", "u4", "u5", "u6"]:
ledger.record(
user_id=user_id,
family="state_specialty",
cell_key="DEU/BE/cardiology",
period_start=date(2026, 3, 16),
epsilon=config.epsilon_split.total,
)
summary = ledger.budget_summary(annual_cap=config.annual_epsilon_cap)
print(f"Worst-case user: {summary['worst_case_spent']:.1f}ε of {summary['annual_cap']}ε cap")
Components
Configuration (dp_group_stats.config)
ContributionBounds— clipping bounds that limit sensitivity (default: planned [0, 80h], actual [0, 120h])EpsilonSplit— how per-cell epsilon is divided across quantities (default: 0.2 planned, 0.8 actual)ReleasePolicyConfig— k-anonymity threshold, dominance rule, activation/deactivation streaksDPGroupStatsConfig— top-level config with budget validation at construction time
Mechanisms (dp_group_stats.mechanisms)
laplace_noise()— Laplace mechanism with injectable RNG for deterministic testinglaplace_ci_half_width()— confidence intervals with rounded group size to avoid leaking exact n
Policy (dp_group_stats.policy)
Publication state machine: suppressed → warming_up → published → cooling_down → suppressed
- Cells must pass k_min and dominance checks for N consecutive periods before publishing
- Grace period before suppression (noise continues during cooldown to prevent information leakage)
Accounting (dp_group_stats.accounting)
PrivacyLedger(Protocol) — pluggable interface for budget trackingInMemoryPrivacyLedger— reference implementation for testing/simulationEpsilonLedger— lightweight cell-only trackercompute_adaptive_epsilon()— graceful degradation that scales epsilon down as the annual cap approaches
Bring your own storage backend by implementing the PrivacyLedger protocol (e.g., SQL-backed for production).
Periods (dp_group_stats.periods)
get_period_bounds()— weekly/biweekly/monthly period boundariesperiod_before()/compute_period_index()— temporal navigation
Design decisions
This library makes specific choices documented in the design specs:
- Substitution neighboring relation: DP protects contribution values, not user presence. Counts are public; only sums are noised.
- Per-user budget accounting: the privacy guarantee lives with users, not cells. A user appearing in multiple release families accumulates epsilon across all of them.
- Time-based degradation: adaptive epsilon is computed from elapsed time and configured spending, not from actual per-user data (which would itself leak information).
- K-anonymity as policy, not proof: k_min and dominance rules are publication safeguards layered on top of DP, not substitutes for it.
Developed for
This library was extracted from Open Working Hours, a privacy-first platform for healthcare workers to track and report working hours. The design was validated through 200+ simulation scenarios covering parameter selection for workforce statistics.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dp_group_stats-0.1.0.tar.gz.
File metadata
- Download URL: dp_group_stats-0.1.0.tar.gz
- Upload date:
- Size: 49.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9cf25804247731f27c69263ca5ad6af3ab2d99287933388e77aef6f5ca18bcea
|
|
| MD5 |
41e26eb4a0b4ff150003c3e5240b4e0b
|
|
| BLAKE2b-256 |
281f13fd02863a2326c145580a4108449e60fa74488b17abfdba7f3878252206
|
File details
Details for the file dp_group_stats-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dp_group_stats-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7e5a2b2518a4f923df1c0ac2480367d77570ed193c590aeabd91d6ac4db1137
|
|
| MD5 |
6f4eb7eebc8fd030a05dd333abcd1aba
|
|
| BLAKE2b-256 |
6dc0c669e4ca20373266100303c1371d437f6d458b835385e14987426609d5e5
|