R/testRetest.r

Defines functions dist mlr2 mlr1 print_psych.testRetest alpha.1

#Developed January 2018 while writing an article on reliability
#modified March 2021 in response to some helpful suggestions by Jacob Lee

"testRetest" <- function(t1,t2=NULL,keys=NULL,id="id",time= "time",select=NULL,check.keys=TRUE,warnings=TRUE,lmer=TRUE,sort=TRUE) {
 cl <- match.call() 
 
 #first, some basic checks of the data
 #there are several ways we can input the data
 #x and y as time 1 and time 2
 #x includes a time variable and an id variable
 x <- t1
 
 y <- t2
 if(NCOL(x) ==1) {just.test <-TRUE } else {just.test <- FALSE}
 keys.orig <- keys
  #first check if we have a y variable, if not, then create it
 
 if(is.null(y)) {n.times <- table(x[time])    
  #sort the data by id and time
  if(id %in% colnames(x)) {
 if(sort) x <- psychTools::dfOrder(x,c(time,id),) }   #don't sort if we don't have ids
 y <- x[x[,time] == names(n.times)[2],]
 x <- x[x[,time] == names(n.times)[1],]
 }  else {
 if(sort) {x <- psychTools::dfOrder(x,c(time,id),) 
           y <- psychTools::dfOrder(y,c(time,id),)
          }} 
 
  #only take matching cases   
if(NROW(x) != NROW(y) ) {warning("The number of subjects in x do not  match those in y")
  not.missing.x <- x[,id] %in% y[,id]
  not.missing.y <- y[,id] %in% x[,id]
  missing.idx <- x[!not.missing.x,id]
  missing.idy <- y[!not.missing.y,id]
  cat("\nThe non-matched subjects were\n",missing.idx, missing.idy,"\n I have deleted them")
  
  x <- x[not.missing.x,]
  y <- y[not.missing.y,]
  } 
 
 n.obs <- NROW(x)
 if(!just.test) {
 if(!is.null(select)) {items <- select
    x <- x[select]
    y <- y[select]
    }

 if(is.null(keys)){ items <- colnames(x) [!colnames(x) %in% c(id,time)]} else {items <- keys }
  #first check if we should reverse any items and convert location numbers (if specified) to location names
 n.items <- length(items)
  if(is.character(items)) {
   temp <- rep(1,n.items)
 
   temp [strtrim(items,1)=="-"] <- -1
   if(any( temp < 0) )  {items <- sub("-","",items) }
   } else {temp <- sign(items)
      items <- colnames(x)[abs(items)] 
    }
 #check for bad input   -- the Mollycoddle option 
if(any( !(items %in% colnames(x)) )) {
 cat("\nVariable names in keys are incorrectly specified. Offending items are ", items[which(!(items %in% colnames(x)))],"\n")
 stop("I am stopping because of improper input in the scoring keys.  See the list above for the bad item(s). ")}  
 
 x <- x[,items,drop=FALSE]
 y <- y[,items,drop=FALSE]
 
#these are the means of the unreversed items
if(NCOL(x) > 1) {mean.x <- colMeans(x,na.rm=TRUE)
mean.y <- colMeans(y,na.rm=TRUE)
}

 


     min.item <- min(x[items],na.rm=TRUE)
     max.item <- max(x[items],na.rm=TRUE)
     miny.item <- min(y[items],na.rm=TRUE)
     maxy.item <- max(y[items],na.rm=TRUE)
  if(any(temp < 0)) {
   #flip items that are negatively keyed
  	 x[items[temp <0]] <- max.item- x[items[temp < 0]] + min.item 
  	 y[items[temp <0]] <- maxy.item- y[items[temp < 0]] + miny.item 
    }
#x and y are now scored in the direction of the keys    
   select <- items
   
  if(any(colnames(x[select]) !=colnames(y[select]))) {stop("Variable names must match across tests")}
  
  
      p1 <- pca(x)
      p2 <- pca(y)
      
#Even though, if given keys, we have already flipped them, we check one more time
    #  if(is.null(keys) ) { keys <- rep(1,n.items) }  else {keys <- temp }
     keys <- rep(1,n.items)
      if((any(p1$loadings < 0)) | (any(p2$loadings < 0))) {if (check.keys) {if(warnings) message("Some items were negatively correlated with total scale and were automatically reversed.\n This is indicated by a negative sign for the variable name.") 
                    keys[p1$loadings < 0] <- -1 } else {
                       if(is.null(keys) && warnings ) {message("Some items were negatively correlated with the total scale and probably \nshould be reversed.  \nTo do this, run the function again with the 'check.keys=TRUE' option")
                       if(warnings) cat("Some items (",rownames(p1$loadings)[(p1$loadings < 0)],") were negatively correlated with the total scale and \nprobably should be reversed.  \nTo do this, run the function again with the 'check.keys=TRUE' option")
                        }} 
    }
 
if(any(keys < 0)) {   
#then find the x and y scores
newx <- t(t(x[select]) * keys + (keys < 0)*  (max.item + min.item) )  #these are now rescaled in the keyed direction  - but we have already done this if given a keys vector
newy <- t(t(y[select]) * keys + (keys < 0)* (maxy.item + miny.item))
} else {
newx <-  x[select]
newy <- y[select]
}

xscore <- rowMeans(newx,na.rm=TRUE)
yscore <- rowMeans(newy,na.rm=TRUE)
r12 <- cor(xscore,yscore,use="pairwise")
#then correlate them to get test retest r

#Now find the alpha for the x and y scales
x.alpha <- alpha.1(cov(newx,use="pairwise"))
y.alpha <- alpha.1(cov(newy,use="pairwise"))
xy.alpha <- rbind(unlist(x.alpha),unlist(y.alpha))
rownames(xy.alpha) <- c("x","y")
colnames(xy.alpha) <- c("raw G3","std G3","G6","av.r","S/N","se","lower","upper","var.r")
#then correlate each matched item

          
dxy <- dist(newx,newy) 
rqq <- dxy$rqq

#now find the item over subjects correlation
rii <- rep(NA,n.items)
for (j in (1:n.items)) {
 if(!(( is.na(sd(x[,items[j]],na.rm=TRUE))) | (is.na(sd(y[,items[j]],na.rm=TRUE)))))  {
  rii[j] <- cor(x[,items[j]],y[,items[j]],use="pairwise")}
 }  


 #ok, the data seem ok lets create a dummy variable and do the lmer on it
 n.obs <- min(NROW(newx),NROW(newy))
 xy.df <- data.frame(id = rep(1:n.obs,2), time=c(rep(1,n.obs), rep(2,n.obs)), rbind(newx,newy),row.names=1:(2*n.obs))   #this is getting it ready for mlr1 
 } else {#The case of  just two tests, no items 
 

   xy.df <- data.frame(id=rep(1:n.obs,2),time=c(time=c(rep(1,n.obs), rep(2,n.obs))),rbind(x,y)[,1],row.names=1:(2*n.obs))
  
  no.items <- TRUE
 } 
 
  
#Now, the data are ready for lmer  
#This is same as 
# ml <- mlr(xy.df,aov=FALSE,lmer=TRUE)   We now need to grab the best of multilevel reliabiity to do the next part



if(!just.test) {
  if(lmer) {ml <- mlr1(xy.df)} else {ml <- list(n.obs=n.obs,n.items=n.items)}
    if(is.null(keys.orig)) keys.orig <- rep(1,n.items)
    item.stats <- data.frame(rii=rii,p1=unclass(p1$loadings),p2=unclass(p2$loadings),mean1 = mean.x, mean2=mean.y, keys=keys,keys.orig=keys.orig)
    colnames(item.stats)[2:3] <- c("PC1", "PC2")
    key <- rownames(item.stats)
    key[item.stats$keys < 0] <- paste0("-", key[item.stats$keys < 0])
    scores <- data.frame(pca1 = p1$scores,pca2 = p2$scores,t1scores =xscore, t2scores = yscore,rqq=rqq,dxy=dxy$dxy,t1sd=dxy$sdx,t2sd=dxy$sdy)
     result <- list(r12=r12,alpha=xy.alpha,rqq=rqq,dxy=dxy,item.stats=item.stats, scores=scores,xy.df=xy.df,key=key,ml=ml,Call=cl) } else {
    if(just.test) {r12 = cor(x,y,use="pairwise")
             ml <- mlr2(xy.df)
                 result <- list(r12 =r12,ml=ml, Call=cl)} }
     class(result) <- c("psych", "testRetest")
     return(result)  
} 


########

 alpha.1 <- function(C,R=NULL) {
    n <- dim(C)[2]
    alpha.raw <- (1- tr(C)/sum(C))*(n/(n-1))
    if(is.null(R)) R <- cov2cor(C)
    sumR <- sum(R)
    alpha.std <-  (1- n/sumR)*(n/(n-1))  
    smc.R <- smc(R)
    G6 <- (1- (n-sum(smc.R))/sumR)
    av.r <- (sumR-n)/(n*(n-1))
    R.adj <- R
    diag(R.adj) <- NA
    var.r  <- var(as.vector(R.adj),na.rm=TRUE)
    mod1 <- matrix(av.r,n,n)
    Res1 <- R - mod1
    GF1 =  1- sum(Res1^2)/sum(R^2)
    Rd <- R - diag(R)
    diag(Res1) <- 0
    GF1.off <- 1 - sum(Res1^2)/sum(Rd^2)  
    sn <- n*av.r/(1-av.r)
   # Q = (2 * n^2/((n-1)^2*(sum(C)^3))) * (sum(C) * (tr(C^2) + (tr(C))^2) - 2*(tr(C) * sum(C^2))) #corrected 1/15/16 
    Q = (2 * n^2/((n - 1)^2 * (sum(C)^3))) * (sum(C) * (tr(C%*%C) +  (tr(C))^2) - 2 * (tr(C) * sum(C%*%C)))   #correction from Tamaki Hattori
    result <- list(raw=alpha.raw,std=alpha.std,G6=G6,av.r=av.r,sn=sn,Q=Q,GF1,GF1.off,var.r = var.r)
    return(result)
    }
    
    

    
print_psych.testRetest<- function(x,digits=2,short=FALSE,...) {
cat("\nTest Retest reliability ")
cat("\nCall: ")
	print(x$Call)
cat('\nNumber of subjects = ',x$ml$n.obs, " Number of items = ", x$ml$n.items)
if(x$ml$n.items  > 1)  { #The normal case
cat("\n Correlation of scale scores over time" , round(x$r12,digits))
cat("\n Alpha reliability statistics for time 1 and time 2 \n")
rownames(x$alpha) <- c("Time 1", "Time 2")
print( round(x$alpha,digits))

meanrii <- mean(x$item.stats$rii,na.rm=TRUE)
meanrqq <- mean(x$rqq,na.rm = TRUE)
sdrqq <- sd(x$rqq,na.rm = TRUE)
meandqq <- mean(x$dxy$dxy,na.rm=TRUE)

cat("\n Mean between person, across item reliability = ",round(meanrii,digits))

cat("\n Mean within person, across item reliability = ",round(meanrqq,digits))
cat(  "\nwith standard deviation of " ,round(sdrqq,digits) ,"\n")
cat("\n Mean within person, across item d2 = ",round(meandqq,digits))
temp <- x
x <- x$ml

if(!is.null(x$R1F)) cat("\nR1F  = ",round(x$R1F,digits) , "Reliability of average of all items for one  time (Random time effects)")

if(!is.null(x$RkF)) cat("\nRkF  = ",round(x$RkF,digits) , "Reliability of average of all items and both times (Fixed time effects)")
	
if(!is.null(x$R1R)) cat("\nR1R  = ",round(x$R1R,digits),"Generalizability of a single time point across all items (Random time effects)")
		
	
	if(!is.null(x$R2R)) cat("\nRkR  = ",round(x$RkR,digits),"Generalizability of average time points across all items (Fixed time effects)")
	
	if(!is.null(x$Rc))  cat("\nRc   = ",round(x$Rc,digits),"Generalizability of change (fixed time points, fixed items) ")
	  
	if(!is.null(x$RkRn) ) cat("\nRkRn = ",round(x$RkRn,digits),"Generalizability of between person differences averaged over time (time nested within people)")
	   
	if(!is.null(x$Rcn)) cat("\nRcn  = ",round(x$Rcn,digits),"Generalizability of within person variations averaged over items  (time nested within people)")
x <- temp	  
if(!is.null(x$ml$components)) {cat("\nMultilevel components of variance\n")
print(round(x$ml$components,digits))}

if(!short) {
cat("\n With Item statistics \n")
print(round(x$item.stats[-7],digits))
}  else {cat("\n To see the item.stats, print with short=FALSE. \nTo see the subject reliabilities and differences, examine the 'scores' object.") } 

} else { cat("\nTest Retest Reliability of two tests", print(round(x$r12,digits)))
  cat("\nMultilevel components of variance\n")
  print(round(x$ml$components,digits))
   }
   }
   
   
#######   
   #grab the best parts of multilevel reliability
   
mlr1 <- function(x,na.action= "na.omit" ) { 
   long <- NULL
   id <- "id"
   time <- "time"
   n.obs <- NROW(x)/2
   items <- colnames(x) [!colnames(x) %in% c("id","time")] 
   n.items <- length(items)
   n.time <- 2
 long <- data.frame(id = rep(1:n.obs,2), time=rep(1:2,each = n.obs),stack(x[items]))
 colnames(long)[4] <- "items"    #just to make it clearer
  mod.lmer <- lme4::lmer(values ~ 1 + (1 | id) + (1 | time) + (1 | items) + (1 | id:time)+ (1 | id:items)+ (1 | items :time),
  data=long,na.action=na.action)
   vc <- lme4::VarCorr(mod.lmer)
  MS_id <- vc$id[1,1]
  MS_time <- vc$time[1,1]
  MS_items <- vc$items[1,1]
 # MS_pxt <- vc[[1]][[1]]
 # MS_pxitem <- vc[[2]][[1]]
 # MS_txitem <- vc[[3]][[1]]              #changed to named locations (which were incorrect when doing by numbers)
  MS_pxt <- vc[["id:time"]][[1]]
  MS_pxitem <- vc[["id:items"]][[1]]
  MS_txitem <- vc[["items:time"]][[1]]

  error <- MS_resid <- (attributes(vc)$sc)^2
  s.lmer <- s.aov <- summary(mod.lmer)
 MS.df <- data.frame(variance= c(MS_id, MS_time ,MS_items, MS_pxt, MS_pxitem, MS_txitem, MS_resid,NA))
  rownames(MS.df) <- c("ID","Time","Items","ID x time", "ID x items", "time x items", "Residual","Total") 

 MS.df["Total",]  <- sum(MS.df[1:7,1],na.rm=TRUE)
MS.df["Percent"] <- MS.df/MS.df["Total",1]
lmer.MS <- MS.df  #save these
#fixed time, not random time,   
  R1f <- (MS_id + MS_pxitem/n.items)/((MS_id + MS_pxitem/n.items + error/( n.items)))
  #average of both times
  
  Rkf <- (MS_id + MS_pxitem/n.items)/((MS_id + MS_pxitem/n.items + error/(n.time * n.items)))
R1r <- (MS_id + MS_pxitem/n.items)/((MS_id + MS_pxitem/n.items + MS_time + MS_pxt + error/( n.items)))  #per Sean Lane
Rkr <- (MS_id + MS_pxitem/n.items)/((MS_id + MS_pxitem/n.items + MS_time/n.time + MS_pxt/n.time + error/( n.time * n.items)))
Rc <- (MS_pxt)/(MS_pxt + error/n.items)

result <- list(n.obs = n.obs, n.items=n.items, components = MS.df,R1F= R1f,RkF =Rkf,R1R = R1r,RkR = Rkr,Rc=Rc)
 return(result)
  }
  
  mlr2 <- function(x,na.action=na.omit) {
  
  #these treats the case of just two tests with no items, we want the variance components
  long <- x   #it is actually already in long format
  n.obs <- NROW(long) /2
  n.items <- 1
   mod.lmer <- lme4::lmer(values ~ 1 + (1 | id) + (1 | time) ,
  data=long,na.action=na.action)
    
    
 vc <- lme4::VarCorr(mod.lmer)
MS_id <- vc$id[1,1]


 error <- MS_resid <- (attributes(vc)$sc)^2
    MS.df <- data.frame(variance= c(MS_id, MS_resid,NA))
  rownames(MS.df) <- c("ID","Residual","Total") 

MS.df["Total",]  <- sum(MS.df[1:2,1],na.rm=TRUE)
 MS.df["Percent"] <- MS.df/MS.df["Total",1]   
 Rxx <- MS_id/MS.df["Total",1]
  result <- list(n.obs=n.obs,n.items=n.items,components = MS.df)    
  }

#find level, scatter, and pattern by subject  
dist <- function(x,y) {
x.level <- rowMeans(x,na.rm=TRUE)
y.level <- rowMeans(y,na.rm=TRUE)
n.obs <- NROW(x)
sdxi <- apply(x,1,function(xx) sd(xx,na.rm=TRUE))
sdyi <- apply(y,1,function(xx) sd(xx,na.rm=TRUE))
dxy <- rowMeans((x - y)^2,na.rm=TRUE)
rxy <- rep(NA,n.obs)
tx <- t(x)
ty <- t(y)
for(i in 1:n.obs) {
 if(!(    (is.na(sdxi[i])) | (sdxi[i]==0) | (is.na(sdyi[i]) | sdyi[i]==0) )     ) {
rxy[i]  <- cor(tx[,i],ty[,i],use="pairwise")}
}
dist.df <- data.frame(x.level=x.level,y.level=y.level,sdx=sdxi,sdy = sdyi,dxy=dxy,rqq=rxy)
return(dist.df)
}

Try the psych package in your browser

Any scripts or data that you put into this service are public.

psych documentation built on Sept. 26, 2023, 1:06 a.m.