Skip to contents

This 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  20

Build 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.71

Fuse 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.016

Now 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.