bgns: Biweight Graph and Network Statistics

Aditya Kshirsagar

Scope

bgns estimates biweight midcorrelation and selects exact nearest neighbors from the resulting similarities. This vignette describes score calculation, edge selection, and incomplete-data handling through reproducible examples. The simulated measurements include positive and negative associations.

Installation

Source installation requires R >= 4.3.0, a C++17 compiler, and the numerical libraries configured for R. Install the appropriate Rtools on Windows or follow the R for macOS toolchain instructions. OpenMP support is optional. WGCNA is not a runtime dependency.

install.packages("remotes")
remotes::install_github("metaddict/bgns")

From measurements to a correlation graph

Arrange observations in rows and the entities to be compared in columns. For expression data, this orientation determines whether the graph connects genes, cells, or metacells. Apply the normalization and feature-selection steps appropriate to the study before calculating similarities.

library(bgns)

set.seed(11)
x <- matrix(rnorm(80 * 6), nrow = 80, ncol = 6)
colnames(x) <- paste0("item_", seq_len(ncol(x)))
x[, 2] <- 0.8 * x[, 1] + 0.2 * rnorm(nrow(x))
x[, 3] <- -0.7 * x[, 1] + 0.3 * rnorm(nrow(x))
x[1:6, 4] <- NA_real_

cm <- bicor(x, min_overlap = 3)
round(cm, 3)
#>        item_1 item_2 item_3 item_4 item_5 item_6
#> item_1  1.000  0.956 -0.907  0.010  0.137 -0.007
#> item_2  0.956  1.000 -0.842  0.036  0.076 -0.002
#> item_3 -0.907 -0.842  1.000 -0.021 -0.136 -0.033
#> item_4  0.010  0.036 -0.021  1.000 -0.047  0.091
#> item_5  0.137  0.076 -0.136 -0.047  1.000 -0.029
#> item_6 -0.007 -0.002 -0.033  0.091 -0.029  1.000

For a dense input, bicor(x) returns the square matrix of column similarities. Supplying a second dense matrix produces an ncol(x) by ncol(y) result. Matrix output retains undefined entries as NA.

Selecting correlation edges

An absolute-score cutoff retains associations of either sign:

edges <- bicor(x, tidy = TRUE, threshold = 0.5)
edges
#>     col1   col2        cor
#> 1 item_1 item_2  0.9564391
#> 2 item_1 item_3 -0.9066908
#> 3 item_2 item_3 -0.8424723

The table reports column identifiers in col1 and col2, and the signed score in cor. A self-comparison reports each eligible pair once, without the diagonal. For bicor(x, y, tidy = TRUE), the identifiers refer to x and y, respectively.

Selecting nearest neighbors

kn <- bicor_knn(x, knn = 2, threshold = 0)
head(kn)
#>     col1   col2        val rank
#> 1 item_1 item_2 0.95643913    1
#> 2 item_1 item_5 0.13657572    2
#> 3 item_2 item_1 0.95643913    1
#> 4 item_2 item_5 0.07624946    2
#> 5 item_4 item_6 0.09099176    1
#> 6 item_4 item_2 0.03638161    2

Here, col1 denotes the target and col2 its selected neighbor. val is the signed score, and rank orders neighbors within each target. Selection is directed: an edge need not be reciprocal. Self-neighbors are excluded when y is omitted, and fewer than knn edges are returned if too few candidates qualify.

The two selection rules are distinct:

Output Eligibility rule Ordering
Correlation edges abs(cor) >= threshold No neighbor ranking
KNN edges val > threshold Decreasing signed score within target

Consequently, a strong negative association can survive an absolute cutoff but fail a positive KNN cutoff. The default threshold = -Inf admits all finite scores, including negative values. The argument does not filter a dense correlation matrix.

Sparse measurements and cross-matrix neighbors

To retain a defined robust scale, construct the sparse example by setting 20% of the simulated measurements to zero. Convert the result to sparse storage after modifying the values.

set.seed(12)
x_zero <- x
index <- sample.int(length(x_zero), floor(0.2 * length(x_zero)))
x_zero[index] <- 0
xs <- Matrix::Matrix(x_zero, sparse = TRUE)

edges_sparse <- bicor(xs, tidy = TRUE, threshold = 0.5)
kn_sparse <- bicor_knn(xs, knn = 2, threshold = 0)
head(edges_sparse)
#>     col1   col2        cor
#> 1 item_1 item_2  0.6677082
#> 2 item_1 item_3 -0.6881657
#> 3 item_2 item_3 -0.6439078
head(kn_sparse)
#>     col1   col2        val rank
#> 1 item_1 item_2 0.66770825    1
#> 2 item_1 item_4 0.10508486    2
#> 3 item_2 item_1 0.66770825    1
#> 4 item_2 item_5 0.01126153    2
#> 5 item_3 item_6 0.03957315    1
#> 6 item_4 item_1 0.10508486    1

Sparse calculations accept double-valued Matrix classes that can be represented as dgCMatrix. Implicit entries contribute observed zeros. Sparse inputs require edge or KNN output; dense correlation-matrix output requires dense inputs. Internal working panels may nevertheless be dense.

For cross-matrix selection, align the observations of both inputs before calling the function. Row order is positional; row names are not matched.

set.seed(13)
y <- cbind(
  target_A = x[, 1] + rnorm(nrow(x), sd = 0.2),
  target_B = x[, 3] + rnorm(nrow(x), sd = 0.2)
)
kn_xy <- bicor_knn(
  xs, y, knn = 2, threshold = 0.5,
  bipartite_levels = "separate"
)
kn_xy
#>       col1   col2       val rank
#> 1 target_A item_1 0.8670493    1
#> 2 target_A item_2 0.7933726    2
#> 3 target_B item_3 0.8340956    1

Each column of y is a target whose candidates are columns of xs. Accordingly, col1 carries target names and col2 carries source names. bipartite_levels = "separate" preserves these distinct name sets. Either input may use dense or sparse storage.

Finite overlap and normalization

For each column, the implementation estimates a median and scaled median absolute deviation (MAD) from its finite observations. The scale is 1.4826 * median(abs(x - median(x))); biweight deviations use a tuning multiplier of 9. These columnwise estimates and weights remain fixed across pairwise comparisons.

With pairwise.complete.obs = TRUE, the numerator combines weighted deviations only at observations finite in both columns. NA, NaN, and infinities are excluded; zeros are retained. The denominator is selected separately:

The following example changes only the denominator convention:

x_missing <- x[, 1:2]
x_missing[1:20, 2] <- NA_real_

r_column <- bicor(x_missing, use_intersection_denominator = FALSE)[1, 2]
r_shared <- bicor(x_missing, use_intersection_denominator = TRUE)[1, 2]
c(column_denominator = r_column, shared_denominator = r_shared)
#> column_denominator shared_denominator 
#>          0.8345499          0.9586580

For fully observed pairs, the two conventions coincide. min_overlap specifies the required count of shared finite observations, independently of their robust weights; the default is 3. It applies to complete data too.

n_shared <- sum(is.finite(x_missing[, 1]) & is.finite(x_missing[, 2]))
n_shared
#> [1] 60
bicor(x_missing, min_overlap = n_shared + 1)[1, 2]
#> [1] NA
bicor(x_missing, pairwise.complete.obs = FALSE)[1, 2]
#> [1] NA

The first correlation is undefined because the overlap requirement exceeds the available observations. The second is undefined because pairwise.complete.obs = FALSE excludes comparisons involving an incomplete column. This option does not delete incomplete rows across the entire matrix.

Undefined scores

A zero MAD or undefined normalization makes bicor unavailable. This can occur even in a nonconstant column when most measurements are identical. Sparse storage does not alter that statistical condition.

z <- cbind(
  variable = x[, 1],
  constant = rep(1, nrow(x)),
  zero_heavy = c(rep(0, 60), seq_len(20)),
  all_missing = rep(NA_real_, nrow(x))
)
diag(bicor(z))
#>    variable    constant  zero_heavy all_missing 
#>           1          NA          NA          NA

Undefined comparisons remain NA in matrix output, including ineligible diagonal entries, and are omitted from edge and KNN tables. Inspect column variability when expected vertices have no reported neighbors.

Resource configuration

The default KNN path (direct_sparse = TRUE) retains bounded candidate sets while processing similarities in panels. It avoids retaining a full dense similarity matrix, but exact self-KNN still evaluates a quadratic number of candidate pairs. A thresholded correlation table can itself approach quadratic size if most pairs qualify.

BGNS_MEM_MB guides panel scratch allocation and defaults to 256 MB. Total process memory also includes inputs, returned results, preprocessing, and numerical-library workspace. BGNS_NUM_THREADS limits the package’s OpenMP regions, with a default of at most two threads; BLAS threading is configured independently. Builds without OpenMP execute those regions serially.

When changing runtime controls, set them before the first computation:

Sys.setenv(BGNS_MEM_MB = "256", BGNS_NUM_THREADS = "2")

Optional SIMD paths depend on the compiled target and runtime processor. See help("bgns") for additional controls. Specify the scientific overlap criterion through min_overlap rather than an environment variable.

Session information

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: aarch64-apple-darwin23
#> Running under: macOS Golden Gate 27.0
#> 
#> Matrix products: default
#> BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
#> 
#> locale:
#> [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#> 
#> time zone: Asia/Kolkata
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] bgns_0.4.3
#> 
#> loaded via a namespace (and not attached):
#>  [1] digest_0.6.39   R6_2.6.1        fastmap_1.2.0   Matrix_1.7-5   
#>  [5] xfun_0.60       lattice_0.22-9  cachem_1.1.0    knitr_1.51     
#>  [9] htmltools_0.5.9 rmarkdown_2.32  lifecycle_1.0.5 cli_3.6.6      
#> [13] grid_4.6.1      sass_0.4.10     jquerylib_0.1.4 compiler_4.6.1 
#> [17] tools_4.6.1     evaluate_1.0.5  bslib_0.12.0    yaml_2.3.12    
#> [21] otel_0.2.0      rlang_1.3.0     jsonlite_2.0.0