Skip to content

5 Evaluate Custom Presidio Analyzer

Evaluate a custom Presidio Analyzer using the Presidio Evaluator framework

This notebook demonstrates how to evaluate a Presidio instance using the presidio-evaluator framework. It builds upon notebook 4, customising the PresidioAnalyzer instance for better accuracy. For more on customising Presidio, see the Presidio Analyzer documentation.

Steps: 1. Load dataset 2. Dataset statistics 3. Define the custom Presidio Analyzer 4. Run predictions 5. Review and adjust entity mapping 6. Evaluate 7. Results and error analysis

# install presidio evaluator via pip if not yet installed

#!pip install presidio-evaluator
#!pip install "presidio-analyzer[transformers]"
import json
import warnings
from collections import Counter
from pathlib import Path
from pprint import pprint

warnings.filterwarnings('ignore')

import pandas as pd
from presidio_analyzer import (
    AnalyzerEngine,
    Pattern,
    PatternRecognizer,
    RecognizerRegistry,
)
from presidio_analyzer.context_aware_enhancers import LemmaContextAwareEnhancer
from presidio_analyzer.nlp_engine import SlimSpacyNlpEngine
from presidio_analyzer.predefined_recognizers.ner import HuggingFaceNerRecognizer

from presidio_evaluator import InputSample
from presidio_evaluator.entity_mapping import CanonicalMapper
from presidio_evaluator.evaluation import ModelError, Plotter, SpanEvaluator
from presidio_evaluator.experiment_tracking import get_experiment_tracker
from presidio_evaluator.models import PresidioAnalyzerWrapper

pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
pd.set_option('display.max_colwidth', None)

%reload_ext autoreload
%autoreload 2
%matplotlib inline

1. Load dataset from file

dataset_name = "synth_dataset_v2.json"
dataset = InputSample.read_dataset_json(Path(Path.cwd().parent, "data", dataset_name))

print(len(dataset))
tokenizing input:   0%|          | 0/1500 [00:00<?, ?it/s]

loading model en_core_web_sm


tokenizing input: 100%|██████████| 1500/1500 [00:04<00:00, 302.00it/s]

1500

This dataset was auto generated. See more info here Synthetic data generation.

def get_entity_counts(dataset: list[InputSample]) -> dict:
    """Return a dictionary with counter per entity type."""
    entity_counter = Counter()
    for sample in dataset:
        for tag in sample.tags:
            entity_counter[tag] += 1
    return entity_counter

2. Simple dataset statistics

entity_counts = get_entity_counts(dataset)
print("Count per entity:")
pprint(entity_counts.most_common(), compact=True)

print(
    "\nMin and max number of tokens in dataset: "
    f"Min: {min([len(sample.tokens) for sample in dataset])}, "
    f"Max: {max([len(sample.tokens) for sample in dataset])}"
)

print(
    f"Min and max sentence length in dataset: "
    f"Min: {min([len(sample.full_text) for sample in dataset])}, "
    f"Max: {max([len(sample.full_text) for sample in dataset])}"
)
Count per entity:
[('O', 19626), ('STREET_ADDRESS', 3071), ('PERSON', 1369), ('GPE', 521),
 ('ORGANIZATION', 504), ('PHONE_NUMBER', 350), ('DATE_TIME', 219),
 ('TITLE', 142), ('CREDIT_CARD', 136), ('US_SSN', 80), ('AGE', 74), ('NRP', 55),
 ('ZIP_CODE', 50), ('EMAIL_ADDRESS', 49), ('DOMAIN_NAME', 37),
 ('IP_ADDRESS', 22), ('IBAN_CODE', 21), ('US_DRIVER_LICENSE', 9)]

Min and max number of tokens in dataset: Min: 3, Max: 78
Min and max sentence length in dataset: Min: 9, Max: 407

3. Define the AnalyzerEngine object

In this case, we customize the AnalyzerEngine to use a different NER model, some custom recognizers and the context aware enhancer.

3.1 Set up the NlpEngine and Hugging Face NER Recognizer

The NLP engine uses spaCy for text processing (lemmas, tokens, etc.). The Hugging Face model is added as a direct recognizer (HuggingFaceNerRecognizer) instead of being part of the NLP engine. This approach gives better control over model predictions.

# Defin a slim Nlp Engine (we're using a Presidio recognizer instead of a Presidio NLP Engine)
nlp_engine = SlimSpacyNlpEngine()
nlp_engine.load()
# # Map OpenMed model entities to Presidio entities
mapping = {
    # Personal Info - Names & Demographics
    "first_name": "FIRST_NAME",
    "last_name": "LAST_NAME",
    "age": "AGE",
    "gender": "GENDER",
    "date_of_birth": "DATE_OF_BIRTH",
    "blood_type": "BLOOD_TYPE",
    "occupation": "OCCUPATION",
    "education_level": "EDUCATION_LEVEL",
    "employment_status": "EMPLOYMENT_STATUS",
    "language": "LANGUAGE",
    "race_ethnicity": "ETHNICITY",
    "sexuality": "SEXUALITY",
    "political_view": "POLITICAL_VIEW",
    "religious_belief": "RELIGIOUS_BELIEF",
    "biometric_identifier": "BIOMETRIC_IDENTIFIER",
    "pin": "ID",
    # Identifiers (20+ types)
    "account_number": "ACCOUNT_NUMBER",  # FINANCIAL_PII branch (bank/IBAN accounts), not generic ID
    "customer_id": "ID",
    "employee_id": "ID",
    "unique_id": "ID",
    "bank_routing_number": "US_BANK_NUMBER",
    "swift_bic": "SWIFT_CODE",
    "certificate_license_number": "PROFESSIONAL_LICENSE",  # GOVERNMENT_ID branch (matches US_DRIVER_LICENSE), not PHI
    "medical_record_number": "MEDICAL_LICENSE",
    "health_plan_beneficiary_number": "ID",
    "credit_debit_card": "CREDIT_CARD",
    "cvv": "CVV",
    "ssn": "US_SSN",
    "tax_id": "ID",
    "license_plate": "LICENSE_PLATE",
    "vehicle_identifier": "ID",
    "mac_address": "ID",
    "device_identifier": "ID",
    # Authentication & Security
    "password": "PASSWORD",
    "user_name": "USER_NAME",
    # Contact Info (4 types)
    "email": "EMAIL_ADDRESS",
    "phone_number": "PHONE_NUMBER",
    "fax_number": "PHONE_NUMBER",
    "url": "URL",
    # Location (6 types)
    "city": "CITY",
    "country": "COUNTRY",
    "county": "COUNTY",
    "state": "STATE",
    "street_address": "STREET_ADDRESS",
    "coordinate": "COORDINATE",
    "postcode": "ZIP_CODE",
    # Network Info
    "ipv4": "IP_ADDRESS",
    "ipv6": "IP_ADDRESS",
    # Temporal (3 types)
    "date": "DATE_TIME",
    "date_time": "DATE_TIME",
    "time": "DATE_TIME",
    # Organization (1 type)
    "company_name": "COMPANY",
}

3.2 Set up the relevant recognizers

Add and remove recognizers to fit the dataset in hand. Removing all the recognizers that don't map to entities in our dataset.

ner_recognizer = HuggingFaceNerRecognizer(
        model_name="OpenMed/OpenMed-PII-SuperClinical-Large-434M-v1",
        label_mapping=mapping,
        aggregation_strategy="first"
    )

# another option: OpenMed/OpenMed-PII-BioClinicalModern-Base-149M-v1

def get_titles_recognizer():
    titles_recognizer = PatternRecognizer(
        deny_list=["Mr.", "Mrs.", "Ms.", "Miss", "Dr.", "Prof."],
        supported_entity="TITLE",
        name="TitlesRecognizer",
    )
    return titles_recognizer


def get_years_recognizer():
    years_recognizer = PatternRecognizer(
        patterns=[Pattern("YEAR", r"\b(19|20)\d{2}\b", score=0.1)],
        supported_entity="DATE_TIME",
        name="YearsRecognizer",
        context=["year", "at", "date", "in", "on"],
    )
    return years_recognizer


def get_age_recognizer():
    weak_regex = r"\b(110|[1-9]?[0-9])\b"
    age_pattern = Pattern(name="age (very weak)", regex=weak_regex, score=0.01)
    age_recognizer = PatternRecognizer(
        supported_entity="AGE",
        patterns=[age_pattern],
        name="AgeRecognizer",
        context=["month", "old", "turn", "age", "y/o"],
    )
    return age_recognizer


# Create Recognizer Registry
registry = RecognizerRegistry()
registry.load_predefined_recognizers(nlp_engine=nlp_engine)

# Add custom pattern recognizers
registry.add_recognizer(ner_recognizer)
registry.add_recognizer(get_titles_recognizer())
registry.add_recognizer(get_years_recognizer())
registry.add_recognizer(get_age_recognizer())

# Remove unnecessary recognizers from presidio
unnecessary = [
    "NhsRecognizer",
    "UkNinoRecognizer",
    "SgFinRecognizer",
    "AuAbnRecognizer",
    "AuAcnRecognizer",
    "AuTfnRecognizer",
    "AuMedicareRecognizer",
    "InPanRecognizer",
    "InAadhaarRecognizer",
    "InVehicleRegistrationRecognizer",
    "InPassportRecognizer",
    "InVoterRecognizer",
    "CryptoRecognizer",
    "SpacyRecognizer",
]

for rec in unnecessary:
    registry.remove_recognizer(rec)
Device set to use cpu

3.3 Configure the context mechanism

Configure the LemmaContextAawareEnhancer which uses surrounding words to increase confidence in detection

# Set up the context aware enhancer
context_enhancer = LemmaContextAwareEnhancer(
    context_prefix_count=10, context_suffix_count=10
)

3.4 Create the AnalyzerEngine object

# Set up the engine and other PII recognizers
analyzer_engine = AnalyzerEngine(
    nlp_engine=nlp_engine,
    context_aware_enhancer=context_enhancer,
    registry=registry,
    default_score_threshold=0.3,
)

# analyzer_engine = AnalyzerEngine()

pprint("Supported entities for English:")
pprint(analyzer_engine.get_supported_entities("en"), compact=True)

print("\nLoaded recognizers for English:")
pprint(
    [
        rec.name
        for rec in analyzer_engine.registry.get_recognizers("en", all_fields=True)
    ],
    compact=True,
)

print("\nLoaded Context Aware Enhancer:")
print(analyzer_engine.context_aware_enhancer.__class__.__name__)
pprint(json.dumps(analyzer_engine.context_aware_enhancer.__dict__), compact=True)
'Supported entities for English:'
['US_DRIVER_LICENSE', 'SEXUALITY', 'MAC_ADDRESS', 'MEDICAL_LICENSE', 'US_ITIN',
 'COORDINATE', 'LANGUAGE', 'SWIFT_CODE', 'ACCOUNT_NUMBER', 'URL',
 'PHONE_NUMBER', 'COUNTY', 'FIRST_NAME', 'LICENSE_PLATE', 'US_SSN',
 'EDUCATION_LEVEL', 'DATE_TIME', 'COMPANY', 'IP_ADDRESS', 'STATE', 'BLOOD_TYPE',
 'COUNTRY', 'OCCUPATION', 'GENDER', 'EMAIL_ADDRESS', 'PROFESSIONAL_LICENSE',
 'PASSWORD', 'USER_NAME', 'POLITICAL_VIEW', 'RELIGIOUS_BELIEF', 'ID',
 'IBAN_CODE', 'CITY', 'TITLE', 'CREDIT_CARD', 'STREET_ADDRESS', 'US_PASSPORT',
 'EMPLOYMENT_STATUS', 'AGE', 'BIOMETRIC_IDENTIFIER', 'DATE_OF_BIRTH',
 'ETHNICITY', 'LAST_NAME', 'CVV', 'ZIP_CODE', 'US_BANK_NUMBER']

Loaded recognizers for English:
['CreditCardRecognizer', 'UsBankRecognizer', 'UsLicenseRecognizer',
 'UsItinRecognizer', 'UsPassportRecognizer', 'UsSsnRecognizer',
 'DateRecognizer', 'EmailRecognizer', 'IbanRecognizer', 'IpRecognizer',
 'MedicalLicenseRecognizer', 'MacAddressRecognizer', 'PhoneRecognizer',
 'UrlRecognizer', 'HuggingFaceNerRecognizer', 'TitlesRecognizer',
 'YearsRecognizer', 'AgeRecognizer']

Loaded Context Aware Enhancer:
LemmaContextAwareEnhancer
('{"context_similarity_factor": 0.35, "min_score_with_context_similarity": '
 '0.4, "context_prefix_count": 10, "context_suffix_count": 10, '
 '"context_matching_mode": "substring"}')
# Test Analyzer
text = "Yesterday in Mt. Sinai AP: Dana Silver, 79 years old female was complaining of stomach pain. Her ID is 154555"
res = analyzer_engine.analyze(text=text, language="en", return_decision_process=True)
for result in res:
    print(f"Entity: {result.entity_type}, Text: {text[result.start : result.end]}")
Asking to truncate to max_length but no maximum length is provided and the model has no predefined maximum length. Default to no truncation.


Entity: FIRST_NAME, Text:  Dana
Entity: LAST_NAME, Text:  Silver,
Entity: AGE, Text:  79
Entity: CITY, Text:  Mt. Sinai
Entity: ID, Text:  154555
Entity: GENDER, Text:  female
# Wrap the analyzer
wrapped_analyzer = PresidioAnalyzerWrapper(
    analyzer_engine=analyzer_engine,
    score_threshold=analyzer_engine.default_score_threshold,
    language="en",
)

4. Run predictions

Run the model on the full dataset. Returns a 5-column DataFrame (sentence_id, token, annotation, prediction, start_indices).

results_df = wrapped_analyzer.predict_dataset(dataset)
results_df.head()
sentence_id token annotation prediction start_indices
0 0 The O O 0
1 0 address O O 4
2 0 of O O 12
3 0 Persint ORGANIZATION FIRST_NAME 15
4 0 is O O 23

5. Review entity mapping

  • The entity mapping process auto-resolves labels using a hierarchical entity tree. The CanonicalMapper class analyzes the results (predictions and annotations), and checks if there are any gaps towards the evaluation process.

  • If you call render_html() to inspect issues, you'd see if there are any gaps that prevent you from completing the evaluation successfully. Make sure you address all the issues by using:

  • mapper.map({"LABEL": "CANONICAL"}) to remap or
  • mapper.map({"LABEL": None}) to suppress.

  • For more information about entity mapping, see Notebook 6 - Entity Mapping.

# Analyze the results DataFrame.
# min_collision_count suppresses COLLISION_CROSS_BRANCH warnings with fewer
# cross-branch token co-occurrences than the threshold (rare label overlaps).
mapper = CanonicalMapper()
mapper.analyze(results_df, min_collision_count=5)
mapper.render_html()

Entity Mapping Audit 37 labels identified

✓ Ready for evaluation
Unresolved: 0Cross-branch: 1Pred-only: 3Dataset-only: 0Same-branch: 6

1. Blocking issuesnone

Labels that could not be mapped automatically (ERROR). Resolve these before calling mapper.get_mapped_results_dataframe().

✓ No blocking issues. All labels are fully resolved.

2. Warnings4 item(s)

Non-blocking warnings (WARNING). These won't prevent evaluation but may affect results.

WARNING  ID
This prediction label co-occurs on the same tokens with annotation labels from a different hierarchy branch that the model never covers. The model predicts this label where a cross-branch entity is annotated, and it never predicts anything on that annotation's branch — so the conflict can't be resolved by hierarchical evaluation.
mapper.map({'ID': 'ADDRESS'}) — Remap 'ID' to 'ADDRESS' (11 token co-occurrences with 'STREET_ADDRESS')
3 more option(s)
  • mapper.map({'ID': 'FINANCIAL'}) — Remap 'ID' to 'FINANCIAL' (8 token co-occurrences with 'CREDIT_CARD')
  • mapper.map({'ID': 'PHONE_NUMBER'}) — Remap 'ID' to 'PHONE_NUMBER' (1 token co-occurrences with 'PHONE_NUMBER')
  • mapper.map({'ID': None}) — Suppress 'ID' from evaluation
WARNING  OCCUPATION
The model predicts this entity type, but the dataset never annotates it. Every prediction would count as a false positive with no possible true positives which would make precision zero and recall undefined. Consider ignoring this label if you don't want it to affect your metrics.
mapper.map({'OCCUPATION': None}) — Suppress 'JOB_TITLE' from evaluation
WARNING  BIOMETRIC_IDENTIFIER
The model predicts this entity type, but the dataset never annotates it. Every prediction would count as a false positive with no possible true positives which would make precision zero and recall undefined. Consider ignoring this label if you don't want it to affect your metrics.
mapper.map({'BIOMETRIC_IDENTIFIER': None}) — Suppress 'FACE' from evaluation
WARNING  PASSWORD
The model predicts this entity type, but the dataset never annotates it. Every prediction would count as a false positive with no possible true positives which would make precision zero and recall undefined. Consider ignoring this label if you don't want it to affect your metrics.
mapper.map({'PASSWORD': None}) — Suppress 'PASSWORD' from evaluation

3. Annotation labels17 label(s) from your dataset

The entity labels in your ground-truth annotations.

Exact matchFuzzy / country match / manualCountry fallback (catch-all)UnresolvedSuppressed
LabelResolved asTokens
STREET_ADDRESSADDRESS3071
PERSONPERSON1369
GPEGPE521
ORGANIZATIONORGANIZATION504
PHONE_NUMBERPHONE_NUMBER350
DATE_TIMEDATE_TIME219
TITLETITLE142
CREDIT_CARDFINANCIAL136
US_SSNSSN80
AGEAGE74
NRPNATIONALITY55
ZIP_CODEADDRESS50
EMAIL_ADDRESSEMAIL_ADDRESS49
DOMAIN_NAMEDOMAIN37
IP_ADDRESSIP_ADDRESS22
IBAN_CODEFINANCIAL21
US_DRIVER_LICENSEDRIVER_LICENSE9

4. Prediction labels31 label(s) from the model

The entity labels your model outputs.

Exact matchFuzzy / country match / manualCountry fallback (catch-all)UnresolvedSuppressed
LabelResolved asTokens
STREET_ADDRESSADDRESS2549
LAST_NAMENAME1079
FIRST_NAMENAME790
CITYADDRESS449
PHONE_NUMBERPHONE_NUMBER415
COMPANYCOMPANY329
CREDIT_CARDFINANCIAL257
ZIP_CODEADDRESS252
COUNTRYADDRESS219
DATE_TIMEDATE_TIME212
OCCUPATIONJOB_TITLE176
STATEADDRESS151
AGEAGE138
EMAIL_ADDRESSEMAIL_ADDRESS97
US_SSNSSN87
TITLETITLE68
DATE_OF_BIRTHBIRTH_DATE62
IDID61
USER_NAMEUSERNAME51
ETHNICITYETHNICITY33
ACCOUNT_NUMBERFINANCIAL29
IP_ADDRESSIP_ADDRESS28
URLURL26
IBAN_CODEFINANCIAL21
GENDERGENDER14
POLITICAL_VIEWPOLITICAL_AFFILIATION12
PROFESSIONAL_LICENSEPROFESSIONAL_LICENSE9
LANGUAGELANGUAGE4
COUNTYADDRESS3
PASSWORDPASSWORD1
BIOMETRIC_IDENTIFIERFACE1
# Suppress all those entities that the model detects but the dataset doesn't have annotations for (prediction-only labels)
mapper.suppress_prediction_only()
# Resolve all blocking (WARNING+) issues. Adjust mappings for your model's labels.
# PREDICTION_ONLY and COLLISION_CROSS_BRANCH are suppressed here — remap if appropriate.
resolutions = {}
for issue in mapper.get_issues():
    if issue.severity.value in ("warning", "error"):
        for lbl in issue.labels:
            resolutions[lbl] = None  # suppress — adjust as needed

if resolutions:
    mapper.map(resolutions)

mapped_results = mapper.get_mapped_results_dataframe()
mapper.render_html()

Entity Mapping Audit 37 labels identified

✓ Ready for evaluation
Unresolved: 0Cross-branch: 0Pred-only: 0Dataset-only: 0Same-branch: 6

1. Blocking issuesnone

Labels that could not be mapped automatically (ERROR). Resolve these before calling mapper.get_mapped_results_dataframe().

✓ No blocking issues. All labels are fully resolved.

2. Warningsnone

Non-blocking warnings (WARNING). These won't prevent evaluation but may affect results.

No warnings.

3. Annotation labels17 label(s) from your dataset

The entity labels in your ground-truth annotations.

Exact matchFuzzy / country match / manualCountry fallback (catch-all)UnresolvedSuppressed
LabelResolved asTokens
STREET_ADDRESSADDRESS3071
PERSONPERSON1369
GPEGPE521
ORGANIZATIONORGANIZATION504
PHONE_NUMBERPHONE_NUMBER350
DATE_TIMEDATE_TIME219
TITLETITLE142
CREDIT_CARDFINANCIAL136
US_SSNSSN80
AGEAGE74
NRPNATIONALITY55
ZIP_CODEADDRESS50
EMAIL_ADDRESSEMAIL_ADDRESS49
DOMAIN_NAMEDOMAIN37
IP_ADDRESSIP_ADDRESS22
IBAN_CODEFINANCIAL21
US_DRIVER_LICENSEDRIVER_LICENSE9

4. Prediction labels31 label(s) from the model

The entity labels your model outputs.

Exact matchFuzzy / country match / manualCountry fallback (catch-all)UnresolvedSuppressed
LabelResolved asTokens
STREET_ADDRESSADDRESS2549
LAST_NAMENAME1079
FIRST_NAMENAME790
CITYADDRESS449
PHONE_NUMBERPHONE_NUMBER415
COMPANYCOMPANY329
CREDIT_CARDFINANCIAL257
ZIP_CODEADDRESS252
COUNTRYADDRESS219
DATE_TIMEDATE_TIME212
STATEADDRESS151
AGEAGE138
EMAIL_ADDRESSEMAIL_ADDRESS97
US_SSNSSN87
TITLETITLE68
DATE_OF_BIRTHBIRTH_DATE62
USER_NAMEUSERNAME51
ETHNICITYETHNICITY33
ACCOUNT_NUMBERFINANCIAL29
IP_ADDRESSIP_ADDRESS28
URLURL26
IBAN_CODEFINANCIAL21
GENDERGENDER14
POLITICAL_VIEWPOLITICAL_AFFILIATION12
PROFESSIONAL_LICENSEPROFESSIONAL_LICENSE9
LANGUAGELANGUAGE4
COUNTYADDRESS3
OCCUPATIONunresolved176
IDunresolved61
PASSWORDunresolved1
BIOMETRIC_IDENTIFIERunresolved1

6. Evaluate

# Set up the experiment tracker and log model + dataset params
experiment = get_experiment_tracker()
params = {"dataset_name": dataset_name, "model_name": wrapped_analyzer.name}
params.update(wrapped_analyzer.to_log())
experiment.log_parameters(params)
experiment.log_dataset_hash(dataset)
experiment.log_parameter("entity_mappings", json.dumps(mapper.get_mapping()))

evaluator = SpanEvaluator(iou_threshold=0.75)
f_beta = 2  # F-beta parameter used throughout evaluation
skip words not provided, using default skip words. If you want the evaluation to not use skip words, pass skip_words=[]
hierarchical_results = evaluator.calculate_hierarchical_scores(
    mapped_results,
    beta=f_beta
    )
binary_results = hierarchical_results["binary"]
branch_results = hierarchical_results["branch"]
detailed_results = hierarchical_results["detailed"]

# Log detailed-level metrics to experiment tracker
experiment.log_metrics(detailed_results.to_log())
entities, confmatrix = detailed_results.to_confusion_matrix()
experiment.log_confusion_matrix(matrix=confmatrix, labels=entities)
experiment.end()

# A. Binary PII/O metrics
print("Level: binary (PII vs O):")
pprint({
    "Precision": round(binary_results.pii_precision, 3),
    "Recall":    round(binary_results.pii_recall, 3),
    f"F{f_beta}":  round(binary_results.pii_f, 3),
})
saving experiment data to /presidio-research/notebooks/experiment_20260723-102549.json
Level: binary (PII vs O):
{'F2': 0.91, 'Precision': 0.921, 'Recall': 0.907}
# B. Branch-level per-entity scores (PERSON, LOCATION, DATE_TIME, …)
branch_plotter = Plotter(
    results=branch_results,
    model_name=wrapped_analyzer.name,
    display_mode="interactive", 
    beta=f_beta,
)
branch_plotter.plot_scores(include_pii_aggregate=False, annotation_entities_only=True)

png

png

png

# C. Detailed-level per-entity scores (NAME, GPE, PHONE_NUMBER, …)
detailed_plotter = Plotter(
    results=detailed_results,
    model_name=wrapped_analyzer.name,
    display_mode="interactive",
    beta=f_beta,
)
detailed_plotter.plot_scores(include_pii_aggregate=False, annotation_entities_only=True)

png

png

png

7. Confusion matrix

# D. Branch-level confusion matrix (only branch entities that appear in the data)
entities, confmatrix = branch_results.to_confusion_matrix()
branch_plotter.plot_confusion_matrix(entities=entities, confmatrix=confmatrix)

png

8. Error analysis

The ModelError class groups similar errors together, finding all the false positive and false negative predictions where the model's behavior is consistent.

This can help in detecting patterns in the model's performance, such as: - Consistent labeling of specific words or patterns
- Systematic overprediction or underprediction

Note: All errors are displayed using the mapped model entity types (e.g., LOCATION, PERSON) rather than the original dataset entity types (e.g., STREET_ADDRESS, GPE). This ensures consistency with how the model was evaluated.

branch_plotter.plot_most_common_tokens()

png

png

8a. False positives

Most common false positive tokens:

ModelError.most_common_fp_tokens(branch_results.model_errors)
Most common false positive tokens:
[('8 16', 9),
 ('greenlander', 8),
 ('russian', 6),
 ('501(c)3', 5),
 ('antall google antall 3369', 4),
 ('ex zombie', 4),
 ('spanish', 4),
 ('american', 4),
 ('palermo cityscan marta palermo 1072', 4),
 ('lantos fritz armstrong tzigane lantos 76556', 4)]
---------------
Example sentence with each FP token:
    - 8 16 (`8 16` pred as DEMOGRAPHIC)
    - Greenlander (`greenlander` pred as DEMOGRAPHIC)
    - Russian (`russian` pred as LOCATION)
    - 501(c)3 (`501(c)3` pred as DEMOGRAPHIC)
    - Antall 
 > Google Antall 
 > 3369 (`antall google antall 3369` pred as PERSON)
    - ex - Zombie (`ex zombie` pred as DEMOGRAPHIC)
    - Spanish (`spanish` pred as LOCATION)
    - American (`american` pred as LOCATION)
    - Palermo 
 > Cityscan 
 > Marta Palermo 
 > 1072 (`palermo cityscan marta palermo 1072` pred as PERSON)
    - Lantos 
 > Fritz - Armstrong 
 > Tzigane Lantos 
 > 76556 (`lantos fritz armstrong tzigane lantos 76556` pred as PERSON)





[('8 16', 9),
 ('greenlander', 8),
 ('russian', 6),
 ('501(c)3', 5),
 ('antall google antall 3369', 4),
 ('ex zombie', 4),
 ('spanish', 4),
 ('american', 4),
 ('palermo cityscan marta palermo 1072', 4),
 ('lantos fritz armstrong tzigane lantos 76556', 4)]

More FP analysis

fps_df = ModelError.get_fps_dataframe(branch_results.model_errors, entity="PERSON")
fps_df[["full_text", "token", "annotation", "prediction"]].head(20)
full_text token annotation prediction
0 Persint persint O PERSON
1 Mary ? mary O PERSON
2 William Hughes \n\n 20789 william hughes 20789 O PERSON
3 William Hughes \n\n 20789 william hughes 20789 O PERSON
4 Avalara avalara O PERSON
5 UPTON SCUDAMORE upton scudamore O PERSON
6 Janka Szász \n\n Network janka szász network O PERSON
7 Marie Langrová , marie langrová O PERSON
8 Davis davis O PERSON
9 Gregory Kudryashov \n\n 233 gregory kudryashov 233 O PERSON
10 0605 πεντέλης 0605 πεντέλης O PERSON
11 Kelly ? kelly O PERSON
12 Tomomi Hawkins , Richardson Santana tomomi hawkins richardson santana O PERSON
13 Tomomi Hawkins , Richardson Santana tomomi hawkins richardson santana O PERSON
14 Toshimi Arata \n Phone : toshimi arata phone O PERSON
15 Evidera evidera O PERSON
16 Stephan Urner \n\n 4844 stephan urner 4844 O PERSON
17 Stephan Urner \n\n 4844 stephan urner 4844 O PERSON
18 Peerj peerj O PERSON
19 Maureen Thibault \n\n 41086 maureen thibault 41086 O PERSON

8b. False negatives (FN)

Most common false negative examples + a few samples with FN

ModelError.most_common_fn_tokens(branch_results.model_errors, n=15)
Most common false negative tokens:
[('greenlander', 7),
 ('russian', 6),
 ('american', 5),
 ('spanish', 4),
 ('saturday', 3),
 ('owens duran oneal', 3),
 ('allianz', 3),
 ('czech', 3),
 ('swedish', 3),
 ('persint', 2),
 ('avalara', 2),
 ('upton scudamore', 2),
 ('evidera', 2),
 ('peerj', 2),
 ('0494 92 82 32', 2)]
---------------
Example sentence with each FN token:
    - Greenlander (`greenlander` annotated as LOCATION)
    - Russian (`russian` annotated as DEMOGRAPHIC)
    - American (`american` annotated as DEMOGRAPHIC)
    - Spanish (`spanish` annotated as DEMOGRAPHIC)
    - Saturday (`saturday` annotated as DATE_TIME)
    - owens , duran and oneal (`owens duran oneal` annotated as ORGANIZATION)
    - Allianz (`allianz` annotated as ORGANIZATION)
    - Czech (`czech` annotated as DEMOGRAPHIC)
    - Swedish (`swedish` annotated as DEMOGRAPHIC)
    - Persint (`persint` annotated as ORGANIZATION)
    - Avalara (`avalara` annotated as ORGANIZATION)
    - UPTON SCUDAMORE (`upton scudamore` annotated as LOCATION)
    - Evidera (`evidera` annotated as ORGANIZATION)
    - Peerj (`peerj` annotated as ORGANIZATION)
    - 0494 92 82 32 (`0494 92 82 32` annotated as CONTACT)





[('greenlander', 7),
 ('russian', 6),
 ('american', 5),
 ('spanish', 4),
 ('saturday', 3),
 ('owens duran oneal', 3),
 ('allianz', 3),
 ('czech', 3),
 ('swedish', 3),
 ('persint', 2),
 ('avalara', 2),
 ('upton scudamore', 2),
 ('evidera', 2),
 ('peerj', 2),
 ('0494 92 82 32', 2)]

More FN analysis

fns_df = ModelError.get_fns_dataframe(branch_results.model_errors, entity="PERSON")
fns_df[["full_text", "token", "annotation", "prediction"]].head(20)
full_text token annotation prediction
0 Ubul Nicole Mary John ubul nicole mary john PERSON O
1 William Hughes william hughes PERSON O
2 Meza meza PERSON O
3 Janka M. Szász Network and computer systems administrator janka m. szász network computer systems administrator PERSON O
4 Marie Langrová Packager marie langrová packager PERSON O
5 Halldór Matthew Kelly Melanie halldór matthew kelly melanie PERSON O
6 Tomomi tomomi PERSON O
7 Toshimi Arata toshimi arata PERSON O
8 Stephan M Urner stephan m urner PERSON O
9 Maureen Thibault maureen thibault PERSON O
10 Hijacinta Godina Technical writer hijacinta godina technical writer PERSON O
11 Marcela marcela PERSON O
12 Marisa Bisliev marisa bisliev PERSON O
13 Sieda Batukayev sieda batukayev PERSON O
14 Julieta S Almeida Staff development specialist julieta s almeida staff development specialist PERSON O
15 Dreux Joly dreux joly PERSON O
16 Philippe Guernon philippe guernon PERSON O
17 Rolling machine operator rolling machine operator PERSON O
18 Ella H. Snider ella h. snider PERSON O
19 Ella H. Snider ella h. snider PERSON O