hccinfhir 0.1.9__py3-none-any.whl → 0.2.1__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 +2 -0
- hccinfhir/constants.py +240 -0
- hccinfhir/datamodels.py +39 -8
- hccinfhir/defaults.py +31 -0
- hccinfhir/extractor_834.py +52 -71
- hccinfhir/extractor_837.py +2 -2
- hccinfhir/hccinfhir.py +27 -10
- hccinfhir/model_calculate.py +27 -23
- hccinfhir/model_coefficients.py +4 -31
- hccinfhir/model_demographics.py +26 -29
- hccinfhir/model_dx_to_cc.py +6 -11
- hccinfhir/model_hierarchies.py +6 -35
- hccinfhir/model_interactions.py +7 -7
- hccinfhir/samples.py +50 -5
- hccinfhir/utils.py +217 -44
- hccinfhir-0.2.1.dist-info/METADATA +946 -0
- {hccinfhir-0.1.9.dist-info → hccinfhir-0.2.1.dist-info}/RECORD +19 -17
- hccinfhir-0.1.9.dist-info/METADATA +0 -782
- {hccinfhir-0.1.9.dist-info → hccinfhir-0.2.1.dist-info}/WHEEL +0 -0
- {hccinfhir-0.1.9.dist-info → hccinfhir-0.2.1.dist-info}/licenses/LICENSE +0 -0
hccinfhir/__init__.py
CHANGED
|
@@ -18,6 +18,7 @@ from .samples import (
|
|
|
18
18
|
get_eob_sample_list,
|
|
19
19
|
get_837_sample,
|
|
20
20
|
get_837_sample_list,
|
|
21
|
+
get_834_sample,
|
|
21
22
|
list_available_samples
|
|
22
23
|
)
|
|
23
24
|
|
|
@@ -43,5 +44,6 @@ __all__ = [
|
|
|
43
44
|
"get_eob_sample_list",
|
|
44
45
|
"get_837_sample",
|
|
45
46
|
"get_837_sample_list",
|
|
47
|
+
"get_834_sample",
|
|
46
48
|
"list_available_samples"
|
|
47
49
|
]
|
hccinfhir/constants.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CMS Risk Adjustment Domain Constants
|
|
3
|
+
|
|
4
|
+
This module contains constants used across the HCC risk adjustment system,
|
|
5
|
+
including dual eligibility codes, OREC/CREC values, and state-specific mappings.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- CMS Rate Announcement and Call Letter
|
|
9
|
+
- Medicare Advantage Enrollment and Disenrollment Guidance
|
|
10
|
+
- X12 834 Implementation Guides
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Set, Dict
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# DUAL ELIGIBILITY CODES
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# CMS Dual Eligibility Status Codes (Medicare + Medicaid)
|
|
19
|
+
# Used in coefficient prefix selection (CNA_, CFA_, CPA_, etc.)
|
|
20
|
+
|
|
21
|
+
VALID_DUAL_CODES: Set[str] = {'00', '01', '02', '03', '04', '05', '06', '08'}
|
|
22
|
+
|
|
23
|
+
# Non-Dual Eligible
|
|
24
|
+
NON_DUAL_CODE: str = '00'
|
|
25
|
+
|
|
26
|
+
# Full Benefit Dual Eligible (receive both Medicare and full Medicaid benefits)
|
|
27
|
+
# Uses CFA_ (Community, Full Benefit Dual, Aged) or CFD_ (Disabled) prefixes
|
|
28
|
+
FULL_BENEFIT_DUAL_CODES: Set[str] = {
|
|
29
|
+
'02', # QMB Plus (Qualified Medicare Beneficiary Plus)
|
|
30
|
+
'04', # SLMB Plus (Specified Low-Income Medicare Beneficiary Plus)
|
|
31
|
+
'08', # Other Full Benefit Dual Eligible
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Partial Benefit Dual Eligible (Medicare + limited Medicaid)
|
|
35
|
+
# Uses CPA_ (Community, Partial Benefit Dual, Aged) or CPD_ (Disabled) prefixes
|
|
36
|
+
PARTIAL_BENEFIT_DUAL_CODES: Set[str] = {
|
|
37
|
+
'01', # QMB Only
|
|
38
|
+
'03', # SLMB Only
|
|
39
|
+
'05', # QDWI (Qualified Disabled and Working Individual)
|
|
40
|
+
'06', # QI (Qualifying Individual)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# OREC - Original Reason for Entitlement Code
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# Determines if beneficiary has ESRD and affects coefficient prefix selection
|
|
47
|
+
|
|
48
|
+
VALID_OREC_VALUES: Set[str] = {'0', '1', '2', '3'}
|
|
49
|
+
|
|
50
|
+
OREC_DESCRIPTIONS: Dict[str, str] = {
|
|
51
|
+
'0': 'Old Age and Survivors Insurance (OASI)',
|
|
52
|
+
'1': 'Disability Insurance Benefits (DIB)',
|
|
53
|
+
'2': 'ESRD - End-Stage Renal Disease',
|
|
54
|
+
'3': 'DIB and ESRD',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# OREC codes indicating ESRD status (per CMS documentation)
|
|
58
|
+
OREC_ESRD_CODES: Set[str] = {'2', '3'}
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# CREC - Current Reason for Entitlement Code
|
|
62
|
+
# =============================================================================
|
|
63
|
+
# Current entitlement status (may differ from OREC)
|
|
64
|
+
|
|
65
|
+
VALID_CREC_VALUES: Set[str] = {'0', '1', '2', '3'}
|
|
66
|
+
|
|
67
|
+
CREC_DESCRIPTIONS: Dict[str, str] = {
|
|
68
|
+
'0': 'Old Age and Survivors Insurance (OASI)',
|
|
69
|
+
'1': 'Disability Insurance Benefits (DIB)',
|
|
70
|
+
'2': 'ESRD - End-Stage Renal Disease',
|
|
71
|
+
'3': 'DIB and ESRD',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# CREC codes indicating ESRD status
|
|
75
|
+
CREC_ESRD_CODES: Set[str] = {'2', '3'}
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# COEFFICIENT PREFIX GROUPS
|
|
79
|
+
# =============================================================================
|
|
80
|
+
# Used for prefix_override logic in model_demographics.py
|
|
81
|
+
|
|
82
|
+
# ESRD model prefixes
|
|
83
|
+
ESRD_PREFIXES: Set[str] = {'DI_', 'DNE_', 'GI_', 'GNE_', 'GFPA_', 'GFPN_', 'GNPA_', 'GNPN_'}
|
|
84
|
+
|
|
85
|
+
# CMS-HCC new enrollee prefixes
|
|
86
|
+
NEW_ENROLLEE_PREFIXES: Set[str] = {'NE_', 'SNPNE_', 'DNE_', 'GNE_'}
|
|
87
|
+
|
|
88
|
+
# CMS-HCC community prefixes
|
|
89
|
+
COMMUNITY_PREFIXES: Set[str] = {'CNA_', 'CND_', 'CFA_', 'CFD_', 'CPA_', 'CPD_'}
|
|
90
|
+
|
|
91
|
+
# Institutionalized prefixes
|
|
92
|
+
INSTITUTIONAL_PREFIXES: Set[str] = {'INS_', 'GI_'}
|
|
93
|
+
|
|
94
|
+
# Full Benefit Dual prefixes
|
|
95
|
+
FULL_BENEFIT_DUAL_PREFIXES: Set[str] = {'CFA_', 'CFD_', 'GFPA_', 'GFPN_'}
|
|
96
|
+
|
|
97
|
+
# Partial Benefit Dual prefixes
|
|
98
|
+
PARTIAL_BENEFIT_DUAL_PREFIXES: Set[str] = {'CPA_', 'CPD_'}
|
|
99
|
+
|
|
100
|
+
# Non-Dual prefixes
|
|
101
|
+
NON_DUAL_PREFIXES: Set[str] = {'CNA_', 'CND_', 'GNPA_', 'GNPN_'}
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# DEMOGRAPHIC CODES
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
VALID_SEX_CODES: Set[str] = {'M', 'F'}
|
|
108
|
+
|
|
109
|
+
# X12 834 Gender Code mappings
|
|
110
|
+
X12_SEX_CODE_MAPPING: Dict[str, str] = {
|
|
111
|
+
'M': 'M',
|
|
112
|
+
'F': 'F',
|
|
113
|
+
'1': 'M', # X12 numeric code
|
|
114
|
+
'2': 'F', # X12 numeric code
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# =============================================================================
|
|
118
|
+
# X12 834 MAINTENANCE TYPE CODES
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# INS03 - Maintenance Type Code
|
|
121
|
+
|
|
122
|
+
MAINTENANCE_TYPE_CHANGE: str = '001'
|
|
123
|
+
MAINTENANCE_TYPE_ADD: str = '021'
|
|
124
|
+
MAINTENANCE_TYPE_CANCEL: str = '024'
|
|
125
|
+
MAINTENANCE_TYPE_REINSTATE: str = '025'
|
|
126
|
+
|
|
127
|
+
MAINTENANCE_TYPE_DESCRIPTIONS: Dict[str, str] = {
|
|
128
|
+
'001': 'Change',
|
|
129
|
+
'021': 'Addition',
|
|
130
|
+
'024': 'Cancellation/Termination',
|
|
131
|
+
'025': 'Reinstatement',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# STATE-SPECIFIC MAPPINGS
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
# -----------------------------------------------------------------------------
|
|
139
|
+
# California DHCS Medi-Cal Aid Codes
|
|
140
|
+
# -----------------------------------------------------------------------------
|
|
141
|
+
# Maps California-specific aid codes to CMS dual eligibility codes
|
|
142
|
+
# Source: California DHCS 834 Implementation Guide
|
|
143
|
+
|
|
144
|
+
MEDI_CAL_AID_CODES: Dict[str, str] = {
|
|
145
|
+
# Full Benefit Dual (QMB Plus, SLMB Plus)
|
|
146
|
+
'4N': '02', # QMB Plus - Aged
|
|
147
|
+
'4P': '02', # QMB Plus - Disabled
|
|
148
|
+
'5B': '04', # SLMB Plus - Aged
|
|
149
|
+
'5D': '04', # SLMB Plus - Disabled
|
|
150
|
+
|
|
151
|
+
# Partial Benefit Dual (QMB Only, SLMB Only, QI)
|
|
152
|
+
'4M': '01', # QMB Only - Aged
|
|
153
|
+
'4O': '01', # QMB Only - Disabled
|
|
154
|
+
'5A': '03', # SLMB Only - Aged
|
|
155
|
+
'5C': '03', # SLMB Only - Disabled
|
|
156
|
+
'5E': '06', # QI - Aged
|
|
157
|
+
'5F': '06', # QI - Disabled
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# -----------------------------------------------------------------------------
|
|
161
|
+
# Medicare Status Code Mappings
|
|
162
|
+
# -----------------------------------------------------------------------------
|
|
163
|
+
# Maps Medicare status codes (from various sources) to CMS dual eligibility codes
|
|
164
|
+
# Used in X12 834 REF*ABB segment and other payer files
|
|
165
|
+
|
|
166
|
+
MEDICARE_STATUS_CODE_MAPPING: Dict[str, str] = {
|
|
167
|
+
# QMB - Qualified Medicare Beneficiary
|
|
168
|
+
'QMB': '01', # QMB Only (Partial)
|
|
169
|
+
'QMBONLY': '01',
|
|
170
|
+
'QMBPLUS': '02', # QMB Plus (Full Benefit)
|
|
171
|
+
'QMB+': '02',
|
|
172
|
+
|
|
173
|
+
# SLMB - Specified Low-Income Medicare Beneficiary
|
|
174
|
+
'SLMB': '03', # SLMB Only (Partial)
|
|
175
|
+
'SLMBONLY': '03',
|
|
176
|
+
'SLMBPLUS': '04', # SLMB Plus (Full Benefit)
|
|
177
|
+
'SLMB+': '04',
|
|
178
|
+
|
|
179
|
+
# Other dual eligibility programs
|
|
180
|
+
'QDWI': '05', # Qualified Disabled and Working Individual
|
|
181
|
+
'QI': '06', # Qualifying Individual
|
|
182
|
+
'QI1': '06',
|
|
183
|
+
'FBDE': '08', # Full Benefit Dual Eligible (Other)
|
|
184
|
+
'OTHERFULL': '08',
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# =============================================================================
|
|
188
|
+
# HELPER FUNCTIONS
|
|
189
|
+
# =============================================================================
|
|
190
|
+
|
|
191
|
+
def is_full_benefit_dual(dual_code: str) -> bool:
|
|
192
|
+
"""Check if dual eligibility code is Full Benefit Dual"""
|
|
193
|
+
return dual_code in FULL_BENEFIT_DUAL_CODES
|
|
194
|
+
|
|
195
|
+
def is_partial_benefit_dual(dual_code: str) -> bool:
|
|
196
|
+
"""Check if dual eligibility code is Partial Benefit Dual"""
|
|
197
|
+
return dual_code in PARTIAL_BENEFIT_DUAL_CODES
|
|
198
|
+
|
|
199
|
+
def is_esrd_by_orec(orec: str) -> bool:
|
|
200
|
+
"""Check if OREC indicates ESRD status"""
|
|
201
|
+
return orec in OREC_ESRD_CODES
|
|
202
|
+
|
|
203
|
+
def is_esrd_by_crec(crec: str) -> bool:
|
|
204
|
+
"""Check if CREC indicates ESRD status"""
|
|
205
|
+
return crec in CREC_ESRD_CODES
|
|
206
|
+
|
|
207
|
+
def normalize_medicare_status_code(status: str) -> str:
|
|
208
|
+
"""Normalize Medicare status code (uppercase, no spaces/hyphens)"""
|
|
209
|
+
if not status:
|
|
210
|
+
return ''
|
|
211
|
+
return status.upper().replace(' ', '').replace('-', '')
|
|
212
|
+
|
|
213
|
+
def map_medicare_status_to_dual_code(status: str) -> str:
|
|
214
|
+
"""Map Medicare status code to dual eligibility code
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
status: Medicare status code (e.g., 'QMB Plus', 'SLMB', 'QI')
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dual eligibility code ('01'-'08') or '00' if not found
|
|
221
|
+
"""
|
|
222
|
+
if not status:
|
|
223
|
+
return NON_DUAL_CODE
|
|
224
|
+
|
|
225
|
+
normalized = normalize_medicare_status_code(status)
|
|
226
|
+
return MEDICARE_STATUS_CODE_MAPPING.get(normalized, NON_DUAL_CODE)
|
|
227
|
+
|
|
228
|
+
def map_aid_code_to_dual_status(aid_code: str) -> str:
|
|
229
|
+
"""Map California Medi-Cal aid code to dual eligibility code
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
aid_code: California aid code (e.g., '4N', '5B')
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dual eligibility code ('01'-'08') or '00' if not found
|
|
236
|
+
"""
|
|
237
|
+
if not aid_code:
|
|
238
|
+
return NON_DUAL_CODE
|
|
239
|
+
|
|
240
|
+
return MEDI_CAL_AID_CODES.get(aid_code, NON_DUAL_CODE)
|
hccinfhir/datamodels.py
CHANGED
|
@@ -11,16 +11,47 @@ ModelName = Literal[
|
|
|
11
11
|
"RxHCC Model V08"
|
|
12
12
|
]
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
# Filename types: allow bundled filenames (with autocomplete) OR any custom string path
|
|
15
|
+
ProcFilteringFilename = Union[
|
|
16
|
+
Literal[
|
|
17
|
+
"ra_eligible_cpt_hcpcs_2023.csv",
|
|
18
|
+
"ra_eligible_cpt_hcpcs_2024.csv",
|
|
19
|
+
"ra_eligible_cpt_hcpcs_2025.csv",
|
|
20
|
+
"ra_eligible_cpt_hcpcs_2026.csv"
|
|
21
|
+
],
|
|
22
|
+
str # Allow any custom file path
|
|
19
23
|
]
|
|
20
24
|
|
|
21
|
-
DxCCMappingFilename =
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
DxCCMappingFilename = Union[
|
|
26
|
+
Literal[
|
|
27
|
+
"ra_dx_to_cc_2025.csv",
|
|
28
|
+
"ra_dx_to_cc_2026.csv"
|
|
29
|
+
],
|
|
30
|
+
str
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
HierarchiesFilename = Union[
|
|
34
|
+
Literal[
|
|
35
|
+
"ra_hierarchies_2025.csv",
|
|
36
|
+
"ra_hierarchies_2026.csv"
|
|
37
|
+
],
|
|
38
|
+
str
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
IsChronicFilename = Union[
|
|
42
|
+
Literal[
|
|
43
|
+
"hcc_is_chronic.csv",
|
|
44
|
+
"hcc_is_chronic_without_esrd_model.csv"
|
|
45
|
+
],
|
|
46
|
+
str
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
CoefficientsFilename = Union[
|
|
50
|
+
Literal[
|
|
51
|
+
"ra_coefficients_2025.csv",
|
|
52
|
+
"ra_coefficients_2026.csv"
|
|
53
|
+
],
|
|
54
|
+
str
|
|
24
55
|
]
|
|
25
56
|
|
|
26
57
|
PrefixOverride = Literal[
|
hccinfhir/defaults.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central location for all default data file loading.
|
|
3
|
+
|
|
4
|
+
This module loads all default data files at import time and provides them
|
|
5
|
+
to the public API functions (calculate_raf) and tests. Internal model_*
|
|
6
|
+
modules should not load their own defaults - they should receive data
|
|
7
|
+
as explicit parameters.
|
|
8
|
+
|
|
9
|
+
Default files are for the 2026 model year.
|
|
10
|
+
"""
|
|
11
|
+
from typing import Dict, Set, Tuple
|
|
12
|
+
from hccinfhir.datamodels import ModelName
|
|
13
|
+
from hccinfhir.utils import (
|
|
14
|
+
load_dx_to_cc_mapping,
|
|
15
|
+
load_hierarchies,
|
|
16
|
+
load_is_chronic,
|
|
17
|
+
load_coefficients,
|
|
18
|
+
load_proc_filtering
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Load all default data files once at module import time
|
|
22
|
+
# These are used by:
|
|
23
|
+
# - calculate_raf() function for direct usage
|
|
24
|
+
# - HCCInFHIR class (though it can override with custom files)
|
|
25
|
+
# - Tests that need default data
|
|
26
|
+
|
|
27
|
+
dx_to_cc_default: Dict[Tuple[str, ModelName], Set[str]] = load_dx_to_cc_mapping('ra_dx_to_cc_2026.csv')
|
|
28
|
+
hierarchies_default: Dict[Tuple[str, ModelName], Set[str]] = load_hierarchies('ra_hierarchies_2026.csv')
|
|
29
|
+
is_chronic_default: Dict[Tuple[str, ModelName], bool] = load_is_chronic('hcc_is_chronic.csv')
|
|
30
|
+
coefficients_default: Dict[Tuple[str, ModelName], float] = load_coefficients('ra_coefficients_2026.csv')
|
|
31
|
+
proc_filtering_default: Set[str] = load_proc_filtering('ra_eligible_cpt_hcpcs_2026.csv')
|
hccinfhir/extractor_834.py
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
from typing import List, Optional, Dict
|
|
1
|
+
from typing import List, Optional, Dict, Any, Tuple
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
from datetime import datetime, date
|
|
4
4
|
from hccinfhir.datamodels import Demographics, EnrollmentData
|
|
5
|
+
from hccinfhir.constants import (
|
|
6
|
+
VALID_DUAL_CODES,
|
|
7
|
+
FULL_BENEFIT_DUAL_CODES,
|
|
8
|
+
PARTIAL_BENEFIT_DUAL_CODES,
|
|
9
|
+
VALID_OREC_VALUES,
|
|
10
|
+
VALID_CREC_VALUES,
|
|
11
|
+
X12_SEX_CODE_MAPPING,
|
|
12
|
+
NON_DUAL_CODE,
|
|
13
|
+
map_medicare_status_to_dual_code,
|
|
14
|
+
map_aid_code_to_dual_status,
|
|
15
|
+
)
|
|
5
16
|
|
|
6
17
|
TRANSACTION_TYPES = {
|
|
7
18
|
"005010X220A1": "834", # Benefit Enrollment and Maintenance
|
|
8
19
|
}
|
|
9
20
|
|
|
10
|
-
# California Medi-Cal Aid Codes mapping to dual eligibility status
|
|
11
|
-
MEDI_CAL_AID_CODES = {
|
|
12
|
-
# Full Benefit Dual (QMB Plus, SLMB Plus)
|
|
13
|
-
'4N': '02', # QMB Plus - Aged
|
|
14
|
-
'4P': '02', # QMB Plus - Disabled
|
|
15
|
-
'5B': '04', # SLMB Plus - Aged
|
|
16
|
-
'5D': '04', # SLMB Plus - Disabled
|
|
17
|
-
|
|
18
|
-
# Partial Benefit Dual (QMB Only, SLMB Only, QI)
|
|
19
|
-
'4M': '01', # QMB Only - Aged
|
|
20
|
-
'4O': '01', # QMB Only - Disabled
|
|
21
|
-
'5A': '03', # SLMB Only - Aged
|
|
22
|
-
'5C': '03', # SLMB Only - Disabled
|
|
23
|
-
'5E': '06', # QI - Aged
|
|
24
|
-
'5F': '06', # QI - Disabled
|
|
25
|
-
}
|
|
26
|
-
|
|
27
21
|
class MemberContext(BaseModel):
|
|
28
22
|
"""Tracks member-level data across segments within 834 transaction"""
|
|
29
23
|
# Identifiers
|
|
@@ -95,7 +89,7 @@ def is_medicaid_terminated(enrollment: EnrollmentData) -> bool:
|
|
|
95
89
|
"""Check if Medicaid coverage is being terminated (maintenance type 024)"""
|
|
96
90
|
return enrollment.maintenance_type == '024'
|
|
97
91
|
|
|
98
|
-
def medicaid_status_summary(enrollment: EnrollmentData) -> Dict[str,
|
|
92
|
+
def medicaid_status_summary(enrollment: EnrollmentData) -> Dict[str, Any]:
|
|
99
93
|
"""Get summary of Medicaid coverage status for monitoring
|
|
100
94
|
|
|
101
95
|
Args:
|
|
@@ -157,45 +151,28 @@ def get_segment_value(segment: List[str], index: int, default: Optional[str] = N
|
|
|
157
151
|
pass
|
|
158
152
|
return default
|
|
159
153
|
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
154
|
+
def parse_composite_ref_value(value: str) -> str:
|
|
155
|
+
"""Parse X12 composite element format: 'qualifier;id;...'
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
X12 uses semicolons to separate sub-elements within a composite data element.
|
|
158
|
+
Example: REF*23*9;20061234; where 9 is the ID type qualifier
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
value: Raw REF segment value (e.g., '9;20061234;' or '20061234')
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The last non-empty sub-element (the actual ID)
|
|
170
165
|
"""
|
|
171
|
-
if not
|
|
172
|
-
return
|
|
166
|
+
if not value:
|
|
167
|
+
return value
|
|
173
168
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
'QMBONLY': '01',
|
|
179
|
-
'QMBPLUS': '02', # QMB Plus (Full Benefit)
|
|
180
|
-
'QMB+': '02',
|
|
181
|
-
'SLMB': '03', # SLMB Only (Partial)
|
|
182
|
-
'SLMBONLY': '03',
|
|
183
|
-
'SLMBPLUS': '04', # SLMB Plus (Full Benefit)
|
|
184
|
-
'SLMB+': '04',
|
|
185
|
-
'QDWI': '05',
|
|
186
|
-
'QI': '06',
|
|
187
|
-
'QI1': '06',
|
|
188
|
-
'FBDE': '08', # Full Benefit Dual Eligible (Other)
|
|
189
|
-
'OTHERFULL': '08',
|
|
190
|
-
}
|
|
169
|
+
if ';' in value:
|
|
170
|
+
# Split by semicolon and filter out empty parts
|
|
171
|
+
parts = [p for p in value.split(';') if p]
|
|
172
|
+
return parts[-1] if parts else value
|
|
191
173
|
|
|
192
|
-
return
|
|
174
|
+
return value
|
|
193
175
|
|
|
194
|
-
def map_aid_code_to_dual_status(aid_code: Optional[str]) -> Optional[str]:
|
|
195
|
-
"""Map California Medi-Cal aid code to dual eligibility status"""
|
|
196
|
-
if not aid_code:
|
|
197
|
-
return None
|
|
198
|
-
return MEDI_CAL_AID_CODES.get(aid_code)
|
|
199
176
|
|
|
200
177
|
def determine_dual_status(member: MemberContext) -> str:
|
|
201
178
|
"""Intelligently derive dual eligibility code from available data
|
|
@@ -208,19 +185,19 @@ def determine_dual_status(member: MemberContext) -> str:
|
|
|
208
185
|
5. Default to non-dual ('00')
|
|
209
186
|
"""
|
|
210
187
|
# Priority 1: Explicit dual_elgbl_cd
|
|
211
|
-
if member.dual_elgbl_cd and member.dual_elgbl_cd in
|
|
188
|
+
if member.dual_elgbl_cd and member.dual_elgbl_cd in VALID_DUAL_CODES:
|
|
212
189
|
return member.dual_elgbl_cd
|
|
213
190
|
|
|
214
191
|
# Priority 2: California aid code mapping
|
|
215
192
|
if member.medi_cal_aid_code:
|
|
216
193
|
dual_code = map_aid_code_to_dual_status(member.medi_cal_aid_code)
|
|
217
|
-
if dual_code:
|
|
194
|
+
if dual_code != NON_DUAL_CODE:
|
|
218
195
|
return dual_code
|
|
219
196
|
|
|
220
197
|
# Priority 3: Medicare status code
|
|
221
198
|
if member.medicare_status_code:
|
|
222
199
|
dual_code = map_medicare_status_to_dual_code(member.medicare_status_code)
|
|
223
|
-
if dual_code:
|
|
200
|
+
if dual_code != NON_DUAL_CODE:
|
|
224
201
|
return dual_code
|
|
225
202
|
|
|
226
203
|
# Priority 4: Both Medicare and Medicaid coverage present
|
|
@@ -229,9 +206,9 @@ def determine_dual_status(member: MemberContext) -> str:
|
|
|
229
206
|
return '08'
|
|
230
207
|
|
|
231
208
|
# Default: Non-dual
|
|
232
|
-
return
|
|
209
|
+
return NON_DUAL_CODE
|
|
233
210
|
|
|
234
|
-
def classify_dual_benefit_level(dual_code: str) ->
|
|
211
|
+
def classify_dual_benefit_level(dual_code: str) -> Tuple[bool, bool]:
|
|
235
212
|
"""Classify as Full Benefit Dual (FBD) or Partial Benefit Dual (PBD)
|
|
236
213
|
|
|
237
214
|
Full Benefit Dual codes: 02, 04, 08
|
|
@@ -242,11 +219,8 @@ def classify_dual_benefit_level(dual_code: str) -> tuple[bool, bool]:
|
|
|
242
219
|
- Uses CPA_ (Community, Partial Benefit Dual, Aged) prefix
|
|
243
220
|
- Uses CPD_ (Community, Partial Benefit Dual, Disabled) prefix
|
|
244
221
|
"""
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
is_fbd = dual_code in full_benefit_codes
|
|
249
|
-
is_pbd = dual_code in partial_benefit_codes
|
|
222
|
+
is_fbd = dual_code in FULL_BENEFIT_DUAL_CODES
|
|
223
|
+
is_pbd = dual_code in PARTIAL_BENEFIT_DUAL_CODES
|
|
250
224
|
|
|
251
225
|
return is_fbd, is_pbd
|
|
252
226
|
|
|
@@ -284,7 +258,7 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
284
258
|
enrollments = []
|
|
285
259
|
member = MemberContext()
|
|
286
260
|
|
|
287
|
-
for
|
|
261
|
+
for segment in segments:
|
|
288
262
|
if len(segment) < 2:
|
|
289
263
|
continue
|
|
290
264
|
|
|
@@ -330,11 +304,11 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
330
304
|
|
|
331
305
|
# Medicaid Identifiers
|
|
332
306
|
elif qualifier == '1D': # Medicaid/Recipient ID
|
|
333
|
-
member.medicaid_id = value
|
|
307
|
+
member.medicaid_id = parse_composite_ref_value(value)
|
|
334
308
|
member.has_medicaid = True
|
|
335
309
|
elif qualifier == '23': # Medicaid Recipient ID (alternative)
|
|
336
310
|
if not member.medicaid_id:
|
|
337
|
-
member.medicaid_id = value
|
|
311
|
+
member.medicaid_id = parse_composite_ref_value(value)
|
|
338
312
|
member.has_medicaid = True
|
|
339
313
|
|
|
340
314
|
# California Medi-Cal Specific
|
|
@@ -345,13 +319,13 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
345
319
|
|
|
346
320
|
# Custom dual eligibility indicators
|
|
347
321
|
elif qualifier == 'F5': # Dual Eligibility Code (custom)
|
|
348
|
-
if value in
|
|
322
|
+
if value in VALID_DUAL_CODES:
|
|
349
323
|
member.dual_elgbl_cd = value
|
|
350
324
|
elif qualifier == 'DX': # OREC (custom)
|
|
351
|
-
if value in
|
|
325
|
+
if value in VALID_OREC_VALUES:
|
|
352
326
|
member.orec = value
|
|
353
327
|
elif qualifier == 'DY': # CREC (custom)
|
|
354
|
-
if value in
|
|
328
|
+
if value in VALID_CREC_VALUES:
|
|
355
329
|
member.crec = value
|
|
356
330
|
elif qualifier == 'EJ': # Low Income Subsidy indicator
|
|
357
331
|
member.low_income = (value.upper() in ['Y', 'YES', '1', 'TRUE'])
|
|
@@ -376,8 +350,8 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
376
350
|
|
|
377
351
|
# DMG03 = Gender Code
|
|
378
352
|
sex = get_segment_value(segment, 3)
|
|
379
|
-
if sex in
|
|
380
|
-
member.sex =
|
|
353
|
+
if sex in X12_SEX_CODE_MAPPING:
|
|
354
|
+
member.sex = X12_SEX_CODE_MAPPING[sex]
|
|
381
355
|
|
|
382
356
|
# ===== DTP - Date Time Periods =====
|
|
383
357
|
elif seg_id == 'DTP' and len(segment) >= 4:
|
|
@@ -440,6 +414,13 @@ def parse_834_enrollment(segments: List[List[str]]) -> List[EnrollmentData]:
|
|
|
440
414
|
member.has_medicare = True
|
|
441
415
|
member.has_medicaid = True
|
|
442
416
|
|
|
417
|
+
# Detect LTI (Long Term Institutionalized)
|
|
418
|
+
if any(keyword in combined for keyword in [
|
|
419
|
+
'LTC', 'LONG TERM CARE', 'LONG-TERM CARE', 'NURSING HOME',
|
|
420
|
+
'SKILLED NURSING', 'SNF', 'INSTITUTIONALIZED'
|
|
421
|
+
]):
|
|
422
|
+
member.lti = True
|
|
423
|
+
|
|
443
424
|
# Don't forget last member
|
|
444
425
|
if member.member_id or member.has_medicare or member.has_medicaid:
|
|
445
426
|
enrollments.append(create_enrollment_data(member))
|
hccinfhir/extractor_837.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Optional, Dict
|
|
1
|
+
from typing import List, Optional, Dict, Tuple
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
from hccinfhir.datamodels import ServiceLevelData
|
|
4
4
|
|
|
@@ -65,7 +65,7 @@ def parse_diagnosis_codes(segment: List[str]) -> Dict[str, str]:
|
|
|
65
65
|
dx_lookup[str(pos)] = code
|
|
66
66
|
return dx_lookup
|
|
67
67
|
|
|
68
|
-
def process_service_line(segments: List[List[str]], start_index: int) ->
|
|
68
|
+
def process_service_line(segments: List[List[str]], start_index: int) -> Tuple[Optional[str], Optional[str]]:
|
|
69
69
|
"""Extract NDC and service date from service line segments"""
|
|
70
70
|
ndc = None
|
|
71
71
|
service_date = None
|
hccinfhir/hccinfhir.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from typing import List, Dict, Any, Union, Optional
|
|
1
|
+
from typing import List, Dict, Any, Union, Optional, Tuple, Set
|
|
2
2
|
from hccinfhir.extractor import extract_sld_list
|
|
3
3
|
from hccinfhir.filter import apply_filter
|
|
4
4
|
from hccinfhir.model_calculate import calculate_raf
|
|
5
|
-
from hccinfhir.datamodels import Demographics, ServiceLevelData, RAFResult, ModelName, ProcFilteringFilename, DxCCMappingFilename, PrefixOverride
|
|
6
|
-
from hccinfhir.utils import load_proc_filtering, load_dx_to_cc_mapping
|
|
5
|
+
from hccinfhir.datamodels import Demographics, ServiceLevelData, RAFResult, ModelName, ProcFilteringFilename, DxCCMappingFilename, HierarchiesFilename, IsChronicFilename, CoefficientsFilename, PrefixOverride
|
|
6
|
+
from hccinfhir.utils import load_proc_filtering, load_dx_to_cc_mapping, load_hierarchies, load_is_chronic, load_coefficients
|
|
7
7
|
|
|
8
8
|
class HCCInFHIR:
|
|
9
9
|
"""
|
|
@@ -13,26 +13,40 @@ class HCCInFHIR:
|
|
|
13
13
|
of the hccinfhir library.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
def __init__(self,
|
|
17
|
-
filter_claims: bool = True,
|
|
16
|
+
def __init__(self,
|
|
17
|
+
filter_claims: bool = True,
|
|
18
18
|
model_name: ModelName = "CMS-HCC Model V28",
|
|
19
19
|
proc_filtering_filename: ProcFilteringFilename = "ra_eligible_cpt_hcpcs_2026.csv",
|
|
20
|
-
dx_cc_mapping_filename: DxCCMappingFilename = "ra_dx_to_cc_2026.csv"
|
|
20
|
+
dx_cc_mapping_filename: DxCCMappingFilename = "ra_dx_to_cc_2026.csv",
|
|
21
|
+
hierarchies_filename: HierarchiesFilename = "ra_hierarchies_2026.csv",
|
|
22
|
+
is_chronic_filename: IsChronicFilename = "hcc_is_chronic.csv",
|
|
23
|
+
coefficients_filename: CoefficientsFilename = "ra_coefficients_2026.csv"):
|
|
21
24
|
"""
|
|
22
25
|
Initialize the HCCInFHIR processor.
|
|
23
|
-
|
|
26
|
+
|
|
24
27
|
Args:
|
|
25
28
|
filter_claims: Whether to apply filtering rules to claims. Default is True.
|
|
26
29
|
model_name: The name of the model to use for the calculation. Default is "CMS-HCC Model V28".
|
|
27
|
-
proc_filtering_filename:
|
|
28
|
-
dx_cc_mapping_filename:
|
|
30
|
+
proc_filtering_filename: Filename or path to the CPT/HCPCS filtering file. Default is "ra_eligible_cpt_hcpcs_2026.csv".
|
|
31
|
+
dx_cc_mapping_filename: Filename or path to the diagnosis to CC mapping file. Default is "ra_dx_to_cc_2026.csv".
|
|
32
|
+
hierarchies_filename: Filename or path to the hierarchies file. Default is "ra_hierarchies_2026.csv".
|
|
33
|
+
is_chronic_filename: Filename or path to the chronic conditions file. Default is "hcc_is_chronic.csv".
|
|
34
|
+
coefficients_filename: Filename or path to the coefficients file. Default is "ra_coefficients_2026.csv".
|
|
29
35
|
"""
|
|
30
36
|
self.filter_claims = filter_claims
|
|
31
37
|
self.model_name = model_name
|
|
32
38
|
self.proc_filtering_filename = proc_filtering_filename
|
|
33
39
|
self.dx_cc_mapping_filename = dx_cc_mapping_filename
|
|
40
|
+
self.hierarchies_filename = hierarchies_filename
|
|
41
|
+
self.is_chronic_filename = is_chronic_filename
|
|
42
|
+
self.coefficients_filename = coefficients_filename
|
|
43
|
+
|
|
44
|
+
# Load all data files once at initialization
|
|
34
45
|
self.professional_cpt = load_proc_filtering(proc_filtering_filename)
|
|
35
46
|
self.dx_to_cc_mapping = load_dx_to_cc_mapping(dx_cc_mapping_filename)
|
|
47
|
+
self.hierarchies_mapping = load_hierarchies(hierarchies_filename)
|
|
48
|
+
self.is_chronic_mapping = load_is_chronic(is_chronic_filename)
|
|
49
|
+
self.coefficients_mapping = load_coefficients(coefficients_filename)
|
|
36
50
|
|
|
37
51
|
|
|
38
52
|
def _ensure_demographics(self, demographics: Union[Demographics, Dict[str, Any]]) -> Demographics:
|
|
@@ -47,7 +61,7 @@ class HCCInFHIR:
|
|
|
47
61
|
maci: float = 0.0,
|
|
48
62
|
norm_factor: float = 1.0,
|
|
49
63
|
frailty_score: float = 0.0) -> RAFResult:
|
|
50
|
-
"""Calculate RAF score using demographics data."""
|
|
64
|
+
"""Calculate RAF score using demographics data and loaded data files."""
|
|
51
65
|
return calculate_raf(
|
|
52
66
|
diagnosis_codes=diagnosis_codes,
|
|
53
67
|
model_name=self.model_name,
|
|
@@ -62,6 +76,9 @@ class HCCInFHIR:
|
|
|
62
76
|
lti=demographics.lti,
|
|
63
77
|
graft_months=demographics.graft_months,
|
|
64
78
|
dx_to_cc_mapping=self.dx_to_cc_mapping,
|
|
79
|
+
is_chronic_mapping=self.is_chronic_mapping,
|
|
80
|
+
hierarchies_mapping=self.hierarchies_mapping,
|
|
81
|
+
coefficients_mapping=self.coefficients_mapping,
|
|
65
82
|
prefix_override=prefix_override,
|
|
66
83
|
maci=maci,
|
|
67
84
|
norm_factor=norm_factor,
|