Nothing
# Copyright 2024 by Roger S. Bivand.
# doi: 10.1111/gean.12390
# François Bavaud (2024) Measuring and Testing Multivariate Spatial
# Autocorrelation in a Weighted Setting: A Kernel Approach,
# Geographical Analysis (2024) 56, 573-599
spatialdelta <- function(dissimilarity_matrix, adjusted_spatial_weights,
regional_weights=NULL, alternative="greater") {
alternative <- match.arg(alternative, c("greater", "less", "two.sided"))
if (is.null(regional_weights)) {
if (inherits(adjusted_spatial_weights, "adjusted_spatial_weights"))
regional_weights <- attr(adjusted_spatial_weights, "regional_weights")
else stop("regional_weights must be provided")
}
stopifnot(all(is.finite(regional_weights)))
stopifnot(all(regional_weights > 0)) # page 576
if (sum(regional_weights) != 1) { # page 576
regional_weights <- regional_weights/sum(regional_weights)
warning("regional_weights changed to sum to unity")
}
n <- length(regional_weights)
stopifnot(nrow(dissimilarity_matrix) == n)
stopifnot(ncol(dissimilarity_matrix) == n)
stopifnot(nrow(adjusted_spatial_weights) == n)
stopifnot(ncol(adjusted_spatial_weights) == n)
rnames <- rownames(adjusted_spatial_weights)
stopifnot(length(rnames) == n)
E <- diag(regional_weights) %*% adjusted_spatial_weights
H <- diag(n) - rep(1, times=n) %*% t(regional_weights) # above eq. 15
B <- -0.5 * (H %*% dissimilarity_matrix %*% t(H)) # eq. 15
sf <- sqrt(regional_weights)
dsf <- diag(sf)
dsf1 <- diag(1/sf)
Kx <- dsf %*% B %*% dsf # eq. 16
Kw <- dsf %*% adjusted_spatial_weights %*% dsf1 - tcrossprod(sf) # eq. 21
trKx <- sum(diag(Kx))
trKxKx <- sum(diag(Kx %*% Kx))
trKwKx <- sum(diag(Kw %*% Kx))
trKwKw <- sum(diag(Kw %*% Kw))
trKw <- sum(diag(Kw))
mubar2 <- (trKwKw/(n-1)) - (trKw/(n-1))^2 # p. 591
lbar2 <- (trKxKx/(n-1)) - (trKx/(n-1))^2 # p. 591
trKwKwKw <- sum(diag(Kw %*% Kw %*% Kw))
mubar3 <- (trKwKwKw/(n-1)) - 3*((trKwKw/(n-1))*(trKw/(n-1))) +
2*((trKw/(n-1))^3) # p. 591
trKxKxKx <- sum(diag(Kx %*% Kx %*% Kx))
lbar3 <- trKxKxKx/(n-1) - 3*((trKxKx/(n-1))*(trKx/(n-1))) +
2*((trKx/(n-1))^3) # p. 591
trKwKwKwKw <- sum(diag(Kw %*% Kw %*% Kw %*% Kw))
mubar4 <- (trKwKwKwKw/(n-1)) - 4*((trKwKwKw/(n-1))*(trKw/(n-1))) +
6*((trKwKw/(n-1))*((trKw/(n-1))^2)) - 3*((trKw/(n-1))^4)
trKxKxKxKx <- sum(diag(Kx %*% Kx %*% Kx %*% Kx)) # p. 591
lbar4 <- (trKxKxKxKx/(n-1)) - 4*((trKxKxKx/(n-1))*(trKx/(n-1))) +
6*((trKxKx/(n-1))*((trKx/(n-1))^2)) - 3*((trKx/(n-1))^4) # p. 591
d <- trKwKx/trKx # eq. 12 and 26
RV <- d*(trKx/(trKwKw*trKxKx)) # eq. 27
trKw <- sum(diag(Kw))
Ed <- trKw/(n-1) # eq. 36
Vd1 <- 2/((n-2)*((n-1)^2)*(n+1))
Vd2 <- (n-1)*trKwKw - trKw^2
Vd3 <- (((n-1)*trKxKx) - trKx^2)/(trKx^2)
Vd <- Vd1*Vd2*Vd3 # eq. 37
VI <- (2/(n^2 - 1))*(sum(diag(adjusted_spatial_weights %*%
adjusted_spatial_weights)) -
1 -((sum(diag(adjusted_spatial_weights)) - 1)^2/(n-1)))
Vx <- ((1/(n-2))*(((n-1)/(trKx^2/trKxKx)) - 1))
Vd0 <- VI*Vx # eq. 38
skewd1 <- sqrt(8*((n-2)*(n+1)))/((n-3)*(n+3))
alphamu <- (mubar3/(mubar2^(3/2)))
alphalambda <- (lbar3/(lbar2^(3/2)))
skewd2 <- alphamu * alphalambda
skewd <- skewd1*skewd2 # eq. 39
gammamu <- (mubar4/mubar2^2) - 3
gammalambda <- (lbar4/lbar2^2) - 3
kurtd1 <- ((3*(n-2)*(n+1)) / ((n-4)*(n-3)*(n-1)*n*(n+3)*(n+5)))
kurtd2 <- (4*(n^2 - n +2)*gammamu*gammalambda) +
(4*n^2 - 8*n + 52)*(gammamu + gammalambda)
kurtd3 <- (4*(5*n^3 - 57*n^2 + 25*n + 169)/((n-2)*(n-1)))
kurtd <- kurtd1 * (kurtd2 - kurtd3) # eq. 40
std_d <- (d-Ed)/sqrt(Vd) # eq. 41
if (alternative == "two.sided")
pv_d <- 2 * pnorm(abs(std_d), lower.tail=FALSE)
else if (alternative == "greater")
pv_d <- pnorm(std_d, lower.tail=FALSE)
else pv_d <- pnorm(std_d)
names(std_d) <- "Standard deviate"
vec <- c(d, Ed, Vd, skewd, kurtd)
names(vec) <- c("delta", "Expectation", "Variance", "Skewness",
"Excess Kurtosis")
method <- "Bavaud delta normal approximation"
data.name <- paste(deparse(substitute(dissimilarity_matrix)), "\nweights:",
deparse(substitute(adjusted_spatial_weights)))
res <- list(statistic=std_d, p.value=pv_d, estimate=vec,
alternative=alternative, method=method, data.name=data.name)
attr(res, "Kx") <- Kx
attr(res, "Kw") <- Kw
attr(res, "regional_weights") <- regional_weights
attr(res, "adjusted_spatial_weights") <- adjusted_spatial_weights
attr(res, "B") <- B
attr(res, "VI") <- VI
attr(res, "Vx") <- Vx
attr(res, "Vd0") <- Vd0
attr(res, "alphamu") <- alphamu
attr(res, "alphalambda") <- alphalambda
attr(res, "gammamu") <- gammamu
attr(res, "gammalambda") <- gammalambda
attr(res, "rnames") <- rnames
class(res) <- c("htest", "spatialdelta")
res
}
summary.spatialdelta <- function(object, ...) {
# Tab. 2
n <- length(attr(object, "regional_weights"))
Kx <- attr(object, "Kx")
# features kernels
lambdabar <- sum(diag(Kx))/(n - 1)
nu <- sum(diag(Kx))^2 / sum(diag(Kx %*% Kx)) # Eq. 42
kappa <- attr(object, "Vx") # Eq. 38
alphalambda <- attr(object, "alphalambda")
gammalambda <- attr(object, "gammalambda")
# spatial weights kernels
mubar <- unname(object$estimate["Expectation"]) # Eq. 36
varI <- attr(object, "VI") # Eq. 38
alphamu <- attr(object, "alphamu")
gammamu <- attr(object, "gammamu")
res <- list(htest=object, lambdabar=lambdabar, nu=nu, kappa=kappa,
alphalambda=alphalambda, gammalambda=gammalambda, mubar=mubar,
varI=varI, alphamu=alphamu, gammamu=gammamu)
class(res) <- c("summary.spatialdelta", class(object))
res
}
print.summary.spatialdelta <- function(x, digits=getOption("digits"), ...) {
# Tab. 2
htest <- x[[1]]
print(htest, digits=digits)
dn <- unlist(strsplit(htest$data.name, " \nweights: "))
cat("\nFeatures kernels: ", dn[1], "\n")
print(unlist(x[2:6]), digits=digits)
cat("\nSpatial weights kernels: ", dn[2], "\n")
print(unlist(x[7:10]), digits=digits)
invisible(x)
}
linearised_diffusive_weights <- function(adjacency_matrix, regional_weights, t_choice=2) {
stopifnot(all(is.finite(regional_weights)))
stopifnot(all(regional_weights > 0))
if (sum(regional_weights) != 1) {
regional_weights <- regional_weights/sum(regional_weights)
warning("regional_weights changed to sum to unity")
}
n <- length(regional_weights)
stopifnot(nrow(adjacency_matrix) == n)
stopifnot(ncol(adjacency_matrix) == n)
rnames <- rownames(adjacency_matrix)
if (is.null(rnames)) rnames <- as.character(1:length(regional_weights))
stopifnot(length(rnames) == length(regional_weights))
rS <- rowSums(adjacency_matrix)
n <- length(rS)
LA <- (diag(rS) - adjacency_matrix) # eq. 7
dsf <- diag(1/sqrt(regional_weights))
lA <- dsf %*% LA %*% dsf # eq. 7
if (t_choice == 2) t <- 1/max(eigen(lA)$values) # below eq. 21
else if (t_choice == 1) t <- min(regional_weights/rS) # below eq. 8
else stop("t_choice 1 or 2")
res <- diag(n) - t*(diag(1/regional_weights) %*%
(diag(rS) - adjacency_matrix)) # eq. 8
rownames(res) <- rnames
colnames(res) <- rnames
attr(res, "t") <- t
attr(res, "regional_weights") <- regional_weights
class(res) <- c("adjusted_spatial_weights", class(res))
res
}
metropolis_hastings_weights <- function(adjacency_matrix, regional_weights) {
stopifnot(all(is.finite(regional_weights)))
stopifnot(all(regional_weights > 0))
if (sum(regional_weights) != 1) {
regional_weights <- regional_weights/sum(regional_weights)
warning("regional_weights changed to sum to unity")
}
n <- length(regional_weights)
stopifnot(nrow(adjacency_matrix) == n)
stopifnot(ncol(adjacency_matrix) == n)
rnames <- rownames(adjacency_matrix)
if (is.null(rnames)) rnames <- as.character(1:length(regional_weights))
stopifnot(length(rnames) == length(regional_weights))
rS <- rowSums(adjacency_matrix)
P <- diag(1/rS) %*% adjacency_matrix
fP <- diag(regional_weights) %*% P
G <- pmin(fP, t(fP))
E <- diag(regional_weights - rowSums(G)) + G
res <- diag(1/regional_weights) %*% E
rownames(res) <- rnames
colnames(res) <- rnames
attr(res, "regional_weights") <- regional_weights
class(res) <- c("adjusted_spatial_weights", class(res))
res
}
iterative_proportional_fitting_weights <- function(adjacency_matrix, regional_weights, g=0.001, iter=1000, tol=1e-10, tol.margins=1e-10, print=FALSE) {
stopifnot(all(is.finite(regional_weights)))
stopifnot(all(regional_weights > 0))
if (sum(regional_weights) != 1) {
regional_weights <- regional_weights/sum(regional_weights)
warning("regional_weights changed to sum to unity")
}
n <- length(regional_weights)
stopifnot(nrow(adjacency_matrix) == n)
stopifnot(ncol(adjacency_matrix) == n)
if (!requireNamespace("mipfp", quietly=TRUE)) {
warning("The mipfp package is required for this method,\n returning Metropolis-Hastings weights")
return(metropolis_hastings_weights(adjacency_matrix, regional_weights))
}
rnames <- rownames(adjacency_matrix)
if (is.null(rnames)) rnames <- as.character(1:length(regional_weights))
stopifnot(length(rnames) == length(regional_weights))
res0 <- mipfp::Ipfp(seed=(adjacency_matrix+g), target.list=list(1, 2),
target.data=list(regional_weights, regional_weights), iter=iter,
tol=tol, tol.margins=tol.margins, print=print)
res <- diag(1/regional_weights) %*% res0$x.hat
rownames(res) <- rnames
colnames(res) <- rnames
attr(res, "g") <- g
attr(res, "regional_weights") <- regional_weights
class(res) <- c("adjusted_spatial_weights", class(res))
res
}
graph_distance_weights <- function(adjacency_matrix, regional_weights, c=NULL) {
stopifnot(all(is.finite(regional_weights)))
stopifnot(all(regional_weights > 0))
if (sum(regional_weights) != 1) {
regional_weights <- regional_weights/sum(regional_weights)
warning("regional_weights changed to sum to unity")
}
n <- length(regional_weights)
stopifnot(nrow(adjacency_matrix) == n)
stopifnot(ncol(adjacency_matrix) == n)
if (!requireNamespace("igraph", quietly=TRUE)) {
warning("The igraph package is required for this method,\n returning Metropolis-Hastings weights")
return(metropolis_hastings_weights(adjacency_matrix, regional_weights))
}
rnames <- rownames(adjacency_matrix)
if (is.null(rnames)) rnames <- as.character(1:length(regional_weights))
stopifnot(length(rnames) == length(regional_weights))
D <- igraph::distances(igraph::graph_from_adjacency_matrix(adjacency_matrix))
H <- diag(n) - rep(1, times=n) %*% t(regional_weights) # above eq. 15
B <- -0.5 * (H %*% D %*% t(H)) # eq. 15
c1 <- -1/min(c(B))
if (is.null(c)) c <- c1
else {
if (c <= 0 || c > c1) stop("c out of range")
}
res <- (1 + (c*B)) %*% diag(regional_weights) # eq. 33
rownames(res) <- rnames
colnames(res) <- rnames
attr(res, "regional_weights") <- regional_weights
class(res) <- c("adjusted_spatial_weights", class(res))
res
}
as.matrix.adjusted_spatial_weights <- function(x, ...) {
class(x) <- c("matrix", "array")
x
}
cornish_fisher <- function(x, ...) {
UseMethod("cornish_fisher")
}
cornish_fisher.default <- function(x, ...) {
stop("x not a spatialdelta object")
}
cornish_fisher.spatialdelta <- function(x, ...) {
s <- unname(x$estimate[4])
k <- unname(x$estimate[5])
s2 <- s^2
k8 <- k/8
dom <- s2/9 - 4*(k8-(s2/6))*(1-k8-((5*s2)/36))
# amedee-manesmeetal:19 p. 446, eq. 24
if (dom > 0) {
warning("domain exceeded: object returned unaltered")
return(x)
}
z <- unname(x$statistic)
res0 <- (s/6)*((z^2) - 1)
res1 <- (k/24)*((z^3) - 3*z)
res2 <- ((s^2)/36)*(2*(z^3) - 5*z)
res <- res0 + res1 - res2 # eq. 45
# amedee-manesmeetal:19 p. 427, eq. 4
# dasgupta:08 p. 193
alternative <- x$alternative
std_d <- ifelse(z <= 0, z + res, z - res)
if (alternative == "two.sided")
pv_d <- 2 * pnorm(abs(std_d), lower.tail=FALSE)
else if (alternative == "greater")
pv_d <- pnorm(std_d, lower.tail=FALSE)
else pv_d <- pnorm(std_d)
x$statistic <- std_d
x$p.value <- pv_d
names(x$statistic) <- paste0(names(x$statistic),
" (Cornish-Fisher corrected)")
x$method <- "Bavaud delta under the Cornish-Fisher correction"
x
}
plot_spatialcoords <- function(x, ...) {
UseMethod("plot_spatialcoords")
}
plot_spatialcoords.default <- function(x, ...) {
stop("x not a spatialdelta object")
}
plot_spatialcoords.spatialdelta <- function(x, cols=c(1L, 2L), mult=c(1, 1),
power=1L, fmult=NULL, names=attr(x, "rnames"), bg=1, pos=3, cex=0.6,
largest=NULL, ...) {
# Fig. 1
cols <- as.integer(cols)
stopifnot(length(cols) == 2L)
stopifnot(length(mult) == 2L)
regional_weights <- attr(x, "regional_weights")
if (is.null(names)) names <- as.character(1:length(regional_weights))
stopifnot(length(names) == length(regional_weights))
dsf1 <- diag(1/sqrt(regional_weights))
Kw <- attr(x, "Kw")
# Fig. 4
power <- as.integer(power)
stopifnot(is.integer(power))
stopifnot(power > 0)
if (power > 1L) {
res <- Kw
for (k in 2:power) res <- res %*% Kw
Kw <- res
}
Kw_eig <- eigen(Kw) # eq. 23
X_hat <- dsf1 %*% Kw_eig$vectors %*% diag(sqrt(abs(Kw_eig$values))) # eq. 24
stopifnot(all(cols <= ncol(X_hat)))
X <- mult[1]*X_hat[, cols[1]]
Y <- mult[2]*X_hat[, cols[2]]
plot(X, Y, type="n", xlab=paste0("spatial coordinate ", cols[1]),
ylab=paste0("spatial coordinate ", cols[2]), ...)
if (is.null(fmult)) {
rX <- diff(range(X))
rf <- diff(range(regional_weights))
fmult <- (0.02*rX)/rf
}
mf <- regional_weights*fmult
if (!is.null(largest)) {
stopifnot(is.integer(largest))
stopifnot(length(largest) == 1L)
stopifnot(largest > 0L)
stopifnot(largest <= length(mf))
o <- order(mf, decreasing=TRUE)
names1 <- character(length(names))
for (i in seq(along=names1))
names1[i] <- ifelse(o[i] <= largest, names[i], "")
names <- names1
}
symbols(X, Y, circles=mf, bg=bg, add=TRUE, inches=FALSE)
text(X, Y, names, pos=pos, cex=cex)
abline(h=0, v=0, lty=2)
invisible(list(X=X, Y=Y, circles=mf, names=names))
}
plot_moran <- function(x, y, ...) {
UseMethod("plot_moran")
}
plot_moran.default <- function(x, y, ...) {
stop("x not a spatialdelta object")
}
plot_moran.spatialdelta <- function(x, y, fmult=NULL, names=attr(x, "rnames"),
bg=1, pos=3, cex=0.6, largest=NULL, ...) {
# Fig. 3 (+ fig. 2)
regional_weights <- attr(x, "regional_weights")
if (is.null(names)) names <- as.character(1:length(regional_weights))
stopifnot(length(names) == length(regional_weights))
stopifnot(is.numeric(y))
stopifnot(length(y) == length(regional_weights))
lw <- mat2listw(attr(x, "adjusted_spatial_weights"), style="B", row.names=names)
ly <- lag.listw(lw, y)
a <- lm(ly ~ y)
yn <- deparse(substitute(y))
plot(y, ly, type="n", xlab=paste0(yn, " (",
formatC(coef(a)[2], format="f", digits=4), ")"),
ylab=paste0("lagged ", yn), ...)
if (is.null(fmult)) {
rX <- diff(range(y))
rf <- diff(range(regional_weights))
fmult <- (0.02*rX)/rf
}
mf <- regional_weights*fmult
if (!is.null(largest)) {
stopifnot(is.integer(largest))
stopifnot(length(largest) == 1L)
stopifnot(largest > 0L)
stopifnot(largest <= length(mf))
o <- order(mf, decreasing=TRUE)
names1 <- character(length(names))
for (i in seq(along=names1))
names1[i] <- ifelse(o[i] <= largest, names[i], "")
names <- names1
}
symbols(y, ly, circles=mf, bg=bg, add=TRUE, inches=FALSE)
text(y, ly, names, pos=pos, cex=cex)
abline(a=a)
invisible(list(y=y, ly=ly, circles=mf, names=names))
}
factorial_coordinates <- function(x) {
UseMethod("factorial_coordinates")
}
factorial_coordinates.default <- function(x) {
stop("x not a spatialdelta object")
}
factorial_coordinates.spatialdelta <- function(x) {
# Fig. 2
stopifnot(inherits(x, "spatialdelta"))
dsf1 <- diag(1/sqrt(attr(x, "regional_weights")))
Kx_eig <- eigen(attr(x, "Kx")) # eq. 18
dsf1 %*% Kx_eig$vectors %*% diag(sqrt(abs(Kx_eig$values))) # eq. 19
}
localdelta <- function(x, ...) {
UseMethod("localdelta")
}
localdelta.default <- function(x, ...) {
stop("x not a spatialdelta object")
}
localdelta.spatialdelta <- function(x, names=attr(x, "rnames"), ...) {
stopifnot(inherits(x, "spatialdelta"))
Kx_eig <- eigen(attr(x, "Kx")) # eq. 18
di <- (1/sum(Kx_eig$values))*diag(attr(x, "adjusted_spatial_weights") %*% attr(x, "B")) # eq. 30
if (is.null(names)) names <- as.character(1:length(di))
stopifnot(length(names) == length(di))
names(di) <- names
di
}
plot_factorialcoords <- function(x, ...) {
UseMethod("plot_factorialcoords")
}
plot_factorialcoords.default <- function(x, ...) {
stop("x not a spatialdelta object")
}
plot_factorialcoords.spatialdelta <- function(x, cols=c(1L, 2L),
mult=c(1, 1), fmult=NULL, names=attr(x, "rnames"), bg=1, pos=3, cex=0.6,
largest=NULL, ...) {
# Fig. 5 left, not right
cols <- as.integer(cols)
stopifnot(length(cols) == 2L)
stopifnot(length(mult) == 2L)
regional_weights <- attr(x, "regional_weights")
if (is.null(names)) names <- as.character(1:length(regional_weights))
stopifnot(length(names) == length(regional_weights))
x_tilde <- factorial_coordinates(x)
stopifnot(all(cols <= ncol(x_tilde)))
X <- mult[1]*x_tilde[, cols[1]]
Y <- mult[2]*x_tilde[, cols[2]]
plot(X, Y, type="n", xlab=paste0("factorial coordinate ", cols[1]),
ylab=paste0("factorial coordinate ", cols[2]), ...)
if (is.null(fmult)) {
rX <- diff(range(Re(X)))
rf <- diff(range(regional_weights))
fmult <- (0.02*rX)/rf
}
mf <- regional_weights*fmult
if (!is.null(largest)) {
stopifnot(is.integer(largest))
stopifnot(length(largest) == 1L)
stopifnot(largest > 0L)
stopifnot(largest <= length(mf))
o <- order(mf, decreasing=TRUE)
names1 <- character(length(names))
for (i in seq(along=names1))
names1[i] <- ifelse(o[i] <= largest, names[i], "")
names <- names1
}
symbols(X, Y, circles=mf, bg=bg, add=TRUE, inches=FALSE)
text(X, Y, names, pos=pos, cex=cex)
abline(h=0, v=0, lty=2)
invisible(list(X=X, Y=Y, circles=mf, names=names))
}
plot_spatialscree <- function(x, ...) {
UseMethod("plot_spatialscree")
}
plot_spatialscree.default <- function(x, ...) {
stop("x not a spatialdelta object")
}
plot_spatialscree.spatialdelta <- function(x, lwd=2, ...) {
# Fig. 8
e <- sort(Re(eigen(attr(x, "adjusted_spatial_weights"), only.values=TRUE)$values),
decreasing=TRUE)
plot(e, type="n", ylab="Eigenvalues", xlab="", ...)
zero <- rep(0, length(e))
n <- 1:length(e)
segments(n, e, n, zero, lwd=lwd)
invisible(e)
}
plot_factorialscree <- function(x, ...) {
UseMethod("plot_factorialscree")
}
plot_factorialscree.default <- function(x, ...) {
stop("x not a spatialdelta object")
}
plot_factorialscree.spatialdelta <- function(x, ...) {
# Fig. 7
e <- sort(Re(eigen(attr(x, "Kx"), only.values=TRUE)$values),
decreasing=TRUE)
plot(e, type="n", ylab="Eigenvalues", xlab="", ...)
zero <- rep(0, length(e))
n <- 1:length(e)
segments(n, e, n, zero, lwd=2)
invisible(e)
}
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.