hccinfhir 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hccinfhir/extractor.py CHANGED
@@ -36,11 +36,11 @@ def extract_sld(
36
36
  raise ValueError(f'Format must be either "837" or "fhir", got {format}')
37
37
 
38
38
 
39
- def extract_sld_list(data: Union[List[str], List[dict]], format: Literal["837", "fhir"] = "fhir") -> List[ServiceLevelData]:
39
+ def extract_sld_list(data: Union[List[str], List[dict]],
40
+ format: Literal["837", "fhir"] = "fhir") -> List[ServiceLevelData]:
40
41
  """Extract SLDs from a list of FHIR EOBs"""
41
42
  output = []
42
43
  for item in data:
43
-
44
44
  try:
45
45
  output.extend(extract_sld(item, format))
46
46
  except TypeError as e:
@@ -38,9 +38,11 @@ def parse_amount(amount_str: str) -> Optional[float]:
38
38
  except (ValueError, TypeError):
39
39
  return None
40
40
 
41
- def get_segment_value(segment: List[str], index: int) -> Optional[str]:
41
+ def get_segment_value(segment: List[str],
42
+ index: int,
43
+ default: Optional[str] = None) -> Optional[str]:
42
44
  """Safely get value from segment at given index"""
43
- return segment[index] if len(segment) > index else None
45
+ return segment[index] if len(segment) > index else default
44
46
 
45
47
  def parse_diagnosis_codes(segment: List[str]) -> Dict[str, str]:
46
48
  """Extract diagnosis codes from HI segment"""
@@ -49,7 +51,11 @@ def parse_diagnosis_codes(segment: List[str]) -> Dict[str, str]:
49
51
  if ':' not in element:
50
52
  continue
51
53
  qualifier, code = element.split(':')[:2]
52
- if qualifier in ['ABK', 'ABF']: # ICD-10 qualifiers
54
+ if qualifier in {'ABK', 'ABF', 'ABJ'}: # ICD-10 qualifiers
55
+ # ABK: Primary Diagnosis
56
+ # ABF: Secondary Diagnosis
57
+ # ABJ: Admitting Diagnosis
58
+ # NOTE: In Risk Adjustment, we do not differentiate between primary and secondary diagnoses
53
59
  dx_lookup[str(pos)] = code
54
60
  return dx_lookup
55
61
 
@@ -61,17 +67,35 @@ def process_service_line(segments: List[List[str]], start_index: int) -> tuple[O
61
67
  for seg in segments[start_index:]:
62
68
  if seg[0] in ['LX', 'CLM', 'SE']:
63
69
  break
64
- if seg[0] == 'LIN' and len(seg) > 3 and seg[2] == 'N4':
65
- ndc = seg[3]
66
- elif seg[0] == 'DTP' and seg[1] == '472':
67
- service_date = parse_date(seg[3])
70
+ if len(seg) > 3:
71
+ if seg[0] == 'LIN' and seg[2] == 'N4':
72
+ ndc = seg[3]
73
+ elif (seg[0] == 'DTP' and
74
+ seg[1] in {'472', '434'} and
75
+ seg[2].endswith('D8')):
76
+ # 472: Service Date
77
+ # 434: From Date in 837I
78
+ # These are not included currently: 435: To Date in 837I, 096 Discharge Date
79
+ if seg[3]:
80
+ service_date = parse_date(seg[3][:8] if len(seg[3]) >= 8 else seg[3])
68
81
  if ndc and service_date:
69
82
  break
70
83
 
71
84
  return ndc, service_date
72
85
 
73
86
  def extract_sld_837(content: str) -> List[ServiceLevelData]:
74
- """Extract service level data from 837 Professional or Institutional claims"""
87
+ """Extract service level data from 837 Professional or Institutional claims
88
+
89
+ Structure:
90
+ Billing Provider (2000A)
91
+ └── Subscriber (2000B)
92
+ └── Patient (2000C) [if needed]
93
+ └── Claim (2300)
94
+ ├── Service Line 1 (2400)
95
+ ├── Service Line 2 (2400)
96
+ └── Service Line N (2400)
97
+
98
+ """
75
99
  if not content:
76
100
  raise ValueError("Input X12 data cannot be empty")
77
101
 
@@ -100,7 +124,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
100
124
  seg_id = segment[0]
101
125
 
102
126
  # Process NM1 segments (Provider and Patient info)
103
- if seg_id == 'NM1':
127
+ if seg_id == 'NM1' and len(segment) > 1:
104
128
  if segment[1] == 'IL': # Subscriber/Patient
105
129
  current_data.patient_id = get_segment_value(segment, 9)
106
130
  in_claim_loop = False
@@ -112,7 +136,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
112
136
  current_data.billing_provider_npi = get_segment_value(segment, 9)
113
137
 
114
138
  # Process Provider Specialty
115
- elif seg_id == 'PRV' and segment[1] == 'PE' and in_rendering_provider_loop:
139
+ elif seg_id == 'PRV' and len(segment) > 1 and segment[1] == 'PE' and in_rendering_provider_loop:
116
140
  current_data.provider_specialty = get_segment_value(segment, 3)
117
141
 
118
142
  # Process Claim Information
@@ -122,29 +146,72 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
122
146
  current_data.claim_id = segment[1] if len(segment) > 1 else None
123
147
 
124
148
  # Parse facility and service type for institutional claims
125
- if claim_type == "837I" and len(segment) > 5 and ':' in segment[5]:
126
- current_data.facility_type = segment[5][0]
149
+ if claim_type == "837I" and len(segment) > 5 and segment[5] and ':' in segment[5]:
150
+ current_data.facility_type = segment[5][0] if segment[5] else None
127
151
  current_data.service_type = segment[5][1] if len(segment[5]) > 1 else None
128
152
 
129
153
  # Process Diagnosis Codes
130
154
  elif seg_id == 'HI' and in_claim_loop:
131
- current_data.dx_lookup = parse_diagnosis_codes(segment)
155
+ # In 837I, there can be multiple HI segments in the claim
156
+ # Also, in 837I, diagnosis position does not matter
157
+ # We will use continuous numbering for diagnosis codes
158
+ # use the last dx_lookup position as the starting position, and update
159
+ hi_segment = parse_diagnosis_codes(segment)
160
+ hi_segment_realigned = {
161
+ str(int(pos) + len(current_data.dx_lookup)): code
162
+ for pos, code in hi_segment.items()
163
+ }
164
+ current_data.dx_lookup.update(hi_segment_realigned)
132
165
 
133
166
  # Process Service Lines
167
+ #
168
+ # SV1 (Professional Services):
169
+ # SV101 (Required) - Procedure Code Composite: HC qualifier + 5-digit HCPCS code, supports up to 4 HCPCS modifiers
170
+ # SV102 (Required) - Charge Amount: Format 99999999.99
171
+ # SV103 (Required) - Unit Type: F2 (International Unit) or UN (Units)
172
+ # SV104 (Required) - Unit Count: Format 9999.99 (decimals allowed)
173
+ # SV105 (Situational) - Place of Service Code: Required for First Steps claims
174
+ # SV107 (Situational) - Diagnosis Code Pointer: Links to HI segment in 2300 loop, valid values 1-8
175
+ #
176
+ # SV2 (Institutional Services):
177
+ # SV201 (Required) - Revenue Code: Facility-specific revenue code for service rendered
178
+ # SV202 (Required) - Procedure Code Composite: HC qualifier + 5-digit HCPCS code, supports up to 4 HCPCS modifiers
179
+ # SV203 (Required) - Charge Amount: Format 99999999.99
180
+ # SV204 (Required) - Unit Type: DA (Days) or UN (Units)
181
+ # SV205 (Required) - Unit Count: Format 9999999.999 (whole numbers only - fractional quantities not recognized)
182
+ # NOTE: Diagnosis Code Pointer is not supported for SV2
183
+ #
134
184
  elif seg_id in ['SV1', 'SV2'] and in_claim_loop:
135
- # Parse procedure info
136
- proc_info = segment[1].split(':')
137
- procedure_code = proc_info[1] if len(proc_info) > 1 else None
138
- modifiers = proc_info[2:] if len(proc_info) > 2 else []
139
185
 
140
- # Get diagnosis pointers and linked diagnoses
141
- dx_pointer_pos = 7 if seg_id == 'SV1' else 11
142
- dx_pointers = get_segment_value(segment, dx_pointer_pos)
143
- linked_diagnoses = [
144
- current_data.dx_lookup[pointer]
145
- for pointer in (dx_pointers.split(',') if dx_pointers else [])
146
- if pointer in current_data.dx_lookup
147
- ]
186
+ linked_diagnoses = []
187
+
188
+ if seg_id == 'SV1':
189
+ # SV1 Professional Service: SV101=procedure, SV104=quantity, SV106=place_of_service
190
+ proc_info = get_segment_value(segment, 1, '').split(':')
191
+ procedure_code = proc_info[1] if len(proc_info) > 1 else None
192
+ modifiers = proc_info[2:] if len(proc_info) > 2 else []
193
+ quantity = parse_amount(get_segment_value(segment, 4))
194
+ place_of_service = get_segment_value(segment, 5)
195
+ # Get diagnosis pointers and linked diagnoses
196
+ dx_pointers = get_segment_value(segment, 7, '')
197
+ linked_diagnoses = [
198
+ current_data.dx_lookup[pointer]
199
+ for pointer in (dx_pointers.split(':') if dx_pointers else [])
200
+ if pointer in current_data.dx_lookup
201
+ ]
202
+ else:
203
+ # SV2 Institutional Service: SV201=revenue, SV202=procedure, SV205=quantity
204
+ # Revenue code in SV201
205
+ revenue_code = get_segment_value(segment, 1)
206
+ # Procedure code in SV202
207
+ proc_info = get_segment_value(segment, 2, '').split(':')
208
+ procedure_code = proc_info[1] if len(proc_info) > 1 else None
209
+ modifiers = proc_info[2:] if len(proc_info) > 2 else []
210
+ # Quantity in SV205
211
+ quantity = parse_amount(get_segment_value(segment, 5))
212
+ place_of_service = None # Not applicable for institutional
213
+ # linked diagnoses are not supported for SV2
214
+
148
215
 
149
216
  # Get service line details
150
217
  ndc, service_date = process_service_line(segments, i)
@@ -154,7 +221,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
154
221
  claim_id=current_data.claim_id,
155
222
  procedure_code=procedure_code,
156
223
  linked_diagnosis_codes=linked_diagnoses,
157
- claim_diagnosis_codes=list(current_data.dx_lookup.values()),
224
+ claim_diagnosis_codes=list(current_data.dx_lookup.values()), # this is used for risk adjustment
158
225
  claim_type=current_data.claim_type,
159
226
  provider_specialty=current_data.provider_specialty,
160
227
  performing_provider_npi=current_data.performing_provider_npi,
@@ -163,8 +230,8 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
163
230
  facility_type=current_data.facility_type,
164
231
  service_type=current_data.service_type,
165
232
  service_date=service_date,
166
- place_of_service=get_segment_value(segment, 6) if seg_id == 'SV1' else None,
167
- quantity=parse_amount(get_segment_value(segment, 4)),
233
+ place_of_service=place_of_service,
234
+ quantity=quantity,
168
235
  modifiers=modifiers,
169
236
  ndc=ndc,
170
237
  allowed_amount=None
hccinfhir/filter.py CHANGED
@@ -9,13 +9,13 @@ professional_cpt_default = load_proc_filtering(professional_cpt_default_fn)
9
9
  def apply_filter(
10
10
  data: List[ServiceLevelData],
11
11
  inpatient_tob: Set[str] = {'11X', '41X'},
12
- outpatient_tob: Set[str] = {'12X', '13X', '43X', '71X', '73X', '76X', '77X', '85X'},
12
+ outpatient_tob: Set[str] = {'12X', '13X', '43X', '71X', '73X', '76X', '77X', '85X', '87X'},
13
13
  professional_cpt: Set[str] = professional_cpt_default
14
14
  ) -> List[ServiceLevelData]:
15
15
  # tob (Type of Bill) Filter is based on:
16
16
  # https://www.hhs.gov/guidance/sites/default/files/hhs-guidance-documents/2012181486-wq-092916_ra_webinar_slides_5cr_092816.pdf
17
17
  # https://www.hhs.gov/guidance/sites/default/files/hhs-guidance-documents/FinalEncounterDataDiagnosisFilteringLogic.pdf
18
-
18
+ # https://www.cms.gov/files/document/encounterdatasystemedit20495andedit01415andtob87x07162021.pdf for 87X
19
19
  # NOTE: If no facility_type or service_type, then the claim is professional, in our implementation.
20
20
  # NOTE: The original CMS logic is for the "record" level, not the service level.
21
21
  # Thus, when preparing the service level data, put all diagnosis codes into the diagnosis field.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hccinfhir
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: HCC Algorithm for FHIR Resources
5
5
  Project-URL: Homepage, https://github.com/mimilabs/hccinfhir
6
6
  Project-URL: Issues, https://github.com/mimilabs/hccinfhir/issues
@@ -1,9 +1,9 @@
1
1
  hccinfhir/__init__.py,sha256=UBTJCLzkCGNCMpG38vkpu3DeMM_RV_7o5JjOWL7ps9c,1048
2
2
  hccinfhir/datamodels.py,sha256=lMLGSuWTlpzoWenKsfhF8qQF3RZJV2NNGyIm_rBkd0o,5038
3
- hccinfhir/extractor.py,sha256=-jHVCIJqFAqvrI9GxkkXZVDQjKDa-7vF7v3PGMGAMnA,1801
4
- hccinfhir/extractor_837.py,sha256=vkTBCd0WBaJoTrUd-Z-zCIaoLk7KV2n4AGqIORhONIk,7147
3
+ hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
4
+ hccinfhir/extractor_837.py,sha256=VWBZdMZpHwDhSAotx7uuYiCIHsRlpsDmOi5E3v8mgBU,10866
5
5
  hccinfhir/extractor_fhir.py,sha256=Rg_L0Vg5tz_L2VJ_jvZwWz6RMlPAkHwj4LiK-OWQvrQ,8458
6
- hccinfhir/filter.py,sha256=YjhOG5jJZZOfBJ1-cDuRs-htrLF07oceoD74PbL8rms,1890
6
+ hccinfhir/filter.py,sha256=j_yD2g6RBXVUV9trKkWzsQ35x3fRvfKUPvEXKUefI64,2007
7
7
  hccinfhir/hccinfhir.py,sha256=KzEPwZQn5qcG8e44I8EahzhWXP9fYR18U4SHTA1DGcI,6855
8
8
  hccinfhir/model_calculate.py,sha256=3lKpNSdTNFn3OREw8yjlOhoNcDhs7LpQj7TIHQ1HvxQ,5519
9
9
  hccinfhir/model_coefficients.py,sha256=ZsVY0S_X_BzDvcCmzCEf31v8uixbGmPAsR6nVEyCbIA,5530
@@ -43,7 +43,7 @@ hccinfhir/samples/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9fNzQEM
43
43
  hccinfhir/samples/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
44
44
  hccinfhir/samples/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
45
45
  hccinfhir/samples/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
46
- hccinfhir-0.1.2.dist-info/METADATA,sha256=AajRjb9nwdxkRwRKIKWVqD9rtwegBZthoMD0pF510Ws,13535
47
- hccinfhir-0.1.2.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
48
- hccinfhir-0.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
49
- hccinfhir-0.1.2.dist-info/RECORD,,
46
+ hccinfhir-0.1.3.dist-info/METADATA,sha256=VuJalbxDC35qPO0YOr-a4DEyKwXo_YlyrKvb5Trh5oY,13535
47
+ hccinfhir-0.1.3.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
48
+ hccinfhir-0.1.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
49
+ hccinfhir-0.1.3.dist-info/RECORD,,