R/roc.R

Defines functions rocSmoothCalc roc.bermantest roc.cdftest roc.spatialCDFframe roc.function roc.rhohat makeRocDesc makeRocTag makeRocFlabel rocDummy plot.roc as.roc.data.frame rocEngine resolveNullModel rocModel rocData rocIm roc.im roc.ppp roc

Documented in as.roc.data.frame makeRocDesc makeRocFlabel makeRocTag plot.roc resolveNullModel roc roc.bermantest roc.cdftest rocData rocDummy rocEngine roc.function roc.im rocIm rocModel roc.ppp roc.rhohat rocSmoothCalc roc.spatialCDFframe

##
##
##  roc.R
##
##  Calculate ROC curve
##       (in spatstat.explore)
##
## Copyright (c) 2017-2025 Adrian Baddeley/Ege Rubak/Rolf Turner/Suman Rakshit
##
##

roc <- function(X, ...) { UseMethod("roc") }

  roc.ppp <-
  function(X, covariate, 
                               ...,
                               baseline=NULL,
                               high = TRUE,
                               weights = NULL,
                               observations = c("exact", "presence"),
                               method = "raw",
                               CI = "none", alpha=0.05,
                               subset=NULL) {
  callframe <- parent.frame()
  stopifnot(is.ppp(X))
  observations <- match.arg(observations)
  ## estimation method
  method <- match.arg(method, c("raw", "monotonic", "smooth", "rhohat", "all"),
                      several.ok = TRUE)
  method <- sub("rhohat", "smooth", method)
  if("all" %in% method)
    method <- c("raw", "monotonic", "smooth", "all")
  ## Resolve null model / baseline
  baseline.is.pattern <- is.ppp(baseline) || is.lpp(baseline)
  if(!baseline.is.pattern) {
    ## baseline is NULL, or a function/image/...  that determines null model
    nullmodel <- resolveNullModel(baseline, X, observations, ...)
  } else {
    ## baseline is a set of dummy points
    if(observations == "presence") {
      ## discretise both patterns
      X <- do.call.matched(discretise, 
                           list(X=quote(X), ..., move.points=TRUE),
                           envir=callframe)
      baseline <-
        do.call.matched(discretise,
                        list(X=quote(baseline), ..., move.points=TRUE),
                        envir=callframe)
    }
  }
  ## confidence intervals using which estimate?
  CI <- match.arg(CI, c("none", "raw", "monotonic", "smooth", "rhohat"))
  CI <- sub("rhohat", "smooth", CI)
  if(CI != "none") {
    method <- union(method, CI)
    if(baseline.is.pattern && CI != "smooth")
      warning(paste("Confidence interval for", sQuote(CI), "method",
                    "treats the baseline ('control') point pattern as fixed"),
              call.=FALSE)
  }
  #' Get covariates
  covariate <- digestCovariates(covariate, W = Window(X))
  #' ensure 'high' is a vector
  ncov <- length(covariate)
  if(length(high) == 1)
    high <- rep(high, ncov)
  stopifnot(length(high) == ncov)
  #' process
  result <- vector(mode="list", length=ncov)
  for(i in seq_len(ncov)) {
    if(baseline.is.pattern){
      result[[i]] <- rocDummy(X, baseline, covariate[[i]],
                              method=method,
                              high = high[i],
                              weights=weights,
                              subset=subset,
                              CI=CI, alpha=alpha,
                              ...)
    } else{
      result[[i]] <- rocEngine(covariate[[i]],
                               nullmodel,
                               method = method,
                               high=high[i],
                               weights=weights,
                               subset=subset,
                               CI=CI, alpha=alpha,
                               ...)
      }
  }
  names(result) <- names(covariate)
  result <- as.anylist(result)
  if(ncov == 1)
    result <- result[[1]]
  return(result)
}

roc.im <- function(X, covariate, ..., high=TRUE) {
  rocIm(X, covariate, ..., high=high)
}

rocIm <- function(X, covariate, ..., high=TRUE,
                  p=seq(0,1,length=plength),
                  plength=1024) {
  ignored <- c("method", "CI")
  if(any(found <- !is.na(match(ignored, names(list(...)))))) 
    warning(paste(ngettext(sum(found), "Argument", "Arguments"),
                  commasep(sQuote(ignored[found])),
                  ngettext(sum(found), "was", "were"),
                  "ignored by roc.im"),
            call.=FALSE)
  Z <- covariate
  FZ <- spatialcdf(Z, ..., normalise=TRUE)
  if(!missing(p)) {
    check.nvector(p)
    stopifnot(min(p) >= 0)
    stopifnot(max(p) <= 1)
    if(prod(range(diff(p))) < 0) stop("p should be a monotone sequence")
    plength <- length(p)
  }
  if(high) p <- 1-p  # reverse again at the end
  thresh <- quantile(FZ, p)
  ## make unique threshold breaks for later use
  if(anydup <- anyDuplicated(thresh)) {
    good <- !duplicated(thresh)
    uthresh <- thresh[good]
  } else {
    uthresh <- thresh
  }
  #' ensure Z is now an image
  if(!is.im(Z)) {
    W <- Window(X)
    Z <- digestCovariates(Z, W=W)[[1L]]
    Z <- as.im(Z, W=W)
  }
  #' extract pixel values on common raster
  dat <- pairs.im(X, Z, plot=FALSE)
  xval <- dat[,"X"]
  zval <- dat[,"Z"]
  #' accumulate using 'cut' with unique breaks
  zgrp <- cut(zval,
              uthresh,
              labels=1:(length(uthresh)-1),
              include.lowest=TRUE)
  xmass <- as.numeric(tapplysum(xval, list(zgrp)))
  xcum <- if(high) c(revcumsum(xmass), 0) else c(0, cumsum(xmass))
  xcum <- xcum/max(xcum)
  if(anydup) {
    uxcum <- xcum
    xcum <- numeric(plength)
    xcum[good] <- uxcum
    if(high) {
      # ensure nonincreasing
      xcum[-plength] <- pmax(xcum[-1], xcum[-plength])
    } else {
      # ensure nondecreasing
      xcum[-1] <- pmax(xcum[-1], xcum[-plength])
    }
  }
  df <- data.frame(p = p, thresh=thresh, null=p, raw=xcum)
  if(high) df <- df[plength:1, ]
  as.roc.data.frame(df)
}

#' Temporary code provides soon-to-be-deprecated functions rocData, rocModel

rocData <- function(covariate, nullmodel, ..., high=TRUE,
                    p=seq(0, 1, length=1024)) {
  rocEngine(covariate, nullmodel, high=high, p=p, covtype="covariate")
}

rocModel <- function(lambda, nullmodel, ..., high=TRUE,
                     p=seq(0, 1, length=1024),
                     lambdatype=c("intensity", "probabilities")) {
  lambdatype <- match.arg(lambdatype)
  covtype <- if(lambdatype == "probabilities") "probability" else "intensity"
  if(!high)
    warning("Argument 'high' is ignored for point process models",
            call.=FALSE)
  rocEngine(lambda, nullmodel, high=TRUE, p=p, covtype=covtype)
}



resolveNullModel <- function(baseline, X,
                             observations=c("exact", "presence"), ...) {

  observations <- match.arg(observations) # used only when X is a point pattern
  
  if(is.ppp(baseline) || is.lpp(baseline))
    return(NULL)
  if(inherits(baseline,
              c("ppm", "kppm", "slrm", "dppm", "lppm",
                "exactppm", "exactlppm")))
    return(baseline)

  Xispattern <- is.ppp(X) || is.lpp(X)
  Xismodel <- inherits(X, c("ppm", "kppm", "slrm", "dppm", "lppm"))
  if(!(Xispattern || Xismodel))
    stop("Internal error: format of 'X' is not understood")

  if(is.null(baseline)) {
    ## baseline was not given; defaults to CSR
    if(Xispattern) 
      switch(observations,
             exact = {
               if(is.ppp(X)) return(exactppm(X))
               if(is.lpp(X)) {
                 needpackage("spatstat.linnet")
                 return(spatstat.linnet::exactlppm(X))
               }
             },
             presence = {
               if(is.ppp(X)) {
                 needpackage("spatstat.model")
                 return(spatstat.model::slrm(X ~ 1, ...))
               }
               if(is.lpp(X))
                 stop("Presence/absence analysis is not yet supported on networks")
             })
    ## X must be a model
    ensureModelSupport(X)
    return(update(X, . ~ 1))
  }

  if(is.im(baseline) || is.function(baseline) || is.owin(baseline) ||
     is.tess(baseline) || is.numeric(baseline)) {
    if(Xispattern)
      switch(observations,
             exact = {
               ## WAS: if(is.ppp(X)) return(ppm(X ~ offset(log(baseline))))
               if(is.ppp(X)) return(exactppm(X, baseline=baseline))
               if(is.lpp(X)) {
                 needpackage("spatstat.linnet")
                 return(spatstat.linnet::lppm(X ~ offset(log(baseline))))
               }
             },
             presence = {
               if(is.ppp(X)) {
                 needpackage("spatstat.model")
                 return(spatstat.model::slrm(X ~ offset(log(baseline)), ...))
               }
               if(is.lpp(X))
                 stop("Presence/absence analysis is not yet supported on networks")
             })
    ## X must be a model
    ensureModelSupport(X)
    ## expand the environment to include 'baseline'
    envy <- list2env(list(baseline=baseline),
                     parent=environment(terms(X)))
    if(inherits(X, "slrm")) {
      ## this only seems to work for 'slrm' objects 
      return(update(X, . ~ offset(log(baseline)), env=envy))
    } else {
      ## workaround for ppm and lppm
      cl <- getCall(X)
      cname <- as.character(cl[[1]])
      if(!(cname %in% c("ppm.formula", "lppm.formula")))
        stop(paste("Sorry, models fitted by", dQuote(cname),
                   "cannot be handled yet;",
                   "please refit using an explicit formula"))
      foname <- if(inherits(X, "ppm")) "Q" else "X"
      oldfo <- cl[[foname]]
      newfo <- update(as.formula(oldfo), . ~ offset(log(baseline)))
      environment(newfo) <- envy
      cl[[foname]] <- newfo
      result <- eval(cl, envir=envy)
      return(result)
    }
  }
  stop("Format of 'baseline' is not understood")
}

rocEngine <- function(discrim, nullmodel,
                      ...,
                      covtype = c("covariate", "intensity", "probability"),
                      fittedmodel = NULL,
                      method = "raw", high = TRUE, weights=NULL,
                      discrimAtPoints = NULL,
                      p = seq(0, 1, length=plength),
                      plength = 1024,
                      interpolate=FALSE, jitter=FALSE,
                      subset=NULL, bw="nrd0", adjust=1,
                      CI="none", alpha=0.05, degfreefun=Inf,
                      leftoneout=FALSE) {
  
  ##  `discrim` is the discriminant function
  ##      (an image, or a list of images for each type of point)
  ##  `fittedmodel' is NULL, or a fitted model
  ##  'discrimAtPoints' is an optional vector, providing different values for
  ##      the discriminant at the data point locations.

  #' validate arguments
  covtype <- match.arg(covtype)
  method <- match.arg(method, c("raw", "monotonic", "smooth", "rhohat", "all"),
                      several.ok = TRUE)
  method <- sub("rhohat", "smooth", method)
  if("all" %in% method)
    method <- c("raw", "monotonic", "smooth", "all")

  #' confidence interval (for one of the estimates)
  CI <- match.arg(CI, c("none", "raw", "monotonic", "smooth", "rhohat"))
  CI <- sub("rhohat", "smooth", CI)
  if(CI != "none")
    method <- union(method, CI)

  ensureModelSupport(nullmodel)

  ## Extract data pattern
  X <- spatstat.model::response(nullmodel)
  nX <- npoints(X)

  if(inherits(nullmodel, "slrm") && is.null(nullmodel$Data$dataAtPoints))
    X <- discretise(X, xy=nullmodel$Data$W, move.points=TRUE)

  ## Validate weights 
  if(!is.null(weights)) {
    if(length(weights) == 1) weights <- rep(weights, npoints(X))
    stopifnot(length(weights) == npoints(X))
    stopifnot(all(weights >= 0))
    if(!is.null(subset)) 
      weights <- weights[ppsubset(X, subset)]
    #' normalise weights to sum to 1
    weights <- weights/sum(weights)
  }

  ## Compute basic needed quantities
  d <- spatialCDFframe(nullmodel, discrim, ...,
                       covariateAtPoints = discrimAtPoints,
                       interpolate=interpolate, jitter=jitter,
                       subset=subset,
                       raster.action="ignore",
                       make.quantile.function=TRUE)
  U <- d$values$U
  UU <- if(high) 1 - U else U
  ec <- ewcdf(UU, weights=weights)
  if(!missing(p)) {
    check.nvector(p)
    stopifnot(min(p) >= 0)
    stopifnot(max(p) <= 1)
    if(prod(range(diff(p))) < 0) stop("p should be a monotone sequence")
  }
  FZ <- d$values$FZ
  FZinverse <- d$values$FZinverse
  ## thresholds
  thresh <- if(high) FZinverse(1-p) else FZinverse(p)
  ## Initialize output object
  df <- data.frame(p=p, thresh=thresh, null=p)

  # Add raw estimate if requested
  if("raw" %in% method){
    df$raw <- Estimate <- ec(p)
    if(CI == "raw") {
      se <- sqrt(Estimate * (1-Estimate)/nX)
      crit <- stats::qnorm(1-alpha/2)
      df <- cbind(df,
                  data.frame(se = se,
                             lo = pmax(0.0, Estimate - crit * se),
                             hi = pmin(1.0, Estimate + crit * se)))
      method <- c(method, "se", "lo", "hi")
    }
  }

  if(!is.null(fittedmodel) || covtype != "covariate") {
    ## Add 'theoretical' curve predicted by fitted model
    switch(covtype,
           intensity = ,
           probability = {
             ## traditional usage: the discriminant is the predicted intensity
             lambdavalues <- discrimvalues <- d$values$Zvalues
           },
           covariate = {
             ## Non-traditional usage: model-predicted ROC of another covariate.
             ## Obtain values of fitted intensity and discriminant at each pixel
             b <- spatialCDFframe(fittedmodel, discrim, ...,
                                  interpolate=interpolate, jitter=jitter,
                                  subset=subset,
                                  raster.action="ignore")
             discrimvalues <- b$values$Zvalues
             lambdavalues  <- b$values$lambda
           })
    if(high) {
      F1negZ <- ewcdf(-discrimvalues, lambdavalues/sum(lambdavalues))
      df$theo <- F1negZ(-FZinverse(1-p))
    } else {
      F1Z <- ewcdf(discrimvalues, lambdavalues/sum(lambdavalues))
      df$theo <- F1Z(FZinverse(p))
    }
  }

  ## Add smooth estimate if requested
  if("smooth" %in% method){
    val <- d$values
    doCI <- (CI == "smooth")
    est <- rocSmoothCalc(val$ZX, val$Zvalues,
                         weightsX=weights,
                         weightsU=val$lambda,
                         high=high, p=p,
                         bw=bw, adjust=adjust,
                         doCI=doCI, alpha=alpha,
                         degfreeU=degfreefun)
    if(doCI) {
      df <- cbind(df, est[,c("smooth", "se", "lo", "hi")])
      method <- c(method, "se", "lo", "hi")
    } else {
      df$smooth <- est$smooth
    }
  }

  ## Add monotonic estimate if requested
  if("monotonic" %in% method){
    f <- monotonicRhoFun(X, discrim, increasing = high,
                   subset=subset, weights=weights, baseline=nullmodel)
    est <- roc.function(f, high = high, tp = data.frame(p, thresh),
                        method = "monotonic")
    df$monotonic <- Estimate <- est$monotonic
    if(CI == "monotonic") {
      se <- sqrt(Estimate * (1-Estimate)/nX)
      crit <- stats::qnorm(1-alpha/2)
      df <- cbind(df,
                  data.frame(se = se,
                             lo = pmax(0.0, Estimate - crit * se),
                             hi = pmin(1.0, Estimate + crit * se)))
      method <- c(method, "se", "lo", "hi")
    }
  }

  ## Convert to roc+fv object with nice labels etc.
  rslt <- as.roc.data.frame(df, method = method, covtype=covtype, CI=CI,
                            leftoneout=leftoneout)

  return(rslt)
}

as.roc.data.frame <- function(x, method = NULL,
                              covtype=c("covariate", "intensity",
                                        "probability"),
                              CI="none", leftoneout=FALSE){
  ## Convert table of calculated ROC values
  ## to an object of class 'roc', 'fv' with nice labels etc.
  covtype <- match.arg(covtype)
  nam <- names(x)
  if(!all(c("p", "thresh") %in% nam))
    stop("x must have columns named 'p' and 'thresh'")
  knownestimators <- c("raw", "monotonic", "smooth", "function", "fun")
  knownextras     <- c("se", "lo", "hi")
  knownmethods  <- c(knownestimators, knownextras)
  unchangedTags <- c("p", "null", "theo", "thresh", "fun")
  if(is.null(method) || "all" %in% method) {
    ## infer method from column names
    method <- intersect(knownmethods, nam)
    ## map abbreviations
    method[method == "function"] <- "fun"
  } else {
    ## validate method(s)
    h <- match(method, knownmethods)
    if(any(unknown <- is.na(h))) {
      nuk <- sum(unknown)
      stop(paste("Unrecognised",
                 ngettext(nuk, "method", "methods"),
                 sQuote(commasep(method[unknown]))),
           call.=FALSE)
    }
    ## map abbreviations
    method[method == "function"] <- "fun"
    ## ensure columns required for method are present in data
    if(any(unavail <- !(method %in% nam))) {
      nun <- sum(unavail)
      stop(paste("Cannot perform",
                 ngettext(nun, "method", "methods"),
                 sQuote(commasep(method[unavail])),
                 "because the data were not provided in x"),
           call.=FALSE)
    }
  }
  #' sort methods in order given by 'knownmethods'
  method <- knownmethods[sort(match(method, knownmethods))]
  ## Initialize output object in correct order
  df <- data.frame(p=x$p, thresh=x$thresh, null=x$p)
  threshname <- paste("threshold for",
                switch(covtype, probability = "presence probability", covtype))
  desc <- c("fraction of area",
            threshname,
            "expected fraction of points if no effect")
  labl <- c("p", "threshold", "%s[null](p)")
  dotnames <- "null"

  ## Add theoretical curve in case of a fitted model
  if("theo" %in% nam){
    df$theo <- x$theo
    desc <- c(desc, "expected fraction of points according to model")
    labl <- c(labl, makeRocFlabel("theo"))
    dotnames <- c("theo", dotnames)
  }

  ## Add raw estimate if requested
  if("raw" %in% method){
    df[[makeRocTag("raw", leftoneout)]] <- x$raw
    desc <- c(desc,
              makeRocDesc("observed fraction of points (raw estimate)",
                       leftoneout))
    labl <- c(labl, makeRocFlabel("raw", leftoneout))
  }

  ## Add function estimate if requested
  if("fun" %in% method){
    df$fun <- x$fun
    desc <- c(desc, "observed fraction of points (function estimate)")
    labl <- c(labl, makeRocFlabel("fun"))
  }

  ## Add monotonic estimate if requested
  if("monotonic" %in% method){
    df[[makeRocTag("monotonic", leftoneout)]] <- x$monotonic
    desc <- c(desc,
              makeRocDesc("observed fraction of points (monotonic estimate)",
                       leftoneout))
    labl <- c(labl, makeRocFlabel("monotonic", leftoneout))
  }

  ## Add smooth estimate if requested
  if("smooth" %in% method){
    df[[makeRocTag("smooth", leftoneout)]] <- x$smooth
    desc <- c(desc,
              makeRocDesc("observed fraction of points (smooth estimate)",
                       leftoneout))
    labl <- c(labl, makeRocFlabel("smooth", leftoneout))
  }
  if("se" %in% method){
    df[[makeRocTag("se", leftoneout)]] <- x$se
    desc <- c(desc,
              makeRocDesc(paste("standard error of", CI, "estimate"),
                       leftoneout))
    labl <- c(labl, paste0("se", paren(makeRocFlabel(CI, leftoneout))))
  }
  if("lo" %in% method){
    df[[makeRocTag("lo", leftoneout)]] <- x$lo
    desc <- c(desc,
              makeRocDesc("lower limit of 95%% confidence band", leftoneout))
    labl <- c(labl, makeRocFlabel("lo", leftoneout))
  }
  if("hi" %in% method){
    df[[makeRocTag("hi", leftoneout)]] <- x$hi
    desc <- c(desc,
              makeRocDesc("upper limit of 95%% confidence band", leftoneout))
    labl <- c(labl, makeRocFlabel("hi", leftoneout))
  }


  ## Ensure data are sorted in increasing order of p
  nr <- nrow(df)
  if(with(df, p[1] > p[nr]))
    df <- df[nr:1, ]
  
  result <- fv(df,
               argu  = "p",
               ylab  = quote(roc(p)),
               valu  = makeRocTag(method[1], leftoneout),
               fmla  = . ~ p,
               desc  = desc,
               labl  = labl,
               fname = "roc")
  
  #' add plot info
  dotnames <- c(method, dotnames)
  if(all(c("lo", "hi") %in% method)) {
    shadenames <- c("lo", "hi")
    dotnames <- setdiff(dotnames, c("se", "lo", "hi"))
  } else {
    shadenames <- NULL
  }
  ## 
  changing <- !(dotnames %in% unchangedTags)
  dotnames[changing] <- sapply(dotnames[changing],
                               makeRocTag, leftoneout=leftoneout)
  fvnames(result, ".") <- as.character(dotnames)
  if(!is.null(shadenames)) {
    shadenames <- sapply(shadenames, makeRocTag, leftoneout=leftoneout)
    fvnames(result, ".s") <- as.character(shadenames)
  }
  #' 
  class(result) <- c("roc", class(result))
  return(result)
}

plot.roc <- function(x, fmla, ..., main, threshold=FALSE) {
  if(missing(main)) main <- short.deparse(substitute(x))
  if(missing(fmla)) fmla <- NULL
  result <- plot.fv(x, fmla=fmla, main=main, ...)
  if(is.null(fmla) && threshold) {
    p <- with(x, .x)
    thresh <- with(x, thresh)
    fobs <- with(x, .y)
    FZXinv <- approxfun(thresh, fobs, rule=2)
    FZinv <- approxfun(thresh, p, rule=2)
    tval <- prettyweird(thresh, fobs)$x
    pval <- FZXinv(tval)
    axis(4, at=pval, labels=tval)
    mtext("Threshold value", 4, 2)
    tval <- prettyweird(thresh, p)$x
    pval <- FZinv(tval)
    nmain <- sum(nzchar(main))
    nline <- if(nmain == 0) 0 else (nmain + 2)
    axis(3, at=pval, labels=tval, line = nline)
    mtext("Threshold value", 3, nline+2)
  }
  return(invisible(result))
}

rocDummy <- function(X, U, covariate, ...,
                     high=TRUE,
                     method = c("raw", "monotonic", "smooth"),
                     subset=NULL, weights=NULL, weightsU=NULL,
                     p = seq(0, 1, length=plength),
                     plength = 1024,
                     bw="nrd0", adjust=1,
                     CI="none", alpha=0.05, degfreefun=NULL) {
  method <- match.arg(method, several.ok=TRUE)
  if(!missing(p)) {
    check.nvector(p)
    stopifnot(min(p) >= 0)
    stopifnot(max(p) <= 1)
    if(prod(range(diff(p))) < 0) stop("p should be a monotone sequence")
  }
  #' confidence interval (for one of the estimates)
  CI <- match.arg(CI, c("none", "raw", "monotonic", "smooth"))
  if(CI != "none") method <- union(method, CI)
  #' X and U are data and dummy point patterns, respectively
  if(!is.null(weights)) {
    if(length(weights) == 1) weights <- rep(weights, npoints(X))
    stopifnot(length(weights) == npoints(X))
    stopifnot(all(weights >= 0))
  }
  if(!is.null(weightsU)) {
    if(length(weightsU) == 1) weightsU <- rep(weights, npoints(U))
    stopifnot(length(weightsU) == npoints(U))
    stopifnot(all(weightsU >= 0))
  }
  if(!is.null(subset)) {
    if(!is.null(weights)) weights <- weights[ppsubset(X, subset)]
    if(!is.null(weightsU)) weightsU <- weightsU[ppsubset(U, subset)]
    X <- X[subset]
    U <- U[subset]
  }
  nX <- npoints(X)
  if(!is.null(weights)) weights <- weights/sum(weights)
  if(!is.null(weightsU)) weightsU <- weightsU/sum(weightsU)
  ZX <- evaluateCovariate(covariate, X)
  ZU <- evaluateCovariate(covariate, U)
  FZX <- ewcdf(ZX, weights=weights)
  FZU <- ewcdf(ZU, weights=weightsU)
  efzu <- environment(FZU)
  FZUinverse <- approxfun(get("y", envir=efzu),
                          get("x", envir=efzu),
                          rule=2)
  thresh <- FZUinverse(if(high) (1-p) else p)
  df <- data.frame(p=p, thresh=thresh, null=p)
  if("raw" %in% method) {
    raw <- if(high) 1 - FZX(thresh) else FZX(thresh)
    df$raw <- raw
    if(CI == "raw") {
      se <- sqrt(raw * (1-raw)/nX)
      crit <- stats::qnorm(1-alpha/2)
      df <- cbind(df,
                  data.frame(se = se,
                             lo = pmax(0.0, raw - crit * se),
                             hi = pmin(1.0, raw + crit * se)))
      method <- c(method, "se", "lo", "hi")
    }
  }
  if("smooth" %in% method) {
    doCI <- (CI == "smooth")
    est <- rocSmoothCalc(ZX, ZU, weightsX=weights, weightsU=weightsU,
                         p=p, high=high, bw=bw, adjust=adjust,
                         doCI=doCI, alpha=alpha, degfreeU=degfreefun)
    if(doCI) {
      df <- cbind(df, est[,c("smooth", "se", "lo", "hi")])
      method <- c(method, "lo", "hi")
    } else {
      df$smooth <- est$smooth
    }
  }
  if("monotonic" %in% method){
    f <- monotonicRhoFunCalc(x=ZX, massx=weights, z=ZU, weightz=weightsU,
                       increasing = high)
    est <- roc.function(f, high = high, tp = df[,c("p", "thresh")],
                        method = "monotonic")
    df$monotonic <- Estimate <- est$monotonic
    if(CI == "monotonic") {
      se <- sqrt(Estimate * (1-Estimate)/nX)
      crit <- stats::qnorm(1-alpha/2)
      df <- cbind(df,
                  data.frame(se = se,
                             lo = pmax(0.0, Estimate - crit * se),
                             hi = pmin(1.0, Estimate + crit * se)))
      method <- c(method, "se", "lo", "hi")
    }
  }
  return(as.roc.data.frame(df, method, CI=CI))
}

makeRocFlabel <- function(sub="raw", leftoneout=FALSE,
                           super=NULL, f="%s", argu="p") {
  if(is.null(super) && leftoneout) super <- "bold(\"-\")"
  z <- if(is.null(super)) {
         paste0(f, "[", sub, "](", argu, ")")
       } else {
         paste0("{", f, "[", sub, "]^{", super, "}}(", argu, ")")
       }
  return(z)
}

makeRocTag <- function(sub="raw", leftoneout=FALSE) {
  paste0(sub, if(leftoneout) "loo" else "")
}

makeRocDesc <- function(desc, leftoneout=FALSE) {
  paste0(desc, if(leftoneout) " (leave-one-out)" else "")
}




#' Compute the expected ROC curve for covariate Z
#' given that the true intensity is lambda(u) = rho(Z(u))

roc.rhohat <- function(X, ..., high = TRUE){
  covar <- attr(X, "stuff")$Zimage
  f <- as.function(X, extrapolate = TRUE)
  roc.function(f, covar, ..., high = high, method = "smooth")
}

roc.function <- function(X, covariate, ...,
                         high = TRUE, tp=NULL, method = "function",
                         nsteps = 1024){
  if(is.null(tp)) {
    if(!is.im(covariate)) covariate <- as.im(covariate, ...)
    if(high) covariate <-  -covariate
    covcdf <- spatialcdf(covariate) 
    p <- seq(0, 1, length.out = nsteps)
    thresh <- quantile.ewcdf(covcdf, p)
    if(high) thresh <- -thresh
  } else {
    p      <- tp$p
    thresh <- tp$thresh
    if(is.null(p) || is.null(thresh))
      stop("tp should be a data frame with columns named 'p' and 'thresh'")
  }
  integrand <- X(thresh)
  est <- cumsum(integrand)
  est <- est/max(est)
  ## Basic data.frame
  df <- data.frame(p = p, null = p, thresh = thresh, est=est)
  ## Correct column name for estimate
  names(df)[4] <- if(method == "function") "fun" else method
  ## compute ROC 
  result <- as.roc.data.frame(df, method=method)
  return(result)
}


#' ROC methods for other classes

roc.spatialCDFframe <- function(X, ..., high=TRUE, plength=1024) {
  trap.extra.arguments(...)
  U <- X$values$U
  UU <- if(high) 1 - U else U
  ec <- ewcdf(UU, weights=X$weights)
  p <- seq(0, 1, length=plength)
  FZ <- X$values$FZ
  FZinverse <- quantilefun(FZ)
  thresh <- if(high) FZinverse(1-p) else FZinverse(p)
  #' set up data
  df <- data.frame(p=p, thresh=thresh, null=p, raw=ec(p))
  #' Convert to roc+fv object with nice labels etc.
  rslt <- as.roc.data.frame(df, method = "raw")
  return(rslt)
}

roc.cdftest <- function(X, ..., high=TRUE) {
  roc(attr(X, "frame"), high=high, ...)
}

roc.bermantest <- function(X, ..., high=TRUE) {
  roc(X[["fram"]], high=high, ...)
}


#'
#'   Compute estimate of ROC by kernel smoothing
#'   and compute confidence bands
#'
#'   Original code by Suman Rakshit 2017, edited by Adrian Baddeley 2018-2023
#' 

rocSmoothCalc <- function(ZX, ZU,
                          ...,
                          weightsX=NULL, weightsU=NULL,
                          high=TRUE,
                          kernel="gaussian", bw="nrd0", adjust=1, 
                          alpha=0.05,
                          nGrid=2048,
                          p=seq(0, 1, length.out=plength),
                          plength=1024,
                          doCI=TRUE,
                          degfreeU=length(ZU)) {
  ra <- range(ZX, ZU, na.rm=TRUE)
  if(!is.null(weightsX)) {
    check.nvector(weightsX, length(ZX), things="points of X")
    weightsX <- weightsX/sum(weightsX)
  }
  if(!is.null(weightsU)) {
    check.nvector(weightsU, length(ZU), things="points/pixels of U")
    weightsU <- weightsU/sum(weightsU)
  }
  if(!missing(p)) {
    check.nvector(p)
    stopifnot(min(p) >= 0)
    stopifnot(max(p) <= 1)
    if(prod(range(diff(p))) < 0) stop("p should be a monotone sequence")
    plength <- length(p)
  }
  #' bw and adjust can be vectors of length 2
  #' specifying smoothing for ROC [1] and densities [2]
  bw <- rep(bw, 2)
  adjust <- rep(adjust, 2)
  #' degfreeU is either a single number (including Inf) or a function
  if(is.null(degfreeU)) degfreeU <- length(ZU) 
  if(is.function(degfreeU)) degfreeU <- degfreeU(length(ZU))
  check.1.real(degfreeU)
  stopifnot(degfreeU > 0)
  #' transform observed values using empirical cdf of null
  uX <- if(high) ewcdf(-ZU, weightsU)(-ZX) else ewcdf(ZU, weightsU)(ZX)
  #' perform density estimation on this scale
  fuX <- density(uX, from=0, to=1, n=nGrid, bw=bw[1], adjust=adjust[1])
  #' integrate to obtain ROC estimate
  ppp <- fuX$x
  yyy <- fuX$y
  ROCfun <- approxfun(ppp, cumsum(yyy)/sum(yyy), yleft=0, yright=1, rule=1)
  ROCp <- ROCfun(p)
  #' estimate pdf's
  fX <- density(ZX, weights=weightsX,
                kernel=kernel, bw=bw[2], adjust=adjust[2],
                from=ra[1], to=ra[2], n=nGrid)
  sigma <- fX$bw
  fU <- density(ZU, weights=weightsU,
                kernel=kernel, bw=sigma,
                from=ra[1], to=ra[2], n=nGrid)
  zz <- fX$x
  fXz <- fX$y
  fUz <- fU$y
  fX <- approxfun(zz, fXz, yleft=0, yright=0)
  fU <- approxfun(zz, fUz, yleft=0, yright=0)
  #' integrate to obtain cdf's
  FXz <- cumsum(fXz)/sum(fXz)
  FUz <- cumsum(fUz)/sum(fUz)
  FX <- approxfun(zz, FXz, yleft=0, yright=1, rule=1)
  FU <- approxfun(zz, FUz, yleft=0, yright=1, rule=1)
  FUinverse <- approxfun(FUz, zz, rule=2)
  #' Compute threshold at each desired value of p
  zzp <- FUinverse(if(high) 1-p else p)
  #' assemble result
  df <- data.frame(p = p , thresh=zzp, smooth=ROCp)
  if(doCI) {
    #' Compute standard error
    se2A <- (1/length(ZX)) * ROCp * (1 - ROCp)
    se2B <- (1/degfreeU) * p    * (1-p)      * ((fX(zzp)/fU(zzp))^2)
    se <- sqrt(se2A + se2B)
    ## Compute the upper and lower pointwise confidence band
    crit <- stats::qnorm(1-alpha/2)
    hi <- pmin(1.0, ROCp + crit * se)
    lo <- pmax(0.0, ROCp - crit * se)
    ## Add to result
    df <- cbind(df, data.frame(se=se, lo = lo, hi = hi))
  }
  return(df)
}

Try the spatstat.explore package in your browser

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

spatstat.explore documentation built on Aug. 8, 2025, 7:36 p.m.