Typed Transforms and Graphs
Brian Muchmore
2026-08-13
Source:vignettes/Typed-Transforms-and-Graphs.Rmd
Typed-Transforms-and-Graphs.RmdThis 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)
cells <- c(1:5, 60:64, 118:122)
cell_cycle <- as.matrix(data_cell_cycle[cells, 2:51])
rownames(cell_cycle) <- paste0(data_cell_cycle$Cell_cycle[cells], "_", cells)
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 similarityDistance 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:
-
The other direction is available and just as explicit.
to = "similarity"turns a distancedinto1 / (1 + d)and leaves a native similarity alone. A correlation or an affinity keeps its numbers and is relabelled, because that is the role it was already playing. This is a practical change of representation, not a claim that conversion is lossless:precise_transform(d, to = "similarity")[, c("distance", "type")] #> # A tibble: 2 × 2 #> distance type #> <chr> <chr> #> 1 euclidean similarity #> 2 cosine similarity -
Coercion never guesses. If a metric has no defensible distance conversion,
precise_transform()errors and names the offenders rather than fabricating a number. Askpd_convertible_to_distance()first if you want to plan the request:pd_convertible_to_distance("cosine", "similarity", "inner_product") #> [1] TRUE pd_convertible_to_distance("tsne_5", "affinity", "tsne") #> [1] FALSE
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 five graph constructors:
g <- precise_graph(
d,
methods = c("knn", "weighted_knn", "mutual_knn", "threshold",
"quantile_threshold"),
params = list(
knn = list(k = 3), # each node keeps its k strongest links (union)
weighted_knn = list(k = 3), # same edges, but keep the edge strengths
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: 10 × 3
#> distance graph_method type
#> <chr> <chr> <chr>
#> 1 euclidean__knn knn similarity
#> 2 euclidean__weighted_knn weighted_knn similarity
#> 3 euclidean__mutual_knn mutual_knn similarity
#> 4 euclidean__threshold threshold similarity
#> 5 euclidean__quantile_threshold quantile_threshold similarity
#> 6 cosine__knn knn similarity
#> 7 cosine__weighted_knn weighted_knn similarity
#> 8 cosine__mutual_knn mutual_knn similarity
#> 9 cosine__threshold threshold similarity
#> 10 cosine__quantile_threshold quantile_threshold similarityThe 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. Every row reports its
undirected edge count and connected-component count in
meta, which is a quick read on how much a constructor kept
and how fragmented the result is.
data.frame(
graph = g$distance,
edges = vapply(g$meta, function(x) x$edges, numeric(1)),
components = vapply(g$meta, function(x) x$components, numeric(1))
)
#> graph edges components
#> 1 euclidean__knn 32 1
#> 2 euclidean__weighted_knn 32 1
#> 3 euclidean__mutual_knn 13 5
#> 4 euclidean__threshold 0 15
#> 5 euclidean__quantile_threshold 21 4
#> 6 cosine__knn 34 1
#> 7 cosine__weighted_knn 34 1
#> 8 cosine__mutual_knn 11 4
#> 9 cosine__threshold 2 13
#> 10 cosine__quantile_threshold 21 3There are also three normalizations for similarity or adjacency
input. laplacian returns the normalized adjacency
D^(-1/2) W D^(-1/2), chua returns a
neighbourhood-overlap weight based on the nonzero support of the graph,
and graphical_lasso estimates a sparse precision matrix
with the optional glasso backend and keeps its nonzero
off-diagonal entries as edges. 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] 32
#>
#> $components
#> [1] 1
#>
#> $normalization
#> [1] "D^(-1/2) W D^(-1/2)"
#>
#> $zero_degree_nodes
#> [1] 0