hccinfhir 0.0.3__py3-none-any.whl → 0.0.5__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.
Files changed (41) hide show
  1. hccinfhir/data/ra_coefficients_2025.csv +6352 -0
  2. hccinfhir/data/ra_dx_to_cc_2025.csv +53952 -0
  3. hccinfhir/data/ra_eligible_cpt_hcpcs_2024.csv +6697 -0
  4. hccinfhir/data/ra_eligible_cpt_hcpcs_2025.csv +6725 -0
  5. hccinfhir/data/ra_hierarchies_2025.csv +487 -0
  6. hccinfhir/datamodels.py +101 -0
  7. hccinfhir/extractor.py +3 -3
  8. hccinfhir/extractor_837.py +1 -2
  9. hccinfhir/extractor_fhir.py +1 -1
  10. hccinfhir/filter.py +3 -5
  11. hccinfhir/hccinfhir.py +149 -0
  12. hccinfhir/model_calculate.py +95 -0
  13. hccinfhir/model_coefficients.py +143 -0
  14. hccinfhir/model_demographics.py +191 -0
  15. hccinfhir/model_dx_to_cc.py +54 -0
  16. hccinfhir/model_hierarchies.py +70 -0
  17. hccinfhir/model_interactions.py +342 -0
  18. hccinfhir/samples/__init__.py +2 -0
  19. hccinfhir/utils.py +51 -0
  20. {hccinfhir-0.0.3.dist-info → hccinfhir-0.0.5.dist-info}/METADATA +147 -9
  21. hccinfhir-0.0.5.dist-info/RECORD +42 -0
  22. hccinfhir/models.py +0 -44
  23. hccinfhir-0.0.3.dist-info/RECORD +0 -28
  24. /hccinfhir/{data → samples}/sample_837_0.txt +0 -0
  25. /hccinfhir/{data → samples}/sample_837_1.txt +0 -0
  26. /hccinfhir/{data → samples}/sample_837_10.txt +0 -0
  27. /hccinfhir/{data → samples}/sample_837_11.txt +0 -0
  28. /hccinfhir/{data → samples}/sample_837_2.txt +0 -0
  29. /hccinfhir/{data → samples}/sample_837_3.txt +0 -0
  30. /hccinfhir/{data → samples}/sample_837_4.txt +0 -0
  31. /hccinfhir/{data → samples}/sample_837_5.txt +0 -0
  32. /hccinfhir/{data → samples}/sample_837_6.txt +0 -0
  33. /hccinfhir/{data → samples}/sample_837_7.txt +0 -0
  34. /hccinfhir/{data → samples}/sample_837_8.txt +0 -0
  35. /hccinfhir/{data → samples}/sample_837_9.txt +0 -0
  36. /hccinfhir/{data → samples}/sample_eob_1.json +0 -0
  37. /hccinfhir/{data → samples}/sample_eob_2.json +0 -0
  38. /hccinfhir/{data → samples}/sample_eob_200.ndjson +0 -0
  39. /hccinfhir/{data → samples}/sample_eob_3.json +0 -0
  40. {hccinfhir-0.0.3.dist-info → hccinfhir-0.0.5.dist-info}/WHEEL +0 -0
  41. {hccinfhir-0.0.3.dist-info → hccinfhir-0.0.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,70 @@
1
+ from typing import Dict, Set, Tuple
2
+ import importlib.resources
3
+ from hccinfhir.datamodels import ModelName
4
+
5
+ # Load default mappings from csv file
6
+ hierarchies_file_default = 'ra_hierarchies_2025.csv'
7
+ hierarchies_default: Dict[Tuple[str, ModelName], Set[str]] = {} # (diagnosis_code, model_name) -> {cc}
8
+
9
+ try:
10
+ with importlib.resources.open_text('hccinfhir.data', hierarchies_file_default) as f:
11
+ for line in f.readlines()[1:]: # Skip header
12
+ try:
13
+ cc_parent, cc_child, model_domain, model_version, _ = line.strip().split(',')
14
+ if model_domain == 'ESRD':
15
+ model_name = f"CMS-HCC {model_domain} Model {model_version}"
16
+ else:
17
+ model_name = f"{model_domain} Model {model_version}"
18
+ key = (cc_parent, model_name)
19
+ if key not in hierarchies_default:
20
+ hierarchies_default[key] = {cc_child}
21
+ else:
22
+ hierarchies_default[key].add(cc_child)
23
+ except ValueError:
24
+ continue # Skip malformed lines
25
+ except Exception as e:
26
+ print(f"Error loading mapping file: {e}")
27
+ hierarchies_default = {}
28
+
29
+ def apply_hierarchies(
30
+ cc_set: Set[str], # Set of active CCs
31
+ model_name: ModelName = "CMS-HCC Model V28",
32
+ hierarchies: Dict[Tuple[str, ModelName], Set[str]] = hierarchies_default
33
+ ) -> Set[str]:
34
+ """
35
+ Apply hierarchical rules to a set of CCs based on model version.
36
+
37
+ Args:
38
+ ccs: Set of current active CCs
39
+ model_name: HCC model name to use for hierarchy rules
40
+ hierarchies: Optional custom hierarchy dictionary
41
+
42
+ Returns:
43
+ Set of CCs after applying hierarchies
44
+ """
45
+ # Track CCs that should be zeroed out
46
+ to_remove = set()
47
+
48
+ # For V28, if none of 221, 222, 224, 225, 226 are present, remove 223
49
+ if model_name == "CMS-HCC Model V28":
50
+ if ("223" in cc_set and
51
+ not any(cc in cc_set for cc in ["221", "222", "224", "225", "226"])):
52
+ cc_set.remove("223")
53
+ elif model_name == "CMS-HCC ESRD Model V21":
54
+ if "134" in cc_set:
55
+ cc_set.remove("134")
56
+ elif model_name == "CMS-HCC ESRD Model V24":
57
+ for cc in ["134", "135", "136", "137"]:
58
+ if cc in cc_set:
59
+ cc_set.remove(cc)
60
+
61
+ # Apply hierarchies
62
+ for cc in cc_set:
63
+ hierarchy_key = (cc, model_name)
64
+ if hierarchy_key in hierarchies:
65
+ # If parent CC exists, remove all child CCs
66
+ child_ccs = hierarchies[hierarchy_key]
67
+ to_remove.update(child_ccs & cc_set)
68
+
69
+ # Return CCs with hierarchical exclusions removed
70
+ return cc_set - to_remove
@@ -0,0 +1,342 @@
1
+ from hccinfhir.datamodels import Demographics, ModelName
2
+ from typing import Optional
3
+
4
+ def has_any_hcc(hcc_list: list[str], hcc_set: set[str]) -> int:
5
+ """Returns 1 if any HCC in the list is present, 0 otherwise"""
6
+ return int(bool(set(hcc_list) & hcc_set))
7
+
8
+ def create_demographic_interactions(demographics: Demographics) -> dict:
9
+ """Creates common demographic-based interactions"""
10
+ interactions = {}
11
+ is_female = demographics.category.startswith('F')
12
+ is_male = demographics.category.startswith('M')
13
+ is_aged = not demographics.non_aged
14
+
15
+ # Original Disability interactions
16
+ if is_aged:
17
+ interactions['OriginallyDisabled_Female'] = int(demographics.orig_disabled) * int(is_female)
18
+ interactions['OriginallyDisabled_Male'] = int(demographics.orig_disabled) * int(is_male)
19
+ else:
20
+ interactions['OriginallyDisabled_Female'] = 0
21
+ interactions['OriginallyDisabled_Male'] = 0
22
+
23
+ # LTI interactions - used for ESRD models
24
+ if demographics.lti:
25
+ interactions['LTI_Aged'] = int(is_aged)
26
+ interactions['LTI_NonAged'] = int(not is_aged)
27
+ else:
28
+ interactions['LTI_Aged'] = 0
29
+ interactions['LTI_NonAged'] = 0
30
+
31
+ nemcaid = False
32
+ if demographics.new_enrollee and demographics.dual_elgbl_cd in {'01', '02', '03', '04', '05', '06', '08'}:
33
+ nemcaid = True
34
+ ne_origds = int(demographics.age >= 65 and (demographics.orec is not None and demographics.orec == "1"))
35
+
36
+ # Four mutually exclusive groups
37
+ interactions.update({
38
+ f'NMCAID_NORIGDIS_{demographics.category}': int(not nemcaid and not ne_origds),
39
+ f'MCAID_NORIGDIS_{demographics.category}': int(nemcaid and not ne_origds),
40
+ f'NMCAID_ORIGDIS_{demographics.category}': int(not nemcaid and ne_origds),
41
+ f'MCAID_ORIGDIS_{demographics.category}': int(nemcaid and ne_origds)
42
+ })
43
+
44
+ return interactions
45
+
46
+ def create_dual_interactions(demographics: Demographics) -> dict:
47
+ """Creates dual status interactions"""
48
+ interactions = {}
49
+ is_female = demographics.category.startswith('F')
50
+ is_male = demographics.category.startswith('M')
51
+ is_aged = not demographics.non_aged
52
+
53
+ if demographics.fbd:
54
+ interactions.update({
55
+ 'FBDual_Female_Aged': int(is_female) * int(is_aged),
56
+ 'FBDual_Female_NonAged': int(is_female) * int(not is_aged),
57
+ 'FBDual_Male_Aged': int(is_male) * int(is_aged),
58
+ 'FBDual_Male_NonAged': int(is_male) * int(not is_aged)
59
+ })
60
+
61
+ if demographics.pbd:
62
+ interactions.update({
63
+ 'PBDual_Female_Aged': int(is_female) * int(is_aged),
64
+ 'PBDual_Female_NonAged': int(is_female) * int(not is_aged),
65
+ 'PBDual_Male_Aged': int(is_male) * int(is_aged),
66
+ 'PBDual_Male_NonAged': int(is_male) * int(not is_aged)
67
+ })
68
+
69
+ return interactions
70
+
71
+ def create_hcc_counts(hcc_set: set[str]) -> dict:
72
+ """Creates HCC count variables"""
73
+ counts = {}
74
+ hcc_count = len(hcc_set)
75
+
76
+ for i in range(1, 10):
77
+ counts[f'D{i}'] = int(hcc_count == i)
78
+ counts['D10P'] = int(hcc_count >= 10)
79
+
80
+ return counts
81
+
82
+ def get_diagnostic_categories(model_name: ModelName, hcc_set: set[str]) -> dict:
83
+ """Creates disease categories based on model version"""
84
+ if model_name == "CMS-HCC Model V28":
85
+ return {
86
+ 'CANCER_V28': has_any_hcc(['17', '18', '19', '20', '21', '22', '23'], hcc_set),
87
+ 'DIABETES_V28': has_any_hcc(['35', '36', '37', '38'], hcc_set),
88
+ 'CARD_RESP_FAIL_V28': has_any_hcc(['211', '212', '213'], hcc_set),
89
+ 'HF_V28': has_any_hcc(['221', '222', '223', '224', '225', '226'], hcc_set),
90
+ 'CHR_LUNG_V28': has_any_hcc(['276', '277', '278', '279', '280'], hcc_set),
91
+ 'KIDNEY_V28': has_any_hcc(['326', '327', '328', '329'], hcc_set),
92
+ 'SEPSIS_V28': int('2' in hcc_set),
93
+ 'gSubUseDisorder_V28': has_any_hcc(['135', '136', '137', '138', '139'], hcc_set),
94
+ 'gPsychiatric_V28': has_any_hcc(['151', '152', '153', '154', '155'], hcc_set),
95
+ 'NEURO_V28': has_any_hcc(['180', '181', '182', '190', '191', '192', '195', '196', '198', '199'], hcc_set),
96
+ 'ULCER_V28': has_any_hcc(['379', '380', '381', '382'], hcc_set)
97
+ }
98
+ elif model_name == "CMS-HCC Model V24":
99
+ return {
100
+ 'CANCER': has_any_hcc(['8', '9', '10', '11', '12'], hcc_set),
101
+ 'DIABETES': has_any_hcc(['17', '18', '19'], hcc_set),
102
+ 'CARD_RESP_FAIL': has_any_hcc(['82', '83', '84'], hcc_set),
103
+ 'CHF': int('85' in hcc_set),
104
+ 'gCopdCF': has_any_hcc(['110', '111', '112'], hcc_set),
105
+ 'RENAL_V24': has_any_hcc(['134', '135', '136', '137', '138'], hcc_set),
106
+ 'SEPSIS': int('2' in hcc_set),
107
+ 'gSubstanceUseDisorder_V24': has_any_hcc(['54', '55', '56'], hcc_set),
108
+ 'gPsychiatric_V24': has_any_hcc(['57', '58', '59', '60'], hcc_set),
109
+ 'PRESSURE_ULCER': has_any_hcc(['157', '158', '159'], hcc_set) # added in 2018-11-20
110
+ }
111
+ elif model_name == "CMS-HCC Model V22":
112
+ return {
113
+ 'CANCER': has_any_hcc(['8', '9', '10', '11', '12'], hcc_set),
114
+ 'DIABETES': has_any_hcc(['17', '18', '19'], hcc_set),
115
+ 'CARD_RESP_FAIL': has_any_hcc(['82', '83', '84'], hcc_set),
116
+ 'CHF': int('85' in hcc_set),
117
+ 'gCopdCF': has_any_hcc(['110', '111', '112'], hcc_set),
118
+ 'RENAL': has_any_hcc(['134', '135', '136', '137'], hcc_set),
119
+ 'SEPSIS': int('2' in hcc_set),
120
+ 'gSubstanceUseDisorder': has_any_hcc(['54', '55'], hcc_set),
121
+ 'gPsychiatric': has_any_hcc(['57', '58'], hcc_set),
122
+ 'PRESSURE_ULCER': has_any_hcc(['157', '158'], hcc_set) # added in 2012-10-19
123
+ }
124
+ elif model_name == "CMS-HCC ESRD Model V24":
125
+ return {
126
+ 'CANCER': has_any_hcc(['8', '9', '10', '11', '12'], hcc_set),
127
+ 'DIABETES': has_any_hcc(['17', '18', '19'], hcc_set),
128
+ 'CARD_RESP_FAIL': has_any_hcc(['82', '83', '84'], hcc_set),
129
+ 'CHF': int('85' in hcc_set),
130
+ 'gCopdCF': has_any_hcc(['110', '111', '112'], hcc_set),
131
+ 'RENAL_V24': has_any_hcc(['134', '135', '136', '137', '138'], hcc_set),
132
+ 'SEPSIS': int('2' in hcc_set),
133
+ 'PRESSURE_ULCER': has_any_hcc(['157', '158', '159', '160'], hcc_set), # added in 2018-11-20
134
+ 'gSubstanceUseDisorder_V24': has_any_hcc(['54', '55', '56'], hcc_set),
135
+ 'gPsychiatric_V24': has_any_hcc(['57', '58', '59', '60'], hcc_set)
136
+ }
137
+ elif model_name == "CMS-HCC ESRD Model V21":
138
+ return {
139
+ 'CANCER': has_any_hcc(['8', '9', '10', '11', '12'], hcc_set),
140
+ 'DIABETES': has_any_hcc(['17', '18', '19'], hcc_set),
141
+ 'IMMUNE': int('47' in hcc_set),
142
+ 'CARD_RESP_FAIL': has_any_hcc(['82', '83', '84'], hcc_set),
143
+ 'CHF': int('85' in hcc_set),
144
+ 'COPD': has_any_hcc(['110', '111'], hcc_set),
145
+ 'RENAL': has_any_hcc(['134', '135', '136', '137', '138', '139', '140', '141'], hcc_set),
146
+ 'COMPL': int('176' in hcc_set),
147
+ 'SEPSIS': int('2' in hcc_set),
148
+ 'PRESSURE_ULCER': has_any_hcc(['157', '158', '159', '160'], hcc_set)
149
+ }
150
+ elif model_name == "RxHCC Model V08":
151
+ # RxModel doesn't seem to have any diagnostic category interactions
152
+ return {}
153
+ return {}
154
+
155
+ def create_disease_interactions(model_name: ModelName,
156
+ diagnostic_cats: dict,
157
+ demographics: Optional[Demographics],
158
+ hcc_set: Optional[set[str]]) -> dict:
159
+ """Creates disease interaction variables based on model version.
160
+
161
+ Args:
162
+ model_name: The HCC model version being used
163
+ diagnostic_cats: Dictionary of diagnostic categories
164
+ demographics: Optional demographic information for age/sex/disability interactions
165
+ hcc_set: Optional set of HCCs for direct HCC checks
166
+
167
+ Returns:
168
+ Dictionary containing all disease interaction variables
169
+ """
170
+ interactions = {}
171
+
172
+ if model_name == "CMS-HCC Model V28":
173
+ # Base V28 disease interactions
174
+ interactions.update({
175
+ 'DIABETES_HF_V28': diagnostic_cats['DIABETES_V28'] * diagnostic_cats['HF_V28'],
176
+ 'HF_CHR_LUNG_V28': diagnostic_cats['HF_V28'] * diagnostic_cats['CHR_LUNG_V28'],
177
+ 'HF_KIDNEY_V28': diagnostic_cats['HF_V28'] * diagnostic_cats['KIDNEY_V28'],
178
+ 'CHR_LUNG_CARD_RESP_FAIL_V28': diagnostic_cats['CHR_LUNG_V28'] * diagnostic_cats['CARD_RESP_FAIL_V28'],
179
+ 'gSubUseDisorder_gPsych_V28': diagnostic_cats['gSubUseDisorder_V28'] * diagnostic_cats['gPsychiatric_V28'],
180
+ 'DISABLED_CANCER_V28': demographics.disabled * diagnostic_cats['CANCER_V28'],
181
+ 'DISABLED_NEURO_V28': demographics.disabled * diagnostic_cats['NEURO_V28'],
182
+ 'DISABLED_HF_V28': demographics.disabled * diagnostic_cats['HF_V28'],
183
+ 'DISABLED_CHR_LUNG_V28': demographics.disabled * diagnostic_cats['CHR_LUNG_V28'],
184
+ 'DISABLED_ULCER_V28': demographics.disabled * diagnostic_cats['ULCER_V28']
185
+ })
186
+
187
+ elif model_name == "CMS-HCC Model V24":
188
+ # Base V24/V22 disease interactions
189
+ interactions.update({
190
+ 'HCC47_gCancer': int('47' in hcc_set) * diagnostic_cats['CANCER'],
191
+ 'DIABETES_CHF': diagnostic_cats['DIABETES'] * diagnostic_cats['CHF'],
192
+ 'CHF_gCopdCF': diagnostic_cats['CHF'] * diagnostic_cats['gCopdCF'],
193
+ 'HCC85_gRenal_V24': diagnostic_cats['CHF'] * diagnostic_cats['RENAL_V24'],
194
+ 'gCopdCF_CARD_RESP_FAIL': diagnostic_cats['gCopdCF'] * diagnostic_cats['CARD_RESP_FAIL'],
195
+ 'HCC85_HCC96': int('85' in hcc_set) * int('96' in hcc_set),
196
+ 'gSubstanceAbuse_gPsych': diagnostic_cats['gSubstanceUseDisorder_V24'] * diagnostic_cats['gPsychiatric_V24'],
197
+ 'SEPSIS_PRESSURE_ULCER': diagnostic_cats['SEPSIS'] * diagnostic_cats['PRESSURE_ULCER'],
198
+ 'SEPSIS_ARTIF_OPENINGS': diagnostic_cats['SEPSIS'] * int('188' in hcc_set),
199
+ 'ART_OPENINGS_PRESS_ULCER': int('188' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
200
+ 'gCopdCF_ASP_SPEC_B_PNEUM': diagnostic_cats['gCopdCF'] * int('114' in hcc_set),
201
+ 'ASP_SPEC_B_PNEUM_PRES_ULC': int('114' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
202
+ 'SEPSIS_ASP_SPEC_BACT_PNEUM': diagnostic_cats['SEPSIS'] * int('114' in hcc_set),
203
+ 'SCHIZOPHRENIA_gCopdCF': int('57' in hcc_set) * diagnostic_cats['gCopdCF'],
204
+ 'SCHIZOPHRENIA_CHF': int('57' in hcc_set) * diagnostic_cats['CHF'],
205
+ 'SCHIZOPHRENIA_SEIZURES': int('57' in hcc_set) * int('79' in hcc_set),
206
+ 'DISABLED_HCC85': demographics.disabled * int('85' in hcc_set),
207
+ 'DISABLED_PRESSURE_ULCER': demographics.disabled * diagnostic_cats['PRESSURE_ULCER'],
208
+ 'DISABLED_HCC161': demographics.disabled * int('161' in hcc_set),
209
+ 'DISABLED_HCC39': demographics.disabled * int('39' in hcc_set),
210
+ 'DISABLED_HCC77': demographics.disabled * int('77' in hcc_set),
211
+ 'DISABLED_HCC6': demographics.disabled * int('6' in hcc_set)
212
+ })
213
+ elif model_name == "CMS-HCC Model V22":
214
+ # Base V24/V22 disease interactions
215
+ interactions.update({
216
+ 'HCC47_gCancer': int('47' in hcc_set) * diagnostic_cats['CANCER'],
217
+ 'HCC85_gDiabetesMellitus': int('85' in hcc_set) * diagnostic_cats['DIABETES'],
218
+ 'HCC85_gCopdCF': int('85' in hcc_set) * diagnostic_cats['gCopdCF'],
219
+ 'HCC85_gRenal': int('85' in hcc_set) * diagnostic_cats['RENAL'],
220
+ 'gRespDepandArre_gCopdCF': diagnostic_cats['CARD_RESP_FAIL'] * diagnostic_cats['gCopdCF'],
221
+ 'HCC85_HCC96': int('85' in hcc_set) * int('96' in hcc_set),
222
+ 'gSubstanceAbuse_gPsychiatric': diagnostic_cats['gSubstanceUseDisorder'] * diagnostic_cats['gPsychiatric'],
223
+ 'DIABETES_CHF': diagnostic_cats['DIABETES'] * diagnostic_cats['CHF'],
224
+ 'CHF_gCopdCF': diagnostic_cats['CHF'] * diagnostic_cats['gCopdCF'],
225
+ 'gCopdCF_CARD_RESP_FAIL': diagnostic_cats['gCopdCF'] * diagnostic_cats['CARD_RESP_FAIL'],
226
+ 'SEPSIS_PRESSURE_ULCER': diagnostic_cats['SEPSIS'] * diagnostic_cats['PRESSURE_ULCER'],
227
+ 'SEPSIS_ARTIF_OPENINGS': diagnostic_cats['SEPSIS'] * int('188' in hcc_set),
228
+ 'ART_OPENINGS_PRESSURE_ULCER': int('188' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
229
+ 'DIABETES_CHF': diagnostic_cats['DIABETES'] * diagnostic_cats['CHF'],
230
+ 'gCopdCF_ASP_SPEC_BACT_PNEUM': diagnostic_cats['gCopdCF'] * int('114' in hcc_set),
231
+ 'ASP_SPEC_BACT_PNEUM_PRES_ULC': int('114' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
232
+ 'SEPSIS_ASP_SPEC_BACT_PNEUM': diagnostic_cats['SEPSIS'] * int('114' in hcc_set),
233
+ 'SCHIZOPHRENIA_gCopdCF': int('57' in hcc_set) * diagnostic_cats['gCopdCF'],
234
+ 'SCHIZOPHRENIA_CHF': int('57' in hcc_set) * diagnostic_cats['CHF'],
235
+ 'SCHIZOPHRENIA_SEIZURES': int('57' in hcc_set) * int('79' in hcc_set),
236
+ 'DISABLED_HCC85': demographics.disabled * int('85' in hcc_set),
237
+ 'DISABLED_PRESSURE_ULCER': demographics.disabled * diagnostic_cats['PRESSURE_ULCER'],
238
+ 'DISABLED_HCC161': demographics.disabled * int('161' in hcc_set),
239
+ 'DISABLED_HCC39': demographics.disabled * int('39' in hcc_set),
240
+ 'DISABLED_HCC77': demographics.disabled * int('77' in hcc_set),
241
+ 'DISABLED_HCC6': demographics.disabled * int('6' in hcc_set)
242
+ })
243
+ elif model_name == "CMS-HCC ESRD Model V24":
244
+ # Base ESRD V24 disease interactions
245
+ interactions.update({
246
+ 'HCC47_gCancer': int('47' in hcc_set) * diagnostic_cats['CANCER'],
247
+ 'DIABETES_CHF': diagnostic_cats['DIABETES'] * diagnostic_cats['CHF'],
248
+ 'CHF_gCopdCF': diagnostic_cats['CHF'] * diagnostic_cats['gCopdCF'],
249
+ 'HCC85_gRenal_V24': int('85' in hcc_set) * diagnostic_cats['RENAL_V24'],
250
+ 'gCopdCF_CARD_RESP_FAIL': diagnostic_cats['gCopdCF'] * diagnostic_cats['CARD_RESP_FAIL'],
251
+ 'HCC85_HCC96': int('85' in hcc_set) * int('96' in hcc_set),
252
+ 'gSubUseDs_gPsych_V24': diagnostic_cats['gSubstanceUseDisorder_V24'] * diagnostic_cats['gPsychiatric_V24'],
253
+ 'NONAGED_gSubUseDs_gPsych': demographics.non_aged * (diagnostic_cats['gSubstanceUseDisorder_V24'] * diagnostic_cats['gPsychiatric_V24']),
254
+ 'NONAGED_HCC6': demographics.non_aged * int('6' in hcc_set),
255
+ 'NONAGED_HCC34': demographics.non_aged * int('34' in hcc_set),
256
+ 'NONAGED_HCC46': demographics.non_aged * int('46' in hcc_set),
257
+ 'NONAGED_HCC110': demographics.non_aged * int('110' in hcc_set),
258
+ 'NONAGED_HCC176': demographics.non_aged * int('176' in hcc_set),
259
+ 'SEPSIS_PRESSURE_ULCER_V24': diagnostic_cats['SEPSIS'] * diagnostic_cats['PRESSURE_ULCER'],
260
+ 'SEPSIS_ARTIF_OPENINGS': diagnostic_cats['SEPSIS'] * int('188' in hcc_set),
261
+ 'ART_OPENINGS_PRESS_ULCER_V24': int('188' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
262
+ 'gCopdCF_ASP_SPEC_B_PNEUM': diagnostic_cats['gCopdCF'] * int('114' in hcc_set),
263
+ 'ASP_SPEC_B_PNEUM_PRES_ULC_V24': int('114' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
264
+ 'SEPSIS_ASP_SPEC_BACT_PNEUM': diagnostic_cats['SEPSIS'] * int('114' in hcc_set),
265
+ 'SCHIZOPHRENIA_gCopdCF': int('57' in hcc_set) * diagnostic_cats['gCopdCF'],
266
+ 'SCHIZOPHRENIA_CHF': int('57' in hcc_set) * diagnostic_cats['CHF'],
267
+ 'SCHIZOPHRENIA_SEIZURES': int('57' in hcc_set) * int('79' in hcc_set),
268
+ 'NONAGED_HCC85': demographics.non_aged * int('85' in hcc_set),
269
+ 'NONAGED_PRESSURE_ULCER_V24': demographics.non_aged * diagnostic_cats['PRESSURE_ULCER'],
270
+ 'NONAGED_HCC161': demographics.non_aged * int('161' in hcc_set),
271
+ 'NONAGED_HCC39': demographics.non_aged * int('39' in hcc_set),
272
+ 'NONAGED_HCC77': demographics.non_aged * int('77' in hcc_set)
273
+ })
274
+
275
+ elif model_name == 'CMS-HCC ESRD Model V21':
276
+ # ESRD Community model interactions
277
+ interactions.update({
278
+ 'SEPSIS_CARD_RESP_FAIL': diagnostic_cats['SEPSIS'] * diagnostic_cats['CARD_RESP_FAIL'],
279
+ 'CANCER_IMMUNE': diagnostic_cats['CANCER'] * diagnostic_cats['IMMUNE'],
280
+ 'DIABETES_CHF': diagnostic_cats['DIABETES'] * diagnostic_cats['CHF'],
281
+ 'CHF_COPD': diagnostic_cats['CHF'] * diagnostic_cats['COPD'],
282
+ 'CHF_RENAL': diagnostic_cats['CHF'] * diagnostic_cats['RENAL'],
283
+ 'COPD_CARD_RESP_FAIL': diagnostic_cats['COPD'] * diagnostic_cats['CARD_RESP_FAIL'],
284
+ 'NONAGED_HCC6': demographics.non_aged * int('6' in hcc_set),
285
+ 'NONAGED_HCC34': demographics.non_aged * int('34' in hcc_set),
286
+ 'NONAGED_HCC46': demographics.non_aged * int('46' in hcc_set),
287
+ 'NONAGED_HCC54': demographics.non_aged * int('54' in hcc_set),
288
+ 'NONAGED_HCC55': demographics.non_aged * int('55' in hcc_set),
289
+ 'NONAGED_HCC110': demographics.non_aged * int('110' in hcc_set),
290
+ 'NONAGED_HCC176': demographics.non_aged * int('176' in hcc_set),
291
+ 'SEPSIS_PRESSURE_ULCER': diagnostic_cats['SEPSIS'] * diagnostic_cats['PRESSURE_ULCER'],
292
+ 'SEPSIS_ARTIF_OPENINGS': diagnostic_cats['SEPSIS'] * int('188' in hcc_set),
293
+ 'ART_OPENINGS_PRESSURE_ULCER': int('188' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
294
+ 'DIABETES_CHF': diagnostic_cats['DIABETES'] * diagnostic_cats['CHF'],
295
+ 'COPD_ASP_SPEC_BACT_PNEUM': diagnostic_cats['COPD'] * int('114' in hcc_set),
296
+ 'ASP_SPEC_BACT_PNEUM_PRES_ULC': int('114' in hcc_set) * diagnostic_cats['PRESSURE_ULCER'],
297
+ 'SEPSIS_ASP_SPEC_BACT_PNEUM': diagnostic_cats['SEPSIS'] * int('114' in hcc_set),
298
+ 'SCHIZOPHRENIA_COPD': int('57' in hcc_set) * diagnostic_cats['COPD'],
299
+ 'SCHIZOPHRENIA_CHF': int('57' in hcc_set) * diagnostic_cats['CHF'],
300
+ 'SCHIZOPHRENIA_SEIZURES': int('57' in hcc_set) * int('79' in hcc_set),
301
+ 'NONAGED_HCC85': demographics.non_aged * int('85' in hcc_set),
302
+ 'NONAGED_PRESSURE_ULCER': demographics.non_aged * diagnostic_cats['PRESSURE_ULCER'],
303
+ 'NONAGED_HCC161': demographics.non_aged * int('161' in hcc_set),
304
+ 'NONAGED_HCC39': demographics.non_aged * int('39' in hcc_set),
305
+ 'NONAGED_HCC77': demographics.non_aged * int('77' in hcc_set)
306
+ })
307
+
308
+ elif model_name == "RxHCC Model V08":
309
+ # RxHCC NonAged interactions
310
+ interactions.update({
311
+ 'NonAged_RXHCC1': demographics.non_aged * int('1' in hcc_set),
312
+ 'NonAged_RXHCC130': demographics.non_aged * int('130' in hcc_set),
313
+ 'NonAged_RXHCC131': demographics.non_aged * int('131' in hcc_set),
314
+ 'NonAged_RXHCC132': demographics.non_aged * int('132' in hcc_set),
315
+ 'NonAged_RXHCC133': demographics.non_aged * int('133' in hcc_set),
316
+ 'NonAged_RXHCC159': demographics.non_aged * int('159' in hcc_set),
317
+ 'NonAged_RXHCC163': demographics.non_aged * int('163' in hcc_set)
318
+ })
319
+
320
+ return interactions
321
+
322
+ def apply_interactions(demographics: Demographics,
323
+ hcc_set: set[str],
324
+ model_name: ModelName = "CMS-HCC Model V28") -> dict:
325
+ """
326
+ Calculate HCC interactions across CMS models. Handles CMS-HCC, ESRD, and RxHCC models.
327
+ """
328
+ # Start with demographic interactions
329
+ interactions = create_demographic_interactions(demographics)
330
+
331
+ # Add dual status interactions
332
+ interactions.update(create_dual_interactions(demographics))
333
+
334
+ # Get diagnostic categories for the model
335
+ diagnostic_cats = get_diagnostic_categories(model_name, hcc_set)
336
+
337
+ interactions.update(create_disease_interactions(model_name, diagnostic_cats, demographics, hcc_set))
338
+
339
+ # Add HCC counts
340
+ interactions.update(create_hcc_counts(hcc_set))
341
+
342
+ return interactions
@@ -0,0 +1,2 @@
1
+ # Empty file to make the package importable
2
+
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
3
+ Version: 0.0.5
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
@@ -45,7 +48,26 @@ sld = [{
45
48
  }, ...]
46
49
  ```
47
50
 
48
- For more details on the SLD format, see the `models.py` file.
51
+ Or, for direct risk score calculation from a list of diagnosis codes, you only need the model name, diagnosis codes, and basic demographic factors:
52
+
53
+ ```python
54
+ from hccinfhir.model_calculate import calculate_raf
55
+
56
+ diagnosis_codes = ['E119', 'I509'] # Diabetes without complications, Heart failure
57
+ age = 67
58
+ sex = 'F'
59
+ model_name = "CMS-HCC Model V24"
60
+
61
+ result = calculate_raf(
62
+ diagnosis_codes=diagnosis_codes,
63
+ model_name=model_name,
64
+ age=age,
65
+ sex=sex
66
+ )
67
+ ```
68
+
69
+
70
+ For more details on the SLD format, see the `datamodels.py` file.
49
71
 
50
72
  ## Core Components
51
73
 
@@ -71,23 +93,99 @@ filtered_sld = apply_filter(sld_list)
71
93
  ```
72
94
 
73
95
 
74
- ### 3. Logic Module (In Development)
96
+ ### 3. Logic Module
75
97
  Implements core HCC calculation logic:
76
98
  - Maps diagnosis codes to HCC categories
77
99
  - Applies hierarchical rules and interactions
78
100
  - Calculates final RAF scores
79
101
  - Integrates with standard CMS data files
80
102
 
81
- ## Usage
82
103
  ```python
83
- from hccinfhir import HCCInFHIR
104
+ from hccinfhir.model_calculate import calculate_raf
105
+
106
+ diagnosis_codes = ['E119', 'I509'] # Diabetes without complications, Heart failure
107
+ result = calculate_raf(
108
+ diagnosis_codes=diagnosis_codes,
109
+ model_name="CMS-HCC Model V24",
110
+ age=67,
111
+ sex='F'
112
+ )
113
+ ```
84
114
 
85
- hcc_processor = HCCInFHIR()
86
- sld_list = hcc_processor.extract_sld_list(eob_list)
87
- filtered_sld = hcc_processor.apply_filters(sld_list) # future
88
- raf_details = hcc_processor.calculate_raf(filtered_sld, demographic_data) # future
115
+ ### 4. HCCInFHIR Class
116
+ The main processor class that integrates extraction, filtering, and calculation components:
117
+
118
+ ```python
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 = Demographics(
132
+ age=67,
133
+ sex='F',
134
+ dual_elgbl_cd='00', # Not dual eligible
135
+ orec='0', # Old age
136
+ crec='0', # Current old age
137
+ new_enrollee=False,
138
+ snp=False
139
+ )
140
+
141
+ # Method 1: Process FHIR EOB resources
142
+ raf_result = hcc_processor.run(eob_list, demographics)
143
+
144
+ # Method 2: Process service level data
145
+ service_data = [{
146
+ "procedure_code": "99214",
147
+ "claim_diagnosis_codes": ["E11.9", "I10"],
148
+ "claim_type": "71",
149
+ "service_date": "2024-01-15"
150
+ }]
151
+ raf_result = hcc_processor.run_from_service_data(service_data, demographics)
152
+
153
+ # Method 3: Direct diagnosis processing
154
+ diagnosis_codes = ['E119', 'I509']
155
+ raf_result = hcc_processor.calculate_from_diagnosis(diagnosis_codes, demographics)
156
+
157
+ # RAF Result contains:
158
+ print(f"Risk Score: {raf_result['risk_score']}")
159
+ print(f"HCC List: {raf_result['hcc_list']}")
160
+ print(f"CC to Diagnosis Mapping: {raf_result['cc_to_dx']}")
161
+ print(f"Applied Coefficients: {raf_result['coefficients']}")
162
+ print(f"Applied Interactions: {raf_result['interactions']}")
89
163
  ```
90
164
 
165
+ The HCCInFHIR class provides three main processing methods:
166
+
167
+ 1. `run(eob_list, demographics)`: Process FHIR ExplanationOfBenefit resources
168
+ - Extracts service data from FHIR resources
169
+ - Applies filtering rules if enabled
170
+ - Calculates RAF scores using the specified model
171
+
172
+ 2. `run_from_service_data(service_data, demographics)`: Process standardized service data
173
+ - Accepts pre-formatted service level data
174
+ - Validates data structure using Pydantic models
175
+ - Applies filtering and calculates RAF scores
176
+
177
+ 3. `calculate_from_diagnosis(diagnosis_codes, demographics)`: Direct diagnosis processing
178
+ - Processes raw diagnosis codes without service context
179
+ - Useful for quick RAF calculations or validation
180
+ - Bypasses service-level filtering
181
+
182
+ Each method returns a RAFResult containing:
183
+ - Final risk score
184
+ - List of HCCs
185
+ - Mapping of condition categories to diagnosis codes
186
+ - Applied coefficients and interactions
187
+ - Processed service level data (when applicable)
188
+
91
189
  ## Testing
92
190
  ```bash
93
191
  $ python3 -m hatch shell
@@ -172,8 +270,48 @@ $ python3 -m pytest tests/*
172
270
  3. Add support for allowed_amount in 837 if available in different segments
173
271
  4. Consider adding more robust error handling in both implementations
174
272
 
273
+ ## Data Files
274
+
275
+ `ra_dx_to_cc_mapping_2025.csv`
276
+ ```sql
277
+ SELECT diagnosis_code, cc, model_name
278
+ FROM ra_dx_to_cc_mapping
279
+ WHERE year = 2025 and model_type = 'Initial';
280
+ ```
281
+
282
+ `ra_hierarchies_2025.csv`
283
+ ```sql
284
+ SELECT cc_parent,
285
+ cc_child,
286
+ model_domain,
287
+ model_version,
288
+ model_fullname
289
+ FROM ra_hierarchies
290
+ WHERE eff_last_date > '2025-01-01';
291
+ ```
292
+
293
+ `ra_coefficients_2025.csv`
294
+ ```sql
295
+ SELECT coefficient, value, model_domain, model_version
296
+ FROM ra_coefficients
297
+ WHERE eff_last_date > '2025-01-01';
298
+ ```
299
+
300
+ `ra_eligible_cpt_hcpcs_2025.csv`
301
+ ```sql
302
+ SELECT DISTINCT cpt_hcpcs_code
303
+ FROM mimi_ws_1.cmspayment.ra_eligible_cpt_hcpcs
304
+ WHERE is_included = 'yes' AND YEAR(mimi_src_file_date) = 2024;
305
+ ```
306
+
175
307
  ## Contributing
176
308
  Join us at [mimilabs](https://mimilabs.ai/signup). Reference data available in MIMILabs data lakehouse.
177
309
 
310
+ ## Publishing (only for those maintainers...)
311
+ ```bash
312
+ $ hatch build
313
+ $ hatch publish
314
+ ```
315
+
178
316
  ## License
179
317
  Apache License 2.0