hccinfhir 0.0.4__py3-none-any.whl → 0.0.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/data/ra_eligible_cpt_hcpcs_2024.csv +6697 -0
- hccinfhir/data/ra_eligible_cpt_hcpcs_2025.csv +6725 -0
- hccinfhir/datamodels.py +37 -20
- hccinfhir/filter.py +2 -4
- hccinfhir/hccinfhir.py +52 -45
- hccinfhir/model_calculate.py +53 -8
- hccinfhir/model_dx_to_cc.py +8 -24
- hccinfhir/utils.py +51 -0
- {hccinfhir-0.0.4.dist-info → hccinfhir-0.0.6.dist-info}/METADATA +80 -5
- {hccinfhir-0.0.4.dist-info → hccinfhir-0.0.6.dist-info}/RECORD +12 -9
- {hccinfhir-0.0.4.dist-info → hccinfhir-0.0.6.dist-info}/WHEEL +0 -0
- {hccinfhir-0.0.4.dist-info → hccinfhir-0.0.6.dist-info}/licenses/LICENSE +0 -0
hccinfhir/datamodels.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import List, Optional, Literal, Dict,
|
|
2
|
+
from typing import List, Optional, Literal, Dict, Set, TypedDict
|
|
3
3
|
|
|
4
4
|
# Define Model Name literal type
|
|
5
5
|
ModelName = Literal[
|
|
@@ -10,6 +10,17 @@ ModelName = Literal[
|
|
|
10
10
|
"CMS-HCC ESRD Model V24",
|
|
11
11
|
"RxHCC Model V08"
|
|
12
12
|
]
|
|
13
|
+
|
|
14
|
+
ProcFilteringFilename = Literal[
|
|
15
|
+
"ra_eligible_cpt_hcpcs_2023.csv",
|
|
16
|
+
"ra_eligible_cpt_hcpcs_2024.csv",
|
|
17
|
+
"ra_eligible_cpt_hcpcs_2025.csv"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
DxCCMappingFilename = Literal[
|
|
21
|
+
"ra_dx_to_cc_2025.csv"
|
|
22
|
+
]
|
|
23
|
+
|
|
13
24
|
class ServiceLevelData(BaseModel):
|
|
14
25
|
"""
|
|
15
26
|
Represents standardized service-level data extracted from healthcare claims.
|
|
@@ -56,29 +67,35 @@ class Demographics(BaseModel):
|
|
|
56
67
|
"""
|
|
57
68
|
Response model for demographic categorization
|
|
58
69
|
"""
|
|
59
|
-
age: int = Field(..., description="Beneficiary age")
|
|
60
|
-
sex: Literal['M', 'F', '1', '2'] = Field(..., description="Beneficiary sex")
|
|
61
|
-
dual_elgbl_cd: Literal[None, 'NA', '99', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10'] = Field('NA', description="Dual status code")
|
|
62
|
-
orec: Literal[None, '0', '1', '2', '3'] = Field('0', description="Original reason for entitlement")
|
|
63
|
-
crec: Literal[None, '0', '1', '2', '3'] = Field('0', description="Current reason for entitlement")
|
|
64
|
-
new_enrollee: bool = Field(False, description="True if beneficiary is a new enrollee")
|
|
65
|
-
snp: bool = Field(False, description="True if beneficiary is in SNP")
|
|
66
|
-
version: str = Field("V2", description="Version of categorization used (V2, V4, V6)")
|
|
67
|
-
low_income: bool = Field(False, description="True if beneficiary is in low income; RxHCC only")
|
|
70
|
+
age: int = Field(..., description="[required] Beneficiary age")
|
|
71
|
+
sex: Literal['M', 'F', '1', '2'] = Field(..., description="[required] Beneficiary sex")
|
|
72
|
+
dual_elgbl_cd: Optional[Literal[None, 'NA', '99', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10']] = Field('NA', description="Dual status code")
|
|
73
|
+
orec: Optional[Literal[None, '0', '1', '2', '3']] = Field('0', description="Original reason for entitlement")
|
|
74
|
+
crec: Optional[Literal[None, '0', '1', '2', '3']] = Field('0', description="Current reason for entitlement")
|
|
75
|
+
new_enrollee: Optional[bool] = Field(False, description="True if beneficiary is a new enrollee")
|
|
76
|
+
snp: Optional[bool] = Field(False, description="True if beneficiary is in SNP")
|
|
77
|
+
version: Optional[str] = Field("V2", description="Version of categorization used (V2, V4, V6)")
|
|
78
|
+
low_income: Optional[bool] = Field(False, description="True if beneficiary is in low income; RxHCC only")
|
|
68
79
|
graft_months: Optional[int] = Field(None, description="Number of months since transplant; ESRD Model only")
|
|
69
|
-
category: str = Field(
|
|
70
|
-
non_aged: bool = Field(False, description="[derived] True if age <= 64")
|
|
71
|
-
orig_disabled: bool = Field(False, description="[derived] True if originally disabled (OREC='1' and not currently disabled)")
|
|
72
|
-
disabled: bool = Field(False, description="[derived] True if currently disabled (age < 65 and OREC != '0')")
|
|
73
|
-
esrd: bool = Field(False, description="[derived] True if ESRD (ESRD Model)")
|
|
74
|
-
lti: bool = Field(False, description="[derived] True if LTI (LTI Model)")
|
|
75
|
-
fbd: bool = Field(False, description="[derived] True if FBD (FBD Model)")
|
|
76
|
-
pbd: bool = Field(False, description="[derived] True if PBD (PBD Model)")
|
|
80
|
+
category: Optional[str] = Field(None, description="[derived] Age-sex category code")
|
|
81
|
+
non_aged: Optional[bool] = Field(False, description="[derived] True if age <= 64")
|
|
82
|
+
orig_disabled: Optional[bool] = Field(False, description="[derived] True if originally disabled (OREC='1' and not currently disabled)")
|
|
83
|
+
disabled: Optional[bool] = Field(False, description="[derived] True if currently disabled (age < 65 and OREC != '0')")
|
|
84
|
+
esrd: Optional[bool] = Field(False, description="[derived] True if ESRD (ESRD Model)")
|
|
85
|
+
lti: Optional[bool] = Field(False, description="[derived] True if LTI (LTI Model)")
|
|
86
|
+
fbd: Optional[bool] = Field(False, description="[derived] True if FBD (FBD Model)")
|
|
87
|
+
pbd: Optional[bool] = Field(False, description="[derived] True if PBD (PBD Model)")
|
|
77
88
|
|
|
78
89
|
|
|
79
90
|
class RAFResult(TypedDict):
|
|
80
91
|
"""Type definition for RAF calculation results"""
|
|
81
92
|
risk_score: float
|
|
82
93
|
hcc_list: List[str]
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
cc_to_dx: Dict[str, Set[str]]
|
|
95
|
+
coefficients: Dict[str, float]
|
|
96
|
+
interactions: Dict[str, float]
|
|
97
|
+
demographics: Demographics
|
|
98
|
+
model_name: ModelName
|
|
99
|
+
version: str
|
|
100
|
+
diagnosis_codes: List[str]
|
|
101
|
+
service_level_data: Optional[List[ServiceLevelData]]
|
hccinfhir/filter.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
from typing import List, Set
|
|
2
2
|
from hccinfhir.datamodels import ServiceLevelData
|
|
3
|
-
import
|
|
3
|
+
from hccinfhir.utils import load_proc_filtering
|
|
4
4
|
|
|
5
5
|
# use import importlib.resources to load the professional_cpt_fn file as a list of strings
|
|
6
6
|
professional_cpt_default_fn = 'ra_eligible_cpt_hcpcs_2023.csv'
|
|
7
|
-
professional_cpt_default =
|
|
8
|
-
with importlib.resources.open_text('hccinfhir.data', professional_cpt_default_fn) as f:
|
|
9
|
-
professional_cpt_default = set(f.read().splitlines())
|
|
7
|
+
professional_cpt_default = load_proc_filtering(professional_cpt_default_fn)
|
|
10
8
|
|
|
11
9
|
def apply_filter(
|
|
12
10
|
data: List[ServiceLevelData],
|
hccinfhir/hccinfhir.py
CHANGED
|
@@ -2,8 +2,8 @@ from typing import List, Dict, Any, Union
|
|
|
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
|
|
6
|
-
|
|
5
|
+
from hccinfhir.datamodels import Demographics, ServiceLevelData, RAFResult, ModelName, ProcFilteringFilename, DxCCMappingFilename
|
|
6
|
+
from hccinfhir.utils import load_proc_filtering, load_dx_to_cc_mapping
|
|
7
7
|
|
|
8
8
|
class HCCInFHIR:
|
|
9
9
|
"""
|
|
@@ -15,17 +15,26 @@ class HCCInFHIR:
|
|
|
15
15
|
|
|
16
16
|
def __init__(self,
|
|
17
17
|
filter_claims: bool = True,
|
|
18
|
-
model_name: ModelName = "CMS-HCC Model V28"
|
|
18
|
+
model_name: ModelName = "CMS-HCC Model V28",
|
|
19
|
+
proc_filtering_filename: ProcFilteringFilename = "ra_eligible_cpt_hcpcs_2025.csv",
|
|
20
|
+
dx_cc_mapping_filename: DxCCMappingFilename = "ra_dx_to_cc_2025.csv"):
|
|
19
21
|
"""
|
|
20
22
|
Initialize the HCCInFHIR processor.
|
|
21
23
|
|
|
22
24
|
Args:
|
|
23
25
|
filter_claims: Whether to apply filtering rules to claims. Default is True.
|
|
24
26
|
model_name: The name of the model to use for the calculation. Default is "CMS-HCC Model V28".
|
|
27
|
+
proc_filtering_filename: The filename of the professional cpt filtering file. Default is "ra_eligible_cpt_hcpcs_2025.csv".
|
|
28
|
+
dx_cc_mapping_filename: The filename of the dx to cc mapping file. Default is "ra_dx_to_cc_2025.csv".
|
|
25
29
|
"""
|
|
26
30
|
self.filter_claims = filter_claims
|
|
27
31
|
self.model_name = model_name
|
|
28
|
-
|
|
32
|
+
self.proc_filtering_filename = proc_filtering_filename
|
|
33
|
+
self.dx_cc_mapping_filename = dx_cc_mapping_filename
|
|
34
|
+
self.professional_cpt = load_proc_filtering(proc_filtering_filename)
|
|
35
|
+
self.dx_to_cc_mapping = load_dx_to_cc_mapping(dx_cc_mapping_filename)
|
|
36
|
+
|
|
37
|
+
|
|
29
38
|
def _ensure_demographics(self, demographics: Union[Demographics, Dict[str, Any]]) -> Demographics:
|
|
30
39
|
"""Convert demographics dict to Demographics object if needed."""
|
|
31
40
|
if not isinstance(demographics, Demographics):
|
|
@@ -33,7 +42,7 @@ class HCCInFHIR:
|
|
|
33
42
|
return demographics
|
|
34
43
|
|
|
35
44
|
def _calculate_raf_from_demographics(self, diagnosis_codes: List[str],
|
|
36
|
-
demographics: Demographics) ->
|
|
45
|
+
demographics: Demographics) -> RAFResult:
|
|
37
46
|
"""Calculate RAF score using demographics data."""
|
|
38
47
|
return calculate_raf(
|
|
39
48
|
diagnosis_codes=diagnosis_codes,
|
|
@@ -46,58 +55,40 @@ class HCCInFHIR:
|
|
|
46
55
|
new_enrollee=demographics.new_enrollee,
|
|
47
56
|
snp=demographics.snp,
|
|
48
57
|
low_income=demographics.low_income,
|
|
49
|
-
graft_months=demographics.graft_months
|
|
58
|
+
graft_months=demographics.graft_months,
|
|
59
|
+
dx_to_cc_mapping=self.dx_to_cc_mapping
|
|
50
60
|
)
|
|
51
61
|
|
|
52
62
|
def _get_unique_diagnosis_codes(self, service_data: List[ServiceLevelData]) -> List[str]:
|
|
53
63
|
"""Extract unique diagnosis codes from service level data."""
|
|
54
|
-
|
|
55
|
-
for sld in service_data:
|
|
56
|
-
all_dx_codes.extend(sld.claim_diagnosis_codes)
|
|
57
|
-
return list(set(all_dx_codes))
|
|
64
|
+
return list({code for sld in service_data for code in sld.claim_diagnosis_codes})
|
|
58
65
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"""
|
|
63
|
-
Format RAF calculation results into a standardized RAFResult format.
|
|
66
|
+
def run(self, eob_list: List[Dict[str, Any]],
|
|
67
|
+
demographics: Union[Demographics, Dict[str, Any]]) -> RAFResult:
|
|
68
|
+
"""Process EOB resources and calculate RAF scores.
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
Args:
|
|
71
|
+
eob_list: List of EOB resources
|
|
72
|
+
demographics: Demographics information
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
RAFResult object containing calculated scores and processed data
|
|
66
76
|
"""
|
|
77
|
+
if not isinstance(eob_list, list):
|
|
78
|
+
raise ValueError("eob_list must be a list; if no eob, pass empty list")
|
|
67
79
|
|
|
68
|
-
# Check if raf_result already has the expected RAFResult structure
|
|
69
|
-
if all(key in raf_result for key in ['risk_score', 'hcc_list', 'details']):
|
|
70
|
-
# Already in RAFResult format, just ensure service data is set
|
|
71
|
-
result = dict(raf_result) # Create a copy to avoid modifying the original
|
|
72
|
-
result['service_level_data'] = service_data
|
|
73
|
-
return result
|
|
74
|
-
|
|
75
|
-
# Handle result from calculate_raf function
|
|
76
|
-
if 'raf' in raf_result and 'coefficients' in raf_result:
|
|
77
|
-
return {
|
|
78
|
-
'risk_score': raf_result['raf'],
|
|
79
|
-
'hcc_list': list(raf_result['coefficients'].keys()),
|
|
80
|
-
'details': raf_result['coefficients'],
|
|
81
|
-
'service_level_data': service_data
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Unrecognized format
|
|
85
|
-
raise ValueError(f"Unrecognized RAF result format: {list(raf_result.keys())}")
|
|
86
|
-
|
|
87
|
-
def run(self, eob_list: List[Dict[str, Any]],
|
|
88
|
-
demographics: Union[Demographics, Dict[str, Any]]) -> RAFResult:
|
|
89
80
|
demographics = self._ensure_demographics(demographics)
|
|
90
81
|
|
|
91
82
|
# Extract and filter service level data
|
|
92
83
|
sld_list = extract_sld_list(eob_list)
|
|
93
84
|
if self.filter_claims:
|
|
94
|
-
sld_list = apply_filter(sld_list)
|
|
85
|
+
sld_list = apply_filter(sld_list, self.professional_cpt)
|
|
95
86
|
|
|
96
87
|
# Calculate RAF score
|
|
97
88
|
unique_dx_codes = self._get_unique_diagnosis_codes(sld_list)
|
|
98
89
|
raf_result = self._calculate_raf_from_demographics(unique_dx_codes, demographics)
|
|
99
|
-
|
|
100
|
-
return
|
|
90
|
+
raf_result['service_level_data'] = sld_list
|
|
91
|
+
return raf_result
|
|
101
92
|
|
|
102
93
|
def run_from_service_data(self, service_data: List[Union[ServiceLevelData, Dict[str, Any]]],
|
|
103
94
|
demographics: Union[Demographics, Dict[str, Any]]) -> RAFResult:
|
|
@@ -126,17 +117,33 @@ class HCCInFHIR:
|
|
|
126
117
|
)
|
|
127
118
|
|
|
128
119
|
if self.filter_claims:
|
|
129
|
-
standardized_data = apply_filter(standardized_data
|
|
120
|
+
standardized_data = apply_filter(standardized_data,
|
|
121
|
+
professional_cpt=self.professional_cpt)
|
|
122
|
+
|
|
130
123
|
|
|
131
124
|
# Calculate RAF score
|
|
132
125
|
unique_dx_codes = self._get_unique_diagnosis_codes(standardized_data)
|
|
133
126
|
raf_result = self._calculate_raf_from_demographics(unique_dx_codes, demographics)
|
|
134
|
-
|
|
135
|
-
|
|
127
|
+
raf_result['service_level_data'] = standardized_data
|
|
128
|
+
|
|
129
|
+
return raf_result
|
|
136
130
|
|
|
137
131
|
def calculate_from_diagnosis(self, diagnosis_codes: List[str],
|
|
138
132
|
demographics: Union[Demographics, Dict[str, Any]]) -> RAFResult:
|
|
133
|
+
"""Calculate RAF scores from a list of diagnosis codes.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
diagnosis_codes: List of diagnosis codes
|
|
137
|
+
demographics: Demographics information
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValueError: If diagnosis_codes is empty or not a list
|
|
141
|
+
"""
|
|
142
|
+
if not isinstance(diagnosis_codes, list):
|
|
143
|
+
raise ValueError("diagnosis_codes must be a list")
|
|
144
|
+
if not diagnosis_codes:
|
|
145
|
+
raise ValueError("diagnosis_codes list cannot be empty")
|
|
146
|
+
|
|
139
147
|
demographics = self._ensure_demographics(demographics)
|
|
140
148
|
raf_result = self._calculate_raf_from_demographics(diagnosis_codes, demographics)
|
|
141
|
-
|
|
142
|
-
return self._format_result(raf_result, [])
|
|
149
|
+
return raf_result
|
hccinfhir/model_calculate.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
from typing import List, Union
|
|
2
|
-
from hccinfhir.datamodels import ModelName
|
|
1
|
+
from typing import List, Union, Dict, Tuple, Set
|
|
2
|
+
from hccinfhir.datamodels import ModelName, RAFResult
|
|
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
|
|
6
6
|
from hccinfhir.model_coefficients import apply_coefficients
|
|
7
7
|
from hccinfhir.model_interactions import apply_interactions
|
|
8
|
+
from hccinfhir.utils import load_dx_to_cc_mapping
|
|
9
|
+
|
|
10
|
+
# Load default mappings from csv file
|
|
11
|
+
mapping_file_default = 'ra_dx_to_cc_2025.csv'
|
|
12
|
+
dx_to_cc_default = load_dx_to_cc_mapping(mapping_file_default)
|
|
8
13
|
|
|
9
14
|
def calculate_raf(diagnosis_codes: List[str],
|
|
10
15
|
model_name: ModelName = "CMS-HCC Model V28",
|
|
@@ -16,14 +21,43 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
16
21
|
new_enrollee: bool = False,
|
|
17
22
|
snp: bool = False,
|
|
18
23
|
low_income: bool = False,
|
|
19
|
-
graft_months: int =
|
|
24
|
+
graft_months: int = None,
|
|
25
|
+
dx_to_cc_mapping: Dict[Tuple[str, ModelName], Set[str]] = dx_to_cc_default) -> RAFResult:
|
|
26
|
+
"""
|
|
27
|
+
Calculate Risk Adjustment Factor (RAF) based on diagnosis codes and demographic information.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
diagnosis_codes: List of ICD-10 diagnosis codes
|
|
31
|
+
model_name: Name of the HCC model to use
|
|
32
|
+
age: Patient's age
|
|
33
|
+
sex: Patient's sex ('M' or 'F')
|
|
34
|
+
dual_elgbl_cd: Dual eligibility code
|
|
35
|
+
orec: Original reason for entitlement code
|
|
36
|
+
crec: Current reason for entitlement code
|
|
37
|
+
new_enrollee: Whether the patient is a new enrollee
|
|
38
|
+
snp: Special Needs Plan indicator
|
|
39
|
+
low_income: Low income subsidy indicator
|
|
40
|
+
graft_months: Number of months since transplant
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary containing RAF score and coefficients used in calculation
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If input parameters are invalid
|
|
47
|
+
"""
|
|
48
|
+
# Input validation
|
|
49
|
+
if not isinstance(age, (int, float)) or age < 0:
|
|
50
|
+
raise ValueError("Age must be a non-negative number")
|
|
51
|
+
|
|
52
|
+
if sex not in ['M', 'F', '1', '2']:
|
|
53
|
+
raise ValueError("Sex must be 'M' or 'F' or '1' or '2'")
|
|
20
54
|
|
|
21
55
|
version = 'V2'
|
|
22
56
|
if 'RxHCC' in model_name:
|
|
23
57
|
version = 'V4'
|
|
24
58
|
elif 'HHS-HCC' in model_name: # not implemented yet
|
|
25
59
|
version = 'V6'
|
|
26
|
-
|
|
60
|
+
|
|
27
61
|
demographics = categorize_demographics(age,
|
|
28
62
|
sex,
|
|
29
63
|
dual_elgbl_cd,
|
|
@@ -35,16 +69,27 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
35
69
|
low_income,
|
|
36
70
|
graft_months)
|
|
37
71
|
|
|
38
|
-
cc_to_dx = apply_mapping(diagnosis_codes,
|
|
72
|
+
cc_to_dx = apply_mapping(diagnosis_codes,
|
|
73
|
+
model_name,
|
|
74
|
+
dx_to_cc_mapping=dx_to_cc_mapping)
|
|
39
75
|
hcc_set = set(cc_to_dx.keys())
|
|
40
76
|
hcc_set = apply_hierarchies(hcc_set, model_name)
|
|
41
77
|
interactions = apply_interactions(demographics, hcc_set, model_name)
|
|
42
78
|
coefficients = apply_coefficients(demographics, hcc_set, interactions, model_name)
|
|
43
79
|
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
risk_score = sum(coefficients.values())
|
|
46
81
|
|
|
47
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
'risk_score': risk_score,
|
|
84
|
+
'hcc_list': list(hcc_set),
|
|
85
|
+
'cc_to_dx': cc_to_dx,
|
|
86
|
+
'coefficients': coefficients,
|
|
87
|
+
'interactions': interactions,
|
|
88
|
+
'demographics': demographics,
|
|
89
|
+
'model_name': model_name,
|
|
90
|
+
'version': version,
|
|
91
|
+
'diagnosis_codes': diagnosis_codes,
|
|
92
|
+
}
|
|
48
93
|
|
|
49
94
|
|
|
50
95
|
|
hccinfhir/model_dx_to_cc.py
CHANGED
|
@@ -1,31 +1,15 @@
|
|
|
1
1
|
from typing import List, Dict, Set, Tuple, Optional
|
|
2
|
-
import importlib.resources
|
|
3
2
|
from hccinfhir.datamodels import ModelName
|
|
3
|
+
from hccinfhir.utils import load_dx_to_cc_mapping
|
|
4
4
|
|
|
5
5
|
# Load default mappings from csv file
|
|
6
6
|
mapping_file_default = 'ra_dx_to_cc_2025.csv'
|
|
7
|
-
dx_to_cc_default
|
|
8
|
-
|
|
9
|
-
try:
|
|
10
|
-
with importlib.resources.open_text('hccinfhir.data', mapping_file_default) as f:
|
|
11
|
-
for line in f.readlines()[1:]: # Skip header
|
|
12
|
-
try:
|
|
13
|
-
diagnosis_code, cc, model_name = line.strip().split(',')
|
|
14
|
-
key = (diagnosis_code, model_name)
|
|
15
|
-
if key not in dx_to_cc_default:
|
|
16
|
-
dx_to_cc_default[key] = {cc}
|
|
17
|
-
else:
|
|
18
|
-
dx_to_cc_default[key].add(cc)
|
|
19
|
-
except ValueError:
|
|
20
|
-
continue # Skip malformed lines
|
|
21
|
-
except Exception as e:
|
|
22
|
-
print(f"Error loading mapping file: {e}")
|
|
23
|
-
dx_to_cc_default = {}
|
|
7
|
+
dx_to_cc_default = load_dx_to_cc_mapping(mapping_file_default)
|
|
24
8
|
|
|
25
9
|
def get_cc(
|
|
26
10
|
diagnosis_code: str,
|
|
27
11
|
model_name: ModelName = "CMS-HCC Model V28",
|
|
28
|
-
|
|
12
|
+
dx_to_cc_mapping: Dict[Tuple[str, ModelName], Set[str]] = dx_to_cc_default
|
|
29
13
|
) -> Optional[Set[str]]:
|
|
30
14
|
"""
|
|
31
15
|
Get CC for a single diagnosis code.
|
|
@@ -33,17 +17,17 @@ def get_cc(
|
|
|
33
17
|
Args:
|
|
34
18
|
diagnosis_code: ICD-10 diagnosis code
|
|
35
19
|
model_name: HCC model name to use for mapping
|
|
36
|
-
|
|
20
|
+
dx_to_cc_mapping: Optional custom mapping dictionary
|
|
37
21
|
|
|
38
22
|
Returns:
|
|
39
23
|
CC code if found, None otherwise
|
|
40
24
|
"""
|
|
41
|
-
return
|
|
25
|
+
return dx_to_cc_mapping.get((diagnosis_code, model_name))
|
|
42
26
|
|
|
43
27
|
def apply_mapping(
|
|
44
28
|
diagnoses: List[str],
|
|
45
29
|
model_name: ModelName = "CMS-HCC Model V28",
|
|
46
|
-
|
|
30
|
+
dx_to_cc_mapping: Dict[Tuple[str, ModelName], Set[str]] = dx_to_cc_default
|
|
47
31
|
) -> Dict[str, Set[str]]:
|
|
48
32
|
"""
|
|
49
33
|
Apply ICD-10 to CC mapping for a list of diagnosis codes.
|
|
@@ -51,7 +35,7 @@ def apply_mapping(
|
|
|
51
35
|
Args:
|
|
52
36
|
diagnoses: List of ICD-10 diagnosis codes
|
|
53
37
|
model_name: HCC model name to use for mapping
|
|
54
|
-
|
|
38
|
+
dx_to_cc_mapping: Optional custom mapping dictionary
|
|
55
39
|
|
|
56
40
|
Returns:
|
|
57
41
|
Dictionary mapping CCs to lists of diagnosis codes that map to them
|
|
@@ -60,7 +44,7 @@ def apply_mapping(
|
|
|
60
44
|
|
|
61
45
|
for dx in set(diagnoses):
|
|
62
46
|
dx = dx.upper().replace('.', '')
|
|
63
|
-
ccs = get_cc(dx, model_name,
|
|
47
|
+
ccs = get_cc(dx, model_name, dx_to_cc_mapping)
|
|
64
48
|
if ccs is not None:
|
|
65
49
|
for cc in ccs:
|
|
66
50
|
if cc not in cc_to_dx:
|
hccinfhir/utils.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Set, Dict, Tuple
|
|
2
|
+
import importlib.resources
|
|
3
|
+
from hccinfhir.datamodels import ModelName, ProcFilteringFilename, DxCCMappingFilename
|
|
4
|
+
|
|
5
|
+
def load_proc_filtering(filename: ProcFilteringFilename) -> Set[str]:
|
|
6
|
+
"""
|
|
7
|
+
Load a single-column CSV file into a set of strings.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
filename: Name of the CSV file in the hccinfhir.data package
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Set of strings from the CSV file
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
with importlib.resources.open_text('hccinfhir.data', filename) as f:
|
|
17
|
+
return set(f.read().splitlines())
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print(f"Error loading {filename}: {e}")
|
|
20
|
+
return set()
|
|
21
|
+
|
|
22
|
+
def load_dx_to_cc_mapping(filename: DxCCMappingFilename) -> Dict[Tuple[str, ModelName], Set[str]]:
|
|
23
|
+
"""
|
|
24
|
+
Load diagnosis to CC mapping from a CSV file.
|
|
25
|
+
Expected format: diagnosis_code,cc,model_name
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
filename: Name of the CSV file in the hccinfhir.data package
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary mapping (diagnosis_code, model_name) to a set of CC codes
|
|
32
|
+
"""
|
|
33
|
+
mapping: Dict[Tuple[str, ModelName], Set[str]] = {}
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with importlib.resources.open_text('hccinfhir.data', filename) as f:
|
|
37
|
+
for line in f.readlines()[1:]: # Skip header
|
|
38
|
+
try:
|
|
39
|
+
diagnosis_code, cc, model_name = line.strip().split(',')
|
|
40
|
+
key = (diagnosis_code, model_name)
|
|
41
|
+
if key not in mapping:
|
|
42
|
+
mapping[key] = {cc}
|
|
43
|
+
else:
|
|
44
|
+
mapping[key].add(cc)
|
|
45
|
+
except ValueError:
|
|
46
|
+
continue # Skip malformed lines
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"Error loading mapping file: {e}")
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
return mapping
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hccinfhir
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: HCC Algorithm for FHIR Resources
|
|
5
5
|
Project-URL: Homepage, https://github.com/mimilabs/hccinfhir
|
|
6
6
|
Project-URL: Issues, https://github.com/mimilabs/hccinfhir/issues
|
|
@@ -20,6 +20,9 @@ A Python library for extracting standardized service-level data from FHIR Explan
|
|
|
20
20
|
- Support for both BCDA (Blue Button 2.0) and standard FHIR R4 formats
|
|
21
21
|
- Pydantic models for type safety and data validation
|
|
22
22
|
- Standardized Service Level Data (SLD) output format
|
|
23
|
+
- Multiple HCC model support (V22, V24, V28, ESRD V21, ESRD V24, RxHCC V08)
|
|
24
|
+
- Flexible input options: FHIR EOBs, service data, or direct diagnosis codes
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
## Installation
|
|
25
28
|
```bash
|
|
@@ -109,16 +112,75 @@ result = calculate_raf(
|
|
|
109
112
|
)
|
|
110
113
|
```
|
|
111
114
|
|
|
112
|
-
### 4.
|
|
115
|
+
### 4. HCCInFHIR Class
|
|
116
|
+
The main processor class that integrates extraction, filtering, and calculation components:
|
|
113
117
|
|
|
114
118
|
```python
|
|
115
|
-
from hccinfhir import HCCInFHIR
|
|
119
|
+
from hccinfhir.hccinfhir import HCCInFHIR
|
|
120
|
+
from hccinfhir.datamodels import Demographics
|
|
121
|
+
|
|
122
|
+
# Initialize with custom configuration
|
|
123
|
+
hcc_processor = HCCInFHIR(
|
|
124
|
+
filter_claims=True, # Enable claim filtering
|
|
125
|
+
model_name="CMS-HCC Model V28", # Choose HCC model version
|
|
126
|
+
proc_filtering_filename="ra_eligible_cpt_hcpcs_2025.csv", # CPT/HCPCS filtering rules
|
|
127
|
+
dx_cc_mapping_filename="ra_dx_to_cc_2025.csv" # Diagnosis to CC mapping
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Define beneficiary demographics
|
|
131
|
+
demographics = {
|
|
132
|
+
age=67,
|
|
133
|
+
sex='F'
|
|
134
|
+
}
|
|
116
135
|
|
|
117
|
-
|
|
136
|
+
# Method 1: Process FHIR EOB resources
|
|
137
|
+
raf_result = hcc_processor.run(eob_list, demographics)
|
|
118
138
|
|
|
119
|
-
|
|
139
|
+
# Method 2: Process service level data
|
|
140
|
+
service_data = [{
|
|
141
|
+
"procedure_code": "99214",
|
|
142
|
+
"claim_diagnosis_codes": ["E11.9", "I10"],
|
|
143
|
+
"claim_type": "71",
|
|
144
|
+
"service_date": "2024-01-15"
|
|
145
|
+
}]
|
|
146
|
+
raf_result = hcc_processor.run_from_service_data(service_data, demographics)
|
|
147
|
+
|
|
148
|
+
# Method 3: Direct diagnosis processing
|
|
149
|
+
diagnosis_codes = ['E119', 'I509']
|
|
150
|
+
raf_result = hcc_processor.calculate_from_diagnosis(diagnosis_codes, demographics)
|
|
151
|
+
|
|
152
|
+
# RAF Result contains:
|
|
153
|
+
print(f"Risk Score: {raf_result['risk_score']}")
|
|
154
|
+
print(f"HCC List: {raf_result['hcc_list']}")
|
|
155
|
+
print(f"CC to Diagnosis Mapping: {raf_result['cc_to_dx']}")
|
|
156
|
+
print(f"Applied Coefficients: {raf_result['coefficients']}")
|
|
157
|
+
print(f"Applied Interactions: {raf_result['interactions']}")
|
|
120
158
|
```
|
|
121
159
|
|
|
160
|
+
The HCCInFHIR class provides three main processing methods:
|
|
161
|
+
|
|
162
|
+
1. `run(eob_list, demographics)`: Process FHIR ExplanationOfBenefit resources
|
|
163
|
+
- Extracts service data from FHIR resources
|
|
164
|
+
- Applies filtering rules if enabled
|
|
165
|
+
- Calculates RAF scores using the specified model
|
|
166
|
+
|
|
167
|
+
2. `run_from_service_data(service_data, demographics)`: Process standardized service data
|
|
168
|
+
- Accepts pre-formatted service level data
|
|
169
|
+
- Validates data structure using Pydantic models
|
|
170
|
+
- Applies filtering and calculates RAF scores
|
|
171
|
+
|
|
172
|
+
3. `calculate_from_diagnosis(diagnosis_codes, demographics)`: Direct diagnosis processing
|
|
173
|
+
- Processes raw diagnosis codes without service context
|
|
174
|
+
- Useful for quick RAF calculations or validation
|
|
175
|
+
- Bypasses service-level filtering
|
|
176
|
+
|
|
177
|
+
Each method returns a RAFResult containing:
|
|
178
|
+
- Final risk score
|
|
179
|
+
- List of HCCs
|
|
180
|
+
- Mapping of condition categories to diagnosis codes
|
|
181
|
+
- Applied coefficients and interactions
|
|
182
|
+
- Processed service level data (when applicable)
|
|
183
|
+
|
|
122
184
|
## Testing
|
|
123
185
|
```bash
|
|
124
186
|
$ python3 -m hatch shell
|
|
@@ -230,8 +292,21 @@ FROM ra_coefficients
|
|
|
230
292
|
WHERE eff_last_date > '2025-01-01';
|
|
231
293
|
```
|
|
232
294
|
|
|
295
|
+
`ra_eligible_cpt_hcpcs_2025.csv`
|
|
296
|
+
```sql
|
|
297
|
+
SELECT DISTINCT cpt_hcpcs_code
|
|
298
|
+
FROM mimi_ws_1.cmspayment.ra_eligible_cpt_hcpcs
|
|
299
|
+
WHERE is_included = 'yes' AND YEAR(mimi_src_file_date) = 2024;
|
|
300
|
+
```
|
|
301
|
+
|
|
233
302
|
## Contributing
|
|
234
303
|
Join us at [mimilabs](https://mimilabs.ai/signup). Reference data available in MIMILabs data lakehouse.
|
|
235
304
|
|
|
305
|
+
## Publishing (only for those maintainers...)
|
|
306
|
+
```bash
|
|
307
|
+
$ python3 -m hatch build
|
|
308
|
+
$ python3 -m hatch publish
|
|
309
|
+
```
|
|
310
|
+
|
|
236
311
|
## License
|
|
237
312
|
Apache License 2.0
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
hccinfhir/__init__.py,sha256=OCyYCv4jTOlYHZbTw2DTks3e6_YT1N2JXAOuyR03KNE,43
|
|
2
|
-
hccinfhir/datamodels.py,sha256=
|
|
2
|
+
hccinfhir/datamodels.py,sha256=ZohKX_LqtcEkGW3-tvtj9AWMepMooNtWpchSv9bOXuQ,4845
|
|
3
3
|
hccinfhir/extractor.py,sha256=-jHVCIJqFAqvrI9GxkkXZVDQjKDa-7vF7v3PGMGAMnA,1801
|
|
4
4
|
hccinfhir/extractor_837.py,sha256=vkTBCd0WBaJoTrUd-Z-zCIaoLk7KV2n4AGqIORhONIk,7147
|
|
5
5
|
hccinfhir/extractor_fhir.py,sha256=Rg_L0Vg5tz_L2VJ_jvZwWz6RMlPAkHwj4LiK-OWQvrQ,8458
|
|
6
|
-
hccinfhir/filter.py,sha256=
|
|
7
|
-
hccinfhir/hccinfhir.py,sha256=
|
|
8
|
-
hccinfhir/model_calculate.py,sha256=
|
|
6
|
+
hccinfhir/filter.py,sha256=8uYThN0-AqwVAKyti29WGiFwQKDiremMhYd_m6QcXhM,2193
|
|
7
|
+
hccinfhir/hccinfhir.py,sha256=YdooDAezKPF1VXoAeW8tsvQwm9SZ6u2SO-a0rhoX0iY,6933
|
|
8
|
+
hccinfhir/model_calculate.py,sha256=6cjWMtayC9fBxhTCnbqSJSlctblYz4Mn9vQYDXx-Iu8,3725
|
|
9
9
|
hccinfhir/model_coefficients.py,sha256=UrDAEWBoqooSC8hy9YSUsLMmmfgIO0YGtVkui6ruOkE,5528
|
|
10
10
|
hccinfhir/model_demographics.py,sha256=LZLlPQOtxPh3Md6q9xwztQ9PwrUo_gloNCYSa2bDirY,6578
|
|
11
|
-
hccinfhir/model_dx_to_cc.py,sha256=
|
|
11
|
+
hccinfhir/model_dx_to_cc.py,sha256=jCFlnAOBkfI9FrCX6tZIh-Sp_DW0HwpY7QrPXGtwInI,1765
|
|
12
12
|
hccinfhir/model_hierarchies.py,sha256=e8QtSayTrfPv2wh149FjK7ToiEmU1ISYMA1Pi38iVk0,2700
|
|
13
13
|
hccinfhir/model_interactions.py,sha256=ZLiKJepPjPkYceKDf7dLXoYE0p44I7t9y2sTOlrxojo,20264
|
|
14
|
+
hccinfhir/utils.py,sha256=5tPwf_neqSJXlerhHbrPPihC0nMKsU40HOBoNCEkGco,1791
|
|
14
15
|
hccinfhir/data/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
|
|
15
16
|
hccinfhir/data/ra_coefficients_2025.csv,sha256=I0S2hoJlfig-D0oSFxy0b3Piv7m9AzOGo2CwR6bcQ9w,215191
|
|
16
17
|
hccinfhir/data/ra_dx_to_cc_2025.csv,sha256=4N7vF6VZndkl7d3Fo0cGsbAPAZdCjAizSH8BOKsZNAo,1618924
|
|
17
18
|
hccinfhir/data/ra_eligible_cpt_hcpcs_2023.csv,sha256=VVoA4s0hsFmcRIugyFdbvSoeLcn7M7z0DITT6l4YqL8,39885
|
|
19
|
+
hccinfhir/data/ra_eligible_cpt_hcpcs_2024.csv,sha256=CawKImfCb8fFMDbWwqvNLRyRAda_u9N8Q3ne8QAAe54,40191
|
|
20
|
+
hccinfhir/data/ra_eligible_cpt_hcpcs_2025.csv,sha256=-tMvv2su5tsSbGUh6fZZCMUEkXInBpcTtbUCi2o_UwI,40359
|
|
18
21
|
hccinfhir/data/ra_hierarchies_2025.csv,sha256=HQSPNloe6mvvwMgv8ZwYAfWKkT2b2eUvm4JQy6S_mVQ,13045
|
|
19
22
|
hccinfhir/samples/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
|
|
20
23
|
hccinfhir/samples/sample_837_0.txt,sha256=eggrD259uHa05z2dfxWBpUDseSDp_AQcLyN_adpHyTw,5295
|
|
@@ -33,7 +36,7 @@ hccinfhir/samples/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9fNzQEM
|
|
|
33
36
|
hccinfhir/samples/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
|
|
34
37
|
hccinfhir/samples/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
|
|
35
38
|
hccinfhir/samples/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
|
|
36
|
-
hccinfhir-0.0.
|
|
37
|
-
hccinfhir-0.0.
|
|
38
|
-
hccinfhir-0.0.
|
|
39
|
-
hccinfhir-0.0.
|
|
39
|
+
hccinfhir-0.0.6.dist-info/METADATA,sha256=ZrsvQOjZWL2E5LMv4-H6VIBr0OiTTFL7SVsmq8ivfdk,11567
|
|
40
|
+
hccinfhir-0.0.6.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
41
|
+
hccinfhir-0.0.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
42
|
+
hccinfhir-0.0.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|