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+ }
0 commit comments