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)Four LLM judges run per record: one that scores detection quality and three that score replacement quality (Substitute mode only).
"Are the detected entities actually correct (value, label) pairs in context?"
This judge runs regardless of which replace mode was used. It looks at 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[
[
"detection_valid",
"type_fidelity_valid",
"attribute_fidelity_valid",
"relational_consistency_valid",
]
]Use trace_dataframe for the full internal trace including raw judge outputs.
All four 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 four roles are detection_validity_judge, replace_type_fidelity_judge, replace_attribute_fidelity_judge, and replace_relational_consistency_judge.
# my_models.yaml
selected_models:
evaluate:
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 detection judge and three holistic quality rubrics.
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 Detection Judge above. 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 the detection validity fraction and all three judge rubrics alongside the rewritten text:
evaluated.display_record(0)For a tabular overview across all records:
evaluated.dataframe[["detection_valid", "judge_evaluation"]]Use trace_dataframe for the full internal trace including raw judge outputs.
The rewrite quality judge defaults to nemotron-30b-thinking. 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:
detection_validity_judge: your-model-alias
rewrite_judge: your-model-alias