Transplant Cost Analysis

1 Study Details

1.1 Background

Tacrolimus is an immunosuppressant used to prevent organ rejection in kidney transplant recipients. Tacrolimus can be dosed using twice-daily immediate release capsules (Prograf, IR-Tac) or once-daily extended-release tablets (Envarsus XR, LCPT). LCPT has been shown to have a better side effect profile than IR-Tac but may be associated with higher costs since it is only available as a brand name medication. It can be more time consuming for pharmacy and clinical staff to navigate payment barriers since some insurance companies are hesitant to approve LCPT due to higher costs compared to IR-Tac, which frequently leads to prescriber hesitancy to change a patient to LCPT.

1.2 Methods

This was a single-center, retrospective review of pharmacy records and billing claims records from 1/1/2021-7/01/2022. Kidney transplant recipients who filled prescriptions written for IR-Tac or LCPT from Vanderbilt Transplant Pharmacy and were written by a provider at the Vanderbilt adult renal transplant clinic were included. Patients below 18 years of age and fills with MAP primary insurance plans were excluded.

1.3 Outcomes of interest

  • OOP costs compared between Envarsus and IR-Tac while adjusting for age, race, gender, and primary insurance

2 Descriptive Statistics

2.1 Patient level characteristics

tab<- summaryM(Age_first_sold_date + gender + race + ethnicity+ number_fills  + total_member_paid_scaled +
                primary_ins_cat + secondary_ins_cat+
                 drug_order ~ drugs , data=erx_pat, na.include=T,overall=T, 
               continuous = 1)

Hmisc::html(tab,na.include=TRUE,long=T,exclude1=F, digits=2,prmsd=T,
            caption= "Patient level characteristics")
Patient level characteristics.
TACROLIMUS
N=1314
ENVARSUS
N=134
Both
N=39
Combined
N=1487
Age at first sold date 42 54 63  (53 ±13) 43 52 65  (53 ±13) 49 55 67  (57 ±12) 43 54 63  (53 ±13)
Gender
    Female 0.45 (596) 0.44 ( 59) 0.49 ( 19) 0.45 (674)
    Male 0.55 (718) 0.56 ( 75) 0.51 ( 20) 0.55 (813)
Race
    White 0.59 (778) 0.36 ( 48) 0.62 ( 24) 0.57 (850)
    Black or African American 0.35 (463) 0.61 ( 82) 0.33 ( 13) 0.38 (558)
    Other 0.05 ( 65) 0.02 ( 3) 0.05 ( 2) 0.05 ( 70)
    NA 0.01 ( 8) 0.01 ( 1) 0.00 ( 0) 0.01 ( 9)
Ethnicity
    Mexican, Mexican American, or Chicano/a 0.01 ( 8) 0.01 ( 2) 0.03 ( 1) 0.01 ( 11)
    Not Hispanic, Latino/a, or Spanish origin 0.95 (1242) 0.95 ( 127) 0.95 ( 37) 0.95 (1406)
    Other Hispanic, Latino/a, or Spanish origin 0.03 ( 39) 0.01 ( 1) 0.03 ( 1) 0.03 ( 41)
    Puerto Rican 0.00 ( 1) 0.01 ( 1) 0.00 ( 0) 0.00 ( 2)
    NA 0.02 ( 24) 0.02 ( 3) 0.00 ( 0) 0.02 ( 27)
Number of fills 6.0 11.0 17.0  (12.2 ± 8.2) 6.0 14.0 18.0  (13.9 ± 8.9) 8.0 12.0 15.5  (12.0 ± 5.7) 6.0 12.0 18.0  (12.4 ± 8.3)
Total member paid scaled by number of fills 0.0 0.0 10.0  (10.4 ±26.4) 0.0 0.0 0.0  ( 6.4 ±31.0) 0.0 0.0 11.5  ( 8.1 ±14.0) 0.0 0.0 9.2  (10.0 ±26.6)
Most often filled primary insurance
    Commercial 0.26 ( 346) 0.21 ( 28) 0.23 ( 9) 0.26 ( 383)
    Medicaid 0.04 ( 48) 0.05 ( 7) 0.00 ( 0) 0.04 ( 55)
    Medicare 0.70 ( 915) 0.74 ( 99) 0.74 ( 29) 0.70 (1043)
    Tricare 0.00 ( 5) 0.00 ( 0) 0.03 ( 1) 0.00 ( 6)
Fills with any secondary insurance
    All 0.18 (243) 0.18 ( 24) 0.00 ( 0) 0.18 (267)
    Some 0.31 (402) 0.41 ( 55) 0.54 ( 21) 0.32 (478)
    None 0.51 (669) 0.41 ( 55) 0.46 ( 18) 0.50 (742)
Order of drugs
    TAC to ENV 0.00 ( 0) 0.00 ( 0) 1.00 ( 39) 0.03 ( 39)
    NA 1.00 (1314) 1.00 ( 134) 0.00 ( 0) 0.97 (1448)
a b c represent the lower quartile a, the median b, and the upper quartile c for continuous variables. x ± s represents X ± 1 SD. Numbers after proportions are frequencies.

2.2 Fill Level Characteristics

tab<- summaryM(primary_plan_cat +
  secondary_plan +
  member_paid +
  primary_paid +
  secondary_paid +
  total_paid ~ drug , data=erx, na.include=T,overall=F, 
               continuous = 1)

Hmisc::html(tab,na.include=TRUE,long=T,exclude1=F, digits=2,prmsd=T,
            caption= "Fill level characteristics")
Fill level characteristics.
N
TACROLIMUS
N=16262
ENVARSUS
N=2149
Primary insurance plan 18411
    Commercial 0.25 ( 4008) 0.13 ( 269)
    Medicaid 0.03 ( 488) 0.04 ( 81)
    Medicare 0.72 (11714) 0.83 ( 1785)
    Tricare 0.00 ( 52) 0.01 ( 14)
Secondary insurance plan 18411
    CAID 0.02 ( 404) 0.04 ( 91)
    COCH 0.18 ( 3005) 0.20 ( 430)
    COFD 0.00 ( 5) 0.00 ( 0)
    COOP/BCBS 0.05 ( 805) 0.07 ( 146)
    COSP 0.00 ( 6) 0.00 ( 0)
    GOV 0.00 ( 63) 0.02 ( 33)
    MCPB 0.04 ( 664) 0.00 ( 0)
    MCRX 0.00 ( 24) 0.00 ( 2)
    NA 0.69 (11286) 0.67 ( 1447)
Member paid 18411 0.0 0.0 3.9  ( 8.1 ±26.2) 0.0 0.0 0.0  ( 5.2 ±33.0)
Primary paid 18411 33 57 112  (101 ±150) 281 531 927  (769 ±820)
Secondary paid 5664 10 17 34  ( 36 ± 75) 41 109 235  (181 ±214)
Total paid 18411 43 69 126  ( 120 ± 168) 289 545 1033  ( 833 ± 899)
a b c represent the lower quartile a, the median b, and the upper quartile c for continuous variables. x ± s represents X ± 1 SD.   N is the number of non-missing values. Numbers after proportions are frequencies.

2.3 Costs by insurance category

2.3.1 Mean costs

erx$map_cat <- ifelse(is.na(erx$secondary_insurance), "Primary Insurance Only",
               ifelse(erx$MAP_applied == "Yes", "Primary Insurance + MAP", "Primary Insurance + Non-MAP"))
erx$map_cat <- factor(erx$map_cat, levels=c("Primary Insurance Only",
                                            "Primary Insurance + MAP", 
                                            "Primary Insurance + Non-MAP"))
print_res_mean <- function(x,r){
  paste0(round(mean(x),r), " (" ,round(sd(x),r),")")
}
print_res_iqr <- function(x,r){
  paste0(round(median(x),r), " (" ,round(quantile(x)[2],r),", ",round(quantile(x)[4],r),")")
}


res <- as.data.frame(matrix(NA, ncol=4, nrow=3))
for(i in 1:lun(erx$map_cat)){
  for(j in 1:4){
    map_cat <- unique(erx$map_cat)[i]
    cost <- c("member_paid","primary_paid", "secondary_paid","total_paid")[j]
    tmp <- erx[erx$map_cat == map_cat,cost]
    if(mean(is.na(tmp))==1){
        res[i,j] <-""
      }else{
        res[i,j] <- print_res_mean(tmp,2)
      }
  }
}
  rownames(res) <- unique(erx$map_cat)
  colnames(res) <- c("Member Paid","Primary Paid", "Secondary Paid","Total Paid")
  #res[3,3] <- ""
  res$n <- NA
  for(i in 1:3){
    res$n[i] <- sum(erx$map_cat == unique(erx$map_cat)[i])
  }
overall <- res

drugs <- c("TACROLIMUS" , "ENVARSUS")
list_res <- list()
for(k in 1:2){
  res <- as.data.frame(matrix(NA, ncol=4, nrow=3))
  for(i in 1:lun(erx$map_cat)){
    for(j in 1:4){
      drug <- drugs[k]
      map_cat <- unique(erx$map_cat)[i]
      cost <- c("member_paid","primary_paid", "secondary_paid","total_paid")[j]
      tmp <- erx[erx$map_cat == map_cat & erx$drug == drug,cost]
      if(mean(is.na(tmp))==1){
        res[i,j] <-""
      }else{
        res[i,j] <- print_res_mean(tmp,2)
      }
    }
  }
  rownames(res) <- unique(erx$map_cat)
  colnames(res) <- c("Member Paid","Primary Paid", "Secondary Paid","Total Paid")
  #res[3,3] <- ""
  res$n <- NA
  for(i in 1:3){
    res$n[i] <- sum(erx$map_cat == unique(erx$map_cat)[i] & erx$drug == drug)
  }
  list_res[[k]] <- res
}
names(list_res) <- drugs

all_res <- rbind(overall, list_res[[1]])
all_res <- rbind(all_res, list_res[[2]])
all_res$`Insurance Category` <- rep(c("Primary Insurance + MAP", "Primary Insurance + Non-MAP", "Primary Insurance Only"),3)
rownames(all_res) <- NULL
all_res %>% 
  select(`Insurance Category`, `Member Paid`, `Primary Paid`, `Secondary Paid`,  n) %>% 
  kable(caption="Mean (SD) costs of fills by insurance category and drug") %>% 
  pack_rows("Overall", 1,3) %>%
  pack_rows("TACROLIMUS",4,6) %>%
  pack_rows("ENVARSUS",7,9) %>%
  kable_classic_2()
Table 2.1: Mean (SD) costs of fills by insurance category and drug
Insurance Category Member Paid Primary Paid Secondary Paid n
Overall
Primary Insurance + MAP 0 (0) 197.84 (445.69) 53.42 (114.84) 3423
Primary Insurance + Non-MAP 0.01 (0.33) 151.39 (341.34) 55.56 (110.49) 2241
Primary Insurance Only 11.22 (31.93) 179.05 (366.66) 12747
TACROLIMUS
Primary Insurance + MAP 0 (0) 106.2 (177.85) 32.74 (59.84) 2996
Primary Insurance + Non-MAP 0.01 (0.36) 84.58 (111.22) 42.1 (92.63) 1969
Primary Insurance Only 11.67 (30.73) 102.85 (148.24) 11297
ENVARSUS
Primary Insurance + MAP 0 (0) 840.84 (948.56) 198.53 (238.03) 427
Primary Insurance + Non-MAP 0 (0) 634.98 (778.51) 152.96 (166.59) 272
Primary Insurance Only 7.66 (39.9) 772.73 (783.13) 1450

2.3.2 Median costs

res <- as.data.frame(matrix(NA, ncol=4, nrow=3))
for(i in 1:lun(erx$map_cat)){
  for(j in 1:4){
    map_cat <- unique(erx$map_cat)[i]
    cost <- c("member_paid","primary_paid", "secondary_paid","total_paid")[j]
    tmp <- erx[erx$map_cat == map_cat,cost]
    if(mean(is.na(tmp))==1){
        res[i,j] <-""
      }else{
        res[i,j] <-  print_res_iqr(tmp,2)
      }
  }
}
  rownames(res) <- unique(erx$map_cat)
  colnames(res) <- c("Member Paid","Primary Paid", "Secondary Paid","Total Paid")
  #res[3,3] <- ""
  res$n <- NA
  for(i in 1:3){
    res$n[i] <- sum(erx$map_cat == unique(erx$map_cat)[i])
  }
overall <- res

drugs <- c("TACROLIMUS" , "ENVARSUS")
list_res <- list()
for(k in 1:2){
  res <- as.data.frame(matrix(NA, ncol=4, nrow=3))
  for(i in 1:lun(erx$map_cat)){
    for(j in 1:4){
      drug <- drugs[k]
      map_cat <- unique(erx$map_cat)[i]
      cost <- c("member_paid","primary_paid", "secondary_paid","total_paid")[j]
      tmp <- erx[erx$map_cat == map_cat & erx$drug == drug,cost]
      if(mean(is.na(tmp))==1){
        res[i,j] <-""
      }else{
        res[i,j] <- print_res_iqr(tmp,2)
      }
    }
  }
  rownames(res) <- unique(erx$map_cat)
  colnames(res) <- c("Member Paid","Primary Paid", "Secondary Paid","Total Paid")
  #res[3,3] <- ""
  res$n <- NA
  for(i in 1:3){
    res$n[i] <- sum(erx$map_cat == unique(erx$map_cat)[i] & erx$drug == drug)
  }
  list_res[[k]] <- res
}
names(list_res) <- drugs

all_res <- rbind(overall, list_res[[1]])
all_res <- rbind(all_res, list_res[[2]])
all_res$`Insurance Category` <- rep(c("Primary Insurance + MAP", "Primary Insurance + Non-MAP", "Primary Insurance Only"),3)
rownames(all_res) <- NULL
all_res %>% 
  select(`Insurance Category`, `Member Paid`, `Primary Paid`, `Secondary Paid`, n) %>% 
  kable(caption="Median (IQR) costs of fills by insurance category and drug") %>% 
  pack_rows("Overall", 1,3) %>%
  pack_rows("TACROLIMUS",4,6) %>%
  pack_rows("ENVARSUS",7,9) %>%
  kable_classic_2()
Table 2.2: Median (IQR) costs of fills by insurance category and drug
Insurance Category Member Paid Primary Paid Secondary Paid n
Overall
Primary Insurance + MAP 0 (0, 0) 63.24 (33.59, 149.12) 16.86 (9.43, 47.16) 3423
Primary Insurance + Non-MAP 0 (0, 0) 60.17 (32.86, 135.65) 22.64 (11.63, 49.86) 2241
Primary Insurance Only 0 (0, 10) 70 (39.1, 152.35) 12747
TACROLIMUS
Primary Insurance + MAP 0 (0, 0) 50.14 (32.44, 105.36) 15.66 (8.6, 33.91) 2996
Primary Insurance + Non-MAP 0 (0, 0) 46.51 (32.44, 101.14) 18.46 (11.39, 34.08) 1969
Primary Insurance Only 0 (0, 11.04) 58.14 (36.36, 115.59) 11297
ENVARSUS
Primary Insurance + MAP 0 (0, 0) 449.12 (286.17, 1133.36) 110.71 (69.39, 283.34) 427
Primary Insurance + Non-MAP 0 (0, 0) 421.76 (163.1, 602.9) 105.44 (39.94, 151.92) 272
Primary Insurance Only 0 (0, 0) 544.84 (288.42, 925) 1450

3 Regression analysis of OOP costs

  • The distribution of member paid is extremely skewed and has many values of 0, making it difficult to model with ordinary least squares regression so an ordinal regression model is used.

  • A mixed effects model would be ideal to account for correlation within subjects, but an abundance of repeating values of the outcome (and over-saturation of 0s) caused the the model to have difficulties converging.

  • Instead the total OOP costs per patient scaled by the number of fills was used as the outcome (mean OOP cost) for an ordinal logistic regression model. And ordinal model was used to account for the severe skew in the distribution, which would violate the assumptions of ordinary least squares regression.

  • Every fill where a patient has secondary insurance results in an OOP expense of 0 so it was excluded from the model.

erx_pat$total_member_paid_scaled <- erx_pat$total_member_paid/erx_pat$number_fills
ddd <- datadist(erx_pat[,c("total_member_paid_scaled","total_member_paid", "race", "gender",
                      "Age_first_sold_date","number_fills","drugs",
                      "mult_strength", "primary_ins_cat", "secondary_ins_cat")])
options(datadist="ddd")         #often a good idea, to store info with fit
fit <- orm(total_member_paid_scaled ~ drugs + I(Age_first_sold_date/10)  +race +gender+ primary_ins_cat , data=erx_pat)
res <- data.frame(Variable = grep('y>',names(coef(fit)),inv=T, v = T), 
             lor = coef(fit)[grep('y>',names(coef(fit)),inv=T, v = T)], 
             rse =sqrt(diag(vcov(fit)))[grep('y>',names(coef(fit)),inv=T, v = T)]) %>% 
    mutate(lower = (lor - 1.96*rse), upper = (lor + 1.96*rse), 
           `p-value` = format.pval( round(pchisq((lor/rse)^2,1, lower.tail = FALSE),4), digits = 3, eps = 0.001)) %>% 
    mutate(`Log Odds` = lor, `Odds` = exp(lor), Lower = exp(lower), Upper = exp(upper)) %>% 
    select(Variable, `Log Odds`,`Odds`, Lower, Upper, `p-value`)

res$Variable <- factor(res$Variable,
                        levels=res$Variable,
                        labels=c("Envarsus",
                                 "Both medications", 
                                 "at first sold date and by 10 years", 
                                 "Black or African American",
                                 "Other",
                                 "Male",
                                 "Medicaid ",
                                 "Medicare ",
                                 "Tricare "
                            
                                 ))

rownames(res) <- NULL
hold <- res
res$Visualization <- ""
res$odds_numeric <- res$Odds
res$Odds <- paste0(round(res$Odds, 2), " (", round(res$Lower, 2)," ,", round(res$Upper, 2),")")

res %>%
  select(1, 3,6, Visualization) %>%
  rename("Odds Ratio (95% CI)" = Odds) %>%
  #arrange(Variable) %>%
  kbl(booktabs = T) %>%
  kable_classic(full_width = T ) %>%
  pack_rows("Drug (Tacrolimus reference)", 1,2) %>%
  pack_rows("Age",3,3)%>%
    pack_rows("Race (White reference)",4,5)%>%
 pack_rows("Gender (Female reference)",6,6)%>%
  pack_rows("Primary Insurance (Commercial reference)", 7, 9) %>%
  column_spec(4, image=spec_pointrange(x=res$odds_numeric, xmin=res$Lower, xmax=res$Upper, vline=1)) 
Variable Odds Ratio (95% CI) p-value Visualization
Drug (Tacrolimus reference)
Envarsus 0.16 (0.09 ,0.3) <0.001
Both medications 1.19 (0.63 ,2.24) 0.589
Age
at first sold date and by 10 years 0.98 (0.9 ,1.07) 0.713
Race (White reference)
Black or African American 1.08 (0.86 ,1.36) 0.517
Other 1.23 (0.76 ,2) 0.406
Gender (Female reference)
Male 1.26 (1.02 ,1.57) 0.036
Primary Insurance (Commercial reference)
Medicaid 0.22 (0.13 ,0.38) <0.001
Medicare 0.17 (0.14 ,0.22) <0.001
Tricare 2.5 (0.76 ,8.26) 0.133