Coverage for presidio_analyzer / predefined_recognizers / country_specific / us / us_npi_recognizer.py: 100%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-29 09:03 +0000

1"""Recognizer for US National Provider Identifier (NPI).""" 

2 

3from typing import List, Optional, Tuple 

4 

5from presidio_analyzer import EntityRecognizer, Pattern, PatternRecognizer 

6 

7 

8class UsNpiRecognizer(PatternRecognizer): 

9 """Recognize US National Provider Identifier (NPI) using regex + checksum. 

10 

11 The NPI is a unique 10-digit number assigned to healthcare providers in the 

12 United States by CMS (Centers for Medicare & Medicaid Services). It is a 

13 HIPAA-mandated identifier that appears on insurance claims, prescriptions, 

14 and provider directories. 

15 

16 Format: 

17 - Exactly 10 digits 

18 - Starts with 1 (Type 1, individual) or 2 (Type 2, organization) 

19 - Last digit is a Luhn check digit (using "80840" prefix per CMS spec) 

20 

21 Reference: https://www.cms.gov/Regulations-and-Guidance/Administrative-Simplification/NationalProvIdentStand 

22 

23 :param patterns: List of patterns to be used by this recognizer 

24 :param context: List of context words to increase confidence in detection 

25 :param supported_language: Language this recognizer supports 

26 :param supported_entity: The entity this recognizer can detect 

27 :param replacement_pairs: List of tuples with potential replacement values 

28 for different strings to be used during pattern matching. 

29 This can allow a greater variety in input, for example by removing dashes or 

30 spaces. 

31 """ 

32 

33 PATTERNS = [ 

34 Pattern( 

35 "NPI (weak)", 

36 r"\b[12]\d{9}\b", 

37 0.1, 

38 ), 

39 Pattern( 

40 "NPI (medium)", 

41 r"\b[12]\d{3}[ -]\d{3}[ -]\d{3}\b", 

42 0.4, 

43 ), 

44 ] 

45 

46 CONTEXT = [ 

47 "npi", 

48 "national provider", 

49 "provider", 

50 "npi number", 

51 "provider id", 

52 "provider identifier", 

53 "taxonomy", 

54 ] 

55 

56 def __init__( 

57 self, 

58 patterns: Optional[List[Pattern]] = None, 

59 context: Optional[List[str]] = None, 

60 supported_language: str = "en", 

61 supported_entity: str = "US_NPI", 

62 replacement_pairs: Optional[List[Tuple[str, str]]] = None, 

63 name: Optional[str] = None, 

64 ): 

65 self.replacement_pairs = ( 

66 replacement_pairs if replacement_pairs else [("-", ""), (" ", "")] 

67 ) 

68 patterns = patterns if patterns else self.PATTERNS 

69 context = context if context else self.CONTEXT 

70 super().__init__( 

71 supported_entity=supported_entity, 

72 patterns=patterns, 

73 context=context, 

74 supported_language=supported_language, 

75 name=name, 

76 ) 

77 

78 def validate_result(self, pattern_text: str) -> bool: # noqa: D102 

79 sanitized_value = EntityRecognizer.sanitize_value( 

80 pattern_text, self.replacement_pairs 

81 ) 

82 return self.__npi_luhn_checksum(sanitized_value) 

83 

84 def invalidate_result(self, pattern_text: str) -> bool: # noqa: D102 

85 sanitized_value = EntityRecognizer.sanitize_value( 

86 pattern_text, self.replacement_pairs 

87 ) 

88 # Reject degenerate patterns where all body digits are identical 

89 # (e.g., 1111111111 or 1111111112 where the last digit is a check digit). 

90 if sanitized_value: 

91 body = sanitized_value[:-1] if len(sanitized_value) > 1 else sanitized_value 

92 if body and len(set(body)) == 1: 

93 return True 

94 return False 

95 

96 @staticmethod 

97 def __npi_luhn_checksum(sanitized_value: str) -> bool: 

98 """Validate NPI using Luhn algorithm with "80840" prefix per CMS spec. 

99 

100 Steps: 

101 1. Take the 10-digit NPI 

102 2. Prepend "80840" to get a 15-digit number 

103 3. Apply standard Luhn algorithm: result mod 10 should equal 0 

104 """ 

105 prefixed = "80840" + sanitized_value 

106 digits = [int(d) for d in prefixed] 

107 

108 # Standard Luhn: from rightmost digit, double every second digit 

109 checksum = 0 

110 for i, digit in enumerate(reversed(digits)): 

111 if i % 2 == 1: 

112 doubled = digit * 2 

113 checksum += doubled - 9 if doubled > 9 else doubled 

114 else: 

115 checksum += digit 

116 

117 return checksum % 10 == 0