New: Philter One, a redaction box for your office, is coming soon. Learn more →

Contact Us

Tell us about your stack and the privacy problems you're trying to solve. We typically respond within one business day.

Prefer email? support@philterd.ai

Please do not enter PII or PHI in this form. If you need to share an example, use a sanitized one.

Knowledge base How-to

Publish PII counts with differential privacy instead of exact numbers

Publish PII counts with differential privacy using Philter Diffuse: choose the types, set the noise scale, and track the privacy budget across releases.

For Philter Diffuse

Counts of PII are useful to people outside the team that holds the data. A compliance report wants to know how many documents contained a Social Security number last quarter, and a dashboard wants the trend. Exact counts can give away more than you intend, though. A count of 3 passport numbers that becomes 4 after a single upload tells anyone comparing the two releases something about that one upload.

Philter Diffuse releases counts per PII type with noise from OpenDP’s Laplace mechanism, so the numbers stay useful in aggregate while what they reveal about any one document is bounded. This guide walks through one release from a JSON file and one from Philter, then covers how to pick the parameters.

Philter Diffuse releases counts per PII type and nothing else. It does not answer arbitrary queries or publish records, and it is early-access software at version 0.1.0, so its command-line interface may change before 1.0.

1. Install

From a checkout of the repository:

pip install -r requirements.txt
python main.py --version

Or use the Docker image, which runs main.py as its entrypoint from /data:

docker pull philterd/philter-diffuse:0.1.0

Mount a directory at /data when you run the container. The image keeps the privacy budget in /data/privacy_budget.json, and without a mount that file is lost when the container exits, so every run would start with a fresh budget.

2. Release counts from a JSON file

The input is a flat JSON object of PII types and whole counts of 0 or more. Any other value stops the run before anything is released or charged. Save this as pii_counts.json:

{
  "ssn": 412,
  "email-address": 3870,
  "phone-number": 2215,
  "passport": 3
}

Then run:

python main.py \
  --input pii_counts.json \
  --source claims-2026-q3 \
  --pii-types ssn,email-address,phone-number,passport \
  --scale 2.0 \
  --threshold 10 \
  --output private_counts.csv

The flags that matter:

  • --source names the dataset. The budget is tracked under this name, so renaming or copying the file does not reset it. Use the same name for every release from the same data.
  • --pii-types is required and fixes the rows of the output. Every listed type gets a row, in the order given, whether or not the data holds it. A type the source lacks is counted as 0 and noised like the rest. Types in the source that you did not list are dropped, and a warning on standard error says how many.
  • --scale sets the noise. The default is 2.0.
  • --threshold writes any noised count below the value as None. The default is 0, which masks nothing.

The CSV looks like this. Your numbers will differ on every run, because the noise is random:

PII Type,Private Count
ssn,410
email-address,3871
phone-number,2218
passport,None

Noised counts below zero are clamped to 0. Standard output gets an audit report with the epsilon spent, a strength label, and the same private counts. It never prints the raw counts, so it can go to CI or container logs.

3. Release counts from Philter

Philter 4.0 and later serve the counts they record at GET /api/pii-counts, and Philter Diffuse reads them with an API key alone. The key comes from the PHILTER_API_KEY environment variable, never from a flag. It needs the pii-counts:read scope, and its user must be a Philter administrator.

export PHILTER_API_KEY=<api key>
python main.py \
  --philter-url https://philter.internal.example.com:8080 \
  --pii-types ssn,email-address,phone-number \
  --context claims-intake \
  --from 2026-10-01 --to 2026-10-31 \
  --output october_counts.csv

--from and --to are inclusive UTC days in YYYY-MM-DD format. Certificates are verified. If Philter uses a self-signed certificate, pass it with --philter-ca-cert philter.pem. Redirects are not followed, so the key only goes to the URL you give.

If Philter reports that count recording is off (its diffuseCountsEnabled setting), the run stops without output and without spending budget. An empty result in that state does not mean Philter found no PII.

How the privacy budget works

Each release costs epsilon equal to the number of listed types divided by the scale. The example in step 2 lists four types at scale 2.0, so it spends 2.0. One document can add one to every type at once, which is why the cost grows with the length of the list and not with what the data holds.

Spent epsilon accumulates per source and never renews on its own. For JSON input and for Philter, it lives in ~/.philter-diffuse/privacy_budget.json, or wherever --budget-file or PHILTER_DIFFUSE_BUDGET_FILE points. A Philter deployment is tracked under its URL. Every read from it draws on one budget, whatever --context, --from, or --to you pass, so narrowing a query does not buy a fresh budget.

The default ceiling is --budget-ceiling 10.0. A release that would pass it exits with code 1, writes nothing, leaves any existing output file unchanged, and charges nothing. The budget is charged before noise is sampled, as one step with the ceiling check, so two concurrent runs cannot both spend the same balance. On Windows, concurrent runs against one budget file are not serialized, so run them one at a time there.

Reusing a CSV you already released costs nothing. Point dashboards at the file, not at Philter Diffuse, or every refresh becomes a new release that spends budget.

Choosing parameters

  • Scale. Higher scale means more noise, a lower epsilon per release, and less accurate counts. The average error on each count is about the scale, so a scale of 5 barely moves a count in the thousands and swamps a count of 3. The audit report labels epsilon up to 0.1 as EXTREME, up to 1.0 as HIGH, up to 5.0 as MODERATE, and above that as “LOW (Testing Only)”.
  • Types. List only the types the report needs, and pick the list from your redaction policy before you look at the counts. Choosing it from the data would reveal which types are present, and no epsilon bounds that.
  • Threshold. It hides small noised counts. Because it acts on the noised values, it adds no privacy cost.
  • Ceiling. Divide it by the cost of one release to see how many releases a source can support. At four types and scale 2.0, the default ceiling allows five.

Be clear about what the bound covers. It limits what a release reveals about one document: one redaction in Philter, or one record in your counts. A person who appears in 20 documents can shift a count by up to 20, and their protection weakens in proportion. Differential privacy also does not by itself make a dataset anonymous in a legal sense. It is one control that supports your compliance efforts, and you should validate the settings against how your own data is structured.

For the full flag list and the budget reset procedure, see the Philter Diffuse documentation.