Skip to contents

This vignette covers the two middle tiers — precise_transform() and precise_graph() — whose job is to keep the type of a relatedness matrix explicit and honest at every step.

set.seed(1)
idx <- c(1:5, 61:65, 121:125)
cell_cycle <- as.matrix(data_cell_cycle[idx, -1])[, 1:50]
storage.mode(cell_cycle) <- "double"
rownames(cell_cycle) <- paste0(substr(data_cell_cycle$Cell_cycle[idx], 1, 2), "_", idx)

precise_transform() — explicit typed conversion

precise_dist() can return a mix of distances and similarities. Some metrics are inherently similarities:

d <- precise_dist(cell_cycle, dists = c("euclidean", "cosine"), verbose = FALSE)
d[, c("distance", "type")]
#> # A tibble: 2 × 2
#>   distance  type      
#>   <chr>     <chr>     
#> 1 euclidean distance  
#> 2 cosine    similarity

Distance is the package’s canonical comparison space, so precise_transform(to = "distance") coerces every row to a distance using a defensible per-type rule (for example a cosine similarity s becomes sqrt(2 * (1 - s))), asserting a zero diagonal as it goes:

d <- precise_transform(d, to = "distance")
unique(d$type)
#> [1] "distance"

Two things are deliberate here:

  • to = "similarity" is explicit. A distance d becomes 1 / (1 + d), while a native similarity remains unchanged. This is a practical change of representation, not a claim that conversion is lossless:

    precise_transform(d, to = "similarity")
    #> # A tibble: 2 × 6
    #>   distance  metric    matrix          type       time_taken_seconds history   
    #>   <chr>     <chr>     <list>          <chr>                   <dbl> <list>    
    #> 1 euclidean euclidean <dbl [15 × 15]> similarity           0.00300  <list [1]>
    #> 2 cosine    cosine    <dbl [15 × 15]> similarity           0.001000 <list [2]>
  • Coercion never guesses. If a metric has no defensible distance conversion, precise_transform() errors and names the offenders rather than fabricating a number. pd_convertible_to_distance() lists what can be converted.

normalize = "range01" (symmetry-preserving rescale to [0, 1]) and diagonal = (write a fixed diagonal) are optional and always run after coercion:

dn <- precise_transform(d, to = "distance", normalize = "range01")
range(dn$matrix[[1]])
#> [1] 0 1

precise_graph() — typed matrix to graph

precise_graph() projects a typed matrix into a graph-derived similarity matrix with a zero diagonal. Which edges survive is decided by the declared type — for a distance the smallest pairwise values are the strongest links; for a similarity the largest — never by inspecting the values themselves.

There are four graph constructors:

g <- precise_graph(
  d,
  methods = c("knn", "mutual_knn", "threshold", "quantile_threshold"),
  params = list(
    knn                = list(k = 3),      # each node keeps its k strongest links (union)
    mutual_knn         = list(k = 3),      # keep an edge only if mutual (intersection)
    threshold          = list(threshold = 0.5),  # keep links past a cutoff
    quantile_threshold = list(prop = 0.2)  # keep the strongest prop of all pairs
  ),
  verbose = FALSE
)
g[, c("distance", "graph_method", "type")]
#> # A tibble: 8 × 3
#>   distance                      graph_method       type      
#>   <chr>                         <chr>              <chr>     
#> 1 euclidean__knn                knn                similarity
#> 2 euclidean__mutual_knn         mutual_knn         similarity
#> 3 euclidean__threshold          threshold          similarity
#> 4 euclidean__quantile_threshold quantile_threshold similarity
#> 5 cosine__knn                   knn                similarity
#> 6 cosine__mutual_knn            mutual_knn         similarity
#> 7 cosine__threshold             threshold          similarity
#> 8 cosine__quantile_threshold    quantile_threshold similarity

The output is a typed tibble (type = "similarity" — an adjacency is a similarity), one row per input matrix per method, ready for precise_viz(views = "graph_layout"), precise_graphml(), or further fusion. mutual_knn reports the number of connected components in its meta, which is a quick read on how fragmented the graph is.

There are also graph normalizers for similarity/adjacency input. laplacian returns the normalized adjacency D^(-1/2) W D^(-1/2), and chua returns a neighbourhood-overlap normalization based on the nonzero support of the graph. These are weighted graph similarities, not new distances.

gn <- precise_graph(
  g[g$graph_method == "knn", ],
  methods = c("laplacian", "chua"),
  verbose = FALSE
)
gn[, c("distance", "graph_method", "type")]
#> # A tibble: 4 × 3
#>   distance                  graph_method type      
#>   <chr>                     <chr>        <chr>     
#> 1 euclidean__knn__laplacian laplacian    similarity
#> 2 euclidean__knn__chua      chua         similarity
#> 3 cosine__knn__laplacian    laplacian    similarity
#> 4 cosine__knn__chua         chua         similarity
gn$meta[[1]]
#> $edges
#> [1] 31
#> 
#> $components
#> [1] 1
#> 
#> $normalization
#> [1] "D^(-1/2) W D^(-1/2)"
#> 
#> $zero_degree_nodes
#> [1] 0