Introduction to BrainNetTest

Maximiliano Martino, Daniel Fraiman

2026-09-23

Overview

BrainNetTest implements the non-parametric L1-distance ANOVA test of Fraiman and Fraiman (2018) for comparing populations of brain networks represented as graphs. The package provides:

This vignette introduces the basic API on simple synthetic data. The companion vignette, Generating and Analyzing Brain Networks with Community Structures, shows the full pipeline on community-structured populations.

Installation

# Release version
install.packages("BrainNetTest")

# Development version from GitHub
# install.packages("remotes")
remotes::install_github("mmaximiliano/BrainNetTest")

A minimal example

library(BrainNetTest)
set.seed(1)

Generating random graphs

generate_random_graph() returns a symmetric binary adjacency matrix with no self-loops:

G <- generate_random_graph(n_nodes = 10, edge_prob = 0.2)
isSymmetric(G)
#> [1] TRUE
all(diag(G) == 0)
#> [1] TRUE

Central graph and Manhattan distance

Given a population (list of adjacency matrices), the central graph is the entry-wise mean; compute_distance() returns the L1 (Manhattan) distance between two graphs:

population <- replicate(
  5, generate_random_graph(n_nodes = 10, edge_prob = 0.2),
  simplify = FALSE)

central <- compute_central_graph(population)
compute_distance(population[[1]], central)
#> [1] 18

The group test statistic T

compute_test_statistic() aggregates within- and between-group Manhattan distances into the statistic T. Larger values indicate stronger evidence that the groups differ:

control <- replicate(
  15, generate_random_graph(n_nodes = 10, edge_prob = 0.20),
  simplify = FALSE)
patient <- replicate(
  15, generate_random_graph(n_nodes = 10, edge_prob = 0.40),
  simplify = FALSE)

populations <- list(Control = control, Patient = patient)
compute_test_statistic(populations, a = 1)
#> [1] -18.54162

The global test

compute_test_statistic() returns only the observed value. global_test() assesses it: it permutes the group labels across the pooled sample, keeping the group sizes, recomputes T for each permutation, and reports the proportion of permutations with a statistic smaller than the observed one as the p-value. The normalisation constant a is fixed at 1; the p-value does not depend on it.

gt <- global_test(populations, n_permutations = 500, seed = 42)
gt
#> 
#> Global test for populations of brain networks
#> 
#> Populations:  2 (Control, Patient), 15 / 15 graphs
#> Nodes:        10 (45 possible edges)
#> Statistic:    T = -18.54162
#> p value:      < 0.002 (500 permutations)

The permutation null distribution is kept in gt$null_distribution, so it can be inspected or plotted. With B permutations the p-value cannot resolve below 1 / B, which is how an estimate of zero is reported.

Critical edges

Once a difference is established, identify_critical_links() looks for the edges responsible. It runs the same global test first and then iteratively removes edges, ranked by their marginal p-values, until the populations are no longer distinguishable:

result <- identify_critical_links(
  populations,
  alpha          = 0.05,
  method         = "fisher",
  n_permutations = 200,
  seed           = 42)

result
#> 
#> Critical edges between populations of brain networks
#> 
#> Populations:     2 (Control, Patient), 15 / 15 graphs
#> Candidate edges: 45
#> Global test:     p < 0.005 (200 permutations, alpha = 0.05)
#> Critical edges:  7 of 45 (15.6%), ranked by Fisher's exact test
#> 
#> Most significant 6 of them:
#>   node1 node2    p_value
#> 1     2     4 0.01419290
#> 2     4    10 0.01419290
#> 3     2     5 0.02093953
#> 4     2    10 0.02093953
#> 5     8    10 0.02532769
#> 6     1     3 0.03518241
#> ... 1 more; see the critical_edges component

The result is a critical_links object. Printing it, as above, gives a compact report; summary() adds the settings the analysis ran with and the node-level ranking:

summary(result)
#> 
#> Critical edges between populations of brain networks
#> 
#> Call:
#> identify_critical_links(populations = populations, alpha = 0.05, 
#>     method = "fisher", n_permutations = 200, seed = 42)
#> 
#> Populations:     2 (Control, Patient), 15 / 15 graphs
#> Candidate edges: 45
#> Global test:     p < 0.005
#> Critical edges:  7 (15.6% of candidates)
#> 
#> Settings:
#>   marginal test        Fisher's exact test
#>   multiplicity         none
#>   significance level   0.05
#>   edges removed / step 1
#>   permutations         200
#>   normalisation a      1
#>   seed                 42
#> 
#> Most significant edges:
#>   node1 node2    p_value
#> 1     2     4 0.01419290
#> 2     4    10 0.01419290
#> 3     2     5 0.02093953
#> 4     2    10 0.02093953
#> 5     8    10 0.02532769
#> 6     1     3 0.03518241
#> 7     4     6 0.05017491
#> 
#> Nodes ranked by critical degree:
#>   node critical_degree
#> 1    2               3
#> 2    4               3
#> 3   10               3
#> 4    1               1
#> 5    3               1
#> 6    5               1
#> 7    6               1
#> 8    8               1

The individual components remain available, result$critical_edges being the ranked table of critical edges. The get_critical_nodes() helper summarises the result at the node level on its own:

get_critical_nodes(result)
#>   node critical_degree
#> 1    2               3
#> 2    4               3
#> 3   10               3
#> 4    1               1
#> 5    3               1
#> 6    5               1
#> 7    6               1
#> 8    8               1

References

Fraiman, D. and Fraiman, R. (2018) An ANOVA approach for statistical comparisons of brain networks. Scientific Reports, 8, 4746. https://doi.org/10.1038/s41598-018-23152-5