---
title: "Position and Equivalence"
author: "by James Hollway"
output:
  learnr::tutorial:
    theme: flatly
runtime: shiny_prerendered
description: >
  This tutorial covers how to identify bridges and measure structural
  holes through constraint, how structural folds differ from holes, how
  to partition a network into structurally, regularly, and automorphically
  equivalent classes, and how to summarise, plot, and interpret the
  resulting blockmodel and reduced graph.
---

```{r pkgs, include = FALSE, eval=TRUE}
library(netrics)
library(autograph)
```

```{r setup, include = FALSE, purl=FALSE, eval=TRUE}
library(learnr)
knitr::opts_chunk$set(echo = FALSE)
library(netrics)
library(autograph)
stocnet_theme("default")
clear_glossary()
learnr::random_phrases_add(language = "fr",
                           praise = c("C'est génial!",
                                      "Beau travail",
                                      "Excellent travail!",
                                      "Bravo!",
                                      "Super!",
                                      "Bien fait",
                                      "Bien joué",
                                      "Tu l'as fait!",
                                      "Je savais que tu pouvais le faire.",
                                      "Ça a l'air facile!",
                                      "C'était un travail de première classe.",
                                      "C'est ce que j'appelle un bon travail!"),
                          encouragement = c("Bon effort",
                                            "Vous l'avez presque maîtrisé!",
                                            "Ça avance bien.",
                                            "Continuez comme ça.",
                                            "Continuez à travailler dur!",
                                            "Vous apprenez vite!",
                                            "Vous faites un excellent travail aujourd'hui."))
learnr::random_phrases_add(language = "en",
                           praise = c("That's brilliant!",
                                      "Great work!",
                                      "Well done!",
                                      "Awesome!"),
                           encouragement = c("Good effort"))
friends <- to_uniplex(ison_algebra, "friends")
social <- to_uniplex(ison_algebra, "social")
tasks <- to_uniplex(ison_algebra, "tasks")
alge <- to_named(ison_algebra)
```

```{r detect-language, context="server-start", purl=FALSE}
sys_locale <- Sys.getlocale("LC_CTYPE")
if (grepl("fr", sys_locale, ignore.case = TRUE)) {
  options(tutorial.language = "fr")
} else {
  options(tutorial.language = "en")
}
```

<style>
html { font-size: 115%; }
.callout {
  border-left: 3px solid #5B9C78;
  padding: 0.1em 1em;
  margin: 1.2em 0;
}
.callout-label { color: #5B9C78; }
.page-toc { font-style: italic; }
.page-toc-link { color: #5B9C78; cursor: pointer; text-decoration: underline; }
.tutorialTitle { color: #5B9C78 !important; }
#tutorial-topic .nav-link { color: #5B9C78 !important; }
.section a:not(.btn) { color: #5B9C78; }
.section a:not(.btn):hover { color: #487E61; }
.btn-tutorial-run,
.btn-tutorial-run:hover,
.btn-tutorial-run:focus,
.btn-tutorial-run:active {
  background-color: #5B9C78;
  border-color: #5B9C78;
  color: #fff;
}
</style>

## Today's target

The earlier tutorials asked how central each node is,
and which cohesive `r gloss("communities","community")` nodes huddle into.
This tutorial asks a different question: what _position_ does each node hold?

Position is a subtle idea, so it is worth pausing on before we start coding.
Two nodes can sit on opposite sides of a network,
never interacting, and never share a single tie —
and yet occupy exactly the _same_ position.
Think of two branch managers in different cities,
or two goalkeepers in different teams:
they never pass the ball to each other,
but each stands in the same relation to the rest of their team.
"Position" is about the _pattern_ of a node's relationships,
not about who its particular neighbours happen to be.

We will approach position from two directions, and take our time over each:

1. First through `r gloss("structural holes","structhole")` and
`r gloss("structural folds","structfold")` —
two competing stories about _where_ in a network innovation happens,
and what it takes for a node to profit from its position.
2. Then through _equivalence_ — grouping nodes that hold similar positions
into classes, or "roles", and summarising how those roles relate to one another
in a `r gloss("blockmodel")` and a `r gloss("reduced graph","reduced")`.
We will meet all three of the classic equivalences:
structural, regular, and automorphic.

<img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMTVtNTJqZmFsdmFpOGRjd24zeHZ1NTM0bDhxbWxrMWdja202NXo1eSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fL1EXX8pQs0dd9zqLR/giphy.gif" alt="gif of rick and morty characters hanging out with themselves"/>

::: {.callout}
<span class="callout-label">**Catching up**:</span>
This tutorial assumes you can already load or make a network in R,
and draw it with `graphr()`.
If any of that is hazy, work through the earlier `{stocnet}` tutorials first:
run `run_tute("Making")` and `run_tute("Manipulating")` for network data,
and `run_tute("Visualising")` for graphing,
or read their static versions on the
[manynet](https://stocnet.github.io/manynet/) and
[autograph](https://stocnet.github.io/autograph/) websites.
It also builds on the `{netrics}` tutorials on centrality
(`run_tute("Centrality")`, for e.g. `node_is_min()` and mapping measures
onto graphs) and community detection (`run_tute("Community")`,
for partitioning nodes into groups).
:::

::: {.callout}
<span class="callout-label">**New to network vocabulary?**:</span>
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:

- [ ] &nbsp; Separate a `r gloss("multiplex")` network into its constituent networks with `to_uniplex()`
- [ ] &nbsp; Identify bridging ties with `tie_is_bridge()` and count them per node with `node_by_bridges()`
- [ ] &nbsp; Measure structural holes with `node_by_constraint()`, and interpret what high and low constraint mean substantively
- [ ] &nbsp; Distinguish structural holes from structural folds, and explain why a fold can constrain a broker rather than free them
- [ ] &nbsp; Distinguish structural, regular, and automorphic equivalence, and compute each with `node_in_structural()`, `node_in_regular()`, and `node_in_automorphic()`
- [ ] &nbsp; Partition a network into equivalent classes, and understand the census, clustering, and _k_-selection choices behind it
- [ ] &nbsp; Read a dendrogram and a blockmodel, and justify a choice of _k_
- [ ] &nbsp; Contract a network into a reduced graph of positions with `to_blocks()`

**Choose your own data**: The worked examples below use `ison_algebra`,
a multiplex network of interactions in an algebra class
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 {#setting-up}

For this session, we're going to use the "ison_algebra" dataset included in the `{manynet}` package.
Do you remember how to call the data?
Can you find out some more information about it via its help file?

**Load the `ison_algebra` dataset, and open its help file to read about it.**
If you get stuck, work through the hints one at a time.

```{r data, exercise = TRUE, purl = FALSE}

```

```{r data-hint-1, purl = FALSE}
# There are two ways to bring a bundled dataset into your session.
# The first is data(), naming the dataset and the package it lives in:
data("ison_algebra", package = "manynet")
```

```{r data-hint-2, purl = FALSE}
# The second is to assign it directly from the package with the :: operator:
ison_algebra <- manynet::ison_algebra
```

```{r data-hint-3, purl = FALSE}
# To open the help file, put a ? in front of the (package-qualified) name:
?manynet::ison_algebra
```

```{r data-solution}
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)
```

From the help file (and by printing the object) we can see that the dataset is
multiplex, meaning that it contains several different _types_ of ties:
friendship (friends), social (social) and task interactions (tasks).
Keeping those three relationships separate, or deliberately combining them,
turns out to matter a lot for the questions in this tutorial,
so that is where we begin.

### Separating multiplex networks {#separating-multiplex-networks}

As a `r gloss("multiplex")` network,
there are actually three different types of ties in this network.
Bundling three relationships into one object is convenient for storage,
but most position measures expect a single kind of tie at a time —
"who advises whom" is a different question from "who is friends with whom".
So our first practical skill is pulling the three relations apart.

We can extract them and investigate them separately using `to_uniplex()`.
Within the parentheses, put the multiplex object's name,
and then as a second argument put the name of the tie attribute in quotation marks.

**Extract all three networks (friends, social, and tasks) with `to_uniplex()`,
then graph each one and give it a descriptive title.**

```{r separatingnets, exercise=TRUE, exercise.setup = "data", purl = FALSE}

```

```{r separatingnets-hint-1, purl = FALSE}
# Here's the basic idea/code syntax you will need to extract each type of network.
# Replace the first blank with a name for the new object,
# and the second with the tie type in quotation marks, e.g. "friends":
____ <- to_uniplex(ison_algebra, _____)
```

```{r separatingnets-hint-2, purl = FALSE}
# So, to extract the friendship network:
friends <- to_uniplex(ison_algebra, "friends")
```

```{r separatingnets-hint-3, purl = FALSE}
# To graph a network and add a title, add a ggtitle() layer with +
graphr(friends) + ggtitle("Friendship")
```

```{r separatingnets-hint-4, purl = FALSE}
# Now do the same for the "social" and "tasks" tie types.
social <- to_uniplex(ison_algebra, "social")
graphr(social) + ggtitle("Social")
```

```{r separatingnets-solution}
friends <- to_uniplex(ison_algebra, "friends")
graphr(friends) + ggtitle("Friendship")

social <- to_uniplex(ison_algebra, "social")
graphr(social) + ggtitle("Social")

tasks <- to_uniplex(ison_algebra, "tasks")
graphr(tasks) + ggtitle("Task")
```

Note also that these are `r gloss("weighted")` networks:
each tie carries a number recording how strong that friendship, social,
or task relationship is.
`graphr()` automatically recognises these different weights and plots them,
drawing stronger ties more heavily.
That weighting matters for the theory we turn to next.

```{r strongties-qa, echo=FALSE, purl = FALSE}
question("If we interpret ties with higher weights as strong ties, and lesser weights as weak ties, then, according to network theory (Granovetter's 'strength of weak ties'), where would we expect novel information to come from?",
         answer("Weak ties",
                correct = TRUE,
                message = paste(learnr::random_praise(), "Weak ties tend to reach beyond our immediate, tightly-knit circle, so they are more likely to bring in information we don't already have.")),
         answer("Strong ties",
                message = "Not quite -- strong ties usually connect us to people who already share our circle (and so our information), which is why Granovetter emphasised the *weak* ones for novelty."),
         answer("Isolates",
                message = "Not quite -- an isolate has no ties at all, so it can neither send nor receive novel information through the network."),
         answer("Highest degree nodes",
                message = "Not quite -- having many ties is not the same as having ties that reach into different parts of the network; a high-degree node may still sit inside a single, redundant cluster."),
  random_answer_order = TRUE,
  allow_retry = TRUE
)
```

::: {.callout}
<span class="callout-label">**In brief**:</span> `to_uniplex()` extracts one type of tie from a
multiplex network as its own (here weighted) network — the position measures
on the following pages each apply to one type of tie at a time, so this is
usually the first step in any positional analysis of multiplex data.
:::

## Structural holes

<span class="page-toc">On this page:
<span class="page-toc-link" onclick="document.getElementById('section-measuring-structural-holes').scrollIntoView({behavior:'auto',block:'start'});">Measures</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-bridges').scrollIntoView({behavior:'auto',block:'start'});">Bridges</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-constraint').scrollIntoView({behavior:'auto',block:'start'});">Constraint</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-the-rest-of-the-family').scrollIntoView({behavior:'auto',block:'start'});">The wider family</span></span>

Our first substantive question for this network is where innovation and
creative ideas might be expected to appear.
Why would network _position_ have anything to do with creativity?
The intuition, developed by Ronald Burt, is about _information_.
If all of your contacts already know each other,
they mostly know the same things, and talking to them tells you little that
is new. But if you are connected to two groups that are _not_ connected to
each other, you hear two different conversations,
and you are uniquely placed to combine ideas from one with problems in the other.

That gap between two otherwise-unconnected parts of a network is a
`r gloss("structural hole","structhole")`,
and a node that spans one is said to be in a brokerage position:
it enjoys early access to non-redundant information,
and control over what passes between the two sides.
Before measuring anything, let's check the core idea is clear.

```{r structinnov-qa, echo=FALSE, purl = FALSE}
question("Which network concepts are associated with innovation?",
         answer("Structural holes",
                correct = TRUE,
                message = "Being positioned in a structural hole is said to be a brokerage position that brings with it information arbitrage possibilities."),
         answer("Structural folds",
                correct = TRUE,
                message = "Being positioned in a structural fold, with in-group membership in multiple groups, can provide not only information arbitrage possibilities, but also the standing in the target group to introduce novelty successfully. (More on this in the next section.)"),
         answer("Structural balance",
                message = "Structural balance is about whether signed (e.g. friend/enemy) ties form stable, tension-free configurations -- a different question from where innovation appears."),
         answer("Structural equivalence",
                message = '<img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmY4ejhpMnZoZmh3OGUxbTdjbXQwMTcwcmg0ZHo5dmJ6NzI3c3FxNyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9dg/2E7VU4zGVm8H1mFSAz/giphy.gif" alt="gif of a morty assassinating another morty"/>'),
         answer("Structuralism",
                message = "Structuralism is a broad intellectual movement, not a specific network mechanism for innovation."),
  random_answer_order = TRUE,
  allow_retry = TRUE
)
```

### Measuring structural holes {#measuring-structural-holes}

The idea of a structural hole is intuitive, but to work with it we need to turn
it into numbers. There is no single "structural hole score";
instead there is a small family of related measures,
each capturing a slightly different facet of the same idea.
Try to reason through which of the following could stand in for the concept.

```{r shmeasures-qa, echo=FALSE, purl = FALSE}
question("There are a number of measures that might be used to approximate the concept of structural holes. Select all that apply.",
         answer("Constraint",
                correct = TRUE,
                message = paste(learnr::random_praise(), "Constraint captures how far a node's contacts are tied to one another -- high constraint means few structural holes to exploit.")),
         answer("Effective size",
                correct = TRUE,
                message = "Yes -- effective size counts a node's *non-redundant* contacts, so it grows as the node spans more structural holes."),
         answer("Bridges",
                correct = TRUE,
                message = "Yes -- a bridge is precisely a tie that spans a structural hole between otherwise disconnected parts of the network."),
         answer("Redundancy",
                correct = TRUE,
                message = "Yes -- redundancy measures how much a node's contacts overlap, so it is the flip side of having structural holes."),
         answer("Efficiency",
                correct = TRUE,
                message = "Yes -- efficiency is effective size relative to degree, i.e. how much structural-hole benefit each contact yields on average."),
         answer("Hierarchy",
                correct = TRUE,
                message = "Yes -- hierarchy captures whether a node's constraint is concentrated in a single dominant contact, another facet of its structural-hole position."),
  random_answer_order = TRUE,
  allow_retry = TRUE
)
```

All of these measures are implemented in `{netrics}`.
Rather than race through all six, we will concentrate on the two most common
entry points — bridges and constraint —
and only then glance at the rest of the family.

### Bridges {#bridges}

One concrete way of thinking about how innovations and ideas flow across
the network is to ask where the _bottlenecks_ are.
A tie that is the sole conduit for information to pass from one part of the
network to another is called a `r gloss("bridge", "bridge")`:
formally, a tie whose removal would increase the number of
`r gloss("components","component")` — that is, cut the network in two.

Remember that only _ties_ can be bridges (a bridge is a kind of tie),
though `{netrics}` also offers a node-based measure that counts how many
bridges each node is adjacent to.
So let's look for bridges in the friends network.
`tie_is_bridge()` returns a logical value for each tie (is it a bridge or not?),
and `node_by_bridges()` returns a count for each node.

**Run the code below to count the bridges in the friends network.**
`sum()` over a logical vector counts the `TRUE`s,
and `any(... > 0)` asks whether _any_ node touches a bridge.

```{r bridges, exercise = TRUE, exercise.setup = "objects-setup"}
sum(tie_is_bridge(friends))
any(node_by_bridges(friends)>0)
```

Both answers come back empty-handed: there are _no_ bridges in the friends
network. That is not a bug — it is a substantive finding.
This friendship network is cohesive enough that no single tie is a lone
conduit; cut any one friendship and information can still flow around another
way. So if bridges can't help us locate brokerage here,
we need a more graded measure. That is where constraint comes in.

### Constraint {#constraint}

Bridges are all-or-nothing: a tie either is one or it isn't.
But some nodes are clearly more deeply _embedded_ in their local
neighbourhood than others, and we would like to measure that by degree.
Burt's `r gloss("constraint", "constraint")` does exactly this.
It asks, for each node: how far are your contacts also tied to _one another_?
The more your contacts form a closed, all-knowing-all triangle around you,
the more "constrained" you are, and the fewer holes you have to exploit.

Let's take a look at which actors are least constrained
by their position in the *task* network —
where advice about the algebra problems flows.
`{netrics}` makes this easy with the `node_by_constraint()` function.

```{r objects-setup}
alge <- to_named(ison_algebra)
friends <- to_uniplex(alge, "friends")
social <- to_uniplex(alge, "social")
tasks <- to_uniplex(alge, "tasks")
```

**Calculate the constraint score of each node in the `tasks` network.**

```{r constraint, exercise = TRUE, exercise.setup = "objects-setup", purl = FALSE}

```

```{r constraint-hint-1, purl = FALSE}
# The function is node_by_constraint(). It takes a network as its argument.
node_by_constraint(____)
```

```{r constraint-hint-2, purl = FALSE}
# Remember we want the *task* network, which we extracted as 'tasks':
node_by_constraint(tasks)
```

```{r constraint-solution}
node_by_constraint(tasks)
```

This function returns a vector of constraint scores that can range between 0 and 1.
Reading a column of numbers is hard, though,
so let's put them back on the graph.
We will size each node by its constraint score,
and additionally pick out the _least_ constrained actor —
the one with the most brokerage potential — with `node_is_min()`,
the minimum-finding cousin of the `node_is_max()` you met in the
centrality tutorial.

**Graph the `tasks` network, sizing nodes by their constraint score and
colouring the least-constrained node.** Build it up through the hints.

```{r constraintplot, exercise=TRUE, exercise.setup = "objects-setup", purl = FALSE}

```

```{r constraintplot-hint-1, purl = FALSE}
# First, attach two new node attributes with mutate():
# the constraint score itself, and a flag for the minimum.
tasks <- tasks %>%
  mutate(constraint = node_by_constraint(____),
         low_constraint = node_is_min(node_by_constraint(____)))
# Don't forget, we are still looking at the 'tasks' network.
```

```{r constraintplot-hint-2, purl = FALSE}
# node_is_min() turns the numeric scores into a TRUE/FALSE flag,
# TRUE for whichever node(s) have the lowest constraint:
tasks <- tasks %>%
  mutate(constraint = node_by_constraint(tasks),
         low_constraint = node_is_min(node_by_constraint(tasks)))
```

```{r constraintplot-hint-3, purl = FALSE}
# Now graph it: map 'constraint' to node_size and 'low_constraint' to node_color.
graphr(____, node_size = "____", node_color = "____")
```

```{r constraintplot-hint-4, purl = FALSE}
graphr(tasks, node_size = "constraint", node_color = "low_constraint")
```

```{r constraintplot-solution}
tasks <- tasks %>%
  mutate(constraint = node_by_constraint(tasks),
         low_constraint = node_is_min(node_by_constraint(tasks)))
graphr(tasks, node_size = "constraint", node_color = "low_constraint")
```

Why look for the _minimum_?
Because constraint measures how well connected each node's partners are,
with the implication that having few partners that are already connected to each
other puts a node in an advantageous position to identify and
share novel solutions to problems.
The lowest-constraint node is therefore the best-placed broker.

It helps to spell out the two extremes substantively:

- A **high** constraint score means a node's contacts mostly know one another.
Whatever one contact knows, the others likely know too,
so the node hears the same information several times over
and has little room to play contacts off against each other.
- A **low** constraint score means the opposite:
the node's contacts are disconnected from one another,
so the node sits across one or more structural holes,
enjoys access to diverse, non-redundant information,
and can broker — choosing what passes between the two sides, and on what terms.

So what can we learn from this plot
about where innovation might occur within this network?

```{r constraintQ, echo=FALSE, purl = FALSE}
question("Do bridges innovate?",
         answer("No",
                correct = TRUE,
                message = paste(learnr::random_praise(), "Ties don't have agency -- people (nodes) do. It is the *broker* sitting in the structural hole who can act on the novel information, not the tie itself.")),
         answer("Yes",
                message = "Bridges can be important conduits for information, but ties do not typically have agency. Nodes do. Nodes in structural holes are called brokers. Nodes that are located in structural holes that would be there but for the node and their ties can be identified through various measures such as the number of bridges they are adjacent to, or their constraint score."),
  random_answer_order = TRUE,
  allow_retry = TRUE
)
```

### The rest of the family {#the-rest-of-the-family}

We flagged four other structural-hole measures in the quiz above,
and they are all one function call away:
`node_by_effsize()` for effective size (a node's non-redundant contacts),
`node_by_redundancy()` for how much a node's contacts overlap,
`node_by_efficiency()` for effective size relative to degree,
and `node_by_hierarchy()` for whether a node's constraint is concentrated
in a single dominant contact.

You do not need to memorise the formulas;
the point is to see that they broadly agree with constraint but each tells a
slightly different part of the story.

**Run a few of these on the task network. Do the least-constrained nodes
also tend to have the largest effective size?** (They should, roughly —
more non-redundant contacts means less constraint.)

```{r shfamily, exercise = TRUE, exercise.setup = "objects-setup"}
node_by_effsize(tasks)
node_by_redundancy(tasks)
node_by_hierarchy(tasks)
```

Broadly, effective size and constraint mirror one another —
nodes with more non-redundant contacts are less constrained —
but they are not interchangeable,
and reporting more than one facet of a node's structural-hole position
is good practice.
For formal definitions and references, see `?measure_broker_node`.

::: {.callout}
<span class="callout-label">**Going further**:</span>
Structural holes are closely related to _brokerage_ as a behaviour:
`node_by_brokering_activity()` and `node_by_brokering_exclusivity()`
measure how often a node sits between its contacts and how exclusively,
and `node_x_brokerage()` counts Gould and Fernandez's five brokerage roles
(coordinator, gatekeeper, representative, itinerant, liaison)
once nodes belong to known groups.
:::

::: {.callout}
<span class="callout-label">**In brief**:</span> Only ties can be `r gloss("bridges","bridge")`:
`tie_is_bridge()` flags them and `node_by_bridges()` counts them per node.
`node_by_constraint()` measures how closed each node's neighbourhood is —
_low_ constraint marks potential brokers spanning structural holes —
and `node_by_effsize()` and friends measure further facets of the same idea.
:::

## Structural folds

<span class="page-toc">On this page:
<span class="page-toc-link" onclick="document.getElementById('section-finding-folds').scrollIntoView({behavior:'auto',block:'start'});">Finding folds</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-ties-that-torture').scrollIntoView({behavior:'auto',block:'start'});">Ties that torture</span></span>

The structural-hole story is a story about _information_:
the broker profits by being the only channel between two worlds.
But there is a well-known objection to it,
which leads to a second, richer story about innovation.

Being the _only_ bridge between two groups gives you access to novel
information — but does it give you the standing to _do_ anything with it?
If you belong to neither group fully, you are an outsider to both,
and an idea carried in by an outsider is easily dismissed.
Burt's broker hears the new idea first; but it may be someone _trusted on both
sides_ who can actually get it adopted.

This is the idea of a `r gloss("structural fold","structfold")`:
not a gap _between_ groups, but an _overlap_ of them.
A node in a structural fold is a genuine, recognised member of two (or more)
cohesive groups at once.
Because it is an insider to both, it enjoys the broker's informational
advantage — it hears two conversations — _and_ the standing to translate a
solution from one group into a problem in the other,
and be taken seriously when it does.
Folds are where information advantage meets the legitimacy to act on it.

```{r foldvshole-qa, echo=FALSE, purl = FALSE}
question("What does a structural fold add to the structural-hole story?",
         answer("Membership: the node is a recognised insider to both groups, giving it the standing to get novel ideas adopted, not just heard.",
                correct = TRUE,
                message = learnr::random_praise()),
         answer("Nothing new -- a fold is just another name for a structural hole.",
                message = "Not quite -- a hole is a *gap between* groups spanned from outside, whereas a fold is an *overlap*: the node belongs to both groups at once."),
         answer("It measures how many ties a node has.",
                message = "Not quite -- that would be degree. A fold is about overlapping *group membership*, not tie count."),
         answer("It guarantees the node has the highest betweenness.",
                message = "Not quite -- betweenness is about lying on shortest paths; a fold is about cohesive membership in more than one group."),
         allow_retry = TRUE,
         random_answer_order = TRUE
)
```

### Finding folds {#finding-folds}

`{netrics}` can look for nodes in structural folds directly,
with `node_is_fold()`, which returns a `TRUE`/`FALSE` flag for each node.
Under the hood it looks for nodes that are cohesively embedded in more than one
group. Let's try it on the (named) algebra network.

**Run `node_is_fold()` on `alge`, and check whether _any_ node is in a fold.**

```{r folds, exercise = TRUE, exercise.setup = "objects-setup"}
node_is_fold(alge)
any(node_is_fold(alge))
```

Just as the friends network had no bridges,
the algebra network turns out to have no structural folds:
every flag comes back `FALSE`.
Again, this is a substantive result, not a failure.
A fold requires two _distinct_ cohesive groups that a node belongs to at once;
in this small, tightly-overlapping class, the groups are not separate enough
for anyone to genuinely straddle two of them.
Folds tend to show up in larger settings —
overlapping project teams, interlocking company boards,
communities of practice — where cohesive groups are real and distinguishable
but some people hold dual membership.

### Ties that torture {#ties-that-torture}

So far both stories have treated an unusual position as an _advantage_:
the broker profits from the hole, the fold-member from the overlap.
But there is a darker side, and it is worth ending this half of the tutorial on.

Krackhardt's "ties that torture" argument turns the fold on its head.
Belonging fully to two cohesive groups does not only grant you two sets of
resources — it also subjects you to two sets of _obligations_.
Each group has its own norms, loyalties, and expectations,
and an insider to both is answerable to both at once.
When those expectations conflict — group A wants what group B forbids —
the person in the fold is caught in the middle,
and may be more constrained, not less.

Notice how neatly this inverts Burt's picture.
For Burt, the person whose contacts are all tied to one another is _constrained_
(recall `node_by_constraint()`): everyone watches everyone, so there is no room
to manoeuvre. Krackhardt points out that the fold-member sits inside not one but
_two_ such dense, watchful groups.
The very embeddedness that gives a fold its legitimacy
can also become a vice that grips.

```{r torture-qa, echo=FALSE, purl = FALSE}
question("According to Krackhardt's 'ties that torture' argument, why might a node in a structural fold be *more* constrained rather than less?",
         answer("It is subject to the norms and obligations of two cohesive groups at once, which may conflict.",
                correct = TRUE,
                message = paste(learnr::random_praise(), "Dual membership means dual accountability -- the same embeddedness that grants legitimacy can also trap the node between competing expectations.")),
         answer("It has too few ties to be influential.",
                message = "Not quite -- the problem isn't too few ties; it is being bound by the (possibly conflicting) obligations of two groups simultaneously."),
         answer("Its constraint score is always exactly zero.",
                message = "Not quite -- the argument is substantive, not about a particular numeric value; the point is that dual embeddedness can be constraining rather than liberating."),
         answer("Structural folds and structural holes are the same thing.",
                message = "Not quite -- they are opposites: a hole is a gap spanned from outside, a fold is an overlap of memberships."),
         allow_retry = TRUE,
         random_answer_order = TRUE
)
```

::: {.callout}
<span class="callout-label">**Going further**:</span>
The tension here — position as opportunity versus position as obligation —
runs right through network theory.
The same overlap that Vedres and Stark celebrate as a fold (a source of
generative friction and innovation) is what Krackhardt warns can torture.
Which reading applies is often an empirical question about whether the two
groups' norms _reinforce_ or _contradict_ one another.
:::

::: {.callout}
<span class="callout-label">**In brief**:</span> A `r gloss("structural hole","structhole")` is a
_gap between_ groups, spanned from outside for information advantage;
a `r gloss("structural fold","structfold")` is an _overlap of_ groups,
occupied from inside for information advantage _plus_ the standing to act.
`node_is_fold()` flags fold members.
Krackhardt's "ties that torture" is the reminder that dual membership brings
dual obligations, and can constrain rather than liberate.
:::

## Structural equivalence

<span class="page-toc">On this page:
<span class="page-toc-link" onclick="document.getElementById('section-finding-classes').scrollIntoView({behavior:'auto',block:'start'});">Finding classes</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-starting-with-a-census').scrollIntoView({behavior:'auto',block:'start'});">Census</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-growing-a-tree-of-similarity').scrollIntoView({behavior:'auto',block:'start'});">Dendrograms</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-identifying-the-number-of-clusters').scrollIntoView({behavior:'auto',block:'start'});">Choosing k</span></span>

<img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGhvaTZ2Z3NxOW0yeTJ6ZjVoZ2ZqM2NqMjlxbXp1YnBmb2VtdTBldiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XcFqv8EzBiAlzeEID9/giphy.gif" alt="gif of rick multiplying"/>

We now switch from asking about _individual_ positions (who is a broker?)
to asking about _shared_ positions: which nodes play the same kind of role?
Grouping nodes that occupy similar positions into classes is called finding
_equivalence_ classes, and there are three classic definitions of what "similar
position" means. We will build up all three, but start with the strictest.

Two nodes are `r gloss("structurally equivalent", "structeq")` if they have the
_same tie partners_ — they are connected to (and from) exactly the same others.
Think of two assistants who both report to the same three managers:
they need not know each other, but their ties point to the same people.
Let's make sure the definition is clear before we compute it.

```{r equiv-qa, echo=FALSE, purl = FALSE}
question("Structural equivalence means identifying classes of nodes with...",
         answer("same/similar tie partners.",
                correct = TRUE,
                message = learnr::random_praise()),
         answer("same/similar pattern of ties.",
                message = "Close, but that is the definition for *regular* equivalence -- equivalent nodes tie to equivalent others, not necessarily the *same* others. We'll get to it."),
         answer("same/similar distance from all others.",
                message = "Not quite -- that is closer to *automorphic* equivalence (interchangeable positions). We'll get to it too."),
         allow_retry = TRUE,
         random_answer_order = TRUE
)
```

We're going to identify structurally equivalent positions
across all the data that we have, including 'task', 'social', and 'friend' ties.
So that is, we are using the multiplex `ison_algebra` dataset again and not
a uniplex subgraph thereof —
a node's "role" here draws on all three kinds of relationship at once.

### Finding structurally equivalent classes {#finding-classes}

In `{netrics}`, finding how the nodes of a network can be partitioned
into structurally equivalent classes can be as easy as one function call,
`node_in_structural()`, which returns a class label for each node.

**Run the code below to compute the structural-equivalence classes and colour
the graph by them.**

```{r find-se, exercise = TRUE, exercise.setup = "data"}
node_in_structural(ison_algebra)

ison_algebra %>%
  mutate(se = node_in_structural(ison_algebra)) %>%
  graphr(node_color = "se")
```

That is all it takes to _get_ an answer.
But a lot is going on behind the scenes,
and understanding it is what separates running the function from being able to
interpret and defend its results.
So over the next three subsections we will slow right down and unpack the three
steps `node_in_structural()` performs for us:
(1) taking a _census_ of each node's ties, (2) building a _dendrogram_ of how
similar those censuses are, and (3) _cutting_ that tree into a chosen number of
classes.

### Step one: starting with a census {#starting-with-a-census}

All equivalence classes are based on nodes' similarity across some profile of
`r gloss("motifs","motif")` — small, countable local structures.
In `{netrics}`, we call these motif *censuses*.
Any kind of census can be used, and `{netrics}` includes a few options,
but `node_in_structural()` is based off of the census of all the nodes' ties,
both outgoing and incoming, to characterise their relationships to tie partners.
The function that produces this tie census is `node_x_tie()`.

**Produce the tie census of `ison_algebra`, and find its dimensions with
`dim()`.** What do the numbers of rows and columns represent?

```{r construct-cor, exercise = TRUE, exercise.setup = "data", purl = FALSE}

```

```{r construct-cor-hint-1, purl = FALSE}
# node_x_tie() takes the network as its argument:
node_x_tie(____)
```

```{r construct-cor-hint-2, purl = FALSE}
node_x_tie(ison_algebra)
```

```{r construct-cor-hint-3, purl = FALSE}
# Wrap the census in dim() to get its number of rows and columns:
dim(node_x_tie(ison_algebra))
```

```{r construct-cor-solution}
node_x_tie(ison_algebra)
dim(node_x_tie(ison_algebra))
```

We can see that the result is a matrix of 16 rows and 96 columns,
because we want to catalogue or take a census of all the different
incoming/outgoing partners our 16 nodes might have across these three networks.
(16 nodes $\times$ 2 directions $\times$ 3 tie types $= 96$ columns.)
Two nodes with identical rows here are structurally equivalent.

Note also that the result is a weighted matrix;
what would you do if you wanted it to be binary (just present/absent)?

**Turn the tie census into a binary one — try it yourself before peeking.**

```{r construct-binary, exercise = TRUE, exercise.setup = "data", purl = FALSE}

```

```{r construct-binary-hint-1, purl = FALSE}
# One way: compare the census to 0 to get TRUE/FALSE, then add 0 to get 1/0.
as.matrix((node_x_tie(ison_algebra)>0)+0)
```

```{r construct-binary-hint-2, purl = FALSE}
# A tidier way: simplify the network first by dropping the tie-type distinction,
# then take the census.
ison_algebra %>%
  select_ties(-type) %>%
  node_x_tie()
```

```{r construct-binary-solution}
# But it's easier to simplify the network by removing the classification into different types of ties.
# Note that this also reduces the total number of possible paths between nodes
ison_algebra %>%
  select_ties(-type) %>%
  node_x_tie()
```

Note that `node_x_tie()` does not need to be passed to `node_in_structural()` ---
this is done automatically!
However, the more generic `node_in_equivalence()` is available and can be used
with whichever census (`node_x_*()` output) is desired.
Feel free to explore using some of the other censuses available in `{netrics}`,
though some common ones are already used in the other equivalence convenience functions,
e.g. `node_x_triad()` in `node_in_regular()`
and `node_x_path()` in `node_in_automorphic()` —
functions we will actually use later in this tutorial.

### Step two: growing a tree of similarity {#growing-a-tree-of-similarity}

The next part takes this census and creates a `r gloss("dendrogram")` based on
distance or dissimilarity among the nodes' census profiles.
This is all done internally within e.g. `node_in_structural()`,
though there are two important parameters that can be set to obtain different results.

First, users can set the type of distance measure used.
For enthusiasts, this is passed on to `stats::dist()`,
so that help page should be consulted for more details.
By default `"euclidean"` is used.

Second, we can also set the type of clustering algorithm employed.
By default, `{netrics}`'s equivalence functions use `r gloss("hierarchical clustering","hierclust")`, `"hier"`,
but for compatibility and enthusiasts, we also offer `"concor"`,
which implements a CONCOR (CONvergence of CORrelations) algorithm.

We can see the difference from varying the clustering algorithm and/or distance
by plotting the dendrograms (hidden) in the output from `node_in_structural()`.

**Run the code to plot three dendrograms — varying first the distance, then the
algorithm — and watch how the tree changes.**

```{r varyclust, exercise = TRUE, exercise.setup = "data"}
alge <- to_named(ison_algebra) # fake names to make comparison clearer
plot(node_in_structural(alge, cluster = "hier", distance = "euclidean"))

# changing the type of distance used
plot(node_in_structural(alge, cluster = "hier", distance = "manhattan"))

# changing the clustering algorithm
plot(node_in_structural(alge, cluster = "concor", distance = "euclidean"))
```

```{r scale-interp, echo = FALSE, purl = FALSE}
question("Do you see any differences?",
         answer("Yes", correct = TRUE,
                message = paste(learnr::random_praise(), "The dendrograms shift as you change the distance measure and the clustering algorithm -- which is exactly why it matters to report which options you used.")),
         answer("No",
                message = "Look again -- changing the distance (euclidean vs. manhattan) and especially the algorithm (hierarchical vs. CONCOR) reshapes the dendrogram and can regroup the nodes."),
        allow_retry = TRUE)
```

So plotting a `membership` vector from `{netrics}` returns a dendrogram
with the names of the nodes on the _y_-axis and the distance between them on the _x_-axis.
Using the census as material, the distances between the nodes
is used to create a dendrogram of (dis)similarity among the nodes.
Basically, as we move to the right, we're allowing for
more and more dissimilarity among those we cluster together.
A fork or branching point indicates the level of dissimilarity
at which those two or more nodes would be said to be equivalent.
Where two nodes' branches join/fork represents the maximum distance among all their leaves,
so more similar nodes' branches fork closer to the tree's canopy,
and less similar (groups of) nodes don't join until they form basically the trunk.

Note that with the results using the hierarchical clustering algorithm,
the distance directly affects the structure of the tree (and the results).

The CONCOR dendrogram operates a bit differently to hierarchical clustering though.
Instead it represents how converging correlations repeatedly bifurcate
the nodes into one of two partitions.
As such the 'distance' is really just the (inverse) number of steps
of bifurcations until nodes belong to the same class.

::: {.callout}
<span class="callout-label">**Beginner note**:</span>
To read a dendrogram, pick a vertical line (a cut-point) and slide it
from right to left: every branch it crosses becomes its own cluster,
containing exactly the leaves (nodes) that branch reaches.
The further right you cut, the fewer and more internally varied the clusters;
the further left, the more numerous and strict.
Since the tree's shape depends on the distance measure and clustering
algorithm you chose, always report those choices alongside your results.
:::

### Step three: identifying the number of clusters {#identifying-the-number-of-clusters}

Another bit of information represented in the dendrogram
is where the tree should be cut (the dashed red line) and
how the nodes are assigned to the branches (clusters) present at that cut-point.

But where does this red line come from?
Or, more technically, how do we identify the number of clusters
into which to assign nodes?

`{netrics}` includes several different ways of establishing `k`,
or the number of clusters.
Remember, the further to the right the red line is
(the lower on the tree the cut point is)
the more dissimilar we're allowing nodes in the same cluster to be.
The simplest option is to set `k` ourselves by just passing it an integer,
say `k = 2`.
What we are saying here is that we want to partition the nodes
into two clusters, no matter how dissimilar they are.

**Run the code to force a two-cluster solution, and see where the red line lands.**

```{r k-discrete, exercise = TRUE, exercise.setup = "varyclust"}
plot(node_in_structural(to_named(ison_algebra), k = 2))
```

But we're really just guessing. Maybe 2 is not the best `k`?
To establish what the best `k` is for this clustering exercise,
we need to iterate through a number of potential `k`
and consider their fitness by some metric.
There are a couple of options here.

One is to consider, for each `k`,
how correlated this partition is with the observed network.
When there is one cluster for each vertex in the network,
cell values will be identical to the observed correlation matrix,
and when there is one cluster for the whole network,
the values will all be equal to the average correlation
across the observed matrix.
So the correlations in each by-cluster matrix are correlated with the observed
correlation matrix to see how well each by-cluster matrix fits the data.

Of course, the perfect partition would then be
where all nodes are in their own cluster,
which is hardly 'clustering' at all.
Also, increasing `k` will always improve the correlation.
But if one were to plot these correlations as a line graph,
then we might expect there to be a relatively rapid increase
in correlation as we move from, for example, 3 clusters to 4 clusters,
but a relatively small increase from, for example, 13 clusters to 14 clusters.
By identifying the inflection point in this line graph,
`{netrics}` selects a number of clusters that represents a trade-off
between fit and parsimony.
This is the `k = "elbow"` method.

The other option is to evaluate a candidate for `k` based
not on correlation but on a metric of
how similar each node in a cluster is to others in its cluster
_and_ how dissimilar each node is to those in a neighbouring cluster.
When averaged over all nodes and all clusters,
this provides a 'silhouette coefficient' for a candidate of `k`.
Choosing the number of clusters that maximizes this coefficient,
which is what `k = "silhouette"` does,
can return a somewhat different result to the elbow method.

**Compare the elbow and silhouette solutions, holding everything else the same.**

```{r elbowsil, exercise = TRUE, exercise.setup = "varyclust"}
plot(node_in_structural(to_named(ison_algebra), k = "elbow"))
plot(node_in_structural(to_named(ison_algebra), k = "silhouette"))
```

Ok, so it looks like the elbow method returns `k == 3` as a good trade-off
between fit and parsimony.
The silhouette method, by contrast, sees `k == 4` as maximising cluster
similarity and dissimilarity.
Either is probably fine here,
and there is much debate around how to select the number of clusters anyway.
However, the silhouette method seems to do a better job of identifying
how unique the 16th node is.
The silhouette method is also the default in `{netrics}`.

Note that there is a somewhat hidden parameter here, `range`.
Since testing across all possible numbers of clusters can get
computationally expensive (not to mention uninterpretable) for large networks,
`{netrics}` only considers up to 8 clusters by default.
This however can be modified to be higher or lower, e.g. `range = 16`.

Finally, one last option is `k = "strict"`,
which only assigns nodes to the same partition
if there is zero distance between them.
This is quick and rigorous solution,
however oftentimes this misses the point in finding clusters of nodes that,
despite some variation, can be considered as similar on some dimension.

**Try the strict solution. How many clusters does it produce, and why?**

```{r strict, exercise = TRUE, exercise.setup = "varyclust"}
plot(node_in_structural(to_named(ison_algebra), k = "strict"))
```

Here for example, no two nodes have precisely the same tie-profile,
otherwise their branches would join/fork at a distance of 0.
As such, `k = "strict"` partitions the network into 16 clusters.
Where networks have a number of nodes with strictly the same profiles,
such a k-selection method might be helpful to recognise nodes in exactly
the same structural position,
but here it essentially just reports nodes' identity.

::: {.callout}
<span class="callout-label">**In brief**:</span> `node_in_structural()` partitions nodes into
structurally equivalent classes in three steps:
a tie census (`node_x_tie()`), a dendrogram built from the distances between
census profiles (`cluster = "hier"` or `"concor"`,
`distance = "euclidean"` or others),
and a cut-point choosing the number of clusters
(`k = ` an integer, `"strict"`, `"elbow"`, or the default `"silhouette"`).
:::

## Blockmodelling

<span class="page-toc">On this page:
<span class="page-toc-link" onclick="document.getElementById('section-summarising-profiles').scrollIntoView({behavior:'auto',block:'start'});">Summarising profiles</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-plotting-blockmodels').scrollIntoView({behavior:'auto',block:'start'});">Plotting blockmodels</span></span>

<img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExeHVzdGlhOGkyOWg2Z3BnMmE2eDgxMnlhZHhwaWx0bGZ3dTNmZnYyaSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dsKkAGVnds7aXQKBI7/giphy.gif" alt="gif of mortys in a block parade"/>

### Summarising profiles {#summarising-profiles}

Ok, so now we have a result from establishing nodes' membership in
structurally equivalent classes.
We can graph this of course, as we did above.

**Colour the graph by structural-equivalence class.**

```{r strplot, exercise = TRUE, exercise.setup = "varyclust"}
alge <- to_named(ison_algebra)
alge %>%
  mutate(se = node_in_structural(alge)) %>%
  graphr(node_color = "se")
```

While this plot adds the structurally equivalent classes information to our
earlier graph, it doesn't really help us understand how the classes _relate_.
That is, we might be less interested in how the individuals in the different
classes relate, and more interested in how the different classes relate in aggregate.
This aggregation of a network into the relationships _between_ classes is called
`r gloss("blockmodelling","blockmodel")`, and it is the pay-off of all the
equivalence work we just did.

<img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmRlczhzaDJzaTBma3Q0c3pydDJjZTFkY20yMmhzbW13YmU3cnl6ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9dg/QIEol1DjEwSOc9PvLu/giphy.gif" alt="gif of mortys learning about roles"/>

One option that can be useful for characterising what
the profile of ties (partners) is for each position/equivalence class
is to use `summary()`.
It summarises some census result by a partition (equivalence/membership) assignment.
By default it takes the average of ties (values),
but this can be tweaked by assigning some other summary statistic as `FUN = `.

**Summarise the tie census by the structural-equivalence membership.**

```{r summ, exercise = TRUE, exercise.setup = "strplot", purl = FALSE}

```

```{r summ-hint-1, purl = FALSE}
# Wrap node_x_tie() inside summary(), and pass a membership result.
# The census goes first, then membership = a membership vector.
summary(node_x_tie(____),
        membership = ____)
```

```{r summ-hint-2, purl = FALSE}
# Use the named algebra network 'alge' for both:
summary(node_x_tie(alge),
        membership = node_in_structural(alge))
```

```{r summ-solution}
summary(node_x_tie(alge),
        membership = node_in_structural(alge))
```

This node census produces 96 columns,
$16 \text{nodes} * 2 \text{directions} * 3 \text{edge types}$,
so it takes a bit to look through what varies between the different classes
as 'blocked'.
But there are only four rows (the four structurally equivalent classes,
according to the default) — a big reduction from sixteen individuals.

### Plotting blockmodels {#plotting-blockmodels}

Reading the summary table is hard work; a picture is easier.
Another way to characterise the classes is to plot the
`r gloss("blockmodel")` as a whole.
Passing the `plot()` function an adjacency/incidence matrix
along with a membership vector allows the matrix to be sorted and framed
(without the membership vector, just the adjacency/incidence matrix is plotted).

**Plot the blockmodel for the whole network, then for each tie type separately.**

```{r block, exercise = TRUE, exercise.setup = "strplot", purl = FALSE}

```

```{r block-hint-1, purl = FALSE}
# Use the same plot() you used for the dendrograms, but pass it a matrix.
# as_matrix() turns the network into its adjacency matrix.
plot(as_matrix(____),
     membership = ____)
```

```{r block-hint-2, purl = FALSE}
# For the whole network:
plot(as_matrix(alge),
     membership = node_in_structural(alge))
```

```{r block-hint-3, purl = FALSE}
# For a single tie type, extract it first with to_uniplex(),
# but keep the membership from the whole network so the blocks line up:
plot(as_matrix(to_uniplex(alge, "friends")),
     membership = node_in_structural(alge))
```

```{r block-solution}
# plot the blockmodel for the whole network
plot(as_matrix(alge),
     membership = node_in_structural(alge))

# plot the blockmodel for the friends, tasks, and social networks separately
plot(as_matrix(to_uniplex(alge, "friends")),
     membership = node_in_structural(alge))
plot(as_matrix(to_uniplex(alge, "tasks")),
     membership = node_in_structural(alge))
plot(as_matrix(to_uniplex(alge, "social")),
     membership = node_in_structural(alge))
```

By passing the membership argument our structural equivalence results,
the matrix is re-sorted to cluster or 'block' nodes from the same class together.
This can help us interpret the general relationships between classes.

::: {.callout}
<span class="callout-label">**Beginner note**:</span>
To read a blockmodel plot: rows are senders and columns receivers,
with the nodes reordered so that members of the same class sit next to
one another, and lines marking the class boundaries.
Each rectangle of cells (a 'block') then shows how one class relates
to another: a dark, dense block means most members of the row class send
(strong) ties to members of the column class, an empty block means they
rarely or never do, and the blocks on the diagonal show ties _within_ each
class.
:::

For example, when we plot the friends, tasks, and social networks using the
structural equivalence results, we might characterise the four classes like so:

- The first group (of 6) are a bit of a mix: there seem to be two popular friends,
one that strongly reciprocates and the other that nominates no friendships but
seems to nominate others in the group as social contacts instead.
The first group rely heavily on the nerd for advice.
- The second group (of 5) seem to be strongly reciprocal in friendship and
social together, lightly advise each other but mostly go to the nerd for advice.
- The third group (of 4) are also strongly reciprocal in friendship,
but also sometimes nominate some in groups one and two as friends too.
There is at least a pair that often hang out together socially,
but this group do not hang out with the nerd much nor ask them for advice
as much as members of the other groups.
- The nerd is a loner, no friends,
but everyone hangs out with them for task advice.

Reading a blockmodel like this — turning blocks of density into a sentence about
each role — is the interpretive skill this whole section is building towards.

```{r blockinterp-qa, echo=FALSE, purl = FALSE}
question("In a blockmodel plot sorted by class, what does an empty (white) off-diagonal block between class A (rows) and class B (columns) tell you?",
         answer("Members of class A rarely or never send ties to members of class B.",
                correct = TRUE,
                message = learnr::random_praise()),
         answer("Classes A and B are the same class.",
                message = "Not quite -- an empty block is about the *absence of ties* from one class to another, not about the classes being identical. Identical classes wouldn't be drawn as two separate blocks."),
         answer("Every member of class A sends a tie to every member of class B.",
                message = "That would be a full, dark block -- the opposite of an empty one."),
         answer("Class A has no members.",
                message = "Not quite -- an empty block still has rows (members); it just records that those members don't tie to class B."),
         allow_retry = TRUE,
         random_answer_order = TRUE
)
```

::: {.callout}
<span class="callout-label">**In brief**:</span> Once you have a membership vector,
`summary(node_x_tie(net), membership = )` averages each class's tie profile,
and `plot(as_matrix(net), membership = )` draws the blockmodel:
the adjacency matrix sorted into blocks whose density (or emptiness)
characterises how each class relates to each other class.
:::

## Reduced graphs

<span class="page-toc">On this page:
<span class="page-toc-link" onclick="document.getElementById('section-from-blocks-to-ties').scrollIntoView({behavior:'auto',block:'start'});">From blocks to ties</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-naming-positions').scrollIntoView({behavior:'auto',block:'start'});">Naming positions</span></span>

The blockmodel plot showed us the classes as sorted blocks of a matrix.
The final step is to treat those classes as nodes in their own right —
to draw the network _of roles_.
Let's use the 4-cluster solution on the valued network (though binary is possible too)
to create a `r gloss("reduced graph","reduced")`.
A reduced graph is a transformation of a network such that
the nodes are no longer the individual nodes but the groups of one or more nodes as a class,
and the ties between these blocked nodes can represent the sum or average tie between these classes.
Of course, this means that there can be self-ties or loops,
because even if the original network was simple (not `r gloss("complex")`),
any within-class ties will end up becoming loops and thus the network will be complex.

### From blocks to ties {#from-blocks-to-ties}

`to_blocks()` carries out this contraction:
pass it the network and a membership vector,
and it returns a matrix with one row and column per _class_,
where each cell holds the average tie (weight) from one class to another.

**Contract the algebra network into its four structural-equivalence blocks.**

```{r structblock, exercise = TRUE, exercise.setup = "varyclust", warning=FALSE}
(bm <- to_blocks(alge, node_in_structural(alge)))
```

Notice how this is just a compact, numerical version of the blockmodel plot
from the previous page:
each cell summarises a whole block of the sorted matrix,
and the diagonal records the average tie within each class.
Sixteen numbers now stand in for a sixteen-node network.

### Naming positions {#naming-positions}

Since the reduced graph is a network like any other,
we can convert it, name its (class-)nodes something more evocative,
and graph it.

**Turn the block matrix into a graph, give the four positions names, and plot it.**

```{r reducedgraph, exercise = TRUE, exercise.setup = "structblock", warning=FALSE}
bm <- bm %>% as_tidygraph %>%
  mutate(name = c("Freaks", "Squares", "Nerds", "Geek"))
graphr(bm)
```

In the reduced graph, the loops show which positions have (on average, strong)
ties among their own members,
while the ties between the class-nodes summarise the relationships we read
off the blockmodel on the previous page —
for instance, every position sends ties towards the "Geek",
the loner everyone consults for task advice.
A reduced graph like this is the pay-off of a positional analysis:
a network of sixteen individuals compressed into a comprehensible
structure of four roles.

::: {.callout}
<span class="callout-label">**In brief**:</span> `to_blocks()` contracts a network into its classes,
yielding a `r gloss("reduced graph","reduced")` whose nodes are positions and
whose ties (including loops) are the average ties within and between classes —
a role-level summary of the whole network.
:::

## Comparing equivalences

<span class="page-toc">On this page:
<span class="page-toc-link" onclick="document.getElementById('section-regular-equivalence').scrollIntoView({behavior:'auto',block:'start'});">Regular</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-automorphic-equivalence').scrollIntoView({behavior:'auto',block:'start'});">Automorphic</span> ·
<span class="page-toc-link" onclick="document.getElementById('section-which-equivalence-when').scrollIntoView({behavior:'auto',block:'start'});">Which when?</span></span>

Structural equivalence was strict: it demanded the _same_ tie partners.
That is often too strict for thinking about _roles_.
Consider two shop managers in different towns:
they supervise different staff and answer to different regional bosses,
so they share no tie partners at all and are _not_ structurally equivalent —
yet intuitively they play the same role.
The other two classic equivalences relax the definition to capture exactly this.

Helpfully, `ison_algebra` lets us compute all three and compare them directly,
because `{netrics}` provides `node_in_regular()` and `node_in_automorphic()`
alongside `node_in_structural()`, all built on the same
census → dendrogram → cut machinery you unpacked earlier —
only starting from a different census.

```{r threeq-qa, echo=FALSE, purl = FALSE}
question("Match the intuition to the equivalence. Which statement describes *regular* equivalence?",
         answer("Nodes tie to equivalent others -- the same *pattern* of ties, not necessarily to the same people.",
                correct = TRUE,
                message = paste(learnr::random_praise(), "Two managers who each supervise their own (different) team are regularly equivalent.")),
         answer("Nodes have exactly the same tie partners.",
                message = "That is *structural* equivalence -- the strictest of the three."),
         answer("Nodes can be swapped without changing the network's structure.",
                message = "That is *automorphic* equivalence -- interchangeable positions."),
         allow_retry = TRUE,
         random_answer_order = TRUE
)
```

### Regular equivalence {#regular-equivalence}

Two nodes are `r gloss("regularly equivalent","regulareq")` if they have similar
_patterns_ of ties — they connect to _equivalent_ others,
not necessarily to the _same_ others.
This is the most permissive of the three definitions,
and usually the closest to the everyday idea of a social "role":
all teachers relate to _some_ students, all students to _some_ teacher,
regardless of which particular ones.
Under the hood, `node_in_regular()` uses a triad census (`node_x_triad()`)
rather than the tie census, because it cares about the shape of local
neighbourhoods rather than their exact membership.

**Compute the regular-equivalence classes for `alge`, plot the dendrogram,
and colour the graph by class.**

```{r regular, exercise = TRUE, exercise.setup = "varyclust"}
node_in_regular(alge)
plot(node_in_regular(alge))

alge %>%
  mutate(re = node_in_regular(alge)) %>%
  graphr(node_color = "re")
```

Regular equivalence lumps almost everyone into a single broad role and splits
off just one node.
Substantively, that lone node is the one whose _pattern_ of relating is
genuinely distinct — the task hub everyone consults but who consults no one back
— while everyone else, despite differing in exactly _whom_ they tie to,
occupies much the same kind of position.

### Automorphic equivalence {#automorphic-equivalence}

Two nodes are `r gloss("automorphically equivalent","automorphiceq")` if they
can be swapped without changing the structure of the network —
if you relabelled one as the other, the network would look identical.
This sits _between_ structural and regular equivalence in strictness:
it does not require the same partners (as structural does),
but it does require the same _structural surroundings_,
which is more demanding than regular equivalence's "equivalent others".
`node_in_automorphic()` uses a path census (`node_x_path()`),
summarising each node by the distances to everyone else.

**Compute the automorphic-equivalence classes for `alge`, plot the dendrogram,
and colour the graph by class.**

```{r automorphic, exercise = TRUE, exercise.setup = "varyclust"}
node_in_automorphic(alge)
plot(node_in_automorphic(alge))

alge %>%
  mutate(ae = node_in_automorphic(alge)) %>%
  graphr(node_color = "ae")
```

Automorphic equivalence here carves the network into many small classes:
requiring identical structural surroundings is demanding,
so few nodes qualify as truly interchangeable and most end up on their own.

### Which equivalence when? {#which-equivalence-when}

It is tempting to line the three up as rungs of one ladder,
and _in theory_ they do nest:
every structurally equivalent pair is also automorphically equivalent,
and every automorphically equivalent pair is also regularly equivalent —
so structural is strictest, regular loosest.

**Run all three side by side and compare how many classes each finds.**

```{r compareq, exercise = TRUE, exercise.setup = "varyclust"}
node_in_structural(alge)
node_in_regular(alge)
node_in_automorphic(alge)
```

In practice, though, the class _counts_ you get here will not line up neatly
with that theoretical nesting.
Each function uses a different census (ties, triads, paths)
and independently chooses its own number of clusters via the silhouette method,
so the raw counts reflect those algorithmic choices as much as the underlying
theory.
The lesson is to treat structural, regular, and automorphic equivalence as three
different _lenses_ on position — a strict one, a role-based one, and an
interchangeability one — rather than three interchangeable rungs.
Which you reach for depends on your question:
"who has identical contacts?" (structural),
"who plays the same kind of role?" (regular),
or "who is structurally interchangeable?" (automorphic).

```{r whichq-qa, echo=FALSE, purl = FALSE}
question("You want to identify which employees play the 'regional manager' role in a company network, regardless of which particular staff or head office they connect to. Which equivalence best fits?",
         answer("Regular equivalence",
                correct = TRUE,
                message = paste(learnr::random_praise(), "Roles defined by *pattern* of ties -- relating to some staff below and some office above -- are exactly what regular equivalence captures.")),
         answer("Structural equivalence",
                message = "Too strict here -- it would require the managers to share the *same* staff and office, which regional managers in different regions do not."),
         answer("Automorphic equivalence",
                message = "Closer, but it demands identical structural surroundings; the everyday notion of the same *role* across different regions is better matched by regular equivalence."),
         allow_retry = TRUE,
         random_answer_order = TRUE
)
```

::: {.callout}
<span class="callout-label">**In brief**:</span> `node_in_structural()` (same partners, via a
tie census), `node_in_regular()` (same pattern / role, via a triad census),
and `node_in_automorphic()` (interchangeable, via a path census) are three
lenses on position, from strictest to loosest in theory —
though each chooses its own _k_, so their class counts won't nest neatly in
practice.
:::

## Free play

<img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExejh4dXBha3JhNW0yenJwMDd5aGpwd3Brc201ZXoxZnY2aWFna3Z4cCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/7ROgX60TtTVK0mZwkJ/giphy.gif" height="400px"/>

Now it's your turn, with a fresh network.

**Find the regularly equivalent classes in the `ison_lawfirm` dataset.**
As this is a multiplex network, you may want to make it a uniplex network first
(remember `to_uniplex()`), then apply `node_in_regular()` and colour a graph by
the result.
**As an extension, also compute the structurally and automorphically equivalent
classes and compare all three** — do they agree on who plays which role?

```{r freeplay, exercise = TRUE, purl = FALSE}

```

```{r freeplay-hint-1, purl = FALSE}
# Load the data and peek at its tie types (it is multiplex):
data("ison_lawfirm", package = "manynet")
ison_lawfirm
```

```{r freeplay-hint-2, purl = FALSE}
# Extract one tie type, e.g. "friends", and find its regular-equivalence classes:
lawfirm_friends <- to_uniplex(ison_lawfirm, "friends")
node_in_regular(lawfirm_friends)
```

```{r freeplay-hint-3, purl = FALSE}
# Colour a graph by the classes, and compare with the other two equivalences:
lawfirm_friends %>%
  mutate(re = node_in_regular(lawfirm_friends)) %>%
  graphr(node_color = "re")
node_in_structural(lawfirm_friends)
node_in_automorphic(lawfirm_friends)
```

If you would rather choose your own data
(browse the bundled datasets with `table_data()`),
here is one suggestion per flavour,
each with the multiplex and/or directed ties that make positional analysis
interesting:

| Classic (small, tidy) | Fiction (moderate) | Real-world (larger) |
|---|---|---|
| `ison_monks` (directed, weighted, multiplex ties among monks in Sampson's monastery — _the_ classic blockmodelling dataset: can you recover the factions he observed as positions?) | `fict_thrones` (directed, multiplex ties among Game of Thrones characters — do the equivalence classes line up with the houses, or cut across them?) | `irps_911` (multiplex covert network around the 9/11 attacks — where are the structural holes, and who spans them while staying inconspicuous?) |

## Summary

<img src="https://media1.tenor.com/m/gSsbNTouixUAAAAC/bravo-applause.gif" alt="gif of an enthusiastic standing ovation and cries of bravo"/>

Well done -- you have completed the tutorial on position and equivalence!
Along the way, you have learned to use these functions:

| Function | What it does |
|---|---|
| `to_uniplex()` | extracts one type of tie from a multiplex network |
| `tie_is_bridge()`, `node_by_bridges()` | flags bridging ties; counts the bridges adjacent to each node |
| `node_by_constraint()` | Burt's constraint: how closed each node's neighbourhood is |
| `node_by_effsize()`, `node_by_redundancy()`, `node_by_efficiency()`, `node_by_hierarchy()` | further structural-holes measures: non-redundant contacts, their overlap, and more |
| `node_is_fold()` | flags nodes in a structural fold (cohesive membership in more than one group) |
| `node_is_min()` | flags the node(s) with the minimum score, for highlighting |
| `node_in_structural()` | structurally equivalent classes (same tie partners; `cluster`, `distance`, `k`, `range` options) |
| `node_in_regular()`, `node_in_automorphic()` | regularly and automorphically equivalent classes (same role; interchangeable) |
| `node_x_tie()`, `node_x_triad()`, `node_x_path()` | the tie, triad, and path censuses behind the three equivalences |
| `node_in_equivalence()` | generic equivalence classes from any `node_x_*()` census |
| `plot()` on a membership vector | draws the dendrogram and its cut-point |
| `plot(as_matrix(net), membership = )` | draws the blockmodel: the matrix sorted into blocks |
| `summary(census, membership = )` | averages each class's census profile |
| `to_blocks()` | contracts a network into a reduced graph of positions |
| `graphr(..., node_color = , node_size = )` | maps memberships or measures onto the graph |

When you are ready, continue with the other `{netrics}` tutorials —
on centrality, community, 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:

`r print_glossary()`
