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:
--sourcenames 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-typesis 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.--scalesets the noise. The default is 2.0.--thresholdwrites any noised count below the value asNone. 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.