Nothing
# ===========================================================================
# Batch 51 kernel --- trust-PageRank (TPR).
#
# Sheng, J., Zhu, J., Wang, Y., Wang, B. and Hou, Z. (2020). Identifying
# Influential Nodes of Complex Networks Based on Trust-Value. Algorithms
# 13(11):280. doi:10.3390/a13110280. Equations (2), (4), (5), (6) and (7)
# and Algorithm 1, journal pages 4-7, read from 200 dpi page images as well
# as from the text layer.
#
# `b` is an adjacency matrix; the kernel projects it to the simple
# undirected skeleton the source defines on and touches no igraph object.
# ===========================================================================
#' Fixed point of the source's restricted SimRank recursion
#'
#' Equation (4) on journal page 5 reads
#'
#' ```
#' s(a, b) = 1 a = b
#' = (C / (|N_a| |N_b|)) sum_{i,j} s(N_a(i), N_b(j)) a != b
#' ```
#'
#' and Algorithm 1 line 4, page 7, restricts the similarity map to
#' **connected** pairs: `for connected node v_u, v_v in V do`. Table 3 on
#' page 6 says the same thing from the other side -- "the short dash
#' indicates that there is no edge between the two nodes" -- so the map
#' holds an entry for every adjacent pair and for the diagonal, and a
#' non-adjacent pair entering the double sum contributes **zero**, not the
#' `0.1` that initializes the adjacent entries. That domain restriction is
#' the whole reason the recursion is usable: the diagonal `s(l, l) = 1` is
#' the only inhomogeneous term, and it enters `s(a, b)` exactly when `l` is
#' a common neighbor of `a` and `b`, so the constant of the affine map is
#' the triangle census of the line.
#'
#' Writing `A` for the binary skeleton and `S` for the similarity carried
#' on the lines, the sweep is one matrix sandwich,
#'
#' ```
#' S <- C * (A (S + I) A) / (d d') masked to the lines of A
#' ```
#'
#' because `(A M A)[a, b] = sum_{l in N_a} sum_{m in N_b} M[l, m]` for a
#' symmetric binary `A`. The iteration starts at `S = 0` rather than at the
#' source's `0.1`: the map is affine and monotone, so from zero the iterates
#' increase to the least nonnegative fixed point, which is the fixed point
#' whenever one is unique, and which stays exactly zero where the recursion
#' is homogeneous instead of drifting to whatever the start happened to be.
#' The start does not matter where the map contracts; it is the whole answer
#' where it does not, and zero is the choice that reports the degeneracy
#' rather than hiding it. **The source's `0.1` initialization is therefore
#' not used**, and `centrality_trust_pagerank()` records why.
#'
#' Every row sum of the linear part is at most `1 - p/(d_a d_b)` for a line
#' carrying `p` triangles, because the `p` diagonal terms are the part of
#' the `d_a d_b` ordered pairs that leaves the linear part, so the spectral
#' radius never exceeds one and is strictly below one on any block that
#' carries a triangle. The iteration converges for every `C` in `(0, 1]`.
#'
#' **The convergence test is RELATIVE, and it has to be.** The similarities
#' on a single graph span many orders of magnitude -- on a long chain at
#' `C = 0.2` the largest is `2.4e-2` and the smallest positive one
#' `2.7e-20` -- because the mass reaching a line decays geometrically with
#' its distance from the nearest triangle. An absolute test on the largest
#' entrywise change stops while the small entries are still an order of
#' magnitude from their limits, and equation (2) then divides two of those
#' small entries by each other. `.cg_tpr_relative_change()` measures the
#' change against the new value on the lines that have one, so every line
#' must settle in its own scale.
#'
#' @param a Binary symmetric loop-free adjacency matrix.
#' @param decay The source's `C`, in `(0, 1]`.
#' @param tol Positive tolerance on the largest relative change.
#' @param max_iter Iteration bound.
#' @return A list with the similarity matrix `s`, the sweep count
#' `iterations` and the flag `converged`.
#' @keywords internal
#' @noRd
.cg_tpr_similarity <- function(a, decay, tol, max_iter) {
n <- nrow(a)
d <- rowSums(a)
# d_a d_b is zero only where an endpoint is isolated, and there the mask
# `* a` already writes zero, so the guard never changes a reported value.
dd <- outer(d, d)
dd[dd <= 0] <- 1
s <- matrix(0, n, n)
iterations <- 0L
delta <- Inf
# A fixed-point sweep consumes the previous sweep, so it cannot be
# vectorized away; the bound and the residual are both reported.
while (iterations < max_iter && !(delta < tol)) {
m <- s
diag(m) <- 1
nxt <- decay * (a %*% m %*% a) / dd * a
delta <- .cg_tpr_relative_change(nxt, s)
s <- nxt
iterations <- iterations + 1L
}
list(s = s, iterations = iterations, converged = isTRUE(delta < tol))
}
#' Largest relative change between two iterates
#'
#' `max |new - old| / new` over the entries where `new` is positive, and
#' zero when none is. Both iterations here start from a nonnegative vector
#' and stay nonnegative, and an entry that is exactly zero in `new` is
#' exactly zero in `old` too, so the skipped entries have no change to
#' measure.
#'
#' @param nxt,previous Numeric objects of the same shape.
#' @return A single nonnegative number.
#' @keywords internal
#' @noRd
.cg_tpr_relative_change <- function(nxt, previous) {
live <- nxt > 0
if (!any(live)) return(0)
max(abs(nxt[live] - previous[live]) / nxt[live])
}
#' Lines that carry a strictly positive similarity at the fixed point
#'
#' The iterates increase from zero, so a line ends up positive exactly when
#' it can reach a triangle-carrying line through the recursion: `s(a, b)`
#' draws on `s(l, m)` for every `l` in `N_a` and `m` in `N_b`, and the only
#' source of mass is the diagonal term, which is present on a line iff that
#' line lies on a triangle. Deciding this **structurally**, by a boolean
#' closure, rather than by testing the converged doubles against zero,
#' matters for the same reason it does in `.cg_reach_closure()`: a value
#' produced by a long chain of divisions can be a legitimate `1e-300`, and
#' a threshold cannot tell that from a rounding artifact.
#'
#' @param a Binary symmetric loop-free adjacency matrix.
#' @return Logical matrix, `TRUE` on the lines with positive similarity.
#' @keywords internal
#' @noRd
.cg_tpr_support <- function(a) {
edge <- a > 0
# A line lies on a triangle iff its endpoints have a common neighbor.
support <- edge & ((a %*% a) > 0)
# Each round moves the frontier one hop at each endpoint, so the closure
# is reached in at most a diameter's worth of rounds; it is monotone and
# bounded, so the loop terminates.
repeat {
grown <- edge & ((a %*% (support * 1) %*% a) > 0)
nxt <- support | grown
if (identical(nxt, support)) break
support <- nxt
}
support
}
#' Trust-PageRank and the parts it is built from
#'
#' Sheng et al.'s trust-value replaces PageRank's uniform `1/k_j` split with
#' a mixture of a similarity ratio and a degree ratio. Journal pages 4 to 7
#' give the five equations:
#'
#' ```
#' Rs(i, j) = s(i, j) / sum_{k in N_j} s(j, k) (2)
#' s(a, b) = 1 if a = b, else (C/(|N_a||N_b|)) sum sum s(.,.) (4)
#' Rd(i, j) = d_i / sum_{k in N_j} d_k (5)
#' T(i, j) = (1 - k) Rs(i, j) + k Rd(i, j) (6)
#' TPR_i^t = (1 - alpha)/n + alpha sum_{j in N_i} T(i, j) TPR_j^{t-1} (7)
#' ```
#'
#' Both ratios in equation (6) are normalized over `N_j`, and `s` is
#' symmetric, so `sum_{i in N_j} Rs(i, j) = 1` and
#' `sum_{i in N_j} Rd(i, j) = 1` for every `k`: **`T` is column-stochastic
#' on the lines**. Equation (7) is therefore an ordinary damped PageRank
#' with a unique fixed point, and its iteration count is a convergence
#' tolerance rather than a modeling choice. The scores sum to one on a
#' graph without isolates; an isolate's column is empty, so it emits
#' nothing and the total falls by the mass it holds.
#'
#' **A node whose lines all carry zero similarity has no trust-value at
#' all.** Equation (2) then divides zero by zero, and unlike the vanishing
#' denominators of `centrality_dil()` and `centrality_lhc()` the quotient is
#' not determined by its numerator: the ratios must sum to one over `N_j`,
#' but nothing in the source says how. Every node of the affected component
#' is returned as `NA`; see `centrality_trust_pagerank()` for why a `1/d_j`
#' fallback was rejected.
#'
#' @param b Adjacency matrix. Direction, weights, loops and parallel edges
#' are dropped by `.cg_undirected_view()`: page 3 sets the paper in an
#' undirected network with `a(i, j) = 1`, and every quantity in the five
#' equations is a count or a ratio of counts.
#' @param alpha The source's jump probability, in `(0, 1)`.
#' @param mix The source's `k`, in `[0, 1]`.
#' @param decay The source's `C`, in `(0, 1]`.
#' @param tol Positive tolerance on the largest relative change, for both
#' recursions.
#' @param max_iter Iteration bound for both recursions.
#' @return A `data.frame` with one row per node in input order and the
#' columns `degree` (`d_i`), `similarity_sum` (`S_i`, the row sum of the
#' similarity over the node's own lines, which is Table 3's `S_v`
#' column), `neighbor_degree_sum` (`D_i`) and `trust_pagerank` (the
#' score). Carried as attributes: `similarity` (`s`), `trust` (`T`,
#' asymmetric where `s` is symmetric), `defined` (a logical vector,
#' `FALSE` at a node returned as `NA`), `iterations` (a two-element
#' vector, the similarity and the PageRank sweep counts) and `converged`
#' (a matching two-element logical vector).
#' @keywords internal
#' @noRd
.cg_tpr_terms <- function(b, alpha = 0.85, mix = 0.85, decay = 1,
tol = 1e-14, max_iter = 1000L) {
n <- nrow(b)
if (is.null(n) || n == 0L) {
out <- data.frame(degree = numeric(), similarity_sum = numeric(),
neighbor_degree_sum = numeric(),
trust_pagerank = numeric())
attr(out, "similarity") <- matrix(0, 0, 0)
attr(out, "trust") <- matrix(0, 0, 0)
attr(out, "defined") <- logical()
attr(out, "iterations") <- c(similarity = 0L, pagerank = 0L)
attr(out, "converged") <- c(similarity = TRUE, pagerank = TRUE)
return(out)
}
a <- .cg_undirected_view(b)
d <- rowSums(a)
fit <- .cg_tpr_similarity(a, decay, tol, max_iter)
s <- fit$s
support <- .cg_tpr_support(a)
# A node needs a trust column only if it has neighbors at all; an
# isolate is never a `j` in equation (7), so it is not undefined.
undefined_node <- d > 0 & rowSums(support) == 0L
reach <- .cg_reach_closure(a)
# An undefined column makes equation (7) undefined for every node of its
# component, because the trust matrix that component solves against has
# no value in that column.
defined <- !as.logical(reach %*% (undefined_node * 1) > 0)
similarity_sum <- rowSums(s)
neighbor_degree_sum <- as.numeric(a %*% d)
# Equations (2) and (5). Both denominators run over N_j, so they are
# column quantities; each is guarded before the division, never after.
safe_s <- similarity_sum
safe_s[!(safe_s > 0)] <- 1
safe_d <- neighbor_degree_sum
safe_d[!(safe_d > 0)] <- 1
rs <- s / matrix(safe_s, n, n, byrow = TRUE)
rd <- matrix(d, n, n) / matrix(safe_d, n, n, byrow = TRUE)
trust <- ((1 - mix) * rs + mix * rd) * a
# A column with no value must not contribute mass to the recursion; the
# component it sits in is reported as NA below regardless.
trust[, !defined] <- 0
# Equation (7), iterated from the uniform vector to the same tolerance.
x <- rep(1 / n, n)
iterations <- 0L
delta <- Inf
while (iterations < max_iter && !(delta < tol)) {
y <- (1 - alpha) / n + alpha * as.numeric(trust %*% x)
delta <- .cg_tpr_relative_change(y, x)
x <- y
iterations <- iterations + 1L
}
x[!defined] <- NA_real_
out <- data.frame(degree = d, similarity_sum = similarity_sum,
neighbor_degree_sum = neighbor_degree_sum,
trust_pagerank = x)
rownames(out) <- NULL
attr(out, "similarity") <- s
attr(out, "trust") <- trust
attr(out, "defined") <- defined
attr(out, "iterations") <- c(similarity = fit$iterations,
pagerank = iterations)
attr(out, "converged") <- c(similarity = fit$converged,
pagerank = isTRUE(delta < tol))
out
}
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.