🕵️ Rewriting Biographies¶
Instead of replacing entities with tokens, rewrite mode generates a
privacy-safe transformation of the entire text. The run() / preview() pipeline:
- Detects entities (same as replace mode, plus latent entity detection)
- Classifies the domain and assigns sensitivity dispositions
- Generates a rewritten version that obscures sensitive entities
- Evaluates quality (utility) and privacy (leakage) with an automated repair loop
Afterward, a separate optional evaluate() call runs LLM judges for
detection validity and holistic privacy, quality, and style scores.
📚 What you'll learn¶
- Configure rewrite mode with
PrivacyGoalto specify what to protect and what to preserve - Set evaluation criteria and risk tolerance for automated quality checks
- Preview rewritten text and inspect utility / leakage scores
- Triage flagged records with
needs_human_review - Run
evaluate()for detection validity and holistic judge scores (privacy, quality, style)
Tip: First time running notebooks? Start with setup instructions.
⚙️ Setup¶
- Check if your
NVIDIA_API_KEYfrom build.nvidia.com is registered for model access.- The default
build.nvidia.com(NVIDIA Build) setup is a convenient way to try Anonymizer and iterate on previews. Use of NVIDIA Build is subject to NVIDIA Build's own terms of service and privacy practices, which are separate from and independent of the NeMo Framework library. NVIDIA Build is intended for evaluation and testing purposes only and may not be used in production environments. Do not upload any confidential information or personal data when using NVIDIA Build. Your use of NVIDIA Build is logged for security purposes and to improve NVIDIA products and services. - Request and token rate limits on
build.nvidia.comvary by account and model access, and lower-volume development access can be slow for full-dataset runs. Start withpreview()on a small sample, then move to your own endpoint for production data and usage.
- The default
- Import
RewriteandPrivacyGoal. Anonymizer()initializes with the default model provider -- no extra config needed.configure_logging(LoggingConfig.default())keeps logs at INFO. Switch toLoggingConfig.debug()when troubleshooting.
import getpass
import os
if not os.getenv("NVIDIA_API_KEY"):
key = getpass.getpass("Enter NVIDIA_API_KEY from build.nvidia.com: ").strip()
if not key:
raise RuntimeError("NVIDIA_API_KEY is required to run these notebooks.")
os.environ["NVIDIA_API_KEY"] = key
from anonymizer import (
Anonymizer,
AnonymizerConfig,
AnonymizerInput,
LoggingConfig,
PrivacyGoal,
Rewrite,
configure_logging,
)
configure_logging(LoggingConfig.default())
anonymizer = Anonymizer()
[11:14:00] [INFO] 🔧 Anonymizer initialized with 3 model configs
[11:14:00] [INFO] |-- 🔎 detector: gliner-pii-detector
[11:14:00] [INFO] |-- ✅ validator: gpt-oss-120b
[11:14:00] [INFO] |-- 🧩 augmenter: gpt-oss-120b
📦 Input data¶
- Same biographies dataset used in earlier notebooks -- familiar data makes it easy to compare rewrite output against replace output.
input_data = AnonymizerInput(
source="https://raw.githubusercontent.com/NVIDIA-NeMo/Anonymizer/refs/heads/main/docs/data/NVIDIA_synthetic_biographies.csv",
text_column="biography",
data_summary="Biographical profiles",
)
🎛️ Configure¶
PrivacyGoalspells out what to protect and what to preserve -- this gives the rewriter clear, domain-specific guidance.risk_tolerance(default"low") andmax_repair_iterations(default3) control the automated quality gate -- see Risk tolerance for presets.
config = AnonymizerConfig(
rewrite=Rewrite(
privacy_goal=PrivacyGoal(
protect="All direct identifiers and quasi-identifier combinations (names, locations, employers, dates)",
preserve="Career trajectory, educational background, and professional accomplishments",
),
risk_tolerance="low",
max_repair_iterations=3,
),
)
👁️ Preview¶
preview()runs on a small sample so you can iterate on privacy goals and evaluation criteria before committing to a full run.
preview = anonymizer.preview(
config=config,
data=input_data,
num_records=3,
)
preview.display_record(0)
[11:14:00] [INFO] 👀 Preview mode: 📂 Loaded 3 records from https://raw.githubusercontent.com/NVIDIA-NeMo/Anonymizer/refs/heads/main/docs/data/NVIDIA_synthetic_biographies.csv (column: 'biography')
[11:14:00] [INFO] 🔍 Running entity detection on 3 records
[11:14:00] [INFO] detection labels in scope: (default: 65 labels; see anonymizer.DEFAULT_ENTITY_LABELS for list)
[11:15:03] [INFO] |-- 📋 Detection complete — 79 entities found across 3 records (0 failed) [62.6s]
[11:15:03] [INFO] |-- labels: first_name=23, organization_name=8, age=5, occupation=5, city=4, state=4, degree=4, university=4, field_of_study=4, last_name=3, race_ethnicity=3, political_view=3, language=2, religious_belief=2, street_address=2, place_name=1, date_of_birth=1, employment_status=1
[11:15:03] [INFO] ✏️ Running rewrite pipeline
[11:19:15] [INFO] Evaluate-repair loop: all rows pass at iteration 0
[11:19:15] [INFO] |-- 📋 Rewrite complete (0 failed) [251.7s]
[11:19:15] [INFO] 🎉 Pipeline complete — 3 records processed, 0 total failures
preview.display_record(1)
How to interpret leakage: Leakage is measured against the sensitivity disposition. Details marked
leave_as_ismay remain without increasingleakage_mass. If an output retains something you expected the privacy goal to protect, inspect the Entity Disposition table.
🚀 Full run¶
result.dataframehas user-facing columns: rewritten text, scores, and the review flag.result.trace_dataframehas every intermediate column for debugging.
result = anonymizer.run(config=config, data=input_data)
result.dataframe.head()
[11:19:15] [INFO] 📂 Loaded 25 records from https://raw.githubusercontent.com/NVIDIA-NeMo/Anonymizer/refs/heads/main/docs/data/NVIDIA_synthetic_biographies.csv (column: 'biography')
[11:19:15] [INFO] 🔍 Running entity detection on 25 records
[11:19:15] [INFO] detection labels in scope: (default: 65 labels; see anonymizer.DEFAULT_ENTITY_LABELS for list)
[11:22:42] [INFO] |-- 📋 Detection complete — 672 entities found across 25 records (0 failed) [207.0s]
[11:22:42] [INFO] |-- labels: first_name=154, organization_name=63, occupation=47, city=41, field_of_study=38, university=36, race_ethnicity=30, last_name=27, age=27, degree=27, state=25, political_view=25, religious_belief=24, street_address=23, language=19, place_name=17, employment_status=11, county=10, date_of_birth=9, company_name=5, date=5, education_level=4, time=1, landmark=1, country=1, gender=1, postcode=1
[11:22:42] [INFO] ✏️ Running rewrite pipeline
[11:34:02] [INFO] Evaluate-repair loop iteration 0: 4/25 rows need repair
[11:34:44] [INFO] Evaluate-repair loop: all rows pass at iteration 1
[11:34:44] [INFO] |-- 📋 Rewrite complete (0 failed) [721.6s]
[11:34:44] [INFO] 🎉 Pipeline complete — 25 records processed, 0 total failures
| biography | biography_rewritten | utility_score | leakage_mass | weighted_leakage_rate | any_high_leaked | needs_human_review | |
|---|---|---|---|---|---|---|---|
| 0 | Bobby Watford, a 40‑year‑old Mexican veterinar... | Ethan Kline, a 40‑year‑old Mexican veterinaria... | 0.918182 | 0.0 | 0.0 | False | False |
| 1 | Jodi Allison, 36, lives at 204 Bluegrass in Cl... | Claire Harper, in her mid‑30s, lives at 317 Ma... | 0.99375 | 0.0 | 0.0 | False | False |
| 2 | James Mills is a 69‑year‑old paramedic who liv... | Victor Harper is a man in his late 60s who wor... | 0.966667 | 0.0 | 0.0 | False | False |
| 3 | Nancy Burton is a 21‑year‑old cashier who live... | Emily Hawkins is in her early twenties and wor... | 0.98 | 0.297 | 0.026757 | False | False |
| 4 | Cheryl Gray is a 33‑year‑old historian living ... | Denise Bennett is a historian in her late 30s,... | 0.933333 | 0.0 | 0.0 | False | False |
result.dataframe[["biography_rewritten", "utility_score", "leakage_mass", "needs_human_review"]].head()
| biography_rewritten | utility_score | leakage_mass | needs_human_review | |
|---|---|---|---|---|
| 0 | Ethan Kline, a 40‑year‑old Mexican veterinaria... | 0.918182 | 0.0 | False |
| 1 | Claire Harper, in her mid‑30s, lives at 317 Ma... | 0.99375 | 0.0 | False |
| 2 | Victor Harper is a man in his late 60s who wor... | 0.966667 | 0.0 | False |
| 3 | Emily Hawkins is in her early twenties and wor... | 0.98 | 0.297 | False |
| 4 | Denise Bennett is a historian in her late 30s,... | 0.933333 | 0.0 | False |
result.trace_dataframe.columns.tolist()
['biography', '_anonymizer_record_id', '_raw_detected_entities', '_seed_entities', '_tag_notation', '_seed_validation_candidates', '_seed_tagged_text', '_validated_entities', '_seed_entities_json', '_initial_tagged_text', '_validated_seed_entities', '_augmented_entities', '_merged_entities', '_merged_tagged_text', '_validation_candidates', '_detected_entities', 'biography_with_spans', '_latent_entities', 'final_entities', '_entities_by_value', '_replacement_map', '_replacement_map_source', '_domain', '_domain_supplement', '_domain_supplement_privacy', '_sensitivity_disposition', '_privacy_qa', '_rewrite_disposition_block', '_sensitivity_disposition_block', '_replacement_map_for_prompt', '_full_rewrite', 'biography_rewritten', '_meaning_units', '_meaning_units_serialized', '_quality_qa', '_repair_iterations', '_privacy_qa_reanswer', '_quality_qa_reanswer', '_quality_qa_compare', 'utility_score', 'leakage_mass', 'weighted_leakage_rate', 'any_high_leaked', '_needs_repair', '_leaked_privacy_items', '_rewritten_text__next', 'needs_human_review']
🚩 Filter by review flag¶
- Records where automated metrics exceed thresholds are flagged for manual review.
needs_human_reviewis threshold-based, so a record can have small nonzero leakage without being flagged.- Use this to prioritize human attention on the records that need it most.
- See Working with flagged records for guidance on diagnosing and resolving flagged records.
df = result.dataframe
flagged = df[df["needs_human_review"] == True] # noqa: E712
print(f"{len(flagged)} of {len(df)} records flagged for human review")
flagged.head()
0 of 25 records flagged for human review
| biography | biography_rewritten | utility_score | leakage_mass | weighted_leakage_rate | any_high_leaked | needs_human_review |
|---|
🔬 Evaluate (optional)¶
Call evaluate() to run LLM-as-judge scoring on the rewrite result — detection validity and three quality rubrics (privacy, quality, style).
Evaluation makes additional LLM calls per record. For larger datasets, evaluate
a preview first; this tutorial evaluates all 25 rows to demonstrate the complete workflow.
This holistic judge is independent of pipeline leakage scoring, so their assessments may differ.
See Evaluation for details.
evaluated = anonymizer.evaluate(result)
evaluated.display_record(0)
⏭️ Next steps¶
- ⚖️ Rewriting Legal Documents -- rewrite legal text with custom entity labels and domain-specific privacy goals.
- 📊 Evaluation -- learn about the detection validity and rewrite quality judges in detail.
- 🎯 Choosing a Replacement Strategy -- compare Redact, Annotate, Hash, and Substitute if you prefer token-level replacement.
- 🔍 Inspecting Detected Entities -- debug what the detection pipeline found before rewriting.