Skip to content

Commit d69d97a

Browse files
authored
Feature: GWPCA (#37)
* feature: gwpca interface * add: glyph.plot for gwpca loadings, add scale option
1 parent 39bdf09 commit d69d97a

File tree

12 files changed

+492
-2
lines changed

12 files changed

+492
-2
lines changed

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ S3method(fitted,gtdrm)
88
S3method(fitted,gwlcrm)
99
S3method(fitted,gwrm)
1010
S3method(fitted,gwrmultiscalem)
11+
S3method(glyph.plot,gwpcam)
1112
S3method(plot,gtdrm)
1213
S3method(plot,gwcorrm)
1314
S3method(plot,gwlcrm)
@@ -19,6 +20,7 @@ S3method(print,gtdrm)
1920
S3method(print,gwavgm)
2021
S3method(print,gwcorrm)
2122
S3method(print,gwlcrm)
23+
S3method(print,gwpcam)
2224
S3method(print,gwrm)
2325
S3method(print,gwrmultiscalem)
2426
S3method(residuals,gtdrm)
@@ -28,11 +30,13 @@ S3method(residuals,gwrmultiscalem)
2830
S3method(step,default)
2931
S3method(step,gtdrm)
3032
S3method(step,gwrm)
33+
export(glyph.plot)
3134
export(gtdr)
3235
export(gtdr_config)
3336
export(gwaverage)
3437
export(gwcorr_config)
3538
export(gwcorrelation)
39+
export(gwpca)
3640
export(gwr_basic)
3741
export(gwr_lcr)
3842
export(gwr_multiscale)

R/RcppExports.R

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ gw_correlation_cal <- function(x1, x2, coords, bw, adaptive, kernel, longlat, p,
1313
.Call(`_GWmodel3_gw_correlation_cal`, x1, x2, coords, bw, adaptive, kernel, longlat, p, theta, initial_type, optim_bw_criterion, parallel_type, parallel_arg, variable_names, verbose)
1414
}
1515

16+
gwpca_cal <- function(x, coords, bw, adaptive, kernel, longlat, p, theta, keep_components) {
17+
.Call(`_GWmodel3_gwpca_cal`, x, coords, bw, adaptive, kernel, longlat, p, theta, keep_components)
18+
}
19+
1620
gwr_basic_fit <- function(x, y, coords, bw, adaptive, kernel, longlat, p, theta, optim_bw_lower, optim_bw_upper, hatmatrix, intercept, parallel_type, parallel_arg, optim_bw, optim_bw_criterion, select_model, select_model_criterion, select_model_threshold, variable_names, verbose) {
1721
.Call(`_GWmodel3_gwr_basic_fit`, x, y, coords, bw, adaptive, kernel, longlat, p, theta, optim_bw_lower, optim_bw_upper, hatmatrix, intercept, parallel_type, parallel_arg, optim_bw, optim_bw_criterion, select_model, select_model_criterion, select_model_threshold, variable_names, verbose)
1822
}

R/gwpca.R

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#' Calibrate a GWPCA model
2+
#'
3+
#' @param formula Regresison model.
4+
#' @param data A `sf` objects.
5+
#' @param bw Should provide a value to set the size of bandwidth,
6+
#' @param adaptive Whether the bandwidth value is adaptive or not.
7+
#' @param kernel Kernel function used.
8+
#' @param longlat Whether the coordinates
9+
#' @param p Power of the Minkowski distance,
10+
#' default to 2, i.e., Euclidean distance.
11+
#' @param theta Angle in radian to roate the coordinate system, default to 0.
12+
#' @param components How many components want to keep, default to 2.
13+
#' @param scale Whether to standardised data.
14+
#'
15+
#' @return A `gwpcam` object.
16+
#'
17+
#' @examples
18+
#' data(LondonHP)
19+
#'
20+
#' # Basic usage
21+
#' gwpca(~FLOORSZ + UNEMPLOY + PROF, LondonHP, 50, TRUE, components = 2)
22+
#'
23+
#' # Standardised
24+
#' m <- gwpca(~FLOORSZ + UNEMPLOY + PROF, LondonHP, 50, TRUE, components = 2, scale = TRUE)
25+
#' # Plot local loadings
26+
#' glyph.plot(m)
27+
#' @seealso glyph.plot
28+
#'
29+
#' @importFrom stats na.action model.frame model.extract model.matrix terms
30+
#' @export
31+
gwpca <- function(
32+
formula,
33+
data,
34+
bw = NA,
35+
adaptive = FALSE,
36+
kernel = c("gaussian", "exp", "bisquare", "tricube", "boxcar"),
37+
longlat = FALSE,
38+
p = 2.0,
39+
theta = 0.0,
40+
components = 2,
41+
scale = FALSE
42+
) {
43+
### Check args
44+
kernel = match.arg(kernel)
45+
attr(data, "na.action") <- getOption("na.action")
46+
47+
### Extract coords
48+
data <- do.call(na.action(data), args = list(data))
49+
coords <- as.matrix(sf::st_coordinates(sf::st_centroid(data)))
50+
if (is.null(coords) || nrow(coords) != nrow(data))
51+
stop("Missing coordinates.")
52+
53+
### Extract variables
54+
mc <- match.call(expand.dots = FALSE)
55+
56+
mf <- model.frame(formula = formula(update(formula, ~ . - 1)), data = sf::st_drop_geometry(data))
57+
mt <- attr(mf, "terms")
58+
x <- model.matrix(mt, mf)
59+
if(scale) x <- scale(x)
60+
indep_vars <- colnames(x)
61+
62+
### Check whether bandwidth is valid.
63+
if (missing(bw)) {
64+
stop("Please input bandwidth!")
65+
}
66+
if (!(is.numeric(bw) || is.integer(bw))) {
67+
stop("Please input correct bandwidth!")
68+
}
69+
70+
components <- as.integer(floor(components))
71+
if (components < 1) {
72+
stop("Components must be an interger greater than 0 !!")
73+
}
74+
if(length(indep_vars) < components) {
75+
stop("Components to keep must be lower than variable counts!")
76+
}
77+
78+
79+
c_result <- tryCatch(gwpca_cal(
80+
x, coords,
81+
bw, adaptive, enum(kernel), longlat, p, theta,
82+
components
83+
), error = function(e) {
84+
stop("Error:", conditionMessage(e))
85+
})
86+
87+
local_loadings <- c_result$local_loadings
88+
# local_scores <- c_result$local_scores
89+
90+
local_PV <- c_result$local_PV
91+
92+
pc_names <- paste0("PC", seq_len(components), "_PV")
93+
colnames(local_PV) <- pc_names
94+
sdf_data <- as.data.frame(local_PV)
95+
sdf_data$geometry <- sf::st_geometry(data)
96+
sdf <- sf::st_sf(sdf_data)
97+
98+
### Return result
99+
gwpcam <- list(
100+
SDF = sdf,
101+
local_loadings = local_loadings,
102+
# local_scores = local_scores,
103+
args = list(
104+
x = x,
105+
coords = coords,
106+
bw = bw,
107+
adaptive = adaptive,
108+
kernel = kernel,
109+
longlat = longlat,
110+
p = p,
111+
theta = theta,
112+
keep_components = components
113+
),
114+
call = mc,
115+
indep_vars = indep_vars
116+
)
117+
class(gwpcam) <- "gwpcam"
118+
gwpcam
119+
}
120+
121+
#' Print description of a `gwpcam` object
122+
#'
123+
#' @param x An `gwpcam` object returned by [gwpca()].
124+
#' @param decimal_fmt The format string passing to [base::sprintf()].
125+
#' @inheritDotParams print_table_md
126+
#'
127+
#' @method print gwpcam
128+
#' @importFrom stats coef fivenum
129+
#' @rdname print
130+
#' @export
131+
print.gwpcam <- function(x, decimal_fmt = "%.3f", ...) {
132+
if (!inherits(x, "gwpcam")) {
133+
stop("It's not a gwpcam object.")
134+
}
135+
136+
### Basic Information
137+
cat("Geographically Weighted Principal Component Analysis", fill = T)
138+
cat("========================================", fill = T)
139+
cat(" Formula:", deparse(x$call$formula), fill = T)
140+
cat(" Data:", deparse(x$call$data), fill = T)
141+
cat(" Kernel:", x$args$kernel, fill = T)
142+
cat("Bandwidth:", x$args$bw, ifelse(x$args$adaptive, "(Nearest Neighbours)", "(Meters)"), fill = T)
143+
cat(" Keep components:", x$args$keep_components, fill = T)
144+
cat("\n", fill = T)
145+
146+
cat("Summary of Local PV", fill = T)
147+
cat("--------------------------------", fill = T)
148+
local_PV <- sf::st_drop_geometry(x$SDF)
149+
local_PV_fivenum <- t(apply(local_PV, 2, fivenum))
150+
colnames(local_PV_fivenum) <- c("Min.", "1st Qu.", "Median", "3rd Qu.", "Max.")
151+
rownames(local_PV_fivenum) <- colnames(local_PV)
152+
loadings_1st_str <- rbind(
153+
c("Local loadings", colnames(local_PV_fivenum)),
154+
cbind(rownames(local_PV_fivenum), matrix2char(local_PV_fivenum, decimal_fmt))
155+
)
156+
print_table_md(loadings_1st_str, ...)
157+
cat("\n", fill = T)
158+
159+
cat("Summary of Local Loadings in PC1", fill = T)
160+
cat("--------------------------------", fill = T)
161+
loadings_1st <- x$local_loadings[, , 1]
162+
loadings_1st_fivenum <- t(apply(loadings_1st, 2, fivenum))
163+
colnames(loadings_1st_fivenum) <- c("Min.", "1st Qu.", "Median", "3rd Qu.", "Max.")
164+
rownames(loadings_1st_fivenum) <- x$indep_vars
165+
loadings_1st_str <- rbind(
166+
c("Local loadings", colnames(loadings_1st_fivenum)),
167+
cbind(rownames(loadings_1st_fivenum), matrix2char(loadings_1st_fivenum, decimal_fmt))
168+
)
169+
print_table_md(loadings_1st_str, ...)
170+
cat("\n", fill = T)
171+
}
172+
173+
#' Glyph plot generic
174+
#' @export
175+
glyph.plot <- function(x, ...) {
176+
UseMethod("glyph.plot")
177+
}
178+
179+
#' Glyph plot for local loadings in PC1 of GWPCA model.
180+
#'
181+
#' @param x A "gwpcam" object.
182+
#' @param y Ignored.
183+
#' @param columns Column names to plot.
184+
#' If it is missing or non-character value, all coefficient columns are plotted.
185+
#' @param sign whether to plot according to the loadings is beyond 0 or not.
186+
#' @param \dots Additional arguments passed to [sf::plot()].
187+
#' @method glyph.plot gwpcam
188+
#' @export
189+
glyph.plot.gwpcam <- function(x, y, ..., columns, sign = FALSE) {
190+
if (!inherits(x, "gwpcam")) {
191+
stop("It's not a gwpcam object.")
192+
}
193+
194+
sdf <- sf::st_as_sf(x$SDF)
195+
coords <- sf::st_coordinates(sdf)
196+
r <- 0.05 * max(diff(range(coords[, 1])), diff(range(coords[, 2])))
197+
198+
ld <- x$local_loadings[, , 1]
199+
colnames(ld) <- x$indep_vars
200+
n.col <- ncol(ld)
201+
202+
colors <- rainbow(n.col)
203+
rgb_colors <- col2rgb(colors) / 255
204+
205+
rowmax <- function(z) z[cbind(1:nrow(z), max.col(abs(z)))]
206+
ld <- sweep(ld, 1, sign(rowmax(ld)), "*")
207+
208+
angles <- (0:(n.col - 1)) * 2 * pi / n.col
209+
J <- 0 + (0 + 1i)
210+
disp <- exp((pi / 2 - angles) * J) * r
211+
loc2 <- coords[, 1] + coords[, 2] * J
212+
ld.max <- max(ld)
213+
ld.scaled <- abs(ld) / ld.max
214+
215+
plot(coords, asp = 1, type = "n")
216+
points(coords, pch = 14, cex = 0.1, col = "black")
217+
218+
for (i in 1:nrow(ld)) {
219+
for (j in 1:ncol(ld)) {
220+
l.from <- loc2[i]
221+
l.to <- loc2[i] + disp[j] * ld.scaled[i, j]
222+
223+
if (sign) {
224+
alpha <- 1
225+
col <- if (ld[i, j] > 0) {
226+
rgb(1, 0, 0, alpha) # red
227+
} else {
228+
rgb(0, 0, 1, alpha) # blue
229+
}
230+
} else {
231+
col_index <- (j - 1) %% n.col + 1
232+
col <- rgb(rgb_colors[1, col_index],
233+
rgb_colors[2, col_index],
234+
rgb_colors[3, col_index],
235+
alpha = 1)
236+
}
237+
238+
lines(Re(c(l.from, l.to)), Im(c(l.from, l.to)), col = col)
239+
}
240+
}
241+
242+
par(cex = 1.3)
243+
legend_labels <- colnames(ld)
244+
245+
if (!sign) {
246+
text.w <- max(strwidth(legend_labels)) * 1.2
247+
legend("bottomleft",
248+
legend = legend_labels,
249+
col = colors,
250+
lty = 1,
251+
lwd = 2,
252+
text.width = text.w,
253+
bg = "white")
254+
} else {
255+
legend("bottomleft",
256+
legend = c("Positive loading", "Negative loading"),
257+
col = c("red", "blue"),
258+
lty = 1,
259+
lwd = 2,
260+
bg = "white")
261+
}
262+
}

man/glyph.plot.Rd

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/glyph.plot.gwpcam.Rd

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)