hccinfhir 0.1.2__py3-none-any.whl → 0.1.4__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.
Files changed (34) hide show
  1. hccinfhir/__init__.py +1 -1
  2. hccinfhir/datamodels.py +17 -15
  3. hccinfhir/extractor.py +2 -2
  4. hccinfhir/extractor_837.py +178 -44
  5. hccinfhir/filter.py +2 -2
  6. hccinfhir/hccinfhir.py +6 -5
  7. hccinfhir/model_calculate.py +14 -14
  8. hccinfhir/model_interactions.py +1 -0
  9. hccinfhir/sample_files/sample_837_12.txt +113 -0
  10. hccinfhir/samples.py +15 -15
  11. hccinfhir-0.1.4.dist-info/METADATA +611 -0
  12. hccinfhir-0.1.4.dist-info/RECORD +49 -0
  13. hccinfhir/sample_utils.py +0 -252
  14. hccinfhir-0.1.2.dist-info/METADATA +0 -390
  15. hccinfhir-0.1.2.dist-info/RECORD +0 -49
  16. /hccinfhir/{samples → sample_files}/__init__.py +0 -0
  17. /hccinfhir/{samples → sample_files}/sample_837_0.txt +0 -0
  18. /hccinfhir/{samples → sample_files}/sample_837_1.txt +0 -0
  19. /hccinfhir/{samples → sample_files}/sample_837_10.txt +0 -0
  20. /hccinfhir/{samples → sample_files}/sample_837_11.txt +0 -0
  21. /hccinfhir/{samples → sample_files}/sample_837_2.txt +0 -0
  22. /hccinfhir/{samples → sample_files}/sample_837_3.txt +0 -0
  23. /hccinfhir/{samples → sample_files}/sample_837_4.txt +0 -0
  24. /hccinfhir/{samples → sample_files}/sample_837_5.txt +0 -0
  25. /hccinfhir/{samples → sample_files}/sample_837_6.txt +0 -0
  26. /hccinfhir/{samples → sample_files}/sample_837_7.txt +0 -0
  27. /hccinfhir/{samples → sample_files}/sample_837_8.txt +0 -0
  28. /hccinfhir/{samples → sample_files}/sample_837_9.txt +0 -0
  29. /hccinfhir/{samples → sample_files}/sample_eob_1.json +0 -0
  30. /hccinfhir/{samples → sample_files}/sample_eob_2.json +0 -0
  31. /hccinfhir/{samples → sample_files}/sample_eob_200.ndjson +0 -0
  32. /hccinfhir/{samples → sample_files}/sample_eob_3.json +0 -0
  33. {hccinfhir-0.1.2.dist-info → hccinfhir-0.1.4.dist-info}/WHEEL +0 -0
  34. {hccinfhir-0.1.2.dist-info → hccinfhir-0.1.4.dist-info}/licenses/LICENSE +0 -0
hccinfhir/__init__.py CHANGED
@@ -12,7 +12,7 @@ from .model_calculate import calculate_raf
12
12
  from .datamodels import Demographics, ServiceLevelData, RAFResult, ModelName
13
13
 
14
14
  # Sample data functions
15
- from .sample_utils import (
15
+ from .samples import (
16
16
  SampleData,
17
17
  get_eob_sample,
18
18
  get_eob_sample_list,
hccinfhir/datamodels.py CHANGED
@@ -89,18 +89,20 @@ class Demographics(BaseModel):
89
89
  pbd: Optional[bool] = Field(False, description="[derived] True if PBD (PBD Model)")
90
90
 
91
91
 
92
- class RAFResult(TypedDict):
93
- """Type definition for RAF calculation results"""
94
- risk_score: float
95
- risk_score_demographics: float
96
- risk_score_chronic_only: float
97
- risk_score_hcc: float
98
- hcc_list: List[str]
99
- cc_to_dx: Dict[str, Set[str]]
100
- coefficients: Dict[str, float]
101
- interactions: Dict[str, float]
102
- demographics: Demographics
103
- model_name: ModelName
104
- version: str
105
- diagnosis_codes: List[str]
106
- service_level_data: Optional[List[ServiceLevelData]]
92
+ class RAFResult(BaseModel):
93
+ """Risk adjustment calculation results"""
94
+ risk_score: float = Field(..., description="Final RAF score")
95
+ risk_score_demographics: float = Field(..., description="Demographics-only risk score")
96
+ risk_score_chronic_only: float = Field(..., description="Chronic conditions risk score")
97
+ risk_score_hcc: float = Field(..., description="HCC conditions risk score")
98
+ hcc_list: List[str] = Field(default_factory=list, description="List of active HCC categories")
99
+ cc_to_dx: Dict[str, Set[str]] = Field(default_factory=dict, description="Condition categories mapped to diagnosis codes")
100
+ coefficients: Dict[str, float] = Field(default_factory=dict, description="Applied model coefficients")
101
+ interactions: Dict[str, float] = Field(default_factory=dict, description="Disease interaction coefficients")
102
+ demographics: Demographics = Field(..., description="Patient demographics used in calculation")
103
+ model_name: ModelName = Field(..., description="HCC model used for calculation")
104
+ version: str = Field(..., description="Library version")
105
+ diagnosis_codes: List[str] = Field(default_factory=list, description="Input diagnosis codes")
106
+ service_level_data: Optional[List[ServiceLevelData]] = Field(default=None, description="Processed service records")
107
+
108
+ model_config = {"extra": "forbid", "validate_assignment": True}
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,38 +67,92 @@ 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
- def extract_sld_837(content: str) -> List[ServiceLevelData]:
74
- """Extract service level data from 837 Professional or Institutional claims"""
75
- if not content:
76
- raise ValueError("Input X12 data cannot be empty")
86
+ def split_into_claims(segments: List[List[str]]) -> List[List[List[str]]]:
87
+ """Split segments into individual claims based on ST/SE boundaries.
77
88
 
78
- # Split content into segments
79
- segments = [seg.strip().split('*') for seg in content.split('~') if seg.strip()]
89
+ Each ST...SE block represents one complete claim.
90
+ Returns a list of claim segment lists.
91
+ """
92
+ claims = []
93
+ current_claim = []
94
+ in_transaction = False
95
+ st_control_number = None
80
96
 
81
- # Detect claim type from GS segment
82
- claim_type = None
83
97
  for segment in segments:
84
- if segment[0] == 'GS' and len(segment) > 8:
85
- claim_type = CLAIM_TYPES.get(segment[8])
86
- break
98
+ if len(segment) < 1:
99
+ continue
100
+
101
+ seg_id = segment[0]
102
+
103
+ if seg_id == 'ST':
104
+ # Start new claim transaction
105
+ if current_claim: # Save previous claim if exists (shouldn't happen with valid X12)
106
+ claims.append(current_claim)
107
+ current_claim = [segment]
108
+ in_transaction = True
109
+ st_control_number = segment[2] if len(segment) > 2 else None
110
+
111
+ elif seg_id == 'SE':
112
+ # End current claim transaction
113
+ if in_transaction:
114
+ current_claim.append(segment)
115
+
116
+ # Validate control numbers match (ST02 == SE02)
117
+ se_control_number = segment[2] if len(segment) > 2 else None
118
+ if st_control_number != se_control_number:
119
+ print(f"Warning: ST/SE control numbers don't match: {st_control_number} != {se_control_number}")
120
+
121
+ claims.append(current_claim)
122
+ current_claim = []
123
+ in_transaction = False
124
+ st_control_number = None
125
+
126
+ elif in_transaction:
127
+ # Add segment to current claim
128
+ current_claim.append(segment)
87
129
 
88
- if not claim_type:
89
- raise ValueError("Invalid or unsupported 837 format")
130
+ # Handle case where file doesn't end with SE (malformed)
131
+ if current_claim:
132
+ print("Warning: Unclosed transaction found (missing SE)")
133
+ claims.append(current_claim)
134
+
135
+ return claims
136
+
137
+ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[ServiceLevelData]:
138
+ """Extract service level data from 837 Professional or Institutional claims
139
+
140
+ Structure:
141
+ Billing Provider (2000A)
142
+ └── Subscriber (2000B)
143
+ └── Patient (2000C) [if needed]
144
+ └── Claim (2300)
145
+ ├── Service Line 1 (2400)
146
+ ├── Service Line 2 (2400)
147
+ └── Service Line N (2400)
90
148
 
91
- encounters = []
149
+ """
150
+ slds = []
92
151
  current_data = ClaimData(claim_type=claim_type)
93
152
  in_claim_loop = False
94
153
  in_rendering_provider_loop = False
95
-
154
+ claim_control_number = None
155
+
96
156
  for i, segment in enumerate(segments):
97
157
  if len(segment) < 2:
98
158
  continue
@@ -100,7 +160,10 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
100
160
  seg_id = segment[0]
101
161
 
102
162
  # Process NM1 segments (Provider and Patient info)
103
- if seg_id == 'NM1':
163
+ if seg_id == 'ST':
164
+ claim_control_number = segment[2] if len(segment) > 2 else None
165
+
166
+ elif seg_id == 'NM1' and len(segment) > 1:
104
167
  if segment[1] == 'IL': # Subscriber/Patient
105
168
  current_data.patient_id = get_segment_value(segment, 9)
106
169
  in_claim_loop = False
@@ -112,7 +175,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
112
175
  current_data.billing_provider_npi = get_segment_value(segment, 9)
113
176
 
114
177
  # Process Provider Specialty
115
- elif seg_id == 'PRV' and segment[1] == 'PE' and in_rendering_provider_loop:
178
+ elif seg_id == 'PRV' and len(segment) > 1 and segment[1] == 'PE' and in_rendering_provider_loop:
116
179
  current_data.provider_specialty = get_segment_value(segment, 3)
117
180
 
118
181
  # Process Claim Information
@@ -122,29 +185,72 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
122
185
  current_data.claim_id = segment[1] if len(segment) > 1 else None
123
186
 
124
187
  # 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]
188
+ if claim_type == "837I" and len(segment) > 5 and segment[5] and ':' in segment[5]:
189
+ current_data.facility_type = segment[5][0] if segment[5] else None
127
190
  current_data.service_type = segment[5][1] if len(segment[5]) > 1 else None
128
191
 
129
192
  # Process Diagnosis Codes
130
193
  elif seg_id == 'HI' and in_claim_loop:
131
- current_data.dx_lookup = parse_diagnosis_codes(segment)
194
+ # In 837I, there can be multiple HI segments in the claim
195
+ # Also, in 837I, diagnosis position does not matter
196
+ # We will use continuous numbering for diagnosis codes
197
+ # use the last dx_lookup position as the starting position, and update
198
+ hi_segment = parse_diagnosis_codes(segment)
199
+ hi_segment_realigned = {
200
+ str(int(pos) + len(current_data.dx_lookup)): code
201
+ for pos, code in hi_segment.items()
202
+ }
203
+ current_data.dx_lookup.update(hi_segment_realigned)
132
204
 
133
205
  # Process Service Lines
206
+ #
207
+ # SV1 (Professional Services):
208
+ # SV101 (Required) - Procedure Code Composite: HC qualifier + 5-digit HCPCS code, supports up to 4 HCPCS modifiers
209
+ # SV102 (Required) - Charge Amount: Format 99999999.99
210
+ # SV103 (Required) - Unit Type: F2 (International Unit) or UN (Units)
211
+ # SV104 (Required) - Unit Count: Format 9999.99 (decimals allowed)
212
+ # SV105 (Situational) - Place of Service Code: Required for First Steps claims
213
+ # SV107 (Situational) - Diagnosis Code Pointer: Links to HI segment in 2300 loop, valid values 1-8
214
+ #
215
+ # SV2 (Institutional Services):
216
+ # SV201 (Required) - Revenue Code: Facility-specific revenue code for service rendered
217
+ # SV202 (Required) - Procedure Code Composite: HC qualifier + 5-digit HCPCS code, supports up to 4 HCPCS modifiers
218
+ # SV203 (Required) - Charge Amount: Format 99999999.99
219
+ # SV204 (Required) - Unit Type: DA (Days) or UN (Units)
220
+ # SV205 (Required) - Unit Count: Format 9999999.999 (whole numbers only - fractional quantities not recognized)
221
+ # NOTE: Diagnosis Code Pointer is not supported for SV2
222
+ #
134
223
  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
224
 
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
- ]
225
+ linked_diagnoses = []
226
+
227
+ if seg_id == 'SV1':
228
+ # SV1 Professional Service: SV101=procedure, SV104=quantity, SV106=place_of_service
229
+ proc_info = get_segment_value(segment, 1, '').split(':')
230
+ procedure_code = proc_info[1] if len(proc_info) > 1 else None
231
+ modifiers = proc_info[2:] if len(proc_info) > 2 else []
232
+ quantity = parse_amount(get_segment_value(segment, 4))
233
+ place_of_service = get_segment_value(segment, 5)
234
+ # Get diagnosis pointers and linked diagnoses
235
+ dx_pointers = get_segment_value(segment, 7, '')
236
+ linked_diagnoses = [
237
+ current_data.dx_lookup[pointer]
238
+ for pointer in (dx_pointers.split(':') if dx_pointers else [])
239
+ if pointer in current_data.dx_lookup
240
+ ]
241
+ else:
242
+ # SV2 Institutional Service: SV201=revenue, SV202=procedure, SV205=quantity
243
+ # Revenue code in SV201
244
+ revenue_code = get_segment_value(segment, 1)
245
+ # Procedure code in SV202
246
+ proc_info = get_segment_value(segment, 2, '').split(':')
247
+ procedure_code = proc_info[1] if len(proc_info) > 1 else None
248
+ modifiers = proc_info[2:] if len(proc_info) > 2 else []
249
+ # Quantity in SV205
250
+ quantity = parse_amount(get_segment_value(segment, 5))
251
+ place_of_service = None # Not applicable for institutional
252
+ # linked diagnoses are not supported for SV2
253
+
148
254
 
149
255
  # Get service line details
150
256
  ndc, service_date = process_service_line(segments, i)
@@ -154,7 +260,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
154
260
  claim_id=current_data.claim_id,
155
261
  procedure_code=procedure_code,
156
262
  linked_diagnosis_codes=linked_diagnoses,
157
- claim_diagnosis_codes=list(current_data.dx_lookup.values()),
263
+ claim_diagnosis_codes=list(current_data.dx_lookup.values()), # this is used for risk adjustment
158
264
  claim_type=current_data.claim_type,
159
265
  provider_specialty=current_data.provider_specialty,
160
266
  performing_provider_npi=current_data.performing_provider_npi,
@@ -163,12 +269,40 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
163
269
  facility_type=current_data.facility_type,
164
270
  service_type=current_data.service_type,
165
271
  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)),
272
+ place_of_service=place_of_service,
273
+ quantity=quantity,
168
274
  modifiers=modifiers,
169
275
  ndc=ndc,
170
276
  allowed_amount=None
171
277
  )
172
- encounters.append(service_data)
278
+ slds.append(service_data)
279
+
280
+ return slds
281
+
282
+
283
+ def extract_sld_837(content: str) -> List[ServiceLevelData]:
284
+
285
+ if not content:
286
+ raise ValueError("Input X12 data cannot be empty")
287
+
288
+ # Split content into segments
289
+ segments = [seg.strip().split('*')
290
+ for seg in content.split('~') if seg.strip()]
291
+
292
+ # Detect claim type from GS segment
293
+ claim_type = None
294
+ for segment in segments:
295
+ if segment[0] == 'GS' and len(segment) > 8:
296
+ claim_type = CLAIM_TYPES.get(segment[8])
297
+ break
298
+
299
+ if not claim_type:
300
+ raise ValueError("Invalid or unsupported 837 format")
301
+
302
+ split_segments = split_into_claims(segments)
303
+ slds = []
304
+ for claim_segments in split_segments:
305
+ slds.extend(parse_837_claim_to_sld(claim_segments, claim_type))
306
+
307
+ return slds
173
308
 
174
- return encounters
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.
hccinfhir/hccinfhir.py CHANGED
@@ -88,8 +88,9 @@ class HCCInFHIR:
88
88
  # Calculate RAF score
89
89
  unique_dx_codes = self._get_unique_diagnosis_codes(sld_list)
90
90
  raf_result = self._calculate_raf_from_demographics(unique_dx_codes, demographics)
91
- raf_result['service_level_data'] = sld_list
92
- return raf_result
91
+
92
+ # Create new result with service data included
93
+ return raf_result.model_copy(update={'service_level_data': sld_list})
93
94
 
94
95
  def run_from_service_data(self, service_data: List[Union[ServiceLevelData, Dict[str, Any]]],
95
96
  demographics: Union[Demographics, Dict[str, Any]]) -> RAFResult:
@@ -122,9 +123,9 @@ class HCCInFHIR:
122
123
  # Calculate RAF score
123
124
  unique_dx_codes = self._get_unique_diagnosis_codes(standardized_data)
124
125
  raf_result = self._calculate_raf_from_demographics(unique_dx_codes, demographics)
125
- raf_result['service_level_data'] = standardized_data
126
-
127
- return raf_result
126
+
127
+ # Create new result with service data included
128
+ return raf_result.model_copy(update={'service_level_data': standardized_data})
128
129
 
129
130
  def calculate_from_diagnosis(self, diagnosis_codes: List[str],
130
131
  demographics: Union[Demographics, Dict[str, Any]]) -> RAFResult:
@@ -113,20 +113,20 @@ def calculate_raf(diagnosis_codes: List[str],
113
113
  risk_score_chronic_only = sum(coefficients_chronic_only.values()) - risk_score_demographics
114
114
  risk_score_hcc = risk_score - risk_score_demographics
115
115
 
116
- return {
117
- 'risk_score': risk_score,
118
- 'risk_score_demographics': risk_score_demographics,
119
- 'risk_score_chronic_only': risk_score_chronic_only,
120
- 'risk_score_hcc': risk_score_hcc,
121
- 'hcc_list': list(hcc_set),
122
- 'cc_to_dx': cc_to_dx,
123
- 'coefficients': coefficients,
124
- 'interactions': interactions,
125
- 'demographics': demographics,
126
- 'model_name': model_name,
127
- 'version': version,
128
- 'diagnosis_codes': diagnosis_codes,
129
- }
116
+ return RAFResult(
117
+ risk_score=risk_score,
118
+ risk_score_demographics=risk_score_demographics,
119
+ risk_score_chronic_only=risk_score_chronic_only,
120
+ risk_score_hcc=risk_score_hcc,
121
+ hcc_list=list(hcc_set),
122
+ cc_to_dx=cc_to_dx,
123
+ coefficients=coefficients,
124
+ interactions=interactions,
125
+ demographics=demographics,
126
+ model_name=model_name,
127
+ version=version,
128
+ diagnosis_codes=diagnosis_codes,
129
+ )
130
130
 
131
131
 
132
132
 
@@ -176,6 +176,7 @@ def create_disease_interactions(model_name: ModelName,
176
176
  'HF_CHR_LUNG_V28': diagnostic_cats['HF_V28'] * diagnostic_cats['CHR_LUNG_V28'],
177
177
  'HF_KIDNEY_V28': diagnostic_cats['HF_V28'] * diagnostic_cats['KIDNEY_V28'],
178
178
  'CHR_LUNG_CARD_RESP_FAIL_V28': diagnostic_cats['CHR_LUNG_V28'] * diagnostic_cats['CARD_RESP_FAIL_V28'],
179
+ 'HF_HCC238_V28': diagnostic_cats['HF_V28'] * int('238' in hcc_set),
179
180
  'gSubUseDisorder_gPsych_V28': diagnostic_cats['gSubUseDisorder_V28'] * diagnostic_cats['gPsychiatric_V28'],
180
181
  'DISABLED_CANCER_V28': demographics.disabled * diagnostic_cats['CANCER_V28'],
181
182
  'DISABLED_NEURO_V28': demographics.disabled * diagnostic_cats['NEURO_V28'],
@@ -0,0 +1,113 @@
1
+ ISA*00* *00* *ZZ*SUBMITTER ID *ZZ*RECEIVER ID *230516*1145*^*00501*000000001*0*P*:~
2
+ GS*HC*SUBMITTER ID*RECEIVER ID*20230516*1145*1*X*005010X222A1~
3
+ ST*837*0001*005010X222A1~
4
+ BHT*0019*00*244579*20230516*1145*CH~
5
+ NM1*41*2*SUBMIT CLINIC*****46*12345~
6
+ PER*IC*CONTACT NAME*TE*5555551234~
7
+ NM1*40*2*RECEIVER NAME*****46*67890~
8
+ HL*1**20*1~
9
+ NM1*85*2*BILLING PROVIDER*****XX*1234567893~
10
+ N3*123 BILLING ST~
11
+ N4*CITY*GA*30001~
12
+ REF*EI*123456789~
13
+ HL*2*1*22*0~
14
+ SBR*P*18*******MC~
15
+ NM1*IL*1*DOE*JOHN****MI*12345678901~
16
+ N3*123 PATIENT ST~
17
+ N4*CITY*GA*30001~
18
+ DMG*D8*19800101*M~
19
+ CLM*12345*150.00***11:B:1*Y*A*Y*Y~
20
+ HI*ABK:I109~
21
+ NM1*82*1*PROVIDER*JANE****XX*9876543210~
22
+ PRV*PE*ZZ*207RC0000X~
23
+ SV1*HC:J1745*150.00*UN*2*11***1~
24
+ DTP*472*D8*20230515~
25
+ LIN**N4*50242004001~
26
+ CTP***2*150.00~
27
+ SE*24*0001~
28
+ ST*837*5856*005010X223A2~
29
+ BHT*0019*00*241205204222*20241205*2042*CH~
30
+ NM1*41*2*HSA PORT ARTHUR, LLC*****XX*1194548073~
31
+ PER*IC*AIMEE GILL*TE*8186660602~
32
+ NM1*40*2*OptimaFourSight*****XX*89242VA018~
33
+ PER*IC*Jose Smith*TE*3750822093~
34
+ HL*1**20*1~
35
+ NM1*85*2*HSA PORT ARTHUR, LLC*****XX*1194548073~
36
+ N3*505 N BRAND BLVD STE 1200~
37
+ N4*GLENDALE*CA*91203~
38
+ REF*EI*71-3391736~
39
+ PER*IC*AIMEE GILL*TE*8186660602~
40
+ HL*2*1*22*0~
41
+ SBR*P*18*731323546******CI~
42
+ NM1*IL*1*Brad*Watson*D***MI*341405376684~
43
+ N3*2658 Edwards Lakes~
44
+ N4*Carterchester*AZ*20036~
45
+ CLM*4742333269*128***11:B:1*Y*A*Y*I~
46
+ DTP*434*RD8*20240422-20240430~
47
+ DTP*435*D8*20240809~
48
+ DTP*096*TM*2337~
49
+ HI*ABK:W214XXA~
50
+ HI*ABK:S31813D~
51
+ HI*ABK:V0492XD~
52
+ HI*ABK:T498X6A~
53
+ LX*1~
54
+ SV1*HC:37180*93*UN*1***3:4:1***~
55
+ DTP*472*D8*20180428~
56
+ REF*6R*142671~
57
+ LX*2~
58
+ SV1*HC:24000*4*UN*1***1:3:4***~
59
+ DTP*472*D8*20180428~
60
+ REF*6R*142671~
61
+ LX*3~
62
+ SV1*HC:16035*31*UN*1***3***~
63
+ DTP*472*D8*20180428~
64
+ REF*6R*142671~
65
+ SE*38*5856~
66
+ ST*837*4763033*005010X223A2~
67
+ BHT*0019*00*241205204221*20241205*2042*CH~
68
+ NM1*41*2*HCA HEALTH SERVICES OF TENNESSEE, INC.*****XX*1265487193~
69
+ PER*IC*DAVID SUMMERS*TE*6153421005~
70
+ NM1*40*2*HMOOffExchangeRegion7*****XX*84014CA002~
71
+ PER*IC*Devon Tran*TE*1808509992~
72
+ HL*1**20*1~
73
+ NM1*85*2*HCA HEALTH SERVICES OF TENNESSEE, INC.*****XX*1265487193~
74
+ N3*313 N MAIN ST~
75
+ N4*ASHLAND CITY*TN*37015~
76
+ REF*EI*99-5971744~
77
+ PER*IC*DAVID SUMMERS*TE*6153421005~
78
+ HL*2*1*22*0~
79
+ SBR*P*18*556791994******CI~
80
+ NM1*IL*1*Jessica*Rodriguez*G***MI*151319361359~
81
+ N3*3226 Andrew Point~
82
+ N4*North Brentville*IA*27129~
83
+ CLM*4742333269*839***11:B:1*Y*A*Y*I~
84
+ DTP*434*RD8*20240422-20240430~
85
+ DTP*435*D8*20240809~
86
+ DTP*096*TM*2337~
87
+ HI*ABK:V9421XS~
88
+ HI*ABK:S35292S~
89
+ HI*ABK:S52272S~
90
+ HI*ABK:H68022~
91
+ HI*ABK:T4144XD~
92
+ HI*ABK:H1030~
93
+ HI*ABK:S82832J~
94
+ HI*ABK:B340~
95
+ LX*1~
96
+ SV1*HC:35650*161*UN*1***7:3:4:5:8***~
97
+ DTP*472*D8*20180428~
98
+ REF*6R*142671~
99
+ LX*2~
100
+ SV1*HC:73200*383*UN*1***5:2:7:4:3:6:8:1***~
101
+ DTP*472*D8*20180428~
102
+ REF*6R*142671~
103
+ LX*3~
104
+ SV1*HC:28262*194*UN*1***7:1:8***~
105
+ DTP*472*D8*20180428~
106
+ REF*6R*142671~
107
+ LX*4~
108
+ SV1*HC:84480*101*UN*1***6:3:1:2***~
109
+ DTP*472*D8*20180428~
110
+ REF*6R*142671~
111
+ SE*46*4763033~
112
+ GE*1*1~
113
+ IEA*1*000000001~
hccinfhir/samples.py CHANGED
@@ -43,7 +43,7 @@ class SampleData:
43
43
  raise ValueError("case_number must be 1, 2, or 3")
44
44
 
45
45
  try:
46
- with importlib.resources.open_text('hccinfhir.samples', f'sample_eob_{case_number}.json') as f:
46
+ with importlib.resources.open_text('hccinfhir.sample_files', f'sample_eob_{case_number}.json') as f:
47
47
  return json.load(f)
48
48
  except FileNotFoundError:
49
49
  raise FileNotFoundError(f"Sample EOB case {case_number} not found")
@@ -75,7 +75,7 @@ class SampleData:
75
75
  """
76
76
  try:
77
77
  output = []
78
- with importlib.resources.open_text('hccinfhir.samples', 'sample_eob_200.ndjson') as f:
78
+ with importlib.resources.open_text('hccinfhir.sample_files', 'sample_eob_200.ndjson') as f:
79
79
  for i, line in enumerate(f):
80
80
  if limit is not None and i >= limit:
81
81
  break
@@ -91,13 +91,13 @@ class SampleData:
91
91
  Retrieve a specific 837 claim sample by case number.
92
92
 
93
93
  Args:
94
- case_number: The case number (0 through 11). Default is 0.
94
+ case_number: The case number (0 through 12). Default is 0.
95
95
 
96
96
  Returns:
97
97
  A string containing the 837 X12 claim data
98
98
 
99
99
  Raises:
100
- ValueError: If case_number is not between 0 and 11
100
+ ValueError: If case_number is not between 0 and 12
101
101
  FileNotFoundError: If the sample file cannot be found
102
102
 
103
103
  Example:
@@ -105,11 +105,11 @@ class SampleData:
105
105
  >>> print("ISA" in sample_837)
106
106
  True
107
107
  """
108
- if case_number < 0 or case_number > 11:
109
- raise ValueError("case_number must be between 0 and 11")
108
+ if case_number < 0 or case_number > 12:
109
+ raise ValueError("case_number must be between 0 and 12")
110
110
 
111
111
  try:
112
- with importlib.resources.open_text('hccinfhir.samples', f'sample_837_{case_number}.txt') as f:
112
+ with importlib.resources.open_text('hccinfhir.sample_files', f'sample_837_{case_number}.txt') as f:
113
113
  return f.read()
114
114
  except FileNotFoundError:
115
115
  raise FileNotFoundError(f"Sample 837 case {case_number} not found")
@@ -138,20 +138,20 @@ class SampleData:
138
138
  >>> # Get all samples
139
139
  >>> all_samples = SampleData.get_837_sample_list()
140
140
  >>> print(len(all_samples))
141
- 12
141
+ 13
142
142
  """
143
143
  if case_numbers is None:
144
- case_numbers = list(range(12)) # 0 through 11
144
+ case_numbers = list(range(13)) # 0 through 12
145
145
 
146
146
  # Validate case numbers
147
147
  for case_num in case_numbers:
148
- if case_num < 0 or case_num > 11:
149
- raise ValueError(f"case_number {case_num} must be between 0 and 11")
148
+ if case_num < 0 or case_num > 12:
149
+ raise ValueError(f"case_number {case_num} must be between 0 and 12")
150
150
 
151
151
  output = []
152
152
  for case_num in case_numbers:
153
153
  try:
154
- with importlib.resources.open_text('hccinfhir.samples', f'sample_837_{case_num}.txt') as f:
154
+ with importlib.resources.open_text('hccinfhir.sample_files', f'sample_837_{case_num}.txt') as f:
155
155
  output.append(f.read())
156
156
  except FileNotFoundError:
157
157
  raise FileNotFoundError(f"Sample 837 case {case_num} not found")
@@ -180,8 +180,8 @@ class SampleData:
180
180
  ],
181
181
  "eob_case_numbers": [1, 2, 3],
182
182
  "eob_list_size": 200,
183
- "837_samples": [f"sample_837_{i}.txt" for i in range(12)],
184
- "837_case_numbers": list(range(12)),
183
+ "837_samples": [f"sample_837_{i}.txt" for i in range(13)],
184
+ "837_case_numbers": list(range(13)),
185
185
  "description": {
186
186
  "eob": "Explanation of Benefits (FHIR resources) for testing HCC calculations",
187
187
  "837": "X12 837 claim data for testing claim processing"
@@ -221,7 +221,7 @@ def get_837_sample(case_number: int = 0) -> str:
221
221
  Convenience function to get an 837 claim sample.
222
222
 
223
223
  Args:
224
- case_number: The case number (0 through 11). Default is 0.
224
+ case_number: The case number (0 through 12). Default is 0.
225
225
 
226
226
  Returns:
227
227
  A string containing the 837 X12 claim data