## ----setup, include=FALSE----------------------------------------------------- # ggplot2 is a Suggests package, and every chunk below either draws something # or feeds something that does. R CMD build runs this file, so the whole # vignette is gated on ggplot2 being installed rather than calling library() # unguarded and failing the build on a machine without it. has_ggplot <- requireNamespace("ggplot2", quietly = TRUE) has_geom <- requireNamespace("geometry", quietly = TRUE) has_ranger <- requireNamespace("ranger", quietly = TRUE) has_gstat <- requireNamespace("gstat", quietly = TRUE) has_gwmodel <- requireNamespace("GWmodel", quietly = TRUE) && requireNamespace("sp", quietly = TRUE) knitr::opts_chunk$set( echo = TRUE, message = FALSE, warning = FALSE, fig.align = "center", out.width = "100%", eval = has_ggplot ) ## ----no-ggplot, echo=FALSE, eval=!has_ggplot, results='asis'------------------ # cat("**ggplot2 is not installed, so the code below is shown but not run.**", # "Install it with `install.packages(\"ggplot2\")` and rebuild.\n") ## ----backend-report, echo=FALSE----------------------------------------------- data.frame( package = c("ggplot2", "geometry", "ranger", "gstat", "GWmodel + sp"), available = c(has_ggplot, has_geom, has_ranger, has_gstat, has_gwmodel), used_for = c("all plots", "true Delaunay triangulation", "random forest backend", "variogram / autocorrelation range", "GWR backend") ) ## ----load-packages------------------------------------------------------------ library(spatialkit) library(sf) library(dplyr) library(ggplot2) set.seed(42) ## ----boundary----------------------------------------------------------------- nc_counties <- st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) nc_boundary <- nc_counties |> st_union() |> st_transform(2264) |> st_as_sf() ## ----fake-data---------------------------------------------------------------- n_points <- 300 pts_raw <- st_sample(nc_boundary, size = n_points, type = "random") pts_coords <- st_coordinates(pts_raw) x_coords <- pts_coords[, 1] y_coords <- pts_coords[, 2] elevation <- scale(x_coords)[, 1] * 500 + rnorm(n_points, 3000, 400) city1 <- c(1530000, 550000) # Charlotte-ish in EPSG:2264 city2 <- c(2150000, 750000) # Raleigh-ish dist_to_city <- pmin( sqrt((x_coords - city1[1])^2 + (y_coords - city1[2])^2), sqrt((x_coords - city2[1])^2 + (y_coords - city2[2])^2) ) pop_density <- pmax(exp(-dist_to_city / 400000) * 5000 + rnorm(n_points, 200, 100), 10) spatial_field <- 20 * sin(x_coords / 250000) * cos(y_coords / 250000) y_response <- 50 + 0.01 * elevation + 0.005 * pop_density + spatial_field + rnorm(n_points, 0, 5) points_sf <- st_sf( y = y_response, elevation = elevation, pop_density = pop_density, geometry = pts_raw ) ## ----quick-peek, fig.height=3.6----------------------------------------------- ggplot() + geom_sf(data = nc_boundary, fill = "grey95", colour = "black") + geom_sf(data = points_sf, aes(colour = y), size = 1.1) + scale_colour_viridis_c(name = "Response (y)") + theme_void() + ggtitle("Raw observation points, North Carolina") ## ----tess-voronoi------------------------------------------------------------- seeds <- get_voronoi_seeds( boundary = nc_boundary, sample_points = points_sf, method = "kmeans", n = 40 ) tess_voronoi <- build_tessellation( seeds, boundary = nc_boundary, method = "voronoi", clip = TRUE, quiet = TRUE ) ## ----tess-grids--------------------------------------------------------------- tess_hex <- build_tessellation( points_sf, boundary = nc_boundary, method = "hex", approx_n_cells = 50, clip = TRUE, quiet = TRUE ) tess_square <- build_tessellation( points_sf, boundary = nc_boundary, method = "square", approx_n_cells = 50, clip = TRUE, quiet = TRUE ) ## ----cell-id-check------------------------------------------------------------ names(tess_hex$cells) ## ----tess-tri, eval=has_ggplot && has_geom------------------------------------ tess_tri <- build_tessellation( points_sf, boundary = nc_boundary, method = "triangles", clip = TRUE, quiet = TRUE ) nrow(tess_tri$cells) ## ----cell-counts-------------------------------------------------------------- cat(sprintf( "Voronoi: %d | Hex: %d | Square: %d | Triangles: %s\n", nrow(tess_voronoi$cells), nrow(tess_hex$cells), nrow(tess_square$cells), if (has_geom) nrow(tess_tri$cells) else "skipped (install 'geometry')" )) ## ----summarize---------------------------------------------------------------- assigned <- assign_features_to_polygons(points_sf, tess_voronoi$cells, polygon_id_col = "cell_id") cell_stats <- summarize_by_cell( assigned, response_var = "y", predictor_vars = c("elevation", "pop_density"), id_col = "cell_id" ) head(as.data.frame(cell_stats)[, c("cell_id", "n", "resp_mean_y", "..sd_resp_y", "..se_resp_y")]) ## ----summarize-deff----------------------------------------------------------- cell_kish <- summarize_by_cell( assigned, response_var = "y", predictor_vars = c("elevation", "pop_density"), id_col = "cell_id", deff = "kish" ) deff <- attr(cell_kish, "deff_applied") cat(sprintf("method = %s | ICC(response) = %.3f | median design effect = %.2f\n", deff$method, deff$icc_resp, stats::median(deff$deff, na.rm = TRUE))) # Inflation of the response standard error, cell by cell. summary(cell_kish$`..se_resp_y` / cell_stats$`..se_resp_y`) ## ----choropleth-helper-------------------------------------------------------- #' Assign points to cells, summarise, and draw a choropleth. make_choropleth <- function(tess, boundary, points, title = NULL) { cells <- tess$cells assigned <- assign_features_to_polygons(points, cells, polygon_id_col = "cell_id") stats_df <- summarize_by_cell(assigned, response_var = "y", id_col = "cell_id") cells <- left_join(cells, as.data.frame(stats_df)[, c("cell_id", "resp_mean_y")], by = "cell_id") plot_tessellation_map( tessellation_sf = cells, boundary = boundary, fill_col = "resp_mean_y", palette = "viridis", tile_alpha = 0.9, outline_col = "white", outline_size = 0.3, boundary_col = "grey20", boundary_size = 0.8, legend_title = "Mean y", title = title, subtitle = sprintf("%d cells | %d observations", nrow(cells), nrow(points)) ) } ## ----choro-voronoi, fig.cap="Voronoi choropleth"------------------------------ make_choropleth(tess_voronoi, nc_boundary, points_sf, "Voronoi tessellation") ## ----choro-hex, fig.cap="Hex grid choropleth"--------------------------------- make_choropleth(tess_hex, nc_boundary, points_sf, "Hexagonal grid") ## ----choro-square, fig.cap="Square grid choropleth"--------------------------- make_choropleth(tess_square, nc_boundary, points_sf, "Square grid") ## ----choro-tri, fig.cap="Delaunay choropleth", eval=has_ggplot && has_geom---- make_choropleth(tess_tri, nc_boundary, points_sf, "Delaunay triangulation") ## ----comparison-panel, fig.width=7, fig.height=2.8, fig.cap="All three at a glance", eval=has_ggplot && requireNamespace("patchwork", quietly = TRUE)---- library(patchwork) (make_choropleth(tess_voronoi, nc_boundary, points_sf, "Voronoi") | make_choropleth(tess_hex, nc_boundary, points_sf, "Hex") | make_choropleth(tess_square, nc_boundary, points_sf, "Square")) + plot_annotation(title = "Tessellation comparison, cell-level mean response") ## ----sac-range, eval=has_ggplot && has_gstat---------------------------------- sac <- estimate_sac_range(points_sf, response_var = "y", predictor_vars = c("elevation", "pop_density")) sac if (is.na(sac)) { cat("no identified range:", attr(sac, "rejected_reason"), "\n") } else { cat(sprintf("range = %.0f ft; anisotropy ratio %.2f; used %s\n", as.numeric(sac), attr(sac, "anisotropy"), attr(sac, "anisotropy_used"))) } ## ----folds-------------------------------------------------------------------- folds_random <- make_folds(points_sf, k = 5, method = "random_kfold", seed = 42) folds_blocked <- make_folds(points_sf, k = 5, method = "block_kfold", seed = 42) c(random = folds_random$k, blocked = folds_blocked$k) ## ----plot-folds, fig.width=7, fig.height=3, eval=has_ggplot && requireNamespace("patchwork", quietly = TRUE)---- library(patchwork) plot_folds(folds_random, points_sf, boundary = nc_boundary) + plot_folds(folds_blocked, points_sf, boundary = nc_boundary) ## ----cv-contrast, eval=has_ggplot && has_ranger------------------------------- rf_args <- list(include_coords = TRUE, num_trees = 300, seed = 1) cv_random <- do.call(cv_rf, c(list(points_sf, "y", c("elevation", "pop_density"), folds = folds_random), rf_args)) cv_blocked <- do.call(cv_rf, c(list(points_sf, "y", c("elevation", "pop_density"), folds = folds_blocked), rf_args)) data.frame( folds = c("random_kfold", "block_kfold"), R2 = c(cv_random$overall$R2, cv_blocked$overall$R2), RMSE = c(cv_random$overall$RMSE, cv_blocked$overall$RMSE) ) ## ----cv-contrast-text, echo=FALSE, results='asis', eval=has_ggplot && has_ranger---- cat(sprintf( "Random folds report R2 = %.3f; blocked folds report %.3f on the same fitted model — a drop of %.0f%% of the reported skill.\n", cv_random$overall$R2, cv_blocked$overall$R2, 100 * (cv_random$overall$R2 - cv_blocked$overall$R2) / cv_random$overall$R2)) ## ----rf-fit, eval=has_ggplot && has_ranger------------------------------------ rf_fit <- fit_rf_model(points_sf, response_var = "y", predictor_vars = c("elevation", "pop_density")) rf_fit ## ----rf-summary, eval=has_ggplot && has_ranger-------------------------------- summary(rf_fit) rf_fit$info$importance # coef() on an rf_fit errors: a forest has no coefficients ## ----rf-resid, fig.height=3.6, eval=has_ggplot && has_ranger------------------ plot(rf_fit, type = "residuals") ## ----rf-moran, eval=has_ggplot && has_ranger---------------------------------- mi <- residual_morans_i(rf_fit) cat(sprintf("Moran's I = %.4f (z = %.2f, p = %.3g)\n", mi$observed, mi$z, mi$p_value)) ## ----gwr-fit, eval=has_ggplot && has_gwmodel---------------------------------- gwr_fit <- fit_gwr_model( data_sf = points_sf, response_var = "y", predictor_vars = c("elevation", "pop_density"), adaptive = TRUE, kernel = "bisquare" ) gwr_met <- model_metrics(gwr_fit) cat(sprintf("Bandwidth: %.1f | in-sample R2: %.3f | RMSE: %.3f\n", gwr_fit$info$bandwidth, gwr_met$R2, gwr_met$RMSE)) ## ----compare, eval=has_ggplot && has_ranger----------------------------------- cmp <- compare_models_cv( points_sf, "y", c("elevation", "pop_density"), models = c("RF", "GWR"), folds = folds_blocked, rf_args = list(num_trees = 300) ) cmp$overall ## ----surface, fig.height=3.6, eval=has_ggplot && has_ranger------------------- surf <- predict_surface(rf_fit, n_cells = 3000, covariates = points_sf, boundary = nc_boundary) ggplot() + geom_sf(data = surf, aes(colour = .pred), size = 0.6) + geom_sf(data = nc_boundary, fill = NA, colour = "grey20") + scale_colour_viridis_c(name = "Predicted y") + theme_void() + ggtitle("Predicted surface") ## ----aoa, fig.height=3.6, eval=has_ggplot && has_ranger----------------------- aoa <- area_of_applicability(surf, model = rf_fit, folds = folds_blocked) cat(sprintf("inside: %d | outside: %d | undetermined: %d | DI threshold %.3f\n", aoa$n_inside, aoa$n_outside, aoa$n_na, aoa$threshold)) # AOA is NA wherever a predictor was missing or non-finite, and `!NA` is NA, # which R silently skips in a subscripted assignment. Test for TRUE and let # anything else count as outside. inside <- aoa$aoa$AOA %in% TRUE surf$.pred_masked <- ifelse(inside, surf$.pred, NA_real_) ggplot() + geom_sf(data = surf, aes(colour = .pred_masked), size = 0.6) + geom_sf(data = nc_boundary, fill = NA, colour = "grey20") + scale_colour_viridis_c(name = "Predicted y", na.value = "grey85") + theme_void() + ggtitle("Predicted surface, extrapolations blanked out") ## ----aoa-outside, eval=has_ggplot && has_ranger------------------------------- extreme <- st_sf( elevation = c(mean(points_sf$elevation), max(points_sf$elevation) * 4), pop_density = c(mean(points_sf$pop_density), max(points_sf$pop_density) * 4), geometry = st_geometry(points_sf)[1:2] ) area_of_applicability(extreme, model = rf_fit, folds = folds_blocked)$aoa[, c("DI", "AOA")] ## ----session-info, echo=FALSE, eval=TRUE-------------------------------------- sessionInfo()