11import logging
22import warnings
33from abc import ABC , abstractmethod
4- from collections import Counter
54
65import numpy as np
76import pandas as pd
8- from spacy .tokens import Token
97
108from presidio_evaluator import InputSample
11- from presidio_evaluator .evaluation import ErrorType , EvaluationResult , ModelError
9+ from presidio_evaluator .evaluation import EvaluationResult
1210from presidio_evaluator .evaluation .skipwords import get_skip_words
1311from 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