ElMaestro
★★★

Denmark,
2026-05-27 12:22
(22 d 17:54 ago)

Posting: # 24633
Views: 1,224
 

 My first and failed attempt into AI code: Who can do it? [🇷 for BE/BA]

Hi all,

long post, I apologise.

Some time ago a weirdo published a paper about fitting mixed models for semi-replicated designs, where he showed how it is possible to work directly in the observational covariance matrix V, rather than using V=ZGZt+R and working in G and R.
This allows us to avoid fitting a model that involves a within-T variance component, since it does not exist with the semi-replicated designs. Great concept, eh? :-D

The author gave some working R code for it. The code did not, however, include derivation of a 90% CI with Satterthwaite's df approximation (or any other approximation for that matter). I have been through literature incl. SAS's helpfiles for Proc Mixed and read into papers on mixed models. As I am not a mathematician, much of it is quite inaccessible to me.


So, I asked https://deepai.org for help with deriving the 90% CI for T-R based on Satt dfs, and so far I failed miserably.

At my end Deepai readily takes on the challenge, and produces all sorts of complicated code which ends up with extremely wrong estimates of the df. For example anywhere from -37165346958870.86 to 1e10.
It ends up in anobscure debate with itself about Hessians, ill-conditioned matrices, ridge-stabilisation, regularisation constants, Fishbutt adjustment, Cojones-Krasilnikoff correction, L-BFGS-B, and much other incomprehensible stuff. It then oscillates between dysfunctional solutions. Whatever I do I seem to end up with severely ill-conditioned Hessians which I guess may be the (or a) reason for badly estmated df's.

Can someone find a way to do it, with or without AI help?

Below is some code to get you started, the code is a slightly adjusted version of the weirdo's spew.
All we need is "the rest" which derives the 90% CI with Satt df.

rm(list=ls(all=TRUE))



##the following lines correspond to EMA dataset II.
Subj=c(1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4,
       5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8,
       9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12,
       13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16,
       17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20,
       21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24, 24)
Trt=c("R", "T", "R", "R", "T", "R", "R", "R", "T", "T",
      "R", "R", "T", "R", "R", "R", "R", "T", "R", "T",
      "R", "T", "R", "R", "R", "T", "R", "T", "R", "R",
      "R", "R", "T", "R", "R", "T", "R", "R", "T", "R",
      "T", "R", "R", "T", "R", "T", "R", "R", "T", "R",
      "R", "R", "R", "T", "R", "T", "R", "R", "R", "T",
      "R", "R", "T", "T", "R", "R", "T", "R", "R", "R",
      "T", "R")
Per=c(1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
      1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
      1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
      1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
      1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3)
Seq=c("RTR", "RTR", "RTR", "RTR", "RTR", "RTR", "RRT",
      "RRT", "RRT", "TRR", "TRR", "TRR", "TRR", "TRR",
      "TRR", "RRT", "RRT", "RRT", "RTR", "RTR", "RTR",
      "TRR", "TRR", "TRR", "RTR", "RTR", "RTR", "TRR",
      "TRR", "TRR", "RRT", "RRT", "RRT", "RRT", "RRT",
      "RRT", "RRT", "RRT", "RRT", "RTR", "RTR", "RTR",
      "RTR", "RTR", "RTR", "TRR", "TRR", "TRR", "TRR",
      "TRR", "TRR", "RRT", "RRT", "RRT", "RTR", "RTR",
      "RTR", "RRT", "RRT", "RRT", "RRT", "RRT", "RRT",
      "TRR", "TRR", "TRR", "TRR", "TRR", "TRR", "RTR",
      "RTR", "RTR")
Y=c(4053.6, 3970.4, 3748.8, 2986.2, 2378.8, 2804.6,
    3464.4, 3340.2, 4028.8, 4105, 3191.2, 3803.6,
    4767.8, 4542.6, 3940, 3050.8, 3027.2, 2419.6,
    2530.2, 3072, 2962.6, 2205, 2041.4, 2018, 4647.6,
    4159.6, 3400, 2228.2, 2360.4, 2221.2, 1863.8,
    2212.4, 2394.4, 2278.4, 3170.4, 3927.2, 2640.4,
    2430.4, 2869.2, 3030.8, 2459.8, 2970.4, 2254.4,
    1994.8, 2724.4, 2959.6, 3442, 3342.6, 2396.8,
    2659.4, 2172, 2725, 2805.6, 3146.6, 2418.8,
    2749.8, 2504, 2662.4, 2929.8, 3037.2, 2869.6,
    2666.4, 3069, 2949, 2926.8, 2855.4, 3154.8,
    3185.6, 3548.6, 1874.8, 1808.8, 2730.8)

D=data.frame(Subj, Seq, Trt, Per, Y)

Mod=lm(log(Y) ~ factor(Subj)+factor(Per)+factor(Seq)+factor(Trt), data=D)
anova(Mod)

Create.Model.Matrix.Fixed.Effects=function(D, Want.Intercept=F)
##creates a full column-rank model matrix
##fixed: Treatment, Period, Sequence
{
 ##first the two treatments
 Int=rep(1, nrow(D))
 TrtT=as.numeric(as.character(D$Trt)=="T")
 TrtR=as.numeric(as.character(D$Trt)=="R")
 X=cbind(TrtT, TrtR)
 if (Want.Intercept) X=cbind(Int, TrtR)
 R= qr(X)$rank

 ##now the Periods
 for (p in unique(D$Per))
 {
  v=as.numeric(D$Per==p)
  XX=data.frame(X, v)
  names(XX)[ncol(XX)] = paste("Per", p, sep="")
  rnk=qr(XX)$rank
  if (rnk>R)
    {
       X=XX
       R=rnk
    }
 }
 
 ##and sequence
 for (q in unique(D$Seq))
 {
  v=as.numeric(D$Seq==q)
  XX=data.frame(X, v)
  names(XX)[ncol(XX)] = paste("Seq", q, sep="")
  rnk=qr(XX)$rank
  if (rnk>R)
    {
       X=XX
       R=rnk
    }
 }
 return(as.matrix(X))
}

Create.CovM=function(Params)
##block diagonal covariance matrix
{
  varT=Params[1]   varBR=Params[2]   varWR=Params[3]   varRT=Params[4]   Nobs=length(D$Y)
  V=matrix(0,ncol=Nobs, nrow=Nobs) ##creates the square matrix of zeros
  for (iRow in 1:Nobs)
  for (iCol in 1:Nobs)
  {
   ## the diagonal
   if (iCol==iRow)
    {
      if (D$Trt[iRow]=="T") V[iRow,iCol]=V[iRow,iCol]+varT
      if (D$Trt[iRow]=="R") V[iRow,iCol]=V[iRow,iCol]+varWR +varBR
    }
   ## off diagonal
   if (iCol!=iRow)
   if (D$Subj[iRow]==D$Subj[iCol])
    {
     if (D$Trt[iCol]==D$Trt[iRow]) V[iRow,iCol]= V[iRow,iCol]+varBR
     if (D$Trt[iCol]!=D$Trt[iRow]) V[iRow,iCol]= V[iRow,iCol]+varRT
    }
  }
  return(as.matrix(V))
}

Log.REML=function(Pars)
{
  CovM=Create.CovM(Pars) ##this is V
  k=  -((length(y)-ncol(X))/2)*log(2*pi)
  A= -0.5*log( det(CovM) )
  B= -0.5*log( det(t(X) %*% solve(CovM) %*% X))
  est.b <<- solve(t(X) %*% solve(CovM) %*% X) %*% t(X) %*% solve(CovM) %*% y
  tmp= y - X %*% est.b
  C=-0.5 *(t(tmp) %*% solve(CovM) %*% tmp)
  return(k+A+B+C)
}


Some.Initial.Guesses=function()
{
 #guess varT
 D1=subset(D, (D$Trt=="T"))
 m=lm(log(Y)~factor(Seq)+factor(Per)   , data=D1)
 varT=anova(m)["Residuals", "Mean Sq"]
 
 #guess varWR
 D1=subset(D, (D$Trt=="R"))
 m=lm(log(Y)~factor(Seq)+factor(Per)+factor(Subj)  , data=D1)
 varWR=anova(m)["Residuals", "Mean Sq"] 

 #guess varBR
 D1=subset(D, (D$Trt=="R"))
 m=lm(log(Y)~factor(Seq)+factor(Per)   , data=D1)
 varBR=anova(m)["Residuals", "Mean Sq"] - varWR

 #guess varRT this is also a between-variance;
 #might be perhaps close to varBR?
 varRT=varBR     

 rslt=c(varT, varBR, varWR, varRT)
 return(rslt)
}

Run.REML=function(parIni=NULL, Want.Intercept=F)
## call this with a vector of four custom guesses
## of varT, varBR, varWR, and  varRT (in this sequence).
## Or leave it out to allow the script to use a set of qualified guesses
{
  X<<-Create.Model.Matrix.Fixed.Effects(D, Want.Intercept) ##public = easier
  y<<-log(D$Y)                                             ##public = easier
  if (is.null(parIni)) parIni=Some.Initial.Guesses()
 
  F=optim(par=parIni, fn=Log.REML, 
          control=list(reltol=1e-9, maxit=2000,
          trace=F, fnscale= -1))
  if (F$convergence ==0) s="The model converged." else s="The model did not converge."
  Nms=c("varT","varBR", "varWR", "varBRT")
  X=data.frame(Nms, parIni, F$par)
  names(X)=c("Var.Component", "Ini.guess", "REML.Estimate")
  rslt=list(Outcome=s, LogLikelihood= F$value,
            Fixed.eff=est.b, Random.eff=X, V=Create.CovM(F$par))
  return(rslt)
}

L=Run.REML() ; print(L)

Pass or fail!
ElMaestro
ElMaestro
★★★

Denmark,
2026-05-30 05:36
(20 d 00:40 ago)

@ ElMaestro
Posting: # 24634
Views: 1,048
 

 mittyri can do it!

I am so impressed, it did not take long for mittyri to propose code that seems to do it.

His code looks like this:


ginv.sym <- function(M, tol=sqrt(.Machine$double.eps)) {
  M <- (M + t(M))/2
  ee <- eigen(M, symmetric=TRUE)
  keep <- ee$values > tol * max(abs(ee$values))
  V <- ee$vectors[, keep, drop=FALSE]
  V %*% diag(1/ee$values[keep], nrow=sum(keep)) %*% t(V)
}
 
Satt.CI.TR <- function(L, alpha=0.10) {
  theta <- L$Random.eff$REML.Estimate
  V <- Create.CovM(theta)
  Vinv <- solve(V)
 
  C <- solve(t(X) %*% Vinv %*% X)
  beta <- C %*% t(X) %*% Vinv %*% y
 
  l <- rep(0, ncol(X))
  names(l) <- colnames(X)
  l["TrtT"] <-  1
  l["TrtR"] <- -1
  l <- matrix(l, ncol=1)
 
  est <- drop(t(l) %*% beta)
  q   <- drop(t(l) %*% C %*% l)
  se  <- sqrt(q)
 
  # Create.CovM is linear in theta.
  dV <- lapply(seq_along(theta), function(i) {
    e <- numeric(length(theta))
    e[i] <- 1
    Create.CovM(e)
  })
 
  P <- Vinv - Vinv %*% X %*% C %*% t(X) %*% Vinv
  ymat <- matrix(y, ncol=1)
  tr <- function(M) sum(diag(M))
 
  # Observed restricted information, analytically
  J <- matrix(NA_real_, length(theta), length(theta))
  for (i in seq_along(theta)) {
    for (j in seq_along(theta)) {
      J[i,j] <-
        drop(t(ymat) %*% P %*% dV[[i]] %*% P %*% dV[[j]] %*% P %*% ymat) -
        0.5 * tr(P %*% dV[[i]] %*% P %*% dV[[j]])
    }
  }
  J <- (J + t(J))/2
  rownames(J) <- colnames(J) <- L$Random.eff$Var.Component
 
  A <- tryCatch(solve(J), error=function(e) ginv.sym(J))
  A <- (A + t(A))/2
 
  # gradient of q with respect to covariance parameters
  g <- numeric(length(theta))
  for (i in seq_along(theta)) {
    dC <- C %*% t(X) %*% Vinv %*% dV[[i]] %*% Vinv %*% X %*% C
    g[i] <- drop(t(l) %*% dC %*% l)
  }
 
  var.q <- drop(t(g) %*% A %*% g)
  df <- 2 * q^2 / var.q
 
  tcrit <- qt(1 - alpha/2, df)
  ci.log <- est + c(-1, 1) * tcrit * se
 
  data.frame(
    contrast = "T - R",
    estimate.log = est,
    SE = se,
    df.Satt = df,
    lower.log = ci.log[1],
    upper.log = ci.log[2],
    ratio = exp(est),
    lower90 = exp(ci.log[1]),
    upper90 = exp(ci.log[2])
  )
}
 
print(Satt.CI.TR(L))
# contrast estimate.log         SE df.Satt   lower.log  upper.log    ratio   lower90  upper90
# 1    T - R   0.02239143 0.03031727 19.8905 -0.02991128 0.07469414 1.022644 0.9705316 1.077555
 


Impressive. :clap:

Pass or fail!
ElMaestro
Helmut
★★★
avatar
Homepage
Vienna, Austria,
2026-05-30 11:33
(19 d 18:43 ago)

@ ElMaestro
Posting: # 24635
Views: 1,025
 

 Something missing?

Hi to both of you!

Sorry that the forum has hiccups and is not accessible most of the time. Fucking DoS-attacks again…

❝ His code looks like this:

I wanted to explore John’s data set, which – as far as I remember – did not converge; neither in SAS nor in Phoenix.

L <- read.csv(file = "https://bebac.at/downloads/nasty1.csv",
              colClasses = c(rep("factor", 4), "numeric"))


But if I try the code, I get:

Error in Create.CovM(theta) : could not find function "Create.CovM"


Am I missing something?

Dif-tor heh smusma 🖖🏼 Довге життя Україна! [image]
Helmut Schütz
[image]

The quality of responses received is directly proportional to the quality of the question asked. 🚮
Science Quotes
ElMaestro
★★★

Denmark,
2026-05-30 13:52
(19 d 16:24 ago)

@ Helmut
Posting: # 24636
Views: 987
 

 Something missing?

Hi Hötzi,

❝ But if I try the code, I get:

Error in Create.CovM(theta) : could not find function "Create.CovM"


❝ Am I missing something?


Yes, it looks like you are missing a function called Create.CovM :-D

Try this:


Create.Model.Matrix.Fixed.Effects=function(D, Want.Intercept=F)
  ##creates a full column-rank model matrix
  ##fixed: Treatment, Period, Sequence
{
  ##first the two treatments
  Int=rep(1, nrow(D))
  TrtT=as.numeric(as.character(D$Trt)=="T")
  TrtR=as.numeric(as.character(D$Trt)=="R")
  X=cbind(TrtT, TrtR)
  if (Want.Intercept) X=cbind(Int, TrtR)
  R= qr(X)$rank
 
  ##now the Periods
  for (p in unique(D$Per))
  {
    v=as.numeric(D$Per==p)
    XX=data.frame(X, v)
    names(XX)[ncol(XX)] = paste("Per", p, sep="")
    rnk=qr(XX)$rank
    if (rnk>R)
    {
      X=XX
      R=rnk
    }
  }
 
  ##and sequence
  for (q in unique(D$Seq))
  {
    v=as.numeric(D$Seq==q)
    XX=data.frame(X, v)
    names(XX)[ncol(XX)] = paste("Seq", q, sep="")
    rnk=qr(XX)$rank
    if (rnk>R)
    {
      X=XX
      R=rnk
    }
  }
  return(as.matrix(X))
}

Create.CovM=function(Params)
  ##block diagonal covariance matrix
{
  NIter <<- NIter+1; Trc<<-rbind(Trc, Params)
  varT=Params[1]   varBR=Params[2]   varWR=Params[3]   varRT=Params[4]   Nobs=length(D$Y)
  V=matrix(0,ncol=Nobs, nrow=Nobs) ##creates the square matrix of zeros
  for (iRow in 1:Nobs)
    for (iCol in 1:Nobs)
    {
      ## the diagonal
      if (iCol==iRow)
      {
        if (D$Trt[iRow]=="T") V[iRow,iCol]=V[iRow,iCol]+varT
        if (D$Trt[iRow]=="R") V[iRow,iCol]=V[iRow,iCol]+varWR +varBR
      }
      ## off diagonal
      if (iCol!=iRow)
        if (D$Subj[iRow]==D$Subj[iCol])
        {
          if (D$Trt[iCol]==D$Trt[iRow]) V[iRow,iCol]= V[iRow,iCol]+varBR
          if (D$Trt[iCol]!=D$Trt[iRow]) V[iRow,iCol]= V[iRow,iCol]+varRT
        }
    }
  return(as.matrix(V))
}

Log.REML=function(Pars)
{
  CovM=Create.CovM(Pars) ##this is V
  k=  -((length(y)-ncol(X))/2)*log(2*pi)
  A= -0.5*log( det(CovM) )
  B= -0.5*log( det(t(X) %*% solve(CovM) %*% X))
  est.b <<- solve(t(X) %*% solve(CovM) %*% X) %*% t(X) %*% solve(CovM) %*% y
  tmp= y - X %*% est.b
  C=-0.5 *(t(tmp) %*% solve(CovM) %*% tmp)
  return(k+A+B+C)
}


Some.Initial.Guesses=function()
{
  #guess varT
  D1=subset(D, (D$Trt=="T"))
  m=lm(log(Y)~factor(Seq)+factor(Per)   , data=D1)
  varT=anova(m)["Residuals", "Mean Sq"]
 
  #guess varWR
  D1=subset(D, (D$Trt=="R"))
  m=lm(log(Y)~factor(Seq)+factor(Per)+factor(Subj)  , data=D1)
  varWR=anova(m)["Residuals", "Mean Sq"] 
 
  #guess varBR
  D1=subset(D, (D$Trt=="R"))
  m=lm(log(Y)~factor(Seq)+factor(Per)   , data=D1)
  varBR=anova(m)["Residuals", "Mean Sq"] - varWR
 
  #guess varRT this is also a between-variance;
  #might be perhaps close to varBR?
  varRT=  varBR
  #varWR= varWR+.3*(varBR-varWR)
  rslt=c(varT, varBR, varWR, varRT)
  #rslt=runif(4, min =-.01, max = +.01) + rslt
  #rslt=c(.1, .04, .04, .02)
  #rslt = rep(varBR, 4)
  return(rslt)
}



PlotHistory=function(L)
{
  u=L$History
  for (i in 1:ncol(u))
  {
    mx=max(u[,i])
    #u[,i]=u[,i] / mx
  }
  mx=max(u); mn=min(u); ix=c(1:nrow(u))
  cls=c("blue", "orange", "pink", "green")
  plot(ix, u[,1], col=cls[1], ylim=c(mn, mx), cex=0.01); lines(ix, u[,1], col=cls[1])
  lines(ix, u[,2], col=cls[2])
  lines(ix, u[,3], col=cls[3])
  lines(ix, u[,4], col=cls[4])
 
}


ginv.sym <- function(M, tol=sqrt(.Machine$double.eps)) {
  M <- (M + t(M))/2
  ee <- eigen(M, symmetric=TRUE)
  keep <- ee$values > tol * max(abs(ee$values))
  V <- ee$vectors[, keep, drop=FALSE]   V %*% diag(1/ee$values[keep], nrow=sum(keep)) %*% t(V)
}

Satt.CI.TR <- function(L, alpha=0.10) {
  theta <- L$Random.eff$REML.Estimate
  V <- Create.CovM(theta)
  Vinv <- solve(V)
 
  C <- solve(t(X) %*% Vinv %*% X)
  beta <- C %*% t(X) %*% Vinv %*% y
 
  l <- rep(0, ncol(X))
  names(l) <- colnames(X)
  l["TrtT"] <-  1
  l["TrtR"] <- -1
  l <- matrix(l, ncol=1)
 
  est <- drop(t(l) %*% beta)
  q   <- drop(t(l) %*% C %*% l)
  se  <- sqrt(q)
 
  # Create.CovM is linear in theta.
  dV <- lapply(seq_along(theta), function(i) {
    e <- numeric(length(theta))
    e[i] <- 1
    Create.CovM(e)
  })
 
  P <- Vinv - Vinv %*% X %*% C %*% t(X) %*% Vinv
  ymat <- matrix(y, ncol=1)
  tr <- function(M) sum(diag(M))
 
  # Observed restricted information, analytically
  J <- matrix(NA_real_, length(theta), length(theta))
  for (i in seq_along(theta)) {
    for (j in seq_along(theta)) {
      J[i,j] <-
        drop(t(ymat) %*% P %*% dV[[i]] %*% P %*% dV[[j]] %*% P %*% ymat) -
        0.5 * tr(P %*% dV[[i]] %*% P %*% dV[[j]])
    }
  }
  J <- (J + t(J))/2
  rownames(J) <- colnames(J) <- L$Random.eff$Var.Component
 
  A <- tryCatch(solve(J), error=function(e) ginv.sym(J))
  A <- (A + t(A))/2
 
  # gradient of q with respect to covariance parameters
  g <- numeric(length(theta))
  for (i in seq_along(theta)) {
    dC <- C %*% t(X) %*% Vinv %*% dV[[i]] %*% Vinv %*% X %*% C
    g[i] <- drop(t(l) %*% dC %*% l)
  }
 
  var.q <- drop(t(g) %*% A %*% g)
  df <- 2 * q^2 / var.q
 
  tcrit <- qt(1 - alpha/2, df)
  ci.log <- est + c(-1, 1) * tcrit * se
 
  data.frame(
    contrast = "T - R",
    estimate.log = est,
    SE = se,
    df.Satt = df,
    lower.log = ci.log[1],
    upper.log = ci.log[2],
    ratio = exp(est),
    lower90 = exp(ci.log[1]),
    upper90 = exp(ci.log[2])
  )
}

anac=function(x) {return(as.numeric(as.character(x)))}

Run.REML.from.file=function(sFN, parIni=NULL, Want.Intercept=F)
  ## call this with a vector of four custom guesses
  ## of varT, varBR, varWR, and  varRT (in this sequence).
  ## Or leave it out to allow the script to use a set of qualified guesses
{
  try({
  rslt=1;
  D<<-read.csv(sFN, header=T, sep="\t")
  options(warn = -1)
  D$Y<<-anac(D$Y);options(warn =0)

  #convert A to T and B to R
  D<<-subset(D, is.na(anac(D$Y)) ==F)
  for (i in 1:nrow(D))
  {
    if (D$Trt[i] =="A") D$Trt[i]<<-"T"
    if (D$Trt[i] =="B") D$Trt[i]<<-"R"
    #yes I hate gsub
  }
 
 
  print(D)
  NIter <<-0; Trc<<-NULL
  X<<-Create.Model.Matrix.Fixed.Effects(D, Want.Intercept) ##public = easier
  y<<-log(D$Y)                                             ##public = easier
  if (is.null(parIni)) parIni=Some.Initial.Guesses()
 
  LB=c(.001, .001, .001, .001)
  UB=c(.17, .17, .14, .17 )
  F=optim(par=parIni, fn=Log.REML,   hessian=T, 
          control=list(reltol=1e-8, maxit=1000, alpha=1, beta=.5, gamma=2,
                       trace=F, fnscale= -1))
 
  #F=optim(par=parIni, fn=Log.REML,   hessian=T,  method="L-BFGS-B",
  #        lower=LB, upper=UB,
  #        control=list(reltol=1e-4, maxit=1000, alpha=1, beta=.5, gamma=2,
  #                     trace=F, fnscale= +1))
  if (F$convergence ==0) s="The model converged." else s="The model did not converge."
  Nms=c("varT","varBR", "varWR", "varBRT")
  X=data.frame(Nms, parIni, F$par)
  names(X)=c("Var.Component", "Ini.guess", "REML.Estimate")
 
  L=list(Outcome=s, LogLikelihood= F$value,
            Fixed.eff=est.b, Random.eff=X, V=Create.CovM(F$par),
            Hess=F$hessian, NI=NIter, History=Trc)
  PlotHistory(L)
  rslt=(Satt.CI.TR(L))
  print(L$History[nrow(L$History),])
  print(nrow(L$History))
  print(X)
  })
 
  if (identical(1, rslt)) return("Trouble.")
  return(rslt)
}




You then beam up Scotty with a call such as:

u=Run.REML.from.file("John300526.csv", parIni=c(1, 1, .5, 1))
print(u)



And you should get something that might look like dis:
  Var.Component Ini.guess REML.Estimate
1          varT       1.0     0.8900386
2         varBR       1.0     1.1451866
3         varWR       0.5     0.4208527
4        varBRT       1.0     0.8382483
> print(u)
  contrast estimate.log        SE df.Satt  lower.log upper.log    ratio   lower90  upper90
1    T - R   0.01012292 0.1313466  27.449 -0.2134683 0.2337142 1.010174 0.8077778 1.263283


For this dataset it is apparently troublesome to figure out a good initial guess (at least the default code doesn't do it too well); the variances are rather high. So we just supply something that works better. I was at some point looking into writing a loop that tries e.g. 20 different approaches to initial guesses and simply reports when one of them results in convergence. There is a bit of work to do. Initial guesses have always been a sore topic for optimisers. If WNL or SAS did not converge, then I bet it is due to a bad combination of initial guesses and optimiser. When the user does not supply some proper initial guesses, the software either not start the optimiser or it will need to guess on its own. That's where the trouble is.


I can't upload the file using the forum's upload function, so I will send it to you directly. :-)

Pass or fail!
ElMaestro
ElMaestro
★★★

Denmark,
2026-05-30 15:08
(19 d 15:08 ago)

@ ElMaestro
Posting: # 24637
Views: 981
 

 A slight improvement

The following code makes a few different attempts.
It first tries the plain old guesses. if that doesn't work it adds a little scatter to the guesses.
This seems to do the trick.

I have not tested anything thoroughly here. Am horizontal in Vadodara.


Create.Model.Matrix.Fixed.Effects=function(Da, Want.Intercept=F)
  ##creates a full column-rank model matrix
  ##fixed: Treatment, Period, Sequence
{
  ##first the two treatments
  Int=rep(1, nrow(Da))
  TrtT=as.numeric(as.character(Da$Trt)=="T")
  TrtR=as.numeric(as.character(Da$Trt)=="R")
  X=cbind(TrtT, TrtR)
  if (Want.Intercept) X=cbind(Int, TrtR)
  R= qr(X)$rank
 
  ##now the Periods
  for (p in unique(Da$Per))
  {
    v=as.numeric(Da$Per==p)
    XX=data.frame(X, v)
    names(XX)[ncol(XX)] = paste("Per", p, sep="")
    rnk=qr(XX)$rank
    if (rnk>R)
    {
      X=XX
      R=rnk
    }
  }
 
  ##and sequence
  for (q in unique(Da$Seq))
  {
    v=as.numeric(Da$Seq==q)
    XX=data.frame(X, v)
    names(XX)[ncol(XX)] = paste("Seq", q, sep="")
    rnk=qr(XX)$rank
    if (rnk>R)
    {
      X=XX
      R=rnk
    }
  }
  return(as.matrix(X))
}

Create.CovM=function(Params)
  ##block diagonal covariance matrix
{
  NIter <<- NIter+1; Trc<<-rbind(Trc, Params)
  varT=Params[1]
  varBR=Params[2]
  varWR=Params[3]
  varRT=Params[4]
  Nobs=length(Da$Y)
  V=matrix(0,ncol=Nobs, nrow=Nobs) ##creates the square matrix of zeros
  for (iRow in 1:Nobs)
    for (iCol in 1:Nobs)
    {
      ## the diagonal
      if (iCol==iRow)
      {
        if (Da$Trt[iRow]=="T") V[iRow,iCol]=V[iRow,iCol]+varT
        if (Da$Trt[iRow]=="R") V[iRow,iCol]=V[iRow,iCol]+varWR +varBR
      }
      ## off diagonal
      if (iCol!=iRow)
        if (Da$Subj[iRow]==Da$Subj[iCol])
        {
          if (Da$Trt[iCol]==Da$Trt[iRow]) V[iRow,iCol]= V[iRow,iCol]+varBR
          if (Da$Trt[iCol]!=Da$Trt[iRow]) V[iRow,iCol]= V[iRow,iCol]+varRT
        }
    }
  return(as.matrix(V))
}

Log.REML=function(Pars)
{
  CovM=Create.CovM(Pars) ##this is V
  k=  -((length(y)-ncol(X))/2)*log(2*pi)
  A= -0.5*log( det(CovM) )
  B= -0.5*log( det(t(X) %*% solve(CovM) %*% X))
  est.b <<- solve(t(X) %*% solve(CovM) %*% X) %*% t(X) %*% solve(CovM) %*% y
  tmp= y - X %*% est.b
  C=-0.5 *(t(tmp) %*% solve(CovM) %*% tmp)
  return(k+A+B+C)
}


Some.Initial.Guesses=function()
{
  #guess varT
  D1=subset(Da, (Da$Trt=="T"))
  m=lm(log(Y)~factor(Seq)+factor(Per)   , data=D1)
  varT=anova(m)["Residuals", "Mean Sq"]
 
  #guess varWR
  D1=subset(Da, (Da$Trt=="R"))
  m=lm(log(Y)~factor(Seq)+factor(Per)+factor(Subj)  , data=D1)
  varWR=anova(m)["Residuals", "Mean Sq"] 
 
  #guess varBR
  D1=subset(Da, (Da$Trt=="R"))
  m=lm(log(Y)~factor(Seq)+factor(Per)   , data=D1)
  varBR=anova(m)["Residuals", "Mean Sq"] - varWR
 
  #guess varRT this is also a between-variance;
  #might be perhaps close to varBR?
  varRT=  varBR
  #varWR= varWR+.3*(varBR-varWR)
  rslt=c(varT, varBR, varWR, varRT)
  #rslt=runif(4, min =-.01, max = +.01) + rslt
  #rslt=c(.1, .04, .04, .02)
  #rslt = rep(varBR, 4)
  cat("Ini guesses:\n")
  print(rslt);
  return(rslt)
}



PlotHistory=function(L)
{
  u=L$History
  for (i in 1:ncol(u))
  {
    mx=max(u[,i])
    #u[,i]=u[,i] / mx
  }
  mx=max(u); mn=min(u); ix=c(1:nrow(u))
  cls=c("blue", "orange", "pink", "green")
  plot(ix, u[,1], col=cls[1], ylim=c(mn, mx), cex=0.01, ylab="Variance estimate", xlab="No. evaluation of V");
  lines(ix, u[,1],  col=cls[1])
  lines(ix, u[,2], col=cls[2])
  lines(ix, u[,3], col=cls[3])
  lines(ix, u[,4], col=cls[4])
  s=c("varT", "varBR", "varWR", "varBRT")
  for (i in 1:4)
    {
     x=floor((.5 + i/25) * nrow(u)  )
     w=strwidth(s[i]); h=strheight(s[i])
     rect(x-.5*w, u[x,i]-.5*h, x+.5*w, u[x,i]+.5*h, col="white", border="white")
     text(x, u[x,i], s[i], col=cls[i])
    }
}


ginv.sym <- function(M, tol=sqrt(.Machine$double.eps)) {
  M <- (M + t(M))/2
  ee <- eigen(M, symmetric=TRUE)
  keep <- ee$values > tol * max(abs(ee$values))
  V <- ee$vectors[, keep, drop=FALSE]   V %*% diag(1/ee$values[keep], nrow=sum(keep)) %*% t(V)
}

Satt.CI.TR <- function(L, alpha=0.10) {
  theta <- L$Random.eff$REML.Estimate
  V <- Create.CovM(theta)
  Vinv <- solve(V)
 
  C <- solve(t(X) %*% Vinv %*% X)
  beta <- C %*% t(X) %*% Vinv %*% y
 
  l <- rep(0, ncol(X))
  names(l) <- colnames(X)
  l["TrtT"] <-  1
  l["TrtR"] <- -1
  l <- matrix(l, ncol=1)
 
  est <- drop(t(l) %*% beta)
  q   <- drop(t(l) %*% C %*% l)
  se  <- sqrt(q)
 
  # Create.CovM is linear in theta.
  dV <- lapply(seq_along(theta), function(i) {
    e <- numeric(length(theta))
    e[i] <- 1
    Create.CovM(e)
  })
 
  P <- Vinv - Vinv %*% X %*% C %*% t(X) %*% Vinv
  ymat <- matrix(y, ncol=1)
  tr <- function(M) sum(diag(M))
 
  # Observed restricted information, analytically
  J <- matrix(NA_real_, length(theta), length(theta))
  for (i in seq_along(theta)) {
    for (j in seq_along(theta)) {
      J[i,j] <-
        drop(t(ymat) %*% P %*% dV[[i]] %*% P %*% dV[[j]] %*% P %*% ymat) -
        0.5 * tr(P %*% dV[[i]] %*% P %*% dV[[j]])
    }
  }
  J <- (J + t(J))/2
  rownames(J) <- colnames(J) <- L$Random.eff$Var.Component
 
  A <- tryCatch(solve(J), error=function(e) ginv.sym(J))
  A <- (A + t(A))/2
 
  # gradient of q with respect to covariance parameters
  g <- numeric(length(theta))
  for (i in seq_along(theta)) {
    dC <- C %*% t(X) %*% Vinv %*% dV[[i]] %*% Vinv %*% X %*% C
    g[i] <- drop(t(l) %*% dC %*% l)
  }
 
  var.q <- drop(t(g) %*% A %*% g)
  df <- 2 * q^2 / var.q
 
  tcrit <- qt(1 - alpha/2, df)
  ci.log <- est + c(-1, 1) * tcrit * se
 
  data.frame(
    contrast = "T - R",
    estimate.log = est,
    SE = se,
    df.Satt = df,
    lower.log = ci.log[1],
    upper.log = ci.log[2],
    ratio = exp(est),
    lower90 = exp(ci.log[1]),
    upper90 = exp(ci.log[2])
  )
}

anac=function(x) {return(as.numeric(as.character(x)))}

Run.REML.from.fileB=function(sFN,   Want.Intercept=F)
  ## call this with a vector of four custom guesses
  ## of varT, varBR, varWR, and  varRT (in this sequence).
  ## Or leave it out to allow the script to use a set of qualified guesses
{
 
  rslt=1; J=0
  while ((identical(rslt,1)) & (J<12))
  { 
  try({
 
  Da<<-read.csv(sFN, header=T, sep="\t")
  options(warn = -1)
  Da$Y<<-anac(Da$Y);

  #convert A to T and B to R
  Da<<-subset(Da, is.na(anac(Da$Y)) ==F)
  for (i in 1:nrow(Da))
  {
    if (Da$Trt[i] =="A") Da$Trt[i]<<-"T"
    if (Da$Trt[i] =="B") Da$Trt[i]<<-"R"
    #yes I hate gsub
  }
 
 
  print(Da)
  NIter <<-0; Trc<<-NULL
  X<<-Create.Model.Matrix.Fixed.Effects(Da, Want.Intercept) ##public = easier
  y<<-log(Da$Y)                                             ##public = easier
  parIniB=Some.Initial.Guesses()
  if (J>0) parIniB=if (J>0) parIniB=Some.Initial.Guesses()*runif(4, .9, 1.1)
 
  F=optim(par=parIniB, fn=Log.REML,   hessian=T, 
          control=list(reltol=1e-8, maxit=1000, alpha=1, beta=.5, gamma=2,
                       trace=F, fnscale= -1))
 
  #F=optim(par=parIni, fn=Log.REML,   hessian=T,  method="L-BFGS-B",
  #        lower=LB, upper=UB,
  #        control=list(reltol=1e-4, maxit=1000, alpha=1, beta=.5, gamma=2,
  #                     trace=F, fnscale= +1))
  if (F$convergence ==0) s="The model converged." else s="The model did not converge."
  Nms=c("varT","varBR", "varWR", "varBRT")
  X=data.frame(Nms, parIniB, F$par)
  names(X)=c("Var.Component", "Ini.guess", "REML.Estimate")
 
  L=list(Outcome=s, LogLikelihood= F$value,
            Fixed.eff=est.b, Random.eff=X, V=Create.CovM(F$par),
            Hess=F$hessian, NI=NIter, History=Trc)
  PlotHistory(L)
  rslt=(Satt.CI.TR(L))
  print(L$History[nrow(L$History),])
  print(nrow(L$History))
  print(X)
  })
  J=J+1
  #  if (!identical(1, rslt)) return("Trouble.")
  }
  options(warn =0)
  return(rslt)
}


Pass or fail!
ElMaestro
ElMaestro
★★★

Denmark,
2026-05-30 20:56
(19 d 09:20 ago)

@ ElMaestro
Posting: # 24638
Views: 939
 

 Remoulade

Hi again,

I looked back at some of the remarks in the old thread with John's data.
And I was reminded how precision of an estimate is, to some people, much like a holy grail.
The difference between .12345 and .12346 is of importance... well... to some:-D
So I will likely be murdered when someone notices I used tol=1e-8 for the Nelder-Mead optimiser in my previous post and therefore the estimates are way off, right? ;-)

So I bring you Remoulade.
This is just code delivering a kind of proof of concept. It is not final, it is not release-grade, it is not pretty and it is not conformant to any standards. But it does the job on my machine.


The function proto is
Remoulade=function(sFN,   tol.varWR=1e-6)

(full portion of Remoulade at the end of this post)


where tol.varWR simply indicates how well (more or less "at which decimal place") we wish to know the estimate on the intrasubject variability for Ref.
We start by finding a set of starter values that will make Nelder-Mead converge. We do so using a cheap, fast and dirty level of tol.
Once that is done, we iteratively decrease tol for Nelder-Mead (using the starter values that work), until we can't improve the estimate of varWR more than tol.varWR. Then we have our estimate to the desired precision.



set.seed(2173)
u=Remoulade("John300526.csv", 1e-5);
print(u)


and I get:

$CI
  contrast estimate.log        SE  df.Satt  lower.log upper.log    ratio   lower90 upper90
1    T - R   0.01016031 0.1313957 27.40926 -0.2135258 0.2338465 1.010212 0.8077313 1.26345

$Variances
  Var.Component Ini.guess REML.Estimate
1          varT 0.8994143     0.8896047
2         varBR 1.1361293     1.1455132
3         varWR 0.4033036     0.4210183
4        varBRT 1.0403790     0.8380152

$parIni
[1] 0.8994143 1.1361293 0.4033036 1.0403790

$Tol
[1] 1e-10



This can be improved much further. For example, if someone specifies a too low value of tol.varWR then Nelder-Mead will not and/or can not converge. And so on. Such matters could be captured. And much else.

Note that the curvature of the likelihood surface depends on the data. That's why a universal tol for NM can't be defined, or at least I do not see a good way to do so.

Here's Remoulade:




Remoulade=function(sFN,   tol.varWR=1e-6)
{
  rslt=1; J=0
  Da<<-read.csv(sFN, header=T, sep="\t")
  options(warn = -1)
  Da$Y<<-anac(Da$Y);
 
  #convert A to T and B to R
  Da<<-subset(Da, is.na(anac(Da$Y)) ==F)
  for (i in 1:nrow(Da))
  {
    if (Da$Trt[i] =="A") Da$Trt[i]<<-"T"
    if (Da$Trt[i] =="B") Da$Trt[i]<<-"R"
    #yes I hate gsub
  }
 
  while ((identical(rslt,1)) & (J<12))
  { 
    try({
      NIter <<-0; Trc<<-NULL
      X<<-Create.Model.Matrix.Fixed.Effects(Da, F) ##public = easier
      y<<-log(Da$Y)                                             ##public = easier
      parIniB=Some.Initial.Guesses()
      if (J>0) parIniB=Some.Initial.Guesses()*runif(4, .9, 1.1)
     
     
      Foo=optim(par=parIniB, fn=Log.REML,   hessian=T, 
              control=list(reltol=1e-6, maxit=1000, alpha=1, beta=.5, gamma=2,
                           trace=F, fnscale= -1))

            if (Foo$convergence ==0) s="The model converged." else s="The model did not converge."
      Nms=c("varT","varBR", "varWR", "varBRT")
      X=data.frame(Nms, parIniB, Foo$par)
      names(X)=c("Var.Component", "Ini.guess", "REML.Estimate")
     
      L=list(Outcome=s, LogLikelihood= Foo$value,
             Fixed.eff=est.b, Random.eff=X, V=Create.CovM(Foo$par),
             Hess=Foo$hessian, NI=NIter, History=Trc)
      #PlotHistory(L)
      rslt=list(CI=Satt.CI.TR(L), Variances=X, parIni=parIniB)
      #print(L$History[nrow(L$History),])
      #print(nrow(L$History))
      #print(X)
    })
    J=J+1
    #  if (!identical(1, rslt)) return("Trouble.")
  }
  options(warn =0)
  if (identical(1, rslt)) return("Trouble.")
 
 
  print(rslt)
 
  Tl=1e-6
  if (!identical(1, rslt))
  { 
    cat("Stat A\n")
     #now we can try to improve the estimate; we are quite sure we will converge
     Last.varWR=1e10; Curr.varWR=rslt$Variances[3,3];
     
     while (abs(Last.varWR - Curr.varWR) > tol.varWR)
     {
       Tl=Tl / 10
       cat("Trying to improve varWR at Tl=", Tl, Curr.varWR, "\n")
       Foo=optim(par=parIniB, fn=Log.REML,
               control=list(reltol=Tl, maxit=2000, alpha=1, beta=.5, gamma=2,
               trace=F, fnscale= -1))
       
       if (Foo$convergence ==0) s="The model converged." else s="The model did not converge."
       Nms=c("varT","varBR", "varWR", "varBRT")
       X=data.frame(Nms, parIniB, Foo$par)
       names(X)=c("Var.Component", "Ini.guess", "REML.Estimate")
       
       L=list(Outcome=s, LogLikelihood= Foo$value,
              Fixed.eff=est.b, Random.eff=X, V=Create.CovM(Foo$par),
              Hess=Foo$hessian, NI=NIter, History=Trc)
       #PlotHistory(L)
       rslt=list(CI=Satt.CI.TR(L), Variances=X, parIni=parIniB, Tol=Tl)
       Last.varWR=Curr.varWR;
       Curr.varWR=rslt$Variances[3,3];
     }
     
  }
  PlotHistory(L)
  return(rslt)
}


Pass or fail!
ElMaestro
Helmut
★★★
avatar
Homepage
Vienna, Austria,
2026-05-31 12:46
(18 d 17:30 ago)

@ ElMaestro
Posting: # 24639
Views: 887
 

 Tartar sauce

Hi ElMaestro,

Can you try John’s 2nd data set as well? Our compiled results in this post.

Dif-tor heh smusma 🖖🏼 Довге життя Україна! [image]
Helmut Schütz
[image]

The quality of responses received is directly proportional to the quality of the question asked. 🚮
Science Quotes
ElMaestro
★★★

Denmark,
2026-05-31 13:43
(18 d 16:33 ago)

@ Helmut
Posting: # 24640
Views: 883
 

 Tartar sauce

Hi Hötzi,

❝ Can you try John’s 2nd data set as well? Our compiled results in this post.


Sure, at tol=1e-8 for NM I get:

  Var.Component Ini.guess REML.Estimate
1          varT      0.51    0.22835698
2         varBR      0.52    0.13851318
3         varWR      0.30    0.06247833
4        varBRT      0.53    0.14791812
  contrast estimate.log         SE  df.Satt  lower.log  upper.log     ratio   lower90  upper90
1    T - R  -0.04817524 0.04480591 49.27421 -0.1232866 0.02693609 0.9529668 0.8840103 1.027302


At 1e-12 I get:

  Var.Component Ini.guess REML.Estimate
1          varT      0.51    0.22836622
2         varBR      0.52    0.13850334
3         varWR      0.30    0.06248276
4        varBRT      0.53    0.14792400
  contrast estimate.log         SE  df.Satt  lower.log  upper.log     ratio   lower90  upper90
1    T - R  -0.04817519 0.04480368 49.28394 -0.1232825 0.02693213 0.9529668 0.8840139 1.027298

Pass or fail!
ElMaestro
UA Flag
Activity
 Admin contact
23,654 posts in 4,992 threads, 1,570 registered users;
186 visitors (0 registered, 186 guests [including 18 identified bots]).
Forum time: 06:16 CEST (Europe/Vienna)

Actually, science starts to become interesting
only where it ends.    Justus von Liebig

The Bioequivalence and Bioavailability Forum is hosted by
BEBAC Ing. Helmut Schütz
HTML5