feat: PII detection and de-identification (Presidio) — #233#248
Open
NKeleher wants to merge 7 commits into
Open
feat: PII detection and de-identification (Presidio) — #233#248NKeleher wants to merge 7 commits into
NKeleher wants to merge 7 commits into
Conversation
Two detection passes: always-available column-name heuristics
(multilingual restricted-word lists ported in spirit from
PovertyAction/PII_detection, plus a sparsity check for free-text
columns) and Presidio BatchAnalyzerEngine value scanning over sampled
string columns when a spaCy model is installed (en/es/fr small models,
downloaded at runtime — never bundled).
Flags persist per dataset in pii_flags_{alias} (logs db) with a
per-column decision (mask/drop/keep/undecided); redaction helpers apply
those decisions to DataFrames and to the correction log.
presidio-analyzer/anonymizer become runtime dependencies;
en_core_web_sm is a dev dependency (direct wheel URL — allowed in
dependency groups) so the Presidio path is tested in CI.
Refs #233
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Masks every non-null value in the selected columns with a redaction label (e.g. [PERSON]) and casts them to String, following the full established prep-action pattern: enum member, descriptions and confirmation message, RedactColumnsOperation, and Stata + Python replication-script emitters. Logged, replayable, and removable like any other prep step. Refs #233 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-dataset section with language/model controls (in-app spaCy model download, English default, Spanish/French available), a Scan for PII button running both detection passes, an editable flags table showing column, detection source, entity type, hit rate and sample matched values, and an apply button that converts mask/drop decisions into prep steps. Includes the indirect-identifier warning. Layout tests updated with spec-aware st.columns mocks and alias-keyed duckdb side effects. Refs #233 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build_replication_package gains include_pii (default False). In de-identified mode, PII flag decisions are applied to the raw, prepped, and corrected datasets before anything downstream is generated — covering CSVs, Parquets, codebook sample values, data-dict.yaml examples, the correction-log CSV, and the value literals embedded in generated correction scripts. Undecided flags are masked (conservative default); the survey key column is never redacted. With-PII exports leave the data untouched and instead bundle 2_scripts/5_deidentify_data.py (a uv-run-ready script encoding the recorded decisions) plus a pii_flags.csv audit log. The export page gets a mode radio (de-identified recommended), a flagged-column summary showing what the export will do, mode-specific warnings and acknowledgment checkboxes (indirect-identifier notice for de-identified downloads), a _deidentified filename suffix, and an updated zip-contents preview. The package README documents the export mode, redacted columns, and the indirect-identifier warning. Refs #233 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two new per-column PII decisions that preserve categorical analyzability
(group-bys, joins, frequencies) instead of destroying it with a constant
mask label:
- hash: HMAC-SHA256 pseudonyms (PERSON_3fa1b9c2) keyed with an
auto-generated per-project secret salt stored in the local cache
(pii_salt, logs db) and never included in de-identified exports, so
recipients cannot dictionary-attack low-entropy domains like names.
Deterministic across repeated exports. Correction-log values for
hashed columns get matching pseudonyms, keeping the audit log
analyzable.
- code: readable sequential category codes (VILLAGE_NAME_001) assigned
in random order (no alphabetical-rank leak) and persisted per project
(pii_code_map_{alias}) so re-exports stay stable. Codes are built
across the raw/prepped/corrected datasets in one pass for consistent
tokens.
Both apply at export time (they need the salt/code maps, which never
leave the local cache in de-identified exports); the prep-action path
remains mask/drop, and the PII Review UI says so. With-PII packages
embed the salt in 5_deidentify_data.py (they carry the raw data anyway)
so its pseudonyms match in-app exports; its category codes are derived
in-script from sorted distinct values.
The indirect-identifier warnings now also note that deterministic
pseudonyms preserve the frequency distribution, so rare categories
remain recognizable by rarity.
Refs #233
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Added two pseudonymization decisions in 4557ca6, per discussion:
Both apply at export time (the salt and code maps never leave the local cache in de-identified exports); mask/drop remain the prep-action path. With-PII packages embed the salt in |
Hash and code decisions were export-time-only, so applying them in the
PII Review left nothing in the change log. RedactColumnsOperation now
supports method="hash" (salted pseudonyms; the salt is fetched from the
local cache at execute time — never stored in prep_args, so it cannot
leak through the exported prep log) and method="code" (persisted
per-alias category codes; prep_args.additional_info carries the alias).
Because HMAC hashing and code mapping are not idempotent by nature, the
apply expressions now guard on the token pattern / existing codes, so
prep-log replay and the export gate can safely re-apply decisions to
already-tokenized data — verified that a package built from
prep-step-hashed data carries identical tokens in raw and prepped
datasets with no double-hashing. build_code_maps likewise skips values
that already are codes.
The Stata and Python prep-script emitters emit NOTE comments for
hash/code steps (no salt or code map is ever exported; de-identified
packages already carry the tokens in the bundled raw data, so replay
stays consistent). Change-log messages name the method ("replaced with
salted hash pseudonyms" / "category codes").
Refs #233
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Pull Request Summary 🚀
What does this PR do? 📝
Implements PII detection and de-identification (#233), using Microsoft Presidio rather than hand-rolled spaCy NER as originally proposed. Four pieces:
processing/pii.py, new): two passes, always-available column-name heuristics (multilingual restricted-word lists ported in spirit from PovertyAction/PII_detection,plus a sparsity check for free-text columns), and Presidio
BatchAnalyzerEnginevalue scanning over sampled string columns (PERSON, PHONE_NUMBER, EMAIL_ADDRESS, LOCATION, ... with confidence thresholds). Flags persist per dataset inpii_flags_{alias}(logs db) with per-column decisions: mask / drop / keep.build_replication_packagegainsinclude_pii(default False → de-identified). De-identified exports apply the flag decisions to every bundled dataset before anything is generated, so the raw CSV/Parquets, codebook sample values,data-dict.yamlexamples, the correction-log CSV, and the value literals embedded in generated correction scripts are all covered. Undecided flags are masked (conservative default); the survey key column is never redacted (the corrections pipeline needs it).2_scripts/5_deidentify_data.py, a uv-run-ready (PEP 723) script encoding the recorded decisions so recipients can de-identify downstream — plus apii_flags.csvaudit log.The export page gets an export-mode radio, a flagged-column summary showing exactly what the export will do, and mode-specific warnings: the existing encrypted-storage acknowledgment for with-PII downloads, and a new
indirect-identifier warning for de-identified downloads (de-identification ≠ anonymization; quasi-identifier combinations may still identify subjects). That warning also appears in the PII Review UI, the package README, and the
de-identify script's output.
Why is this change needed? 🤔
DataSure exports survey data that routinely contains PII with no detection or removal step — the replication package writes the full raw dataset, real sample values in the codebook/data-dict, and actual data values in the correction log. The only safeguard was an all-or-nothing checkbox. See #233.
How was this implemented? 🛠️
presidio-analyzer+presidio-anonymizer) becomes a runtime dependency; spaCy models are not bundled — users download the small models (~15–40 MB) from within the app.en_core_web_smis a dev dependency (direct wheel URL, allowed in dependency groups) so the Presidio path runs for real in CI; model-dependent tests skip when absent.[PERSON],[PHONE],*****fallback); masked numeric columns cast to String.How to test or reproduce? 🧪
uv sync uv run python -m pytest tests/processing/test_pii.py tests/replication/ tests/views/ -q just lint-py && just fmt-pythonManual:
uv run datasureon the demo project → Prepare Data → PII Review → Scan for PII (demo data hasenum_name,household_latitude/longitude,village_nameto flag) → set decisions → apply as prep steps. Then Export Replication Package in both modes: the de-identified zip has masked/dropped columns everywhere (filename gains_deidentified); the with-PII zip contains5_deidentify_data.py— runuv run 5_deidentify_data.pyin the unzipped package to produce*_deidentified.parquetcopies.Screenshots (if applicable) 📷
UI additions: PII Review section on Prepare Data; export-mode radio + flags summary + mode-specific warnings on Export Replication Package.
Checklist ✅
presidio-analyzer,presidio-anonymizer(runtime) anden-core-web-sm(dev).Reviewer Emoji Legend
:code::smiley::+1::100:...and I want the author to know it! This is a way to highlight positive parts of a code review.
:star: :star: :star:And I am providing reasons why it needs to be addressed as well as suggested improvements.
:star: :star:And I am providing suggestions where it could be improved either in this PR or later.
:star:...and consider this a suggestion, not a requirement.
:question:This should be a fully formed question with sufficient information and context that requires a response.
:memo::pick:This does not require any changes and is often better left unsaid. This may include stylistic, formatting, or organization suggestions and should likely be prevented/enforced by linting if they really matter
:recycle:Should include enough context to be actionable and not be considered a nitpick.