--- title: Choose parameters for your data name: choose-parameters description: Learn how parcel count and spatial weighting change the estimand. output: rmarkdown::html_vignette: toc: yes toc_depth: 2 css: albers.css includes: in_header: albers-header.html params: family: red base_size: 13 content_width: 72 preset: homage resource_files: - albers.css - albers.js - albers-header.html - fonts vignette: | %\VignetteIndexEntry{Choose parameters for your data} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} if (requireNamespace("albersdown", quietly = TRUE)) albersdown::albers_register_fonts() knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.align = "center", fig.retina = 2, out.width = "100%", fig.width = 8.5, fig.asp = 0.56, message = FALSE, warning = FALSE ) options(width = 88) suppressPackageStartupMessages({ library(neurocluster) library(neuroim2) }) parcel_colors <- c("#0072B2", "#E69F00", "#009E73", "#CC79A7", "#56B4E9", "#D55E00", "#F0E442", "#000000") align_labels <- function(labels, truth) { ids <- sort(unique(labels)) truth_ids <- sort(unique(truth)) stopifnot(length(ids) == length(truth_ids)) overlap <- table(factor(labels, levels = ids), factor(truth, levels = truth_ids)) map <- truth_ids[as.integer(clue::solve_LSAP(overlap, maximum = TRUE))] aligned <- unname(map[match(labels, ids)]) stopifnot(length(unique(aligned)) == length(ids)) aligned } draw_slice <- function(labels, dims, z, title) { slice <- array(labels, dims)[, , z] image(t(slice[, ncol(slice):1]), col = parcel_colors, axes = FALSE, useRaster = TRUE, asp = 1, main = title) box(col = "#4A4A4A") } ``` ```{r albers-classes, echo=FALSE, results='asis'} cat(sprintf(paste0( '' ), params$family, params$preset, params$content_width)) ``` # Which decisions come first? {#decisions} Start with the scale of the scientific question, not with a search for the largest validation score. `n_clusters` sets the target granularity. The `spatial_weight` parameter sets how strongly feature similarity is traded against spatial proximity, but its exact mapping is method-specific. Both alter the partition you are estimating. A sensible tuning exercise therefore holds the dataset and method fixed, changes one quantity at a time, and records several named diagnostics. The example below uses G3S and a known synthetic partition so that parameter effects are visible. ```{r tuning-data} syn <- generate_synthetic_volume( scenario = "gaussian_blobs", dims = c(16, 16, 8), n_clusters = 4, n_time = 40, noise_sd = 0.08, seed = 42 ) ``` # What does K change? {#choose-k} K is a target parcel count, not a quality score. Fewer parcels make coarser summaries; more parcels make finer summaries and reduce observations per parcel. Some methods may return a different final count, so always retain `actual_k`. ```{r fit-k-grid} k_grid <- c(3L, 4L, 6L) k_fits <- lapply(k_grid, function(k) { cluster4d( syn$vec, syn$mask, k, method = "g3s", spatial_weight = 0.5, max_iterations = 5, connectivity = 26, parallel = FALSE ) }) ``` ```{r summarize-k-grid, echo=FALSE} k_metrics <- lapply(k_fits, cluster_metrics, truth = syn$truth) k_summary <- data.frame( Requested_K = k_grid, Actual_K = vapply(k_fits, `[[`, integer(1), "actual_k"), ARI_to_known = vapply(k_metrics, `[[`, numeric(1), "ari_truth"), Spatial_RMS_mm = vapply(k_metrics, `[[`, numeric(1), "spatial_rms_distance_mm"), Minimum_size = vapply(k_metrics, function(x) x$size_summary[["min"]], numeric(1)) ) knitr::kable( transform(k_summary, ARI_to_known = round(ARI_to_known, 3), Spatial_RMS_mm = round(Spatial_RMS_mm, 2)), caption = "Observed G3S diagnostics while changing only the target K." ) ``` ```{r check-k-grid, include=FALSE} k_checks <- lapply(k_fits, validate_cluster4d, vec = syn$vec, mask = syn$mask) stopifnot( all(vapply(k_checks, `[[`, logical(1), "valid")), identical(k_summary$Actual_K, k_grid), all(k_summary$Minimum_size > 0), all(diff(k_summary$Spatial_RMS_mm) < 0) ) ``` ```{r k-tradeoff, echo=FALSE, fig.cap='Changing target K changes both granularity and observed diagnostics on this fixture. The known partition has four regions, but the highest ARI here occurs at K = 3; matching a nominal count does not guarantee the best partition.', fig.alt='Two line charts over K values 3, 4, and 6. Adjusted Rand index is highest at 3 and declines, while spatial RMS distance declines as K increases.'} old_par <- par(mfrow = c(1, 2), mar = c(4.5, 4.5, 2.5, 1)) plot(k_summary$Requested_K, k_summary$ARI_to_known, type = "b", pch = 16, col = "#0072B2", lwd = 2, ylim = c(0, 1), bty = "l", xlab = "Target K", ylab = "ARI to known partition") abline(v = 4, lty = 3, col = "#777777") plot(k_summary$Requested_K, k_summary$Spatial_RMS_mm, type = "b", pch = 17, col = "#D55E00", lwd = 2, bty = "l", xlab = "Target K", ylab = "Spatial RMS distance (mm)") abline(v = 4, lty = 3, col = "#777777") par(old_par) ``` # What does spatial weight change? {#spatial-weight} For G3S, larger `spatial_weight` puts more emphasis on spatial proximity and less on feature similarity. That usually encourages compact parcels, but the effect and useful range depend on the data and method. Evaluate it rather than assuming that more compact is automatically better. ```{r fit-weight-grid} weight_grid <- c(0.2, 0.5, 0.8) weight_fits <- lapply(weight_grid, function(weight) { cluster4d( syn$vec, syn$mask, 4, method = "g3s", spatial_weight = weight, max_iterations = 5, connectivity = 26, parallel = FALSE ) }) ``` ```{r summarize-weight-grid, echo=FALSE} weight_metrics <- lapply(weight_fits, cluster_metrics, truth = syn$truth) weight_summary <- data.frame( Spatial_weight = weight_grid, ARI_to_known = vapply(weight_metrics, `[[`, numeric(1), "ari_truth"), Spatial_RMS_mm = vapply(weight_metrics, `[[`, numeric(1), "spatial_rms_distance_mm") ) knitr::kable( transform(weight_summary, ARI_to_known = round(ARI_to_known, 3), Spatial_RMS_mm = round(Spatial_RMS_mm, 2)), caption = "Observed G3S diagnostics while changing only spatial weight." ) ``` ```{r check-weight-grid, include=FALSE} weight_checks <- lapply(weight_fits, validate_cluster4d, vec = syn$vec, mask = syn$mask) stopifnot( all(vapply(weight_checks, `[[`, logical(1), "valid")), all(vapply(weight_fits, `[[`, integer(1), "actual_k") == 4L), all(diff(weight_summary$Spatial_RMS_mm) < 0), all(weight_summary$ARI_to_known >= -1 & weight_summary$ARI_to_known <= 1) ) ``` ```{r weight-slices, echo=FALSE, fig.cap='Middle axial slices as G3S spatial weight increases. A one-to-one maximum-overlap assignment preserves a distinct color for every predicted parcel while aligning arbitrary IDs to the known synthetic colors. The rightmost partition is physically tighter but agrees less with the known feature-defined regions.', fig.alt='Three axial grid panels for spatial weights 0.2, 0.5, and 0.8. Each panel retains four distinct parcel colors; boundaries become more regular and compact from left to right.'} old_par <- par(mfrow = c(1, 3), mar = c(1, 1, 3, 1)) for (i in seq_along(weight_fits)) { draw_slice(align_labels(weight_fits[[i]]$labels, syn$truth), syn$dims, 4, paste("Spatial weight", weight_grid[i])) } par(old_par) ``` On this fixture, increasing spatial weight reduces RMS distance from `r sprintf("%.2f", weight_summary$Spatial_RMS_mm[1])` mm to `r sprintf("%.2f", weight_summary$Spatial_RMS_mm[3])` mm, while agreement with the known partition falls. The executable checks establish that observation for this example only. # How should you tune real data? {#real-data} 1. Choose a scientifically meaningful range of parcel counts before examining outcomes. 2. Hold preprocessing, mask, method, and geometry fixed while varying one parameter. 3. Record `actual_k`, parcel-size distributions, spatial dispersion, temporal coherence, and stability under perturbation. 4. Reject invalid or degenerate results before ranking diagnostics. 5. Report the full selection rule, not only the chosen setting. Continue with [Validate and compare](validate-compare.html) for the exact validation and comparison contracts.