hccinfhir 0.2.3__py3-none-any.whl → 0.2.4__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/ph_race_and_ethnicity_cdc_v1.3.csv +1325 -0
- hccinfhir/datamodels.py +4 -2
- hccinfhir/extractor_834.py +59 -7
- hccinfhir/utils.py +40 -0
- {hccinfhir-0.2.3.dist-info → hccinfhir-0.2.4.dist-info}/METADATA +3 -1
- {hccinfhir-0.2.3.dist-info → hccinfhir-0.2.4.dist-info}/RECORD +8 -7
- {hccinfhir-0.2.3.dist-info → hccinfhir-0.2.4.dist-info}/WHEEL +0 -0
- {hccinfhir-0.2.3.dist-info → hccinfhir-0.2.4.dist-info}/licenses/LICENSE +0 -0
hccinfhir/datamodels.py
CHANGED
|
@@ -287,7 +287,8 @@ class EnrollmentData(BaseModel):
|
|
|
287
287
|
# HCP (Health Care Plan) Info
|
|
288
288
|
hcp_code: Current HCP code (HD04 first part)
|
|
289
289
|
hcp_status: Current HCP status (HD04 second part)
|
|
290
|
-
|
|
290
|
+
amount_qualifier: AMT qualifier code (e.g., 'D' = premium, 'C1' = copay)
|
|
291
|
+
amount: Premium or cost share amount (numeric)
|
|
291
292
|
|
|
292
293
|
# HCP History (multiple coverage periods)
|
|
293
294
|
hcp_history: List of historical HCP coverage periods
|
|
@@ -367,7 +368,8 @@ class EnrollmentData(BaseModel):
|
|
|
367
368
|
# HCP Info
|
|
368
369
|
hcp_code: Optional[str] = None
|
|
369
370
|
hcp_status: Optional[str] = None
|
|
370
|
-
|
|
371
|
+
amount_qualifier: Optional[str] = None
|
|
372
|
+
amount: Optional[float] = None
|
|
371
373
|
|
|
372
374
|
# HCP History
|
|
373
375
|
hcp_history: List[HCPCoveragePeriod] = []
|
hccinfhir/extractor_834.py
CHANGED
|
@@ -21,6 +21,21 @@ from hccinfhir.constants import (
|
|
|
21
21
|
map_medicare_status_to_dual_code,
|
|
22
22
|
map_aid_code_to_dual_status,
|
|
23
23
|
)
|
|
24
|
+
from hccinfhir.utils import load_race_ethnicity
|
|
25
|
+
|
|
26
|
+
# Load race/ethnicity mapping at module level
|
|
27
|
+
_RACE_ETHNICITY_MAPPING: Optional[Dict[str, str]] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_race_ethnicity_mapping() -> Dict[str, str]:
|
|
31
|
+
"""Lazy load race/ethnicity mapping"""
|
|
32
|
+
global _RACE_ETHNICITY_MAPPING
|
|
33
|
+
if _RACE_ETHNICITY_MAPPING is None:
|
|
34
|
+
try:
|
|
35
|
+
_RACE_ETHNICITY_MAPPING = load_race_ethnicity()
|
|
36
|
+
except (FileNotFoundError, RuntimeError):
|
|
37
|
+
_RACE_ETHNICITY_MAPPING = {}
|
|
38
|
+
return _RACE_ETHNICITY_MAPPING
|
|
24
39
|
|
|
25
40
|
# Constants
|
|
26
41
|
TRANSACTION_TYPES = {"005010X220A1": "834"}
|
|
@@ -114,7 +129,8 @@ class MemberContext(BaseModel):
|
|
|
114
129
|
# HCP Info
|
|
115
130
|
hcp_code: Optional[str] = None
|
|
116
131
|
hcp_status: Optional[str] = None
|
|
117
|
-
|
|
132
|
+
amount_qualifier: Optional[str] = None
|
|
133
|
+
amount: Optional[float] = None
|
|
118
134
|
|
|
119
135
|
# HCP History
|
|
120
136
|
hcp_history: List[HCPContext] = []
|
|
@@ -206,6 +222,35 @@ def contains_any_keyword(text: str, keywords: set) -> bool:
|
|
|
206
222
|
return any(kw in text_upper for kw in keywords)
|
|
207
223
|
|
|
208
224
|
|
|
225
|
+
def parse_race_code(raw_value: Optional[str]) -> Optional[str]:
|
|
226
|
+
"""Parse race code from DMG05 and translate to human-readable name.
|
|
227
|
+
|
|
228
|
+
Handles formats like:
|
|
229
|
+
- ":RET:2135-2" -> "Hispanic or Latino"
|
|
230
|
+
- "2135-2" -> "Hispanic or Latino"
|
|
231
|
+
- "2106-3" -> "White"
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
raw_value: Raw race value from DMG segment
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Human-readable race/ethnicity name, or original value if not found
|
|
238
|
+
"""
|
|
239
|
+
if not raw_value:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
# Extract code from formats like ":RET:2135-2" or "2135-2"
|
|
243
|
+
code = raw_value
|
|
244
|
+
if ':' in raw_value:
|
|
245
|
+
parts = raw_value.split(':')
|
|
246
|
+
# Take the last non-empty part which should be the code
|
|
247
|
+
code = next((p for p in reversed(parts) if p), raw_value)
|
|
248
|
+
|
|
249
|
+
# Look up in mapping
|
|
250
|
+
mapping = _get_race_ethnicity_mapping()
|
|
251
|
+
return mapping.get(code, raw_value)
|
|
252
|
+
|
|
253
|
+
|
|
209
254
|
# ============================================================================
|
|
210
255
|
# Dual Eligibility Logic
|
|
211
256
|
# ============================================================================
|
|
@@ -248,7 +293,7 @@ def parse_ref_23(value: str, member: MemberContext) -> None:
|
|
|
248
293
|
member.cin_check_digit = get_composite_part(value, 0)
|
|
249
294
|
card_date = get_composite_part(value, 1)
|
|
250
295
|
if card_date and len(card_date) >= 8:
|
|
251
|
-
member.fame_card_issue_date = card_date[:8]
|
|
296
|
+
member.fame_card_issue_date = parse_date(card_date[:8])
|
|
252
297
|
|
|
253
298
|
|
|
254
299
|
def parse_ref_3h(value: str, member: MemberContext) -> None:
|
|
@@ -277,7 +322,7 @@ def parse_ref_dx(value: str, member: MemberContext) -> None:
|
|
|
277
322
|
member.carrier_code = strip_leading_zeros(carrier)
|
|
278
323
|
policy_start = get_composite_part(value, 2)
|
|
279
324
|
if policy_start and len(policy_start) >= 8:
|
|
280
|
-
member.coverage_start_date = policy_start[:8]
|
|
325
|
+
member.coverage_start_date = parse_date(policy_start[:8])
|
|
281
326
|
|
|
282
327
|
|
|
283
328
|
def parse_ref_17(value: str, member: MemberContext) -> None:
|
|
@@ -495,7 +540,8 @@ def _finalize_member(member: MemberContext, source: str, report_date: str) -> En
|
|
|
495
540
|
res_zip_deliv_code=member.res_zip_deliv_code,
|
|
496
541
|
orec=member.orec, crec=member.crec, snp=member.snp,
|
|
497
542
|
low_income=member.low_income, lti=member.lti, new_enrollee=new_enrollee,
|
|
498
|
-
hcp_code=member.hcp_code, hcp_status=member.hcp_status,
|
|
543
|
+
hcp_code=member.hcp_code, hcp_status=member.hcp_status,
|
|
544
|
+
amount_qualifier=member.amount_qualifier, amount=member.amount,
|
|
499
545
|
hcp_history=hcp_history
|
|
500
546
|
)
|
|
501
547
|
|
|
@@ -594,7 +640,7 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
|
|
|
594
640
|
sex = get_segment_value(segment, 3)
|
|
595
641
|
if sex in X12_SEX_CODE_MAPPING:
|
|
596
642
|
member.sex = X12_SEX_CODE_MAPPING[sex]
|
|
597
|
-
member.race = get_segment_value(segment, 5)
|
|
643
|
+
member.race = parse_race_code(get_segment_value(segment, 5))
|
|
598
644
|
death_str = get_segment_value(segment, 6)
|
|
599
645
|
if death_str and len(death_str) >= 8:
|
|
600
646
|
member.death_date = parse_date(death_str[:8])
|
|
@@ -638,7 +684,13 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
|
|
|
638
684
|
|
|
639
685
|
# AMT - Amount
|
|
640
686
|
elif seg_id == 'AMT' and len(segment) >= 3:
|
|
641
|
-
member.
|
|
687
|
+
member.amount_qualifier = get_segment_value(segment, 1)
|
|
688
|
+
amt_val = get_segment_value(segment, 2)
|
|
689
|
+
if amt_val:
|
|
690
|
+
try:
|
|
691
|
+
member.amount = float(amt_val)
|
|
692
|
+
except ValueError:
|
|
693
|
+
pass
|
|
642
694
|
|
|
643
695
|
# Finalize last member
|
|
644
696
|
if member.member_id or member.has_medicare or member.has_medicaid:
|
|
@@ -696,7 +748,7 @@ def extract_enrollment_834(content: str) -> List[EnrollmentData]:
|
|
|
696
748
|
source = get_segment_value(segment, 2)
|
|
697
749
|
gs_date = get_segment_value(segment, 4)
|
|
698
750
|
if gs_date and len(gs_date) >= 8:
|
|
699
|
-
report_date =
|
|
751
|
+
report_date = parse_date(gs_date[:8])
|
|
700
752
|
if len(segment) > 8 and segment[8] not in TRANSACTION_TYPES:
|
|
701
753
|
raise ValueError("Invalid or unsupported 834 format")
|
|
702
754
|
break
|
hccinfhir/utils.py
CHANGED
|
@@ -247,6 +247,46 @@ def load_coefficients(file_path: str) -> Dict[Tuple[str, ModelName], float]:
|
|
|
247
247
|
return coefficients
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
def load_race_ethnicity(file_path: str = "ph_race_and_ethnicity_cdc_v1.3.csv") -> Dict[str, str]:
|
|
251
|
+
"""
|
|
252
|
+
Load CDC race and ethnicity codes from CSV file.
|
|
253
|
+
Expected format: Concept Code,Hierarchical Property,Concept Name,...
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
file_path: Filename or path to the CSV file
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Dictionary mapping concept code to concept name
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
FileNotFoundError: If file cannot be found
|
|
263
|
+
RuntimeError: If file cannot be loaded or parsed
|
|
264
|
+
"""
|
|
265
|
+
mapping: Dict[str, str] = {}
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
resolved_path = resolve_data_file(file_path)
|
|
269
|
+
with open(resolved_path, "r", encoding="utf-8", errors="replace") as file:
|
|
270
|
+
content = file.read()
|
|
271
|
+
except FileNotFoundError as e:
|
|
272
|
+
raise FileNotFoundError(f"Could not load race/ethnicity mapping: {e}")
|
|
273
|
+
except Exception as e:
|
|
274
|
+
raise RuntimeError(f"Error loading race/ethnicity file '{file_path}': {e}")
|
|
275
|
+
|
|
276
|
+
for line in content.splitlines()[1:]: # Skip header
|
|
277
|
+
try:
|
|
278
|
+
parts = line.split(',')
|
|
279
|
+
if len(parts) >= 3:
|
|
280
|
+
concept_code = parts[0].strip()
|
|
281
|
+
concept_name = parts[2].strip()
|
|
282
|
+
if concept_code and concept_name:
|
|
283
|
+
mapping[concept_code] = concept_name
|
|
284
|
+
except (ValueError, IndexError):
|
|
285
|
+
continue # Skip malformed lines
|
|
286
|
+
|
|
287
|
+
return mapping
|
|
288
|
+
|
|
289
|
+
|
|
250
290
|
def load_labels(file_path: str) -> Dict[Tuple[str, ModelName], str]:
|
|
251
291
|
"""
|
|
252
292
|
Load HCC labels from a CSV file.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hccinfhir
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -95,6 +95,7 @@ print(f"HCCs: {result.hcc_list}")
|
|
|
95
95
|
- **Use Case**: Extract dual eligibility status, detect Medicaid coverage loss
|
|
96
96
|
- **Features**: California DHCS aid code mapping, Medicare status codes, coverage tracking
|
|
97
97
|
- **Output**: Demographics with accurate dual eligibility for risk calculations
|
|
98
|
+
- **Architecture**: See [834 Parsing Documentation](./README_PARSING834.md) for transaction structure and parsing logic
|
|
98
99
|
|
|
99
100
|
### 3. **FHIR ExplanationOfBenefit Resources**
|
|
100
101
|
- **Input**: FHIR EOB from CMS Blue Button 2.0 / BCDA API
|
|
@@ -1073,6 +1074,7 @@ Apache License 2.0. See [LICENSE](LICENSE) for details.
|
|
|
1073
1074
|
## 📞 Support
|
|
1074
1075
|
|
|
1075
1076
|
- **Claude Code Documentation**: [CLAUDE.md](./CLAUDE.md) - Comprehensive developer guide
|
|
1077
|
+
- **834 Parsing Architecture**: [README_PARSING834.md](./README_PARSING834.md) - X12 834 transaction structure and parsing logic
|
|
1076
1078
|
- **Issues**: [GitHub Issues](https://github.com/mimilabs/hccinfhir/issues)
|
|
1077
1079
|
|
|
1078
1080
|
## 👥 Contributors
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
hccinfhir/__init__.py,sha256=3aFYtjTklZJg3wIlnMJNgfDBaDCfKXVlYsacdsZ9L4I,1113
|
|
2
2
|
hccinfhir/constants.py,sha256=C4Vyjtzgyd4Jm2I2X6cTYQZLe-jAMC8boUcy-7OXQDQ,8473
|
|
3
|
-
hccinfhir/datamodels.py,sha256=
|
|
3
|
+
hccinfhir/datamodels.py,sha256=LNk94V3ez8f1J4Y8PXxNgHslVfwNQoJCl0SO4etgxs4,15593
|
|
4
4
|
hccinfhir/defaults.py,sha256=aKdXPhf9bYUzpGvXM1GIXZaKxqkKInt3v9meLB9fWog,1394
|
|
5
5
|
hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
|
|
6
|
-
hccinfhir/extractor_834.py,sha256=
|
|
6
|
+
hccinfhir/extractor_834.py,sha256=gXbSQJOAdQiOub2LHUYX4Eb7ABKL0WC5r0-ZX1pe70k,29579
|
|
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
|
|
@@ -15,10 +15,11 @@ hccinfhir/model_dx_to_cc.py,sha256=Yjc6xKI-jMXsbOzS_chc4NI15Bwagb7BwZZ8cKQaTbk,1
|
|
|
15
15
|
hccinfhir/model_hierarchies.py,sha256=cboUnSHZZfOxA8QZKV4QIE-32duElssML32OqYT-65g,1542
|
|
16
16
|
hccinfhir/model_interactions.py,sha256=g6jK27Xu8RQUHS3lk4sk2v6w6wqd52mdbGn0BsnR7Pk,21394
|
|
17
17
|
hccinfhir/samples.py,sha256=2VSWS81cv9EnaHqK7sd6CjwG6FUI9E--5wHgD000REI,9952
|
|
18
|
-
hccinfhir/utils.py,sha256=
|
|
18
|
+
hccinfhir/utils.py,sha256=WQ2atW0CrdX7sAz_YRLeY4JD-CuH0o-WRusQ_xVVfgY,12152
|
|
19
19
|
hccinfhir/data/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
|
|
20
20
|
hccinfhir/data/hcc_is_chronic.csv,sha256=Bwd-RND6SdEsKP-assoBaXnjUJAuDXhSkwWlymux72Y,19701
|
|
21
21
|
hccinfhir/data/hcc_is_chronic_without_esrd_model.csv,sha256=eVVI4_8mQNkiBiNO3kattfT_zfcV18XgmiltdzZEXSo,17720
|
|
22
|
+
hccinfhir/data/ph_race_and_ethnicity_cdc_v1.3.csv,sha256=5tw_ATN1mQWVUIahXZyIa5GOX-977PzfhNlGvm43tD8,146970
|
|
22
23
|
hccinfhir/data/ra_coefficients_2025.csv,sha256=I0S2hoJlfig-D0oSFxy0b3Piv7m9AzOGo2CwR6bcQ9w,215191
|
|
23
24
|
hccinfhir/data/ra_coefficients_2026.csv,sha256=0gfjGgVdIEWkBO01NaAbTLMzHCYINA0rf_zl8ojngCY,288060
|
|
24
25
|
hccinfhir/data/ra_dx_to_cc_2025.csv,sha256=4N7vF6VZndkl7d3Fo0cGsbAPAZdCjAizSH8BOKsZNAo,1618924
|
|
@@ -54,7 +55,7 @@ hccinfhir/sample_files/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9f
|
|
|
54
55
|
hccinfhir/sample_files/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
|
|
55
56
|
hccinfhir/sample_files/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
|
|
56
57
|
hccinfhir/sample_files/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
|
|
57
|
-
hccinfhir-0.2.
|
|
58
|
-
hccinfhir-0.2.
|
|
59
|
-
hccinfhir-0.2.
|
|
60
|
-
hccinfhir-0.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|