R/ICP-plausible_predictor_test.R

Defines functions plausible_predictor_test plausible_predictor_test.default plausible_predictor_test.TimeVar plausible_predictor_test.EnvirRel check_for_degeneracy check_for_degeneracy.default check_for_degeneracy.numeric check_for_degeneracy.Surv plausible_predictor_test.CR intersect_CR intersect_CR.marginal intersect_CR.pairwise intersect_CR.QC intersect_marginal_rectangle intersect_ellipsoid_pair intersect_ellipsoid

Documented in plausible_predictor_test plausible_predictor_test.CR plausible_predictor_test.default plausible_predictor_test.EnvirRel plausible_predictor_test.TimeVar

#' Internal Test for plausible predictors
#'
#' \code{plausible_predictor_test} is a generic function used to test whether a
#' set of predictors might be plausible causal predictors.
#'
#' The \code{plausible_predictor_test} is meant to be used in the wrapper
#' function \code{\link{ICP}}, but can also be used on its own as a simple
#' hypothesis testing function. The \strong{method object} \code{method}, which
#' is created by the \code{\link{method_obj}} function dictates the
#' \code{plausible_predictor_test} method. As this function is generic which
#' means that new testing methods can easily be added for new classes.
#'
#' For the \code{plausible_predictor_test} to work correctly in the
#' \code{\link{ICP}} wrapper it must be a test of the null hypothesis
#' \deqn{H_{0,S}: S is an invariant set w.r.t. (X,Y)}
#' where an invariant set is defined as a set of indicies \eqn{S} such that
#' \deqn{(Y^e | X^e_S) = (Y^f | X^f_S)}
#' in distribution for all environments \eqn{e,f}. If the data is time dependent
#' we formulate an analog invariance statement for all time points \eqn{t} and
#' \eqn{s}. For more detailes and discussion of this hypothesis and invariance
#' see the references.
#'
#' As the \code{plausible_predictor_test} is a hypothesis test it must return a
#' p value, that is a number between 0 and 1.
#'
#'
#' @param method a method object describing the model class to use in the
#'   fitting procedure and the testing method. When used in the
#'   \code{\link{ICP}} function the method object is inherited from the
#'   \code{\link{ICP}} function.
#' @param Y a vector or \code{\link[survival]{Surv}} object describing the
#'   target variable. The \code{Y} will be passed to the \code{\link{fit_model}}
#'   function for fitting, and it is therefor important that the class of
#'   \code{Y} is understood by the regression method used in
#'   \code{\link{fit_model}}. So if \code{method} specifies that a cox
#'   regression is to be used, then \code{Y} must be a
#'   \code{\link[survival]{Surv}} object.
#' @param X a matrix, vector or data frame describing the covariates.
#' @param E a vector describing the environmants
#' @param level the alpha level of testing, but it is only relevant if the
#'   method is iterative \emph{and} \code{fullAnalysis} is \code{FALSE}.
#' @param fullAnalysis if \code{TRUE} a p-value must be retured. If \code{FALSE}
#'   the method is 'allowed' to only return 0/1 for hypothesis rejected/accepted.
#' @param Bonferroni if \code{TRUE} Bonferroni correcting will be used if relevant.
#' @param ... additional arguments to be passed to lower level functions.
#'
#'
#' @return Returns a p-value, i.e. a number between 0 and 1, to be used in the
#'   \code{\link{ICP}} function.
#'
#' @seealso \code{\link{ICP}} for the full wrapper function.
#'
#' @keywords internal
#' @export

plausible_predictor_test <- function(method, Y, X, ...) {
  UseMethod("plausible_predictor_test", method)
}

#' @rdname plausible_predictor_test
#' @export
plausible_predictor_test.default <- function(method, Y, X, ...) {
  stop(method$method,
          "is not recocnised as a testing method by the",
          "'plausible_predictor_test'")
  return(0)
}


#' @rdname plausible_predictor_test
#' @export
plausible_predictor_test.TimeVar <- function(method, Y, X,
                                              level, Bonferroni = TRUE, ...) {
  fit <- fit_nonparam_model(method, Y, X, ...)
  if (method$n.sim != 0) {
    assign("smallest_possible_pvalue", 1 / method$n.sim, envir = parent.frame())
    if(method$nonparamtest == "sup") {
      pvals <- fit$sup
    } else if (method$nonparamtest == "int") {
      pvals <- fit$int
    } else {
      pvals <- c(fit$sup, fit$int)
    }
  } else {
    # TODO remember to use bonferroni correction
    stop("Khmaladze transformation not yet implemented")
  }
  if (sum(is.na(pvals)) > 0) {
    stop("Faild to compute the necessary p-vlaues:\n",
         "The error is in the 'timereg' package and has been reported.")
  }
  BonCorr <- ifelse(Bonferroni, length(pvals[!is.na(pvals)]), 1)
  res <- min(1, min(pvals) * BonCorr, na.rm = TRUE) # min(pvals) or max(pvals)
  return(res)
}



#' @rdname plausible_predictor_test
#' @export
plausible_predictor_test.EnvirRel <- function(method, Y, X, E, ...) {
  if (is.null(E)) {
    stop("The 'EnvirRel' method needs environments")
  }
  fitE <- fit_model(method, Y, cbind(X,E))
  fit0 <- fit_model(method, Y, X)
  pval <- stats::pchisq((fit0$deviance - fitE$deviance)/fitE$scale,
                        df = fit0$df - fitE$df,
                        lower.tail = FALSE)
  return(pval)
}






check_for_degeneracy <- function(Y, X, E, ...) {
  UseMethod("check_for_degeneracy", Y)
}
check_for_degeneracy.default <- function(Y, X, E, ...) {
  text <- paste0("Target variable of class ", class(Y), " is not recognised by ",
                 "the 'check_for_degeneracy' test for the Intersecting ",
                 "Confidence Regions test (CR).")
  stop(text)
}
check_for_degeneracy.numeric <- function(Y, X, E, ...) {
  index <- lapply(unique(E), function(e) {
    which(E == e)
  })
  Y_check <- X_check <- TRUE
  for (i in seq_along(index)) {
    Y_check <- ifelse(length(unique(Y[index[[i]]])) > 1, TRUE, FALSE)
    if (!Y_check) {break}
    for (j in seq_len(ncol(X))) {
      X_check <- ifelse(length(unique(X[index[[i]],j])) > 1, TRUE, FALSE)
      if (!X_check) {break}
    }
  }
  if (Y_check & X_check) {
    return(TRUE)
  } else {
    return(FALSE)
  }
}
check_for_degeneracy.Surv <- function(Y, X, E, ...) {
  index <- lapply(unique(E), function(e) {
    which(E == e)
  })
  X_check <- TRUE
  for (i in seq_along(index)) {
    for (j in seq_len(ncol(X))) {
      X_check <- ifelse(length(unique(X[index[[i]],j])) > 1, TRUE, FALSE)
      if (!X_check) {break}
    }
  }
  if (X_check) {
    return(TRUE)
  } else {
    return(FALSE)
  }
}




#' @rdname  plausible_predictor_test
#' @export
plausible_predictor_test.CR <- function(method, Y, X, E,
                                        level, fullAnalysis,
                                        Bonferroni = TRUE, ...) {
  if (!check_for_degeneracy(Y, X, E)) {
    stop("One or more variables become degenerated when subsetting data by ",
         "environment 'E'")
  }
  if (is.null(method$splits)) {
    method$splits <- "all"
  }
  if (method$splits == "LOO") {
    models <- lapply(unique(E), function(e) {
      list(fit_model(method, Y, X, subset = (E == e)),
           fit_model(method, Y, X, subset = (E != e)))
    })
    BonCorr <- ifelse(Bonferroni, length(models) * 2, 1)
    alpha <- ifelse(is.null(level), NULL, level / BonCorr)
    res <- sapply(models, function(m) {
      intersect_CR(method, m, alpha, fullAnalysis, ...)
    })
    res <- ifelse(is.logical(res),
                  all(res),
                  min(1, min(res) * BonCorr))
  } else {
    models <- lapply(unique(E), function(e) {
      fit_model(method, Y, X, subset = (E == e))
    })
    BonCorr <- ifelse(Bonferroni, length(models), 1)
    alpha <- ifelse(is.null(level), NULL, level / BonCorr)
    res <- intersect_CR(method, models, alpha, fullAnalysis, ...)
    res <- ifelse(is.logical(res),
                  res,
                  min(1, res * BonCorr))
  }
  return(res)
}

intersect_CR <- function(method, models, alpha, ...) { # TODO
  UseMethod("intersect_CR", method)
}

intersect_CR.marginal <- function(method, models, alpha,
                                  fullAnalysis = FALSE, tol = 2e-10, ...) {
  assign("smallest_possible_pvalue", tol, envir = parent.frame(n = 2))
  if (is.null(alpha) | fullAnalysis) {
    max_true <- 0
    min_false <- 1
    while ((min_false - max_true) > tol) {
      alpha <- (min_false + max_true) / 2
      test <- intersect_marginal_rectangle(models, alpha)
      if (test) {max_true <- alpha} else {min_false <- alpha}
    }
    return(max_true) # alpha value
  } else {
    return(intersect_marginal_rectangle(models, alpha)) # boolean
  }
}

intersect_CR.pairwise <- function(method, models, alpha,
                                  fullAnalysis = FALSE, tol = 2e-10, ...) {
  assign("smallest_possible_pvalue", tol, envir = parent.frame(n = 2))
  pairs <- utils::combn(seq_along(models), 2)
  if (is.null(alpha) | fullAnalysis) {
    max_true <- 0
    min_false <- 1
    while ((min_false - max_true) > tol) {
      alpha <- (min_false + max_true) / 2
      test <- apply(pairs, 2, function(c) {
        intersect_ellipsoid_pair(models[[c[1]]], models[[c[2]]], alpha)
      })
      if (all(test)) {max_true <- alpha} else {min_false <- alpha}
    }
    return(max_true) # alpha value
  } else {
    res <- apply(pairs, 2, function(c) {
      intersect_ellipsoid_pair(models[[c[1]]], models[[c[2]]], alpha)
    })
    return(all(res)) # boolean
  }
}

intersect_CR.QC <- function(method, models, alpha, fullAnalysis = TRUE, ...) {
  m <- length(models[[1]]$coefficients)
  if (fullAnalysis) {
    res <- intersect_ellipsoid(models)
    return(stats::pchisq(res[1], df = m, lower.tail = FALSE)) # alpha value
  } else {
    test <- intersect_marginal_rectangle(models, alpha)
    if (test) {
      res <- intersect_ellipsoid(models)
      return(stats::pchisq(res[1], df = m, lower.tail = FALSE)) # alpha value
    } else {
      return(FALSE)
    }
  }
}


intersect_marginal_rectangle <- function(models, alpha) {
  all_names <- Reduce(union,
                      lapply(models, function(m) {names(m$coefficients)}))
  q <- stats::qchisq(1 - alpha, df = length(all_names))
  endpoints <- sapply(models, function(m) {
    L <- m$coefficients - sqrt(q) * sqrt(diag(m$covariance))
    U <- m$coefficients + sqrt(q) * sqrt(diag(m$covariance))
    #if (! identical(names(m$coefficients), all_names)) {
    #  mis <- which(! all_names %in% names(m$coefficients))
    #  for (i in seq_along(mis)) {
    #    L <- append(L, -Inf, after = mis[i] - 1)
    #    U <- append(U, Inf, after = mis[i] - 1)
    #  }
    #}
    return(rbind(L,U))
  })
  endpoint_test <- sapply(seq_len(nrow(endpoints) / 2), function(u) {
    max(endpoints[u * 2 - 1, ], na.rm = TRUE) <= min(endpoints[u * 2, ], na.rm = TRUE)
  })
  return(all(endpoint_test))
}


intersect_ellipsoid_pair <- function(M1, M2, alpha){

  c1 <- M1$coefficients
  C1 <- M1$covariance
  c2 <- M2$coefficients
  C2 <- M2$covariance

  if (any(!is.finite(C1))) {
    ind <- which(!is.finite(C1))
    C1[ind] <- max(C1[-ind], 1) * 100000
  }
  if (any(!is.finite(C2))) {
    ind <- which(!is.finite(C2))
    C2[ind] <- max(C2[-ind], 1) * 100000
  }

  m <- length(c1)
  q <- sqrt(stats::qchisq(1 - alpha, df = m))

  if (m == 1) {
    return(abs(c1 - c2) < (sqrt(C1) + sqrt(C2)) * q)
  } else {

    e1 <- eigen(C1)
    d1 <- e1$values
    d1 <- make_positive(d1)
    d1 <- sqrt(d1)
    U1 <- e1$vectors

    c <- (1 / q) * diag(1 / d1) %*% t(U1) %*% (c2 - c1)
    C <- diag(1 / d1) %*% t(U1) %*% C2 %*% U1 %*% diag(1 / d1)

    e <- eigen(C)
    U <- e$vectors
    d <- e$values
    d <- make_positive(d, silent = TRUE)
    d <- sqrt(d)

    y <- -t(U) %*% c
    y <- abs(y) # 0 expressed in coordinate system of l and rotated to the first quadrant

    if(sum((y / d) ^ 2) <= 1){ # y inside the ellipse
      return(TRUE)
    } else { # Newton-Rhapson iterations

      # f goes monotone, quadratically to -1, so sure and fast convergence
      f <- function(t) sum(( d * y / (t + d^2))^2) - 1
      df <- function(t) - 2 * sum((y * d)^2 / (t + d^2)^3)

      t0 <- 0
      ft0 <- f(t0)

      while(ft0 > 1e-4){
        t0 <- t0 - f(t0) / df(t0)
        ft0 <- f(t0)
      }

      x0 <- y * d^2 / (d^2 + t0) # projection of y onto (c,C)
      dist <- sqrt(sum((y - x0)^2))
      return(dist < 1)
    }
  }
}

# (very) adhoc way of dealing with negative eigenvalues
make_positive <- function (v, silent = TRUE){
  w <- which(v < 10^(-14))
  if (length(w) > 0 & !silent)
    warning("Some eigenvalues are below 10^(-14) and will therefore be regularized")
  for (ww in w) {
    if (v[ww] < 0) {
      v[ww] <- v[ww] - 2 * v[ww] + 10^(-14)
    }
    else {
      v[ww] <- v[ww] + 10^(-14)
    }
  }
  return(v)
}



intersect_ellipsoid <- function(models) { # should return number
  # -- tuning parameters -------------------------------------------------------
  barrier_iter <- 5 # find better stopping criteria ?
  tol_newton <- 0.1
  small <- 1
  mu <- 20
  linesearch <- TRUE # page 464 Boyd & Vandenbergh
  alpha <- 0.3 # must be between 0 and 0.5
  beta <- 0.8 # must be between 0 and 1

  # -- Find relevant sizes -----------------------------------------------------
  ellipsoids <- lapply(models, function(m) {
    var <- m$covariance
    if (any(!is.finite(var))) {
      ind <- which(!is.finite(var))
      var[ind] <- max(var[-ind], 1) * 1000
    }
    rr <- rcond(var)
    if (rr < 2e-15) {
      add <- diag(diag(var) * 0.01, ncol(var))
      P <- solve(var + add)
    } else {
      P <- solve(var)
    }
    b <- matrix(m$coefficients, ncol = 1)
    q <- - 2 * (P %*% b)
    return(list(P = P, b = b, q = q))
  })

  # -- create relevant functions -----------------------------------------------
  p <- length(ellipsoids)
  f <- function(x) {
    sapply(ellipsoids, function(e) {
      diff <- x - e$b
      t(diff) %*% e$P %*% diff
    })
  }
  gf <- function(x) {
    lapply(ellipsoids, function(e) {
      2 * e$P %*% x + e$q
    })
  }
  hf <- function(x) {
    lapply(ellipsoids, function(e) {
      2 * e$P
    })
  }
  g <- function(s, tune, f) {tune * s - sum(log(s - f)) - log(s)}
  linesearch_test <- function(s, x, f, decrement, tune, delta, step) {
    s_delta <- s + step * delta[1, ]
    if (s_delta <= 0) {return(TRUE)}
    f_delta <- f(x + step * delta[-1, ])
    if (max(f_delta) >= s_delta){return(TRUE)}
    if(g(s_delta, tune, f_delta) >
       g(s, tune, f) + alpha * step * decrement) {
      return(TRUE)
    }
    return(FALSE)
  }
  gradient<- function(s, tune, f, gf) {
    top <- tune - sum(1 / (s - f))
    v <- lapply(1:p, function(i) {
      (1 / (s - f[[i]])) * gf[[i]]
    })
    bottom <- Reduce("+", v)
    return(matrix(c(top, bottom), ncol = 1))
  }
  hessian <- function(s, f, gf, hf) {
    dsds <- sum(1 / (s - f) ^ 2)
    v <- lapply(1:p, function(i) {
      (1 / (s - f[[i]]) ^ 2) * gf[[i]]
    })
    dsdx <- - Reduce("+", v)
    m <- lapply(1:p, function(i) {
      (1 / (s - f[[i]]) ^ 2) * gf[[i]] %*% t(gf[[i]]) +
        (1 / (s - f[[i]])) * hf[[i]]
    })
    dxdx <- Reduce("+", m)
    return(cbind(rbind(dsds, dsdx), rbind(t(dsdx), dxdx)))
  }


  # -- logarithmic barrier method ----------------------------------------------
  #x <- matrix(rowMeans(sapply(ellipsoids, function(e){e$b})), ncol = 1)
  x <- ellipsoids[[1]]$b
  s <- max(f(x)) + small
  tune <- (sum(1 / (s - f(x))) * s + 1) / s

  for (i in seq_len(barrier_iter)) {

    # -- centering :  Newtons method
    lambda <- tol_newton * 5
    while (lambda / 2 > tol_newton) {
      # .. calculate direction and decrement
      f_val <- f(x)
      gf_val <- gf(x)
      hf_val <- hf(x)
      gx <- gradient(s, tune, f_val, gf_val)
      #invhx <- solve(hessian(s, f_val, gf_val, hf_val))
      hg <- hessian(s, f_val, gf_val, hf_val)
      rr <- rcond(hg)
      if (rr < 1e-10) {
        if (rr < 1e-16) {
          break
        } else{
          add <- diag(diag(hg) * 0.01, ncol(hg))
          invhx <- solve(hg + add)
        }
      } else {
        invhx <- solve(hg)
      }
      delta <- - invhx %*% gx
      lambda <- - t(gx) %*% delta

      # .. linesearch
      step <- 1

      while (linesearch_test(s, x, f_val, lambda, tune, delta, step)) {
        step <- beta * step
      }

      # .. update (s,x)
      s <- s + step * delta[1, ]
      x <- x + step * delta[-1, ]
    }
    tune <- tune * mu
  }
  return(c(s,x))
}
Laksafoss/ICPSurv documentation built on Feb. 26, 2020, 11:32 a.m.