Nothing
# ---- Network comparison (descriptive, no inference) ----
#
# Mirrors `tna::compare()` exactly: same scaling menu, edge-level metrics,
# summary metrics (weight deviations, correlations, dissimilarities,
# similarities, pattern agreements), network metrics (via summary.netobject
# from R/summary_network.R), and optional centrality differences.
#
# All metrics are descriptive. Inferential comparisons (NCT, permutation
# tests, sequence comparisons) live in their own files.
#' Compare two networks descriptively
#'
#' Computes a battery of descriptive comparison metrics between two networks
#' or two weight matrices: weight deviations (mean / median / RMS / max
#' absolute difference, relative mean absolute difference, coefficient-of-
#' variation ratio), four correlation measures (Pearson, Spearman, Kendall,
#' distance correlation), five dissimilarity measures (Euclidean, Manhattan,
#' Canberra, Bray-Curtis, Frobenius), five similarity measures (Cosine,
#' Jaccard, Dice, Overlap, RV), pattern agreements, and side-by-side network
#' metrics. Optionally adds centrality differences and centrality
#' correlations.
#'
#' Mirrors `tna::compare()` numerically. Inputs are converted to weight
#' matrices and scaled before comparison; the choice of scaling determines
#' how weights from different estimators are placed on a common footing.
#'
#' @param x A `netobject`, `cograph_network`, or numeric square matrix.
#' @param y A `netobject`, `cograph_network`, or numeric square matrix.
#' @param scaling Scaling applied to both weight matrices before comparison.
#' One of:
#' \describe{
#' \item{`"none"`}{Identity (default).}
#' \item{`"minmax"`}{\eqn{(w - \min) / (\max - \min)}; maps to \eqn{[0, 1]}.}
#' \item{`"max"`}{\eqn{w / \max(|w|)}; preserves sign.}
#' \item{`"rank"`}{Min-max of average ranks; ordinal scaling.}
#' \item{`"zscore"`}{\eqn{(w - \bar w) / s_w}; standard score.}
#' \item{`"robust"`}{\eqn{(w - \mathrm{med}(w)) / \mathrm{mad}(w)};
#' Huber-style robust z-score, resists outliers.}
#' \item{`"log"`}{\eqn{\log(w)}; requires \eqn{w > 0}.}
#' \item{`"log1p"`}{\eqn{\log(1 + w)}; admits \eqn{w \ge 0}.}
#' \item{`"softmax"`}{Numerically stable softmax over the flattened
#' vector.}
#' \item{`"quantile"`}{Empirical CDF of the flattened vector.}
#' \item{`"frobenius"`}{Divide the matrix by its Frobenius norm
#' \eqn{\|W\|_F = \sqrt{\sum w_{ij}^2}}; matrix-level normalisation.}
#' \item{`"row"`}{Row-stochastic normalisation (each row's absolute
#' values sum to 1). Only meaningful for non-negative matrices; rows
#' summing to zero are left unchanged.}
#' }
#' Scalings that produce negative weights (`zscore`, `robust`) are
#' compatible with `network = TRUE` because the side-by-side metrics use
#' Nestimate's base-R Floyd-Warshall, which handles negative weights.
#' @param measures Character vector of centrality measures to compare. Empty
#' by default (no centrality block). Valid names are \code{"InStrength"},
#' \code{"OutStrength"}, and \code{"Betweenness"}. Unknown names are
#' ignored with a warning.
#' @param network Logical. Include side-by-side network metrics from
#' `summary()`? Default `TRUE`.
#' @param ... Ignored.
#' @return A `net_comparison` object: a named list with `matrices`,
#' `difference_matrix`, `edge_metrics`, `summary_metrics`, optionally
#' `network_metrics`, `centrality_differences`, `centrality_correlations`.
#' @export
compare_model <- function(x, ...) UseMethod("compare_model")
#' @rdname compare_model
#' @export
compare_model.netobject <- function(x, y, scaling = "none", measures = character(0),
network = TRUE, ...) {
.compare_impl(.weights_of(x), .weights_of(y), scaling, measures, network)
}
#' @rdname compare_model
#' @export
compare_model.cograph_network <- function(x, y, scaling = "none",
measures = character(0), network = TRUE,
...) {
.compare_impl(.weights_of(x), .weights_of(y), scaling, measures, network)
}
#' @rdname compare_model
#' @export
compare_model.matrix <- function(x, y, scaling = "none", measures = character(0),
network = TRUE, ...) {
.compare_impl(.weights_of(x), .weights_of(y), scaling, measures, network)
}
#' Compare two networks within a netobject_group
#'
#' Selects two members of a `netobject_group` (by index or name) and
#' dispatches to `compare_model.netobject()`.
#'
#' @param x A `netobject_group`.
#' @param i Integer or character. Index or name of the first network.
#' Default `1L`.
#' @param j Integer or character. Index or name of the second network.
#' Default `2L`.
#' @param scaling See `compare_model()`.
#' @param measures See `compare_model()`.
#' @param network See `compare_model()`.
#' @param ... Passed to `compare_model.netobject()`.
#' @return A `net_comparison` object.
#' @export
compare_model.netobject_group <- function(x, i = 1L, j = 2L, scaling = "none",
measures = character(0), network = TRUE,
...) {
stopifnot(inherits(x, "netobject_group"))
if (is.character(i)) i <- match(i, names(x))
if (is.character(j)) j <- match(j, names(x))
stopifnot(!is.na(i), !is.na(j), i >= 1L, i <= length(x),
j >= 1L, j <= length(x), i != j)
compare_model(x[[i]], x[[j]], scaling = scaling, measures = measures,
network = network, ...)
}
# ---- Internal: extract weight matrix ----
.weights_of <- function(z) {
if (is.matrix(z)) {
stopifnot(nrow(z) == ncol(z), is.numeric(z))
if (is.null(rownames(z)) || is.null(colnames(z))) {
labs <- paste0("V", seq_len(nrow(z)))
rownames(z) <- colnames(z) <- labs
}
return(z)
}
if (inherits(z, "netobject") || inherits(z, "cograph_network")) {
return(z$weights)
}
stop("`compare_model()` expects a netobject, cograph_network, or numeric matrix.",
call. = FALSE)
}
# ---- Internal: the comparison machinery ----
.compare_impl <- function(x, y, scaling, measures, network) {
stopifnot(identical(dim(x), dim(y)))
stopifnot(!anyNA(x), !anyNA(y))
n <- nrow(x)
scaling <- match.arg(scaling, .compare_scaling_names())
x_scaled <- .apply_compare_scaling(x, scaling)
y_scaled <- .apply_compare_scaling(y, scaling)
d <- x_scaled - y_scaled
x_vec <- as.vector(x_scaled)
y_vec <- as.vector(y_scaled)
abs_diff <- abs(x_vec - y_vec)
abs_x <- abs(x_vec)
abs_y <- abs(y_vec)
pos <- abs_x > 0 & abs_y > 0
rn <- rownames(x_scaled)
edges <- expand.grid(source = rn, target = rn,
KEEP.OUT.ATTRS = FALSE,
stringsAsFactors = FALSE)
edges$weight_x <- x_vec
edges$weight_y <- y_vec
edges$raw_difference <- as.vector(d)
edges$absolute_difference <- abs_diff
edges$squared_difference <- abs_diff^2
edges$relative_difference <- abs_diff / (x_vec + y_vec)
edges$similarity_strength_index <- x_vec / y_vec
edges$difference_index <- (x_vec - y_vec) / y_vec
edges$rank_difference <- abs(rank(x_vec, na.last = "keep") -
rank(y_vec, na.last = "keep"))
edges$percentile_difference <- abs(stats::ecdf(x_vec)(x_vec) -
stats::ecdf(y_vec)(y_vec))
edges$logarithmic_ratio <- log1p(x_vec) - log1p(y_vec)
edges$standardized_weight_x <- (x_vec - mean(x_scaled)) / stats::sd(x_scaled)
edges$standardized_weight_y <- (y_vec - mean(y_scaled)) / stats::sd(y_scaled)
edges$standardized_score_inflation <-
edges$standardized_weight_x / edges$standardized_weight_y
# Single source of truth: the worker computes all 22 metrics; this block
# just adds category labels and pretty names for the data.frame view.
sim <- .network_similarity(x_scaled, y_scaled)
summary_metrics <- data.frame(
category = c(
rep("Weight Deviations", 6L),
rep("Correlations", 4L),
rep("Dissimilarities", 5L),
rep("Similarities", 5L),
rep("Pattern Similarities", 2L)
),
metric = c(
"Mean Abs. Diff.", "Median Abs. Diff.", "RMS Diff.",
"Max Abs. Diff.", "Rel. Mean Abs. Diff.", "CV Ratio",
"Pearson", "Spearman", "Kendall", "Distance",
"Euclidean", "Manhattan", "Canberra", "Bray-Curtis", "Frobenius",
"Cosine", "Jaccard", "Dice", "Overlap", "RV",
"Rank Agreement", "Sign Agreement"
),
value = unname(sim[c(
"mean_abs_diff", "median_abs_diff", "rms_diff",
"max_abs_diff", "rel_mean_abs", "cv_ratio",
"pearson", "spearman", "kendall", "distance_cor",
"euclidean", "manhattan", "canberra", "bray_curtis", "frobenius",
"cosine", "jaccard", "dice", "overlap", "rv",
"rank_agreement", "sign_agreement"
)]),
stringsAsFactors = FALSE
)
out <- list(
matrices = list(x = x_scaled, y = y_scaled),
difference_matrix = d,
edge_metrics = edges,
summary_metrics = summary_metrics
)
if (network) {
sx <- .summary_metrics_from_weights(x_scaled,
directed = !isSymmetric(x_scaled))
sy <- .summary_metrics_from_weights(y_scaled,
directed = !isSymmetric(y_scaled))
out$network_metrics <- data.frame(metric = sx$metric,
x = sx$value,
y = sy$value,
stringsAsFactors = FALSE)
}
if (length(measures) > 0L) {
cents <- .compare_centralities(x_scaled, y_scaled, measures)
out$centrality_differences <- cents$differences
out$centrality_correlations <- cents$correlations
}
structure(out, class = "net_comparison")
}
# ---- Scaling catalogue ----
# Names of every scaling option understood by `compare_model()`.
.compare_scaling_names <- function() {
c("none", "minmax", "max", "rank", "zscore", "robust",
"log", "log1p", "softmax", "quantile",
"frobenius", "row")
}
# Apply a single scaling option to a weight matrix. Most options operate on
# the flattened vector and preserve dimnames; `frobenius` and `row` are
# matrix-level normalisations.
.apply_compare_scaling <- function(W, scaling) {
if (scaling == "row") {
rs <- rowSums(abs(W))
nz <- rs > 0
out <- W
out[nz, ] <- W[nz, ] / rs[nz]
return(out)
}
if (scaling == "frobenius") {
fro <- sqrt(sum(W^2))
return(if (fro == 0) W else W / fro)
}
vec_fn <- switch(scaling,
none = identity,
minmax = function(w) {
rng <- range(w)
if (diff(rng) == 0) w else (w - rng[1L]) / diff(rng)
},
max = function(w) {
m <- max(w, na.rm = TRUE)
if (m == 0) w else w / m
},
rank = function(w) {
r <- rank(w, ties.method = "average")
rng <- range(r)
if (diff(rng) == 0) r else (r - rng[1L]) / diff(rng)
},
zscore = function(w) {
s <- stats::sd(w)
if (s == 0) w - mean(w) else (w - mean(w)) / s
},
robust = function(w) {
m <- stats::mad(w)
if (m == 0) w - stats::median(w) else (w - stats::median(w)) / m
},
log = log,
log1p = log1p,
softmax = function(w) {
mx <- max(w); e <- exp(w - mx); e / sum(e)
},
quantile = function(w) stats::ecdf(w)(w),
stop("Unknown scaling: ", scaling, call. = FALSE)
)
out <- W
out[] <- vec_fn(as.vector(W))
out
}
# ---- Network similarity worker ----
# Single source of truth for descriptive similarity between two networks.
# Returns a named numeric vector. Inputs can be matrices (full 22 metrics)
# or vectors (subset that does not need matrix structure - RV and
# rank_agreement are skipped).
#
# Used by `.compare_impl()`, `.split_half_metrics()` (reliability), and
# the casedrop drop-fraction loop. When you add a metric, add it once
# here and every caller picks it up.
.network_similarity <- function(x, y, metrics = NULL) {
all_metrics <- c(
"mean_abs_diff", "median_abs_diff", "rms_diff", "max_abs_diff",
"rel_mean_abs", "cv_ratio",
"pearson", "spearman", "kendall", "distance_cor",
"euclidean", "manhattan", "canberra", "bray_curtis", "frobenius",
"cosine", "jaccard", "dice", "overlap", "rv",
"rank_agreement", "sign_agreement"
)
if (is.null(metrics)) metrics <- all_metrics
metrics <- match.arg(metrics, all_metrics, several.ok = TRUE)
has_matrix <- is.matrix(x) && is.matrix(y)
if (has_matrix) {
stopifnot(identical(dim(x), dim(y)))
x_mat <- x; y_mat <- y
x_vec <- as.vector(x); y_vec <- as.vector(y)
n <- nrow(x_mat)
} else {
x_vec <- as.numeric(x); y_vec <- as.numeric(y)
stopifnot(length(x_vec) == length(y_vec))
n <- length(x_vec)
}
# Cheap shared quantities
abs_diff <- abs(x_vec - y_vec)
needs_abs_xy <- any(metrics %in% c("canberra", "bray_curtis",
"jaccard", "dice", "overlap"))
if (needs_abs_xy) {
abs_x <- abs(x_vec); abs_y <- abs(y_vec); pos <- abs_x > 0 & abs_y > 0
}
needs_sd <- any(metrics %in% c("pearson", "spearman", "kendall", "cv_ratio"))
if (needs_sd) { sd_x <- stats::sd(x_vec); sd_y <- stats::sd(y_vec) }
# Compute requested metrics on demand. Each branch only fires if asked.
one <- function(m) switch(m,
mean_abs_diff = mean(abs_diff),
median_abs_diff = stats::median(abs_diff),
rms_diff = sqrt(mean(abs_diff^2)),
max_abs_diff = max(abs_diff),
rel_mean_abs = {
my <- mean(abs(y_vec))
if (my == 0) NA_real_ else mean(abs_diff) / my
},
cv_ratio = {
mx <- mean(x_vec); my <- mean(y_vec)
if (mx == 0 || sd_y == 0) NA_real_ else sd_x * my / (mx * sd_y)
},
pearson = if (sd_x == 0 || sd_y == 0) NA_real_
else stats::cor(x_vec, y_vec, method = "pearson",
use = "complete.obs"),
spearman = if (sd_x == 0 || sd_y == 0) NA_real_
else stats::cor(x_vec, y_vec, method = "spearman",
use = "complete.obs"),
kendall = if (sd_x == 0 || sd_y == 0) NA_real_
else stats::cor(x_vec, y_vec, method = "kendall",
use = "complete.obs"),
distance_cor = .distance_correlation(x_vec, y_vec),
euclidean = sqrt(sum(abs_diff^2)),
manhattan = sum(abs_diff),
canberra = sum(abs_diff[pos] / (abs_x[pos] + abs_y[pos])),
bray_curtis = {
s <- sum(abs_x + abs_y)
if (s == 0) NA_real_ else sum(abs_diff) / s
},
frobenius = if (has_matrix) sqrt(sum(abs_diff^2)) / sqrt(n / 2)
else NA_real_,
cosine = {
d <- sqrt(sum(x_vec^2)) * sqrt(sum(y_vec^2))
if (d == 0) NA_real_ else sum(x_vec * y_vec) / d
},
jaccard = {
s <- sum(pmax(abs_x, abs_y))
if (s == 0) NA_real_ else sum(pmin(abs_x, abs_y)) / s
},
dice = {
s <- sum(abs_x) + sum(abs_y)
if (s == 0) NA_real_ else 2 * sum(pmin(abs_x, abs_y)) / s
},
overlap = {
m <- min(sum(abs_x), sum(abs_y))
if (m == 0) NA_real_ else sum(pmin(abs_x, abs_y)) / m
},
rv = if (has_matrix) .rv_coefficient(x_mat, y_mat)
else NA_real_,
rank_agreement = if (has_matrix)
mean(sign(diff(x_mat)) == sign(diff(y_mat)))
else NA_real_,
sign_agreement = mean(sign(x_vec) == sign(y_vec))
)
out <- vapply(metrics, one, numeric(1L))
names(out) <- metrics
out
}
# ---- Helpers ----
# Distance correlation per Szekely et al. (2007).
.distance_correlation <- function(x, y) {
dist_x <- as.matrix(stats::dist(x, diag = TRUE, upper = TRUE))
dist_y <- as.matrix(stats::dist(y, diag = TRUE, upper = TRUE))
n <- ncol(dist_x)
rm_x <- matrix(.rowMeans(dist_x, n, n), n, n)
rm_y <- matrix(.rowMeans(dist_y, n, n), n, n)
cm_x <- matrix(.colMeans(dist_x, n, n), n, n, byrow = TRUE)
cm_y <- matrix(.colMeans(dist_y, n, n), n, n, byrow = TRUE)
m_x <- mean(dist_x); m_y <- mean(dist_y)
xx <- dist_x - rm_x - cm_x + m_x
yy <- dist_y - rm_y - cm_y + m_y
v_xy <- n^-2 * sum(xx * yy)
v_x <- n^-2 * sum(xx^2)
v_y <- n^-2 * sum(yy^2)
v_xy / sqrt(v_x * v_y)
}
# RV coefficient per Robert & Escoufier (1976).
.rv_coefficient <- function(x, y) {
x <- scale(x, scale = FALSE)
y <- scale(y, scale = FALSE)
xx <- tcrossprod(x)
yy <- tcrossprod(y)
sum(diag(xx %*% yy)) /
sqrt(sum(diag(xx %*% xx)) * sum(diag(yy %*% yy)))
}
.compare_centralities <- function(x, y, measures) {
# Validate against the full built-in vocabulary, not the DEFAULT columns of
# centrality() — the defaults shrank in 0.7.6 and silently turned valid
# measures (e.g. OutStrength) into "unknown" here.
valid <- .centrality_builtin_measures()
keep <- intersect(measures, valid)
unmatched <- setdiff(measures, valid)
if (length(unmatched) > 0L) {
warning("compare_model(): unknown centrality measure",
if (length(unmatched) == 1L) " " else "s ",
paste(unmatched, collapse = ", "),
" ignored. Valid measures: ",
paste(valid, collapse = ", "), ".", call. = FALSE)
}
if (length(keep) == 0L) {
return(list(differences = data.frame(),
correlations = data.frame()))
}
cx <- net_centrality(.wrap_netobject(x, method = "compare_model",
directed = TRUE, data = NULL),
measures = keep)
cy <- net_centrality(.wrap_netobject(y, method = "compare_model",
directed = TRUE, data = NULL),
measures = keep)
cx_df <- as.data.frame(cx)
cy_df <- as.data.frame(cy)
state <- if ("state" %in% names(cx_df)) cx_df$state
else if ("name" %in% names(cx_df)) cx_df$name
else seq_len(nrow(cx_df))
diffs <- do.call(rbind, lapply(keep, function(m) {
data.frame(state = state, centrality = m,
x = cx_df[[m]], y = cy_df[[m]],
difference = cx_df[[m]] - cy_df[[m]],
stringsAsFactors = FALSE)
}))
cors <- do.call(rbind, lapply(keep, function(m) {
r <- tryCatch(stats::cor(cx_df[[m]], cy_df[[m]], use = "complete.obs"),
error = function(e) NA_real_)
data.frame(centrality = m, correlation = r, stringsAsFactors = FALSE)
}))
list(differences = diffs, correlations = cors)
}
# ---- print method ----
#' @export
print.net_comparison <- function(x, ...) {
cat("Network comparison\n")
cat("==================\n")
cat("Summary metrics:\n")
sm <- x$summary_metrics
sm$value <- vapply(sm$value,
function(v) formatC(v, digits = 4L, format = "g"),
character(1L))
print.data.frame(sm, row.names = FALSE)
if (!is.null(x$network_metrics)) {
cat("\nNetwork metrics (x vs y):\n")
nm <- x$network_metrics
nm$x <- .format_metric_values(nm$metric, nm$x)
nm$y <- .format_metric_values(nm$metric, nm$y)
print.data.frame(nm, row.names = FALSE)
}
if (!is.null(x$centrality_correlations)) {
cat("\nCentrality correlations:\n")
cc <- x$centrality_correlations
cc$correlation <- vapply(cc$correlation,
function(v) formatC(v, digits = 4L, format = "g"),
character(1L))
print.data.frame(cc, row.names = FALSE)
}
invisible(x)
}
# ---- plot method ----
#' Plot a network comparison
#'
#' Visualises a `net_comparison` object. Currently supports the edge-weight
#' scatterplot (default), with the diagonal reference (perfect agreement)
#' and the OLS regression line annotated by Pearson, Spearman, and Kendall
#' correlations.
#'
#' @param x A `net_comparison` object from [compare_model()].
#' @param type Character. One of `"scatter"` (default - edge-weight scatter
#' with OLS fit and correlation overlay), `"heatmap"` (n by n grid of
#' x - y differences using the diverging palette), `"diff_hist"`
#' (histogram of |x - y| absolute differences with rug + density),
#' `"weight_dist"` (overlaid distributions of |x| and |y| edge weights),
#' or `"all"` (2 by 2 grid of all four panels; requires the gridExtra
#' package).
#' @param combined When `type = "all"` and `combined = TRUE` (default),
#' the four panels are stitched into a 2x2 gtable. When `FALSE`, returns
#' a named list of the four ggplots so each can be printed, saved, or
#' re-laid-out independently. Ignored for other `type` values.
#' @param ... Ignored.
#' @return A `ggplot` object; for `type = "all"` with `combined = TRUE`
#' a `gtable` arranged 2 by 2; for `type = "all"` with `combined = FALSE`
#' a named list of four ggplots.
#' @export
plot.net_comparison <- function(x,
type = c("scatter", "heatmap",
"diff_hist", "weight_dist",
"all"),
combined = TRUE,
...) {
type <- match.arg(type)
stopifnot(is.logical(combined), length(combined) == 1L)
if (!requireNamespace("ggplot2", quietly = TRUE)) {
stop("ggplot2 is required for plot.net_comparison().", call. = FALSE)
}
switch(type,
scatter = .plot_comparison_scatter(x),
heatmap = .plot_comparison_heatmap(x),
diff_hist = .plot_comparison_diff_hist(x),
weight_dist = .plot_comparison_weight_dist(x),
all = .plot_comparison_all(x, combined = combined)
)
}
# 2x2 grid of all four panels, stripped of titles so the layout reads as
# a single figure. Uses gridExtra::arrangeGrob (already in Suggests).
# combined = FALSE returns the four ggplots as a named list instead of
# stitching them - useful when each panel needs its own device or the
# user wants to save them separately.
.plot_comparison_all <- function(x, combined = TRUE) {
strip <- function(p, label) {
p +
ggplot2::labs(title = label, subtitle = NULL) +
ggplot2::theme(plot.title = ggplot2::element_text(size = 11,
face = "bold"))
}
panels <- list(
scatter = strip(.plot_comparison_scatter(x), "Scatter"),
heatmap = strip(.plot_comparison_heatmap(x), "Heatmap (x - y)"),
diff_hist = strip(.plot_comparison_diff_hist(x), "|x - y|"),
weight_dist = strip(.plot_comparison_weight_dist(x), "Weight distributions")
)
if (!combined) return(invisible(panels))
if (!requireNamespace("gridExtra", quietly = TRUE)) {
stop("type = 'all' with combined = TRUE requires the gridExtra package.",
call. = FALSE)
}
combined_grob <- gridExtra::arrangeGrob(panels$scatter, panels$heatmap,
panels$diff_hist, panels$weight_dist,
nrow = 2L, ncol = 2L)
grid::grid.draw(combined_grob)
invisible(combined_grob)
}
# Edge-weight scatter with diagonal, OLS line, and correlation annotations.
# House defaults: theme_minimal(base_size = 12), linewidth = 0.4. The
# regression band uses the project diverging palette's high-end blue
# (#4A6FE3) so it is consistent with the heatmap palette to follow.
.plot_comparison_scatter <- function(x) {
em <- x$edge_metrics
sm <- x$summary_metrics
pick <- function(label) {
v <- sm$value[sm$metric == label]
if (length(v) == 0L) NA_real_ else v[[1L]]
}
r_p <- pick("Pearson")
r_s <- pick("Spearman")
r_k <- pick("Kendall")
ann <- sprintf("Pearson = %.3f\nSpearman = %.3f\nKendall = %.3f",
r_p, r_s, r_k)
rng <- range(c(em$weight_x, em$weight_y), na.rm = TRUE)
pad <- 0.04 * diff(rng)
lo <- rng[1L] - pad
hi <- rng[2L] + pad
ggplot2::ggplot(em, ggplot2::aes(x = .data$weight_x, y = .data$weight_y)) +
ggplot2::geom_abline(slope = 1, intercept = 0,
linetype = "dashed", colour = "grey60",
linewidth = 0.4) +
ggplot2::geom_smooth(method = "lm", se = TRUE, formula = y ~ x,
colour = "#4A6FE3", fill = "#4A6FE3", alpha = 0.15,
linewidth = 0.4) +
ggplot2::geom_point(alpha = 0.7, size = 1.6, colour = "#333333") +
ggplot2::annotate("text", x = lo, y = hi,
label = ann, hjust = 0, vjust = 1, size = 3.5,
family = "mono") +
ggplot2::coord_cartesian(xlim = c(lo, hi), ylim = c(lo, hi)) +
ggplot2::labs(x = "Network x: edge weight",
y = "Network y: edge weight",
title = "Edge-weight comparison",
subtitle = "Dashed: y = x. Blue: OLS fit + 95% CI.") +
ggplot2::theme_minimal(base_size = 12)
}
# Heatmap of (x - y), the scaled difference matrix. Diverging palette
# matches the project house defaults (red -> white -> blue, midpoint = 0).
# Per CLAUDE.md, never touch the diagonal - it stays as estimated.
.plot_comparison_heatmap <- function(x) {
d <- x$difference_matrix
rn <- rownames(d); cn <- colnames(d)
if (is.null(rn)) rn <- as.character(seq_len(nrow(d)))
if (is.null(cn)) cn <- as.character(seq_len(ncol(d)))
long <- data.frame(
source = factor(rep(rn, times = ncol(d)), levels = rn),
target = factor(rep(cn, each = nrow(d)), levels = rev(cn)),
diff = as.vector(d),
stringsAsFactors = FALSE
)
lim <- max(abs(long$diff), na.rm = TRUE)
ggplot2::ggplot(long,
ggplot2::aes(x = .data$target, y = .data$source,
fill = .data$diff)) +
ggplot2::geom_tile(colour = "grey90", linewidth = 0.4) +
ggplot2::scale_fill_gradient2(low = "#D33F6A", mid = "white",
high = "#4A6FE3", midpoint = 0,
limits = c(-lim, lim),
name = "x - y") +
ggplot2::labs(x = "to", y = "from",
title = "Difference matrix (x - y)",
subtitle = "Red: x > y. Blue: y > x. White: agreement.") +
ggplot2::theme_minimal(base_size = 12) +
ggplot2::theme(panel.grid = ggplot2::element_blank(),
axis.text.x = ggplot2::element_text(angle = 45,
hjust = 1))
}
# Histogram of absolute edge-weight differences |x - y| with rug and
# density overlay. Magnitude-of-disagreement view: 0 means perfect
# agreement at that cell; larger means bigger discrepancy. Rug guarantees
# each individual edge is visible even when most cells cluster near zero.
.plot_comparison_diff_hist <- function(x) {
abs_diffs <- abs(as.vector(x$difference_matrix))
.magnitude_density_plot(
abs_diffs,
title = "Distribution of |edge-weight differences|",
subtitle = "Mean (red), median (grey). Rug: individual edges.",
x_lab = "|x - y|"
)
}
# Overlaid distributions of |edge weight| for the two scaled networks.
# Lets the reader see whether the two networks have similar weight
# magnitudes overall (same dynamic range, similar concentration of mass
# near zero) or whether one is systematically heavier-tailed than the
# other.
.plot_comparison_weight_dist <- function(x) {
vx <- abs(as.vector(x$matrices$x))
vy <- abs(as.vector(x$matrices$y))
df <- data.frame(
weight = c(vx, vy),
network = factor(rep(c("x", "y"), c(length(vx), length(vy))),
levels = c("x", "y"))
)
ggplot2::ggplot(df,
ggplot2::aes(x = .data$weight,
fill = .data$network,
colour = .data$network)) +
ggplot2::geom_histogram(ggplot2::aes(y = ggplot2::after_stat(density)),
bins = 30, alpha = 0.4,
position = "identity",
colour = "white", linewidth = 0.4) +
ggplot2::geom_density(linewidth = 0.6, fill = NA) +
ggplot2::geom_rug(sides = "b", alpha = 0.5,
length = ggplot2::unit(0.025, "npc")) +
ggplot2::scale_fill_manual(values = c(x = "#4A6FE3", y = "#D33F6A")) +
ggplot2::scale_colour_manual(values = c(x = "#1d3a8a", y = "#8a1d3a")) +
ggplot2::labs(x = "|edge weight|", y = "density",
title = "Distribution of |edge weights| per network",
subtitle = "Blue: x. Red: y. Rug: individual edges.") +
ggplot2::theme_minimal(base_size = 12) +
ggplot2::theme(legend.position = "bottom")
}
# Shared layout for the magnitude-style histogram with rug + density.
.magnitude_density_plot <- function(values, title, subtitle, x_lab) {
values <- values[is.finite(values)]
med <- stats::median(values)
mu <- mean(values)
iqr <- stats::IQR(values)
rng <- range(values)
bw <- if (iqr > 0) 2 * iqr / (length(values)^(1 / 3))
else diff(rng) / 12
nbins <- max(8L, min(30L, ceiling(diff(rng) / max(bw, .Machine$double.eps))))
ggplot2::ggplot(data.frame(value = values),
ggplot2::aes(x = .data$value)) +
ggplot2::geom_histogram(ggplot2::aes(y = ggplot2::after_stat(density)),
bins = nbins, fill = "#4A6FE3", alpha = 0.55,
colour = "white", linewidth = 0.4) +
ggplot2::geom_density(colour = "#1d3a8a", linewidth = 0.6,
fill = NA) +
ggplot2::geom_rug(sides = "b", alpha = 0.6,
length = ggplot2::unit(0.03, "npc")) +
ggplot2::geom_vline(xintercept = mu, colour = "#D33F6A",
linewidth = 0.4) +
ggplot2::geom_vline(xintercept = med, colour = "grey50",
linetype = "dashed", linewidth = 0.4) +
ggplot2::annotate("text", x = mu, y = Inf,
label = sprintf(" mean = %.3f", mu),
hjust = 0, vjust = 1.5, size = 3.5,
colour = "#D33F6A", family = "mono") +
ggplot2::annotate("text", x = med, y = Inf,
label = sprintf(" median = %.3f", med),
hjust = 0, vjust = 3, size = 3.5,
colour = "grey40", family = "mono") +
ggplot2::labs(x = x_lab, y = "density",
title = title, subtitle = subtitle) +
ggplot2::theme_minimal(base_size = 12)
}
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.