hccinfhir 0.2.7__py3-none-any.whl → 0.2.9__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_dx_edits.csv +108 -0
- hccinfhir/data/ra_proposed_coefficients_2027.csv +3722 -0
- hccinfhir/datamodels.py +7 -3
- hccinfhir/defaults.py +4 -1
- hccinfhir/extractor_834.py +43 -5
- hccinfhir/model_calculate.py +8 -1
- hccinfhir/model_edits.py +108 -0
- hccinfhir/utils.py +79 -3
- {hccinfhir-0.2.7.dist-info → hccinfhir-0.2.9.dist-info}/METADATA +91 -1
- {hccinfhir-0.2.7.dist-info → hccinfhir-0.2.9.dist-info}/RECORD +12 -9
- {hccinfhir-0.2.7.dist-info → hccinfhir-0.2.9.dist-info}/WHEEL +0 -0
- {hccinfhir-0.2.7.dist-info → hccinfhir-0.2.9.dist-info}/licenses/LICENSE +0 -0
hccinfhir/datamodels.py
CHANGED
|
@@ -9,7 +9,10 @@ ModelName = Literal[
|
|
|
9
9
|
"CMS-HCC Model V28",
|
|
10
10
|
"CMS-HCC ESRD Model V21",
|
|
11
11
|
"CMS-HCC ESRD Model V24",
|
|
12
|
-
"RxHCC Model V08"
|
|
12
|
+
"RxHCC Model V08",
|
|
13
|
+
"RxHCC Model V08 PDP_AND_MAPD",
|
|
14
|
+
"RxHCC Model V08 PDP_ONLY",
|
|
15
|
+
"RxHCC Model V08 MAPD_ONLY"
|
|
13
16
|
]
|
|
14
17
|
|
|
15
18
|
# Filename types: allow bundled filenames (with autocomplete) OR any custom string path
|
|
@@ -50,7 +53,8 @@ IsChronicFilename = Union[
|
|
|
50
53
|
CoefficientsFilename = Union[
|
|
51
54
|
Literal[
|
|
52
55
|
"ra_coefficients_2025.csv",
|
|
53
|
-
"ra_coefficients_2026.csv"
|
|
56
|
+
"ra_coefficients_2026.csv",
|
|
57
|
+
"ra_proposed_coefficients_2027.csv"
|
|
54
58
|
],
|
|
55
59
|
str
|
|
56
60
|
]
|
|
@@ -276,7 +280,7 @@ class EnrollmentData(BaseModel):
|
|
|
276
280
|
is_partial_benefit_dual: Partial Benefit Dual (uses CPA_/CPD_ prefix)
|
|
277
281
|
medicare_status_code: QMB, SLMB, QI, QDWI, etc.
|
|
278
282
|
medi_cal_aid_code: California Medi-Cal aid code
|
|
279
|
-
medi_cal_eligibility_status: Medi-Cal eligibility status
|
|
283
|
+
medi_cal_eligibility_status: Medi-Cal eligibility status (derived: "Active"/"Terminated"/None)
|
|
280
284
|
|
|
281
285
|
# CA DHCS / FAME Specific
|
|
282
286
|
fame_county_id: FAME county ID (REF*ZX or N4*CY)
|
hccinfhir/defaults.py
CHANGED
|
@@ -16,8 +16,10 @@ from hccinfhir.utils import (
|
|
|
16
16
|
load_is_chronic,
|
|
17
17
|
load_coefficients,
|
|
18
18
|
load_proc_filtering,
|
|
19
|
-
load_labels
|
|
19
|
+
load_labels,
|
|
20
|
+
load_edits
|
|
20
21
|
)
|
|
22
|
+
from hccinfhir.model_edits import EditRule
|
|
21
23
|
|
|
22
24
|
# Load all default data files once at module import time
|
|
23
25
|
# These are used by:
|
|
@@ -31,3 +33,4 @@ is_chronic_default: Dict[Tuple[str, ModelName], bool] = load_is_chronic('hcc_is_
|
|
|
31
33
|
coefficients_default: Dict[Tuple[str, ModelName], float] = load_coefficients('ra_coefficients_2026.csv')
|
|
32
34
|
proc_filtering_default: Set[str] = load_proc_filtering('ra_eligible_cpt_hcpcs_2026.csv')
|
|
33
35
|
labels_default: Dict[Tuple[str, ModelName], str] = load_labels('ra_labels_2026.csv')
|
|
36
|
+
edits_default: Dict[Tuple[str, ModelName], EditRule] = load_edits('ra_dx_edits.csv')
|
hccinfhir/extractor_834.py
CHANGED
|
@@ -216,6 +216,36 @@ def is_new_enrollee(coverage_start_date: Optional[str], reference_date: Optional
|
|
|
216
216
|
return False
|
|
217
217
|
|
|
218
218
|
|
|
219
|
+
def derive_medi_cal_eligibility_status(coverage_end_date: Optional[str], report_date: Optional[str]) -> Optional[str]:
|
|
220
|
+
"""Derive Medi-Cal eligibility status from coverage end date and report date.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
coverage_end_date: Coverage end date in YYYY-MM-DD format
|
|
224
|
+
report_date: Report date in YYYY-MM-DD format
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
"Active" if coverage extends through or beyond report month
|
|
228
|
+
"Terminated" if coverage ended before report month
|
|
229
|
+
None if no coverage_end_date
|
|
230
|
+
"""
|
|
231
|
+
if not coverage_end_date:
|
|
232
|
+
return None
|
|
233
|
+
try:
|
|
234
|
+
end_date = datetime.strptime(coverage_end_date, "%Y-%m-%d").date()
|
|
235
|
+
if report_date:
|
|
236
|
+
ref_date = datetime.strptime(report_date, "%Y-%m-%d").date()
|
|
237
|
+
else:
|
|
238
|
+
ref_date = date.today()
|
|
239
|
+
# Get first day of report month for comparison
|
|
240
|
+
first_of_report_month = ref_date.replace(day=1)
|
|
241
|
+
if end_date < first_of_report_month:
|
|
242
|
+
return "Terminated"
|
|
243
|
+
else:
|
|
244
|
+
return "Active"
|
|
245
|
+
except (ValueError, AttributeError):
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
219
249
|
def contains_any_keyword(text: str, keywords: set) -> bool:
|
|
220
250
|
"""Check if text contains any of the keywords"""
|
|
221
251
|
text_upper = text.upper()
|
|
@@ -326,10 +356,15 @@ def parse_ref_dx(value: str, member: MemberContext) -> None:
|
|
|
326
356
|
|
|
327
357
|
|
|
328
358
|
def parse_ref_17(value: str, member: MemberContext) -> None:
|
|
329
|
-
"""REF*17: YYYYMM
|
|
359
|
+
"""REF*17: YYYYMM;YYYYMMDD;YYYYMM (redetermination date; death date; reporting month)"""
|
|
360
|
+
# Position 0: FAME redetermination date (YYYYMM)
|
|
330
361
|
yyyymm = get_composite_part(value, 0)
|
|
331
362
|
if yyyymm and len(yyyymm) >= 6:
|
|
332
363
|
member.fame_redetermination_date = f"{yyyymm[:4]}-{yyyymm[4:6]}-01"
|
|
364
|
+
# Position 1: FAME death date (YYYYMMDD)
|
|
365
|
+
death_date_str = get_composite_part(value, 1)
|
|
366
|
+
if death_date_str and len(death_date_str) == 8:
|
|
367
|
+
member.fame_death_date = parse_date(death_date_str)
|
|
333
368
|
|
|
334
369
|
|
|
335
370
|
# ============================================================================
|
|
@@ -503,6 +538,7 @@ def _finalize_member(member: MemberContext, source: str, report_date: str) -> En
|
|
|
503
538
|
dual_code = determine_dual_status(member)
|
|
504
539
|
is_fbd, is_pbd = classify_dual_benefit_level(dual_code)
|
|
505
540
|
new_enrollee = is_new_enrollee(member.coverage_start_date)
|
|
541
|
+
medi_cal_elig_status = derive_medi_cal_eligibility_status(member.coverage_end_date, report_date)
|
|
506
542
|
|
|
507
543
|
hcp_history = [
|
|
508
544
|
HCPCoveragePeriod(
|
|
@@ -530,6 +566,7 @@ def _finalize_member(member: MemberContext, source: str, report_date: str) -> En
|
|
|
530
566
|
dual_elgbl_cd=dual_code, is_full_benefit_dual=is_fbd, is_partial_benefit_dual=is_pbd,
|
|
531
567
|
medicare_status_code=member.medicare_status_code,
|
|
532
568
|
medi_cal_aid_code=member.medi_cal_aid_code,
|
|
569
|
+
medi_cal_eligibility_status=medi_cal_elig_status,
|
|
533
570
|
fame_county_id=member.fame_county_id, case_number=member.case_number,
|
|
534
571
|
fame_card_issue_date=member.fame_card_issue_date,
|
|
535
572
|
fame_redetermination_date=member.fame_redetermination_date,
|
|
@@ -583,6 +620,11 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
|
|
|
583
620
|
member.maintenance_type = get_segment_value(segment, 3)
|
|
584
621
|
member.maintenance_reason_code = get_segment_value(segment, 4)
|
|
585
622
|
member.benefit_status_code = get_segment_value(segment, 5)
|
|
623
|
+
# INS12 is Member Death Date when INS11 = D8
|
|
624
|
+
if get_segment_value(segment, 11) == 'D8':
|
|
625
|
+
death_str = get_segment_value(segment, 12)
|
|
626
|
+
if death_str:
|
|
627
|
+
member.death_date = parse_date(death_str)
|
|
586
628
|
|
|
587
629
|
# REF - Reference identifiers
|
|
588
630
|
elif seg_id == 'REF' and len(segment) >= 3:
|
|
@@ -641,9 +683,6 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
|
|
|
641
683
|
if sex in X12_SEX_CODE_MAPPING:
|
|
642
684
|
member.sex = X12_SEX_CODE_MAPPING[sex]
|
|
643
685
|
member.race = parse_race_code(get_segment_value(segment, 5))
|
|
644
|
-
death_str = get_segment_value(segment, 6)
|
|
645
|
-
if death_str and len(death_str) >= 8:
|
|
646
|
-
member.death_date = parse_date(death_str[:8])
|
|
647
686
|
|
|
648
687
|
# DTP - Dates
|
|
649
688
|
elif seg_id == 'DTP' and len(segment) >= 4:
|
|
@@ -675,7 +714,6 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
|
|
|
675
714
|
member.has_medicare = True
|
|
676
715
|
elif qualifier == '435':
|
|
677
716
|
member.death_date = parsed
|
|
678
|
-
member.fame_death_date = parsed
|
|
679
717
|
|
|
680
718
|
# HD - Health coverage
|
|
681
719
|
elif seg_id == 'HD' and len(segment) >= 4:
|
hccinfhir/model_calculate.py
CHANGED
|
@@ -2,10 +2,11 @@ from typing import List, Union, Dict, Tuple, Set, Optional
|
|
|
2
2
|
from hccinfhir.datamodels import ModelName, RAFResult, PrefixOverride, HCCDetail
|
|
3
3
|
from hccinfhir.model_demographics import categorize_demographics
|
|
4
4
|
from hccinfhir.model_dx_to_cc import apply_mapping
|
|
5
|
+
from hccinfhir.model_edits import apply_edits, EditRule
|
|
5
6
|
from hccinfhir.model_hierarchies import apply_hierarchies
|
|
6
7
|
from hccinfhir.model_coefficients import apply_coefficients
|
|
7
8
|
from hccinfhir.model_interactions import apply_interactions
|
|
8
|
-
from hccinfhir.defaults import dx_to_cc_default, hierarchies_default, is_chronic_default, coefficients_default, labels_default
|
|
9
|
+
from hccinfhir.defaults import dx_to_cc_default, hierarchies_default, is_chronic_default, coefficients_default, labels_default, edits_default
|
|
9
10
|
|
|
10
11
|
def calculate_raf(diagnosis_codes: List[str],
|
|
11
12
|
model_name: ModelName = "CMS-HCC Model V28",
|
|
@@ -24,6 +25,7 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
24
25
|
hierarchies_mapping: Dict[Tuple[str, ModelName], Set[str]] = hierarchies_default,
|
|
25
26
|
coefficients_mapping: Dict[Tuple[str, ModelName], float] = coefficients_default,
|
|
26
27
|
labels_mapping: Dict[Tuple[str, ModelName], str] = labels_default,
|
|
28
|
+
edits_mapping: Dict[Tuple[str, ModelName], EditRule] = edits_default,
|
|
27
29
|
prefix_override: Optional[PrefixOverride] = None,
|
|
28
30
|
maci: float = 0.0,
|
|
29
31
|
norm_factor: float = 1.0,
|
|
@@ -49,6 +51,7 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
49
51
|
hierarchies_mapping: Mapping of parent HCCs to child HCCs for hierarchical rules; defaults to packaged 2026 mappings.
|
|
50
52
|
coefficients_mapping: Mapping of coefficient names to values; defaults to packaged 2026 mappings.
|
|
51
53
|
labels_mapping: Mapping of (cc, model_name) to human-readable HCC labels; defaults to packaged 2026 mappings.
|
|
54
|
+
edits_mapping: Mapping of (icd10, model_name) to EditRule for age/sex-based CC modifications; defaults to packaged edits.
|
|
52
55
|
prefix_override: Optional prefix to override auto-detected demographic prefix.
|
|
53
56
|
Use when demographic categorization from orec/crec is incorrect.
|
|
54
57
|
Common values: 'DI_' (ESRD Dialysis), 'DNE_' (ESRD Dialysis New Enrollee),
|
|
@@ -92,6 +95,10 @@ def calculate_raf(diagnosis_codes: List[str],
|
|
|
92
95
|
cc_to_dx = apply_mapping(diagnosis_codes,
|
|
93
96
|
model_name,
|
|
94
97
|
dx_to_cc_mapping=dx_to_cc_mapping)
|
|
98
|
+
|
|
99
|
+
# Apply age/sex edits (CMS hardcoded rules from V28I0ED and similar)
|
|
100
|
+
cc_to_dx = apply_edits(cc_to_dx, age, sex, model_name, edits_mapping)
|
|
101
|
+
|
|
95
102
|
hcc_set = set(cc_to_dx.keys())
|
|
96
103
|
hcc_set = apply_hierarchies(hcc_set, model_name, hierarchies_mapping)
|
|
97
104
|
interactions = apply_interactions(demographics, hcc_set, model_name)
|
hccinfhir/model_edits.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import Dict, Set, Tuple, Union, Optional, NamedTuple
|
|
2
|
+
from hccinfhir.datamodels import ModelName
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EditRule(NamedTuple):
|
|
6
|
+
"""Represents a single edit rule from ra_dx_edits.csv"""
|
|
7
|
+
edit_type: str # "sex" or "age"
|
|
8
|
+
sex: Optional[str] # For sex edits: "1" (male) or "2" (female)
|
|
9
|
+
age_min: Optional[int] # For age edits: minimum age (inclusive), None if not set
|
|
10
|
+
age_max: Optional[int] # For age edits: maximum age (inclusive), None if not set
|
|
11
|
+
action: str # "invalid" or "override"
|
|
12
|
+
cc_override: Optional[str] # CC to assign when action is "override"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_edits(
|
|
16
|
+
cc_to_dx: Dict[str, Set[str]],
|
|
17
|
+
age: Union[int, float],
|
|
18
|
+
sex: str,
|
|
19
|
+
model_name: ModelName,
|
|
20
|
+
edits_mapping: Dict[Tuple[str, ModelName], EditRule]
|
|
21
|
+
) -> Dict[str, Set[str]]:
|
|
22
|
+
"""
|
|
23
|
+
Apply age/sex edits to CC mappings based on CMS edit rules.
|
|
24
|
+
|
|
25
|
+
This implements the hardcoded edits from CMS SAS macro V28I0ED (and similar).
|
|
26
|
+
Edits are applied AFTER initial ICD->CC mapping but BEFORE hierarchies.
|
|
27
|
+
|
|
28
|
+
Edit types:
|
|
29
|
+
- invalid: Remove the diagnosis (don't assign any CC)
|
|
30
|
+
- override: Assign a different CC than the default mapping
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
cc_to_dx: Dictionary mapping CC codes to sets of diagnosis codes
|
|
34
|
+
age: Patient's age
|
|
35
|
+
sex: Patient's sex ('M', 'F', '1', or '2')
|
|
36
|
+
model_name: HCC model name
|
|
37
|
+
edits_mapping: Dictionary mapping (icd10, model_name) to EditRule
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Modified cc_to_dx dictionary with edits applied
|
|
41
|
+
"""
|
|
42
|
+
# Normalize sex to CMS format ("1" = male, "2" = female)
|
|
43
|
+
sex_normalized = sex
|
|
44
|
+
if sex == 'M':
|
|
45
|
+
sex_normalized = '1'
|
|
46
|
+
elif sex == 'F':
|
|
47
|
+
sex_normalized = '2'
|
|
48
|
+
|
|
49
|
+
# Collect all diagnoses across all CCs for edit checking
|
|
50
|
+
all_dx_to_cc: Dict[str, str] = {}
|
|
51
|
+
for cc, dx_set in cc_to_dx.items():
|
|
52
|
+
for dx in dx_set:
|
|
53
|
+
all_dx_to_cc[dx] = cc
|
|
54
|
+
|
|
55
|
+
# Track modifications
|
|
56
|
+
dx_to_remove: Dict[str, str] = {} # dx -> original cc (to remove)
|
|
57
|
+
dx_to_override: Dict[str, Tuple[str, str]] = {} # dx -> (original cc, new cc)
|
|
58
|
+
|
|
59
|
+
for dx, original_cc in all_dx_to_cc.items():
|
|
60
|
+
edit_key = (dx, model_name)
|
|
61
|
+
if edit_key not in edits_mapping:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
rule = edits_mapping[edit_key]
|
|
65
|
+
should_apply = False
|
|
66
|
+
|
|
67
|
+
if rule.edit_type == "sex":
|
|
68
|
+
# Sex-based edit: apply if patient sex matches the rule's sex
|
|
69
|
+
if rule.sex == sex_normalized:
|
|
70
|
+
should_apply = True
|
|
71
|
+
|
|
72
|
+
elif rule.edit_type == "age":
|
|
73
|
+
# Age-based edit: check age bounds
|
|
74
|
+
# age_max set means "if age <= age_max" (e.g., age < 50 means age_max=49)
|
|
75
|
+
# age_min set means "if age >= age_min" (e.g., age >= 2 means age_min=2)
|
|
76
|
+
if rule.age_max is not None and age <= rule.age_max:
|
|
77
|
+
should_apply = True
|
|
78
|
+
elif rule.age_min is not None and age >= rule.age_min:
|
|
79
|
+
should_apply = True
|
|
80
|
+
|
|
81
|
+
if should_apply:
|
|
82
|
+
if rule.action == "invalid":
|
|
83
|
+
dx_to_remove[dx] = original_cc
|
|
84
|
+
elif rule.action == "override" and rule.cc_override:
|
|
85
|
+
dx_to_override[dx] = (original_cc, rule.cc_override)
|
|
86
|
+
|
|
87
|
+
# Apply removals
|
|
88
|
+
for dx, original_cc in dx_to_remove.items():
|
|
89
|
+
if original_cc in cc_to_dx and dx in cc_to_dx[original_cc]:
|
|
90
|
+
cc_to_dx[original_cc].discard(dx)
|
|
91
|
+
# Remove CC entirely if no diagnoses left
|
|
92
|
+
if not cc_to_dx[original_cc]:
|
|
93
|
+
del cc_to_dx[original_cc]
|
|
94
|
+
|
|
95
|
+
# Apply overrides
|
|
96
|
+
for dx, (original_cc, new_cc) in dx_to_override.items():
|
|
97
|
+
# Remove from original CC
|
|
98
|
+
if original_cc in cc_to_dx and dx in cc_to_dx[original_cc]:
|
|
99
|
+
cc_to_dx[original_cc].discard(dx)
|
|
100
|
+
if not cc_to_dx[original_cc]:
|
|
101
|
+
del cc_to_dx[original_cc]
|
|
102
|
+
|
|
103
|
+
# Add to new CC
|
|
104
|
+
if new_cc not in cc_to_dx:
|
|
105
|
+
cc_to_dx[new_cc] = set()
|
|
106
|
+
cc_to_dx[new_cc].add(dx)
|
|
107
|
+
|
|
108
|
+
return cc_to_dx
|
hccinfhir/utils.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import Set, Dict, Tuple, Optional
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
import importlib.resources
|
|
4
4
|
from hccinfhir.datamodels import ModelName, ProcFilteringFilename, DxCCMappingFilename
|
|
5
|
+
from hccinfhir.model_edits import EditRule
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def resolve_data_file(file_path: str) -> str:
|
|
@@ -233,11 +234,24 @@ def load_coefficients(file_path: str) -> Dict[Tuple[str, ModelName], float]:
|
|
|
233
234
|
parts = line.strip().split(',')
|
|
234
235
|
coefficient, value, model_domain, model_version = parts[0], parts[1], parts[2], parts[3]
|
|
235
236
|
|
|
237
|
+
# Extract version number and optional variant suffix
|
|
238
|
+
# Handles: "C28", "R08", "D24" (old) and "R08_PDP_AND_MAPD" (new)
|
|
239
|
+
if '_' in model_version:
|
|
240
|
+
# New format with variant: "R08_PDP_AND_MAPD" -> version="08", variant="PDP_AND_MAPD"
|
|
241
|
+
version_num = model_version[1:3]
|
|
242
|
+
variant = model_version[4:] # Everything after "R08_"
|
|
243
|
+
else:
|
|
244
|
+
# Old format: "C28", "R08", "D24" -> version="28", "08", "24"
|
|
245
|
+
version_num = model_version[-2:]
|
|
246
|
+
variant = None
|
|
247
|
+
|
|
236
248
|
# Construct model name based on domain
|
|
237
249
|
if model_domain == 'ESRD':
|
|
238
|
-
model_name = f"CMS-HCC {model_domain} Model V{
|
|
250
|
+
model_name = f"CMS-HCC {model_domain} Model V{version_num}"
|
|
239
251
|
else:
|
|
240
|
-
model_name = f"{model_domain} Model V{
|
|
252
|
+
model_name = f"{model_domain} Model V{version_num}"
|
|
253
|
+
if variant:
|
|
254
|
+
model_name = f"{model_name} {variant}"
|
|
241
255
|
|
|
242
256
|
key = (coefficient.lower(), model_name)
|
|
243
257
|
coefficients[key] = float(value)
|
|
@@ -351,4 +365,66 @@ def load_labels(file_path: str) -> Dict[Tuple[str, ModelName], str]:
|
|
|
351
365
|
except (ValueError, IndexError):
|
|
352
366
|
continue # Skip malformed lines
|
|
353
367
|
|
|
354
|
-
return labels
|
|
368
|
+
return labels
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def load_edits(file_path: str = "ra_dx_edits.csv") -> Dict[Tuple[str, ModelName], EditRule]:
|
|
372
|
+
"""
|
|
373
|
+
Load age/sex edit rules from a CSV file.
|
|
374
|
+
Expected format: icd10,edit_type,sex,age_min,age_max,action,cc_override,model,description
|
|
375
|
+
|
|
376
|
+
These edits implement the hardcoded rules from CMS SAS macros (e.g., V28I0ED3)
|
|
377
|
+
that modify CC assignments based on patient age or sex.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
file_path: Filename or path to the CSV file
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Dictionary mapping (icd10, model_name) to EditRule
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
FileNotFoundError: If file cannot be found
|
|
387
|
+
RuntimeError: If file cannot be loaded or parsed
|
|
388
|
+
"""
|
|
389
|
+
edits: Dict[Tuple[str, ModelName], EditRule] = {}
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
resolved_path = resolve_data_file(file_path)
|
|
393
|
+
with open(resolved_path, "r", encoding="utf-8") as file:
|
|
394
|
+
content = file.read()
|
|
395
|
+
except FileNotFoundError as e:
|
|
396
|
+
raise FileNotFoundError(f"Could not load edits: {e}")
|
|
397
|
+
except Exception as e:
|
|
398
|
+
raise RuntimeError(f"Error loading edits file '{file_path}': {e}")
|
|
399
|
+
|
|
400
|
+
for line in content.splitlines()[1:]: # Skip header
|
|
401
|
+
try:
|
|
402
|
+
parts = line.strip().split(',')
|
|
403
|
+
if len(parts) < 8:
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
icd10 = parts[0].strip()
|
|
407
|
+
edit_type = parts[1].strip()
|
|
408
|
+
sex = parts[2].strip() if parts[2].strip() else None
|
|
409
|
+
age_min = int(parts[3]) if parts[3].strip() else None
|
|
410
|
+
age_max = int(parts[4]) if parts[4].strip() else None
|
|
411
|
+
action = parts[5].strip()
|
|
412
|
+
cc_override = parts[6].strip() if parts[6].strip() else None
|
|
413
|
+
model_name = parts[7].strip()
|
|
414
|
+
|
|
415
|
+
rule = EditRule(
|
|
416
|
+
edit_type=edit_type,
|
|
417
|
+
sex=sex,
|
|
418
|
+
age_min=age_min,
|
|
419
|
+
age_max=age_max,
|
|
420
|
+
action=action,
|
|
421
|
+
cc_override=cc_override
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
key = (icd10, model_name)
|
|
425
|
+
edits[key] = rule
|
|
426
|
+
|
|
427
|
+
except (ValueError, IndexError):
|
|
428
|
+
continue # Skip malformed lines
|
|
429
|
+
|
|
430
|
+
return edits
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hccinfhir
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
|
@@ -408,6 +408,96 @@ if result.interactions:
|
|
|
408
408
|
| `"CMS-HCC ESRD Model V21"` | 2024-2025 | ESRD populations | ✅ |
|
|
409
409
|
| `"CMS-HCC ESRD Model V24"` | 2025-2026 | ESRD populations | ✅ |
|
|
410
410
|
| `"RxHCC Model V08"` | 2024-2026 | Part D prescription drug | ✅ |
|
|
411
|
+
| `"RxHCC Model V08 PDP_AND_MAPD"` | 2027 (proposed) | Part D - Combined reference estimate | ✅ |
|
|
412
|
+
| `"RxHCC Model V08 PDP_ONLY"` | 2027 (proposed) | Part D - Standalone PDP plans | ✅ |
|
|
413
|
+
| `"RxHCC Model V08 MAPD_ONLY"` | 2027 (proposed) | Part D - MA-PD plans | ✅ |
|
|
414
|
+
|
|
415
|
+
### Using Proposed 2027 Coefficients
|
|
416
|
+
|
|
417
|
+
The library includes proposed CMS coefficients for 2027 payment year (`ra_proposed_coefficients_2027.csv`). These are useful for:
|
|
418
|
+
- **Prospective planning**: Estimate future RAF scores before final rates are published
|
|
419
|
+
- **Impact analysis**: Compare current vs. proposed coefficient changes
|
|
420
|
+
- **Research**: Model different payment scenarios
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from hccinfhir import HCCInFHIR, Demographics
|
|
424
|
+
|
|
425
|
+
# CMS-HCC with proposed 2027 coefficients
|
|
426
|
+
processor_2027 = HCCInFHIR(
|
|
427
|
+
model_name="CMS-HCC Model V28",
|
|
428
|
+
coefficients_filename="ra_proposed_coefficients_2027.csv"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
demographics = Demographics(age=70, sex="M", dual_elgbl_cd="00")
|
|
432
|
+
diagnosis_codes = ["E11.9", "I10", "N18.3"]
|
|
433
|
+
|
|
434
|
+
result = processor_2027.calculate_from_diagnosis(diagnosis_codes, demographics)
|
|
435
|
+
print(f"2027 Proposed RAF Score: {result.risk_score:.3f}")
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**RxHCC Plan-Specific Variants**
|
|
439
|
+
|
|
440
|
+
CMS is introducing plan-specific RxHCC coefficients for 2027, separating standalone PDP and MA-PD plans. The combined PDP_AND_MAPD estimate is also provided as a traditional reference:
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
# PDP and MA-PD combined (traditional reference estimate)
|
|
444
|
+
processor_pdp_mapd = HCCInFHIR(
|
|
445
|
+
model_name="RxHCC Model V08 PDP_AND_MAPD",
|
|
446
|
+
coefficients_filename="ra_proposed_coefficients_2027.csv"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# PDP-only plans (standalone Part D)
|
|
450
|
+
processor_pdp = HCCInFHIR(
|
|
451
|
+
model_name="RxHCC Model V08 PDP_ONLY",
|
|
452
|
+
coefficients_filename="ra_proposed_coefficients_2027.csv"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# MA-PD only plans (Medicare Advantage with Part D)
|
|
456
|
+
processor_mapd = HCCInFHIR(
|
|
457
|
+
model_name="RxHCC Model V08 MAPD_ONLY",
|
|
458
|
+
coefficients_filename="ra_proposed_coefficients_2027.csv"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Compare scores across plan types
|
|
462
|
+
demographics = Demographics(age=70, sex="F", low_income=True)
|
|
463
|
+
diagnosis_codes = ["E11.9"]
|
|
464
|
+
|
|
465
|
+
for name, proc in [("PDP_AND_MAPD", processor_pdp_mapd),
|
|
466
|
+
("PDP_ONLY", processor_pdp),
|
|
467
|
+
("MAPD_ONLY", processor_mapd)]:
|
|
468
|
+
result = proc.calculate_from_diagnosis(diagnosis_codes, demographics)
|
|
469
|
+
print(f"{name}: {result.risk_score:.3f}")
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Comparing 2026 vs 2027 Coefficients**
|
|
473
|
+
|
|
474
|
+
```python
|
|
475
|
+
from hccinfhir import HCCInFHIR, Demographics
|
|
476
|
+
|
|
477
|
+
# Current 2026 coefficients
|
|
478
|
+
processor_2026 = HCCInFHIR(
|
|
479
|
+
model_name="CMS-HCC Model V28",
|
|
480
|
+
coefficients_filename="ra_coefficients_2026.csv"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Proposed 2027 coefficients
|
|
484
|
+
processor_2027 = HCCInFHIR(
|
|
485
|
+
model_name="CMS-HCC Model V28",
|
|
486
|
+
coefficients_filename="ra_proposed_coefficients_2027.csv"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
demographics = Demographics(age=70, sex="M", dual_elgbl_cd="00")
|
|
490
|
+
diagnosis_codes = ["E11.9", "I10", "N18.3"]
|
|
491
|
+
|
|
492
|
+
result_2026 = processor_2026.calculate_from_diagnosis(diagnosis_codes, demographics)
|
|
493
|
+
result_2027 = processor_2027.calculate_from_diagnosis(diagnosis_codes, demographics)
|
|
494
|
+
|
|
495
|
+
print(f"2026 RAF Score: {result_2026.risk_score:.3f}")
|
|
496
|
+
print(f"2027 RAF Score: {result_2027.risk_score:.3f}")
|
|
497
|
+
print(f"Change: {((result_2027.risk_score / result_2026.risk_score) - 1) * 100:.1f}%")
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
> **Note**: Proposed coefficients are subject to change. Always verify against final CMS publications for payment calculations.
|
|
411
501
|
|
|
412
502
|
### Custom Data Files
|
|
413
503
|
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
hccinfhir/__init__.py,sha256=3aFYtjTklZJg3wIlnMJNgfDBaDCfKXVlYsacdsZ9L4I,1113
|
|
2
2
|
hccinfhir/constants.py,sha256=C4Vyjtzgyd4Jm2I2X6cTYQZLe-jAMC8boUcy-7OXQDQ,8473
|
|
3
|
-
hccinfhir/datamodels.py,sha256=
|
|
4
|
-
hccinfhir/defaults.py,sha256=
|
|
3
|
+
hccinfhir/datamodels.py,sha256=c4DXyI74g76QL3I1l10bWA7_Y6Vh3S4nUrSCk7t8YgI,17764
|
|
4
|
+
hccinfhir/defaults.py,sha256=CTZAa5LI4YqKA50_OGzjEFBqadSoTQPK7UPpXBsGmdU,1538
|
|
5
5
|
hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
|
|
6
|
-
hccinfhir/extractor_834.py,sha256=
|
|
6
|
+
hccinfhir/extractor_834.py,sha256=rIzMjZUQ9dUGqvg7BnCqRlYabP8q5VpWHDYzW7ycuzI,31109
|
|
7
7
|
hccinfhir/extractor_837.py,sha256=fGsvBTWIj9dsHLGGR67AdlYDSsFi5qnSVlTgwkL1f-E,15334
|
|
8
8
|
hccinfhir/extractor_fhir.py,sha256=wUN3vTm1oTZ-KvfcDebnpQMxAC-7YlRKv12Wrv3p85A,8490
|
|
9
9
|
hccinfhir/filter.py,sha256=j_yD2g6RBXVUV9trKkWzsQ35x3fRvfKUPvEXKUefI64,2007
|
|
10
10
|
hccinfhir/hccinfhir.py,sha256=NydnH3WBvuyskn76hY70LpUS6XuIEoax_kip1mgfpHw,11225
|
|
11
|
-
hccinfhir/model_calculate.py,sha256=
|
|
11
|
+
hccinfhir/model_calculate.py,sha256=HAM_e-4f4eW4OGyrtVfoOaYnwwj3eurnWLQfSUtC0cg,8867
|
|
12
12
|
hccinfhir/model_coefficients.py,sha256=PGZDAFRyz7asT0epl4xTatNCsuzYaISdbXEHU2wQ27U,5504
|
|
13
13
|
hccinfhir/model_demographics.py,sha256=nImKtJCq1HkR9w2GU8aikybJFgow71CPufBRV8Jn7fM,8932
|
|
14
14
|
hccinfhir/model_dx_to_cc.py,sha256=Yjc6xKI-jMXsbOzS_chc4NI15Bwagb7BwZZ8cKQaTbk,1540
|
|
15
|
+
hccinfhir/model_edits.py,sha256=Nduf4LcuPQoVwGcb4Cal9sV62DPylhaoBRY92ybG2Jg,4119
|
|
15
16
|
hccinfhir/model_hierarchies.py,sha256=cboUnSHZZfOxA8QZKV4QIE-32duElssML32OqYT-65g,1542
|
|
16
17
|
hccinfhir/model_interactions.py,sha256=prguJoOWBIO97UEpD0njXPvYM6-hoNjBIFYxDOxkLt0,25816
|
|
17
18
|
hccinfhir/samples.py,sha256=2VSWS81cv9EnaHqK7sd6CjwG6FUI9E--5wHgD000REI,9952
|
|
18
|
-
hccinfhir/utils.py,sha256=
|
|
19
|
+
hccinfhir/utils.py,sha256=iMTruSuwAj8D8tdQjVNedgCEIP7OVimCXQTWJXOrFhQ,14930
|
|
19
20
|
hccinfhir/data/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
|
|
20
21
|
hccinfhir/data/hcc_is_chronic.csv,sha256=Bwd-RND6SdEsKP-assoBaXnjUJAuDXhSkwWlymux72Y,19701
|
|
21
22
|
hccinfhir/data/hcc_is_chronic_without_esrd_model.csv,sha256=eVVI4_8mQNkiBiNO3kattfT_zfcV18XgmiltdzZEXSo,17720
|
|
22
23
|
hccinfhir/data/ph_race_and_ethnicity_cdc_v1.3.csv,sha256=5tw_ATN1mQWVUIahXZyIa5GOX-977PzfhNlGvm43tD8,146970
|
|
23
24
|
hccinfhir/data/ra_coefficients_2025.csv,sha256=I0S2hoJlfig-D0oSFxy0b3Piv7m9AzOGo2CwR6bcQ9w,215191
|
|
24
25
|
hccinfhir/data/ra_coefficients_2026.csv,sha256=0gfjGgVdIEWkBO01NaAbTLMzHCYINA0rf_zl8ojngCY,288060
|
|
26
|
+
hccinfhir/data/ra_dx_edits.csv,sha256=_DPfODDzB41Ifrdjb9B--Lxr_L_HeTnujDAm23CzLiM,11965
|
|
25
27
|
hccinfhir/data/ra_dx_to_cc_2025.csv,sha256=4N7vF6VZndkl7d3Fo0cGsbAPAZdCjAizSH8BOKsZNAo,1618924
|
|
26
28
|
hccinfhir/data/ra_dx_to_cc_2026.csv,sha256=YT9HwQFUddL_bxuE9nxHWsBtZzojINL0DzABBMp6kms,1751007
|
|
27
29
|
hccinfhir/data/ra_eligible_cpt_hcpcs_2023.csv,sha256=VVoA4s0hsFmcRIugyFdbvSoeLcn7M7z0DITT6l4YqL8,39885
|
|
@@ -31,6 +33,7 @@ hccinfhir/data/ra_eligible_cpt_hcpcs_2026.csv,sha256=EYGN7k_rgCpJe59lL_yNInUcCkd
|
|
|
31
33
|
hccinfhir/data/ra_hierarchies_2025.csv,sha256=HQSPNloe6mvvwMgv8ZwYAfWKkT2b2eUvm4JQy6S_mVQ,13045
|
|
32
34
|
hccinfhir/data/ra_hierarchies_2026.csv,sha256=pKevSx-dYfLyO-Leruh2AFLn5uO4y49O9EOr-O6-cbY,19595
|
|
33
35
|
hccinfhir/data/ra_labels_2026.csv,sha256=P-Ym0np06E_CxwELdBGZZ7j5NwhXLsHoRPnp3jeYWn4,50248
|
|
36
|
+
hccinfhir/data/ra_proposed_coefficients_2027.csv,sha256=8lEFaURJIuippJBRyJM9zHkV80MUY2AJ3GfYLvniOfk,165411
|
|
34
37
|
hccinfhir/sample_files/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
|
|
35
38
|
hccinfhir/sample_files/sample_834_01.txt,sha256=J2HMXfY6fAFpV36rvLQ3QymRRS2TPqf3TQY6CNS7TrE,1627
|
|
36
39
|
hccinfhir/sample_files/sample_834_02.txt,sha256=vSvjM69kKfOW9e-8dvlO9zDcRPpOD7LmekLu68z4aB4,926
|
|
@@ -55,7 +58,7 @@ hccinfhir/sample_files/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9f
|
|
|
55
58
|
hccinfhir/sample_files/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
|
|
56
59
|
hccinfhir/sample_files/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
|
|
57
60
|
hccinfhir/sample_files/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
|
|
58
|
-
hccinfhir-0.2.
|
|
59
|
-
hccinfhir-0.2.
|
|
60
|
-
hccinfhir-0.2.
|
|
61
|
-
hccinfhir-0.2.
|
|
61
|
+
hccinfhir-0.2.9.dist-info/METADATA,sha256=GLRnofCiwRDTjmdVCxiNOf8-G5iLHhUKGSqUuJjKeT0,40728
|
|
62
|
+
hccinfhir-0.2.9.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
63
|
+
hccinfhir-0.2.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
64
|
+
hccinfhir-0.2.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|