Nothing
# Per-observation likelihood weights on the SPATIAL tulpa_laplace() route
# (gcol33/tulpa#385).
#
# `weights` used to reach only the non-spatial route. dispatch_laplace_spatial()
# carried no `weights` argument at all, so the spatial kernels found the
# UNWEIGHTED mode -- and then .marginal_H_beta_*(), which has always consumed
# `weights`, built the WEIGHTED marginal precision at that point. The reported
# mode and the reported precision described two different models: on the ICAR
# fixture below the weighted score at the reported mode was 14.86 rather than 0,
# and the slope moved 19% once the weight reached the Newton loop.
#
# The channel was already there -- BuiltinFamilyResponse::weights scales each
# row's log-density, score and Fisher curvature alike -- so the fix is the
# argument reaching the kernels, not new arithmetic. The gates here are
# therefore (a) an all-ones weight is EXACTLY a no-op on every kernel, (b) an
# integer weight is exactly row replication, and (c) the weighted mode is the
# stationary point of the weighted penalized objective, written out
# independently of the engine.
# The penalized objective the ICAR kernel maximizes, from the definition:
# sum_i w_i * ll(y_i | eta_i)
# - 0.5 * tau_beta * ||beta||^2 (sigma_beta = 100 in the kernel)
# - 0.5 * tau * u' (D - A) u (ICAR structure, tau_spatial = 1)
# - 0.5 * (tau / J) * (sum_c u)^2 (sum-to-zero pin, J the component
# size; inst/include/tulpa/sum_to_zero.h)
.icar_ref <- function(y, ntr, X, W, unit, w) {
n_units <- nrow(W); N <- length(unit); p <- ncol(X)
Q <- diag(rowSums(W)) - W + matrix(1 / n_units, n_units, n_units)
Z <- matrix(0, N, n_units); Z[cbind(seq_len(N), unit)] <- 1
M <- cbind(X, Z)
Qj <- as.matrix(Matrix::bdiag(Matrix::Diagonal(p, 1 / 100^2),
Matrix::Matrix(Q, sparse = TRUE)))
score <- function(th) {
e <- as.numeric(M %*% th)
as.numeric(crossprod(M, w * (y - ntr * plogis(e)))) - as.numeric(Qj %*% th)
}
hess <- function(th) {
e <- as.numeric(M %*% th); pr <- plogis(e)
crossprod(M, (w * ntr * pr * (1 - pr)) * M) + Qj
}
th <- rep(0, p + n_units)
for (it in seq_len(300)) {
st <- solve(hess(th), score(th))
th <- th + st
if (max(abs(st)) < 1e-12) break
}
# The kernel centres the field after the Newton loop and folds the removed
# mean into the intercept, leaving eta unchanged; match that convention.
m <- mean(th[p + seq_len(n_units)])
th[1] <- th[1] + m
th[p + seq_len(n_units)] <- th[p + seq_len(n_units)] - m
list(theta = th, score = score, hess = hess, M = M, Qj = Qj, p = p,
n_units = n_units)
}
.areal_fixture <- function(seed, nr = 5L, nc = 5L, N = 300L) {
set.seed(seed)
W <- rook_adj(nr, nc)
n_units <- nrow(W)
unit <- sample.int(n_units, N, replace = TRUE)
X <- cbind(1, rnorm(N), rnorm(N))
u <- rnorm(n_units, 0, 0.7); u <- u - mean(u)
ntr <- rep(1L, N)
y <- rbinom(N, ntr, plogis(as.numeric(X %*% c(-0.4, 0.8, -0.5)) + u[unit]))
list(W = W, unit = unit, X = X, y = as.numeric(y), ntr = ntr, N = N,
spatial = list(type = "icar", adjacency = W, spatial_idx = unit,
scale_factor = 1.0))
}
# A validated NNGP spec: site i conditions on its `nn` nearest predecessors in
# the natural order, the layout laplace_gp_at() reads (nn_order 1-based, nn_idx
# 1-based into that order).
.nngp_fixture <- function(ng, seed, nn = 5L) {
set.seed(seed)
coords <- cbind(runif(ng), runif(ng))
fld <- as.numeric(scale(sin(3 * coords[, 1]) + coords[, 2]))
y <- as.numeric(rbinom(ng, 1, plogis(fld)))
X <- cbind(1, rnorm(ng))
d <- as.matrix(dist(coords))
nn_idx <- matrix(0L, ng, nn); nn_dist <- matrix(0, ng, nn)
for (i in seq_len(ng)) {
prev <- seq_len(i - 1L); if (!length(prev)) next
k <- min(nn, length(prev)); o <- order(d[i, prev])[seq_len(k)]
nn_idx[i, seq_len(k)] <- as.integer(prev[o])
nn_dist[i, seq_len(k)] <- d[i, prev[o]]
}
list(y = y, X = X, ntr = rep(1L, ng),
spatial = list(type = "gp", cov = "exponential",
unique_coords = coords, n_spatial = ng, nn = nn,
obs_to_loc = seq_len(ng),
neighbor_info = list(nn_idx = nn_idx, nn_dist = nn_dist,
nn_order = seq_len(ng)),
sigma2_gp = 0.9, phi_gp = 0.4))
}
test_that("a spatial fit rejects a weights vector of the wrong length", {
a <- .areal_fixture(702L, N = 60L)
expect_error(
tulpa_laplace(a$y, a$ntr, a$X, family = "binomial", spatial = a$spatial,
weights = rep(1, a$N - 1L)),
regexp = "length"
)
})
skip_on_cran()
test_that("an all-ones weight vector is exactly a no-op on every spatial kernel", {
skip_on_cran()
# This is the gate that protects every existing spatial fit: nothing may move
# when no weight is supplied, so the comparison is at tolerance = 0.
set.seed(700)
a <- .areal_fixture(700L, N = 180L)
ones <- rep(1, a$N)
for (ty in c("icar", "car", "bym2", "car_proper")) {
sp <- if (ty == "car_proper")
list(type = "car_proper", adjacency = a$W, spatial_idx = a$unit,
tau = 1.0, rho = 0.4)
else list(type = ty, adjacency = a$W, spatial_idx = a$unit,
scale_factor = 1.0)
f0 <- tulpa_laplace(a$y, a$ntr, a$X, family = "binomial", spatial = sp)
f1 <- tulpa_laplace(a$y, a$ntr, a$X, family = "binomial", spatial = sp,
weights = ones)
expect_equal(f0$mode, f1$mode, tolerance = 0, label = ty)
expect_equal(f0$log_marginal, f1$log_marginal, tolerance = 0, label = ty)
expect_equal(f0$H_beta, f1$H_beta, tolerance = 0, label = ty)
}
# HSGP (dense basis) and NNGP (Vecchia, sparse Newton) take different
# scatters, so both are checked rather than one standing for the other.
coords <- cbind(runif(a$N), runif(a$N))
sp_h <- list(type = "hsgp", coords_matrix = coords, m = 6L, c = 1.5,
sigma2 = 0.8, lengthscale = 0.5)
h0 <- tulpa_laplace(a$y, a$ntr, a$X, family = "binomial", spatial = sp_h)
h1 <- tulpa_laplace(a$y, a$ntr, a$X, family = "binomial", spatial = sp_h,
weights = ones)
expect_equal(h0$mode, h1$mode, tolerance = 0)
expect_equal(h0$log_marginal, h1$log_marginal, tolerance = 0)
g <- .nngp_fixture(90L, 12L)
n0 <- tulpa_laplace(g$y, g$ntr, g$X, family = "binomial", spatial = g$spatial)
n1 <- tulpa_laplace(g$y, g$ntr, g$X, family = "binomial", spatial = g$spatial,
weights = rep(1, length(g$y)))
expect_equal(n0$mode, n1$mode, tolerance = 0)
expect_equal(n0$log_marginal, n1$log_marginal, tolerance = 0)
})
test_that("an integer weight is exactly row replication (areal + NNGP)", {
skip_on_cran()
# sum_i w_i ll_i is the log-likelihood of the data set with row i repeated
# w_i times, and neither the field prior nor the fixed-effect prior depends on
# N -- so the two penalized objectives are the same function and must have the
# same mode. This reads the weighted path against the engine's own unweighted
# solver on a different data set, so a weight applied to the score but not the
# curvature (or to neither) fails it.
a <- .areal_fixture(701L, N = 150L)
w <- rep(c(1, 2, 3), length.out = a$N)
rep_idx <- rep(seq_len(a$N), times = w)
for (ty in c("icar", "bym2", "car_proper")) {
sp_w <- if (ty == "car_proper")
list(type = "car_proper", adjacency = a$W, spatial_idx = a$unit,
tau = 1.0, rho = 0.4)
else list(type = ty, adjacency = a$W, spatial_idx = a$unit,
scale_factor = 1.0)
sp_r <- sp_w
sp_r$spatial_idx <- a$unit[rep_idx]
fw <- tulpa_laplace(a$y, a$ntr, a$X, family = "binomial", spatial = sp_w,
weights = w, max_iter = 300L, tol = 1e-12)
fr <- tulpa_laplace(a$y[rep_idx], a$ntr[rep_idx],
a$X[rep_idx, , drop = FALSE], family = "binomial",
spatial = sp_r, max_iter = 300L, tol = 1e-12)
expect_equal(fw$mode, fr$mode, tolerance = 1e-6, label = ty)
expect_equal(fw$H_beta, fr$H_beta, tolerance = 1e-6, label = ty)
}
g <- .nngp_fixture(80L, 13L)
n <- length(g$y)
wg <- rep(c(1, 3), length.out = n)
ri <- rep(seq_len(n), times = wg)
sp_r <- g$spatial
sp_r$obs_to_loc <- ri # replicated rows share their field node
fw <- tulpa_laplace(g$y, g$ntr, g$X, family = "binomial",
spatial = g$spatial, weights = wg,
max_iter = 300L, tol = 1e-12)
fr <- tulpa_laplace(g$y[ri], g$ntr[ri], g$X[ri, , drop = FALSE],
family = "binomial", spatial = sp_r,
max_iter = 300L, tol = 1e-12)
expect_equal(fw$mode, fr$mode, tolerance = 1e-6)
})
test_that("the weighted spatial mode is the weighted penalized optimum", {
skip_on_cran()
# The arbiter is an R Newton solve on the objective written from its
# definition, not on anything the engine computes.
a <- .areal_fixture(20385L)
w <- ifelse(seq_len(a$N) %% 2L == 0L, 4, 1)
fit <- tulpa_laplace(a$y, a$ntr, a$X, family = "binomial",
spatial = a$spatial, weights = w,
max_iter = 300L, tol = 1e-12)
ref_w <- .icar_ref(a$y, a$ntr, a$X, a$W, a$unit, w)
ref_u <- .icar_ref(a$y, a$ntr, a$X, a$W, a$unit, rep(1, a$N))
p <- ncol(a$X)
expect_lt(max(abs(fit$mode[seq_len(p)] - ref_w$theta[seq_len(p)])), 1e-6)
# The weighted and unweighted optima are genuinely apart on this fixture, so
# the assertion above is not satisfied by simply ignoring the weight.
expect_gt(max(abs(ref_w$theta[seq_len(p)] - ref_u$theta[seq_len(p)])), 0.05)
# Stationarity of the weighted objective at the point the fit reports. The
# Newton loop stops on its step, so this residual settles at a floor set by
# conditioning rather than at zero (~1e-06 here). What the bound separates is
# a settled weighted mode from the unweighted one, which left this same
# quantity at 14.86 -- seven orders of magnitude up.
expect_lt(max(abs(ref_w$score(fit$mode))), 1e-4)
})
test_that("the spatial mode and its H_beta describe the same weighted model", {
skip_on_cran()
# #385 itself: H_beta was already the weighted Schur, the mode was not. The
# pair is only a posterior once both read the same w.
a <- .areal_fixture(20386L, N = 260L)
w <- runif(a$N, 0.2, 3)
fit <- tulpa_laplace(a$y, a$ntr, a$X, family = "binomial",
spatial = a$spatial, weights = w,
max_iter = 300L, tol = 1e-12)
ref <- .icar_ref(a$y, a$ntr, a$X, a$W, a$unit, w)
p <- ncol(a$X)
H <- ref$hess(fit$mode)
idx_b <- seq_len(p); idx_u <- p + seq_len(ref$n_units)
# The field's level is a hard sum-to-zero constraint, so the Schur takes the
# field block's inverse conditioned on 1'u = 0 (gcol33/tulpa#901).
Pinv <- solve(H[idx_u, idx_u])
a <- rowSums(Pinv)
Pinv_c <- Pinv - tcrossprod(a) / sum(a)
schur <- H[idx_b, idx_b] -
H[idx_b, idx_u] %*% Pinv_c %*% t(H[idx_b, idx_u])
# The engine's marginal omits the weak fixed-effect ridge the mode carries.
schur <- schur - diag(1 / 100^2, p)
expect_lt(max(abs(fit$H_beta - schur)) / max(abs(schur)), 1e-6)
expect_lt(max(abs(ref$score(fit$mode))), 1e-4)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.