*! version 1.0 29 Apr 2022

Title

tacts -- A clinical trial simulator

Syntax

tacts
[,
options]

Required

data(string) specifies the data generating commands.

analysis(string) specifies the analysis commands.

output(string) specifies what is saved for each simulation.

ss(string) specifies the sample sizes for each stage of the design.

Optional
decision(string) specifies what decisions are made at each interim analysis.

outputsummary(string) specifies commands used on the final simulated dataset.

nsims(#) specifies the number of simulations to carry out.

setup(string) specifies a set of commands to run at the start of the simulation.

saving(string) specifies the name of the final dataset.

replace specifies whether to replace the final saved dataset.

seed(#) specifies the random number seed.

Description

tacts simulates data from different types of trial and summarises the results.

Options

Main

data(string) specifies the data generating commands.

analysis(string) specifies the analysis commands.

output(string) specifies what is saved for each simulation.

ss(string) specifies the sample sizes for each stage of the design.

decision(string) specifies what decisions are made at each interim analysis.

outputsummary(string) specifies commands used on the final simulated dataset.

nsims(#) specifies the number of simulations to carry out.

setup(string) specifies a set of commands to run at the start of the simulation.

saving(string) specifies the name of the final dataset.

replace replace specifies whether to replace the final saved dataset.

seed(#) specifies the random number seed.

Examples

The first example is a two-arm parallel group trial, the treatment indicator is
x and outcome variable y is normally distributed (conditional on x) and
there is a difference of 2.6 between the two groups. The output is
whether the p-value from the t-test is below 5% and the sample size is 400.

tacts, data(x~fixed(0.5, 0.5); y~N(2.6*x, 8);) analysis(ttest y, by(x);) output(power = < 0.05;) ss(n=400) nsims(5000) saving(results) replace seed(1)

The second example is the same trial but the outcome is simulated under no
difference between the two arms.

tacts, data(x~fixed(0.5, 0.5); y~N(0, 8);) analysis(ttest y, by(x);) output(type = < 0.05;) ss(n=400) nsims(5000) saving(results) replace seed(2)

Lastly this command has the same trial with the mean difference between the
groups as 2.6 but now uses linear regression and estimates various statistics
at the end of the simulation.

tacts, data(x~fixed(0.5, 0.5); y~N(2.6*x, 8);) analysis(reg y x; test x;) output(power = < 0.05; slope = _b[x];) ss(n=400) nsims(5000) seed(1) outputsummary(Bias = slope1-2.6; Power=power1; MSE=(slope1-2.6)^2;)

The following examples show different designs with a small description but it does
not cover all the aspects of the code but they can be easily adapted.

Group sequential design stopping only for efficacy

A group sequential design with two interim analyses and only stopping for efficacy under the
alternative hypothesis.

tacts, ss(n=100 n=200 n=400) nsims(10000) data(x~fixed(0.5, 0.5);y~N(2.6*x, 8);)
analysis(reg y x;) decision(n=100: stoptrialE if _b[x]/_se[x]> 2.5;
n=200: stoptrialE if _b[x]/_se[x]> 2.2; n=400: stoptrialE if _b[x]/_se[x]> 2;)
output(stop = ; ttest = _b[x]/_se[x]; slope=_b[x];)
outputsummary(Power=(stop1==1)+(stop2==1)+(stop3==1);PET=(stop1==1)+(stop2==1);
ESS_H1=100*(stop1==1)+200*(stop2==1)+400*(stop1==0)*(stop2==0);
Bias=cond(stop1==1,slope1-2.6,cond(stop2==1,slope2-2.6,slope3-2.6));) seed(1)

The same group sequential design as previous but under the null hypothesis, so as to
calculate the type 1 error.

tacts, ss(n=100 n=200 n=400) nsims(10000) data(x~fixed(0.5, 0.5);y~N(0, 8))
analysis(reg y x; test x) decision(n=100: stoptrialE if _b[x]/_se[x]> 2.5;
n=200: stoptrialE if _b[x]/_se[x]> 2.2; n=400: stoptrialE if _b[x]/_se[x]> 2;)
output(stop = ; ttest = _b[x]/_se[x]; slope=_b[x];)
outputsummary(Power=(stop1==1)+(stop2==1)+(stop3==1);PET=(stop1==1)+(stop2==1);
ESS_H1=100*(stop1==1)+200*(stop2==1)+400*(stop1==0)*(stop2==0);
Bias=cond(stop1==1,slope1,cond(stop2==1,slope2,slope3));) seed(2)

Survival analysis sample size calculations

A two-arm parallel groups trial with a survival endpoint.

tacts, nsims(50000) ss(n=124) data(x~fixed(0.5,0.5); timein~U(0,18);
S~Exp(1/(0.65*ln(2)/5.4*x+ln(2)/5.4*(1-x)));
timeout= cond(timein+S < 24,timein+S,24); D= timeout < 24;)
analysis(stset timeout, failure(D) origin(time timein); count if D==1;
local E=r(N); count if D==1 & x==1; local E1=r(N);count if D==1 & x==0;
local E0=r(N); sts test x; local pv=chi2tail(r(df), r(chi2)); stcox x;)
output(pv =; lhr= _b[x]; ttest= _b[x]/_se[x]; nevents= ; n1events= ;
n0events= ;) outputsummary( HR= exp(lhr1); Bias= exp(lhr1)-0.65;
Power= ttest < -0.8416; Power_rank= (pv1<0.4); Deaths= nevents1;
Pr_E= nevents1/124; Pr_S1= (62-n1events1)/62; Pr_S0= (62-n0events1)/62;) seed(1)

Co-primary endpoints

A trial with three outcome variables generated under a multivariate normal distribution.
The outputs are various different definitions of power: marginal,conjunctive and disjunctive.

mat SigmaInd = ( 1, 0, 0 \ 0, 1, 0 \ 0, 0, 1)

tacts, nsims(10000) ss(n=1050) data(x~fixed(0.5,0.5);
y~MVN((0.2*x, 0.2*x, 0.2*x), SigmaInd)) analysis(reg y1 x;test x;local pv1=r(p);
reg y2 x; test x; local pv2=r(p); reg y3 x; test x; local pv3=r(p);)
output(Marg_pow1=(<0.01667);Marg_pow2=(<0.01667);
Marg_pow3=(<0.01667);
Disjpower = ((<0.01667) | (<0.01667) | (<0.01667));
Conjpower = (<0.01667)*(<0.01667)*(<0.01667);
Holm_power =
((<0.01667)*(<0.0333)*(<0.05)+(<0.01667)*(<0.0333)*
(<0.05)+(<0.01667)*(<0.0333)*(<0.05)+(<0.01667)*
(<0.0333)*(<0.05)+ (<0.01667)*(<0.0333)*(<0.05)+
(<0.01667)*(<0.0333)*(<0.05))>0;)
seed(1)

mat Sigma = ( 1, 0.5, 0.5 \ 0.5, 1, 0.5 \ 0.5, 0.5, 1)

tacts, nsims(10000) ss(n=1050) data(x~fixed(0.5,0.5);
y~MVN((0.2*x, 0.2*x, 0.2*x), Sigma)) analysis(reg y1 x;test x;local pv1=r(p);
reg y2 x; test x; local pv2=r(p); reg y3 x; test x; local pv3=r(p);)
output(Marg_pow1=(<0.01667);Marg_pow2=(<0.01667);
Marg_pow3=(<0.01667);
Disjpower = ((<0.01667) | (<0.01667) | (<0.01667));
Conjpower = (<0.01667)*(<0.01667)*(<0.01667);
Holm_power =
((<0.01667)*(<0.0333)*(<0.05)+(<0.01667)*(<0.0333)*
(<0.05)+(<0.01667)*(<0.0333)*(<0.05)+(<0.01667)*
(<0.0333)*(<0.05)+ (<0.01667)*(<0.0333)*(<0.05)+
(<0.01667)*(<0.0333)*(<0.05))>0;)
seed(1)

Longitudinal group sequential trial design

Similar code to the previous but now the multivariate normal distribution
is used to generate longitudinal dataset. Note the use of recruitment time and
that the interims are specified using the ss() option

mat Sig=(1,0,0,0,0\ 0,1,0,0,0\ 0,0,1,0,0\ 0,0,0,1,0\ 0,0,0,0,1)

tacts, nsims(1000) ss(n=120,itime<=20 itime<=36) data(x~fixed(0.66667,0.33333);
y~MVN((0,0.1*x-0.1,0.2*x-0.2,0.3*x-0.3,0.4-0.7*x),Sig);ui~N(0,1);timein~U(0,24);
yy0=y1+ui;yy3=y2+ui;yy6=y3+ui;yy9=y4+ui;yy12=y5+ui; id=_n;
itime3=3+timein;itime6=6+timein;itime9= 9+timein;itime12=12+timein;
reshape long yy itime, i(id) j(time);)
analysis(xtmixed yy i.time#i.x, nocons || id:; local obs = e(N);
cap test [yy]12.time#1.x = [yy]12.time#0.x;
local rerror = _rc;
local p1=r(p); local trteff = _b[yy:12.time#1.x] - _b[yy:12.time#0.x];)
output(p = ; nobs = ; trteff = ; error=;)
decision(itime<=20: stoptrial if >0.5;)
outputsummary(Power = p2<0.05; Eobs = cond(nobs2==.,nobs1, nobs2);
Trt = cond(nobs2==.,trteff1, trteff2);) seed(1)

Sample size re-estimation

An example of a blinded sample size re-estimation for a two-arm parallel groups trial

tacts, ss(n=199 n=398) nsims(10000) data(x~fixed(0.5, 0.5); y~N(-2.6*x,8))
analysis(reg y x;
local maxn = ceil(2*(invnorm(0.975)+invnorm(0.9))^2*2*(e(rmse)^2)/(2.6^2));
local SS=e(N); test x;) output(pv = ; maxn= ; ss = ;)
decision(n=199: SSR local n2 = ; stoptrialE if <=199;)
outputsummary(Power = pv2<0.05; maxn=maxn1; final_ss=ss2;) seed(1)

Generating under the null distribution to get the type 1 error of the previous trial design.

tacts, ss(n=199 n=398) nsims(10000) data(x~fixed(0.5, 0.5); y~N(0,8))
analysis(reg y x;
local maxn = ceil(2*(invnorm(0.975)+invnorm(0.9))^2*2*(e(rmse)^2)/(2.6^2));
local SS=e(N); test x;) output(pv = ; maxn= ; ss = ;)
decision(n=199: SSR local n2 = ; stoptrialE if <=199;)
outputsummary(Power = pv2<0.05; maxn=maxn1; final_ss=ss2;) seed(1)

Generating data that is less variable than planned to show the efficiency gain
in sample size re-estimation.

tacts, ss(n=199 n=398) nsims(10000) data(x~fixed(0.5, 0.5); y~N(-2.6*x,6))
analysis(reg y x;
local maxn = ceil(2*(invnorm(0.975)+invnorm(0.9))^2*2*(e(rmse)^2)/(2.6^2));
local SS=e(N); test x;) output(pv = ; maxn= ; ss = ;)
decision(n=199: SSR local n2 = ; stoptrialE if <=199;)
outputsummary(Power = cond(maxn1>199, pv2<0.05, pv1<0.05); PET= (maxn1<=199);
final_ss= cond(maxn1>199,ss2, ss1);) seed(1)

Response adaptive randomisation

An example of altering the allocation ratios at a single interim analysis in a four-arm
trial.

tacts, ss(n=200 n=584) nsims(10000) setup(local p0 0.25; local p1 0.25;
local p2 0.25; local p3 0.25;) data(x~fixed(,,,);
x1=x==1; x2=x==2; x3=x==3; y~N(-1*x1-0.5*x2-2.6*x3, 6))
analysis(reg y x1 x2 x3; test x1; local pv1=r(p); test x2; local pv2=r(p);
test x3; local pv3=r(p); count if x1==1; local N1=r(N); count if x2==1;
local N2=r(N); count if x3==1; local N3=r(N); count if x1==0&x2==0&x3==0;
local N0 = r(N);)
output(n_x0=; n_x1=; n_x2=; n_x3=; pv_x1=; pv_x2=;
pv_x3=;)
decision(n=200: RAR local p1=0.75*(1-)/(3---);
RAR local p2=0.75*(1-)/(3---);
RAR local p3=0.75*(1-)/(3---);)
outputsummary(Power1=pv_x12<0.01666; Power2=pv_x22<0.01666; Power3=pv_x32<0.01666;
Stage2_SS_0=n_x02; Stage2_SS_1=n_x12; Stage2_SS_2=n_x22; Stage2_SS_3=n_x32;) seed(1)

Drop the loser design

A drop the loser design under the alternative hypothesis

local final_bound = 1.978

tacts, ss(n=240 n=360) nsims(10000)
setup(local p0 0.25; local p1 0.25; local p2 0.25; local p3 0.25;)
data(x~fixed(,,,); x1=x==1; x2=x==2; x3=x==3; y~N(2.6*x1,6))
analysis(reg y x1 x2 x3; local tt1=_b[x1]/_se[x1]; local tt2=_b[x2]/_se[x2];
local tt3 = _b[x3]/_se[x3]; count if x1==1; local N1=r(N); count if x2==1;
local N2=r(N); count if x3==1; local N3=r(N); count if x1==0&x2==0&x3==0;
local N0 = r(N);)
output(n_x0=; n_x1=; n_x2=; n_x3=; tt1=; tt2=;
tt3=;)
decision(n=240: droparm (< | <) local p1=0;
droparm (< | <) local p2=0;
droparm (< | <) local p3=0;)
outputsummary(
Disjpower = ((tt12>)|(tt22>)|(tt32>));
Marg_power1 = tt12>; Marg_power2 = tt22>;
Marg_power3 = tt32>; Stage2_SS_0=n_x02; Stage2_SS_1=n_x12;
Stage2_SS_2=n_x22; Stage2_SS_3=n_x32;) seed(1)

The same drop the loser design under the null hypothesis.

local final_bound = 1.978

tacts, ss(n=240 n=360) nsims(10000)
setup(local p0 0.25; local p1 0.25; local p2 0.25; local p3 0.25;)
data(x~fixed(,,,); x1=x==1; x2=x==2; x3=x==3; y~N(0,6))
analysis(reg y x1 x2 x3; local tt1=_b[x1]/_se[x1]; local tt2=_b[x2]/_se[x2];
local tt3 = _b[x3]/_se[x3]; count if x1==1; local N1=r(N); count if x2==1;
local N2=r(N); count if x3==1; local N3=r(N); count if x1==0&x2==0&x3==0;
local N0 = r(N);)
output(n_x0=; n_x1=; n_x2=; n_x3=; tt1=; tt2=;
tt3=;)
decision(n=240: droparm (< | <) local p1=0;
droparm (< | <) local p2=0;
droparm (< | <) local p3=0;)
outputsummary(
FWER = ((tt12>)|(tt22>)|(tt32>));
Stage2_SS_0=n_x02; Stage2_SS_1=n_x12; Stage2_SS_2=n_x22; Stage2_SS_3=n_x32;)
seed(1)

MAMS design

A MAMS design with two experimental arms and a single control arm, the trial stops
as soon as any test associated with the experimental arms crosses a boundary.

tacts, nsims(10000) ss(n=132 n=264 n=396)
setup( local p0 = 0.5; local p1=0.25; local p2 = 0.25;)
data(x~fixed(, , ); x1=x==1; x2=x==2; y~N(2.6*x1, 6))
analysis(reg y x1 x2; local N= ; local in1 = r(N); count if x1==1;
local in1 = r(N); count if x2==1; local in2 = r(N); local tt1 = _b[x1]/_se[x1];
local tt2 = _b[x2]/_se[x2];)
output(tt_x1 = ; tt_x2 = ; stop=; N=;)
decision(
n=132: droparm ( < -2.27) local p1=0; local n2=-33; local n3=-66;
n=132: droparm ( < -2.27) local p2=0; local n2=-33; local n3=-66;
n=132: stoptrial if (>2.27 | >2.27);
n=132: stoptrial if (<-2.27 & <-2.27);
n=264: droparm ( < -2.27 & ==66) local p1=0; local n3=-33;
n=264: droparm ( < -2.27 & ==66) local p2=0; local n3=-33;
n=264: stoptrial if (>2.27 | >2.27);
n=264: stoptrial if (<-2.27 & <-2.27);
)
outputsummary(Marg_power1=tt_x11>2.27 | (tt_x12>2.27 & tt_x12~=.) |
(tt_x13>2.27 & tt_x13~=.);
Marg_power2=tt_x21>2.27| (tt_x22>2.27 & tt_x22~=.) | (tt_x23>2.27 & tt_x23~=.);
Disjpower=tt_x11>2.27 | (tt_x12>2.27 & tt_x22~=.) | (tt_x13>2.27 & tt_x13~=.)
| tt_x21>2.27 | (tt_x22>2.27 & tt_x22~=.) | (tt_x23>2.27 & tt_x23~=.);
SS = cond(N2==.,N1,cond(N3==.,N2,N3)); n2=N2; n3=N3;) seed(1)

The same MAMS design but now the study only stops if all experimental arms cross
boundaries

tacts, nsims(10000) ss(n=128 n=256 n=384)
setup(local p0 0.5; local p1 0.25; local p2 0.25;)
data(x~fixed(, , ); x1=x==1; x2=x==2; y~N(2.6*x1, 6))
analysis(reg y x1 x2; local N= ; count if x1==1; local in1 = r(N);
count if x2==1; local in2=r(N); local tt1=_b[x1]/_se[x1];
local tt2=_b[x2]/_se[x2];)
output(tt_x1=; tt_x2=; stop=; N=; IN1=; IN2=;)
decision(
n=128:droparm (>2.27|<-2.27) local p1=0; local n2=-32; local n3=-64;
n=128:droparm (>2.27|<-2.27) local p2=0; local n2=-32; local n3=-64;
n=128:stoptrial if (>2.27 & >2.27);
n=128:stoptrial if (<-2.27 & <-2.27);
n=128:stoptrial if (<-2.27 & >2.27);
n=128:stoptrial if (>2.27 & <-2.27);
n=256:droparm ((>2.27|<-2.27)&==64) local p1=0; local n3=-32;
n=256:droparm ((>2.27|<-2.27)&==64) local p2=0; local n3=-32;
n=256:stoptrial if ((>2.27 | ==32) & (>2.27 | ==32));
n=256:stoptrial if ((<-2.27 | ==32) & (<-2.27 | ==32));
n=256:stoptrial if ((<-2.27 | ==32) & (>2.27 | ==32));
n=256:stoptrial if ((>2.27 | ==32) & (<-2.27 | ==32));
)
outputsummary(Marg_power1=tt_x11>2.27 | (tt_x12>2.27 & tt_x12~=.) |
(tt_x13>2.27 & tt_x13~=.);
Marg_power2=tt_x21>2.27 | (tt_x22>2.27 & tt_x22~=.) | (tt_x23>2.27 & tt_x23~=.);
Disjpower=tt_x11>2.27 | (tt_x12>2.27 & tt_x12~=.) | (tt_x13>2.27 & tt_x13~=.) |
tt_x21>2.27 | (tt_x22>2.27 & tt_x22~=.) | (tt_x23>2.27 & tt_x23~=.);
SS = cond(N2==.,N1,cond(N3==.,N2,N3)); n2=N2; n3=N3;) seed(1)

Author

Prof Adrian Mander, Cardiff University.

Email mandera@cardiff.ac.uk