Anonymizer provides LLM-as-judge evaluation for both modes, replace and rewrite, but they work differently:
| Mode | How evaluation runs |
|---|---|
| Replace | Post-hoc, via a separate Anonymizer.evaluate() call after run() / preview(). |
| Rewrite | Automatic leakage/utility scoring runs as part of every run() / preview() call. A separate Anonymizer.evaluate() call adds LLM-as-judge quality scoring. |
Replace evaluation is optional and post-hoc β you call Anonymizer.evaluate() on a result from run() or preview():
from anonymizer import Anonymizer, AnonymizerConfig, AnonymizerInput, Substitute
anonymizer = Anonymizer()
cfg = AnonymizerConfig(replace=Substitute())
src = AnonymizerInput(source="data.csv", text_column="text")
result = anonymizer.run(config=cfg, data=src)
evaluated = anonymizer.evaluate(result)
evaluated.display_record(0)Both run() and preview() results can be saved and evaluated in a separate session:
import pickle
preview = anonymizer.preview(config=cfg, data=src, num_records=15)
with open("/tmp/preview.pkl", "wb") as f:
pickle.dump(preview, f)
# β¦ later β¦
with open("/tmp/preview.pkl", "rb") as f:
loaded = pickle.load(f)
evaluated = anonymizer.evaluate(loaded)The active judges depend on the anonymization strategy and EvaluateConfig:
| Evaluation | Applies to |
|---|---|
| Entity Coverage | All anonymization strategies |
| Detection Validity | Optional; enable with EvaluateConfig(compute_detection_validity=True) |
| Type Fidelity, Attribute Fidelity, Relational Consistency | Substitute mode only |
"Which sensitive values in the original text did the anonymizer fail to detect?"
Entity coverage is the primary judge-anchored recall metric and always runs. The judge scans the original text independently to extract all in-scope PII candidates, without knowledge of what the anonymizer detected. After scope and literal-span filtering, candidates are deduplicated by normalized value. A deterministic postprocessing step then classifies each unique candidate value as covered (also detected by the anonymizer) or missed (not detected). The score is n_covered / (n_covered + n_missed) β recall over the judge's unique candidate values.
Note: the judge measures detection recall, not output leakage. A value detected but replaced with a poor substitute still scores as covered. Extra anonymizer detections do not directly enter the numerator or denominator, though a detection that matches a judge candidate under the value-matching rules classifies that candidate as covered. Matching is value-based; detection validity separately evaluates whether detected value-label pairs are correct.
The judge is scoped and contextualized by the same signals used during anonymization:
entity_labelsβ the detection taxonomy in scope; the judge only reports values whose type falls within it.data_summaryβ used purely to interpret literal values and their semantic types, never to invent entities absent from the text.
| Output column | Type | Description |
|---|---|---|
entity_coverage |
float | None |
n_covered / (n_covered + n_missed) β fraction of the judge's unique candidate values that the anonymizer detected. 1.0 means no missed entities; None if the judge was unavailable. |
missed_entities |
list |
Each unique candidate value not detected by the anonymizer, with its value, judge-assigned label, and one-sentence reasoning. Empty when no entities were missed. |
Special values:
| Scenario | entity_coverage |
missed_entities |
|---|---|---|
| Judge found no PII candidates in the text | 1.0 β No candidates found by the judge |
[] |
| Judge found candidates and anonymizer caught all | 1.0 |
[] |
| Judge found candidates and anonymizer missed some | 0β1 fraction | populated |
| Judge call failed or returned a malformed response | None |
[] |
"Are the detected entities actually correct (value, label) pairs in context?"
Detection validity is disabled by default. To enable it, set compute_detection_validity=True in EvaluateConfig. This metric is intended for internal model and threshold evaluation. It examines each detected span and flags:
- false_positive β the span is not actually identifying or sensitive in this context (common word, generic phrase, boilerplate).
- wrong_label β the span is sensitive but the label sits in a clearly different domain (e.g. a company name labeled
first_name). Sibling labels within the same broad domain are treated as valid. - not_in_text β the literal value does not appear in the original text.
- wrong_boundary β the span is a clear partial or over-extended capture (omits part of the actual value, or absorbs surrounding function words). Descriptive words in natural prose around a bare entity value are not a boundary error.
- contextual_mismatch β the span refers to something other than the labeled entity type in this context (e.g. "Apple" as fruit labeled
company_name).
| Output column | Type | Description |
|---|---|---|
detection_valid |
bool | None |
True if all detections pass; None if the judge was unavailable. |
detection_invalid_entities |
list |
Each flagged detection with value, label, and one-sentence reasoning. |
Special values:
| Scenario | detection_valid |
Display | Log |
|---|---|---|---|
| No entities detected in this record | True |
Satisfied | INFO: "N passthrough row(s) have no detected entities β detection_valid set to True (trivially valid)" |
| Judge ran and all detections passed | True |
Satisfied | β |
| Judge ran and flagged one or more detections | False |
Not Satisfied / Partially Satisfied | β |
| Judge call failed or returned a malformed response | None |
Unavailable | β |
When the source result used the Substitute mode, three additional LLM judges run in parallel β one per quality dimension.
"Does each synthetic value still belong to the same entity class and match the expected format for that class?"
The judge checks that replacements are shape-compatible with their originals β same granularity and character class β anchored by what the original itself looks like. It does not check semantic attributes (gender, age bucket) or cross-entity consistency; those are separate metrics.
| Output column | Type | Description |
|---|---|---|
type_fidelity_valid |
bool | None |
True if all replacements pass; None if the judge was unavailable. |
type_fidelity_invalid_replacements |
list |
Each failing replacement with label, original, synthetic, and reasoning. |
"Does each synthetic value preserve the salient within-entity attributes of the original?"
The judge checks two attributes:
- Gender of name β applies to
first_name,last_name,user_name. Only checked when the original name clearly implies a gender. Adjacent or ambiguous cases pass. - Age bucket β applies to
ageanddate_of_birth. Buckets: child (0β12), teen (13β19), young adult (20β29), adult (30β44), middle-aged (45β64), senior (65+). Adjacent buckets pass; only clear flips (adult β child) fail.
All other labels are outside the scope of this metric.
| Output column | Type | Description |
|---|---|---|
attribute_fidelity_valid |
bool | None |
True if all checked attributes pass; None if unavailable. |
attribute_fidelity_invalid_entities |
list |
Each failing entity with attributes checked and reasoning. |
"Do the synthetic entities preserve the same relational coherence with each other that the originals had?"
The judge inspects cross-entity relationships within a record β for example, whether a synthetic city is actually located in the synthetic state, or whether a synthetic date of birth is consistent with a synthetic age. Records with no checkable relationships always pass.
Relationships inspected include geographic pairings (city β state, city β postcode), temporal coherence (date of birth β age), and nameβemail alignment.
| Output column | Type | Description |
|---|---|---|
relational_consistency_valid |
bool | None |
True if all relations pass; None if unavailable. |
relational_consistency_invalid_relations |
list |
Each failing relation with participants and reasoning. |
display_record() renders a formatted per-record view that includes all four judge verdicts alongside the replacement map:
evaluated.display_record(0)For a tabular overview across all records:
evaluated.dataframe[
[
"entity_coverage",
"type_fidelity_valid",
"attribute_fidelity_valid",
"relational_consistency_valid",
# "detection_valid" β present only if compute_detection_validity=True
]
]Use trace_dataframe for the full internal trace including raw judge outputs.
The entity coverage judge defaults to nemotron-super; the other replace-evaluation judges default to gpt-oss-120b. Defaults are defined in evaluate.yaml. Override them by passing a model_configs YAML to Anonymizer(model_configs=...) β see Models for the full override pattern.
The roles are entity_coverage_judge, detection_validity_judge, replace_type_fidelity_judge, replace_attribute_fidelity_judge, and replace_relational_consistency_judge.
# my_models.yaml
selected_models:
evaluate:
entity_coverage_judge: your-model-alias
detection_validity_judge: your-model-alias
replace_type_fidelity_judge: your-model-alias
replace_attribute_fidelity_judge: your-model-alias
replace_relational_consistency_judge: your-model-aliasRewrite evaluation has two layers:
-
Automatic (always runs) β leakage mass, utility score, weighted leakage rate, and
needs_human_revieware computed as part of everyrun()/preview()call. See Rewrite for the repair loop and output columns. -
Post-hoc LLM judges (optional) β call
Anonymizer.evaluate()on a completed rewrite result to add the entity coverage judge (always), three holistic quality rubrics (always), and the detection validity judge (only withEvaluateConfig(compute_detection_validity=True)).
from anonymizer import Anonymizer, AnonymizerConfig, AnonymizerInput, Rewrite
anonymizer = Anonymizer()
cfg = AnonymizerConfig(rewrite=Rewrite())
src = AnonymizerInput(source="data.csv", text_column="text")
result = anonymizer.run(config=cfg, data=src)
evaluated = anonymizer.evaluate(result)
evaluated.display_record(0)Both run() and preview() results can be saved and evaluated in a separate session:
import pickle
preview = anonymizer.preview(config=cfg, data=src, num_records=15)
with open("/tmp/preview.pkl", "wb") as f:
pickle.dump(preview, f)
# β¦ later β¦
with open("/tmp/preview.pkl", "rb") as f:
loaded = pickle.load(f)
evaluated = anonymizer.evaluate(loaded)Same judge as in replace mode β see Entity Coverage above. It always runs and emits entity_coverage and missed_entities.
Same judge as in replace mode β see Entity Detection Judge above, and like there it is opt-in via EvaluateConfig(compute_detection_validity=True) (off by default). In rewrite mode, detection_valid is returned as a 0β1 fraction (the share of detected entities that passed), rather than a boolean. A value of 1.0 means all detections are valid; lower values mean more entities were flagged β the value itself is the fraction that passed.
| Output column | Type | Description |
|---|---|---|
detection_valid |
float | None |
1.0 if all detections pass; fraction of valid entities otherwise; None if the score is unavailable. |
detection_invalid_entities |
list |
Each flagged detection with value, label, and one-sentence reasoning. |
Special values:
| Scenario | detection_valid |
Display | Log |
|---|---|---|---|
| No entities detected in this record | 1.0 |
1.00 | INFO: "N passthrough row(s) have no detected entities β detection_valid set to 1.0 (trivially valid)" |
| Judge ran and all detections passed | 1.0 |
1.00 | β |
| Judge ran and flagged one or more detections | 0β1 fraction | numeric score | β |
| Judge call failed or entity data unreadable | None |
Unavailable | WARNING: "Could not parse entities_by_value to compute detection_valid fraction" |
Three rubrics evaluate the holistic quality of the rewritten text. All three run as a single LLM judge call and are stored together under judge_evaluation.
"Does the rewritten text adequately remove linkage risk to the original record?"
Scores residual linkage risk after the rewrite β comparing rewritten values to originals, distinguishing direct identifiers from quasi-identifiers, and assessing whether remaining details narrow the candidate set of plausible matches.
| Score | Meaning |
|---|---|
high |
Original direct identifiers removed; remaining quasi-identifiers create low linkage risk. |
medium |
No obvious direct identifiers remain, but a distinctive quasi-identifier bundle creates noticeable linkage risk. |
low |
Easily or near-certainly linkable β direct identifiers remain or enough detail survives that re-identification requires minimal effort. |
"How well does the rewritten text preserve important meaning, facts, and structure?"
Evaluates content preservation independent of privacy and style. Changes made for privacy reasons are not penalized when the core meaning is intact.
| Score | Meaning |
|---|---|
high |
Important meaning, facts, and structure fully preserved. |
medium |
Most content preserved; minor details lost or slightly distorted. |
low |
Material loss of important information, contradictions, or distorted core meaning. |
"Does the rewritten text read as fluent, coherent, and human-written prose?"
Evaluates readability, grammatical correctness, clarity, and phrasing β independent of content changes.
| Score | Meaning |
|---|---|
high |
Fluent, coherent, human-written prose. |
medium |
Mostly readable; isolated awkward phrasing or stiff transitions. |
low |
Noticeably unnatural; broken grammar, placeholder-like language, or machine-generated feel. |
The three rubric scores are stored together under the judge_evaluation column as a dict:
# Example judge_evaluation value for a single record
{
"privacy": {"score": "high", "reasoning": "All direct identifiers removed..."},
"quality": {"score": "medium", "reasoning": "Key facts preserved but some details lost..."},
"style": {"score": "high", "reasoning": "Reads naturally throughout..."},
}display_record() renders a formatted per-record view that includes entity coverage, all three judge rubrics, and (when enabled) the detection validity fraction alongside the rewritten text:
evaluated.display_record(0)For a tabular overview across all records:
evaluated.dataframe[["entity_coverage", "judge_evaluation"]]
# add "detection_valid" if you evaluated with compute_detection_validity=TrueUse trace_dataframe for the full internal trace including raw judge outputs.
The rewrite quality judge defaults to nemotron-30b-thinking and the entity coverage judge to nemotron-super. The detection validity judge shares the detection_validity_judge role used by replace evaluation. Defaults are defined in evaluate.yaml. Override them via model_configs:
# my_models.yaml
selected_models:
evaluate:
entity_coverage_judge: your-model-alias
detection_validity_judge: your-model-alias
rewrite_judge: your-model-alias