The do file with all the code from this tutorial is available to download here
This tutorial will illustrate some of the more advanced capabilities of merlin
when modelling survival data,
but hopefully with a rather simple example. In some previous work, Paul Lambert and I developed stgenreg
for modelling
survival data with a general user-specified hazard function, which is then estimated using numerical integration. Before continuing
with this example, please look through the accompanying material provided with stgenreg
; the methods paper can be found
here and a software tutorial can be found here.
Consider our standard proportional hazards model,
$$ h(t) = h_{0}(t) \exp (X \beta)$$
Within a general hazard model, we can essentially specify any function for our baseline hazard function, subject to the constraint that $h_{0}(t)>0$ for all $t>0$. The easiest way to do this is to model on the log hazard scale. Let’s model our baseline log hazard function with fractional polynomials, such as,
$$ \log h_{0}(t) = \gamma_{0} + \gamma_{1} t + \gamma_{2} \log(t)$$
This model can be fitted using stgenreg
, but with the introduction of merlin
, we can do the same as stgenreg
, and a
whole lot more. I’ll use the catheter
dataset to fit some models.
. . webuse catheter, clear
(Kidney data, McGilchrist and Aisbett, Biometrics, 1991)
Our dataset consists of the following,
. . list patient time infect age female in 1/6, noobs
+----------------------------------------+
| patient time infect age female |
|----------------------------------------|
| 1 16 1 28 0 |
| 1 8 1 28 0 |
| 2 13 0 48 1 |
| 2 23 1 48 1 |
| 3 22 1 32 0 |
|----------------------------------------|
| 3 28 1 32 0 |
+----------------------------------------+
with patient
our individual patient identifier, time
is our time of infection at the catheter insertion point, infect
is our event indicator with an event being an infection, age
is patient age at baseline and female
a binary indicator variable. We immediately see that patients can experience multiple infections, and so we have events nested within patients. For now, I will ignore this clustering.
To fit our model with fractional polynomials for our baseline log hazard function, we need to write a little Mata
function which calculates and returns our hazard function. This is really easy to do:
. mata:
------------------------------------------------- mata (type end to exit) ---------------
: real matrix userhaz(transmorphic gml, real colvector t)
> {
> real matrix linpred
> real colvector gammas
>
> linpred = merlin_util_xzb(gml)
> gammas = merlin_util_ap(gml,1)\merlin_util_ap(gml,2)
> return(exp(linpred :+ merlin_fp(t,(0,1)) * gammas))
> }
: end
-----------------------------------------------------------------------------------------
Let’s go through it line by line. First thing to note is that we declare a chunk of Mata
code within
mata:
end
We need to define a function, called whatever we like, in this case I’ll call it userhaz()
, which returns a real matrix
, and it’s going to have
two inputs. The first is a transmorphic
object called gml
. This is the internal struct
which contains all the information
needed by merlin
in the background. You shouldn’t attempt to alter its contents. The second argument is a real colvector
which I’m calling t
.
This represents the vector of time points that we wish to calculate our hazard function at. Internally, our function will be called by merlin
at both
our core time variable, and our quadrature points needed to calculate the cumulative hazard, and therefore this second input is needed.
Next we declare smoe intermediate vectors/matrices that we’ll need. Explicit declaration of each object’s type is good programming practice.
real matrix linpred
real colvector gammas
Now we call our first utility function merlin_util_xzb()
, passing it the gml
structure and also our time vector. This returns our main complex linear predictor,
it’s as simple as that.
linpred = merlin_util_xzb(gml,t)
Because we pass our time vector $t$, any time-dependent effects that we specify in our linear predictor, or calls to the utilities
EV[]
, dEV[]
or iEV[]
etc., which may be time dependent, are automatically taken care of!
We then have two other ancillary parameters to handle, i.e. the coefficients of the fractional polynomial terms, which we extract using
merlin_util_ap(gml,i)
where i
is the ancillary parameter number. In this case we have two extra parameters to estimate, so we build a
column vector called gammas
as follows,
gammas = merlin_util_ap(gml,1)\merlin_util_ap(gml,2)
Finally we need to return
our hazard function, which is done very simply,
return(exp(linpred :+ merlin_fp(t,(1,0)) * gammas))
This makes use of the internal merlin_fp()
function, which returns fractional polynomials, in this case an FP2 function with powers 1 and 0.
In just a few lines of code we have defined our model framework, which can now be used with anything specified in the linear predictor when we fit
our merlin
models. This provides a very powerful modelling framework.
Let’s now fit a model using our userhaz()
function. We can call merlin
as follows,
. merlin (time age female, family(user, hfunc(userhaz) failure(infect) nap(2)))
Fitting full model:
Iteration 0: log likelihood = -8198.9889
Iteration 1: log likelihood = -339.39401
Iteration 2: log likelihood = -334.76772
Iteration 3: log likelihood = -334.39315
Iteration 4: log likelihood = -334.39242
Iteration 5: log likelihood = -334.39242
Fixed effects regression model Number of obs = 76
Log likelihood = -334.39242
------------------------------------------------------------------------------
| Coef. Std. Err. z P>|z| [95% Conf. Interval]
-------------+----------------------------------------------------------------
time: |
age | .0035436 .0092129 0.38 0.701 -.0145134 .0216006
female | -.8639105 .2920079 -2.96 0.003 -1.436236 -.2915854
_cons | -4.081803 .6379757 -6.40 0.000 -5.332212 -2.831393
ap:1 | -.0310416 .1456656 -0.21 0.831 -.3165409 .2544577
ap:2 | -.0010321 .0017629 -0.59 0.558 -.0044872 .0024231
------------------------------------------------------------------------------
I’m telling merlin
that I want to fit a model with a user
defined family, and in particular I provide the name of the Mata
function
through hazfunction()
. The survival time variable and event indicator are declared as normal. I also tell it that there are 2 ancillary parameters to
estimate through nap(2)
. In my linear predictor I’ve adjusted for age
and female
.
Note that there are no random effects in this model…merlin
can still be used!
Given that we inevitably have correlation between events suffered by the same patient, we can now add in a random intercept at the patient level to account for this,
. merlin (time age female M1[patient]@1, ///
> family(user, hfunc(userhaz) failure(infect) nap(2)))
Fitting fixed effects model:
Fitting full model:
Iteration 0: log likelihood = -336.03458
Iteration 1: log likelihood = -330.8822
Iteration 2: log likelihood = -330.03033
Iteration 3: log likelihood = -329.82043
Iteration 4: log likelihood = -329.81922
Iteration 5: log likelihood = -329.81923
Mixed effects regression model Number of obs = 76
Log likelihood = -329.81923
------------------------------------------------------------------------------
| Coef. Std. Err. z P>|z| [95% Conf. Interval]
-------------+----------------------------------------------------------------
time: |
age | .0077964 .0139772 0.56 0.577 -.0195984 .0351912
female | -1.655001 .5304 -3.12 0.002 -2.694566 -.6154359
M1[patient] | 1 . . . . .
_cons | -4.649168 .9124865 -5.10 0.000 -6.437609 -2.860728
ap:1 | .214339 .2014649 1.06 0.287 -.1805251 .609203
ap:2 | .000749 .00213 0.35 0.725 -.0034258 .0049238
-------------+----------------------------------------------------------------
patient: |
sd(M1) | .93747 .28642 .5151035 1.706162
------------------------------------------------------------------------------
Which gives us a standard deviation for the random intercept of $\sigma = $0.94, indicating substantial heterogeneity between patients.
We can investigate non-proportional hazards, for example in the effect of age
as follows, remembering to add the
timevar()
option,
. merlin (time age age#fp(time, powers(0)) female M1[patient]@1, ///
> family(user, hfunc(userhaz) failure(infect) nap(2)) timevar(time))
variables created for model 1, component 2: _cmp_1_2_1 to _cmp_1_2_1
Fitting fixed effects model:
Fitting full model:
Iteration 0: log likelihood = -274.64295
Iteration 1: log likelihood = -270.67862 (not concave)
Iteration 2: log likelihood = -270.42114
Iteration 3: log likelihood = -269.13615
Iteration 4: log likelihood = -269.09274
Iteration 5: log likelihood = -269.0763
Iteration 6: log likelihood = -269.07693
Iteration 7: log likelihood = -269.07687
Iteration 8: log likelihood = -269.07687
Mixed effects regression model Number of obs = 76
Log likelihood = -269.07687
------------------------------------------------------------------------------
| Coef. Std. Err. z P>|z| [95% Conf. Interval]
-------------+----------------------------------------------------------------
time: |
age | .3845645 .0681329 5.64 0.000 .2510265 .5181025
age#fp() | -.0850253 .0148084 -5.74 0.000 -.1140493 -.0560013
female | -1.805519 .6942923 -2.60 0.009 -3.166306 -.4447307
M1[patient] | 1 . . . . .
_cons | -19.06474 3.331987 -5.72 0.000 -25.59532 -12.53417
ap:1 | 3.816655 .8012333 4.76 0.000 2.246266 5.387043
ap:2 | -.0036826 .0028632 -1.29 0.198 -.0092944 .0019292
-------------+----------------------------------------------------------------
patient: |
sd(M1) | 1.289988 .4326368 .6685124 2.489213
------------------------------------------------------------------------------
I’ve formed an interaction between age
and $\log(t)$ by using the #
notation, using the fp()
element.
In this case we find evidence of a time-dependent effect of age.
This example used merlin
version 2.0.2.