---
title: "How to implement a rescaled random-effects excess hazard regression model to handle situations involving inappropriate life tables."
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{How to implement a rescaled random-effects excess hazard regression model to handle situations involving inappropriate life tables}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
options(rmarkdown.html_vignette.check_title = FALSE)
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(xhaz)
```


# Introduction

xhaz is an R package for fitting excess hazard regression models, with or without the assumption of proportional hazards. In the presence of hierarchical data, inter-cluster heterogeneity can affect excess hazard estimates. To address this issue, the mexhaz R package implements multilevel excess hazard regression models. If the user wants to check the validity of the non-comparability bias, an easy way is to use the mexhazLT function of the xhaz R package. 
The user can also specify whether the framework is consistent with classical excess hazard modeling, 
i.e., assuming that the expected mortality of the individuals studied is appropriate, using the pophaz argument is equivalent to classical.
The user can also consider a different framework by specifying that pophaz equals rescaled: the expected mortality available in the life table is not accurate, i.e., there is a non-comparability source of bias with respect to the expected mortality of the study population [Goungounga et al. (2023)](https://pubmed.ncbi.nlm.nih.gov/36890623/).



(*Here's the abstract from Goungounga et al. (2023) paper:* In the presence of competing causes of event occurrence (e.g., death), the interest might not only be in the overall survival but also in the so-called net survival, that is, the hypothetical survival that would be observed if the disease under study were the only possible cause of death. Net survival estimation is commonly based on the excess hazard approach in which the hazard rate of individuals is assumed to be the sum of a disease-specific and expected hazard rate, supposed to be correctly approximated by the mortality rates obtained from general population life tables...

A related software package can be found at <https://CRAN.R-project.org/package=xhaz>.

## Installation

The most recent version of `xhaz` can be installed directly from the cran repository using


```{r, eval=FALSE, echo=TRUE}
install.packages("xhaz")

```


`xhaz` depends on the `stats`, `survival`, `optimParallel`, `numDeriv`, `statmod`, `gtools`  and `splines` packages which can be installed directly from CRAN.

It also utilizes `survexp.fr`, the R package containing the French life table. For example, to install `survexp.fr` follow the instructions available at the [RStudio page on R and survexp.fr](https://cran.r-project.org/package=survexp.fr).

First, install the R package via github.

```{r, eval=FALSE}
install.packages("survexp.fr")
```


Then, when these other packages are installed, please load the xhaz R package.

```{r}
library(survival)
library(xhaz)
```




### Fitting an classical excess hazard regression model with a cubic B-splines for the baseline hazard as in mexhaz

For this illustration, we used a simulated dataset from the original paper by Goungounga et al. (2023). This dataset consists of 4,978 patients with information on their treatment arm and clinical center ID, as this information may affect the excess mortality rate. The US life table can be used to estimate the model parameters. mexhazLT() function will be used with argument (````pophaz = classic```).



```{r}
library(survexp.fr)
data("breast", package = "xhaz")

head(breast)
dim(breast)

# armt: numbers and percentages
breast$armt2 <- factor(breast$armt, levels = c("0","1"))
table(breast$armt2)

# The life table to be used is survexp.us. Note that SEX is coded 2 instead of female in survexp.us.
breast$sexe <- "female"

fit.haz <- exphaz(
               formula = Surv(temps, statut) ~ 1,
               data = breast, ratetable = survexp.us,
               only_ehazard = FALSE,
               rmap = list(age = 'age', sex = 'sexe',
                           year = 'date'))



breast$expected <- fit.haz$ehazard
breast$expectedCum <- fit.haz$ehazardInt


qknots <- quantile(breast[breast$statut==1,]$temps, probs=c(1:2/3))
mod.bs <- mexhazLT(
  formula = Surv(temps, statut) ~ agecr + armt2,
  data = breast, degree = 3,
  knots = qknots, expected = "expected", 
  expectedCum = "expectedCum",
  base = "exp.bs", pophaz = "classic")
                  
mod.bs
```



### Fitting a rescaled excess hazard regression model with a cubic B-splines for the baseline hazard as in mexhaz

The new parameter to be added to `mexhazLT()` function is `pophaz="rescaled"`: it allows to rescale the life table available for the estimation of the excess hazard parameters. This model concerns that proposed by Goungounga et al. (2016).


```{r}
mod.bs2 <- mexhazLT(
  formula = Surv(temps, statut) ~ agecr + armt2,
  data = breast, degree = 3,
  knots = qknots, expected = "expected",
  expectedCum = "expectedCum",
  base = "exp.bs", pophaz = "rescaled")
                  
mod.bs2
```



### Fitting a random-effects excess hazard regression model with a cubic B-splines for the baseline hazard as in mexhaz

As in `mexhaz`, the new parameter to be added to the `mexhazLT()` function is `random="hosp"`: it allows to specify the variable indicating the cluster levels. However, `pophaz` argument is set to be equal to `"classic"`. This excess hazard regression model is the one proposed by Charvat et al (2016). In the output, `loglik` corresponds to the total log-likelihood including the sum of the cumulative expected hazards, which is often removed in classical excess hazard regression models because it is considered a nuisance parameter. 

```{r}

mod.bs3 <- mexhazLT(
  formula = Surv(temps, statut) ~ agecr + armt2,
  data = breast, degree = 3,
  knots = qknots, expected = "expected",
  expectedCum = "expectedCum",
  base = "exp.bs", pophaz = "classic", random = "hosp")
                  
mod.bs3
```



### Fitting a rescaled random-effects excess hazard regression model with a cubic B-splines for the baseline hazard as in mexhaz

As in mexhaz, the new parameter to be added to the `mexhazLT()` function is `random="hosp"`: it allows to specify the variable indicating the cluster levels. However, pophaz is set to be equal to "rescaled" (````pophaz = "rescaled"```). This excess hazard regression model is the one proposed by Goungounga et al (2023). 

```{r}

mod.bs4 <- mexhazLT(
  formula = Surv(temps, statut) ~ agecr + armt2,
  data = breast, degree = 3,
  knots = qknots, expected = "expected", 
  expectedCum = "expectedCum",
  base = "exp.bs", pophaz = "rescaled", random = "hosp")
                  
mod.bs4
```

We can compare the output of these two models using AIC or BIC criteria.


```{r}
compared_models <- list(mod.bs,mod.bs2, mod.bs3, mod.bs4)
names(compared_models) <- c("mod.bs","mod.bs2", "mod.bs3", "mod.bs4")

sapply(compared_models, function(i) {
  AIC(i)
})

```

A statistical comparison between two nested models can be performed with a likelihood ratio test calculated by function anova method implemented in xhaz. 

For example, suppose we want to test whether we can drop the rescaling parameter between the different excess hazard regression models.

As in survival package, we compare the models using anova(), i.e.,


```{r}
anova(mod.bs,mod.bs2)
anova(mod.bs3,mod.bs4)
```


Note that it is the user's responsibility to provide properly nested hazard models for the LRT to be valid.
The results suggest that by correcting for between-cluster heterogeneity and non-comparability bias with a rescaled random effects excess hazard regression model, the user will be able to provide more accurate estimates of net survival.

### Plot of net survival and excess hazard for different models

One could be interested in the prediction of net survival and excess hazard for the individual with the same characteristics as individual 1 in the breast dataset (age 30.95, armt equals 0) as performed in mexhaz.

```{r}
# hazard and survival in the two arms
predict_mod_amr0 <- predict(mod.bs,
                          time.pts=seq(0.1,10,by=0.1),
                          data.val=data.frame(agecr = 0,
                                              armt2 = "0"))

predict_mod_amr1 <- predict(mod.bs,
                          time.pts=seq(0.1,10,by=0.1),
                          data.val=data.frame(agecr = 0,
                                              armt2 = "1"))





predict_mod2_arm0 <- predict(mod.bs2,
                           time.pts=seq(0.1,10,by=0.1),
                           data.val=data.frame(agecr = 0,
                                               armt2 ="0"))
predict_mod2_arm1 <- predict(mod.bs2,
                           time.pts=seq(0.1,10,by=0.1),
                           data.val=data.frame(agecr = 0,
                                               armt2 ="1"))



predict_mod3_arm0 <- predict(mod.bs3,
                        time.pts=seq(0.1,10,by=0.1),
                        data.val=data.frame(agecr = 0,
                                            armt2 =  "0"),
                        marginal = TRUE)

predict_mod3_arm1 <- predict(mod.bs3,
                           time.pts=seq(0.1,10,by=0.1),
                           data.val=data.frame(agecr = 0,
                                               armt2 = "1"),
                           marginal = TRUE)



predict_mod4_arm0 <- predict(mod.bs4,
                        time.pts=seq(0.1,10,by=0.1),
                        data.val=data.frame(agecr = 0,
                                            armt2 =  "0"),
                        marginal = TRUE)

predict_mod4_arm1 <- predict(mod.bs4,
                        time.pts=seq(0.1,10,by=0.1),
                        data.val=data.frame(agecr = 0,
                                            armt2 =  "1"),
                        marginal = TRUE)

```

The graphs can be obtained as follows:


```{r, fig.width=10, fig.height=10}
old.par <- par(no.readonly = TRUE)
on.exit({ layout(1); par(old.par) })

## ----- 1) Make a 2-row layout: top row = legend, bottom row = 2 plots -----
layout(
  matrix(c(1,1,  2,3), nrow = 2, byrow = TRUE),   # top spans both columns
  heights = c(1.2, 8)                              # adjust top strip height
)

## ----- 2) TOP STRIP: shared model legend (outside the plots) -----
par(mar = c(0, 0, 0, 0))
plot.new()
legend("center", bty = "n", horiz = TRUE, title = "Model",
       legend = c("mod.bs", "mod.bs2", "mod.bs3", "mod.bs4"),
       lty = 1, lwd = 2, col = c("black","blue","red","green"), cex = 1)

## ----- 3) LEFT PANEL -----
par(mar = c(4, 4, 2, 1))
plot(predict_mod_amr0$results$time.pts, predict_mod_amr0$results$hazard,
     type = "l", lwd = 2, xlab = "Time (years)", ylab = "Excess hazard",
     ylim = c(0, 0.5))
lines(predict_mod_amr1$results$time.pts, predict_mod_amr1$results$hazard, lwd = 2, lty = 2)
lines(predict_mod2_arm0$results$time.pts, predict_mod2_arm0$results$hazard, lwd = 2, col = "blue",  lty = 1)
lines(predict_mod2_arm1$results$time.pts, predict_mod2_arm1$results$hazard, lwd = 2, col = "blue",  lty = 2)
lines(predict_mod3_arm0$results$time.pts, predict_mod3_arm0$results$hazard, lwd = 2, col = "red",   lty = 1)
lines(predict_mod3_arm1$results$time.pts, predict_mod3_arm1$results$hazard, lwd = 2, col = "red",   lty = 2)
lines(predict_mod4_arm0$results$time.pts, predict_mod4_arm0$results$hazard, lwd = 2, col = "green", lty = 1)
lines(predict_mod4_arm1$results$time.pts, predict_mod4_arm1$results$hazard, lwd = 2, col = "green", lty = 2)
grid()
legend("topright", bty = "n", title = "Treatment arm",
       legend = c("No immunotherapy","Immunotherapy"),
       lty = c(1,2), lwd = 2, col = "black", cex = 0.9)

## ----- 4) RIGHT PANEL -----
par(mar = c(4, 4, 2, 1))
plot(predict_mod_amr0$results$time.pts, predict_mod_amr0$results$surv,
     type = "l", lwd = 2, xlab = "Time (years)", ylab = "Net survival",
     ylim = c(0, 1))
lines(predict_mod_amr1$results$time.pts, predict_mod_amr1$results$surv, lwd = 2, lty = 2)
lines(predict_mod2_arm0$results$time.pts, predict_mod2_arm0$results$surv, lwd = 2, col = "blue",  lty = 1)
lines(predict_mod2_arm1$results$time.pts, predict_mod2_arm1$results$surv, lwd = 2, col = "blue",  lty = 2)
lines(predict_mod3_arm0$results$time.pts, predict_mod3_arm0$results$surv, lwd = 2, col = "red",   lty = 1)
lines(predict_mod3_arm1$results$time.pts, predict_mod3_arm1$results$surv, lwd = 2, col = "red",   lty = 2)
lines(predict_mod4_arm0$results$time.pts, predict_mod4_arm0$results$surv, lwd = 2, col = "green", lty = 1)
lines(predict_mod4_arm1$results$time.pts, predict_mod4_arm1$results$surv, lwd = 2, col = "green", lty = 2)
grid()
legend("topright", bty = "n", title = "Treatment arm",
       legend = c("No immunotherapy","Immunotherapy"),
       lty = c(1,2), lwd = 2, col = "black", cex = 0.9)
```


## License

GPL 3.0, for academic use.

## Acknowledgments

We are grateful to the members of the CENSUR Survival Group for their helpful comments.


## Session Info

```{r}
sessionInfo()
```
