Local de-identification and consistent pseudonymization for qualitative research data: interview transcripts, focus groups, and open ended responses.
QualiDeID finds the identifiers in a transcript (names, emails, phone numbers, organizations, places, and more), gives each person, organization, and place a consistent pseudonym, lets you review and correct the proposals, and writes de-identified copies plus a pseudonym map and an audit log. The same identifier becomes the same pseudonym across every file and every mention, which is the part that hand editing in a word processor always gets wrong.
It runs fully offline. The default detection engine is pure Python (no model download, no network, no API key). Optional backends (Microsoft Presidio, or a local LLM via Ollama for quasi identifiers) can be switched on, but are never required.
General PII tools exist (Presidio is the obvious one), but they are frameworks
aimed at structured or enterprise data and one shot redaction. None of them
provides the qualitative research workflow: consistent pseudonyms across a whole
corpus, a human review and merge step, transcript formats (DOCX, VTT, SRT), and
an audit log fit for an ethics board. Searches for a research specific tool turn
up only manually anonymized thesis data, not software. QualiDeID is that missing
workflow layer. See ../OPPORTUNITIES.md for the evidence.
Core:
- Ingest transcripts in plain text (.txt), Word (.docx), WebVTT (.vtt), and SubRip (.srt). For captions, only the spoken text is changed; timing lines and cue identifiers are preserved. For Word, paragraph structure is preserved.
- A built in detection engine covering emails, phone numbers, URLs, IP addresses, SSNs, credit cards (Luhn checked), IBANs, dates, persons (titles, self introductions, and a bundled given name gazetteer), organizations, locations and addresses, and transcript speaker labels.
- A persistent SQLite project store that makes pseudonyms consistent: an entity is keyed by type and normalized surface, so the same name maps to the same pseudonym across files and runs.
- Per type de-identification actions: pseudonym (P01, ORG1, LOC1), redact to a generic tag ([EMAIL]), or keep. Defaults are sensible and configurable.
- A review workflow: list detected entities, change any decision, reject false positives, and merge entities (so "Jane" and "Jane Doe" share one pseudonym).
- De-identified output files, a pseudonym map (CSV), and an audit log (CSV).
Optional and opt in:
- A Presidio backend for spaCy based NER.
- A local LLM pass (via Ollama) that proposes quasi identifiers (employer, unique role, rare place).
- An interactive terminal review command.
Requires Python 3.10 or newer (developed and tested on 3.14).
python -m venv .venv
# Windows PowerShell: .venv\Scripts\Activate.ps1
# Git Bash / Linux / macOS: source .venv/bin/activate
pip install -e .Exact pinned environment:
pip install -r requirements-lock.txt
pip install -e . --no-depsOptional extras:
pip install -e ".[presidio]" # spaCy based NER backend (also needs a spaCy model)
pip install -e ".[tui]" # interactive Textual reviewer
pip install -e ".[dev]" # pytest and hypothesis for the test suitequalideid deid interview.txt -o out/This scans the file, applies the default decisions, and writes
out/interview.deid.txt, out/pseudonym_map.csv, out/audit_log.csv, and a
project store out/qualideid.sqlite. Example output:
INTERVIEWER: Could you introduce yourself?
P: My name is P01. You can reach me at [EMAIL] or [PHONE].
I work at ORG01 and I live in LOC01. My colleague P02 joined in 2019.
# 1. Scan one or more transcripts into a project store
qualideid scan interview1.txt interview2.vtt --project study.sqlite
# 2. Review what was found and how it will be replaced
qualideid entities --project study.sqlite
qualideid entities --project study.sqlite --csv review.csv # or export to CSV
# 3. Correct decisions: reject a false positive, change an action, or merge
qualideid set --project study.sqlite --id 7 --status rejected
qualideid set --project study.sqlite --id 3 --action redact
qualideid merge --project study.sqlite --into 2 --from 5 # "Jane" and "Jane Doe"
# 4. Apply to produce de-identified copies plus the map and audit log
qualideid apply interview1.txt interview2.vtt --project study.sqlite --out out/
# Inspect the mapping and the trail at any time
qualideid map --project study.sqlite
qualideid audit --project study.sqliteRun python -m qualideid ... if you prefer not to install the console script.
Settings come from built in defaults, an optional TOML file (--config), and two
environment variables. A TOML file can change the action per entity type and the
pseudonym label prefixes:
[qualideid]
backend = "builtin" # or "presidio"
[qualideid.actions]
DATE = "redact" # redact dates instead of keeping them
LOCATION = "pseudonym"
[qualideid.labels]
PERSON = "Participant"Environment variables: QUALIDEID_BACKEND and QUALIDEID_OLLAMA_HOST (used only
by the optional LLM pass). No secrets are read or stored.
The built in engine is deliberately cautious and is not a substitute for a human
reviewer; that is exactly why the review and merge steps exist. Names are found
by titles (Dr., Ms.), self introductions ("my name is ..."), a bundled given name
list, and capitalized name sequences. Co-reference (linking "Jane" to "Jane Doe")
is resolved by the merge command. Enable the Presidio or LLM backends for
stronger name, organization, and quasi identifier coverage.
pip install -e ".[dev]"
pytestThe suite (42 tests) builds its own transcripts (including DOCX, VTT, and SRT) in temporary directories and runs fully offline. It covers the recognizers, overlap resolution, the store and pseudonym consistency, the redactor, every transcript format, the end to end de-identification property, the CLI, configuration, and the security properties below.
models.pyentity types, actions, and the entity record.config.pyper type actions, labels, and detection settings.recognizers.pythe built in detectors (structured and heuristic).names.pythe bundled given name gazetteer and stop word list.detect.pyruns recognizers and optional backends, then de-overlaps spans.pseudonyms.pypseudonym formatting and surface normalization.store.pythe SQLite project store (parameterized SQL only).transcripts.pyformat specific reading and writing.engine.pyscanning, the redactor, and applying decisions.pii_backends.pythe optional Presidio and Ollama backends.cli.pythe Typer command line interface.
QualiDeID handles the most sensitive data a researcher holds, so safety is a first class concern.
- No SQL injection. Every database statement uses bound parameters, never
string formatting, so transcript text (which becomes entity surfaces) can never
be interpreted as SQL. There is a test that a malicious surface containing
DROP TABLEis stored as data and the database is unharmed. - Output stays in the output directory. Output file names are sanitized and the resolved path is confirmed to be inside the chosen output directory, so a crafted input file name cannot write elsewhere.
- Bounded input. Files larger than a configurable limit (default 25 MB) are refused, and every detection regex is linear and length bounded to avoid catastrophic backtracking (there is a test on adversarial input).
- Offline by default, opt in network. The default engine makes no network calls. The only outbound traffic is the optional LLM pass, and it goes only to the local Ollama host you configure, with a timeout. The host is read from the environment and never logged.
- No unsafe execution. There is no
eval, noexec, nopickle, and no untrusted deserialization. Configuration is read with the standard librarytomllib, which cannot execute code. - Sensitive artifacts. By design the project store and the pseudonym map
contain the link between real identifiers and pseudonyms (this is what makes
pseudonyms consistent). Treat the
.sqlitestore andpseudonym_map.csvas sensitive as the source data: keep them out of shared folders and version control, and delete them when the de-identified outputs are finalized.
De-identification is never perfect. Always have a human review the output before sharing or archiving data, especially for quasi identifiers (unique roles, rare combinations of attributes) that no automatic tool can catch reliably.
Apache-2.0. See LICENSE.