tests/mstate2.R

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])

Try the survival package in your browser

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

survival documentation built on June 22, 2024, 10:49 a.m.