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.
- hccinfhir/__init__.py +1 -1
- hccinfhir/datamodels.py +17 -15
- hccinfhir/extractor.py +2 -2
- hccinfhir/extractor_837.py +178 -44
- hccinfhir/filter.py +2 -2
- hccinfhir/hccinfhir.py +6 -5
- hccinfhir/model_calculate.py +14 -14
- hccinfhir/model_interactions.py +1 -0
- hccinfhir/sample_files/sample_837_12.txt +113 -0
- hccinfhir/samples.py +15 -15
- hccinfhir-0.1.4.dist-info/METADATA +611 -0
- hccinfhir-0.1.4.dist-info/RECORD +49 -0
- hccinfhir/sample_utils.py +0 -252
- hccinfhir-0.1.2.dist-info/METADATA +0 -390
- hccinfhir-0.1.2.dist-info/RECORD +0 -49
- /hccinfhir/{samples → sample_files}/__init__.py +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_0.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_1.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_10.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_11.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_2.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_3.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_4.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_5.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_6.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_7.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_8.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_837_9.txt +0 -0
- /hccinfhir/{samples → sample_files}/sample_eob_1.json +0 -0
- /hccinfhir/{samples → sample_files}/sample_eob_2.json +0 -0
- /hccinfhir/{samples → sample_files}/sample_eob_200.ndjson +0 -0
- /hccinfhir/{samples → sample_files}/sample_eob_3.json +0 -0
- {hccinfhir-0.1.2.dist-info → hccinfhir-0.1.4.dist-info}/WHEEL +0 -0
- {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 .
|
|
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(
|
|
93
|
-
"""
|
|
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]],
|
|
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:
|
hccinfhir/extractor_837.py
CHANGED
|
@@ -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],
|
|
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
|
|
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
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
74
|
-
"""
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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 == '
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if
|
|
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=
|
|
167
|
-
quantity=
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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:
|
hccinfhir/model_calculate.py
CHANGED
|
@@ -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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
hccinfhir/model_interactions.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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 >
|
|
109
|
-
raise ValueError("case_number must be between 0 and
|
|
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.
|
|
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
|
-
|
|
141
|
+
13
|
|
142
142
|
"""
|
|
143
143
|
if case_numbers is None:
|
|
144
|
-
case_numbers = list(range(
|
|
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 >
|
|
149
|
-
raise ValueError(f"case_number {case_num} must be between 0 and
|
|
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.
|
|
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(
|
|
184
|
-
"837_case_numbers": list(range(
|
|
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
|
|
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
|