library(survival) aeq <- function(x, y, ...) all.equal(as.vector(x), as.vector(y), ...) # This is a test of the influence matrix for an Andersen-Gill fit, using the # formulas found in the methods document, and implemented in the survfitaj.c # code. As much as anything it was a help in debugging -- both the mathematics # and the program. # The test case below has tied events, tied event/censoring, entry in mutiple # states, staggered entry, repeated events for a subject, varying case weights # within a subject, ... on purpose tdata <- data.frame(id= c(1, 1, 1, 2, 2, 3, 4, 4, 4, 4, 5, 5, 6, 6), t1= c(0, 4, 9, 1, 5, 2, 0, 2, 5, 8, 1, 3, 3, 5), t2= c(4, 9, 10, 5, 7, 9, 2, 5, 8, 9, 3, 11, 5, 8), st= c(2, 3, 2, 3, 1, 2, 2, 4, 4, 1, 3, 1, 3, 2), i0= c(1, 2, 3, 2, 3, 1, 1, 2, 4, 4, 4, 3, 2, 3), wt= c(1:8, 8:3)) tdata$st <- factor(tdata$st, c(1:4), labels=c("censor", "a", "b", "c")) tdata$i0 <- factor(tdata$i0, 1:4, labels=c("entry","a", "b", "c")) check <- survcheck(Surv(t1, t2,st) ~1, tdata, id=id, istate=i0) if (FALSE) { #useful picture plot(c(0,11), c(1,6.5), type='n', xlab="Time", ylab= "Subject") with(tdata, segments(t1+.1, id, t2, id, col=as.numeric(check$istate))) with(subset(tdata, st!= "censor"), text(t2, id+.15, as.character(st))) with(tdata, text((t1+t2)/2, id+.25, wt)) with(subset(tdata, !duplicated(id)), text(t1, id+.15, as.character(i0))) #segments are colored by current state, case weight in center, events at ends abline(v=c(2:5, 8:11), lty=3, col='gray') } # Compute the unweighted per observation leverages, using the approach in # the methods document, as a check of both it and the C code. # These IJ residuals can be directly verified using emprical derivatives, # and collapsed to test the weighted+collapsed results from survfitAJ. # survfitaj <- function(t1, t2, state, istate=NULL, wt, id, p0, start.time=NULL, debug = FALSE) { check <- survcheck(Surv(t1, t2, state) ~ 1, id=id, istate=istate) if (any(check$flag >0)) stop("failed survcheck") states <- check$states nstate <- length(states) istate <- check$istate # will have the correct levels isn <- as.numeric(istate) n <- length(t1) if (length(t2) !=n || length(state) !=n || length(istate) !=n || length(wt) !=n || length(id) !=n) stop("input error") newstate <- factor(state, unique(c(levels(state)[1], states))) Y <- Surv(t1, t2, newstate) # makes the levels match up position <- survival:::survflag(Y, id) uid <- unique(id) nid <- length(uid) id <- match(id, uid) # turn it into 1,2,... event <- (Y[,3] >0) U <- A <- matrix(0, n, nstate) # per observation influence, unweighted if (missing(p0)) { if (!missing(start.time)) t0 <- start.time else { if (all(Y[, 3] ==0)) t0 <- min(Y[, 2]) # no events! else t0 <- min(Y[event, 2]) } atrisk <- (Y[,1] < t0 & Y[,2] >= t0) wtsum <- sum(wt[atrisk]) # weights at that time p0 <- tapply(wt[atrisk], istate[atrisk], sum) / wtsum p0 <- ifelse(is.na(p0), 0, p0) #if a state has no one, tapply =NA if (all(p0 <1)) { # compute intitial leverage for (j in 1:nstate) { U[atrisk,j] <- (ifelse(istate[atrisk]==states[j], 1, 0) - p0[j])/wtsum } } } else { if (missing(start.time)) t0 <- 0 else t0 <- start.time } utime <- sort(unique(c(0, Y[event | position>1, 2]))) ntime <- length(utime) phat <- matrix(0, ntime, nstate) phat[1,] <- p0 n.risk <- matrix(0, ntime, nstate) n.risk[1,] <- table(istate[Y[,1]< start.time & Y[,2] > start.time]) # count the number of transitions, and make an index to them temp <- table(istate[event], factor(Y[event,3], 1:nstate, states)) trmat <- cbind(from= row(temp)[temp>0], to= col(temp)[temp>0]) nhaz <- nrow(trmat) n.event <- matrix(0, ntime, nhaz) C <- matrix(0, n, nhaz) chaz <- matrix(0, ntime, nhaz) hash <- trmat %*% c(1,10) tindx <- match(isn + 10*Y[,3], hash, nomatch=0) #index to transitions # at this point I have the initial inflence matrices (U= pstate, # C= cumhaz, A= auc). The auc and cumhaz are 0 at the starting point # so their influence is 0. Usave <- array(0, dim=c(dim(U), ntime)) Usave[,,1] <- U Csave <- array(0, dim= c(dim(C), ntime)) #chaz and AUC are 0 at start.time Asave <- array(0, dim= c(dim(A), ntime)) for (it in 2:ntime) { # AUC if (it==2) delta <- utime[it]- t0 else delta <- utime[it] - utime[it-1] A <- A + delta* U # count noses atrisk <- (t1 < utime[it] & t2 >= utime[it]) temp <- tapply(wt[atrisk], istate[atrisk], sum) n.risk[it,] <- ifelse(is.na(temp), 0, temp) event <- (Y[,2]== utime[it] & Y[,3]>0) temp <- tapply(wt[event], factor(tindx[event], 1:nhaz), sum) n.event[it,] <- ifelse(is.na(temp), 0, temp) # Add events to C and create the H matrix H <- diag(nstate) for (i in which(event)) { j <- isn[i] # from, to, and transition indices k <- Y[i,3] jk <- match(j+10*k, hash) C[i, jk] <- C[i, jk] + 1/n.risk[it,j] if (j!=k) { H[j,j] <- H[j,j] - wt[i]/n.risk[it,j] H[j,k] <- H[j,k] + wt[i]/n.risk[it,j] } } U <- U %*% H phat[it,] <- phat[it-1,] %*% H if (debug) browser() # Add events to U for (i in which(event)) { j <- isn[i] # from, to, and transition indices k <- Y[i,3] if (j != k) { U[i,j] <- U[i,j] - phat[it-1,j]/n.risk[it,j] U[i,k] <- U[i,k] + phat[it-1,j]/n.risk[it,j] } } if (debug) browser() # now the hazard part for (h in which(n.event[it,] >0)) { j <- trmat[h,1] k <- trmat[h,2] haz <- n.event[it,h]/n.risk[it, j] h2 <- haz/n.risk[it,j] who <- (atrisk & isn ==j) # at risk, currently in state j C[who,h] <- C[who,h] - h2 if (j != k) { U[who,j] <- U[who,j] + h2 * phat[it-1,j] U[who,k] <- U[who,k] - h2 * phat[it-1,j] } } if (debug) browser() Usave[,,it] <- U Csave[,,it] <- C Asave[,,it] <- A } colnames(n.event) <- paste(trmat[,1], trmat[,2], sep=':') colnames(n.risk) <- check$states colnames(phat) <- check$states list(time = utime, n.risk= n.risk, n.event=n.event, pstate= phat, C=Csave, U=Usave, A=Asave) } mfit <- survfit(Surv(t1, t2, st) ~ 1, tdata, id=id, istate=i0, weights=wt, influence=TRUE) mtest <- with(tdata, survfitaj(t1, t2, st, i0, wt, id)) # mtest <- with(tdata, survfitaj(t1, t2, st, i0, wt, id, debug=TRUE)) # p0 and U0 from the methods document p0 <- c(8, 4,0,6)/ 18 U0 <- rbind(c(1,0,0,0) - p0, 0, 0, c(0,1,0,0) - p0, 0, 0, c(1,0,0,0) - p0, 0, 0, 0, c(0,0,0,1) - p0, 0, 0, 0) /18 aeq(mtest$pstate[1,], p0) aeq(mtest$U[,,1], U0) aeq(mtest$time[-1], mfit$time) # mtest includes U(2-eps) as 'time 0' aeq(mtest$pstate[-1,], mfit$pstate) aeq(mfit$p0, p0) aeq(mfit$i0, rowsum(U0*tdata$wt, tdata$id)) # direct check that mtest has the correct answer eps <- 1e-6 delta <- array(0, dim= c(nrow(tdata), dim(mfit$pstate))) deltaC<- array(0, dim= c(nrow(tdata), dim(mfit$cumhaz))) for (i in 1:nrow(tdata)) { twt <- tdata$wt twt[i] <- twt[i] + eps tfit <- survfit(Surv(t1, t2, st) ~1, tdata, id=id, istate=i0, weights= twt) delta[i,,] <- (tfit$pstate - mfit$pstate)/eps deltaC[i,,] <-(tfit$cumhaz - mfit$cumhaz)/eps } temp <- aperm(mtest$U, c(1,3,2)) # drop time 0, put state last all.equal(temp[,-1,], delta, tol=eps/2) tempC <-aperm(mtest$C, c(1,3,2)) all.equal(tempC[,-1,], deltaC, tol= eps/2) # Now check mfit, which returns the weighted collapsed values BD <- t(model.matrix(~ factor(id) -1, tdata)) %*% diag(tdata$wt) rownames(BD) <- 1:6 collapse <- function(U, cmat=BD) { # for each time point, replace the inflence matrix U with BDU if (is.matrix(U)) BD %*% U else { dd <- dim(U) temp <- cmat %*% matrix(U, nrow = dd[1]) #fake out matrix multiply array(temp, dim= c(nrow(temp), dd[2:3])) } } sqsum <- function(x) sqrt(sum(x^2)) temp <- collapse(mtest$U[,,-1]) # mtest has time 0, mfit does not # mfit$influence is in id, time, state order aeq(aperm(temp, c(1,3,2)), mfit$influence) # mtest has time 0, mfit does not setemp <- apply(collapse(mtest$U[,,-1]), 2:3, sqsum) aeq(t(setemp), mfit$std.err) ctemp <- apply(collapse(mtest$C[,,-1]), 2:3, sqsum) aeq(t(ctemp), mfit$std.chaz) atemp <- apply(collapse(mtest$A[,,-1]), 2:3, sqsum) aeq(t(atemp), mfit$std.auc) # check residuals rr1 <- resid(mfit, times=mfit$time, type='pstate') aeq(rr1, mtest$U[,,-1]) rr2 <- resid(mfit, times=mfit$time, type='auc') aeq(rr2, mtest$A[,,-1])