hccinfhir 0.1.4__py3-none-any.whl → 0.1.6__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/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_fhir.py +2 -2
- hccinfhir/hccinfhir.py +74 -23
- hccinfhir/model_calculate.py +48 -28
- hccinfhir/model_coefficients.py +11 -7
- hccinfhir/model_demographics.py +62 -12
- hccinfhir/model_interactions.py +34 -11
- {hccinfhir-0.1.4.dist-info → hccinfhir-0.1.6.dist-info}/METADATA +183 -12
- {hccinfhir-0.1.4.dist-info → hccinfhir-0.1.6.dist-info}/RECORD +14 -13
- {hccinfhir-0.1.4.dist-info → hccinfhir-0.1.6.dist-info}/WHEEL +0 -0
- {hccinfhir-0.1.4.dist-info → hccinfhir-0.1.6.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_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,29 +72,34 @@ 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()
|
|
98
|
+
interactions_chronic = {}
|
|
86
99
|
for hcc in hcc_set:
|
|
87
100
|
if is_chronic_mapping.get((hcc, model_name), False):
|
|
88
101
|
hcc_chronic.add(hcc)
|
|
102
|
+
interactions_chronic = apply_interactions(demographics, hcc_chronic, model_name)
|
|
89
103
|
|
|
90
104
|
demographic_interactions = {}
|
|
91
105
|
for key, value in interactions.items():
|
|
@@ -98,26 +112,32 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
98
112
|
elif key.startswith('OriginallyDisabled_'):
|
|
99
113
|
demographic_interactions[key] = value
|
|
100
114
|
|
|
101
|
-
coefficients_demographics = apply_coefficients(demographics,
|
|
102
|
-
set(),
|
|
103
|
-
demographic_interactions,
|
|
104
|
-
model_name
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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)
|
|
109
125
|
|
|
110
126
|
# Calculate risk scores
|
|
127
|
+
print(f"Coefficients: {coefficients}")
|
|
111
128
|
risk_score = sum(coefficients.values())
|
|
129
|
+
print(f"Risk Score: {risk_score}")
|
|
112
130
|
risk_score_demographics = sum(coefficients_demographics.values())
|
|
113
131
|
risk_score_chronic_only = sum(coefficients_chronic_only.values()) - risk_score_demographics
|
|
114
132
|
risk_score_hcc = risk_score - risk_score_demographics
|
|
133
|
+
risk_score_payment = risk_score * (1 - maci) / norm_factor + frailty_score
|
|
115
134
|
|
|
116
135
|
return RAFResult(
|
|
117
136
|
risk_score=risk_score,
|
|
118
137
|
risk_score_demographics=risk_score_demographics,
|
|
119
138
|
risk_score_chronic_only=risk_score_chronic_only,
|
|
120
139
|
risk_score_hcc=risk_score_hcc,
|
|
140
|
+
risk_score_payment=risk_score_payment,
|
|
121
141
|
hcc_list=list(hcc_set),
|
|
122
142
|
cc_to_dx=cc_to_dx,
|
|
123
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}'
|