Create intersection k-nearest neighbor graphs with dual pruning
Source:R/iknn_graphs.R
create.iknn.graphs.RdFor each \(k \in [k_{\mathrm{min}},\,k_{\mathrm{max}}]\), builds an intersection-weighted k-NN graph and applies two pruning schemes: (1) geometric (path-to-edge ratio) and (2) intersection-size. Optionally performs PCA before graph construction.
Usage
create.iknn.graphs(
X,
k.values,
max.path.edge.ratio.deviation.thld = 0.1,
path.edge.ratio.percentile = 0.5,
threshold.percentile = 0,
compute.full = TRUE,
with.isize.pruning = FALSE,
with.edge.pruning.stats = FALSE,
pca.dim = 100,
variance.explained = 0.99,
knn.metric = c("euclidean", "linf.simplex"),
linf.tol = sqrt(.Machine$double.eps),
verbose = TRUE,
knn.cache.path = NULL,
knn.cache.mode = c("none", "read", "write", "readwrite")
)Arguments
- X
A numeric matrix (or object coercible to a numeric matrix) with rows = observations and columns = features.
- k.values
Strictly increasing integer vector of neighborhood sizes, each between 1 and n - 1.
- max.path.edge.ratio.deviation.thld
Numeric in \([0, 0.2)\). Geometric pruning removes an edge \((i,j)\) when there exists an alternative path between \(i\) and \(j\) whose path/edge length ratio minus 1.0 is less than this threshold. This is a deviation threshold \(\delta\) in \([0, 0.2)\). Internally we compare the path-to-edge ratio R to \(1 + \delta\).
- path.edge.ratio.percentile
Numeric in \([0,1]\). Only edges with length above this percentile are considered for geometric pruning.
- threshold.percentile
Numeric in \([0, 0.5]\). Percentile threshold for quantile-based edge length pruning. Default is 0 (no quantile pruning). When greater than 0, edges in the top (1 - threshold.percentile) quantile by length are removed if their removal preserves graph connectivity. For example, threshold.percentile = 0.9 removes edges in the top 10\ This pruning is applied after geometric pruning and targets unusually long edges based on absolute edge lengths rather than path-to-edge ratios.
- compute.full
Logical controlling graph-list outputs. If
TRUE, returngeom_pruned_graphs(andisize_pruned_graphswhenwith.isize.pruning = TRUE). IfFALSE, both graph lists areNULLand only summary outputs are returned (k_statistics, plusedge_pruning_statswhen requested).- with.isize.pruning
Logical. If
TRUE, compute and return intersection-size pruned graphs/statistics. Default isFALSE.- with.edge.pruning.stats
Logical. If
TRUE, compute and return edge-level pruning statistics. Default isFALSE.- pca.dim
Positive integer or
NULL. If notNULLandncol(X) > pca.dim, PCA is used to reduce to at mostpca.dimcomponents.- variance.explained
Numeric in \((0,1]\) or
NULL. If notNULL, choose the smallest number of PCs whose cumulative variance explained exceeds this threshold, capped bypca.dim.- knn.metric
Character scalar specifying the geometry used for kNN search.
"euclidean"uses ordinary ambient Euclidean distance."linf.simplex"uses the unfolded intrinsic metric on the \(L^\infty\)-simplex and requires rows ofXto be \(L^\infty\)-normalized.- linf.tol
Positive numeric tolerance used only when
knn.metric = "linf.simplex"to identify active simplex faces and validate thatmax(row) = 1.Native graph construction is serial.
- verbose
Logical; print progress and timing.
- knn.cache.path
Optional character scalar path to a binary kNN cache file. Used only when
knn.cache.mode != "none".- knn.cache.mode
Character cache mode:
"none": always compute kNN; do not read/write cache."read": read kNN from cache only; if cache is missing/invalid, error."write": always compute kNN and atomically write/overwrite cache."readwrite": read valid cache when available; if cache file is missing, compute and write cache; if cache exists but is invalid, error.
Value
A list of class "iknn_graphs" with entries:
- k_statistics
Matrix of per-\(k\) edge counts and reductions. (If the C++ side supplies column names, they’re preserved. Otherwise we add names consistent with what the C++ returns.)
- geom_pruned_graphs
If
compute.full=TRUE, list of geometrically pruned graphs (adjacency + weights); otherwiseNULL.- isize_pruned_graphs
If
compute.full=TRUEandwith.isize.pruning=TRUE, list of intersection-size pruned graphs; otherwiseNULL.- edge_pruning_stats
If
with.edge.pruning.stats=TRUE, list (per \(k\)) of edge-level statistics; otherwiseNULL.
Details
Geometric pruning uses the deviation threshold
max.path.edge.ratio.deviation.thld and the filtering percentile
path.edge.ratio.percentile. Intersection-size pruning currently uses
a maximum alternative path length of 2.
Output behavior by flags:
compute.full = FALSE: bothgeom_pruned_graphsandisize_pruned_graphsareNULL.compute.full = TRUEandwith.isize.pruning = FALSE:geom_pruned_graphsis returned andisize_pruned_graphsisNULL.
Examples
# Generate sample data
X <- matrix(rnorm(100 * 5), 100, 5)
# Basic usage
res1 <- create.iknn.graphs(X, k.values = seq.int(3, 10), compute.full = FALSE)
#> compute.full=FALSE: geom_pruned_graphs and isize_pruned_graphs will be NULL; returning k_statistics (and edge_pruning_stats if requested).
#> Running single-threaded.
#> Processing k values from 3 to 10 for 100 vertices
#> Requested parallel.mode: auto
#> Parallel mode: serial
#> Starting graph processing
#> [compute_knn] running...
[compute_knn] 0.000s
#> [graph_build/geometric_prune/isize_prune] running...
[graph_build/geometric_prune/isize_prune] 0.103s
#> Graph processing completed (0.103)
#> [compute_knn] 0.000s
#> [graph_build] 0.003s
#> [geometric_prune] 0.101s
#> [isize_prune] skipped (with.isize.pruning=FALSE)
#> Creating return list objects ... DONE (0.000)
#> Total elapsed time (0.103)
# With custom pruning parameters
res2 <- create.iknn.graphs(X, k.values = seq.int(3, 10),
max.path.edge.ratio.deviation.thld = 0.1,
path.edge.ratio.percentile = 0.5, compute.full = TRUE, verbose = TRUE)
#> with.isize.pruning=FALSE: isize_pruned_graphs will be NULL.
#> Running single-threaded.
#> Processing k values from 3 to 10 for 100 vertices
#> Requested parallel.mode: auto
#> Parallel mode: serial
#> Starting graph processing
#> [compute_knn] running...
[compute_knn] 0.000s
#> [graph_build/geometric_prune/isize_prune] running...
[graph_build/geometric_prune/isize_prune] 0.103s
#> Graph processing completed (0.103)
#> [compute_knn] 0.000s
#> [graph_build] 0.003s
#> [geometric_prune] 0.100s
#> [isize_prune] skipped (with.isize.pruning=FALSE)
#> Creating return list objects ... DONE (0.000)
#> Total elapsed time (0.103)
# View statistics for each k
print(res2$k_statistics)
#> k n_edges n_edges_in_pruned_graph n_removed_edges edge_reduction_ratio
#> [1,] 3 538 279 259 0.4814126
#> [2,] 4 834 424 410 0.4916067
#> [3,] 5 1154 586 568 0.4922010
#> [4,] 6 1450 733 717 0.4944828
#> [5,] 7 1787 900 887 0.4963626
#> [6,] 8 2151 1082 1069 0.4969781
#> [7,] 9 2526 1271 1255 0.4968329
#> [8,] 10 2863 1438 1425 0.4977297
#> n_edges_in_isize_pruned_graph n_removed_edges_in_double_pruning
#> [1,] NaN NaN
#> [2,] NaN NaN
#> [3,] NaN NaN
#> [4,] NaN NaN
#> [5,] NaN NaN
#> [6,] NaN NaN
#> [7,] NaN NaN
#> [8,] NaN NaN
#> double_edge_reduction_ratio
#> [1,] NaN
#> [2,] NaN
#> [3,] NaN
#> [4,] NaN
#> [5,] NaN
#> [6,] NaN
#> [7,] NaN
#> [8,] NaN