# Some comments on using steffects in Stata

## Using `stteffects`

There is a command in Stata called `stteffects`

which calculates marginal effects for survival-time data. This is the description in the helpfile:

*“ stteffects estimates average treatment effects, average treatment effects on the treated, and potential-outcome means using observational survival-time data. The available estimators are regression adjustment, inverse-probability weighting, and more efficient methods that combine regression adjustment and inverse-probability weighting.”*

I will concentrate on regression adjustment as this is essentially what `standsurv`

does. The other estimators are just different methods to estimate the same underlying quantities.

I do not use `stteffects`

as the estimand of interest is based on the mean survival time. In many cases this relies on extrapolation beyond the range of follow-up. In my applications I am not willing to make such strong assumptions. I will illustrate this with an example using the Rotterdam Breast cancer data. The code below loads and `stset`

’s the data. I restrict follow-up time to 5 years as ths highlights some of the extrapolation issues. I would usually use the `exit()`

option of `stset`

to restrict follow-up time, but for some reason `sttefects`

does not allow you to do this.

```
. use https://www.pclambert.net/data/rott2b if nodes>0, clear
(Rotterdam breast cancer data (augmented with cause of death))
. gen os2 = cond(os<(5*12),os,60)
. gen osi2 = cond(os<(5*12),osi,0)
. stset os2, f(osi2==1) scale(12)
failure event: osi2 == 1
obs. time interval: (0, os2]
exit on or before: failure
t for analysis: time/12
------------------------------------------------------------------------------
1,546 total observations
0 exclusions
------------------------------------------------------------------------------
1,546 observations remaining, representing
565 failures in single-record/single-failure data
6,343.591 total analysis time at risk and under observation
at risk from t = 0
earliest observed entry t = 0
last observed exit t = 5
.
. sts graph, by(hormon)
failure _d: osi2 == 1
analysis time _t: os2/12
```

I have plotted the Kaplan-Meier curve comparing those who receive and do not receive hormonal treatment, which shows that those taking treatment have slightly better survival. As shown in other examples there is imbalance between the groups for a number of potential confounders. For simplicity, I will just use age at diagnosis and the number of positive lymph nodes as potential confounders.

```
. tabstat age nodes ,by(hormon)
Summary statistics: mean
by categories of: hormon (Hormonal therapy)
hormon | age nodes
---------+--------------------
no | 54.12925 5.094449
yes | 62.54867 5.719764
---------+--------------------
Total | 55.97542 5.231565
------------------------------
```

Those receiving treatment are older and have slightly more positive lymph nodes.

I will first use `stteffects`

using regression adjustment (`ra`

) with `age`

and `enodes`

as covariates. I will use the potential outcomes framework,

- $T^{x=0}$ is the potential survival time for the non-treated/unexposed.
- $T^{x=1}$ is the potential survival time for the treated/exposed.

An average causal effect could be defined as the difference in the expected values of these two potential outcomes

$$ E\left[T^{x=1}\right] - E\left[T^{x=0}\right] $$

I use `stteffects`

to estimate this below,

```
. stteffects ra (age enodes, weibull) (hormon),
failure _d: osi2 == 1
analysis time _t: os2/12
Iteration 0: EE criterion = 1.789e-18
Iteration 1: EE criterion = 1.067e-29
Survival treatment-effects estimation Number of obs = 1,546
Estimator : regression adjustment
Outcome model : Weibull
Treatment model: none
Censoring model: none
------------------------------------------------------------------------------
| Robust
_t | Coef. Std. Err. z P>|z| [95% Conf. Interval]
-------------+----------------------------------------------------------------
ATE |
hormon |
(yes vs no) | 1.067025 1.230018 0.87 0.386 -1.343765 3.477816
-------------+----------------------------------------------------------------
POmean |
hormon |
no | 7.841406 .3771598 20.79 0.000 7.102186 8.580625
------------------------------------------------------------------------------
```

The output has given the mean potential outcome for those not receiving treatment and the average causal difference (labelled ATE). Thus, the estimated mean survival time is 7.84 years for those not taking treatment and estimated to be 1.07 year lower for those taking treatmnet. The 95% confidence interval for this difference spans zero.

What has `stteffects`

done? Well effectively it has fitted a Weibull regression model that includes `age`

and `enodes`

and `hormon`

and the interactions between `enodes`

and `hormon`

and between `age`

and `hormon`

. It has also allowed the shape parameter of the Weibull model to vary by `hormon`

. This model is the same as fitting separate Weibull models for the untreated and treated groups. It has also estimated the mean survival time if all individuals were untreated and the mean difference in survival time between the treated and untreated. It actually simultaneously estimates all this, but as I will show below, it is doing exactly the same as regression standardization using `standsurv`

.

I have assumed a Weibull model in the above code, so it is of interest to see if fitting a different model gives notable differences. Below I run `steffects`

again, but now using a lognormal model.

```
. stteffects ra (age enodes, lnormal) (hormon),
failure _d: osi2 == 1
analysis time _t: os2/12
Iteration 0: EE criterion = 1.716e-25
Iteration 1: EE criterion = 9.506e-30
Survival treatment-effects estimation Number of obs = 1,546
Estimator : regression adjustment
Outcome model : lnormal
Treatment model: none
Censoring model: none
------------------------------------------------------------------------------
| Robust
_t | Coef. Std. Err. z P>|z| [95% Conf. Interval]
-------------+----------------------------------------------------------------
ATE |
hormon |
(yes vs no) | 3.430201 3.669848 0.93 0.350 -3.762568 10.62297
-------------+----------------------------------------------------------------
POmean |
hormon |
no | 12.56943 1.027354 12.23 0.000 10.55586 14.58301
------------------------------------------------------------------------------
```

The mean survival time for the non-treated has increased from 7.84 years to 12.57 and the average causal difference has increased from 1.07 years to 3.43 years. This should be concerning has the estimates appear to be strongly dependent on the choice of parametric distribution.

Let’s explore what is happening here and why the estimates are so different. The code below fits all models available in `streg`

to the non-treated group (`hormon=0`

) and compares the estimated survival functions to the Kaplan-Meier estimate. I am not fitting covariates here as I am just making a point about extrapolating beyond the range of your data.

```
foreach dist in weibull gompertz loglogistic lognormal ggamma {
streg if hormon == 1, dist(`dist')
predict surv_`dist', surv
estimates store `dist'
}
sts gen s_km = s if hormon == 1
twoway (line s_km _t, connect(stairstep) sort lcolor(black)) ///
(line surv_* _t, sort), ///
legend(order(1 "Kaplan-Meier" 2 "Weibull" 3 "Gompertz" ///
4 "LogLogistic" 5 "LogNormal" 6 "Ggamma") ///
ring(0) cols(1) pos(1)) ///
xtitle("Years from Surgery") ///
ytitle("S(t)") ///
ylabel(,format(%3.1f))
```

None of the fitted line are perfect (we will do better with `stpm2`

later), but are in broad agreement. The problems is we have censored survival data and the maximum follow-up time is 5 years. Mean survival can be calculated as the area under the survival function and so the graph above does not show what we want. I now perform predictions up to 80 years. Here were are extrapolating beyond the range of our follow-up.

```
preserve
gen oldt = _t
drop _t
range _t 0 80
foreach dist in weibull gompertz loglogistic lognormal ggamma {
estimates restore `dist'
predict surv80_`dist', surv
}
twoway (line s_km oldt, connect(stairstep) sort lcolor(black)) ///
(line surv80_* _t, sort), ///
legend(order(1 "Kaplan-Meier" 2 "Weibull" 3 "Gompertz" ///
4 "LogLogistic" 5 "LogNormal" 6 "Ggamma") ///
ring(0) cols(1) pos(1)) ///
xtitle("Years from Surgery") ///
ytitle("S(t)") ///
ylabel(,format(%3.1f)) ///
xline(5, lpattern(dash))
restore
```

Now we can see the difference between the estimated survival functions between the different models. There is very little difference up to 5 years, where we have follow-up information to, but they are very different beyond this point. The mean survival time is the area under each curve and we can see why the Weibull model gives a lower mean than the log-normal model. In fact the Log-logistic and Log-Normal model are still clearly above zero at 80 years.

What does this mean? Well if you have a lot of censoring and the survival function is clearly above zero at the end of your follow-up then you are making strong assumptions about what happens beyond where you have data if you use `stteffects`

.

## Equivalent model using `stpm2`

and `standsurv`

I will show that I can get the same estimates as `stteffects`

by fitting an `stpm2`

model followed by using `standsurv`

. First I create interactions between `hormon`

and `age`

and between `hormon`

and `enodes`

. Then I fit the model.

```
. gen age_h = age*hormon
. gen enodes_h = enodes*hormon
.
. stpm2 hormon age enodes age_h enodes_h, ///
> scale(hazard) df(1) eform nolog tvc(hormon) dftvc(1)
Log likelihood = -1329.3579 Number of obs = 1,546
--------------------------------------------------------------------------------
| exp(b) Std. Err. z P>|z| [95% Conf. Interval]
---------------+----------------------------------------------------------------
xb |
hormon | 1.134897 .8353114 0.17 0.863 .2681956 4.802432
age | 1.013167 .0036718 3.61 0.000 1.005996 1.020389
enodes | .1548263 .0282304 -10.23 0.000 .1083029 .2213345
age_h | .9973785 .0109109 -0.24 0.810 .9762212 1.018994
enodes_h | .5988075 .2563495 -1.20 0.231 .2587545 1.385756
_rcs1 | 2.209114 .0764872 22.89 0.000 2.064175 2.36423
_rcs_hormon1 | 1.083153 .0889351 0.97 0.331 .9221461 1.272272
_cons | .4478668 .105619 -3.41 0.001 .2821063 .7110253
--------------------------------------------------------------------------------
Note: Estimates are transformed only in the first equation.
```

I have used `df(1)`

as this is equivalent to a Weibull model. By using `tvc(hormon)`

and `dftvc(1)`

I have allowed the shape parameter of the Weibull model to vary between those receiving and not-receiving treatment. I could have fitted separate models by treatment group and the parameter estimates would be the same, but by fitting one model I will be able to form contrasts between the treatment groups using `standsurv`

.

I will now `standsurv`

to estimate the marginal survival functions up to 40 years for the non-treated and treated subjects. As the model has interactions I need to make sure these are all set to zero for the non-treated (`hormon=0`

) and take the values of the relevant covariates for the treated (`hormon=1`

).

```
. range ttlong 0 40 100
(1,446 missing values generated)
. standsurv, atvar(S_h0b S_h1b) timevar(ttlong) ci ///
> at1(hormon 0 age_h 0 enodes_h 0) ///
> at2(hormon 1 age_h = age enodes_h = enodes)
```

The two survival functions can then plotted. A reference line at 5 years has been added to indicate where we have follow-up information to.

```
. twoway (line S_h0b ttlong, sort lcolor(red)) ///
> (line S_h1b ttlong, sort lcolor(blue)) ///
> , legend(order(1 "No hormonal treatment" 2 "Hormonal treatment") ring(0) cols(1) pos(1)) ///
> ylabel(0(0.1)1,angle(h) format(%3.1f)) ///
> ytitle("S(t)") ///
> xtitle("Years from surgery") ///
> xline(5, lpattern(dash) lcolor(black%50))
```

The estimated average potential outcome for the untreated can be approximated by using `integ`

to approximate the area under the survival curve (I will use `standsurv`

to do this better shortly).

```
. integ S_h0b ttlong
number of points = 100
integral = 7.8376079
```

This is similar to the output from `stteffects above`

(7.841406 vs 7.8376079). Using the `rmst`

option of `standsurv`

will do more accurate integration. I will integrate up to 100 years (theoretically the integral is to infinity, but the survival is virtually zero at 100 years). `rmst`

stands for restricted mean survival time, but if $t$ is large enough so that $S(t)\approx 0$ then this is effectively the area under the full survival curve.

```
. gen tt100 = 100 in 1
(1,545 missing values generated)
. standsurv, rmst ci timevar(tt100) trans(none) ///
> at1(hormon 0 age_h 0 enodes_h 0) ///
> at2(hormon 1 age_h = age enodes_h = enodes) ///
> contrast(difference) ///
> atvars(rmst_h0b rmst_h1b) ///
> contrastvar(rmstdiff100) mestimation
. list rmst_h0b rmst_h1b rmstdiff100* in 1, noobs
+------------------------------------------------------------+
| rmst_h0b rmst_h1b rmstd~100 rmstdi~lci rmstd~uci |
|------------------------------------------------------------|
| 7.8414209 8.9084369 1.0670161 -1.3445604 3.4785925 |
+------------------------------------------------------------+
```

I have used the `mestimation`

option to calculate the equivalent of robust standard errors so that these and the confidence intervals are comparable with `stteffects`

. I have also used the `trans(none)`

option as `sttefects`

calculates standard errors on the untransformed survival scale, while the default in `standsurv`

is the log scale.

The estimates are now very similar to `stteffects`

(to 4 or 5 decimal places).

This has shown that I can obtain the same estimates as `stteffects`

using `standsurv`

, but generally I would not be happy in doing so as to obtain the estimated mean of the potential outcomes I have had to extrapolate my two survival functions way beyond the end of follow-up. I am not sure is this is obvious to users of `stteffects`

.

## Using restricted mean survival time (RMST) as an alternative.

I have previous discussed using RMST in another tutorial. As a reminder the restricted mean survival time at time $t^*$ is defined as, $$ E\left[min(t,t^*)\right] $$ i.e. it is the mean up to some point $t^*$. The RMST can be estimated by calculating the area under the survival curve between 0 and $t^*$. In an observational study where we need to take account of potential confounders, we can define the RMST of the standardized survival function as

$$ RMST(t^*|X=x,Z) = \int_0^{t^*} E\left[S(t|X=x,Z)\right] $$

We used the `rmst`

option of `standsurv`

above, but integrated the survival function to 100 years, to where $S(t)\approx 0$. So if we feed the `timevar`

option a lower value of $t$, then we can calculate RMST to a point within our follow-up period. In the code below I use 5 years.

```
. gen tt5 = 5 in 1
(1,545 missing values generated)
. standsurv, rmst ci timevar(tt5) trans(none) ///
> at1(hormon 0 age_h 0 enodes_h 0) ///
> at2(hormon 1 age_h = age enodes_h = enodes) ///
> contrast(difference) ///
> atvars(rmst5_h0b rmst5_h1b) ///
> contrastvar(rmstdiff5) mestimation
. list rmst5_h0b rmst5_h1b rmstdiff5* in 1, noobs
+-----------------------------------------------------------+
| rmst5_h0b rmst5_h1b rmstdiff5 rms~5_lci rms~5_uci |
|-----------------------------------------------------------|
| 4.1250222 4.3241716 .19914943 .05495712 .34334175 |
+-----------------------------------------------------------+
```

We get a difference of 0.199 (95% CI 0.055 to 0.343) years between the two groups. The difference in RMST can still be interpreted as a causal effect but, unlike `stteffects`

, does not reply on extrapolation. Alternatively, the difference in standardized survival functions could be presented.