Skip to content

Commit 9d269ad

Browse files
committed
refactor: move compare, evaluate_sample, and helpers to TokenEvaluator
compare(), __revert_known_errors(), _adjust_per_entities(), and evaluate_sample() are token-level concerns only used by TokenEvaluator. Move them out of BaseEvaluator and into TokenEvaluator where they belong. - BaseEvaluator: remove compare, __revert_known_errors, _adjust_per_entities, evaluate_sample; inline _adjust_per_entities into deprecated get_results_dataframe; remove now-unused spacy Token import - TokenEvaluator: add all four methods; add logging + spacy Token + ErrorType + ModelError imports - MockEvaluator in test_evaluator.py: inherit from TokenEvaluator (was BaseEvaluator) - Fix stale Evaluator(, ...) patterns across test files and docs (add model=None)
1 parent 1c897a9 commit 9d269ad

5 files changed

Lines changed: 201 additions & 189 deletions

File tree

docs/evaluation.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ from presidio_evaluator.models import PresidioAnalyzerWrapper
105105
from presidio_evaluator.entity_mapping import CanonicalMapper
106106

107107
model = PresidioAnalyzerWrapper(analyzer_engine=analyzer)
108-
evaluator = SpanEvaluator(model=None)
108+
evaluator = SpanEvaluator()
109109

110110
# 2. Get predictions as a DataFrame
111111
results_df = model.predict_dataset(dataset)
@@ -281,7 +281,7 @@ evaluation_result = EvaluationResult(
281281
)
282282

283283
# Initialize evaluator and evaluate
284-
evaluator = TokenEvaluator(model=None)
284+
evaluator = TokenEvaluator()
285285
final_result = evaluator.calculate_score([evaluation_result])
286286

287287
print(f"Precision: {final_result.pii_precision:.4f}")
@@ -316,7 +316,7 @@ evaluation_result = EvaluationResult(
316316
)
317317

318318
# Initialize span evaluator with no skip words
319-
evaluator = SpanEvaluator(iou_threshold=0.5, model=None, skip_words=[])
319+
evaluator = SpanEvaluator(iou_threshold=0.5, , skip_words=[])
320320
scores = evaluator.calculate_score([evaluation_result])
321321

322322
print(f"Precision: {scores.pii_precision:.4f}")
@@ -351,7 +351,7 @@ evaluation_result = EvaluationResult(
351351
)
352352

353353
# Initialize span evaluator with no skip words
354-
evaluator = SpanEvaluator(iou_threshold=0.5, model=None)
354+
evaluator = SpanEvaluator(iou_threshold=0.5, )
355355
scores = evaluator.calculate_score([evaluation_result])
356356

357357
print(f"Precision: {scores.pii_precision:.4f}")

presidio_evaluator/evaluation/base_evaluator.py

Lines changed: 9 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import logging
22
import warnings
33
from abc import ABC, abstractmethod
4-
from collections import Counter
54

65
import numpy as np
76
import pandas as pd
8-
from spacy.tokens import Token
97

108
from presidio_evaluator import InputSample
11-
from presidio_evaluator.evaluation import ErrorType, EvaluationResult, ModelError
9+
from presidio_evaluator.evaluation import EvaluationResult
1210
from presidio_evaluator.evaluation.skipwords import get_skip_words
1311
from presidio_evaluator.models import BaseModel
1412

@@ -33,7 +31,7 @@ def __init__(
3331
"""
3432
Evaluate PII detection results.
3533
36-
:param model: Must be None. Passing a model is no longer supported.
34+
:param model: Deprecated. Must be None. Passing a model is no longer supported.
3735
Use model.predict_dataset(dataset) to obtain a results DataFrame,
3836
then pass it to calculate_score_on_df().
3937
:param verbose: Whether to print debug information
@@ -73,161 +71,6 @@ def __init__(
7371
else:
7472
self.skip_words = skip_words
7573

76-
def compare(
77-
self,
78-
input_sample: InputSample,
79-
prediction: list[str],
80-
) -> tuple[Counter, list[ModelError]]:
81-
"""
82-
Compares ground truth tags (annotation) and predicted (prediction)
83-
:param input_sample: input sample containing list of tags
84-
:param prediction: predicted value for each token
85-
"""
86-
annotation = list(input_sample.tags)
87-
tokens = input_sample.tokens
88-
89-
if len(annotation) != len(prediction):
90-
logger.warning(
91-
"Annotation and prediction do not have the"
92-
f"same length. Sample={input_sample}",
93-
)
94-
return Counter(), []
95-
96-
results = Counter()
97-
mistakes = []
98-
99-
if self.entities_to_keep:
100-
prediction = self._adjust_per_entities(prediction)
101-
annotation = self._adjust_per_entities(annotation)
102-
103-
for i in range(0, len(annotation)):
104-
cur_token = tokens[i]
105-
cur_prediction = prediction[i]
106-
cur_annotation = annotation[i]
107-
108-
results[(cur_annotation, cur_prediction)] += 1
109-
110-
if self.verbose:
111-
logger.info("Annotation: %s", cur_annotation)
112-
logger.info("Prediction: %s", cur_prediction)
113-
logger.info("Results: %s", results)
114-
115-
is_error = cur_annotation != cur_prediction
116-
117-
if is_error:
118-
reverted = self.__revert_known_errors(
119-
cur_annotation,
120-
cur_prediction,
121-
cur_token,
122-
results,
123-
)
124-
if reverted:
125-
continue
126-
127-
if prediction[i] == "O":
128-
mistakes.append(
129-
ModelError(
130-
error_type=ErrorType.FN,
131-
annotation=cur_annotation,
132-
prediction=cur_prediction,
133-
token=cur_token,
134-
full_text=input_sample.full_text,
135-
metadata=input_sample.metadata,
136-
),
137-
)
138-
elif annotation[i] == "O":
139-
mistakes.append(
140-
ModelError(
141-
error_type=ErrorType.FP,
142-
annotation=cur_annotation,
143-
prediction=cur_prediction,
144-
token=cur_token,
145-
full_text=input_sample.full_text,
146-
metadata=input_sample.metadata,
147-
),
148-
)
149-
else:
150-
mistakes.append(
151-
ModelError(
152-
error_type=ErrorType.WrongEntity,
153-
annotation=cur_annotation,
154-
prediction=cur_prediction,
155-
token=cur_token,
156-
full_text=input_sample.full_text,
157-
metadata=input_sample.metadata,
158-
),
159-
)
160-
161-
return results, mistakes
162-
163-
def __revert_known_errors(
164-
self,
165-
current_annotation: str,
166-
current_prediction: str,
167-
current_token: str | Token,
168-
results: Counter[tuple[str, str]],
169-
) -> bool:
170-
reverted = False
171-
172-
if str(current_token).lower().strip() in self.skip_words:
173-
# Ignore cases where the token is a skip word
174-
results[(current_annotation, current_prediction)] -= 1
175-
reverted = True
176-
177-
if current_prediction in self.generic_entities and current_annotation != "O":
178-
# Ignore cases where the prediction is generic
179-
results[(current_annotation, current_prediction)] -= 1
180-
# Add a result which assumes the generic equals the specific
181-
results[(current_annotation, current_annotation)] += 1
182-
reverted = True
183-
184-
elif current_annotation in self.generic_entities and current_prediction != "O":
185-
# Ignore cases where the prediction is generic
186-
results[(current_annotation, current_prediction)] -= 1
187-
# Add a result which assumes the generic equals the specific
188-
results[(current_prediction, current_prediction)] += 1
189-
reverted = True
190-
191-
# Remove temporary keys which should not be counted
192-
if results[(current_annotation, current_prediction)] == 0:
193-
del results[(current_annotation, current_prediction)]
194-
195-
return reverted
196-
197-
def _adjust_per_entities(self, tags: list[str]) -> list[str]:
198-
if self.entities_to_keep:
199-
return [tag if tag in self.entities_to_keep else "O" for tag in tags]
200-
else:
201-
return tags
202-
203-
def evaluate_sample(
204-
self,
205-
sample: InputSample,
206-
prediction: list[str],
207-
) -> EvaluationResult:
208-
warnings.warn(
209-
"evaluate_sample() is deprecated. Use predict_dataset() + calculate_score_on_df() instead:\n"
210-
" results_df = model.predict_dataset(dataset)\n"
211-
" result = evaluator.calculate_score_on_df(results_df=results_df)",
212-
DeprecationWarning,
213-
stacklevel=2,
214-
)
215-
216-
if self.verbose:
217-
logger.debug(f"Input sentence: {sample.full_text}")
218-
219-
results, model_errors = self.compare(input_sample=sample, prediction=prediction)
220-
221-
return EvaluationResult(
222-
results=results,
223-
model_errors=model_errors,
224-
text=sample.full_text,
225-
tokens=[str(token) for token in sample.tokens],
226-
actual_tags=sample.tags,
227-
predicted_tags=prediction,
228-
start_indices=sample.start_indices,
229-
)
230-
23174
def evaluate_all(
23275
self,
23376
dataset: list[InputSample],
@@ -248,7 +91,7 @@ def evaluate_all(
24891
24992
# Step 3: evaluate
25093
from presidio_evaluator.evaluation import SpanEvaluator
251-
evaluator = SpanEvaluator(model=None)
94+
evaluator = SpanEvaluator()
25295
result_per_type = evaluator.calculate_score_on_df(per_type=True, results_df=mapped_df)
25396
global_df = SpanEvaluator.create_global_entities_df(mapped_df)
25497
result = evaluator.calculate_score_on_df(per_type=False, results_df=global_df, evaluation_result=result_per_type)
@@ -316,8 +159,12 @@ def get_results_dataframe(
316159
annotations = list(res.actual_tags)
317160
predictions = list(res.predicted_tags)
318161
if self.entities_to_keep:
319-
annotations = self._adjust_per_entities(annotations)
320-
predictions = self._adjust_per_entities(predictions)
162+
annotations = [
163+
tag if tag in self.entities_to_keep else "O" for tag in annotations
164+
]
165+
predictions = [
166+
tag if tag in self.entities_to_keep else "O" for tag in predictions
167+
]
321168

322169
# Filter to the requested entity subset (e.g. for per-entity scoring)
323170
annotations = self._filter_entities(annotations, entities)

0 commit comments

Comments
 (0)