De-identifying clinical text
openmed.deidentify detects PHI/PII and rewrites the text so it can be shared,
stored, or analyzed without exposing patients. It runs fully on-device after
a one-time model download — no network calls, no telemetry, no raw PHI leaving
the process. This is the single most important OpenMed entry point for privacy
work; everything else (policies, audit, multilingual, date-shifting) layers on
top of it.
When to use this skill
Reach for deidentify when you need to transform text — replace, mask, remove,
hash, or date-shift the identifiers. If you only need to locate PHI spans
without changing the text, use extract_pii (see extracting-pii-entities). To
restore masked text later, use reidentify (see reidentifying-text).
Quick start
import openmed
note = (
"Patient John Doe (MRN 1234567) was seen on 2024-03-02 by Dr. Alice Reed. "
"Contact: john.doe@example.com, 617-555-0142."
)
result = openmed.deidentify(
note,
method="mask", # mask | remove | replace | hash | shift_dates
confidence_threshold=0.7, # safety default; raise to reduce false negatives' impact
policy="hipaa_safe_harbor", # optional bundled profile (see below)
)
print(result.deidentified_text)
# Patient [NAME] (MRN [ID_NUM]) was seen on [DATE] by Dr. [NAME]. ...
for e in result.pii_entities:
# NEVER log e.text / e.original_text — those are raw PHI. Use offsets + label.
print(e.canonical_label, e.start, e.end, round(e.confidence, 3))
deidentify returns a DeidentificationResult with these fields (note the
exact names):
| Field |
What it holds |
.deidentified_text |
the rewritten, PHI-safe string (your output) |
.pii_entities |
list[PIIEntity] — each has start, end, canonical_label, confidence, action, surrogate; original_text/text hold raw PHI |
.mapping |
redacted→original dict, only when keep_mapping=True (secret) |
.method |
the method actually applied |
.metadata |
run metadata (model, policy, counts) |
The five methods
method= |
Effect |
Reversible? |
Use when |
"mask" |
John Doe → [NAME] |
with keep_mapping=True |
default; clear that redaction happened |
"remove" |
deletes the span entirely |
no |
minimal-footprint output |
"replace" |
type-matched fake value (John Doe→Mark Lee) |
with keep_mapping=True |
keep notes readable/parseable (see generating-synthetic-surrogates) |
"hash" |
stable hash per value, links repeats |
no (one-way) |
cohort linkage without revealing identity |
"shift_dates" |
moves dates, preserves intervals |
n/a |
research needing temporal structure (see shifting-clinical-dates) |
Workflow
- Pick a method and a policy. Start from a bundled
policy= profile
(hipaa_safe_harbor, gdpr_pseudonymization, research_limited_dataset, …)
so per-label actions are set for you. See configuring-privacy-policies.
- Set
confidence_threshold deliberately. Default is 0.7. For de-id,
prefer over-redaction: a missed identifier is a breach, an over-redacted
token is just noise. The bundled safety sweep catches structured IDs
(SSN, MRN-like, emails) even below threshold.
- Run
deidentify. Inspect result.pii_entities by offset and label,
not raw text, to confirm coverage.
- For stable surrogates, pass
consistent=True, seed=<int> so the same
input maps to the same fake value every run (reproducible pipelines).
- For reversibility, pass
keep_mapping=True and store result.mapping
in a secured vault — never alongside the de-identified output.
- Verify, don't assume. Check residual risk with
audit=True
(auditing-deidentification-runs) and the 18-identifier checklist
(auditing-safe-harbor-checklist).
Consistent surrogates and reversibility
# Same fake identity for every mention of the same person, reproducibly:
r = openmed.deidentify(note, method="replace", consistent=True, seed=42)
# Reversible de-id (keep the mapping secret and separate from output):
r = openmed.deidentify(note, method="mask", keep_mapping=True)
restored = openmed.reidentify(r.deidentified_text, r.mapping)
assert restored == note
Hand-off to / from OpenMed
- Detect only:
openmed.extract_pii(text) → PredictionResult with
.entities (spans, no rewrite). Use it to preview coverage first.
- Restore:
openmed.reidentify(deidentified_text, mapping) — requires
keep_mapping=True at de-id time and proper authorization.
- Policies:
configuring-privacy-policies to choose/customize a policy=.
- Audit:
deidentify(..., audit=True) → AuditReport with offsets, hashes,
detector provenance, and residual-risk — never plaintext.
- Other surfaces (same engine): MCP tool
openmed_deidentify; REST
POST /pii/deidentify. There is no CLI de-id command.
Edge cases & gotchas
- Attribute names. It is
result.deidentified_text and
result.pii_entities — not .text/.entities. (extract_pii returns a
PredictionResult whose spans are at .entities.)
- Raw PHI never leaves the span objects.
PIIEntity.text and
.original_text contain real identifiers. Do not print, log, or cache them.
Audit and logs use offsets, canonical_label, and hashes only.
- Threshold is a safety dial, not an accuracy dial. Lowering it redacts
more; in de-id, false positives are cheap and false negatives are breaches.
shift_dates is for dates only; combine with keep_year/date_shift_days
(see shifting-clinical-dates). It does not touch names or IDs.
keep_mapping output is sensitive as PHI. The mapping re-identifies
everyone — store it encrypted, access-controlled, and apart from the output.
- Multilingual: pass
lang= (and locale= for surrogates) for non-English
notes; see deidentifying-multilingual-text. Do not run English models on
other languages.
- De-id is verified, not assumed. Gate releases on leakage/residual-risk,
not F1 alone.
Standards & references
1---2name: deidentifying-clinical-text3description: Remove, mask, or replace PHI/PII in clinical free text on-device with OpenMed's deidentify(). Use when the user needs to de-identify medical notes, strip patient identifiers, redact PHI before sharing or analysis, anonymize discharge summaries, or pick a de-id method (mask vs remove vs replace vs hash vs shift_dates). Covers confidence_threshold for safety, consistent+seed for stable surrogates, keep_mapping for reversible de-id, policy= profiles, and the DeidentificationResult fields. Pairs with OpenMed extract_pii (detect spans), reidentify (restore), configuring-privacy-policies, and auditing-deidentification-runs.4license: Apache-2.05---67# De-identifying clinical text89`openmed.deidentify` detects PHI/PII and rewrites the text so it can be shared,10stored, or analyzed without exposing patients. It runs **fully on-device** after11a one-time model download — no network calls, no telemetry, no raw PHI leaving12the process. This is the single most important OpenMed entry point for privacy13work; everything else (policies, audit, multilingual, date-shifting) layers on14top of it.1516## When to use this skill1718Reach for `deidentify` when you need to transform text — replace, mask, remove,19hash, or date-shift the identifiers. If you only need to **locate** PHI spans20without changing the text, use `extract_pii` (see `extracting-pii-entities`). To21**restore** masked text later, use `reidentify` (see `reidentifying-text`).2223## Quick start2425```python26import openmed2728note = (29 "Patient John Doe (MRN 1234567) was seen on 2024-03-02 by Dr. Alice Reed. "30 "Contact: john.doe@example.com, 617-555-0142."31)3233result = openmed.deidentify(34 note,35 method="mask", # mask | remove | replace | hash | shift_dates36 confidence_threshold=0.7, # safety default; raise to reduce false negatives' impact37 policy="hipaa_safe_harbor", # optional bundled profile (see below)38)3940print(result.deidentified_text)41# Patient [NAME] (MRN [ID_NUM]) was seen on [DATE] by Dr. [NAME]. ...4243for e in result.pii_entities:44 # NEVER log e.text / e.original_text — those are raw PHI. Use offsets + label.45 print(e.canonical_label, e.start, e.end, round(e.confidence, 3))46```4748`deidentify` returns a `DeidentificationResult` with these fields (note the49exact names):5051| Field | What it holds |52| --- | --- |53| `.deidentified_text` | the rewritten, PHI-safe string (your output) |54| `.pii_entities` | `list[PIIEntity]` — each has `start`, `end`, `canonical_label`, `confidence`, `action`, `surrogate`; `original_text`/`text` hold raw PHI |55| `.mapping` | redacted→original dict, only when `keep_mapping=True` (secret) |56| `.method` | the method actually applied |57| `.metadata` | run metadata (model, policy, counts) |5859## The five methods6061| `method=` | Effect | Reversible? | Use when |62| --- | --- | --- | --- |63| `"mask"` | `John Doe` → `[NAME]` | with `keep_mapping=True` | default; clear that redaction happened |64| `"remove"` | deletes the span entirely | no | minimal-footprint output |65| `"replace"` | type-matched fake value (`John Doe`→`Mark Lee`) | with `keep_mapping=True` | keep notes readable/parseable (see `generating-synthetic-surrogates`) |66| `"hash"` | stable hash per value, links repeats | no (one-way) | cohort linkage without revealing identity |67| `"shift_dates"` | moves dates, preserves intervals | n/a | research needing temporal structure (see `shifting-clinical-dates`) |6869## Workflow70711. **Pick a method and a policy.** Start from a bundled `policy=` profile72 (`hipaa_safe_harbor`, `gdpr_pseudonymization`, `research_limited_dataset`, …)73 so per-label actions are set for you. See `configuring-privacy-policies`.742. **Set `confidence_threshold` deliberately.** Default is `0.7`. For de-id,75 prefer *over*-redaction: a missed identifier is a breach, an over-redacted76 token is just noise. The bundled safety sweep catches structured IDs77 (SSN, MRN-like, emails) even below threshold.783. **Run `deidentify`.** Inspect `result.pii_entities` by **offset and label**,79 not raw text, to confirm coverage.804. **For stable surrogates**, pass `consistent=True, seed=<int>` so the same81 input maps to the same fake value every run (reproducible pipelines).825. **For reversibility**, pass `keep_mapping=True` and store `result.mapping`83 in a secured vault — never alongside the de-identified output.846. **Verify, don't assume.** Check residual risk with `audit=True`85 (`auditing-deidentification-runs`) and the 18-identifier checklist86 (`auditing-safe-harbor-checklist`).8788## Consistent surrogates and reversibility8990```python91# Same fake identity for every mention of the same person, reproducibly:92r = openmed.deidentify(note, method="replace", consistent=True, seed=42)9394# Reversible de-id (keep the mapping secret and separate from output):95r = openmed.deidentify(note, method="mask", keep_mapping=True)96restored = openmed.reidentify(r.deidentified_text, r.mapping)97assert restored == note98```99100## Hand-off to / from OpenMed101102- **Detect only:** `openmed.extract_pii(text)` → `PredictionResult` with103 `.entities` (spans, no rewrite). Use it to preview coverage first.104- **Restore:** `openmed.reidentify(deidentified_text, mapping)` — requires105 `keep_mapping=True` at de-id time and proper authorization.106- **Policies:** `configuring-privacy-policies` to choose/customize a `policy=`.107- **Audit:** `deidentify(..., audit=True)` → `AuditReport` with offsets, hashes,108 detector provenance, and residual-risk — never plaintext.109- **Other surfaces (same engine):** MCP tool `openmed_deidentify`; REST110 `POST /pii/deidentify`. There is **no** CLI de-id command.111112## Edge cases & gotchas113114- **Attribute names.** It is `result.deidentified_text` and115 `result.pii_entities` — not `.text`/`.entities`. (`extract_pii` returns a116 `PredictionResult` whose spans are at `.entities`.)117- **Raw PHI never leaves the span objects.** `PIIEntity.text` and118 `.original_text` contain real identifiers. Do not print, log, or cache them.119 Audit and logs use offsets, `canonical_label`, and hashes only.120- **Threshold is a safety dial, not an accuracy dial.** Lowering it redacts121 *more*; in de-id, false positives are cheap and false negatives are breaches.122- **`shift_dates` is for dates only**; combine with `keep_year`/`date_shift_days`123 (see `shifting-clinical-dates`). It does not touch names or IDs.124- **`keep_mapping` output is sensitive as PHI.** The mapping re-identifies125 everyone — store it encrypted, access-controlled, and apart from the output.126- **Multilingual:** pass `lang=` (and `locale=` for surrogates) for non-English127 notes; see `deidentifying-multilingual-text`. Do not run English models on128 other languages.129- **De-id is verified, not assumed.** Gate releases on leakage/residual-risk,130 not F1 alone.131132## Standards & references133134- HIPAA De-identification, 45 CFR 164.514(b) — Safe Harbor & Expert135 Determination: https://www.hhs.gov/hipaa/for-professionals/privacy/special-topics/de-identification/index.html136- HHS Safe Harbor 18-identifier guidance: see `auditing-safe-harbor-checklist`.137- OpenMed source: `openmed/core/pii.py` (`deidentify`, `extract_pii`,138 `reidentify`, `DeidentificationResult`, `PIIEntity`).