hccinfhir 0.2.4__py3-none-any.whl → 0.2.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/datamodels.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, Field
2
- from typing import List, Optional, Literal, Dict, Set, TypedDict, Union
2
+ from typing import List, Optional, Literal, Dict, Set, TypedDict, Union, Any
3
+ from datetime import date
3
4
 
4
5
  # Define Model Name literal type
5
6
  ModelName = Literal[
@@ -203,6 +204,21 @@ class HCPCoveragePeriod(BaseModel):
203
204
  hcp_status: Optional[str] = None
204
205
  aid_codes: Optional[str] = None # REF*CE composite
205
206
 
207
+ def model_dump_with_dates(self, **kwargs) -> Dict[str, Any]:
208
+ """Return dict with date fields as date objects instead of strings.
209
+
210
+ Args:
211
+ **kwargs: Additional arguments passed to model_dump()
212
+
213
+ Returns:
214
+ Dict with start_date and end_date as date objects (if present)
215
+ """
216
+ data = self.model_dump(**kwargs)
217
+ for field in ('start_date', 'end_date'):
218
+ if data.get(field):
219
+ data[field] = date.fromisoformat(data[field])
220
+ return data
221
+
206
222
 
207
223
  class EnrollmentData(BaseModel):
208
224
  """
@@ -372,4 +388,47 @@ class EnrollmentData(BaseModel):
372
388
  amount: Optional[float] = None
373
389
 
374
390
  # HCP History
375
- hcp_history: List[HCPCoveragePeriod] = []
391
+ hcp_history: List[HCPCoveragePeriod] = []
392
+
393
+ def model_dump_with_dates(self, **kwargs) -> Dict[str, Any]:
394
+ """Return dict with date fields as date objects instead of strings.
395
+
396
+ Converts all YYYY-MM-DD string date fields to date objects.
397
+ Also converts dates in nested hcp_history items.
398
+
399
+ Args:
400
+ **kwargs: Additional arguments passed to model_dump()
401
+
402
+ Returns:
403
+ Dict with date fields as date objects (if present)
404
+
405
+ Example:
406
+ >>> enrollment = extract_enrollment_834(content)[0]
407
+ >>> data = enrollment.model_dump_with_dates()
408
+ >>> isinstance(data['dob'], date) # True
409
+ """
410
+ data = self.model_dump(**kwargs)
411
+
412
+ # EnrollmentData date fields
413
+ date_fields = (
414
+ 'report_date',
415
+ 'dob',
416
+ 'death_date',
417
+ 'coverage_start_date',
418
+ 'coverage_end_date',
419
+ 'fame_card_issue_date',
420
+ 'fame_redetermination_date',
421
+ 'fame_death_date',
422
+ )
423
+ for field in date_fields:
424
+ if data.get(field):
425
+ data[field] = date.fromisoformat(data[field])
426
+
427
+ # Convert dates in hcp_history items
428
+ if data.get('hcp_history'):
429
+ for hcp in data['hcp_history']:
430
+ for field in ('start_date', 'end_date'):
431
+ if hcp.get(field):
432
+ hcp[field] = date.fromisoformat(hcp[field])
433
+
434
+ return data
@@ -619,8 +619,8 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
619
619
  # Strip state suffix from city if embedded
620
620
  if city and state and city.upper().endswith(' ' + state.upper()):
621
621
  city = city[:-len(state)-1].strip()
622
- member.city = city.lower() if city else None
623
- member.state = state.lower() if state else None
622
+ member.city = city
623
+ member.state = state
624
624
  member.zip = get_segment_value(segment, 3)
625
625
  # County code
626
626
  if len(segment) > 6 and segment[5] == 'CY' and not member.fame_county_id:
@@ -112,11 +112,23 @@ def apply_coefficients(demographics: Demographics,
112
112
  if interaction_value < 1:
113
113
  continue
114
114
 
115
+ # Standard prefix-based lookup
115
116
  key = (f"{prefix}{interaction_key}".lower(), model_name)
116
117
  if key in coefficients:
117
118
  value = coefficients[key]
118
119
  output[interaction_key] = value
119
120
 
121
+ # No-prefix lookup for ESRD duration coefficients stored without prefix
122
+ # ESRD V21: GE65_DUR*, LT65_DUR*; ESRD V24: FGC_*, FGI_*, LTI_GE65/LT65
123
+ if (interaction_key.startswith('FGC') or
124
+ interaction_key.startswith('FGI') or
125
+ interaction_key.startswith('GE65_DUR') or
126
+ interaction_key.startswith('LT65_DUR') or
127
+ interaction_key in ('LTI_GE65', 'LTI_LT65')):
128
+ key = (interaction_key.lower(), model_name)
129
+ if key in coefficients:
130
+ value = coefficients[key]
131
+ output[interaction_key] = value
120
132
 
121
133
  return output
122
134
 
@@ -6,50 +6,145 @@ def has_any_hcc(hcc_list: List[str], hcc_set: Set[str]) -> int:
6
6
  return int(bool(set(hcc_list) & hcc_set))
7
7
 
8
8
  def create_demographic_interactions(demographics: Demographics) -> dict:
9
- """Creates common demographic-based interactions"""
9
+ """Creates common demographic-based interactions.
10
+
11
+ This function creates interaction variables that are model-agnostic.
12
+ The coefficient lookup will match only the relevant coefficients for
13
+ each model. Comments indicate which models primarily use each interaction.
14
+ """
10
15
  interactions = {}
11
- # Determine sex from demographics.sex instead of category
12
- # Category can start with 'NEM'/'NEF' for new enrollees, not just 'M'/'F'
16
+
17
+ # Common demographic flags
13
18
  is_female = demographics.sex in ('F', '2')
14
19
  is_male = demographics.sex in ('M', '1')
15
20
  is_aged = not demographics.non_aged
16
-
17
- # Original Disability interactions
21
+ lti = int(demographics.lti) if demographics.lti else 0
22
+ fbd = demographics.fbd
23
+ pbd = demographics.pbd
24
+ graft_months = demographics.graft_months
25
+
26
+ # Medicaid indicator (any dual status)
27
+ mcaid = 1 if demographics.dual_elgbl_cd in {
28
+ '01', '02', '03', '04', '05', '06', '07', '08', '09', '10'
29
+ } else 0
30
+
31
+ # Original Disability interactions (V22, V24, V28, ESRD V21, V24)
32
+ # Only for aged (65+); looked up with prefix (e.g., CNA_, DI_)
18
33
  if is_aged:
19
34
  interactions['OriginallyDisabled_Female'] = int(demographics.orig_disabled) * int(is_female)
20
35
  interactions['OriginallyDisabled_Male'] = int(demographics.orig_disabled) * int(is_male)
21
- else:
22
- interactions['OriginallyDisabled_Female'] = 0
23
- interactions['OriginallyDisabled_Male'] = 0
24
36
 
25
- # LTI interactions - used for ESRD models
26
- if demographics.lti:
37
+ # Originally ESRD interactions (ESRD V21, V24 Dialysis); looked up as DI_Originally_ESRD_*
38
+ if is_aged and demographics.orec in ('2', '3'):
39
+ interactions['Originally_ESRD_Female'] = int(is_female)
40
+ interactions['Originally_ESRD_Male'] = int(is_male)
41
+
42
+ # MCAID × sex × age interactions (ESRD V21 Dialysis and Community Graft only)
43
+ # V21 used MCAID; V24 uses FBDual/PBDual (handled in create_dual_interactions)
44
+ if mcaid:
45
+ interactions['MCAID_Female_Aged'] = int(is_female) * int(is_aged)
46
+ interactions['MCAID_Female_NonAged'] = int(is_female) * int(not is_aged)
47
+ interactions['MCAID_Male_Aged'] = int(is_male) * int(is_aged)
48
+ interactions['MCAID_Male_NonAged'] = int(is_male) * int(not is_aged)
49
+
50
+ # LTI interactions for ESRD models
51
+ if lti:
52
+ # ESRD V24 Dialysis: looked up as DI_LTI_Aged, DI_LTI_NonAged
27
53
  interactions['LTI_Aged'] = int(is_aged)
28
54
  interactions['LTI_NonAged'] = int(not is_aged)
29
- else:
30
- interactions['LTI_Aged'] = 0
31
- interactions['LTI_NonAged'] = 0
32
-
55
+ # ESRD V24 Graft Institutional: looked up WITHOUT prefix as LTI_GE65, LTI_LT65
56
+ interactions['LTI_GE65'] = int(is_aged)
57
+ interactions['LTI_LT65'] = int(not is_aged)
58
+
59
+ # LTIMCAID for V24, V28 Institutional model; looked up as INS_LTIMCAID
60
+ if lti and mcaid:
61
+ interactions['LTIMCAID'] = lti * mcaid
62
+
63
+ # New Enrollee interactions for V24, V28, ESRD V21, ESRD V24
33
64
  nemcaid = False
34
- if demographics.new_enrollee and demographics.dual_elgbl_cd in {'01', '02', '03', '04', '05', '06', '08'}:
65
+ if demographics.new_enrollee and demographics.dual_elgbl_cd in {
66
+ '01', '02', '03', '04', '05', '06', '08'
67
+ }:
35
68
  nemcaid = True
36
- ne_origds = int(demographics.age >= 65 and (demographics.orec is not None and demographics.orec == "1"))
37
-
38
- fbd = demographics.fbd
69
+ ne_origds = int(
70
+ demographics.age >= 65 and
71
+ demographics.orec is not None and
72
+ demographics.orec == "1"
73
+ )
39
74
 
40
- # Four mutually exclusive groups
75
+ # V24, V28, ESRD V21: MCAID/NMCAID style; looked up with NE_ or SNPNE_ prefix
41
76
  interactions.update({
42
77
  f'NMCAID_NORIGDIS_{demographics.category}': int(not nemcaid and not ne_origds),
43
78
  f'MCAID_NORIGDIS_{demographics.category}': int(nemcaid and not ne_origds),
44
79
  f'NMCAID_ORIGDIS_{demographics.category}': int(not nemcaid and ne_origds),
45
80
  f'MCAID_ORIGDIS_{demographics.category}': int(nemcaid and ne_origds),
81
+ })
82
+
83
+ # ESRD V24: FBD/ND_PBD style; looked up with DNE_ or GNE_ prefix
84
+ interactions.update({
46
85
  f'FBD_NORIGDIS_{demographics.category}': int(fbd and not ne_origds),
47
86
  f'FBD_ORIGDIS_{demographics.category}': int(fbd and ne_origds),
48
87
  f'ND_PBD_NORIGDIS_{demographics.category}': int(not fbd and not ne_origds),
49
88
  f'ND_PBD_ORIGDIS_{demographics.category}': int(not fbd and ne_origds)
50
89
  })
51
90
 
52
- # output only non-zero interactions
91
+ # Functioning Graft Duration "transplant bumps" for ESRD models
92
+ # All looked up WITHOUT prefix - they match directly by name
93
+ if graft_months and graft_months >= 4:
94
+ is_dur4_9 = (4 <= graft_months <= 9)
95
+ is_dur10pl = (graft_months >= 10)
96
+
97
+ # ESRD V21: simple age-based bumps (GE65_DUR4_9, LT65_DUR4_9, etc.)
98
+ if is_dur4_9:
99
+ interactions['GE65_DUR4_9'] = int(is_aged)
100
+ interactions['LT65_DUR4_9'] = int(not is_aged)
101
+ if is_dur10pl:
102
+ interactions['GE65_DUR10PL'] = int(is_aged)
103
+ interactions['LT65_DUR10PL'] = int(not is_aged)
104
+
105
+ # ESRD V24: FGC (Community) / FGI (Institutional) stratified by dual status
106
+ if not fbd:
107
+ # Non-Dual and Partial Benefit Dual (ND_PBD)
108
+ if is_dur4_9:
109
+ interactions.update({
110
+ 'FGC_GE65_DUR4_9_ND_PBD': int(is_aged) * int(not lti),
111
+ 'FGC_LT65_DUR4_9_ND_PBD': int(not is_aged) * int(not lti),
112
+ 'FGI_GE65_DUR4_9_ND_PBD': int(is_aged) * lti,
113
+ 'FGI_LT65_DUR4_9_ND_PBD': int(not is_aged) * lti,
114
+ })
115
+ if is_dur10pl:
116
+ interactions.update({
117
+ 'FGC_GE65_DUR10PL_ND_PBD': int(is_aged) * int(not lti),
118
+ 'FGC_LT65_DUR10PL_ND_PBD': int(not is_aged) * int(not lti),
119
+ 'FGI_GE65_DUR10PL_ND_PBD': int(is_aged) * lti,
120
+ 'FGI_LT65_DUR10PL_ND_PBD': int(not is_aged) * lti,
121
+ })
122
+ # Extra PBD flag for Partial Benefit Dual members
123
+ if pbd:
124
+ interactions.update({
125
+ 'FGC_PBD_GE65_flag': int(is_aged) * int(not lti),
126
+ 'FGC_PBD_LT65_flag': int(not is_aged) * int(not lti),
127
+ 'FGI_PBD_GE65_flag': int(is_aged) * lti,
128
+ 'FGI_PBD_LT65_flag': int(not is_aged) * lti,
129
+ })
130
+ else:
131
+ # Full Benefit Dual (FBD)
132
+ if is_dur4_9:
133
+ interactions.update({
134
+ 'FGC_GE65_DUR4_9_FBD': int(is_aged) * int(not lti),
135
+ 'FGC_LT65_DUR4_9_FBD': int(not is_aged) * int(not lti),
136
+ 'FGI_GE65_DUR4_9_FBD': int(is_aged) * lti,
137
+ 'FGI_LT65_DUR4_9_FBD': int(not is_aged) * lti,
138
+ })
139
+ if is_dur10pl:
140
+ interactions.update({
141
+ 'FGC_GE65_DUR10PL_FBD': int(is_aged) * int(not lti),
142
+ 'FGC_LT65_DUR10PL_FBD': int(not is_aged) * int(not lti),
143
+ 'FGI_GE65_DUR10PL_FBD': int(is_aged) * lti,
144
+ 'FGI_LT65_DUR10PL_FBD': int(not is_aged) * lti,
145
+ })
146
+
147
+ # Output only non-zero interactions
53
148
  interactions = {k: v for k, v in interactions.items() if v > 0}
54
149
 
55
150
  return interactions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hccinfhir
3
- Version: 0.2.4
3
+ Version: 0.2.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
@@ -1,19 +1,19 @@
1
1
  hccinfhir/__init__.py,sha256=3aFYtjTklZJg3wIlnMJNgfDBaDCfKXVlYsacdsZ9L4I,1113
2
2
  hccinfhir/constants.py,sha256=C4Vyjtzgyd4Jm2I2X6cTYQZLe-jAMC8boUcy-7OXQDQ,8473
3
- hccinfhir/datamodels.py,sha256=LNk94V3ez8f1J4Y8PXxNgHslVfwNQoJCl0SO4etgxs4,15593
3
+ hccinfhir/datamodels.py,sha256=u-KICO7ODOo8GCsZ0JmyhPauL-irQ9aVjNaiGiVHdks,17592
4
4
  hccinfhir/defaults.py,sha256=aKdXPhf9bYUzpGvXM1GIXZaKxqkKInt3v9meLB9fWog,1394
5
5
  hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
6
- hccinfhir/extractor_834.py,sha256=gXbSQJOAdQiOub2LHUYX4Eb7ABKL0WC5r0-ZX1pe70k,29579
6
+ hccinfhir/extractor_834.py,sha256=H5756zwjkZN9nXlGHr_V63VgNjQEjIB7nqaoBYazAR8,29526
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
11
  hccinfhir/model_calculate.py,sha256=_TUWNVUsBym0pre3wltXvRuipQaONQ0QBfWPFNAeDsQ,8347
12
- hccinfhir/model_coefficients.py,sha256=5n3QzHX6FJ3MlO0cV9NS7Bqt-lxzVvT_M3zFaWq6Gng,4685
12
+ hccinfhir/model_coefficients.py,sha256=jRHip10AzMrcbaqkumxEXKjrlOSe8taQcXsGmQNzVcA,5330
13
13
  hccinfhir/model_demographics.py,sha256=nImKtJCq1HkR9w2GU8aikybJFgow71CPufBRV8Jn7fM,8932
14
14
  hccinfhir/model_dx_to_cc.py,sha256=Yjc6xKI-jMXsbOzS_chc4NI15Bwagb7BwZZ8cKQaTbk,1540
15
15
  hccinfhir/model_hierarchies.py,sha256=cboUnSHZZfOxA8QZKV4QIE-32duElssML32OqYT-65g,1542
16
- hccinfhir/model_interactions.py,sha256=g6jK27Xu8RQUHS3lk4sk2v6w6wqd52mdbGn0BsnR7Pk,21394
16
+ hccinfhir/model_interactions.py,sha256=5CHRf-YwF4jMYpd0u3XTNvGjwInb7mhOsF4L9boPsuY,25810
17
17
  hccinfhir/samples.py,sha256=2VSWS81cv9EnaHqK7sd6CjwG6FUI9E--5wHgD000REI,9952
18
18
  hccinfhir/utils.py,sha256=WQ2atW0CrdX7sAz_YRLeY4JD-CuH0o-WRusQ_xVVfgY,12152
19
19
  hccinfhir/data/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
@@ -29,8 +29,8 @@ hccinfhir/data/ra_eligible_cpt_hcpcs_2024.csv,sha256=CawKImfCb8fFMDbWwqvNLRyRAda
29
29
  hccinfhir/data/ra_eligible_cpt_hcpcs_2025.csv,sha256=-tMvv2su5tsSbGUh6fZZCMUEkXInBpcTtbUCi2o_UwI,40359
30
30
  hccinfhir/data/ra_eligible_cpt_hcpcs_2026.csv,sha256=EYGN7k_rgCpJe59lL_yNInUcCkdETDWGSFTXII3LZ0Y,40497
31
31
  hccinfhir/data/ra_hierarchies_2025.csv,sha256=HQSPNloe6mvvwMgv8ZwYAfWKkT2b2eUvm4JQy6S_mVQ,13045
32
- hccinfhir/data/ra_hierarchies_2026.csv,sha256=A6ZQZb0rpRWrySBB_KA5S4PGtMxWuzB2guU3aBE09v0,19596
33
- hccinfhir/data/ra_labels_2026.csv,sha256=YstfP7s-3ZwjP4I_GYPPj3_yn-PQK3Q0Q_MVYZhsfjY,50248
32
+ hccinfhir/data/ra_hierarchies_2026.csv,sha256=pKevSx-dYfLyO-Leruh2AFLn5uO4y49O9EOr-O6-cbY,19595
33
+ hccinfhir/data/ra_labels_2026.csv,sha256=P-Ym0np06E_CxwELdBGZZ7j5NwhXLsHoRPnp3jeYWn4,50248
34
34
  hccinfhir/sample_files/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
35
35
  hccinfhir/sample_files/sample_834_01.txt,sha256=J2HMXfY6fAFpV36rvLQ3QymRRS2TPqf3TQY6CNS7TrE,1627
36
36
  hccinfhir/sample_files/sample_834_02.txt,sha256=vSvjM69kKfOW9e-8dvlO9zDcRPpOD7LmekLu68z4aB4,926
@@ -55,7 +55,7 @@ hccinfhir/sample_files/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9f
55
55
  hccinfhir/sample_files/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
56
56
  hccinfhir/sample_files/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
57
57
  hccinfhir/sample_files/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
58
- hccinfhir-0.2.4.dist-info/METADATA,sha256=wjhOKhD3HpfuCs69t-sBSrQn_kuNngbRaA2lLNuCTno,37381
59
- hccinfhir-0.2.4.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
60
- hccinfhir-0.2.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
- hccinfhir-0.2.4.dist-info/RECORD,,
58
+ hccinfhir-0.2.6.dist-info/METADATA,sha256=rNYXVJ44S85cLd_TT-abUSjs5mLw4h-jLw8hGrJ6BsU,37381
59
+ hccinfhir-0.2.6.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
60
+ hccinfhir-0.2.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
+ hccinfhir-0.2.6.dist-info/RECORD,,