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/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 from REF*6O
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')
@@ -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;;... (FAME redetermination date)"""
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:
@@ -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)
@@ -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{model_version[-2:]}"
250
+ model_name = f"CMS-HCC {model_domain} Model V{version_num}"
239
251
  else:
240
- model_name = f"{model_domain} Model V{model_version[-2:]}"
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.7
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=u-KICO7ODOo8GCsZ0JmyhPauL-irQ9aVjNaiGiVHdks,17592
4
- hccinfhir/defaults.py,sha256=aKdXPhf9bYUzpGvXM1GIXZaKxqkKInt3v9meLB9fWog,1394
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=H5756zwjkZN9nXlGHr_V63VgNjQEjIB7nqaoBYazAR8,29526
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=32JOFItbEQ5OAGPrvfLnQSgW2aBiZ-JCS7ixEB6TAZY,8429
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=WQ2atW0CrdX7sAz_YRLeY4JD-CuH0o-WRusQ_xVVfgY,12152
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.7.dist-info/METADATA,sha256=d52fnw3hxrWH7BLxEtkB-aefn4K6bo0g6JjLivHTBFM,37381
59
- hccinfhir-0.2.7.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
60
- hccinfhir-0.2.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
- hccinfhir-0.2.7.dist-info/RECORD,,
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,,