Skip to content

Commit

Permalink
Implement ESRDv24 model
Browse files Browse the repository at this point in the history
closes yubin-park#66. adds full_partial_nondual, lti and graft_duration arguments to HCCEngine.riskprofile() necessary for the ESRDv24 model. Adds _E2423T1M for ESRDv24 interactions  and _E2423T1P for ESRDv24 risk coefficient lookups.
  • Loading branch information
sethclaw committed Jul 26, 2024
1 parent 550f388 commit 51e4aeb
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 8 deletions.
67 changes: 67 additions & 0 deletions hccpy/_E2423T1M.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from collections import Counter

def create_interactions(cc_lst, DISABL, age):

x = Counter(cc_lst)
z = Counter()

# payable HCC's count interaction
hcccnt = len([k for k in x.keys() if "HCC" in k])
if hcccnt > 9:
x["D10P"] = 1
elif hcccnt > 0:
x["D{}".format(hcccnt)] = 1

# diagnostic categories
z["CANCER"] = max(x["HCC8"], x["HCC9"], x["HCC10"],
x["HCC11"], x["HCC12"])
z["DIABETES"] = max(x["HCC17"], x["HCC18"], x["HCC19"])
#z["IMMUNE"] = x["HCC47"]
z["CARD_RESP_FAIL"] = max(x["HCC82"], x["HCC83"], x["HCC84"])
z["CHF"] = x["HCC85"]
z["gCopdCF"] = max(x["HCC110"], x["HCC111"], x["HCC112"])
z["RENAL_V24"] = max(x["HCC134"], x["HCC135"], x["HCC136"],
x["HCC137"], x["HCC138"])
#z["COMPL"] = x["HCC176"]
z["SEPSIS"] = x["HCC2"]
z["gSubstanceUseDisorder_V24"] = max(x["HCC54"], x["HCC55"], x["HCC56"])
z["gPsychiatric_V24"] = max(x["HCC57"], x["HCC58"], x["HCC59"], x["HCC60"])

# community model interactions
x["HCC47_gCancer"] = x["HCC47"] * z["CANCER"]
x["DIABETES_CHF"] = z["DIABETES"] * z["CHF"]
x["CHF_gCopdCF"] = z["CHF"] * z["gCopdCF"]
x["HCC85_gRenal_V24"] =x["HCC85"] * z["RENAL_V24"]
x["gCopdCF_CARD_RESP_FAIL"] = z["gCopdCF"] * z["CARD_RESP_FAIL"]
x["HCC85_HCC96"] = x["HCC85"] * x["HCC96"]
x["gSubUseDs_gPsych_V24"] = z["gSubstanceUseDisorder_V24"] * z["gPsychiatric_V24"]

x["NONAGED_gSubUseDs_gPsych"] = DISABL * x["gSubUseDs_gPsych_V24"]
x["NONAGED_HCC6"] = DISABL * x["HCC6"]
x["NONAGED_HCC34"] = DISABL * x["HCC34"]
x["NONAGED_HCC46"] = DISABL * x["HCC46"]
x["NONAGED_HCC110"] = DISABL * x["HCC110"]
x["NONAGED_HCC176"] = DISABL * x["HCC176"]

# institutional model interactions
x["PRESSURE_ULCER"] = max(x["HCC157"], x["HCC158"],
x["HCC159"])
x["SEPSIS_PRESSURE_ULCER_V24"] = z["SEPSIS"] * x["PRESSURE_ULCER"]
x["SEPSIS_ARTIF_OPENINGS"] = z["SEPSIS"] * x["HCC188"]
x["ART_OPENINGS_PRESSURE_ULCER_V24"] = x["HCC188"] * x["PRESSURE_ULCER"]
x["gCopdCF_ASP_SPEC_B_PNEUM"] = z["gCopdCF"] * x["HCC114"]
x["ASP_SPEC_B_PNEUM_PRES_ULC_V24"] = x["HCC114"] * x["PRESSURE_ULCER"]
x["SEPSIS_ASP_SPEC_BACT_PNEUM"] = z["SEPSIS"] * x["HCC114"]
x["SCHIZOPHRENIA_gCopdCF"] = x["HCC57"] * z["gCopdCF"]
x["SCHIZOPHRENIA_CHF"] = x["HCC57"] * z["CHF"]
x["SCHIZOPHRENIA_SEIZURES"] = x["HCC57"] * x["HCC79"]

x["NONAGED_HCC85"] = DISABL * x["HCC85"]
x["NONAGED_PRESSURE_ULCER_V24"] = DISABL * x["PRESSURE_ULCER"]
x["NONAGED_HCC161"] = DISABL * x["HCC161"]
x["NONAGED_HCC39"] = DISABL * x["HCC39"]
x["NONAGED_HCC77"] = DISABL * x["HCC77"]

cc_lst = [k for k, v in x.items() if v > 0]

return cc_lst
169 changes: 169 additions & 0 deletions hccpy/_E2423T1P.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
def get_risk_dct(coefn, hcc_lst, age, sex, elig, origds, origesrd, full_partial_nondual, disabled, lti, graft_duration):

risk_dct = {}

# build demographic bracket strings and add to risk_dct

if elig in {"DI", "GI",}:
elig_sex = elig + "_" + sex
elif elig in {'GF','GNP'}:
# broken out by aged and non aged
aged = 'N' if disabled else 'A'
elig += aged
elig_sex = elig + "_" + sex
elif elig in {'DNE', 'GNE'}:
# new enrolle
elig_sex = "NE" + sex
elif elig in {"TRANSPLANT_KIDNEY_ONLY_1M",
"TRANSPLANT_KIDNEY_ONLY_2M",
"TRANSPLANT_KIDNEY_ONLY_3M"}:
elig_sex = elig



# build age bracket strings and add to risk_dct
# ESRD new enrolee age variables do not follow samme structure

if elig == 'GNE' and 65 <= age <= 69:
age_match = elig_sex + str(age)
elif elig in {"TRANSPLANT_KIDNEY_ONLY_1M",
"TRANSPLANT_KIDNEY_ONLY_2M",
"TRANSPLANT_KIDNEY_ONLY_3M"}:
age_match = elig_sex
else:
age_ranges = [x for x in coefn.keys() if elig_sex in x and elig in x and '_' in x[-5:]]
if not age_ranges:
print('\t',elig)
print('\t',disabled)
print('\t',elig_sex)
print('\t',age_ranges)
age_match = ""
for age_range in age_ranges:

age_tokens = age_range[-5:].replace(sex, "").split("_")
lb, ub = 0, 999
if len(age_tokens) == 1:
lb = int(age_tokens[0])
ub = lb + 1
elif age_tokens[1] == "GT":
lb = int(age_tokens[0])
else:
lb = int(age_tokens[0])
ub = int(age_tokens[1]) + 1
if lb <= age < ub:
if elig in {'DNE', 'GNE'}:
age_match = elig_sex + age_range.split(elig_sex)[1]
else:
age_match = age_range
break

# Dual status interactions with Age and Sex used in dialysis CE model;
elig_fbpb = ''
if elig == "DI" and full_partial_nondual != 'N':
if full_partial_nondual == 'F':
elig_fbpb = elig + '_FBDual'
else:
elig_fbpb = elig + '_PBDual'
if sex == 'M':
elig_fbpb += '_Male'
else:
elig_fbpb += '_Female'
if disabled:
elig_fbpb += '_NonAged'
else:
elig_fbpb += '_Aged'

# Originally Disabled Interactions with Sex used in dialysis CE model and functioning graft community aged models;
elig_origds = ''
if origds > 0:
elig_origds = elig + "_OriginallyDisabled_"
if sex == "M":
elig_origds += "Male"
else:
elig_origds += "Female"

# Originally ESRD interactions with Sex used in dialysis CE model;
elig_origesrd = ''
if elig == 'DI' and origesrd > 0:
elig_origesrd = elig + "_Originally_ESRD_"
if sex == "M":
elig_origesrd += "Male"
else:
elig_origesrd += "Female"

# LTI interactions with Aged used in dialysis CE model;
elig_ltia = ''
if elig == "DI" and lti > 0:
elig_ltia = elig + '_LTI_'
if disabled:
elig_ltia += 'NonAged'
else:
elig_ltia += 'Aged'

# dualstatus (ND_PBD / FBD), origds (NORIGDIS / ORIGDIS), modelcode ('', G), sex_age_band (== age_match)

base_demo_variable = ''
if elig in {'DNE', 'GNE'}:
base_demo_variable = '_'.join([
elig,
'FBD' if full_partial_nondual == 'F' else 'ND_PBD',
'ORIGDIS' if origds > 0 else 'NORIGDIS',
age_match if elig == 'DNE' else 'G_' + age_match
])
else:
base_demo_variable = age_match

fgf_variable = '' # functioning graft factor transplant bump-up factors
pbd_factor_variable = '' # partial benefit dual status bump up factors
actadj_variable = ''
if 'G' in elig:
if graft_duration:
fgf_variable = '_'.join([
'FGI' if lti > 0 else 'FGC',
'LT65' if disabled > 0 else 'GE65',
graft_duration,
'FBD' if full_partial_nondual == 'F' else 'ND_PBD',
])
if full_partial_nondual == 'P' and not 'NE' in elig:
pbd_factor_variable = '_'.join([
'FGI' if lti > 0 else 'FGC',
'PBD',
'LT65' if disabled > 0 else 'GE65',
'flag'
])

# Actuarial adjustment factor for new enrollees functioning graft model transplant bumps
if elig == 'GNE' and graft_duration:
actadj_variable = 'ActAdj_'
actadj_variable += graft_duration

actadj_factor = coefn.get(actadj_variable, 1.0)

if base_demo_variable:
risk_dct[base_demo_variable] = coefn.get(base_demo_variable, 0.0)

if elig_fbpb:
risk_dct[elig_fbpb] = coefn.get(elig_fbpb, 0.0)

if elig_origds:
risk_dct[elig_origds] = coefn.get(elig_origds, 0.0)

if elig_origds:
risk_dct[elig_origds] = coefn.get(elig_origds, 0.0)

if elig_ltia:
risk_dct[elig_ltia] = coefn.get(elig_ltia, 0.0)

if fgf_variable:
risk_dct[fgf_variable] = coefn.get(fgf_variable, 0.0)

if pbd_factor_variable:
risk_dct[pbd_factor_variable] = coefn.get(pbd_factor_variable, 0.0)


# # build hcc factor strings and add to risk_dict
for hcc in hcc_lst:
elig_hcc = elig + '_' + hcc
risk_dct[elig_hcc] = coefn.get(elig_hcc, 0.0)

return risk_dct, actadj_factor
77 changes: 69 additions & 8 deletions hccpy/hcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import hccpy._V2419P1M as V2419P1M # interactions (v24)
import hccpy._V2823T2M as V2823T2M # interactions (v28)
import hccpy._E2118P1M as E2118P1M # interactions (ESRD)
import hccpy._E2423T1M as E2423T1M # interactions (ESRDv24)
import hccpy._AGESEXV2 as AGESEXV2 # disabled/origds (v22, v23, v24, v28)
import hccpy._V2218O1P as V2218O1P # risk coefn (v22, v23, v24, v28)
import hccpy._E2118P1P as E2118P1P # risk coefn for ESRD

import hccpy._E2423T1P as E2423T1P # risk coefn for ESRDv24

class HCCEngine:

Expand Down Expand Up @@ -43,6 +44,7 @@ def __init__(self,
# Y2023, {"D": 1.034, "G": 1.048}
# Y2024, {"D": 1.022, "G": 1.028}


fnmaps = {
"22": {
"dx2cc": {"2017": "data/F2217O1P.TXT",
Expand Down Expand Up @@ -88,6 +90,14 @@ def __init__(self,
"coefn": "data/ESRDhcccoefn.csv",
"label": "data/V20H87L1.txt",
"hier": "data/V20H87H1.txt"
},
"ESRDv24": {
"dx2cc": {"2023": "data/F2423P1M.TXT",
"Combined": "data/F2423P1M.TXT"},
"coefn": "data/D2423T2M.csv",
"label": "data/V24H86L1.TXT",
"label_short": "data/V24_label_short.json",
"hier": "data/V24H86H1.TXT"
}
}

Expand Down Expand Up @@ -140,6 +150,8 @@ def _apply_interactions(self, cc_lst, age, disabled):
cc_lst = V2823T2M.create_interactions(cc_lst, disabled, age)
elif self.version == "ESRDv21":
cc_lst = E2118P1M.create_interactions(cc_lst, disabled, age)
elif self.version == "ESRDv24":
cc_lst = E2423T1M.create_interactions(cc_lst, disabled, age)

return cc_lst

Expand All @@ -155,7 +167,8 @@ def _sexmap(self, sex):
return sex

def profile(self, dx_lst, age=70, sex="M",
elig="CNA", orec="0", medicaid=False):
elig="CNA", orec="0", medicaid=False,
full_partial_nondual='N', lti=0, graft_duration=""):
"""Returns the HCC risk profile of a given patient information.
Parameters
Expand All @@ -178,6 +191,17 @@ def profile(self, dx_lst, age=70, sex="M",
- "INS": Long Term Institutional
- "NE": New Enrollee
- "SNPNE": SNP NE
The following are allowed values related to the ESRDv24 model
and require the full_partial_nondual, lti and graft_duration
fields to also be populated:
- "DNE": Dialysis New Enrollee
- "GI": Institutional Post Graft
- "GNE": New Enrollee Post Graft
- "GF": Community Post Graft Full Dual
- "GNP": Community Post Graft Non-Dual or Partial Dual
- "DI": Dialysis
- "TRANSPLANT_KIDNEY_ONLY_1M": Transplant First Month
- "TRANSPLANT_KIDNEY_ONLY_2M": Transplant Second/Third Month
orec: str
Original reason for entitlement code.
- "0": Old age and survivor's insurance
Expand All @@ -186,16 +210,32 @@ def profile(self, dx_lst, age=70, sex="M",
- "3": Both DIB and ESRD
medicaid: bool
If the patient is in Medicaid or not.
full_partial_nondual: string
Full, Partial or Non-dual Eligibility status of the patient.
Used for Dual status interactions with Age/Sex for ESRDv24
dialysis model and "bump up factors" for ESRDv24 Graft models;
{"F", "P", "N"}
lti: int
LTI status of the patient. Used to determine LTI interaction
with Aged status for ESRDv24 dialysis model; {0, 1}
graft_duration : str
ESRDv24 post-graft duration band of the patient; Used
for ESRD Functioning Graft Factor (FGF) determination
- "": Not on Post-Graft Model
- "DUR4_9": 4-9 Months Post-Graft
- "DUR10PL": 10+ Months Post-Graft
"""

sex = self._sexmap(sex)
disabled, origds, elig = AGESEXV2.get_ds(age, orec, medicaid, elig)
disabled, origds, origesrd, elig = AGESEXV2.get_ds(age, orec, medicaid, elig)

dx_set = {dx.strip().upper().replace(".","") for dx in dx_lst}
cc_dct = {dx:self.dx2cc[dx] for dx in dx_set if dx in self.dx2cc}
if self.version == "28":
cc_dct = V28I0ED1.apply_agesex_edits(cc_dct, age, sex)
cc_dct = V28I0ED1.apply_agesex_edits(cc_dct, age, sex)
elif self.version == "ESRDv24":
# gets taken care of in E2423T1P.get_risk_dct
pass
else:
cc_dct = V22I0ED2.apply_agesex_edits(cc_dct, age, sex)
hcc_lst = self._apply_hierarchy(cc_dct, age, sex)
Expand All @@ -209,8 +249,15 @@ def profile(self, dx_lst, age=70, sex="M",
if "ESRD" not in self.version:
risk_dct = V2218O1P.get_risk_dct(self.coefn, hcc_lst, age,
sex, elig, origds, medicaid)
else:

elif self.version == "ESRDv21":
risk_dct = E2118P1P.get_risk_dct(self.coefn, hcc_lst, age, sex)
elif self.version == "ESRDv24":
# this model can get a different adj factor for NE FG
risk_dct, adj_factor = E2423T1P.get_risk_dct(self.coefn, hcc_lst, age,
sex, elig, origds, origesrd,
full_partial_nondual, disabled, lti, graft_duration)


score = round(np.sum([x for x in risk_dct.values()]), 4)

Expand All @@ -220,13 +267,27 @@ def profile(self, dx_lst, age=70, sex="M",

nf = 1 # normalization factor
if "ESRD" in self.version:
# We assume elig for ESRD is one of
# "DI", "GC", "GI", "DNE", "GNE"
nf = self.norm_params[elig[0]]
# Assume elig for ESRD is one of
# "DI", "GC", "GI", "DNE", "GNE", "TRANSPLANT_KIDNEY_ONLY_XM"
# See E2424T2M: transplant model gets Dialysis normalization factor
if elig in {"TRANSPLANT_KIDNEY_ONLY_1M",
"TRANSPLANT_KIDNEY_ONLY_2M",
"TRANSPLANT_KIDNEY_ONLY_3M"}:
nf = self.norm_params["D"]
else:
nf = self.norm_params[elig[0]]
else:
nf = self.norm_params["C"]

cif = self.cif
if elig in {"DI", "DNE",
"TRANSPLANT_KIDNEY_ONLY_1M",
"TRANSPLANT_KIDNEY_ONLY_2M",
"TRANSPLANT_KIDNEY_ONLY_3M"} and self.version == 'ESRDv24':
# these model types do not have a CIF applied
cif = 0.0



# https://csscoperations.com/internet/csscw3.nsf/RiskAdjustmentMethodologyTranscript.pdf
# see page 1-11
Expand Down

0 comments on commit 51e4aeb

Please sign in to comment.