hccinfhir 0.1.5__py3-none-any.whl → 0.1.7__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/data/hcc_is_chronic.csv +835 -836
- hccinfhir/data/hcc_is_chronic_without_esrd_model.csv +877 -0
- hccinfhir/datamodels.py +41 -1
- hccinfhir/extractor_837.py +2 -5
- hccinfhir/extractor_fhir.py +2 -2
- hccinfhir/hccinfhir.py +74 -23
- hccinfhir/model_calculate.py +44 -28
- hccinfhir/model_coefficients.py +11 -7
- hccinfhir/model_demographics.py +62 -12
- hccinfhir/model_interactions.py +34 -11
- {hccinfhir-0.1.5.dist-info → hccinfhir-0.1.7.dist-info}/METADATA +91 -2
- {hccinfhir-0.1.5.dist-info → hccinfhir-0.1.7.dist-info}/RECORD +14 -13
- {hccinfhir-0.1.5.dist-info → hccinfhir-0.1.7.dist-info}/WHEEL +0 -0
- {hccinfhir-0.1.5.dist-info → hccinfhir-0.1.7.dist-info}/licenses/LICENSE +0 -0
hccinfhir/datamodels.py
CHANGED
|
@@ -23,6 +23,45 @@ DxCCMappingFilename = Literal[
|
|
|
23
23
|
"ra_dx_to_cc_2026.csv"
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
PrefixOverride = Literal[
|
|
27
|
+
# CMS-HCC Community prefixes
|
|
28
|
+
"CNA_", # Community, Non-Dual, Aged
|
|
29
|
+
"CND_", # Community, Non-Dual, Disabled
|
|
30
|
+
"CFA_", # Community, Full Benefit Dual, Aged
|
|
31
|
+
"CFD_", # Community, Full Benefit Dual, Disabled
|
|
32
|
+
"CPA_", # Community, Partial Benefit Dual, Aged
|
|
33
|
+
"CPD_", # Community, Partial Benefit Dual, Disabled
|
|
34
|
+
# CMS-HCC Institutional
|
|
35
|
+
"INS_", # Long-Term Institutionalized
|
|
36
|
+
# CMS-HCC New Enrollee
|
|
37
|
+
"NE_", # New Enrollee
|
|
38
|
+
"SNPNE_", # Special Needs Plan New Enrollee
|
|
39
|
+
# ESRD Dialysis
|
|
40
|
+
"DI_", # Dialysis
|
|
41
|
+
"DNE_", # Dialysis New Enrollee
|
|
42
|
+
# ESRD Graft
|
|
43
|
+
"GI_", # Graft, Institutionalized
|
|
44
|
+
"GNE_", # Graft, New Enrollee
|
|
45
|
+
"GFPA_", # Graft, Full Benefit Dual, Aged
|
|
46
|
+
"GFPN_", # Graft, Full Benefit Dual, Non-Aged
|
|
47
|
+
"GNPA_", # Graft, Non-Dual, Aged
|
|
48
|
+
"GNPN_", # Graft, Non-Dual, Non-Aged
|
|
49
|
+
# ESRD Transplant
|
|
50
|
+
"TRANSPLANT_KIDNEY_ONLY_1M", # 1 month post-transplant
|
|
51
|
+
"TRANSPLANT_KIDNEY_ONLY_2M", # 2 months post-transplant
|
|
52
|
+
"TRANSPLANT_KIDNEY_ONLY_3M", # 3 months post-transplant
|
|
53
|
+
# RxHCC Community Enrollee
|
|
54
|
+
"Rx_CE_LowAged_", # Community Enrollee, Low Income, Aged
|
|
55
|
+
"Rx_CE_LowNoAged_", # Community Enrollee, Low Income, Non-Aged
|
|
56
|
+
"Rx_CE_NoLowAged_", # Community Enrollee, Not Low Income, Aged
|
|
57
|
+
"Rx_CE_NoLowNoAged_", # Community Enrollee, Not Low Income, Non-Aged
|
|
58
|
+
"Rx_CE_LTI_", # Community Enrollee, Long-Term Institutionalized
|
|
59
|
+
# RxHCC New Enrollee
|
|
60
|
+
"Rx_NE_Lo_", # New Enrollee, Low Income
|
|
61
|
+
"Rx_NE_NoLo_", # New Enrollee, Not Low Income
|
|
62
|
+
"Rx_NE_LTI_", # New Enrollee, Long-Term Institutionalized
|
|
63
|
+
]
|
|
64
|
+
|
|
26
65
|
class ServiceLevelData(BaseModel):
|
|
27
66
|
"""
|
|
28
67
|
Represents standardized service-level data extracted from healthcare claims.
|
|
@@ -93,8 +132,9 @@ class RAFResult(BaseModel):
|
|
|
93
132
|
"""Risk adjustment calculation results"""
|
|
94
133
|
risk_score: float = Field(..., description="Final RAF score")
|
|
95
134
|
risk_score_demographics: float = Field(..., description="Demographics-only risk score")
|
|
96
|
-
risk_score_chronic_only: float = Field(..., description="Chronic conditions risk score")
|
|
135
|
+
risk_score_chronic_only: float = Field(..., description="Chronic conditions risk score")
|
|
97
136
|
risk_score_hcc: float = Field(..., description="HCC conditions risk score")
|
|
137
|
+
risk_score_payment: float = Field(..., description="Payment RAF score (adjusted for MACI, normalization, and frailty)")
|
|
98
138
|
hcc_list: List[str] = Field(default_factory=list, description="List of active HCC categories")
|
|
99
139
|
cc_to_dx: Dict[str, Set[str]] = Field(default_factory=dict, description="Condition categories mapped to diagnosis codes")
|
|
100
140
|
coefficients: Dict[str, float] = Field(default_factory=dict, description="Applied model coefficients")
|
hccinfhir/extractor_837.py
CHANGED
|
@@ -149,7 +149,6 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
149
149
|
"""
|
|
150
150
|
slds = []
|
|
151
151
|
current_data = ClaimData(claim_type=claim_type)
|
|
152
|
-
in_claim_loop = False
|
|
153
152
|
in_rendering_provider_loop = False
|
|
154
153
|
claim_control_number = None
|
|
155
154
|
|
|
@@ -166,7 +165,6 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
166
165
|
elif seg_id == 'NM1' and len(segment) > 1:
|
|
167
166
|
if segment[1] == 'IL': # Subscriber/Patient
|
|
168
167
|
current_data.patient_id = get_segment_value(segment, 9)
|
|
169
|
-
in_claim_loop = False
|
|
170
168
|
in_rendering_provider_loop = False
|
|
171
169
|
elif segment[1] == '82' and len(segment) > 8 and segment[8] == 'XX': # Rendering Provider
|
|
172
170
|
current_data.performing_provider_npi = get_segment_value(segment, 9)
|
|
@@ -180,7 +178,6 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
180
178
|
|
|
181
179
|
# Process Claim Information
|
|
182
180
|
elif seg_id == 'CLM':
|
|
183
|
-
in_claim_loop = True
|
|
184
181
|
in_rendering_provider_loop = False
|
|
185
182
|
current_data.claim_id = segment[1] if len(segment) > 1 else None
|
|
186
183
|
|
|
@@ -190,7 +187,7 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
190
187
|
current_data.service_type = segment[5][1] if len(segment[5]) > 1 else None
|
|
191
188
|
|
|
192
189
|
# Process Diagnosis Codes
|
|
193
|
-
elif seg_id == 'HI'
|
|
190
|
+
elif seg_id == 'HI':
|
|
194
191
|
# In 837I, there can be multiple HI segments in the claim
|
|
195
192
|
# Also, in 837I, diagnosis position does not matter
|
|
196
193
|
# We will use continuous numbering for diagnosis codes
|
|
@@ -220,7 +217,7 @@ def parse_837_claim_to_sld(segments: List[List[str]], claim_type: str) -> List[S
|
|
|
220
217
|
# SV205 (Required) - Unit Count: Format 9999999.999 (whole numbers only - fractional quantities not recognized)
|
|
221
218
|
# NOTE: Diagnosis Code Pointer is not supported for SV2
|
|
222
219
|
#
|
|
223
|
-
elif seg_id in ['SV1', 'SV2']
|
|
220
|
+
elif seg_id in ['SV1', 'SV2']:
|
|
224
221
|
|
|
225
222
|
linked_diagnoses = []
|
|
226
223
|
|
hccinfhir/extractor_fhir.py
CHANGED
|
@@ -162,8 +162,8 @@ def extract_sld_fhir(eob_data: dict) -> List[ServiceLevelData]:
|
|
|
162
162
|
eob.billablePeriod.get_service_date() if eob.billablePeriod else None),
|
|
163
163
|
'place_of_service': item.locationCodeableConcept.get_code(SYSTEMS['context']['place'])
|
|
164
164
|
if item.locationCodeableConcept else None,
|
|
165
|
-
'modifiers': [m.
|
|
166
|
-
|
|
165
|
+
'modifiers': [code for m in (item.modifier or [])
|
|
166
|
+
if m and (code := m.get_code(SYSTEMS['procedures']['hcpcs'])) is not None],
|
|
167
167
|
'allowed_amount': next((adj.get('amount', {}).get('value')
|
|
168
168
|
for adj in (item.adjudication or [])
|
|
169
169
|
if any(c.get('code') == 'eligible'
|
hccinfhir/hccinfhir.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from typing import List, Dict, Any, Union
|
|
1
|
+
from typing import List, Dict, Any, Union, Optional
|
|
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
|
|
5
|
+
from hccinfhir.datamodels import Demographics, ServiceLevelData, RAFResult, ModelName, ProcFilteringFilename, DxCCMappingFilename, PrefixOverride
|
|
6
6
|
from hccinfhir.utils import load_proc_filtering, load_dx_to_cc_mapping
|
|
7
7
|
|
|
8
8
|
class HCCInFHIR:
|
|
@@ -41,8 +41,12 @@ class HCCInFHIR:
|
|
|
41
41
|
return Demographics(**demographics)
|
|
42
42
|
return demographics
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
|
|
44
|
+
def _calculate_raf_from_demographics_and_dx_codes(self, diagnosis_codes: List[str],
|
|
45
|
+
demographics: Demographics,
|
|
46
|
+
prefix_override: Optional[PrefixOverride] = None,
|
|
47
|
+
maci: float = 0.0,
|
|
48
|
+
norm_factor: float = 1.0,
|
|
49
|
+
frailty_score: float = 0.0) -> RAFResult:
|
|
46
50
|
"""Calculate RAF score using demographics data."""
|
|
47
51
|
return calculate_raf(
|
|
48
52
|
diagnosis_codes=diagnosis_codes,
|
|
@@ -55,22 +59,36 @@ class HCCInFHIR:
|
|
|
55
59
|
new_enrollee=demographics.new_enrollee,
|
|
56
60
|
snp=demographics.snp,
|
|
57
61
|
low_income=demographics.low_income,
|
|
62
|
+
lti=demographics.lti,
|
|
58
63
|
graft_months=demographics.graft_months,
|
|
59
|
-
dx_to_cc_mapping=self.dx_to_cc_mapping
|
|
64
|
+
dx_to_cc_mapping=self.dx_to_cc_mapping,
|
|
65
|
+
prefix_override=prefix_override,
|
|
66
|
+
maci=maci,
|
|
67
|
+
norm_factor=norm_factor,
|
|
68
|
+
frailty_score=frailty_score
|
|
60
69
|
)
|
|
61
70
|
|
|
62
71
|
def _get_unique_diagnosis_codes(self, service_data: List[ServiceLevelData]) -> List[str]:
|
|
63
72
|
"""Extract unique diagnosis codes from service level data."""
|
|
64
73
|
return list({code for sld in service_data for code in sld.claim_diagnosis_codes})
|
|
65
74
|
|
|
66
|
-
def run(self, eob_list: List[Dict[str, Any]],
|
|
67
|
-
demographics: Union[Demographics, Dict[str, Any]]
|
|
75
|
+
def run(self, eob_list: List[Dict[str, Any]],
|
|
76
|
+
demographics: Union[Demographics, Dict[str, Any]],
|
|
77
|
+
prefix_override: Optional[PrefixOverride] = None,
|
|
78
|
+
maci: float = 0.0,
|
|
79
|
+
norm_factor: float = 1.0,
|
|
80
|
+
frailty_score: float = 0.0) -> RAFResult:
|
|
68
81
|
"""Process EOB resources and calculate RAF scores.
|
|
69
|
-
|
|
82
|
+
|
|
70
83
|
Args:
|
|
71
84
|
eob_list: List of EOB resources
|
|
72
85
|
demographics: Demographics information
|
|
73
|
-
|
|
86
|
+
prefix_override: Optional prefix to override auto-detected demographic prefix.
|
|
87
|
+
Use when demographic categorization is incorrect (e.g., ESRD patients with orec=0).
|
|
88
|
+
maci: Major Adjustment to Coding Intensity (0.0-1.0, default 0.0)
|
|
89
|
+
norm_factor: Normalization factor (default 1.0)
|
|
90
|
+
frailty_score: Frailty adjustment score (default 0.0)
|
|
91
|
+
|
|
74
92
|
Returns:
|
|
75
93
|
RAFResult object containing calculated scores and processed data
|
|
76
94
|
"""
|
|
@@ -84,16 +102,36 @@ class HCCInFHIR:
|
|
|
84
102
|
|
|
85
103
|
if self.filter_claims:
|
|
86
104
|
sld_list = apply_filter(sld_list, professional_cpt=self.professional_cpt)
|
|
87
|
-
|
|
105
|
+
|
|
88
106
|
# Calculate RAF score
|
|
89
107
|
unique_dx_codes = self._get_unique_diagnosis_codes(sld_list)
|
|
90
|
-
raf_result = self.
|
|
91
|
-
|
|
108
|
+
raf_result = self._calculate_raf_from_demographics_and_dx_codes(
|
|
109
|
+
unique_dx_codes, demographics, prefix_override, maci, norm_factor, frailty_score
|
|
110
|
+
)
|
|
111
|
+
|
|
92
112
|
# Create new result with service data included
|
|
93
113
|
return raf_result.model_copy(update={'service_level_data': sld_list})
|
|
94
114
|
|
|
95
|
-
def run_from_service_data(self, service_data: List[Union[ServiceLevelData, Dict[str, Any]]],
|
|
96
|
-
demographics: Union[Demographics, Dict[str, Any]]
|
|
115
|
+
def run_from_service_data(self, service_data: List[Union[ServiceLevelData, Dict[str, Any]]],
|
|
116
|
+
demographics: Union[Demographics, Dict[str, Any]],
|
|
117
|
+
prefix_override: Optional[PrefixOverride] = None,
|
|
118
|
+
maci: float = 0.0,
|
|
119
|
+
norm_factor: float = 1.0,
|
|
120
|
+
frailty_score: float = 0.0) -> RAFResult:
|
|
121
|
+
"""Process service-level data and calculate RAF scores.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
service_data: List of ServiceLevelData objects or dictionaries
|
|
125
|
+
demographics: Demographics information
|
|
126
|
+
prefix_override: Optional prefix to override auto-detected demographic prefix.
|
|
127
|
+
Use when demographic categorization is incorrect (e.g., ESRD patients with orec=0).
|
|
128
|
+
maci: Major Adjustment to Coding Intensity (0.0-1.0, default 0.0)
|
|
129
|
+
norm_factor: Normalization factor (default 1.0)
|
|
130
|
+
frailty_score: Frailty adjustment score (default 0.0)
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
RAFResult object containing calculated scores and processed data
|
|
134
|
+
"""
|
|
97
135
|
demographics = self._ensure_demographics(demographics)
|
|
98
136
|
|
|
99
137
|
if not isinstance(service_data, list):
|
|
@@ -116,25 +154,36 @@ class HCCInFHIR:
|
|
|
116
154
|
)
|
|
117
155
|
|
|
118
156
|
if self.filter_claims:
|
|
119
|
-
standardized_data = apply_filter(standardized_data,
|
|
157
|
+
standardized_data = apply_filter(standardized_data,
|
|
120
158
|
professional_cpt=self.professional_cpt)
|
|
121
159
|
|
|
122
|
-
|
|
160
|
+
|
|
123
161
|
# Calculate RAF score
|
|
124
162
|
unique_dx_codes = self._get_unique_diagnosis_codes(standardized_data)
|
|
125
|
-
raf_result = self.
|
|
126
|
-
|
|
163
|
+
raf_result = self._calculate_raf_from_demographics_and_dx_codes(
|
|
164
|
+
unique_dx_codes, demographics, prefix_override, maci, norm_factor, frailty_score
|
|
165
|
+
)
|
|
166
|
+
|
|
127
167
|
# Create new result with service data included
|
|
128
168
|
return raf_result.model_copy(update={'service_level_data': standardized_data})
|
|
129
169
|
|
|
130
170
|
def calculate_from_diagnosis(self, diagnosis_codes: List[str],
|
|
131
|
-
demographics: Union[Demographics, Dict[str, Any]]
|
|
171
|
+
demographics: Union[Demographics, Dict[str, Any]],
|
|
172
|
+
prefix_override: Optional[PrefixOverride] = None,
|
|
173
|
+
maci: float = 0.0,
|
|
174
|
+
norm_factor: float = 1.0,
|
|
175
|
+
frailty_score: float = 0.0) -> RAFResult:
|
|
132
176
|
"""Calculate RAF scores from a list of diagnosis codes.
|
|
133
|
-
|
|
177
|
+
|
|
134
178
|
Args:
|
|
135
179
|
diagnosis_codes: List of diagnosis codes
|
|
136
180
|
demographics: Demographics information
|
|
137
|
-
|
|
181
|
+
prefix_override: Optional prefix to override auto-detected demographic prefix.
|
|
182
|
+
Use when demographic categorization is incorrect (e.g., ESRD patients with orec=0).
|
|
183
|
+
maci: Major Adjustment to Coding Intensity (0.0-1.0, default 0.0)
|
|
184
|
+
norm_factor: Normalization factor (default 1.0)
|
|
185
|
+
frailty_score: Frailty adjustment score (default 0.0)
|
|
186
|
+
|
|
138
187
|
Raises:
|
|
139
188
|
ValueError: If diagnosis_codes is empty or not a list
|
|
140
189
|
"""
|
|
@@ -142,7 +191,9 @@ class HCCInFHIR:
|
|
|
142
191
|
raise ValueError("diagnosis_codes must be a list")
|
|
143
192
|
if not diagnosis_codes:
|
|
144
193
|
raise ValueError("diagnosis_codes list cannot be empty")
|
|
145
|
-
|
|
194
|
+
|
|
146
195
|
demographics = self._ensure_demographics(demographics)
|
|
147
|
-
raf_result = self.
|
|
196
|
+
raf_result = self._calculate_raf_from_demographics_and_dx_codes(
|
|
197
|
+
diagnosis_codes, demographics, prefix_override, maci, norm_factor, frailty_score
|
|
198
|
+
)
|
|
148
199
|
return raf_result
|
hccinfhir/model_calculate.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from typing import List, Union, Dict, Tuple, Set
|
|
2
|
-
from hccinfhir.datamodels import ModelName, RAFResult
|
|
1
|
+
from typing import List, Union, Dict, Tuple, Set, Optional
|
|
2
|
+
from hccinfhir.datamodels import ModelName, RAFResult, PrefixOverride
|
|
3
3
|
from hccinfhir.model_demographics import categorize_demographics
|
|
4
4
|
from hccinfhir.model_dx_to_cc import apply_mapping
|
|
5
5
|
from hccinfhir.model_hierarchies import apply_hierarchies
|
|
@@ -17,17 +17,22 @@ is_chronic_default = load_is_chronic(mapping_file_default)
|
|
|
17
17
|
|
|
18
18
|
def calculate_raf(diagnosis_codes: List[str],
|
|
19
19
|
model_name: ModelName = "CMS-HCC Model V28",
|
|
20
|
-
age: Union[int, float] = 65,
|
|
21
|
-
sex: str = 'F',
|
|
20
|
+
age: Union[int, float] = 65,
|
|
21
|
+
sex: str = 'F',
|
|
22
22
|
dual_elgbl_cd: str = 'NA',
|
|
23
|
-
orec: str = '0',
|
|
23
|
+
orec: str = '0',
|
|
24
24
|
crec: str = '0',
|
|
25
|
-
new_enrollee: bool = False,
|
|
25
|
+
new_enrollee: bool = False,
|
|
26
26
|
snp: bool = False,
|
|
27
27
|
low_income: bool = False,
|
|
28
|
+
lti: bool = False,
|
|
28
29
|
graft_months: int = None,
|
|
29
30
|
dx_to_cc_mapping: Dict[Tuple[str, ModelName], Set[str]] = dx_to_cc_default,
|
|
30
|
-
is_chronic_mapping: Dict[Tuple[str, ModelName], bool] = is_chronic_default
|
|
31
|
+
is_chronic_mapping: Dict[Tuple[str, ModelName], bool] = is_chronic_default,
|
|
32
|
+
prefix_override: Optional[PrefixOverride] = None,
|
|
33
|
+
maci: float = 0.0,
|
|
34
|
+
norm_factor: float = 1.0,
|
|
35
|
+
frailty_score: float = 0.0) -> RAFResult:
|
|
31
36
|
"""
|
|
32
37
|
Calculate Risk Adjustment Factor (RAF) based on diagnosis codes and demographic information.
|
|
33
38
|
|
|
@@ -43,6 +48,10 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
43
48
|
snp: Special Needs Plan indicator
|
|
44
49
|
low_income: Low income subsidy indicator
|
|
45
50
|
graft_months: Number of months since transplant
|
|
51
|
+
prefix_override: Optional prefix to override auto-detected demographic prefix.
|
|
52
|
+
Use when demographic categorization from orec/crec is incorrect.
|
|
53
|
+
Common values: 'DI_' (ESRD Dialysis), 'DNE_' (ESRD Dialysis New Enrollee),
|
|
54
|
+
'INS_' (Institutionalized), 'CFA_' (Community Full Dual Aged), etc.
|
|
46
55
|
|
|
47
56
|
Returns:
|
|
48
57
|
Dictionary containing RAF score and coefficients used in calculation
|
|
@@ -63,24 +72,27 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
63
72
|
elif 'HHS-HCC' in model_name: # not implemented yet
|
|
64
73
|
version = 'V6'
|
|
65
74
|
|
|
66
|
-
demographics = categorize_demographics(age,
|
|
67
|
-
sex,
|
|
68
|
-
dual_elgbl_cd,
|
|
69
|
-
orec,
|
|
70
|
-
crec,
|
|
71
|
-
version,
|
|
72
|
-
new_enrollee,
|
|
73
|
-
snp,
|
|
74
|
-
low_income,
|
|
75
|
-
|
|
75
|
+
demographics = categorize_demographics(age,
|
|
76
|
+
sex,
|
|
77
|
+
dual_elgbl_cd,
|
|
78
|
+
orec,
|
|
79
|
+
crec,
|
|
80
|
+
version,
|
|
81
|
+
new_enrollee,
|
|
82
|
+
snp,
|
|
83
|
+
low_income,
|
|
84
|
+
lti,
|
|
85
|
+
graft_months,
|
|
86
|
+
prefix_override=prefix_override)
|
|
76
87
|
|
|
77
|
-
cc_to_dx = apply_mapping(diagnosis_codes,
|
|
78
|
-
model_name,
|
|
88
|
+
cc_to_dx = apply_mapping(diagnosis_codes,
|
|
89
|
+
model_name,
|
|
79
90
|
dx_to_cc_mapping=dx_to_cc_mapping)
|
|
80
91
|
hcc_set = set(cc_to_dx.keys())
|
|
81
92
|
hcc_set = apply_hierarchies(hcc_set, model_name)
|
|
82
93
|
interactions = apply_interactions(demographics, hcc_set, model_name)
|
|
83
|
-
coefficients = apply_coefficients(demographics, hcc_set, interactions, model_name
|
|
94
|
+
coefficients = apply_coefficients(demographics, hcc_set, interactions, model_name,
|
|
95
|
+
prefix_override=prefix_override)
|
|
84
96
|
|
|
85
97
|
hcc_chronic = set()
|
|
86
98
|
interactions_chronic = {}
|
|
@@ -100,14 +112,16 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
100
112
|
elif key.startswith('OriginallyDisabled_'):
|
|
101
113
|
demographic_interactions[key] = value
|
|
102
114
|
|
|
103
|
-
coefficients_demographics = apply_coefficients(demographics,
|
|
104
|
-
set(),
|
|
105
|
-
demographic_interactions,
|
|
106
|
-
model_name
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
coefficients_demographics = apply_coefficients(demographics,
|
|
116
|
+
set(),
|
|
117
|
+
demographic_interactions,
|
|
118
|
+
model_name,
|
|
119
|
+
prefix_override=prefix_override)
|
|
120
|
+
coefficients_chronic_only = apply_coefficients(demographics,
|
|
121
|
+
hcc_chronic,
|
|
122
|
+
interactions_chronic,
|
|
123
|
+
model_name,
|
|
124
|
+
prefix_override=prefix_override)
|
|
111
125
|
|
|
112
126
|
# Calculate risk scores
|
|
113
127
|
print(f"Coefficients: {coefficients}")
|
|
@@ -116,12 +130,14 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
116
130
|
risk_score_demographics = sum(coefficients_demographics.values())
|
|
117
131
|
risk_score_chronic_only = sum(coefficients_chronic_only.values()) - risk_score_demographics
|
|
118
132
|
risk_score_hcc = risk_score - risk_score_demographics
|
|
133
|
+
risk_score_payment = risk_score * (1 - maci) / norm_factor + frailty_score
|
|
119
134
|
|
|
120
135
|
return RAFResult(
|
|
121
136
|
risk_score=risk_score,
|
|
122
137
|
risk_score_demographics=risk_score_demographics,
|
|
123
138
|
risk_score_chronic_only=risk_score_chronic_only,
|
|
124
139
|
risk_score_hcc=risk_score_hcc,
|
|
140
|
+
risk_score_payment=risk_score_payment,
|
|
125
141
|
hcc_list=list(hcc_set),
|
|
126
142
|
cc_to_dx=cc_to_dx,
|
|
127
143
|
coefficients=coefficients,
|
hccinfhir/model_coefficients.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from typing import Dict, Tuple
|
|
1
|
+
from typing import Dict, Tuple, Optional
|
|
2
2
|
import importlib.resources
|
|
3
|
-
from hccinfhir.datamodels import ModelName, Demographics
|
|
3
|
+
from hccinfhir.datamodels import ModelName, Demographics, PrefixOverride
|
|
4
4
|
|
|
5
5
|
# Load default mappings from csv file
|
|
6
6
|
coefficients_file_default = 'ra_coefficients_2026.csv'
|
|
@@ -90,15 +90,16 @@ def get_coefficent_prefix(demographics: Demographics,
|
|
|
90
90
|
return prefix + '_'
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def apply_coefficients(demographics: Demographics,
|
|
94
|
-
hcc_set: set[str],
|
|
93
|
+
def apply_coefficients(demographics: Demographics,
|
|
94
|
+
hcc_set: set[str],
|
|
95
95
|
interactions: dict,
|
|
96
96
|
model_name: ModelName = "CMS-HCC Model V28",
|
|
97
|
-
coefficients: Dict[Tuple[str, ModelName], float] = coefficients_default
|
|
97
|
+
coefficients: Dict[Tuple[str, ModelName], float] = coefficients_default,
|
|
98
|
+
prefix_override: Optional[PrefixOverride] = None) -> dict:
|
|
98
99
|
"""Apply risk adjustment coefficients to HCCs and interactions.
|
|
99
100
|
|
|
100
101
|
This function takes demographic information, HCC codes, and interaction variables and returns
|
|
101
|
-
a dictionary mapping each variable to its corresponding coefficient value based on the
|
|
102
|
+
a dictionary mapping each variable to its corresponding coefficient value based on the
|
|
102
103
|
specified model.
|
|
103
104
|
|
|
104
105
|
Args:
|
|
@@ -108,13 +109,16 @@ def apply_coefficients(demographics: Demographics,
|
|
|
108
109
|
model_name: Name of the risk adjustment model to use (default: "CMS-HCC Model V28")
|
|
109
110
|
coefficients: Dictionary mapping (variable, model) tuples to coefficient values
|
|
110
111
|
(default: coefficients_default)
|
|
112
|
+
prefix_override: Optional prefix to override auto-detected demographic prefix.
|
|
113
|
+
Common values: 'DI_' (ESRD Dialysis), 'DNE_' (ESRD Dialysis New Enrollee),
|
|
114
|
+
'INS_' (Institutionalized), 'CFA_' (Community Full Dual Aged), etc.
|
|
111
115
|
|
|
112
116
|
Returns:
|
|
113
117
|
Dictionary mapping HCC codes and interaction variables to their coefficient values
|
|
114
118
|
for variables that are present (HCC in hcc_set or interaction value = 1)
|
|
115
119
|
"""
|
|
116
120
|
# Get the coefficient prefix
|
|
117
|
-
prefix = get_coefficent_prefix(demographics, model_name)
|
|
121
|
+
prefix = prefix_override if prefix_override is not None else get_coefficent_prefix(demographics, model_name)
|
|
118
122
|
|
|
119
123
|
output = {}
|
|
120
124
|
|
hccinfhir/model_demographics.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
from typing import Union
|
|
2
|
-
from hccinfhir.datamodels import Demographics
|
|
1
|
+
from typing import Union, Optional
|
|
2
|
+
from hccinfhir.datamodels import Demographics, PrefixOverride
|
|
3
3
|
|
|
4
|
-
def categorize_demographics(age: Union[int, float],
|
|
5
|
-
sex: str,
|
|
4
|
+
def categorize_demographics(age: Union[int, float],
|
|
5
|
+
sex: str,
|
|
6
6
|
dual_elgbl_cd: str = None,
|
|
7
|
-
orec: str = None,
|
|
7
|
+
orec: str = None,
|
|
8
8
|
crec: str = None,
|
|
9
9
|
version: str = 'V2',
|
|
10
10
|
new_enrollee: bool = False,
|
|
11
11
|
snp: bool = False,
|
|
12
12
|
low_income: bool = False,
|
|
13
|
-
|
|
13
|
+
lti: bool = False,
|
|
14
|
+
graft_months: int = None,
|
|
15
|
+
prefix_override: Optional[PrefixOverride] = None
|
|
14
16
|
) -> Demographics:
|
|
15
17
|
"""
|
|
16
18
|
Categorize a beneficiary's demographics into risk adjustment categories.
|
|
@@ -23,10 +25,15 @@ def categorize_demographics(age: Union[int, float],
|
|
|
23
25
|
sex: Beneficiary sex ('M'/'F' or '1'/'2')
|
|
24
26
|
dual_elgbl_cd: Dual eligibility code ('00'-'10')
|
|
25
27
|
orec: Original reason for entitlement code ('0'-'3')
|
|
26
|
-
crec: Current reason for entitlement code ('0'-'3')
|
|
28
|
+
crec: Current reason for entitlement code ('0'-'3')
|
|
27
29
|
version: Version of categorization to use ('V2', 'V4', 'V6')
|
|
28
30
|
new_enrollee: Whether beneficiary is a new enrollee
|
|
29
31
|
snp: Whether beneficiary is in a Special Needs Plan
|
|
32
|
+
low_income: Whether beneficiary is low income (RxHCC only)
|
|
33
|
+
lti: Whether beneficiary is long-term institutionalized
|
|
34
|
+
graft_months: Number of months since transplant (ESRD only)
|
|
35
|
+
prefix_override: Optional prefix to override demographic detection
|
|
36
|
+
(e.g., 'DI_', 'DNE_', 'INS_', 'CFA_', etc.)
|
|
30
37
|
|
|
31
38
|
Returns:
|
|
32
39
|
Demographics object containing derived fields like age/sex category,
|
|
@@ -82,6 +89,44 @@ def categorize_demographics(age: Union[int, float],
|
|
|
82
89
|
esrd_crec = crec in {'2', '3'} if crec else False
|
|
83
90
|
esrd = esrd_orec or esrd_crec
|
|
84
91
|
|
|
92
|
+
# Override demographics based on prefix_override
|
|
93
|
+
if prefix_override:
|
|
94
|
+
# ESRD model prefixes
|
|
95
|
+
esrd_prefixes = {'DI_', 'DNE_', 'GI_', 'GNE_', 'GFPA_', 'GFPN_', 'GNPA_', 'GNPN_'}
|
|
96
|
+
# CMS-HCC new enrollee prefixes
|
|
97
|
+
new_enrollee_prefixes = {'NE_', 'SNPNE_', 'DNE_', 'GNE_'}
|
|
98
|
+
# CMS-HCC community prefixes
|
|
99
|
+
community_prefixes = {'CNA_', 'CND_', 'CFA_', 'CFD_', 'CPA_', 'CPD_'}
|
|
100
|
+
# Institutionalized prefix
|
|
101
|
+
institutional_prefixes = {'INS_', 'GI_'}
|
|
102
|
+
|
|
103
|
+
# TODO: RxHCC prefixes
|
|
104
|
+
|
|
105
|
+
# Set esrd flag
|
|
106
|
+
if prefix_override in esrd_prefixes:
|
|
107
|
+
esrd = True
|
|
108
|
+
|
|
109
|
+
# Set new_enrollee flag
|
|
110
|
+
if prefix_override in new_enrollee_prefixes:
|
|
111
|
+
new_enrollee = True
|
|
112
|
+
elif prefix_override in community_prefixes or prefix_override in institutional_prefixes:
|
|
113
|
+
new_enrollee = False
|
|
114
|
+
|
|
115
|
+
# Set dual eligibility flags based on prefix
|
|
116
|
+
if prefix_override in {'CFA_', 'CFD_', 'GFPA_', 'GFPN_'}:
|
|
117
|
+
is_fbd = True
|
|
118
|
+
is_pbd = False
|
|
119
|
+
elif prefix_override in {'CPA_', 'CPD_'}:
|
|
120
|
+
is_fbd = False
|
|
121
|
+
is_pbd = True
|
|
122
|
+
elif prefix_override in {'CNA_', 'CND_', 'GNPA_', 'GNPN_'}:
|
|
123
|
+
is_fbd = False
|
|
124
|
+
is_pbd = False
|
|
125
|
+
|
|
126
|
+
# Set lti flag based on prefix
|
|
127
|
+
if prefix_override in institutional_prefixes:
|
|
128
|
+
lti = True
|
|
129
|
+
|
|
85
130
|
result_dict = {
|
|
86
131
|
'version': version,
|
|
87
132
|
'non_aged': non_aged,
|
|
@@ -97,6 +142,7 @@ def categorize_demographics(age: Union[int, float],
|
|
|
97
142
|
'fbd': is_fbd,
|
|
98
143
|
'pbd': is_pbd,
|
|
99
144
|
'esrd': esrd,
|
|
145
|
+
'lti': lti,
|
|
100
146
|
'graft_months': graft_months,
|
|
101
147
|
'low_income': low_income
|
|
102
148
|
}
|
|
@@ -131,10 +177,14 @@ def categorize_demographics(age: Union[int, float],
|
|
|
131
177
|
if orec is None or orec == '':
|
|
132
178
|
orec = '0' # Default to 0 if OREC is None
|
|
133
179
|
|
|
134
|
-
#
|
|
180
|
+
# Determine prefix based on new_enrollee status
|
|
135
181
|
if new_enrollee:
|
|
136
182
|
prefix = 'NEF' if std_sex == '2' else 'NEM'
|
|
137
|
-
|
|
183
|
+
else:
|
|
184
|
+
prefix = 'F' if std_sex == '2' else 'M'
|
|
185
|
+
|
|
186
|
+
# CMS-HCC new enrollee logic with detailed 65-69 categories
|
|
187
|
+
if new_enrollee and not esrd:
|
|
138
188
|
if age <= 34:
|
|
139
189
|
category = f'{prefix}0_34'
|
|
140
190
|
elif 34 < age <= 44:
|
|
@@ -167,9 +217,9 @@ def categorize_demographics(age: Union[int, float],
|
|
|
167
217
|
category = f'{prefix}90_94'
|
|
168
218
|
else:
|
|
169
219
|
category = f'{prefix}95_GT'
|
|
170
|
-
|
|
220
|
+
|
|
221
|
+
# Standard logic with grouped 65_69 (for non-new-enrollee OR ESRD)
|
|
171
222
|
else:
|
|
172
|
-
prefix = 'F' if std_sex == '2' else 'M'
|
|
173
223
|
age_ranges = [
|
|
174
224
|
(0, 34, '0_34'),
|
|
175
225
|
(34, 44, '35_44'),
|
|
@@ -184,7 +234,7 @@ def categorize_demographics(age: Union[int, float],
|
|
|
184
234
|
(89, 94, '90_94'),
|
|
185
235
|
(94, float('inf'), '95_GT')
|
|
186
236
|
]
|
|
187
|
-
|
|
237
|
+
|
|
188
238
|
for low, high, suffix in age_ranges:
|
|
189
239
|
if low < age <= high:
|
|
190
240
|
category = f'{prefix}{suffix}'
|