mirror of
https://github.com/data-privacy-stack/presidio.git
synced 2026-09-21 05:27:53 -05:00
feat(analyzer): Add Healthcare identifiers recognizer (#2159)
This commit is contained in:
@@ -34,9 +34,15 @@ For more information, refer to the [adding new recognizers documentation](analyz
|
||||
|US_BANK_NUMBER|A US bank account number is between 8 to 17 digits.|Pattern match and context|
|
||||
|US_DRIVER_LICENSE|A US driver license according to <https://ntsi.com/drivers-license-format/>|Pattern match and context|
|
||||
|US_ITIN | US Individual Taxpayer Identification Number (ITIN). Nine digits that start with a "9" and contain a "7" or "8" as the 4 digit.|Pattern match and context|
|
||||
|US_CLAIM_NUMBER|A US healthcare claim identifier used in billing and claims processing.|Pattern match, context enhancement, and entity threshold|
|
||||
|US_HEALTH_INSURANCE_MEMBER_ID|A US health insurance member or subscriber identifier printed on an insurance card. Healthcare or insurance context increases detection confidence.|Pattern match, context enhancement, and entity threshold|
|
||||
|US_MBI|A US Medicare Beneficiary Identifier (MBI) with 11 alphanumeric characters.|Pattern match and context|
|
||||
|US_NPI|A US National Provider Identifier (NPI) is a 10-digit number issued to healthcare providers by CMS under HIPAA.|Pattern match, context and checksum|
|
||||
|US_PASSPORT |A US passport number with 9 digits.|Pattern match and context|
|
||||
|US_PRESCRIPTION_NUMBER|A US prescription or pharmacy order identifier.|Pattern match, context enhancement, and entity threshold|
|
||||
|US_PRIOR_AUTHORIZATION_NUMBER|A US prior authorization identifier used for treatment or drug approval requests.|Pattern match, context enhancement, and entity threshold|
|
||||
|US_PROVIDER_TAX_ID|A US provider organization tax identifier (TIN/EIN) used in healthcare billing workflows.|Pattern match, context enhancement, and entity threshold|
|
||||
|US_REFERRAL_NUMBER|A US healthcare referral identifier, including specialty or infusion referral numbers.|Pattern match, context enhancement, and entity threshold|
|
||||
|US_SSN|A US Social Security Number (SSN) with 9 digits.|Pattern match and context|
|
||||
|
||||
### UK
|
||||
|
||||
@@ -91,6 +91,48 @@ recognizers:
|
||||
enabled: false
|
||||
country_code: us
|
||||
|
||||
- name: UsHealthInsuranceMemberIdRecognizer
|
||||
supported_languages:
|
||||
- en
|
||||
type: predefined
|
||||
enabled: false
|
||||
country_code: us
|
||||
|
||||
- name: UsPriorAuthorizationNumberRecognizer
|
||||
supported_languages:
|
||||
- en
|
||||
type: predefined
|
||||
enabled: false
|
||||
country_code: us
|
||||
|
||||
- name: UsClaimNumberRecognizer
|
||||
supported_languages:
|
||||
- en
|
||||
type: predefined
|
||||
enabled: false
|
||||
country_code: us
|
||||
|
||||
- name: UsPrescriptionNumberRecognizer
|
||||
supported_languages:
|
||||
- en
|
||||
type: predefined
|
||||
enabled: false
|
||||
country_code: us
|
||||
|
||||
- name: UsReferralNumberRecognizer
|
||||
supported_languages:
|
||||
- en
|
||||
type: predefined
|
||||
enabled: false
|
||||
country_code: us
|
||||
|
||||
- name: UsProviderTaxIdRecognizer
|
||||
supported_languages:
|
||||
- en
|
||||
type: predefined
|
||||
enabled: false
|
||||
country_code: us
|
||||
|
||||
- name: NhsRecognizer
|
||||
supported_languages:
|
||||
- en
|
||||
|
||||
@@ -157,6 +157,16 @@ from .country_specific.us.aba_routing_recognizer import AbaRoutingRecognizer
|
||||
from .country_specific.us.medical_license_recognizer import MedicalLicenseRecognizer
|
||||
from .country_specific.us.us_bank_recognizer import UsBankRecognizer
|
||||
from .country_specific.us.us_driver_license_recognizer import UsLicenseRecognizer
|
||||
from .country_specific.us.us_health_insurance_member_id_recognizer import (
|
||||
UsHealthInsuranceMemberIdRecognizer,
|
||||
)
|
||||
from .country_specific.us.us_healthcare_admin_recognizers import (
|
||||
UsClaimNumberRecognizer,
|
||||
UsPrescriptionNumberRecognizer,
|
||||
UsPriorAuthorizationNumberRecognizer,
|
||||
UsProviderTaxIdRecognizer,
|
||||
UsReferralNumberRecognizer,
|
||||
)
|
||||
from .country_specific.us.us_itin_recognizer import UsItinRecognizer
|
||||
from .country_specific.us.us_mbi_recognizer import UsMbiRecognizer
|
||||
from .country_specific.us.us_npi_recognizer import UsNpiRecognizer
|
||||
@@ -229,11 +239,17 @@ __all__ = [
|
||||
"SgFinRecognizer",
|
||||
"UrlRecognizer",
|
||||
"UsBankRecognizer",
|
||||
"UsClaimNumberRecognizer",
|
||||
"UsHealthInsuranceMemberIdRecognizer",
|
||||
"UsItinRecognizer",
|
||||
"UsLicenseRecognizer",
|
||||
"UsMbiRecognizer",
|
||||
"UsNpiRecognizer",
|
||||
"UsPassportRecognizer",
|
||||
"UsPrescriptionNumberRecognizer",
|
||||
"UsPriorAuthorizationNumberRecognizer",
|
||||
"UsProviderTaxIdRecognizer",
|
||||
"UsReferralNumberRecognizer",
|
||||
"UsSsnRecognizer",
|
||||
"EsNifRecognizer",
|
||||
"SpacyRecognizer",
|
||||
|
||||
+16
@@ -4,6 +4,16 @@ from .aba_routing_recognizer import AbaRoutingRecognizer
|
||||
from .medical_license_recognizer import MedicalLicenseRecognizer
|
||||
from .us_bank_recognizer import UsBankRecognizer
|
||||
from .us_driver_license_recognizer import UsLicenseRecognizer
|
||||
from .us_health_insurance_member_id_recognizer import (
|
||||
UsHealthInsuranceMemberIdRecognizer,
|
||||
)
|
||||
from .us_healthcare_admin_recognizers import (
|
||||
UsClaimNumberRecognizer,
|
||||
UsPrescriptionNumberRecognizer,
|
||||
UsPriorAuthorizationNumberRecognizer,
|
||||
UsProviderTaxIdRecognizer,
|
||||
UsReferralNumberRecognizer,
|
||||
)
|
||||
from .us_itin_recognizer import UsItinRecognizer
|
||||
from .us_mbi_recognizer import UsMbiRecognizer
|
||||
from .us_npi_recognizer import UsNpiRecognizer
|
||||
@@ -15,9 +25,15 @@ __all__ = [
|
||||
"UsItinRecognizer",
|
||||
"UsBankRecognizer",
|
||||
"UsLicenseRecognizer",
|
||||
"UsClaimNumberRecognizer",
|
||||
"UsHealthInsuranceMemberIdRecognizer",
|
||||
"UsMbiRecognizer",
|
||||
"UsNpiRecognizer",
|
||||
"UsPassportRecognizer",
|
||||
"UsPrescriptionNumberRecognizer",
|
||||
"UsPriorAuthorizationNumberRecognizer",
|
||||
"UsProviderTaxIdRecognizer",
|
||||
"UsReferralNumberRecognizer",
|
||||
"AbaRoutingRecognizer",
|
||||
"UsSsnRecognizer",
|
||||
]
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
"""Recognizer for US health insurance member identifiers."""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from presidio_analyzer import Pattern, PatternRecognizer
|
||||
|
||||
|
||||
class UsHealthInsuranceMemberIdRecognizer(PatternRecognizer):
|
||||
"""Recognize US health insurance member/subscriber IDs with context.
|
||||
|
||||
US health insurance member identifiers are payer-specific and do not have a
|
||||
single universal checksum or format. To avoid broad matching of generic
|
||||
alphanumeric IDs, this recognizer requires both:
|
||||
- a plausible alphanumeric member ID pattern, and
|
||||
- nearby healthcare/insurance context.
|
||||
|
||||
CMS consumer guidance explicitly labels the payer-assigned member number on
|
||||
a sample insurance card. Medicaid T-MSIS defines MEMBER-ID as the value shown
|
||||
on the insurance carrier's card and permits up to 20 characters. These
|
||||
sources establish the identifier and upper bound, not a universal syntax;
|
||||
the default regex is therefore a conservative, replaceable heuristic.
|
||||
Presidio applies ``re.IGNORECASE`` through its default global regex flags,
|
||||
so the uppercase character classes also match lowercase and mixed-case IDs.
|
||||
|
||||
CMS card reference: https://www.cms.gov/files/document/11818-sample-insurance-card-english.pdf
|
||||
Medicaid data reference: https://www.medicaid.gov/tmsis/dataguide/v4/data-elements/tpl003036/
|
||||
|
||||
:param patterns: List of patterns to be used by this recognizer
|
||||
:param context: List of context words which increase detection confidence
|
||||
:param supported_language: Language this recognizer supports
|
||||
:param supported_entity: The entity this recognizer can detect
|
||||
"""
|
||||
|
||||
COUNTRY_CODE = "us"
|
||||
|
||||
PATTERNS = [
|
||||
Pattern(
|
||||
"Health insurance member ID (weak)",
|
||||
r"\b(?=[A-Z0-9-]{6,20}\b)(?=[A-Z0-9-]*[A-Z])"
|
||||
r"(?=[A-Z0-9-]*\d)[A-Z]{1,5}-?[A-Z0-9]{5,14}\b",
|
||||
0.1,
|
||||
),
|
||||
]
|
||||
|
||||
CONTEXT = [
|
||||
"member",
|
||||
"subscriber",
|
||||
"insurance",
|
||||
"policy",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Optional[List[Pattern]] = None,
|
||||
context: Optional[List[str]] = None,
|
||||
supported_language: str = "en",
|
||||
supported_entity: str = "US_HEALTH_INSURANCE_MEMBER_ID",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
patterns = patterns if patterns else self.PATTERNS
|
||||
context = context if context else self.CONTEXT
|
||||
super().__init__(
|
||||
supported_entity=supported_entity,
|
||||
patterns=patterns,
|
||||
context=context,
|
||||
supported_language=supported_language,
|
||||
name=name,
|
||||
)
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
"""Recognizers for US healthcare administrative identifiers."""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from presidio_analyzer import Pattern, PatternRecognizer
|
||||
|
||||
|
||||
class UsPriorAuthorizationNumberRecognizer(PatternRecognizer):
|
||||
"""Recognize US healthcare prior authorization numbers with context.
|
||||
|
||||
CMS identifies prior authorization and referral numbers as payer-assigned
|
||||
values. There is no universal US syntax. The primary pattern anchors a
|
||||
numeric identifier on its label, while a weak prefixed pattern supports
|
||||
structured data containing values such as ``PA-987654321``.
|
||||
|
||||
Reference: https://www.cms.gov/outreach-and-education/mln/wbt/mln4462429-mln-wbt-1500/1500/lesson04/18/index.html
|
||||
"""
|
||||
|
||||
COUNTRY_CODE = "us"
|
||||
|
||||
PATTERNS = [
|
||||
Pattern(
|
||||
"Prior authorization number (labelled)",
|
||||
r"(?<=\b(?:prior\s+authorization|prior\s+auth|preauthorization|"
|
||||
r"pre-auth|authorization)(?:\s*(?:#|no\.?|number|id)\s*:?\s*|"
|
||||
r"\s*:\s*|\s+))"
|
||||
r"(?:PA-?)?\d{6,12}\b",
|
||||
0.35,
|
||||
),
|
||||
Pattern(
|
||||
"Prior authorization number (weak prefixed)",
|
||||
r"\bPA-?\d{6,12}\b",
|
||||
0.1,
|
||||
),
|
||||
]
|
||||
|
||||
CONTEXT = [
|
||||
"authorization",
|
||||
"auth",
|
||||
"preauthorization",
|
||||
"approval",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Optional[List[Pattern]] = None,
|
||||
context: Optional[List[str]] = None,
|
||||
supported_language: str = "en",
|
||||
supported_entity: str = "US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
patterns = patterns if patterns else self.PATTERNS
|
||||
context = context if context else self.CONTEXT
|
||||
super().__init__(
|
||||
supported_entity=supported_entity,
|
||||
patterns=patterns,
|
||||
context=context,
|
||||
supported_language=supported_language,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
class UsClaimNumberRecognizer(PatternRecognizer):
|
||||
"""Recognize US healthcare claim numbers with billing/claims context.
|
||||
|
||||
CMS describes a claim number as the reference number shown on an
|
||||
explanation of benefits, but does not prescribe a universal syntax. The
|
||||
primary pattern anchors a numeric identifier on its claim label, while a
|
||||
weak prefixed pattern supports structured data containing ``CLM`` values.
|
||||
|
||||
Reference: https://www.cms.gov/medical-bill-rights/help/guides/explanation-of-benefits
|
||||
"""
|
||||
|
||||
COUNTRY_CODE = "us"
|
||||
|
||||
PATTERNS = [
|
||||
Pattern(
|
||||
"Claim number (labelled)",
|
||||
r"(?<=\b(?:claim|medical\s+claim|healthcare\s+claim)"
|
||||
r"(?:\s*(?:#|no\.?|number|id)\s*:?\s*|\s*:\s*|\s+))"
|
||||
r"(?:CLM-?)?\d{6,15}\b",
|
||||
0.35,
|
||||
),
|
||||
Pattern(
|
||||
"Claim number (weak prefixed)",
|
||||
r"\bCLM-?\d{6,15}\b",
|
||||
0.1,
|
||||
),
|
||||
]
|
||||
|
||||
CONTEXT = [
|
||||
"claim",
|
||||
"billing",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Optional[List[Pattern]] = None,
|
||||
context: Optional[List[str]] = None,
|
||||
supported_language: str = "en",
|
||||
supported_entity: str = "US_CLAIM_NUMBER",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
patterns = patterns if patterns else self.PATTERNS
|
||||
context = context if context else self.CONTEXT
|
||||
super().__init__(
|
||||
supported_entity=supported_entity,
|
||||
patterns=patterns,
|
||||
context=context,
|
||||
supported_language=supported_language,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
class UsPrescriptionNumberRecognizer(PatternRecognizer):
|
||||
"""Recognize US prescription numbers with pharmacy context.
|
||||
|
||||
CMS defines the prescription/service reference number as a pharmacy-assigned
|
||||
alphanumeric value. Because there is no universal syntax, the primary
|
||||
pattern anchors a numeric identifier on an ``Rx`` or ``prescription`` label.
|
||||
A weak prefixed pattern remains available for structured data.
|
||||
|
||||
Reference: https://www.cms.gov/files/document/cms-medicare-part-d-340b-repository-companion-guide-v-1.pdf
|
||||
"""
|
||||
|
||||
COUNTRY_CODE = "us"
|
||||
|
||||
PATTERNS = [
|
||||
Pattern(
|
||||
"Prescription number (Rx labelled)",
|
||||
r"(?<=\brx(?:\s*(?:#|no\.?|number|id)\s*:?\s*|\s*:\s*|\s+))"
|
||||
r"(?:RX-?)?\d{6,12}\b",
|
||||
0.6,
|
||||
),
|
||||
Pattern(
|
||||
"Prescription number (labelled)",
|
||||
r"(?<=\bprescription"
|
||||
r"(?:\s*(?:#|no\.?|number|id)\s*:?\s*|\s*:\s*|\s+))"
|
||||
r"(?:RX-?)?\d{6,12}\b",
|
||||
0.35,
|
||||
),
|
||||
Pattern(
|
||||
"Prescription number (weak prefixed)",
|
||||
r"\bRX-?\d{6,12}\b",
|
||||
0.1,
|
||||
),
|
||||
]
|
||||
|
||||
CONTEXT = [
|
||||
"prescription",
|
||||
"pharmacy",
|
||||
"medication",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Optional[List[Pattern]] = None,
|
||||
context: Optional[List[str]] = None,
|
||||
supported_language: str = "en",
|
||||
supported_entity: str = "US_PRESCRIPTION_NUMBER",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
patterns = patterns if patterns else self.PATTERNS
|
||||
context = context if context else self.CONTEXT
|
||||
super().__init__(
|
||||
supported_entity=supported_entity,
|
||||
patterns=patterns,
|
||||
context=context,
|
||||
supported_language=supported_language,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
class UsReferralNumberRecognizer(PatternRecognizer):
|
||||
"""Recognize US healthcare referral numbers with referral context.
|
||||
|
||||
CMS documents referral numbers as payer-assigned values reported in the same
|
||||
CMS-1500 field as prior authorization numbers. There is no universal syntax.
|
||||
The primary pattern anchors a numeric identifier on its referral label, and
|
||||
a weak prefixed pattern supports structured ``REF`` or ``INF`` values.
|
||||
|
||||
Reference: https://www.cms.gov/outreach-and-education/mln/wbt/mln4462429-mln-wbt-1500/1500/lesson04/18/index.html
|
||||
"""
|
||||
|
||||
COUNTRY_CODE = "us"
|
||||
|
||||
PATTERNS = [
|
||||
Pattern(
|
||||
"Referral number (labelled)",
|
||||
r"(?<=\b(?:referral|infusion\s+referral)"
|
||||
r"(?:\s*(?:#|no\.?|number|id)\s*:?\s*|\s*:\s*|\s+))"
|
||||
r"(?:(?:REF|INF)-?)?\d{6,12}\b",
|
||||
0.35,
|
||||
),
|
||||
Pattern(
|
||||
"Referral number (weak prefixed)",
|
||||
r"\b(?:REF|INF)-?\d{6,12}\b",
|
||||
0.1,
|
||||
),
|
||||
]
|
||||
|
||||
CONTEXT = [
|
||||
"referral",
|
||||
"infusion",
|
||||
"specialty",
|
||||
"referring",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Optional[List[Pattern]] = None,
|
||||
context: Optional[List[str]] = None,
|
||||
supported_language: str = "en",
|
||||
supported_entity: str = "US_REFERRAL_NUMBER",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
patterns = patterns if patterns else self.PATTERNS
|
||||
context = context if context else self.CONTEXT
|
||||
super().__init__(
|
||||
supported_entity=supported_entity,
|
||||
patterns=patterns,
|
||||
context=context,
|
||||
supported_language=supported_language,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
class UsProviderTaxIdRecognizer(PatternRecognizer):
|
||||
"""Recognize US provider TIN/EIN values with healthcare provider context.
|
||||
|
||||
CMS uses a provider's EIN or SSN as the billing provider tax ID. This
|
||||
recognizer intentionally matches only the IRS-defined EIN format and valid
|
||||
two-digit EIN prefixes to avoid treating SSNs as provider organization IDs.
|
||||
|
||||
CMS reference: https://www.cms.gov/outreach-and-education/mln/wbt/mln4462429-mln-wbt-1500/1500/lesson04/12/index.html
|
||||
IRS prefix reference: https://www.irs.gov/businesses/small-businesses-self-employed/valid-eins
|
||||
"""
|
||||
|
||||
COUNTRY_CODE = "us"
|
||||
|
||||
# The IRS prefix list excludes 00, 07-09, 17-19, 28-29, 49, 69-70,
|
||||
# 78-79, 89, and 96-97.
|
||||
VALID_EIN_PREFIX = (
|
||||
r"(?:0[1-6]|1[0-6]|2[0-7]|3[0-9]|4[0-8]|5[0-9]|6[0-8]|"
|
||||
r"7[1-7]|8[0-8]|9[0-5]|9[89])"
|
||||
)
|
||||
|
||||
PATTERNS = [
|
||||
Pattern(
|
||||
"Provider tax ID (labelled)",
|
||||
r"(?<=\b(?:(?:(?:billing|rendering|healthcare)\s+provider|"
|
||||
r"provider\s+organization|provider)\s+(?:tax\s*(?:id|number|"
|
||||
r"identification\s+number)|tin|ein)|billing\s+provider)"
|
||||
r"(?:\s*(?:#|no\.?|number|id)\s*:?\s*|\s*:\s*|\s+))"
|
||||
+ VALID_EIN_PREFIX
|
||||
+ r"-\d{7}\b",
|
||||
0.35,
|
||||
),
|
||||
Pattern(
|
||||
"Provider tax ID (weak valid EIN)",
|
||||
r"\b" + VALID_EIN_PREFIX + r"-\d{7}\b",
|
||||
0.1,
|
||||
),
|
||||
]
|
||||
|
||||
CONTEXT = [
|
||||
"tax",
|
||||
"tin",
|
||||
"ein",
|
||||
"billing",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Optional[List[Pattern]] = None,
|
||||
context: Optional[List[str]] = None,
|
||||
supported_language: str = "en",
|
||||
supported_entity: str = "US_PROVIDER_TAX_ID",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
patterns = patterns if patterns else self.PATTERNS
|
||||
context = context if context else self.CONTEXT
|
||||
super().__init__(
|
||||
supported_entity=supported_entity,
|
||||
patterns=patterns,
|
||||
context=context,
|
||||
supported_language=supported_language,
|
||||
name=name,
|
||||
)
|
||||
@@ -1,5 +1,9 @@
|
||||
from .nlp_engine_mock import NlpEngineMock
|
||||
from .app_tracer_mock import AppTracerMock
|
||||
from .nlp_engine_mock import NlpEngineMock
|
||||
from .recognizer_registry_mock import RecognizerRegistryMock
|
||||
|
||||
__all__ = ["NlpEngineMock", "AppTracerMock", "RecognizerRegistryMock"]
|
||||
__all__ = [
|
||||
"NlpEngineMock",
|
||||
"AppTracerMock",
|
||||
"RecognizerRegistryMock",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Iterable, Iterator, Tuple, Dict, List
|
||||
from typing import Dict, Iterable, Iterator, List, Tuple
|
||||
|
||||
from presidio_analyzer.nlp_engine import NlpEngine, NlpArtifacts
|
||||
from presidio_analyzer.nlp_engine import NlpArtifacts, NlpEngine
|
||||
|
||||
|
||||
class NlpEngineMock(NlpEngine):
|
||||
|
||||
@@ -166,7 +166,7 @@ def test_when_text_with_only_additional_context_lemma_based_context_enhancer_the
|
||||
results_with_additional_context[0].analysis_explanation.supportive_context_word
|
||||
== "driver"
|
||||
)
|
||||
assert results_with_additional_context[0].score == 0.6499999999999999
|
||||
assert results_with_additional_context[0].score == pytest.approx(0.65)
|
||||
|
||||
|
||||
def test_when_text_with_context_then_improves_score(
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import pytest
|
||||
from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
|
||||
from presidio_analyzer.predefined_recognizers import (
|
||||
UsHealthInsuranceMemberIdRecognizer,
|
||||
)
|
||||
|
||||
from tests import assert_result
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def recognizer():
|
||||
"""Return an instance of the US health insurance member ID recognizer."""
|
||||
return UsHealthInsuranceMemberIdRecognizer()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def entity():
|
||||
"""Return the US health insurance member ID entity name."""
|
||||
return "US_HEALTH_INSURANCE_MEMBER_ID"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def analyze_member_id(spacy_nlp_engine):
|
||||
"""Return a member ID analyzer using production spaCy tokenization."""
|
||||
|
||||
def analyze(text, recognizer, entity, score_threshold=0.4):
|
||||
registry = RecognizerRegistry()
|
||||
registry.add_recognizer(recognizer)
|
||||
analyzer = AnalyzerEngine(registry=registry, nlp_engine=spacy_nlp_engine)
|
||||
return analyzer.analyze(
|
||||
text=text,
|
||||
language="en",
|
||||
entities=[entity],
|
||||
score_threshold=score_threshold,
|
||||
)
|
||||
|
||||
return analyze
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, expected_positions",
|
||||
[
|
||||
# fmt: off
|
||||
("Member ID ABC123456789", ((10, 22),)),
|
||||
("member number ZX-987654321 appears on the card", ((14, 26),)),
|
||||
("Subscriber ID HPN12345A9 is active", ((14, 24),)),
|
||||
("Insurance ID BCBSM1234567 was verified", ((13, 25),)),
|
||||
("Insurance plan ID UHC-12345AB covers the visit", ((18, 29),)),
|
||||
("Plan member ID AET987654 for this policy", ((15, 24),)),
|
||||
("Policy ID CIGNA123456 belongs to the patient", ((10, 21),)),
|
||||
("The insurance card lists subscriber number K123456789", ((43, 53),)),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_when_member_id_has_context_then_detected(
|
||||
text, expected_positions, recognizer, entity, analyze_member_id
|
||||
):
|
||||
"""Test context raises plausible member IDs above the caller threshold."""
|
||||
results = analyze_member_id(text, recognizer, entity)
|
||||
results = sorted(results, key=lambda result: result.start)
|
||||
assert len(results) == len(expected_positions)
|
||||
for result, (start, end) in zip(results, expected_positions):
|
||||
assert result.entity_type == entity
|
||||
assert result.start == start
|
||||
assert result.end == end
|
||||
assert result.score == pytest.approx(0.45)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, expected_value",
|
||||
[
|
||||
("member id abc123456", "abc123456"),
|
||||
("MeMbEr Id AbC123456", "AbC123456"),
|
||||
("Subscriber ID zx-987654321.", "zx-987654321"),
|
||||
],
|
||||
)
|
||||
def test_member_id_matching_is_case_insensitive_and_ignores_trailing_punctuation(
|
||||
text, expected_value, recognizer, entity, analyze_member_id
|
||||
):
|
||||
"""Test casing and punctuation do not change a plausible member ID match."""
|
||||
results = analyze_member_id(text, recognizer, entity)
|
||||
start = text.index(expected_value)
|
||||
assert len(results) == 1
|
||||
assert results[0].entity_type == entity
|
||||
assert results[0].start == start
|
||||
assert results[0].end == start + len(expected_value)
|
||||
assert results[0].score == pytest.approx(0.45)
|
||||
|
||||
|
||||
def test_when_text_has_multiple_member_ids_then_all_are_detected(
|
||||
recognizer, entity, analyze_member_id
|
||||
):
|
||||
"""Test every contextual member ID in one input is returned."""
|
||||
text = "Member ID ABC123456 and subscriber ID ZX-987654321."
|
||||
expected_values = ["ABC123456", "ZX-987654321"]
|
||||
results = sorted(
|
||||
analyze_member_id(text, recognizer, entity),
|
||||
key=lambda result: result.start,
|
||||
)
|
||||
assert [text[result.start : result.end] for result in results] == expected_values
|
||||
assert all(result.score == pytest.approx(0.45) for result in results)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, expected_value",
|
||||
[
|
||||
("Member ID A12345", "A12345"),
|
||||
("Member ID ABCDE-12345678901234", "ABCDE-12345678901234"),
|
||||
],
|
||||
)
|
||||
def test_member_id_minimum_and_maximum_lengths_are_detected(
|
||||
text, expected_value, recognizer, entity, analyze_member_id
|
||||
):
|
||||
"""Test the documented 6-to-20-character member ID boundaries."""
|
||||
results = analyze_member_id(text, recognizer, entity)
|
||||
start = text.index(expected_value)
|
||||
assert len(results) == 1
|
||||
assert results[0].entity_type == entity
|
||||
assert results[0].start == start
|
||||
assert results[0].end == start + len(expected_value)
|
||||
assert results[0].score == pytest.approx(0.45)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"ABC123456789",
|
||||
"Please store HPN12345A9 in the table",
|
||||
"Order number ABC123456789 shipped yesterday",
|
||||
"Tracking number ZX-987654321 is in transit",
|
||||
"Case number HPN12345A9 is pending review",
|
||||
"Claim number BCBSM1234567 was denied",
|
||||
"covid19",
|
||||
"sha256",
|
||||
"iphone15pro",
|
||||
"rfc2119",
|
||||
"gpt4turbo",
|
||||
"ICD10CM123",
|
||||
"ABC-1234567",
|
||||
],
|
||||
)
|
||||
def test_when_member_id_lacks_insurance_context_then_below_threshold(
|
||||
text, recognizer, entity, analyze_member_id
|
||||
):
|
||||
"""Test pattern-only and unrelated-context values are suppressed."""
|
||||
assert analyze_member_id(text, recognizer, entity) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"Member ID 1234567890",
|
||||
"Subscriber ID A123",
|
||||
"Member ID ABCDE-123456789012345",
|
||||
],
|
||||
)
|
||||
def test_when_member_id_pattern_is_implausible_then_not_detected(
|
||||
text, recognizer, entity, analyze_member_id
|
||||
):
|
||||
"""Test numeric-only and short values do not match the base pattern."""
|
||||
assert (
|
||||
analyze_member_id(
|
||||
text,
|
||||
recognizer,
|
||||
entity,
|
||||
score_threshold=0,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_explicit_request_threshold_can_return_pattern_only_member_id(
|
||||
recognizer, entity, analyze_member_id
|
||||
):
|
||||
"""Test structured callers can opt into the raw pattern match."""
|
||||
text = "ABC123456789"
|
||||
results = analyze_member_id(
|
||||
text,
|
||||
recognizer,
|
||||
entity,
|
||||
score_threshold=0,
|
||||
)
|
||||
assert len(results) == 1
|
||||
assert_result(results[0], entity, 0, len(text), 0.1)
|
||||
|
||||
|
||||
def test_us_health_insurance_member_id_recognizer_metadata(recognizer, entity):
|
||||
"""Test entity metadata and context without a recognizer threshold."""
|
||||
assert recognizer.supported_entities == [entity]
|
||||
assert recognizer.supported_language == "en"
|
||||
assert recognizer.context == ["member", "subscriber", "insurance", "policy"]
|
||||
assert recognizer.patterns[0].name == "Health insurance member ID (weak)"
|
||||
assert recognizer.patterns[0].score == 0.1
|
||||
assert recognizer.score_thresholds == {}
|
||||
@@ -0,0 +1,641 @@
|
||||
import pytest
|
||||
from presidio_analyzer import AnalyzerEngine, PatternRecognizer, RecognizerRegistry
|
||||
from presidio_analyzer.predefined_recognizers import (
|
||||
UsClaimNumberRecognizer,
|
||||
UsPrescriptionNumberRecognizer,
|
||||
UsPriorAuthorizationNumberRecognizer,
|
||||
UsProviderTaxIdRecognizer,
|
||||
UsReferralNumberRecognizer,
|
||||
)
|
||||
|
||||
from tests import assert_result
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def analyze_with_recognizer(spacy_nlp_engine):
|
||||
"""Return an administrative ID analyzer using production spaCy tokenization."""
|
||||
|
||||
def analyze(text, entity, recognizer, score_threshold=0.6):
|
||||
registry = RecognizerRegistry()
|
||||
registry.add_recognizer(recognizer)
|
||||
analyzer = AnalyzerEngine(registry=registry, nlp_engine=spacy_nlp_engine)
|
||||
return analyzer.analyze(
|
||||
text=text,
|
||||
language="en",
|
||||
entities=[entity],
|
||||
score_threshold=score_threshold,
|
||||
)
|
||||
|
||||
return analyze
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text, expected_positions",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"Prior authorization PA-987654321 approved for treatment.",
|
||||
((20, 32),),
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Processed healthcare claim CLM456789123 was paid.",
|
||||
((27, 39),),
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Prescription number RX789456123 was filled by the pharmacy.",
|
||||
((20, 31),),
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"Infusion referral number INF2025001234 is ready for scheduling.",
|
||||
((25, 38),),
|
||||
),
|
||||
(
|
||||
UsProviderTaxIdRecognizer(),
|
||||
"US_PROVIDER_TAX_ID",
|
||||
"Provider Tax ID 12-3456789 belongs to the billing provider.",
|
||||
((16, 26),),
|
||||
),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_when_us_healthcare_admin_id_has_context_then_detected(
|
||||
recognizer, entity, text, expected_positions, analyze_with_recognizer
|
||||
):
|
||||
"""Test context enhancement raises matches above the caller threshold."""
|
||||
results = analyze_with_recognizer(text, entity, recognizer)
|
||||
results = sorted(results, key=lambda result: result.start)
|
||||
assert len(results) == len(expected_positions)
|
||||
for result, (start, end) in zip(results, expected_positions):
|
||||
assert_result(result, entity, start, end, 0.7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text, expected_value",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"pRiOr AuThOrIzAtIoN pa-123456",
|
||||
"pa-123456",
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"cLaIm clm123456",
|
||||
"clm123456",
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"pReScRiPtIoN rX123456",
|
||||
"rX123456",
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"rEfErRaL inf123456",
|
||||
"inf123456",
|
||||
),
|
||||
(
|
||||
UsProviderTaxIdRecognizer(),
|
||||
"US_PROVIDER_TAX_ID",
|
||||
"bIlLiNg PrOvIdEr eIn: 12-3456789",
|
||||
"12-3456789",
|
||||
),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_admin_id_matching_is_case_insensitive(
|
||||
recognizer, entity, text, expected_value, analyze_with_recognizer
|
||||
):
|
||||
"""Test mixed-case labels and prefixes are detected."""
|
||||
results = analyze_with_recognizer(text, entity, recognizer)
|
||||
start = text.index(expected_value)
|
||||
assert len(results) == 1
|
||||
assert_result(results[0], entity, start, start + len(expected_value), 0.7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text, expected_values",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"Prior authorization PA-123456; prior authorization PA-654321.",
|
||||
["PA-123456", "PA-654321"],
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Claim CLM123456 and claim CLM654321.",
|
||||
["CLM123456", "CLM654321"],
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Prescription RX123456 and prescription RX654321.",
|
||||
["RX123456", "RX654321"],
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"Referral REF123456 and referral INF654321.",
|
||||
["REF123456", "INF654321"],
|
||||
),
|
||||
(
|
||||
UsProviderTaxIdRecognizer(),
|
||||
"US_PROVIDER_TAX_ID",
|
||||
"Provider EIN 12-3456789 and provider TIN 20-1234567.",
|
||||
["12-3456789", "20-1234567"],
|
||||
),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_when_text_has_multiple_admin_ids_then_all_are_detected(
|
||||
recognizer, entity, text, expected_values, analyze_with_recognizer
|
||||
):
|
||||
"""Test every contextual administrative ID in one input is returned."""
|
||||
results = sorted(
|
||||
analyze_with_recognizer(text, entity, recognizer),
|
||||
key=lambda result: result.start,
|
||||
)
|
||||
assert [text[result.start : result.end] for result in results] == expected_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text, expected_value",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"Prior authorization PA-123456.",
|
||||
"PA-123456",
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Claim CLM123456,",
|
||||
"CLM123456",
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Prescription RX123456;",
|
||||
"RX123456",
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"Referral REF123456.",
|
||||
"REF123456",
|
||||
),
|
||||
(
|
||||
UsProviderTaxIdRecognizer(),
|
||||
"US_PROVIDER_TAX_ID",
|
||||
"Provider EIN 12-3456789.",
|
||||
"12-3456789",
|
||||
),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_admin_id_matching_ignores_trailing_punctuation(
|
||||
recognizer, entity, text, expected_value, analyze_with_recognizer
|
||||
):
|
||||
"""Test trailing sentence punctuation stays outside the result span."""
|
||||
results = analyze_with_recognizer(text, entity, recognizer)
|
||||
start = text.index(expected_value)
|
||||
assert len(results) == 1
|
||||
assert_result(results[0], entity, start, start + len(expected_value), 0.7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text, expected_value",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"Prior authorization PA-123456",
|
||||
"PA-123456",
|
||||
),
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"Prior authorization PA-123456789012",
|
||||
"PA-123456789012",
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Claim CLM123456",
|
||||
"CLM123456",
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Claim CLM123456789012345",
|
||||
"CLM123456789012345",
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Prescription RX123456",
|
||||
"RX123456",
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Prescription RX123456789012",
|
||||
"RX123456789012",
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"Referral REF123456",
|
||||
"REF123456",
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"Referral INF123456789012",
|
||||
"INF123456789012",
|
||||
),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_admin_id_minimum_and_maximum_lengths_are_detected(
|
||||
recognizer, entity, text, expected_value, analyze_with_recognizer
|
||||
):
|
||||
"""Test each variable-length administrative ID at its exact boundaries."""
|
||||
results = analyze_with_recognizer(text, entity, recognizer)
|
||||
start = text.index(expected_value)
|
||||
assert len(results) == 1
|
||||
assert_result(results[0], entity, start, start + len(expected_value), 0.7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"PA-12345",
|
||||
),
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"PA-1234567890123",
|
||||
),
|
||||
(UsClaimNumberRecognizer(), "US_CLAIM_NUMBER", "CLM12345"),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"CLM1234567890123456",
|
||||
),
|
||||
(UsPrescriptionNumberRecognizer(), "US_PRESCRIPTION_NUMBER", "RX12345"),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"RX1234567890123",
|
||||
),
|
||||
(UsReferralNumberRecognizer(), "US_REFERRAL_NUMBER", "REF12345"),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"INF1234567890123",
|
||||
),
|
||||
(UsProviderTaxIdRecognizer(), "US_PROVIDER_TAX_ID", "12-123456"),
|
||||
(UsProviderTaxIdRecognizer(), "US_PROVIDER_TAX_ID", "12-12345678"),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_too_short_and_too_long_admin_ids_do_not_match(
|
||||
recognizer, entity, text, analyze_with_recognizer
|
||||
):
|
||||
"""Test values one digit outside each supported length do not match."""
|
||||
assert analyze_with_recognizer(text, entity, recognizer, score_threshold=0) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text, expected_value, expected_score",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"Prior authorization number: 987654321 approved.",
|
||||
"987654321",
|
||||
0.7,
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Claim number: 1234567890123 was paid.",
|
||||
"1234567890123",
|
||||
0.7,
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Claim ID 123456789012345 was paid.",
|
||||
"123456789012345",
|
||||
0.7,
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Rx #1234567",
|
||||
"1234567",
|
||||
0.6,
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Prescription number: 7654321",
|
||||
"7654321",
|
||||
0.7,
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"prescription 4455667",
|
||||
"4455667",
|
||||
0.7,
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"Infusion referral number: 2025001234",
|
||||
"2025001234",
|
||||
0.7,
|
||||
),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_when_admin_id_follows_label_then_identifier_only_is_detected(
|
||||
recognizer,
|
||||
entity,
|
||||
text,
|
||||
expected_value,
|
||||
expected_score,
|
||||
analyze_with_recognizer,
|
||||
):
|
||||
"""Test labels enable bare numeric IDs without entering the result span."""
|
||||
results = analyze_with_recognizer(text, entity, recognizer)
|
||||
start = text.index(expected_value)
|
||||
assert len(results) == 1
|
||||
assert_result(
|
||||
results[0], entity, start, start + len(expected_value), expected_score
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, expected_value",
|
||||
[
|
||||
("Billing provider EIN: 12-3456789", "12-3456789"),
|
||||
("Rendering provider TIN 20-1234567", "20-1234567"),
|
||||
("Healthcare provider tax number: 67-1234567", "67-1234567"),
|
||||
("Billing provider: 99-1234567", "99-1234567"),
|
||||
("Provider TIN# 12-3456789", "12-3456789"),
|
||||
("Billing provider EIN No. 20-1234567", "20-1234567"),
|
||||
],
|
||||
)
|
||||
def test_when_provider_ein_has_provider_tax_label_then_detected(
|
||||
text, expected_value, analyze_with_recognizer
|
||||
):
|
||||
"""Test valid EINs immediately following provider tax labels are detected."""
|
||||
results = analyze_with_recognizer(
|
||||
text,
|
||||
"US_PROVIDER_TAX_ID",
|
||||
UsProviderTaxIdRecognizer(),
|
||||
)
|
||||
start = text.index(expected_value)
|
||||
assert len(results) == 1
|
||||
assert_result(
|
||||
results[0],
|
||||
"US_PROVIDER_TAX_ID",
|
||||
start,
|
||||
start + len(expected_value),
|
||||
0.7,
|
||||
)
|
||||
|
||||
|
||||
def test_when_number_has_different_workflow_label_then_prescription_not_detected(
|
||||
analyze_with_recognizer,
|
||||
):
|
||||
"""Test a claim label does not support a prescription number match."""
|
||||
recognizer = UsPrescriptionNumberRecognizer()
|
||||
assert (
|
||||
analyze_with_recognizer(
|
||||
"The claim 1234567 was paid",
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
recognizer,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"PA-987654321",
|
||||
),
|
||||
(UsClaimNumberRecognizer(), "US_CLAIM_NUMBER", "CLM456789123"),
|
||||
(UsPrescriptionNumberRecognizer(), "US_PRESCRIPTION_NUMBER", "RX789456123"),
|
||||
(UsReferralNumberRecognizer(), "US_REFERRAL_NUMBER", "INF2025001234"),
|
||||
(UsProviderTaxIdRecognizer(), "US_PROVIDER_TAX_ID", "12-3456789"),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_when_us_healthcare_admin_id_lacks_context_then_below_threshold(
|
||||
recognizer, entity, text, analyze_with_recognizer
|
||||
):
|
||||
"""Test normal analyzer calls suppress pattern-only matches."""
|
||||
assert analyze_with_recognizer(text, entity, recognizer) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"Order number PA-987654321 is ready.",
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
"Tracking number CLM456789123 is active.",
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"Case number RX789456123 is pending.",
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
"Claim number INF2025001234 was denied.",
|
||||
),
|
||||
(
|
||||
UsProviderTaxIdRecognizer(),
|
||||
"US_PROVIDER_TAX_ID",
|
||||
"Invoice number 12-3456789 was posted.",
|
||||
),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_when_us_healthcare_admin_id_has_unrelated_context_then_not_detected(
|
||||
recognizer, entity, text, analyze_with_recognizer
|
||||
):
|
||||
"""Test similar-looking workflow IDs stay below the threshold."""
|
||||
assert analyze_with_recognizer(text, entity, recognizer) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"Provider phone extension 12-3456789",
|
||||
"provider 00-0000000 listed",
|
||||
"Employee tax ID 12-3456789",
|
||||
],
|
||||
)
|
||||
def test_when_ein_lacks_provider_tax_label_then_not_detected(
|
||||
text, analyze_with_recognizer
|
||||
):
|
||||
"""Test generic provider or tax wording cannot promote an EIN-shaped value."""
|
||||
assert (
|
||||
analyze_with_recognizer(
|
||||
text,
|
||||
"US_PROVIDER_TAX_ID",
|
||||
UsProviderTaxIdRecognizer(),
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_prefix",
|
||||
[
|
||||
"00",
|
||||
"07",
|
||||
"08",
|
||||
"09",
|
||||
"17",
|
||||
"18",
|
||||
"19",
|
||||
"28",
|
||||
"29",
|
||||
"49",
|
||||
"69",
|
||||
"70",
|
||||
"78",
|
||||
"79",
|
||||
"89",
|
||||
"96",
|
||||
"97",
|
||||
],
|
||||
)
|
||||
def test_when_provider_ein_prefix_is_not_irs_valid_then_not_detected(
|
||||
invalid_prefix, analyze_with_recognizer
|
||||
):
|
||||
"""Test values outside the IRS-assigned EIN prefix set do not match."""
|
||||
text = f"Provider Tax ID {invalid_prefix}-1234567"
|
||||
assert (
|
||||
analyze_with_recognizer(
|
||||
text,
|
||||
"US_PROVIDER_TAX_ID",
|
||||
UsProviderTaxIdRecognizer(),
|
||||
score_threshold=0,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, text, expected_score",
|
||||
[
|
||||
# fmt: off
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
"PA-987654321",
|
||||
0.1,
|
||||
),
|
||||
(UsClaimNumberRecognizer(), "US_CLAIM_NUMBER", "CLM456789123", 0.1),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
"RX789456123",
|
||||
0.1,
|
||||
),
|
||||
(UsReferralNumberRecognizer(), "US_REFERRAL_NUMBER", "INF2025001234", 0.1),
|
||||
(UsProviderTaxIdRecognizer(), "US_PROVIDER_TAX_ID", "12-3456789", 0.1),
|
||||
# fmt: on
|
||||
],
|
||||
)
|
||||
def test_explicit_request_threshold_can_return_pattern_only_matches(
|
||||
recognizer, entity, text, expected_score, analyze_with_recognizer
|
||||
):
|
||||
"""Test callers can opt into raw pattern matches for structured analysis."""
|
||||
results = analyze_with_recognizer(text, entity, recognizer, score_threshold=0)
|
||||
assert len(results) == 1
|
||||
assert_result(results[0], entity, 0, len(text), expected_score)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"recognizer, entity, expected_context",
|
||||
[
|
||||
(
|
||||
UsPriorAuthorizationNumberRecognizer(),
|
||||
"US_PRIOR_AUTHORIZATION_NUMBER",
|
||||
["authorization", "auth", "preauthorization", "approval"],
|
||||
),
|
||||
(
|
||||
UsClaimNumberRecognizer(),
|
||||
"US_CLAIM_NUMBER",
|
||||
["claim", "billing"],
|
||||
),
|
||||
(
|
||||
UsPrescriptionNumberRecognizer(),
|
||||
"US_PRESCRIPTION_NUMBER",
|
||||
["prescription", "pharmacy", "medication"],
|
||||
),
|
||||
(
|
||||
UsReferralNumberRecognizer(),
|
||||
"US_REFERRAL_NUMBER",
|
||||
["referral", "infusion", "specialty", "referring"],
|
||||
),
|
||||
(
|
||||
UsProviderTaxIdRecognizer(),
|
||||
"US_PROVIDER_TAX_ID",
|
||||
["tax", "tin", "ein", "billing"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_us_healthcare_admin_recognizer_metadata(recognizer, entity, expected_context):
|
||||
"""Test entity metadata and context without a recognizer threshold."""
|
||||
assert isinstance(recognizer, PatternRecognizer)
|
||||
assert PatternRecognizer in type(recognizer).__bases__
|
||||
assert recognizer.COUNTRY_CODE == "us"
|
||||
assert recognizer.supported_entities == [entity]
|
||||
assert recognizer.supported_language == "en"
|
||||
assert recognizer.context == expected_context
|
||||
assert recognizer.score_thresholds == {}
|
||||
Reference in New Issue
Block a user