Today’s target
In the Centrality tutorial, we asked which nodes stand out in a network — centrality as an expression of structural inequality. But nodes don’t always seek to differentiate themselves in a network. Sometimes they are interested in being part of a group. This tutorial considers various ways in which such ‘groupness’ might be examined: how cohesive a network is overall, whether its ties close into groups, how it breaks into components and factions, and how well any proposed grouping fits — its modularity — including groupings discovered by community detection algorithms.

Catching up: This
tutorial assumes you can already load or make a network in R, draw it
with graphr(), and measure it with node_by_*()
and net_by_*() functions. If any of that is hazy, work
through the earlier {stocnet} tutorials first: run
run_tute("Making") and
run_tute("Manipulating") for network data,
run_tute("Visualising") for graphing, and
run_tute("Centrality") for the measuring syntax used
throughout, or read their static versions on the manynet, autograph, and netrics websites.
New to network vocabulary?: Throughout this tutorial, key terms are italicised: hover over them for a definition, and a full glossary of the terms used appears at the end of the tutorial.
Aims
By the end of this tutorial, you should be able to:
Choose your own data: The worked examples below use
ison_algebra, ison_southern_women, and
irps_blogs, all bundled with {manynet}. But
wherever there is an exercise box, you are encouraged to swap in a
network that interests you. Remember the three flavours of
bundled data as a rough difficulty ladder — Classic
(ison_*, small & tidy), Fiction
(fict_*, mid-sized & fun), Real-world
(irps_*, larger & realistic) — and that you can browse
the full list with table_data().
Setting up
The data we’re going to use here, “ison_algebra”, is included in the
{manynet} package. Do you remember how to call the data?
Can you find out some more information about it?
# Let's call and load the 'ison_algebra' dataset
data("ison_algebra", package = "manynet")
# Or you can retrieve like this:
ison_algebra <- manynet::ison_algebra
# If you want to learn more about the 'ison_algebra' dataset, use the following function (below)
?manynet::ison_algebra
data("ison_algebra", package = "manynet")
?manynet::ison_algebra
# If you want to see the network object, you can run the name of the object
ison_algebra
# or print the code with brackets at the front and end of the code
(ison_algebra <- manynet::ison_algebra)
We can see after printing the object that the dataset is multiplex , meaning that it contains several different types of ties: friendship (friends), social (social) and task interactions (tasks).
Adding names
The network is also anonymous, but I think it would be nice to add
some names, even if it’s just pretend. Luckily, {manynet}
has a function for this, to_named(). This makes plotting
the network just a wee bit more accessible and interpretable. Let’s try
adding names and graphing the network now:
ison_algebra <- to_named(ison_algebra)
graphr(ison_algebra)
ison_algebra <- to_named(ison_algebra)
graphr(ison_algebra)
Note that you will likely get a different set of names, as they are assigned randomly from a pool of (American) first names.
Separating multiplex networks
As a multiplex network, there are actually three different types of
ties (friends, social, and tasks) in this network. We can extract them
and graph them separately using to_uniplex():
# to_uniplex extracts ties of a single type,
# focusing on the 'friends' tie attribute here
friends <- to_uniplex(ison_algebra, "friends")
gfriend <- graphr(friends) + ggtitle("Friendship")
# now let's focus on the 'social' tie attribute
social <- to_uniplex(ison_algebra, "social")
gsocial <- graphr(social) + ggtitle("Social")
# and the 'tasks' tie attribute
tasks <- to_uniplex(ison_algebra, "tasks")
gtask <- graphr(tasks) + ggtitle("Task")
# now, let's compare each attribute's graph, side-by-side
gfriend + gsocial + gtask
# if you get an error here, you may need to install and load
# the package 'patchwork'.
# It's highly recommended for assembling multiple plots together.
# Otherwise you can just plot them separately on different lines.
friends <- to_uniplex(ison_algebra, "friends")
gfriend <- graphr(friends) + ggtitle("Friendship")
social <- to_uniplex(ison_algebra, "social")
gsocial <- graphr(social) + ggtitle("Social")
tasks <- to_uniplex(ison_algebra, "tasks")
gtask <- graphr(tasks) + ggtitle("Task")
# We now have three separate networks depicting each type of tie from the ison_algebra network:
gfriend + gsocial + gtask
Beginner note:
Placing two or more gg-based plots side by side with +
relies on the {patchwork} package. If
gfriend + gsocial + gtask errors for you, install and load
{patchwork} — or just plot each object on its own line.
Note also that these are
weighted networks. graphr() automatically
recognises these different weights and plots them. Where useful (less
dense
directed networks), graphr() also bends
reciprocated arcs. What (else) can we say about these three
networks?
Cohesion
On this page: Defining density · Calculating density
Let’s concentrate on the task network for now and calculate perhaps the most common generalised measure of cohesion: density .
Defining density
Density represents a generalised measure of cohesion, characterising how cohesive the network is in terms of how many potential ties (i.e. dyads ) are actualised. Recall that there are different equations depending on the type of network. Below are three equations:
\[A: \frac{|T|}{|N|(|N|-1)}\] \[B: \frac{2|T|}{|N|(|N|-1)}\] \[C: \frac{|T|}{|N||M|}\]
where \(|T|\) is the number of ties in the network, and \(|N|\) and \(|M|\) are the number of nodes in the first and second mode respectively.
Calculating density
One can calculate the density of the network using the number of
nodes and the number of ties using the functions
net_nodes() and net_ties(), respectively:
# calculating network density manually according to equation
net_ties(tasks)/(net_nodes(tasks)*(net_nodes(tasks)-1))
but we can also just use the {netrics} function for
calculating the density, which always uses the equation appropriate for
the type of network…
net_by_density(tasks)
Note that the various measures in {netrics} print
results to three decimal points by default, but the underlying result
retains the same recurrence. So same result…
Beginner note:
The net_by_ prefix signals a whole-network measure, just as
node_by_ signalled a nodal measure in the Centrality
tutorial. The same grammar recurs throughout {netrics}:
net_by_density(), net_by_reciprocity(),
net_by_transitivity(), net_by_components(),
net_by_modularity(), and so on.
Density offers an important baseline measure for characterising the network as a whole. Here a little over a third of the possible task ties are present (0.367) — far from complete, yet busy enough that task activity clearly spreads beyond a handful of pairs. Because density depends so strongly on network size (each new node adds many more possible ties than actual ones), raw densities should only be compared across networks of similar size — larger networks are almost always sparser.
Going further:
Density is not the only sense in which a network can be cohesive.
net_by_cohesion() reports the minimum number of nodes that
would need to be removed to disconnect the network, and
net_by_adhesion() the minimum number of ties —
adhesion is thus the tie-based counterpart to node-based
cohesion. See ?measure_cohesion for the full set.
In brief: Density
is the proportion of possible ties that are actually present:
net_by_density() always applies the equation appropriate to
the network’s type, or calculate it by hand from net_ties()
and net_nodes(). It is a size-sensitive baseline for how
cohesive a network is as a whole.
Closure
On this page: Reciprocity · Transitivity
In this section we’re going to move from generalised measures of cohesion, like density, to more localised measures of cohesion. These are not only measures of cohesion though, but are also often associated with certain mechanisms of closure. Closure involves ties being more likely because other ties are present. There are two common examples of this in the literature: reciprocity , where a directed tie is often likely to prompt a reciprocating tie, and transitivity , where a directed two-path is likely to be shortened by an additional arc connecting the first and third nodes on that path.

Reciprocity
First, let’s calculate reciprocity in the task network. While one
could do this by hand, it’s more efficient to do this using the
{netrics} package. Can you guess the correct name of the
function?
net_by_reciprocity(tasks)
# this function calculates the amount of reciprocity in the whole network
Wow, this seems quite high based on what we observed visually! But if
we look closer, this makes sense. We can use
tie_is_reciprocated() to identify those ties that are
reciprocated and not.
tasks |> mutate_ties(rec = tie_is_reciprocated(tasks)) |>
graphr(edge_color = "rec")
net_by_indegree(tasks)
So we can see that indeed there are very few asymmetric ties, and yet node 16 is both the sender and receiver of most of the task activity. So our reciprocity measure has taught us something about this network that might not have been obvious visually: task interactions here are nearly always mutual — where one node works with another, the recognition flows both ways — even though how much each node participates remains very unequal.
Transitivity
And let’s calculate transitivity in the task network. Again, can you guess the correct name of this function?
net_by_transitivity(tasks)
# this function calculates the amount of transitivity in the whole network
At 0.568, well over half of this network’s two-paths are closed into triangles — if two students each work with a common third, they usually end up working with each other too. That is exactly what we would expect of task interactions, which tend to happen within teams, and it foreshadows this tutorial’s destination: where closure concentrates, groups emerge.
Going further:
Both closure measures also come in nodal versions:
node_by_reciprocity() and
node_by_transitivity() score each node’s local
closure, and tie_is_transitive() flags the ties involved in
closed triads. See ?measure_closure and
?measure_closure_node for definitions and references.
In brief: Closure
measures capture ties begetting ties: net_by_reciprocity()
for how often directed ties are returned (with
tie_is_reciprocated() to map which ones), and
net_by_transitivity() for how often two-paths close into
triangles. Both hint at the group structure explored in the rest of this
tutorial.
Projection
On this page: A two-mode network · Two projections · Projection options · Two-mode closure
A two-mode network
The next dataset, ‘ison_southern_women’, is also available in
{manynet}. It’s a classic
two-mode network about the attendance of 18 women at 14
social events in the 1930s American South. For more details see
?ison_southern_women for documentation in your R/RStudio.
Let’s load and graph the data.
# let's load the data and analyze it
data("ison_southern_women")
ison_southern_women
graphr(ison_southern_women, node_color = "type")
graphr(ison_southern_women, "railway", node_color = "type")
data("ison_southern_women")
ison_southern_women
graphr(ison_southern_women, node_color = "type")
Two projections
Now what if we are only interested in one part of the network? For that, we can obtain a projection of the two-mode network. There are two ways of doing this. The hard way…
twomode_matrix <- as_matrix(ison_southern_women)
women_matrix <- twomode_matrix %*% t(twomode_matrix)
event_matrix <- t(twomode_matrix) %*% twomode_matrix
Or the easy way:
# women-graph
# to_mode1(): Results in a weighted one-mode object that retains the row nodes from
# a two-mode object, and weights the ties between them on the basis of their joint
# ties to nodes in the second mode (columns)
women_graph <- to_mode1(ison_southern_women)
graphr(women_graph)
# note that projection `to_mode1` involves keeping one type of nodes
# this is different from to_uniplex above, which keeps one type of ties in the network
# event-graph
# to_mode2(): Results in a weighted one-mode object that retains the column nodes from
# a two-mode object, and weights the ties between them on the basis of their joint ties
# to nodes in the first mode (rows)
event_graph <- to_mode2(ison_southern_women)
graphr(event_graph)
The women’s projection now records which women moved in the same social circles, and the event projection which events drew a common crowd — but note how much denser both look than the original two-mode network: every event has been replaced by ties among all of its attendees, a point we will return to below.
Projection options
{manynet} also includes several other options for how to
construct the projection. The default (“count”) might be interpreted as
indicating the degree or amount of shared ties to the other mode.
“jaccard” divides this count by the number of nodes in the other mode to
which either of the nodes are tied. It can thus be interpreted as
opportunity weighted by participation. “rand” instead counts both shared
ties and shared absences, and can thus be interpreted as the degree of
behavioural mirroring between the nodes. Lastly, “pearson” (Pearson’s
coefficient) and “yule” (Yule’s Q) produce correlations in ties for
valued and binary data respectively.
tie_weights(to_mode1(ison_southern_women, similarity = "count"))
tie_weights(to_mode1(ison_southern_women, similarity = "jaccard"))
tie_weights(to_mode1(ison_southern_women, similarity = "rand"))
tie_weights(to_mode1(ison_southern_women, similarity = "pearson"))
tie_weights(to_mode1(ison_southern_women, similarity = "yule"))
Practically speaking, most projection is conducted using the “count” method, as it reflects the absolute overlap, the frequency of co-occurrence. As such, it is most commonly used in social and organisational network analysis, reflecting shared group membership or collaboration intensity. But it is sensitive to asymmetries in the number of nodes in each node set and the degree distribution of each node set, and ignores the absence of ties as uninformative. To normalise for these effects, “jaccard” and “rand” are often used. “pearson” and “yule” are less commonly used, but can be useful when interested in the correlation of ties between nodes.
Closure in two-mode networks
Let’s return to the question of closure. First try one of the closure
measures we have already treated that gives us a sense of shared
partners for one-mode networks. Then compare this with
net_by_equivalency(), which can be used on the original
two-mode network.
# net_by_transitivity(): Calculate transitivity in a network
net_by_transitivity(women_graph)
net_by_transitivity(event_graph)
# net_by_equivalency(): Calculate equivalence or reinforcement in a (usually two-mode) network
net_by_equivalency(ison_southern_women)
net_by_transitivity(women_graph)
net_by_transitivity(event_graph)
net_by_equivalency(ison_southern_women)
The lesson here is that projection manufactures closure: every event becomes a clique of its attendees, and since cliques are perfectly transitive, the projections’ transitivity scores (0.93 and 0.83) are inflated well beyond anything the underlying behaviour requires. The two-mode equivalency score (0.47) is the more honest summary of how much the women’s attendance patterns actually reinforce one another.
Try to explain in no more than a paragraph why projection can lead to misleading transitivity measures and what some consequences of this might be.
In brief:
to_mode1()/to_mode2() project a two-mode
network into ties among one of its node sets, with a
similarity argument (“count”, “jaccard”, “rand”, “pearson”,
“yule”) governing the weights. Projection inflates transitivity and
hides memberships, so where possible measure the two-mode network
directly, e.g. with net_by_equivalency().
Components
On this page: Counting components · Component membership
Counting components
Now let’s look at the friendship network, ‘friends’. We’re interested
here in how many
components there are. By default, the
net_by_components() function will return the number of
strong components for directed networks. For weak
components, you will need to first make the network
undirected . Remember the difference between weak and
strong components?
net_by_components(friends)
# note that friends is a directed network
# you can see this by calling the object 'friends'
# or by running `is_directed(friends)`
# Now let's look at the number of components for objects connected by an undirected edge
# Note: to_undirected() returns an object with all tie direction removed,
# so any pair of nodes with at least one directed edge
# will be connected by an undirected edge in the new network.
net_by_components(to_undirected(friends))
# note that friends is a directed network
net_by_components(friends)
net_by_components(to_undirected(friends))
Component membership
So we know how many components there are, but maybe we’re also
interested in which nodes are members of which components?
node_in_component() returns a membership vector that can be
used to color nodes in graphr():
friends <- friends |>
mutate_nodes(weak_comp = node_in_component(to_undirected(friends)),
strong_comp = node_in_component(friends))
# node_in_component returns a vector of nodes' memberships to components in the network
# here, we are adding the nodes' membership to components as an attribute in the network
# alternatively, we can also use the function `add_node_attribute()`
# eg. `add_node_attribute(friends, "weak_comp", node_in_component(to_undirected(friends)))`
graphr(friends, node_color = "weak_comp") + ggtitle("Weak components") +
graphr(friends, node_color = "strong_comp") + ggtitle("Strong components")
# by using the 'node_color' argument, we are telling graphr to color
# the nodes in the graph according to the values of the 'weak_comp' attribute in the network
friends <- friends |>
mutate_nodes(weak_comp = node_in_component(to_undirected(friends)),
strong_comp = node_in_component(friends))
graphr(friends, node_color = "weak_comp") + ggtitle("Weak components") +
graphr(friends, node_color = "strong_comp") + ggtitle("Strong components")
Substantively, the difference between the two maps is one node that only receives friendship nominations: following the arrows it can reach no one, so it counts as its own strong component, but ignoring direction it rejoins the main friendship cluster. Components thus give us a first, strict sense of groupness — who is connected to whom at all — before we ask the subtler question of who clusters with whom.
In brief:
Components partition a network by
reachability : net_by_components() counts them
(strong by default for directed networks; wrap in
to_undirected() for weak), and
node_in_component() returns each node’s membership, ready
to map onto node_color in graphr().
Factions
On this page: A bigger network · The giant component · Finding a partition
A bigger network
Components offer a precise way of understanding groups in a network.
However, they can also ignore some ‘groupiness’ that is obvious to even
a cursory examination of the graph. The irps_blogs network
concerns the url links between political blogs in the 2004 election. It
is a big network (you can check below), so in our experience it can take
a few seconds to graph — we will concentrate on a random sample to speed
things up.
# This is a large network
net_nodes(irps_blogs)
# Let's concentrate on just a sample of 240
# (by deleting a random sample of 1250)
blogs <- delete_nodes(irps_blogs, sample(1:1490, 1250))
graphr(blogs)
But are they all actually linked? Even among the smaller sample,
there seems to be a number of
isolates . We can calculate the number of isolates by
simply summing node_is_isolate().
sum(node_is_isolate(blogs))
Since there are many isolates, there will be many components, even if we look at weak components and not just strong components.
node_in_component(blogs)
node_in_component(to_undirected(blogs))
The giant component
So, it looks like most of the (weak) components are due to isolates! How do we concentrate on the main component of this network? Well, the main/largest component in a network is called the giant component .
blogs <- blogs |> to_giant()
sum(node_is_isolate(blogs))
graphr(blogs)
Finally, we have a single ‘giant’ component to examine. However, now we have a different kind of challenge: everything is one big hairball. And yet, if we think about what we might expect of the structure of a network of political blogs, we might not think it is so undifferentiated. We might hypothesise that, despite the graphical presentation of a hairball, there is actually a reasonable partition of the network into two factions .
Finding a partition
To find a partition in a network, we use the
node_in_partition() function.
Beginner note:
All node_in_*() functions return a string vector the length
of the number of nodes in the network — a string vector because
membership is a categorical result, unlike the numeric vectors the
node_by_*() measures return. Since it is the length of the
nodes, we can assign the result to the nodes as an attribute and graph
with it.
blogs |> mutate_nodes(part = node_in_partition()) |>
graphr(node_color = "part")
We see from this graph that indeed there seems to be an obvious separation between the left and right ‘hemispheres’ of the network. Where the components view saw a single undifferentiated blob, the bipartition recovers a plausible two-camp structure — but how good a description of the network is it really? That question needs a measure of fit, which is where we turn next.
Going further:
net_by_factions() offers a whole-network summary of how
factional a network is: it measures the correlation between the observed
network and an idealised component model with the same dimensions, using
node_in_partition()’s bipartition when no membership is
supplied. See ?measure_features for details.
In brief: When a
network is dominated by isolates and minor components,
to_giant() focuses analysis on the giant component (count
isolates first with sum(node_is_isolate())).
node_in_partition() then splits the network into two
factions, returning a categorical membership vector you can map onto
node_color.
Modularity
On this page: Measuring fit · Given memberships
Measuring fit
But what is the ‘fit’ of this assignment of the blog nodes into two partitions? The most common measure of the fit of a community assignment in a network is modularity .
net_by_modularity(blogs, membership = node_in_partition(blogs))
Remember that modularity is conventionally described as ranging between 1 and -1, though in practice the lowest value a real partition reaches is only about -0.5. How can we interpret this result?
A clearly positive modularity like this one tells us the two-faction reading of the blogosphere is not an artefact of the layout: ties really do fall within the two camps more often than a random rewiring of the network would produce.
Given memberships
While the partition algorithm is useful for deriving a partition of the network into the number of factions assigned, it is still an algorithm that tries to maximise modularity. Other times we might instead have an empirically collected grouping, and we are keen to see how ‘modular’ the network is around this attribute. This only works on categorical attributes, of course, but is otherwise quite flexible.
graphr(blogs, node_color = "Leaning")
net_by_modularity(blogs, membership = node_attribute(blogs, "Leaning"))

How interesting. Perhaps the partitioning algorithm is not the algorithm that maximises modularity after all… The blogs’ own declared political leanings describe the network’s tie pattern better than the bipartition the algorithm found — so there must be room for other solutions that return an even greater modularity criterion.
In brief:
net_by_modularity() scores the fit of any
membership vector — algorithmic (e.g. node_in_partition())
or empirical (e.g. node_attribute()) — as the excess of
within-group ties over a random baseline. Positive values mean the
grouping captures real structure, and competing groupings can be
compared on the same yardstick.
Communities
On this page: Walktrap · Edge betweenness · Fast greedy · Detecting communities
Ok, the friendship network has 3-4 components, but how many ‘groups’ are there? Visually, it looks like there are some denser clusters within the main component.
Today we’ll use the ‘friends’ subgraph for exploring community detection methods. For clarity and simplicity, we will concentrate on the main component (the so-called ‘giant’ component) and consider friendship undirected.
# to_giant() returns an object that includes only the main component without any smaller components or isolates
(friends <- to_giant(friends))
(friends <- to_undirected(friends))
graphr(friends)
Comparing friends before and after these operations,
you’ll notice the number of ties decreases as reciprocated directed ties
are consolidated into single undirected ties, and the number of nodes
decreases as two isolates are removed.
There is no one single best community detection algorithm. Instead
there are several, each with their strengths and weaknesses. Since this
is a rather small network, we’ll focus on the following methods:
walktrap, edge betweenness, and fast greedy. (Others are included in
{netrics}/{igraph}) As you use them, consider
how they portray communities and consider which one(s) afford a sensible
view of the social world as cohesively organized.
Walktrap
This algorithm detects communities through a series of short random walks, with the idea that nodes encountered on any given random walk are more likely to be within a community than not. It was proposed by Pons and Latapy (2005).
The algorithm initially treats all nodes as communities of their own,
then merges them into larger communities, still larger communities, and
so on. In each step a new community is created from two other
communities, and its ID will be one larger than the largest community ID
so far. This means that before the first merge we have n communities
(the number of vertices in the graph) numbered from zero to n-1. The
first merge creates community n, the second community n+1, etc. This
merge history is returned by the function:
# ?igraph::cluster_walktrap
Note the “steps=” argument that specifies the length of the random
walks. While {igraph} sets this to 4 by default, which is
what is recommended by Pons and Latapy, Waugh et al (2009) found that
for many groups (Congresses), these lengths did not provide the maximum
modularity score. To be thorough in their attempts to optimize
modularity, they ran the walktrap algorithm 50 times for each group
(using random walks of lengths 1–50) and selected the network partition
with the highest modularity value from those 50. They call this the
“maximum modularity partition” and insert the parenthetical “(though,
strictly speaking, this cannot be proven to be the optimum without
computationally-prohibitive exhaustive enumeration (Brandes et
al. 2008)).”
So let’s try and get a community classification using the walktrap
algorithm, node_in_walktrap(), with path lengths of the
random walks specified to be 50.
friend_wt <- node_in_walktrap(friends, times=50)
friend_wt # note that it prints pretty, but underlying its just a vector:
c(friend_wt)
# This says that dividing the graph into 3 communities maximises modularity,
# one with the nodes
which(friend_wt == "A")
# another with
which(friend_wt == "B")
# and the last with
which(friend_wt == "C")
# resulting in a modularity of
net_by_modularity(friends, friend_wt)
friend_wt <- node_in_walktrap(friends, times=50)
# results in a modularity of
net_by_modularity(friends, friend_wt)
We can also visualise the clusters on the original network How does the following look? Plausible?
# plot 1: groups by node color
friends <- friends |>
mutate_nodes(walk_comm = friend_wt)
graphr(friends, node_color = "walk_comm")
#plot 2: groups by borders
# to be fancy, we could even draw the group borders around the nodes using the node_group argument
graphr(friends, node_group = "walk_comm")
# plot 3: group and node colors
# or both!
graphr(friends,
node_color = "walk_comm",
node_group = "walk_comm") +
ggtitle("Walktrap",
subtitle = round(net_by_modularity(friends, friend_wt), 3))
# the function `round()` rounds the values to a specified number of decimal places
# here, we are telling it to round the net_by_modularity score to 3 decimal places
friends <- friends |>
mutate_nodes(walk_comm = friend_wt)
graphr(friends, node_color = "walk_comm")
# to be fancy, we could even draw the group borders around the nodes using the node_group argument
graphr(friends, node_group = "walk_comm")
# or both!
graphr(friends,
node_color = "walk_comm",
node_group = "walk_comm") +
ggtitle("Walktrap",
subtitle = round(net_by_modularity(friends, friend_wt), 3))
This can be helpful when polygons overlap to better identify membership Or you can use node color and size to indicate other attributes…
The walktrap solution divides the giant component’s friendships into three communities at a modularity of about 0.41 — comfortably positive, so these clusters hold together much more than a random rewiring would suggest.
Edge betweenness
Edge betweenness is like betweenness centrality but for ties not nodes. The edge-betweenness score of an edge measures the number of shortest paths from one vertex to another that go through it.
The idea of the edge-betweenness based community structure detection is that it is likely that edges connecting separate clusters have high edge-betweenness, as all the shortest paths from one cluster to another must traverse through them. So if we iteratively remove the edge with the highest edge-betweenness score we will get a hierarchical map ( dendrogram ) of the communities in the graph.
The following works similarly to walktrap, but no need to set a step length.
friend_eb <- node_in_betweenness(friends)
friend_eb
How does community membership differ here from that found by walktrap?
We can see how the edge betweenness community detection method works here: http://jfaganuk.github.io/2015/01/24/basic-network-analysis/
To visualise the result:
# create an object
friends <- friends |>
mutate_nodes(eb_comm = friend_eb)
# create a graph with a title and subtitle returning the modularity score
graphr(friends,
node_color = "eb_comm",
node_group = "eb_comm") +
ggtitle("Edge-betweenness",
subtitle = round(net_by_modularity(friends, friend_eb), 3))
friends <- friends |>
mutate_nodes(eb_comm = friend_eb)
graphr(friends,
node_color = "eb_comm",
node_group = "eb_comm") +
ggtitle("Edge-betweenness",
subtitle = round(net_by_modularity(friends, friend_eb), 3))
For more on this algorithm, see M Newman and M Girvan: Finding and evaluating community structure in networks, Physical Review E 69, 026113 (2004), https://arxiv.org/abs/cond-mat/0308217.
On this small network, cutting the bridges between clusters arrives at essentially the same three friendship communities as chasing random walks did — reassuring, since the two algorithms reason about communities in quite different ways.
Fast greedy
This algorithm is the Clauset-Newman-Moore algorithm. Whereas edge betweenness was divisive (top-down), the fast greedy algorithm is agglomerative (bottom-up).
At each step, the algorithm seeks a merge that would most increase modularity. This is very fast, but has the disadvantage of being a greedy algorithm, so it might not produce the best overall community partitioning, although I personally find it both useful and in many cases quite “accurate”.
friend_fg <- node_in_greedy(friends)
friend_fg # Does this result in a different community partition?
net_by_modularity(friends, friend_fg) # Compare this to the edge betweenness procedure
# Again, we can visualise these communities in different ways:
friends <- friends |>
mutate_nodes(fg_comm = friend_fg)
graphr(friends,
node_color = "fg_comm",
node_group = "fg_comm") +
ggtitle("Fast-greedy",
subtitle = round(net_by_modularity(friends, friend_fg), 3))
#
friend_fg <- node_in_greedy(friends)
friend_fg # Does this result in a different community partition?
net_by_modularity(friends, friend_fg) # Compare this to the edge betweenness procedure
# Again, we can visualise these communities in different ways:
friends <- friends |>
mutate_nodes(fg_comm = friend_fg)
graphr(friends,
node_color = "fg_comm",
node_group = "fg_comm") +
ggtitle("Fast-greedy",
subtitle = round(net_by_modularity(friends, friend_fg), 3))
See A Clauset, MEJ Newman, C Moore: Finding community structure in very large networks, https://arxiv.org/abs/cond-mat/0408187
Detecting communities
Lastly, {netrics} includes a function to run through and
find the membership assignment that maximises modularity across any of
the applicable community detection procedures. This is helpfully called
node_in_community(). This function pays attention to the
size and other salient network properties, such as whether it is
directed, weighted, or connected. It either uses the algorithm that
maximises modularity by default (node_in_optimal()) for
smaller networks, or surveys the approximations provided by other
algorithms for the result that maximises the modularity.
node_in_community(friends)
It is important to name which algorithm produced the membership assignment that is then subsequently analysed though.
Going further:
The three algorithms treated here are only the start of the menagerie:
{netrics} also offers node_in_louvain() and
node_in_leiden() (fast multilevel modularity maximisers),
node_in_infomap() (information-flow based),
node_in_spinglass() (simulated annealing),
node_in_fluid() (fixed number of communities), and
node_in_eigen() (spectral). See
?member_community for the full list with definitions and
references.
In brief:
Community detection algorithms cluster nodes by tie density:
node_in_walktrap() via random walks,
node_in_betweenness() divisively by cutting bridging ties,
and node_in_greedy() agglomeratively by
modularity-improving merges. node_in_community() surveys
the applicable algorithms (exhaustively via
node_in_optimal() on small networks) and returns the
assignment that maximises modularity — but always report which algorithm
produced the result you analyse.
Free play

We’ve looked here at the irps_blogs dataset. Now choose
another dataset included in {manynet} (browse them with
table_data()). What is the density? Does it make sense to
investigate reciprocity, transitivity, or equivalence? How can we
interpret the results? How many components are in the network? Is there
a strong factional structure? Which community detection algorithm
returns the highest modularity score, or corresponds best to what is in
the data or what you see in the graph?
If you are not sure where to start, here is one suggestion per flavour, each well suited to the measures from this tutorial:
| Classic (small, tidy) | Fiction (moderate) | Real-world (larger) |
|---|---|---|
ison_dolphins (62 dolphins observed associating off New
Zealand — a classic community-detection benchmark: do the algorithms
recover the two groups the observers saw?) |
fict_lotr (36 Middle-Earth characters and their
interactions — how modular is the fellowship around the
Race attribute compared with detected communities?) |
irps_books (105 co-purchased US politics books with a
Leaning attribute — do purchasing communities align with
political leaning, as the blogs did?) |
irps_books
Summary

Well done – you have completed the tutorial on cohesion and community! Along the way, you have learned to use these functions:
| Function | What it does |
|---|---|
net_nodes(), net_ties() |
counts the nodes and ties in a network |
net_by_density() |
proportion of possible ties present, by the equation for the network’s type |
net_by_reciprocity() |
how often directed ties are returned |
tie_is_reciprocated() |
flags each tie as reciprocated or not, for mapping with
edge_color |
net_by_indegree() |
centralisation of incoming ties, to see how unequal participation is |
net_by_transitivity() |
how often two-paths close into triangles |
to_uniplex(), to_named(),
to_undirected(), to_giant() |
extracts one tie type, adds names, drops direction, keeps the giant component |
as_matrix() |
coerces a network to a matrix, e.g. for manual projection |
to_mode1(), to_mode2() |
projects a two-mode network onto its row or column nodes, with a
similarity option |
tie_weights() |
extracts the tie weights, e.g. of a projection |
net_by_equivalency() |
equivalence/reinforcement measured on the two-mode network itself |
net_by_components() |
number of (strong) components; wrap in to_undirected()
for weak |
node_in_component() |
each node’s component membership |
node_is_isolate() |
flags isolates (sum it to count them) |
delete_nodes() |
removes chosen (e.g. sampled) nodes |
node_in_partition() |
splits a network into two factions |
net_by_factions() |
how factional the network is around a (bi)partition |
net_by_modularity() |
fit of any membership assignment against a random baseline |
node_attribute() |
extracts a nodal attribute, e.g. as an empirical membership |
node_in_walktrap(), node_in_betweenness(),
node_in_greedy() |
community detection via random walks, divisive tie removal, agglomerative merges |
node_in_community() |
surveys applicable algorithms and returns the best assignment by modularity |
mutate_nodes(), mutate_ties() |
adds measures or memberships to the network for graphing |
graphr(..., node_color = , node_group = , edge_color = ) |
maps memberships onto colours and group outlines |
When you are ready, continue with the other {netrics}
tutorials — on position and topology — where further ways of summarising
network structure are introduced. Run run_tute() at the
console to see all available tutorials.
Glossary
Here are some of the terms that we have covered in this tutorial:
- Adhesion
- The minimum number of ties to remove to increase the number of components.
- Agglomerative
- An agglomerative algorithm is one that starts with each node in its own cluster and then merges them.
- Betweenness
- The betweenness centrality of a node is the proportion of shortest paths between all pairs of nodes that pass through that node.
- Clique
- A clique is a set of mutually adjacent nodes.
- Cohesion
- The minimum number of nodes to remove to increase the number of components.
- Community
- A community is a set of nodes more densely connected to one another than to other nodes in the network.
- Component
- A component is a connected subgraph not part of a larger connected subgraph.
- Dendrogram
- A dendrogram is a tree diagram that records the sequences of merges or splits from some, say, hierarchical clustering.
- Density
- The density of a network is the proportion of possible ties that are present.
- Directed
- A directed network is a network where the ties have a direction, from a sender to a receiver.
- Divisive
- A divisive algorithm is one that starts with all nodes in one cluster and then splits them.
- Dyad
- A dyad is a pair of nodes and the ties between them.
- Equivalency
- Equivalency or reinforcement is the proportion of three-paths in a two-mode network that are closed by a fourth tie into a four-cycle.
- Faction
- A faction is one of a fixed number of mutually exclusive groups into which a network is partitioned.
- Giant
- The giant component is the component that includes the most nodes in the network.
- Isolate
- An isolate is a node with degree equal to zero.
- Modularity
- The modularity of a membership assignment is the difference of the cross-cluster ties from the expected value.
- Multiplex
- A network that includes multiple types of tie.
- Network
- A network comprises one or more sets of nodes, one or more sets of ties among them, and potentially some node, tie, or network-level attributes.
- Node
- A node or vertex is an entity or actor within a network.
- Partition
- A partition is a division of the nodes in a network into mutually exclusive groups.
- Projection
- A projection reduces a two-mode network to a one-mode network of shared ties to the other mode among one set of its nodes.
- Reachability
- The ability of reach one node from another in a network.
- Reciprocity
- A measure of how often nodes in a directed network are mutually linked.
- Transitivity
- Triadic closure is where if the connections A-B and A-C exist among three nodes, there is a tendency for B-C also to be formed.
- Triangle
- A cycle of length three in a network.
- Twomode
- A two-mode (or bipartite) network is a network with two different sets of nodes, where ties connect only nodes from different sets, such as people and the events they attend.
- Undirected
- An undirected or line network is one in which tie direction is undefined.
- Walk
- A walk is a sequence of ties that joins a sequence of nodes.
- Weighted
- A weighted network is where the ties have been assigned weights.