Extract an Interpretable Subnetwork Around a Set of Nodes
Source:R/extract_subnetwork.R
extract_subnetwork.RdGiven a set of nodes of interest – a gene list, a set of disease genes, a group of drug targets – returns a connected, readable subnetwork around them. Five strategies are offered, from the plain induced subgraph through to an approximate Steiner tree and a diffusion-based expansion.
Usage
extract_subnetwork(
graph,
nodes,
method = c("induced", "neighbors", "shortest_paths", "steiner", "diffusion"),
order = 1,
max_degree = NULL,
top_n = 100,
diffusion_method = c("rwr", "laplacian", "heat"),
largest_component = FALSE,
weights = NULL,
weight_type = c("strength", "distance"),
plot = TRUE,
...
)Arguments
- graph
An
igraphobject representing the network to analyze, or a data frame containing a symbolic edge list in the first two columns. Additional columns are considered as edge attributes.- nodes
Character vector of vertex names to build the subnetwork around (the "seeds", or in the Steiner case the "terminals").
- method
How to choose the nodes to keep:
"induced"Only
nodesthemselves, with whatever edges run between them. The baseline every other method should be compared against."neighbors"nodesplus theirorder-step neighborhood."shortest_paths"The union of all shortest paths between every pair of
nodes. The classic "connect my gene list"."steiner"An approximate minimum Steiner tree over
nodes: the smallest tree that connects them all. Usually the most readable figure, because it is a tree rather than a union of overlapping paths."diffusion"Propagate from
nodeswithnetwork_diffusion()and keep thetop_nhighest-scoring vertices.
- order
Integer. Neighborhood radius when
method = "neighbors". Default is1.- max_degree
Integer or
NULL. Whenmethod = "neighbors", exclude neighbors whose degree exceeds this. Without it, first-neighbor expansion on a hub-dominated network returns most of the network; see Details.- top_n
Integer. Number of vertices to keep when
method = "diffusion". Default is100.- diffusion_method
Passed to
network_diffusion()asmethodwhenmethod = "diffusion". Default is"rwr".- largest_component
Logical. If
TRUE, return only the largest connected component of the result. Default isFALSE, which keeps a forest when the seeds span several components.- weights
Optional edge weights:
NULL(default) to ignore them, the name of an edge attribute, or a numeric vector of lengthigraph::ecount(graph). Path lengths and the Steiner tree use edge costs; diffusion uses strengths. See netkit-weights.- weight_type
Either
"strength"(default) or"distance". See netkit-weights.- plot
Logical. If
TRUE(default), draws the extracted subnetwork withplot_Net(), seeds highlighted.- ...
Additional arguments passed to
plot_Net().
Value
A list with:
resultA tibble with one row per retained vertex:
node,reason(why it was kept –"seed","neighbor","on_path","steiner"or"diffused"),is_seed, andscore(the diffusion score, orNAfor the other methods).graphThe extracted subgraph, with
is_seedandreasonattached as vertex attributes. Formethod = "steiner"this is the Steiner tree itself – exactly the tree's edges. For every other method it is the subgraph induced on the selected vertices, so it carries all edges that run between them, which is more context but may contain cycles.methodA human-readable description of what was done.
Note there is no plot element: like plot_Net(), find_modules() and
highlight_nodes(), this function renders through base graphics via
plot.igraph and so has no plot object to hand back.
Details
"shortest_paths" and "steiner" both answer "how are these nodes connected
to each other", but differently. The union of shortest paths keeps every
equally short route, so it grows quickly and can be dense; the Steiner tree
keeps one connecting structure of minimum total cost, so it is always a tree
with vcount - 1 edges and is far easier to read. Use the union when you care
about redundancy of connection, the tree when you want a figure.
The Steiner tree problem is NP-hard. This implementation uses the
Kou-Markowsky-Berman heuristic: build the metric closure over the terminals
(their pairwise shortest-path distances), take a minimum spanning tree of that
closure, expand each closure edge back into the actual path it stands for, and
prune non-terminal leaves. The result is guaranteed within a factor of
2 - 2/|terminals| of the true optimum, which in practice is close.
Because the problem is only solved approximately, the tree returned is one of
possibly several near-minimal trees, and which one depends on how ties between
equal-cost paths are broken. That tie-breaking is not the same in the weighted
and unweighted code paths – igraph uses breadth-first search when no
weights are given and Dijkstra when they are – so passing weights whose
values happen to be all equal can return a different tree of slightly
different total cost than omitting them. Both satisfy the approximation bound;
neither is "the" Steiner tree. Treat the specific vertex set as one valid
answer rather than a canonical one.
max_degree exists because first-neighbor expansion is dominated by hubs. In
a protein interaction network a handful of promiscuous proteins are adjacent
to a large fraction of the graph, so the one-step neighborhood of almost any
gene list is most of the network. Capping degree removes them and leaves a
subnetwork whose edges carry information.
Edge weights
Functions that can use edge weights take two arguments:
weightsNULL(the default) to ignore edge weights; the name of an edge attribute, such as"weight"; or a numeric vector with one value per edge, inigraph::E(graph)order.weight_type"strength"(the default) if a larger value means a more tightly connected pair – confidence scores, correlations, co-expression, read counts, interaction scores."distance"if a larger value means further apart – costs, dissimilarities, reaction times.
Declaring which you have is not bookkeeping. igraph reads the weight
attribute implicitly and gives it opposite meanings in different functions:
a cost in igraph::betweenness(), igraph::distances(),
igraph::diameter() and igraph::mean_distance(), but a strength in
igraph::cluster_louvain() and the other community detection algorithms.
Attaching a confidence score and letting that happen implicitly therefore
inverts every path-based metric – a high-confidence interaction is treated as
a long distance – while community detection reads the same numbers the way
you intended.
netkit resolves weights and weight_type once per call and derives both a
strength and a distance vector from them, so each metric receives the one it
needs. A "strength" is converted to a distance by reciprocal
(\(1/w\)); a "distance" is converted to a strength by reflection
(\(\max(w) - w + \min(w)\)), which keeps a zero distance finite.
References
Kou, L., Markowsky, G., & Berman, L. (1981). A fast algorithm for Steiner trees. Acta Informatica, 15(2), 141-145. doi:10.1007/BF00288961
See also
network_diffusion() for the scores behind method = "diffusion",
find_modules() for unsupervised community structure.
Examples
g <- igraph::sample_pa(80, power = 1.5, directed = FALSE)
igraph::V(g)$name <- paste0("n", seq_len(igraph::vcount(g)))
seeds <- c("n5", "n20", "n40", "n60")
# The minimal tree connecting the seeds: always vcount - 1 edges.
st <- extract_subnetwork(g, seeds, method = "steiner", plot = FALSE)
igraph::vcount(st$graph)
#> [1] 5
st$result
#> # A tibble: 5 × 4
#> node reason is_seed score
#> <chr> <chr> <lgl> <dbl>
#> 1 n2 steiner FALSE NA
#> 2 n5 seed TRUE NA
#> 3 n20 seed TRUE NA
#> 4 n40 seed TRUE NA
#> 5 n60 seed TRUE NA
# Every shortest route between them, which is a superset.
sp <- extract_subnetwork(g, seeds, method = "shortest_paths", plot = FALSE)
igraph::vcount(sp$graph) >= igraph::vcount(st$graph)
#> [1] TRUE
# `top_n` is small here to keep the example fast on an 80-node graph.
df <- extract_subnetwork(g, seeds, method = "diffusion", top_n = 15,
plot = FALSE)
head(df$result)
#> # A tibble: 6 × 4
#> node reason is_seed score
#> <chr> <chr> <lgl> <dbl>
#> 1 n2 diffused FALSE 0.0231
#> 2 n4 diffused FALSE 0.0162
#> 3 n5 seed TRUE 0.554
#> 4 n12 diffused FALSE 0.0162
#> 5 n15 diffused FALSE 0.0162
#> 6 n17 diffused FALSE 0.388