## ----------------------------------------------------------------------------- #| label: setup #| message: false #| include: false library(dplyr) library(sf) library(tmap) library(pycnogrid) ## ----------------------------------------------------------------------------- #| label: fig-sample_population_count #| fig-cap: "Sample population count data for Lower Manhattan" #| warning: false #| echo: false tm_shape(nyc_ct_small) + tm_fill( fill = "populationE", fill.scale = tm_scale_intervals( values = "viridis", style = "jenks", n = 6, label.format = tm_label_format(big.num.abbr = c("K" = 3)) ), fill_alpha = .75, fill.legend = tm_legend(title = "Population Count", frame = FALSE) ) + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout( frame = FALSE, bg.color = "grey90" ) ## ----------------------------------------------------------------------------- #| label: tbl-grids #| tbl-cap: "Supported target grid systems and their principal geometric and hierarchical properties" #| echo: false tibble::tribble( ~grid_system, ~cell_geometry, ~area_property, ~extent_and_hierarchy, ~subdivision_or_aperture, ~neighbour_topology, ~analytical_implication, "H3", "Mostly hexagons, with twelve pentagons", "Approximately equal-area", "Global discrete grid with a nested hierarchy", "Aperture-7 hierarchy with alternating cell orientation across resolutions", "Usually six edge-neighbours; pentagons have five", "Strong indexing and neighbourhood structure, particularly useful for mobility, accessibility, and spatial aggregation", "ISEA3H, ISEA4H", # ISEA7H", "Mostly hexagons, with twelve pentagons on an icosahedral projection", "Equal-area", "Global discrete grid with a nested hierarchy", "Aperture-3 and Aperture-4 hierarchies, #, and Aperture-7 hierarchies", "Usually six edge-neighbours; pentagons have five", "Equal-area hexagonal option with relatively fine-grained hierarchical scaling between resolutions", #"ISEA4H", #"Mostly hexagons, with twelve pentagons on an icosahedral projection", #"Equal-area", #"Global discrete grid with a nested hierarchy", #"Aperture-4 hierarchy", #"Usually six edge-neighbours; pentagons have five", #"Equal-area hexagonal option with a different resolution progression and nesting structure from aperture-3 systems", #"ISEA7H", #"Mostly hexagons, with twelve pentagons on an icosahedral projection", #"Equal-area", #"Global discrete grid with a nested hierarchy", #"Aperture-7 hierarchy", #"Usually six edge-neighbours; pentagons have five", #"Equal-area hexagonal option with resolution scaling broadly comparable to H3 but without H3's specific indexing system", #"ISEA4/3H", #"Mostly hexagons, with twelve pentagons on an icosahedral projection", #"Equal-area", #"Global discrete grid with a nested hierarchy", #"Mixed aperture-4 and aperture-3 hierarchy", #"Usually six edge-neighbours; pentagons have five", #"Allows a mixed resolution progression where the scale change between successive levels follows the selected aperture sequence", "A5", "Equal-area pentagonal cells", "Equal-area", "Global discrete grid with a hierarchical structure", "Five-way initial refinement, followed by four-way refinement", "Predominantly five edge-neighbours", "Provides a global equal-area alternative with strong indexing, although its pentagonal geometry may produce more directional variation than hexagonal grids", "S2", "Quadrilateral cells projected from the faces of a cube", "Not equal-area", "Global discrete grid with a nested hierarchy", "Quadtree subdivision: each cell has four children", "Variable topology across cube-face boundaries", "Strong global indexing and web-mapping infrastructure, but cell areas vary substantially across locations", "Local Raster", "Rectangular cells in a projected CRS", "Equal-area only when constructed in an appropriate projected CRS", "Usually regional or local; hierarchy is not intrinsic", "Resolution specified directly in map units", "Four- or eight-neighbour structure, depending on rook or queen contiguity", "Simple and familiar benchmark, but geometric properties depend on the selected projection and resolution", "Local Hex", "Regular hexagonal cells in a projected CRS", "Equal-area only when constructed in an appropriate projected CRS", "Usually regional or local; hierarchy is not intrinsic", "Resolution specified directly in map units", "Usually six edge-neighbours in a complete tessellation", "Local hexagonal support with six symmetric first-order neighbours, but geometric properties depend on the selected projection and resolution" ) |> gt::gt() |> gt::cols_label( grid_system = "Grid System", cell_geometry = "Cell Geometry", area_property = "Area Property", extent_and_hierarchy = "Extent and Hierarchy", subdivision_or_aperture = "Subdivision / Aperture", neighbour_topology = "Neighbour Topology", analytical_implication = "Analytical Implication" ) |> gt::tab_options( table.width = gt::pct(100), table.font.size = 12 ) ## ----------------------------------------------------------------------------- #| label: fig-sample_dggs_grids #| fig-cap: "Sample DGGS hierarchies at different resolutions" #| fig-height: 6 #| fig-width: 9 #| warning: false #| echo: false sample_dggs_grids <- bind_rows( h3_grids <- purrr::map_dfr( list(8, 9, 10), ~ pycnogrid:::prepare_target_cells( source_polys = nyc_ct_small, grid_type = "h3", resolution = .x, cell_inclusion = "intersect" ) |> mutate(grid_type = "h3", cell_resolution = .x, zorder = cell_resolution) ), isea3h_grids <- purrr::map_dfr( list(17, 18, 19), ~ pycnogrid:::prepare_target_cells( source_polys = nyc_ct_small, grid_type = "isea3h", resolution = .x, cell_inclusion = "intersect" ) |> mutate(grid_type = "isea3h", cell_resolution = .x, zorder = cell_resolution) ), isea4h_grids <- purrr::map_dfr( list(13, 14, 15), ~ pycnogrid:::prepare_target_cells( source_polys = nyc_ct_small, grid_type = "isea4h", resolution = .x, cell_inclusion = "intersect" ) |> mutate(grid_type = "isea4h", cell_resolution = .x, zorder = cell_resolution) ), a5_grids <- purrr::map_dfr( list(13, 14, 15), ~ pycnogrid:::prepare_target_cells( source_polys = nyc_ct_small, grid_type = "a5", resolution = .x, cell_inclusion = "intersect" ) |> mutate(grid_type = "a5", cell_resolution = .x, zorder = cell_resolution) ), s2_grids <- purrr::map_dfr( list(14, 15, 16), ~ pycnogrid:::prepare_target_cells( source_polys = nyc_ct_small, grid_type = "s2", resolution = .x, cell_inclusion = "intersect" ) |> mutate(grid_type = "s2", cell_resolution = .x, zorder = cell_resolution) ) ) |> mutate(grid_type = forcats::fct_relevel(grid_type, "h3", "isea3h", "isea4h", "a5", "s2")) |> arrange(desc(zorder)) tm_shape(sample_dggs_grids) + tm_lines( col = "cell_resolution", col.scale = tm_scale_categorical(), col.free = TRUE, col.legend = tm_legend(title = "resolution", frame = FALSE) ) + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_facets(by = "grid_type", ncol = 3, free.coords = FALSE) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout( panel.label.frame = FALSE, panel.label.bg = FALSE, frame = FALSE, bg.color = "grey90" ) ## ----------------------------------------------------------------------------- #| label: fig-sample_local_grids #| fig-cap: "Sample local grid hierarchies at different resolutions" #| fig-height: 3 #| fig-width: 9 #| message: false #| echo: false sample_local_grids <- dplyr::bind_rows( raster_grids <- purrr::map_dfr( list(1000, 500, 250), ~ pycnogrid:::prepare_target_cells( source_polys = nyc_ct_small, grid_type = "raster", resolution = .x, cell_inclusion = "intersect" ) |> mutate( grid_type = "raster", cell_resolution = .x, zorder = 1 / cell_resolution ) ), hex_grids <- purrr::map_dfr( list(1000, 500, 250), ~ pycnogrid:::prepare_target_cells( source_polys = nyc_ct_small, grid_type = "hex", resolution = .x, cell_inclusion = "intersect" ) |> mutate( grid_type = "hex", cell_resolution = .x, zorder = 1 / cell_resolution ) ) ) |> mutate(grid_type = forcats::fct_relevel(grid_type, "raster", "hex")) |> arrange(desc(zorder)) tm_shape(sample_local_grids) + tm_lines( col = "cell_resolution", col.scale = tm_scale_categorical(), col.free = TRUE, col.legend = tm_legend(title = "resolution", frame = FALSE) ) + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_facets_wrap(by = "grid_type", #ncol = 2, free.coords = FALSE) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout( panel.label.frame = FALSE, panel.label.bg = FALSE, frame = FALSE, bg.color = "grey90" ) ## ----------------------------------------------------------------------------- #| label: run to_grid pycno_nyc_ct_small <- nyc_ct_small |> pycnogrid::to_grid( value_col = populationE, grid_type = "h3", resolution = 10 ) ## ----------------------------------------------------------------------------- #| label: fig-pycno_nyc_ct_small #| fig-cap: "Census tract population counts interpolated to an H3 grid" #| echo: false tm_shape(pycno_nyc_ct_small) + tm_fill( fill = "pycno_populationE", fill.scale = tm_scale_intervals( values = "viridis", style = "jenks", n = 6, label.format = tm_label_format(big.num.abbr = c("K" = 3)) ), fill_alpha = .75, fill.legend = tm_legend(title = "Interpolated \nPopulation", frame = FALSE) ) + tm_shape(nyc_ct_small) + tm_lines(col = "grey45") + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout( frame = FALSE, bg.color = "grey90" ) ## ----------------------------------------------------------------------------- #| label: fig-pycno_nyc_ct_small_resolutions #| fig-cap: "Census tract population counts interpolated to all four grid types" #| fig-height: 8 #| fig-width: 10 #| echo: false grid_list <- list("h3", "isea3h", "isea4h", "a5", "s2", "raster", "hex") resolution_list <- list(10, 20, 16, 16, 16, 100, 100) pycno_nyc_ct_small_resolutions <- purrr::map2_dfr( grid_list, resolution_list, ~ to_grid( source = nyc_ct_small, value_col = "populationE", grid_type = .x, resolution = .y ) |> mutate(grid_type = .x, resolution = .y, facet_label = paste0(grid_type, " at resolution ", resolution)) ) tm_shape(pycno_nyc_ct_small_resolutions |> filter(grid_type != "h3" & grid_type != "isea4h" & grid_type != "hex")) + tm_fill( fill = "pycno_populationE", fill.scale = tm_scale_intervals( values = "viridis", style = "jenks", n = 6, label.format = tm_label_format(big.num.abbr = c("K" = 3)) ), fill_alpha = .75, fill.legend = tm_legend(title = "Interpolated \nPopulation", frame = FALSE), fill.free = TRUE ) + tm_facets(by = "facet_label", ncol = 2, free.coords = TRUE) + tm_shape(nyc_ct_small) + tm_lines(col = "grey45") + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout(panel.label.frame = FALSE, panel.label.bg = FALSE, frame = FALSE, bg.color = "grey90") ## ----------------------------------------------------------------------------- #| label: tbl-grid_descriptives #| tbl-cap: "Population descriptive statistics for different grid types" #| echo: false pycno_nyc_ct_small_resolutions |> st_drop_geometry() |> group_by(grid_type) |> summarize( resolution = min(resolution), cell_count = n(), mean_pop = mean(pycno_populationE), sum_pop = sum(pycno_populationE) ) |> ungroup() |> mutate(grid_type = forcats::fct_relevel(grid_type, "h3", "isea3h", "isea4h", "a5", "s2", "raster", "hex")) |> arrange(grid_type) |> gt::gt() |> gt::cols_label( grid_type = "grid type", resolution = "grid resolution", cell_count = "grid cell count", mean_pop = "mean cell population", sum_pop = "total population" ) ## ----------------------------------------------------------------------------- #| label: fig-pycno_nyc_ct_small_combinations #| fig-cap: "Interpolated population counts with varying inclusion and allocation parameters" #| fig-height: 8 #| fig-width: 8 #| echo: false combinations <- tidyr::expand_grid( inclusion_list = list("intersect", "centroid"), allocation_list = list("area", "centroid") ) pycno_nyc_ct_small_combinations <- purrr::map2_dfr( combinations |> purrr::pluck("inclusion_list"), combinations |> purrr::pluck("allocation_list"), ~ to_grid( source = nyc_ct_small, value_col = "populationE", id_col = id, grid_type = "h3", resolution = 10, cell_inclusion = .x, cell_allocation = .y, nb_order = 1 ) |> mutate( inclusion = paste0("inclusion: ", .x), allocation = paste0("allocation: ", .y) ) ) |> mutate(inclusion = forcats::fct_relevel(inclusion, "inclusion: intersect", "inclusion: centroid")) tm_shape(pycno_nyc_ct_small_combinations) + tm_fill( fill = "pycno_populationE", fill.scale = tm_scale_intervals( values = "viridis", style = "jenks", n = 6, label.format = tm_label_format(big.num.abbr = c("K" = 3)) ), fill_alpha = .75, fill.legend = tm_legend(title = "Interpolated \nPopulation", frame = FALSE) ) + tm_facets_grid(rows = "inclusion", columns = "allocation", free.coords = FALSE) + tm_shape(nyc_ct_small) + tm_lines(col = "grey45") + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout(panel.label.frame = FALSE, panel.label.bg = FALSE, frame = FALSE, bg.color = "grey90") ## ----------------------------------------------------------------------------- #| label: fig-pycno_nyc_ct_small_nb_order #| fig-cap: "Interpolated population counts with increasing neighbourhood order" #| fig-height: 8 #| fig-width: 8 #| echo: false neighbours_list <- seq(1, 4, by = 1) pycno_nyc_ct_small_neighbourss <- purrr::map_dfr( neighbours_list, ~ to_grid( source = nyc_ct_small, value_col = "populationE", grid_type = "h3", resolution = 10, nb_order = .x ) |> mutate(nb_order = .x, facet_label = paste0("nb_order = ", nb_order)) ) tm_shape(pycno_nyc_ct_small_neighbourss) + tm_fill( fill = "pycno_populationE", fill.scale = tm_scale_intervals( values = "viridis", style = "jenks", n = 6, label.format = tm_label_format(big.num.abbr = c("K" = 3)) ), fill_alpha = .75, fill.legend = tm_legend(title = "Interpolated \nPopulation", frame = FALSE), fill.free = FALSE ) + tm_facets(by = "facet_label", ncol = 2, free.coords = TRUE) + tm_shape(nyc_ct_small) + tm_lines(col = "grey45") + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout(panel.label.frame = FALSE, panel.label.bg = FALSE, frame = FALSE, bg.color = "grey90") ## ----------------------------------------------------------------------------- #| label: fig-nyc_ct_subset #| fig-cap: "Source zone subset and target cells with centroid-based inclusion criteria" #| echo: false subset_ids <- c( "36061005800", "36061008700", "36061009100" ) nyc_ct_subset <- nyc_ct |> filter(id %in% subset_ids) |> mutate(source_id = paste0("S", row_number())) prepped_source <- pycnogrid:::prep_source(source = nyc_ct_subset, value_col = "populationE", id_col = "id") source_polys <- prepped_source |> purrr::pluck("source") |> mutate(source_label_id = paste0("S", .sid)) source_values <- prepped_source |> purrr::pluck("source_values") input_total_original <- prepped_source |> purrr::pluck("input_total_original") input_total_represented <- prepped_source |> purrr::pluck("input_total_represented") target_cells <- pycnogrid:::prepare_target_cells( source_polys, grid_type = "h3", resolution = 9, cell_inclusion = "centroid" #cell_inclusion = "intersect" ) |> mutate(target_label_id = paste0("T", .tid)) tm_shape(source_polys) + tm_fill(col = "grey35", col_alpha = .8, fill = "grey", fill_alpha = .1) + tm_text("source_label_id", ymod = -1, angle = 330) + tm_shape(target_cells) + tm_fill(col = "blue", fill = "blue", fill_alpha = .1) + tm_text("target_label_id", col = "blue") + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout(panel.label.frame = FALSE, panel.label.bg = FALSE, frame = FALSE, bg.color = "grey90") ## ----------------------------------------------------------------------------- #| echo: false #| warning: false # manually: inter_raw <- sf::st_intersection(source_polys, target_cells) |> dplyr::mutate(.weight = as.numeric(sf::st_area(geometry))) |> sf::st_drop_geometry() |> dplyr::filter(.weight > 0) |> dplyr::select(source_id, .tid, .weight) represented_sources <- inter_raw |> dplyr::distinct(source_id) source_index <- source_values |> dplyr::mutate(.sid = dplyr::row_number()) |> dplyr::select(source_id, .sid) inter <- inter_raw |> # semi join to drop any target cells that don't have any sources dplyr::semi_join(source_values, by = "source_id") |> # attach the numeric source index .sid. dplyr::left_join(source_index, by = "source_id") |> # Sum weights in case the same source-cell pair appears more than once, # which can happen after geometric operations involving multipart polygons # or multiple intersection fragments. dplyr::group_by(.sid, .tid, source_id) |> dplyr::summarise(.weight = sum(.weight, na.rm = TRUE), .groups = "drop") n_target <- nrow(target_cells) n_source <- nrow(source_values) A_matrix <- Matrix::sparseMatrix( i = inter$.sid, j = inter$.tid, x = inter$.weight, dims = c(n_source, n_target) ) # row and column normalize the A_matrix overlap matrix # need some sparse matrix trickery here for row and column sums row_sums <- Matrix::rowSums(A_matrix) W_matrix <- Matrix::Diagonal(x = 1 / pmax(row_sums, .Machine$double.eps), n = n_source) %*% A_matrix col_sums <- as.numeric(Matrix::colSums(A_matrix)) V_matrix <- A_matrix %*% Matrix::Diagonal(x = 1 / pmax(col_sums, .Machine$double.eps), n = n_target) # cross-reference with package function: source_cell_weights <- pycnogrid:::prepare_source_cell_weights( source_polys, source_values, target_cells, grid_type = "h3", resolution = 9, cell_allocation = "area", missing_policy = "warn", input_total_original ) #A_matrix == source_cell_weights$A_matrix #W_matrix == source_cell_weights$W_matrix #V_matrix == source_cell_weights$V_matrix ## ----------------------------------------------------------------------------- #| echo: false y <- source_index |> dplyr::left_join(source_values, by = "source_id") |> dplyr::arrange(.sid) |> dplyr::pull(source_value) # Calculate initial areal allocation x <- as.numeric(Matrix::t(W_matrix) %*% y) ## ----------------------------------------------------------------------------- #| echo: false density <- x / col_sums ## ----------------------------------------------------------------------------- #| echo: false S_matrix <- pycnogrid:::build_smoothing_matrix( target_cells = target_cells, nb_order = 1, include_self = TRUE ) ## ----------------------------------------------------------------------------- #| echo: false d_smooth <- as.numeric(S_matrix %*% density) ## ----------------------------------------------------------------------------- #| echo: false source_est <- as.numeric(A_matrix %*% density) ## ----------------------------------------------------------------------------- #| echo: false r_ratio <- y / source_est ## ----------------------------------------------------------------------------- #| echo: false q_correction <- as.numeric(Matrix::t(V_matrix) %*% r_ratio) ## ----------------------------------------------------------------------------- #| echo: false density_t_plus_one <- density * q_correction ## ----------------------------------------------------------------------------- #| echo: false abs_change <- abs(density_t_plus_one - density) denom <- pmax(abs(density), 1e-12) last_error <- mean(abs_change / denom, na.rm = TRUE) ## ----------------------------------------------------------------------------- #| label: fig-pycno_nyc_ct_subset #| fig-cap: "Source zone and interpolated population values" #| fig-width: 8 #| fig-height: 4 #| echo: false subset_results <- dplyr::bind_rows(nyc_ct_subset |> to_grid( value_col = "populationE", id_col = id, grid_type = "h3", resolution = 9, #grid_type = "raster", #resolution = 275, cell_inclusion = "centroid", cell_allocation = "area", nb_order = 1 ) |> mutate( label_id = paste0("T", .tid), population = pycno_populationE, type = "target cells" ), source_polys |> mutate( population = .source_value, label_id = paste0("S", .sid), type = "source zones") ) subset_overlay <- dplyr::bind_rows( source_polys |> mutate( type = "target cells"), target_cells |> mutate(type = "source zones") ) tm_shape(subset_results) + tm_fill( fill = "population", fill.scale = tm_scale_ordinal( values = "viridis", label.format = tm_label_format(big.num.abbr = c("K" = 3)) ), fill_alpha = .75, fill.legend = tm_legend(title = "Population", frame = FALSE, position = tm_pos_in()), fill.free = TRUE ) + #tm_text("label_id") + tm_facets(by = "type", free.coords = FALSE) + tm_shape(subset_overlay) + tm_lines(col = "grey35", col_alpha = .8) + tm_facets(by = "type", free.coords = FALSE) + #tm_basemap("CartoDB.PositronNoLabels", alpha = .5) + tm_shape(nyc_ct) + tm_fill(fill = "grey97", zindex = 3) + tm_layout(panel.label.frame = FALSE, panel.label.bg = FALSE, frame = FALSE, bg.color = "grey90")