Nothing
#' Construct a co-occurrence network via two-mode multiplication
#'
#' The unified engine for all bibliometric networks. Operates entirely in
#' sparse representation — never allocates a dense n x n matrix.
#'
#' @param B A sparse bipartite matrix (works x entities), already weighted
#' by the counting method.
#' @param mode Character. `"columns"` for column-mode co-occurrence
#' (e.g., co-citation), `"rows"` for row-mode (e.g., coupling).
#' @param similarity Character. Normalization method (see [normalize()]).
#' @param threshold Numeric. Minimum edge weight to retain.
#' @param top_n Integer or `NULL`. If specified, keep only the top `n`
#' most frequent nodes and return all edges among them.
#'
#' @return A data frame with columns `from`, `to`, `weight`, `count`.
#' @keywords internal
multiply_bipartite <- function(B, mode = "columns",
similarity = "none",
threshold = 0,
top_n = NULL,
self_loops = FALSE) {
## Coupling-strength: deferred row-normalization from apply_counting.
## This denominator is defined for row-mode projections (e.g., document
## coupling). In column-mode projections, edge indices refer to column nodes,
## so applying a row-level denominator would be dimensionally wrong.
row_scale <- attr(B, "row_scale")
## Weighted co-occurrence matrix (sparse)
if (mode == "columns") {
A <- Matrix::crossprod(B)
} else {
A <- Matrix::tcrossprod(B)
}
## Raw binary co-occurrence for the count column
B_bin <- (B > 0) * 1
if (mode == "columns") {
A_raw <- Matrix::crossprod(B_bin)
} else {
A_raw <- Matrix::tcrossprod(B_bin)
}
## Top-n node filter: keep only the most frequent entities
if (!is.null(top_n)) {
freq <- Matrix::diag(A)
if (length(freq) > top_n) {
keep_idx <- order(-freq)[seq_len(top_n)]
A <- A[keep_idx, keep_idx]
A_raw <- A_raw[keep_idx, keep_idx]
if (!is.null(row_scale) && mode == "rows") {
row_scale <- row_scale[keep_idx]
}
}
}
## Normalize weighted matrix (or just zero the diagonal)
if (similarity != "none") {
A_norm <- normalize(A, method = similarity)
} else {
if (!self_loops) Matrix::diag(A) <- 0
A_norm <- A
}
## Extract upper-triangle edges directly from sparse (no dense conversion)
triplets <- Matrix::summary(A_norm)
i <- triplets$i
j <- triplets$j
x <- triplets$x
## Remove lower triangle and zeros (diagonal kept when self_loops = TRUE)
keep <- (if (self_loops) i <= j else i < j) & x != 0
i <- i[keep]; j <- j[keep]; x <- x[keep]
if (length(i) == 0L) {
return(data.frame(
from = character(0), to = character(0),
weight = numeric(0), count = integer(0),
stringsAsFactors = FALSE
))
}
## Coupling-strength: divide by n_refs_i × n_refs_j
if (!is.null(row_scale) && mode == "rows" && similarity == "none") {
x <- x / (row_scale[i] * row_scale[j])
}
## Threshold filter early (before name lookup)
if (threshold > 0) {
keep <- x >= threshold
i <- i[keep]; j <- j[keep]; x <- x[keep]
}
if (length(i) == 0L) {
return(data.frame(
from = character(0), to = character(0),
weight = numeric(0), count = integer(0),
stringsAsFactors = FALSE
))
}
node_names <- rownames(A_norm)
if (is.null(node_names)) node_names <- as.character(seq_len(nrow(A_norm)))
## Look up raw counts for matched edges (vectorized sparse access)
raw_counts <- A_raw[cbind(i, j)]
edges <- data.frame(
from = node_names[i],
to = node_names[j],
weight = x,
count = as.integer(raw_counts),
stringsAsFactors = FALSE
)
## Sort by weight descending
edges <- edges[order(-edges$weight), ]
rownames(edges) <- NULL
edges
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.