Resampling Inference in R (boot, boot.ci, perm.log.reg)

Context: FIT2086_MOC · Studio 10 · the boot package for any statistic + a coefficient-wise permutation test for glm · theory ➔ Bootstrap, Permutation Tests · hand-rolled loops ➔ R Toolkit (Cheatsheet) · scoring helper from Logistic Regression in R (glm, pROC, step) Problem it solves: a CI or a -value for a statistic with no usable formula — a median, a fitted model’s AUC, a logistic coefficient — by computation instead of derivation.

Quick Revision

  • 🎯 Trigger: “CI for any statistic” ➔ write stat(data, indices) ➔ boot(data, stat, R) ➔ boot.ci(bs, 0.95, type = "bca") · “-value without normal theory” ➔ permute the target, refit, compare .
  • ⚡ Key Constraint: the statistic must take (data, indices) and subset with them — x[I] for a vector, data[indices, ] for a data frame (the comma keeps whole rows); extra arguments such as formula go to boot(…) by name.

🔧 Minimal Working Example

library(boot)                                         # install.packages("boot") once
bpdata = read.csv("bpdata.csv")                       # n = 20 men, BSA in m^2
 
t.test(x = bpdata$BSA, conf.level = 0.95)             # parametric reference
bs.mean = boot(bpdata$BSA, function(x, I) { return(mean(x[I])) }, 1000)
bs.mean                                               # original / bias / std. error
boot.ci(bs.mean, 0.95, type = "basic")
boot.ci(bs.mean, 0.95, type = "bca")
bs.med = boot(bpdata$BSA, function(x, I) { return(median(x[I])) }, 1000)
boot.ci(bs.med, 0.95, type = "basic")
plot(bs.mean); plot(bs.med)                           # histogram + QQ of the R replicates

Expected output (Studio 10 notebook, set.seed(2086); reruns move the 3rd decimal):

StatisticOriginalBootstrap biasBootstrap seBasic 95%BCa 95%
mean BSA–
median BSA–—
-test (reference)———
  • Reading ➔ all mean intervals overlap heavily; bootstrap intervals slightly narrower than · the median’s se is the mean’s and its interval wider · plot(bs.med) is spiky at ➔ why: Bootstrap §7.

🔀 Variations

Bootstrapping a model statistic (AUC of a fitted logistic regression)

pima.train = read.csv("pima.train.csv", stringsAsFactors = T)
source("studio10.prediction.stats.R")                 # my.pred.stats(..., display = F) RETURNS a list
fit = glm(DIABETES ~ ., pima.train, family = binomial)
rv  = my.pred.stats(predict(fit, pima.train, type = "response"), pima.train$DIABETES, display = F)
rv$auc                                                # 0.848 (train); also $ca $sens $spec $log.loss
 
boot.auc = function(formula, data, indices) {
  d   = data[indices, ]                               # resample whole ROWS
  fit = glm(formula, d, family = binomial)            # refit on the resample
  target = as.character(fit$terms[[2]])               # "DIABETES" pulled from the formula
  rv = my.pred.stats(predict(fit, d, type = "response"), d[, target], display = F)
  return(rv$auc)
}
bs = boot(data = pima.train, statistic = boot.auc, R = 1000, formula = DIABETES ~ .)
boot.ci(bs, conf = 0.95, type = "bca"); plot(bs)      # ~(0.811, 0.872)
 
pima.test = read.csv("pima.test.csv", stringsAsFactors = T)
my.pred.stats(predict(fit, pima.test, type = "response"), pima.test$DIABETES)   # test AUC 0.816
  • Result ➔ train CA , sens , spec , AUC · test CA , AUC — inside the bootstrap interval, but the interval is centred on an optimistic in-sample AUC.

Permutation test for every glm coefficient (perm.log.reg.R)

perm.log.reg = function(formula, data, R) {
  rv = list(); n = nrow(data)
  rv$fit = glm(formula, data, family = binomial)      # observed coefficients
  target = as.character(rv$fit$terms[[2]])
  rv$perm.coef = matrix(NA, R, length(rv$fit$coefficients),
                        dimnames = list(NULL, names(rv$fit$coefficients)))
  d = data
  for (i in 1:R) {
    d[, target] = d[sample(n), target]                # shuffle the target column only
    rv$perm.coef[i, ] = glm(formula, d, family = binomial)$coefficients
  }
  # row-wise |perm| > |obs| for every column, then the proportion per coefficient
  rv$p.value = colMeans(sweep(abs(rv$perm.coef), 2, abs(rv$fit$coefficients), ">"))
  return(rv)
}
rv = perm.log.reg(DIABETES ~ ., pima.train, R = 10000)
rv$p.value; summary(rv$fit)
hist(rv$perm.coef[, "INS"]); hist(rv$perm.coef[, "BP"])   # empirical null per coefficient

Expected output:

CoefficientWald Pr(>|z|)Perm , Perm ,
PREG
PLAS
BP
SKIN
INS
BMI
PED
AGE
  • Reading ➔ same big picture, different small -values (BP, PED, AGE): permutation simulates the global null, Wald tests given the others ➔ Permutation Tests §4 · INS: deep inside a null spanning ⟹ · BP: outside the null bulk ⟹ .

✍️ Practice

⚠️ Common Mistakes

  • 💡 Statistic ignores indices ➔ function(x, I) mean(x) returns the original value every time ⟹ se , degenerate interval.
  • 💡 data[indices] on a data frame ➔ selects columns; rows need data[indices, ].
  • 💡 Shipped perm.log.reg vs the solutions’ claim ➔ the solutions say “no permutation -value is exactly zero (uses )”, but the supplied code computes and prints 0.0000 for PREG, PLAS, BMI ➔ report "", or patch it (Practice 3).
  • 💡 plot(bs) read as the data’s histogram ➔ it shows the replicates of the statistic — the bootstrap sampling distribution.