Function Factory and Parameter Sweeps
Brian Muchmore
2026-07-14
Source:vignettes/articles/Function-Factory-Parameter-Sweeps.Rmd
Function-Factory-Parameter-Sweeps.RmdThis vignette assumes you have already read the main example workflow. Here the question is narrower: what if the representation we want to inspect is not a single named metric, but a family of related metrics created by varying one parameter?
precise_func_fact() builds those metric functions for
us. Instead of asking precise_dist() for one registered
metric name, we create a list of related functions and pass that list
through dist_funcs.
Setup
Use the same balanced subset as the main workflow so the visual comparisons are familiar.
set.seed(1)
label_col <- data_cell_cycle$Cell_cycle
rows_by_label <- split(seq_len(nrow(data_cell_cycle)), label_col)
workflow_rows <- unlist(lapply(rows_by_label, head, 20), use.names = FALSE)
workflow_features <- seq_len(500)
cell_cycle <- as.matrix(data_cell_cycle[workflow_rows, -1, drop = FALSE][, workflow_features])
storage.mode(cell_cycle) <- "double"
cell_cycle_labels <- label_col[workflow_rows]
rownames(cell_cycle) <- sprintf("%s_%02d", cell_cycle_labels, seq_along(cell_cycle_labels))
cell_cycle_groups <- setNames(cell_cycle_labels, rownames(cell_cycle))
cell_cycle_palette <- c(G1 = "#0017FF", G2M = "#000000", S = "#FF0017")
dim(cell_cycle)
#> [1] 60 500
table(cell_cycle_labels)
#> cell_cycle_labels
#> G1 G2M S
#> 20 20 20Build a Parameterized Metric Family
Random forest relatedness accepts mtry: the number of
variables randomly sampled as candidates at each split.
precise_func_fact() lets us create a small family of random
forest distances that differ only in that parameter.
For a full local analysis, you can use more parameter values and the future backend:
cell_cycle_rf_params <- round(seq(2, 21, length.out = 20))
cell_cycle_rf_funcs <- precise_func_fact(
func = "random_forest",
params = cell_cycle_rf_params
)
library(future)
library(doFuture)
doFuture::registerDoFuture()
future::plan(
future::multisession,
workers = min(10, future::availableCores())
)
cell_cycle_rf_dists <- precise_dist(
cell_cycle,
dist_funcs = cell_cycle_rf_funcs,
suffix = "cell_rf_",
file = NULL,
parallel = TRUE,
max_time_per_metric = Inf,
verbose = TRUE
)
future::plan(future::sequential)For the website build, use five values. That is enough to show the mechanics while keeping the article responsive.
set.seed(1)
cell_cycle_rf_params <- round(seq(2, 21, length.out = 5))
cell_cycle_rf_funcs <- precise_func_fact(
func = "random_forest",
params = cell_cycle_rf_params
)
str(cell_cycle_rf_funcs[1:5])
#> List of 5
#> $ random_forest_2 :function (data)
#> ..- attr(*, "srcref")= 'srcref' int [1:8] 132 19 144 7 19 7 3669 3681
#> .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55907259f930>
#> ..- attr(*, "pd_type")= chr "distance"
#> $ random_forest_7 :function (data)
#> ..- attr(*, "srcref")= 'srcref' int [1:8] 132 19 144 7 19 7 3669 3681
#> .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55907259f930>
#> ..- attr(*, "pd_type")= chr "distance"
#> $ random_forest_12:function (data)
#> ..- attr(*, "srcref")= 'srcref' int [1:8] 132 19 144 7 19 7 3669 3681
#> .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55907259f930>
#> ..- attr(*, "pd_type")= chr "distance"
#> $ random_forest_16:function (data)
#> ..- attr(*, "srcref")= 'srcref' int [1:8] 132 19 144 7 19 7 3669 3681
#> .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55907259f930>
#> ..- attr(*, "pd_type")= chr "distance"
#> $ random_forest_21:function (data)
#> ..- attr(*, "srcref")= 'srcref' int [1:8] 132 19 144 7 19 7 3669 3681
#> .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55907259f930>
#> ..- attr(*, "pd_type")= chr "distance"Functions created by precise_func_fact() carry type
metadata. The random forest factory returns 1 - proximity,
so these rows enter the workflow as typed distances rather than unknown
custom matrices.
cell_cycle_rf_dists <- precise_dist(
cell_cycle,
dist_funcs = cell_cycle_rf_funcs,
suffix = "cell_rf_",
parallel = FALSE,
max_time_per_metric = Inf,
verbose = FALSE
)
cell_cycle_rf_dists[, c("distance", "metric", "type", "time_taken_seconds")]
#> # A tibble: 5 × 4
#> distance metric type time_taken_seconds
#> <chr> <chr> <chr> <dbl>
#> 1 cell_rf_random_forest_2 NA distance 6.10
#> 2 cell_rf_random_forest_7 NA distance 5.84
#> 3 cell_rf_random_forest_12 NA distance 5.77
#> 4 cell_rf_random_forest_16 NA distance 5.75
#> 5 cell_rf_random_forest_21 NA distance 5.71Fuse the Dense Distances
First, fuse the random forest distances directly. DiSTATIS is a
useful choice here because random forest distances often respond well to
compromise geometry. Use DiSTATIS when the optional backend is
installed; otherwise fall back to the base mean reducer so
the vignette remains executable.
cell_cycle_rf_fusion_method <- if (requireNamespace("DistatisR", quietly = TRUE)) {
"distatis"
} else {
"mean"
}
cell_cycle_rf_fused <- precise_fusion(
cell_cycle_rf_dists,
methods = cell_cycle_rf_fusion_method,
verbose = FALSE
)
cell_cycle_rf_fused[, c("method", "type", "time_taken_seconds")]
#> # A tibble: 1 × 3
#> method type time_taken_seconds
#> <chr> <chr> <dbl>
#> 1 distatis distance 0.016Now inspect the fused distance as an embedding.
cell_cycle_rf_fused_embedding <- precise_viz(
cell_cycle_rf_fused,
views = "embedding",
params = list(
embedding = list(
method = "umap",
n_neighbors = 15,
min_dist = 0.01,
spread = 1,
engine = workflow_plot_engine,
seed = 42,
color = cell_cycle_groups,
colors = cell_cycle_palette,
show_labels = FALSE
)
),
verbose = FALSE
)
cell_cycle_rf_fused_embedding$panel[[1]]Graph Before Fusion
Direct fusion is not the visual endpoint. If individual random forest distances look better separately than after fusion, one possibility is that too many weak relationships are being carried into the consensus. The graph tier lets us test that directly: keep only the strongest local relationships for each random forest distance, then fuse the graph matrices.
precise_dist() -> precise_transform() -> precise_graph() -> precise_fusion()
Here precise_transform() is a no-op for type because the
factory already returned distances, but keeping it in the code makes the
cutoff explicit.
cell_cycle_rf_transformed <- precise_transform(
cell_cycle_rf_dists,
to = "distance"
)
cell_cycle_rf_graphs <- precise_graph(
cell_cycle_rf_transformed,
methods = "knn",
params = list(
knn = list(k = 5)
),
parallel = FALSE,
verbose = FALSE
)
cell_cycle_rf_graph_fused <- precise_fusion(
cell_cycle_rf_graphs,
methods = "mean",
verbose = FALSE
)
cell_cycle_rf_graph_fused[, c("method", "type", "inputs")]
#> # A tibble: 1 × 3
#> method type inputs
#> <chr> <chr> <list>
#> 1 mean similarity <chr [5]>Now visualize the fused graph in 3-D.
cell_cycle_rf_graph_viz <- precise_viz(
cell_cycle_rf_graph_fused,
views = "graph_layout",
params = list(
graph_layout = list(
layout = "fr",
dimensions = 3,
engine = "threejs",
render = "graph",
seed = 1,
color = cell_cycle_groups,
colors = cell_cycle_palette,
show_labels = FALSE,
size = 1
)
),
verbose = FALSE
)
cell_cycle_rf_graph_viz$panel[[1]]By fusing retained local structure rather than full dense distance matrices, the graph representation can become cleaner and easier to inspect. That does not make random forest universally better, and it does not make graph-before-fusion always better. It means the workflow lets us ask that question directly.