Skip to Tutorial Content

## Today’s target

Centrality is one of the oldest and most-used ideas in network analysis: the question of which nodes are, in some sense, the most important, prominent, or well-placed in a network . The catch is that “important” can mean several different things, and each meaning has its own measure — indeed its own family of measures. This tutorial shows how to measure and map degree , betweenness , closeness , eigenvector , and related types of centrality, explore their distributions , and summarise the whole network’s centralisation .

Catching up: 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 and autograph 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:

- Calculate degree centrality by hand and with `node_by_degree()`, including in- and out-degree for directed networks and strength for weighted ones
- Compute betweenness, closeness, and eigenvector centrality and interpret what each one means
- Recognise the wider betweenness-, closeness-, and eigenvector-type families, and choose an appropriate variant (e.g. reach, pagerank, or power)
- Highlight the most central node(s) on a graph with `node_is_max()` and `graphr()`
- Read a measure’s distribution with `plot()`, and see how its shape drives a network’s centralisation , computed with `net_by_*()`
- Know when scores are normalised, and how to obtain the raw scores

Choose your own data: The worked examples below use `ison_brandes`, a small, tidy teaching network 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

For this exercise, we’ll use the `ison_brandes` dataset in `{manynet}`. This dataset is in a ‘tidygraph’ format, but `manynet` makes it easy to coerce this into other forms to be compatible with other packages. We can create a two-mode version of the dataset by renaming the nodal attribute “twomode_type” to just “type”. Let’s begin by graphing these datasets using `graphr()`.

    # Let's graph the one-mode version
    graphr(____)

    # Now, let's create a two-mode version 'ison_brandes2' and graph it.
    ison_brandes2 <- ison_brandes |> rename_nodes(type = twomode_type)
    graphr(____)

    # plot the one-mode version
    graphr(ison_brandes)
    ison_brandes2 <- ison_brandes |> rename_nodes(type = twomode_type)
    # plot the two-mode version
    graphr(ison_brandes2, layout = "bipartite")

The network is 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:

    ison_brandes <- to_named(ison_brandes)

    # Now, let's graph using the object names: "ison_brandes"
    graphr(____)

    ison_brandes <- to_named(ison_brandes)
    # plot network with names
    graphr(ison_brandes)

Note that you will likely get a different set of names (though still alphabetical), as they are assigned randomly from a pool of (American) first names.

## Degree centrality

On this page: Counting ties · Direction · Strength

### Counting ties

Let’s start with calculating degree . Remember that degree centrality is just the number of incident edges/ties to each node. It is therefore easy to calculate yourself. Just sum the rows or columns of the matrix!

    # We can calculate degree centrality like this:
    (mat <- as_matrix(ison_brandes))
    (degrees <- rowSums(mat))
    rowSums(mat) == colSums(mat)

    # Or by using a built in command in netrics like this:
    node_by_degree(ison_brandes, normalized = FALSE)
    # node_by_deg() is a shortcut for exactly this unnormalised version:
    node_by_deg(ison_brandes)

    # manually calculate degree centrality
    mat <- as_matrix(ison_brandes)
    degrees <- rowSums(mat)
    rowSums(mat) == colSums(mat)
    # You can also just use a built in command in netrics though:
    node_by_degree(ison_brandes, normalized = FALSE)
    # node_by_deg() is a shortcut for exactly this unnormalised version:
    node_by_deg(ison_brandes)

A node’s degree is the most immediate sense in which it can be “central”: a well-connected node has more opportunities to send or receive whatever flows through the network, and more visibility to the nodes around it. But degree is a local measure — it counts a node’s ties without regard to where those ties lead — so a high-degree node is best read as an active or busy one, not necessarily a powerful or well-positioned one. The other measures in this tutorial each capture a different, less local sense of importance.

Going further: All centrality measures in `{netrics}` return normalised scores by default, so that values are comparable across networks of different sizes. For the raw scores, add `normalized = FALSE` to any of these functions — or, for degree, just use the truncated `node_by_deg()`, which is an alias for the unnormalised version.

### Degrees of direction

So far each node has had a single degree, because `ison_brandes` is undirected . In a directed network, ties are sent from one node to another, so every node has two degrees: an in-degree (ties received) and an out-degree (ties sent). These can tell quite different stories: in an advice network, for example, a high in-degree suggests a sought-after source of advice, while a high out-degree marks an eager asker.

`ison_brandes` has no natural direction, but `{manynet}`’s `to_directed()` will assign a random direction to each tie — just as `to_named()` assigned random names — which is all we need for a worked comparison. Run the code, and check whether the row and column sums still agree.

    ison_brandes_dir <- to_directed(ison_brandes)
    node_by_indegree(ison_brandes_dir, normalized = FALSE)
    node_by_outdegree(ison_brandes_dir, normalized = FALSE)
    mat <- as_matrix(ison_brandes_dir)
    rowSums(mat) == colSums(mat)

Now the row sums (out-degree) and column sums (in-degree) part ways — the answer to the earlier quiz question flips as soon as direction enters. Because the directions here are assigned at random, your in- and out-degrees will differ from run to run; with real directed data they would of course be meaningful and stable. `node_by_indegree()` and `node_by_outdegree()` are shortcuts for `node_by_degree()`’s `direction = "in"` and `direction = "out"` arguments.

### Strength of ties

What about weighted networks, where ties record not just presence but amounts — messages exchanged, hours worked together, tonnes traded? There, counting ties is only half the story: two nodes with three ties each are hardly equivalent if one’s ties are ten times heavier. Summing the weights of a node’s ties instead of counting them yields what is often called strength centrality.

Again `{manynet}` can conjure the missing property for practice purposes: `to_weighted()` assigns random weights to the ties. The `alpha` argument to `node_by_deg()` then trades off between counting ties and summing weights: `alpha = 0` ignores the weights entirely (pure degree), `alpha = 1` ignores the counts entirely (pure strength), and values in between mix the two. Compare the two extremes.

    ison_brandes_wtd <- to_weighted(ison_brandes)
    node_by_deg(ison_brandes_wtd)             # alpha = 0: count ties
    node_by_deg(ison_brandes_wtd, alpha = 1)  # alpha = 1: sum tie weights

Note that on a weighted network, `node_by_degree()` itself defaults to strength (`alpha = 1`) — the assumption being that if you have measured weights, you probably care about them.

Going further: The degree family has several further members for special kinds of networks: `node_by_multidegree()` compares tie types in a multiplex network, `node_by_posneg()` handles signed networks with positive and negative ties, `node_by_neighbours_degree()` averages the degrees of each node’s neighbours, and `tie_by_degree()` scores each tie by the degrees of its endpoints. See `?measure_central_degree` for definitions and references.

In brief: Degree centrality counts each node’s ties: `node_by_degree()` for normalised scores, `node_by_deg()` for raw counts, `node_by_indegree()`/`node_by_outdegree()` for directed networks, and `alpha = 1` to sum tie weights (strength) in weighted networks. It is a local measure of activity.

## Betweenness centralities

On this page: Betweenness · Tie betweenness · Variants

The next three pages tour the three main families of centrality measures beyond degree. What the members of the betweenness family all have in common is that they measure how much of the traffic between other nodes depends on a given node or tie — importance as control over flow. Where they differ is in what they assume about how things flow: along shortest paths only, along all paths, or in electrical-current fashion.

### Betweenness centrality

Betweenness measures how often a node lies on the shortest paths ( geodesics ) between other pairs of nodes. A node with high betweenness sits at a crossroads: whatever passes between otherwise-distant parts of the network has to go through it, giving it the opportunity to broker, filter, or bottleneck that flow. Calculate the betweenness centralities for `ison_brandes`.

    # Use the node_by_betweenness() function to calculate the
    # betweenness centralities of nodes in a network
    node_by_betweenness(____)

    node_by_betweenness(ison_brandes)

### Ties can be between too

Nodes are not the only things that can lie between: `tie_by_betweenness()` counts the shortest paths that run through each tie. Ties with high betweenness often act as bridges between otherwise separate parts of the network — indeed this score is the basis of a famous community-detection algorithm (Girvan-Newman), which you will meet in the community tutorial. Map tie betweenness onto tie width to see the network’s main thoroughfares.

    ison_brandes %>%
      mutate_ties(bridgeness = tie_by_betweenness(ison_brandes)) %>%
      graphr(edge_size = "bridgeness")

The widest ties are the ones the network can least afford to lose: cut one and many shortest paths between nodes lengthen — or disappear.

### Induced and other variants

The rest of the betweenness family keeps the “who depends on whom” logic but varies the assumptions:

- `node_by_induced()` measures induced or vitality betweenness: how much the network’s total betweenness changes when the node is removed. It answers “what would we lose without this node?” rather than “how often is this node in between?”.
- `node_by_flow()` measures flow betweenness, which models spreading as electrical current across all paths, not just the shortest ones — useful when whatever flows does not know the shortest route.
- `node_by_stress()` counts the shortest paths through each node without weighting them by how many alternatives exist — an older, “rawer” cousin of betweenness.

Compare classic and induced betweenness — do they agree here?

    node_by_betweenness(ison_brandes, normalized = FALSE)
    node_by_induced(ison_brandes, normalized = FALSE)

For this network the rankings broadly agree, but on networks with more redundancy they can diverge: a node can lie on few shortest paths itself yet still be sorely missed when removed. For formal definitions and references, see `?measure_central_between`.

In brief: The betweenness family measures control over flow: `node_by_betweenness()` via shortest paths, `tie_by_betweenness()` for ties rather than nodes, `node_by_induced()` via a node’s contribution to total betweenness, `node_by_flow()` via current across all paths, and `node_by_stress()` via raw path counts.

## Closeness centralities

On this page: Closeness · Reach & distance · Variants

What the members of the closeness family have in common is that they measure how easily a node can reach — or be reached by — the rest of the network: importance as access or independence rather than control. Where they differ is in how they summarise the distances involved: their total, their average, their maximum, or how many nodes fall within a given range.

### Closeness centrality

Closeness measures how short a node’s paths are to all other nodes — the smaller the total geodesic distance, the higher the closeness. A node with high closeness can reach the rest of the network in few steps, so it is well placed to spread (or hear) something quickly. Calculate the closeness centralities for `ison_brandes`.

    # Use the node_by_closeness() function to calculate the
    # closeness centrality of nodes in a network
    node_by_closeness(____)

    node_by_closeness(ison_brandes)

### Reach and distance

Sometimes what matters is not the average distance to everyone, but a more concrete question: how much of the network can this node reach within k steps? That is `node_by_reach()`, which by default counts the proportion of the network within two steps — a good proxy for “who could mobilise the most others quickly?”. And sometimes the question is about one node in particular: `node_by_distance()` returns every node’s geodesic distance from (or to) a given node — the network equivalent of dropping a pin on a map. Try both: how far does the first-named node’s reach extend?

    node_by_reach(ison_brandes)
    node_by_distance(ison_brandes, from = node_names(ison_brandes)[1])

Note that `node_by_reach()` takes a `cutoff` argument to change the number of steps considered: `cutoff = 1` reduces it to degree, while a large cutoff approaches a measure of component membership.

### Harmonic and other variants

Classic closeness has a well-known weakness: in a disconnected network, some distances are infinite, and the measure breaks down. The family includes several responses to this, and other refinements:

- `node_by_harmonic()` sums the reciprocals of distances, so unreachable nodes simply contribute zero — the recommended drop-in replacement for closeness on disconnected networks.
- `node_by_eccentricity()` reports each node’s worst case: the distance to whatever node is furthest from it.
- `node_by_information()` measures current-flow closeness, crediting all paths (weighted by efficiency) rather than only the shortest.
- `node_by_vitality()` measures how much the network’s total closeness would change without the node — the closeness analogue of induced betweenness.

For formal definitions and references, see `?measure_central_close`.

In brief: The closeness family measures access: `node_by_closeness()` via total distances, `node_by_reach()` via the share of the network within k steps, `node_by_distance()` via distances from a chosen node, `node_by_harmonic()` as the disconnection-proof variant, and `node_by_eccentricity()` via each node’s furthest distance.

## Eigenvector centralities

On this page: Eigenvector · Power · Pagerank & hubs

What the members of the eigenvector family have in common is recursion: a node’s score depends on the scores of the nodes it is connected to, which depend in turn on their neighbours, and so on — importance as standing among the important. Where they differ is in how that recursion is tempered: whether alters’ scores always help, how far along walks influence travels, and whether sending and receiving are scored separately.

### Eigenvector centrality

Eigenvector centrality weights a node’s ties by how central its neighbours are: you are important if you are connected to important others. It captures influence rather than mere activity — a node with only a few ties, but to very well-connected nodes, can outscore a node with many ties to peripheral ones. Calculate the eigenvector centralities for `ison_brandes`.

    # Use the node_by_eigenvector() function to calculate
    # the eigenvector centrality of nodes in a network
    node_by_eigenvector(____)

    node_by_eigenvector(ison_brandes)

Going further: A note of caution: eigenvector centrality is only well-behaved on a single connected component — on a disconnected network the scores for the smaller components can be misleading. The pagerank variant below is one common response.

### Power and influence

Bonacich famously argued that power and influence are not the same thing. Under eigenvector logic, well-connected neighbours always raise your score. But in bargaining or exchange, the reverse can hold: being connected to poorly-connected others makes them dependent on you — that is power. `node_by_power()` implements Bonacich’s measure, and its `exponent` argument sets the sign of the recursion: a positive exponent rewards well-connected neighbours (influence-like), while a negative one rewards dependent, poorly-connected neighbours (power). Compare the two.

    node_by_power(ison_brandes, exponent = 1)
    node_by_power(ison_brandes, exponent = -1)

Try to answer the following questions for yourself:

- which nodes swap fortunes when the exponent flips, and what do their neighbourhoods look like?
- can you think of a real-world example when an actor might be central but not powerful, or powerful but not central?

### Pagerank, hubs, and authorities

Two further members of this family were developed for the web, and shine on directed networks:

- `node_by_pagerank()` measures pagerank, the algorithm behind early Google: a random surfer follows ties and occasionally teleports to a random node, and a node’s score is how often the surfer visits. The teleporting makes it robust on disconnected and directed networks where plain eigenvector centrality struggles.
- `node_by_hub()` and `node_by_authority()` split reputation in two: authorities are nodes that many point to, while hubs are nodes that point to many good authorities — think review sites (hubs) versus the restaurants they recommend (authorities).
- `node_by_alpha()` measures alpha or Katz centrality, which extends eigenvector logic to count longer walks at a discount — see `?measure_central_eigen` for details on the whole family.

Try these on a directed version of our network.

    ison_brandes_dir <- to_directed(ison_brandes)
    node_by_pagerank(ison_brandes_dir)
    node_by_hub(ison_brandes_dir)
    node_by_authority(ison_brandes_dir)

Since the directions are again assigned at random, run the code a few times: watch how a node’s authority score rises and falls with the arrows pointing at it.

You have now met all four families of centrality — degree, betweenness, closeness, and eigenvector — each ranking the nodes by a different notion of importance. With four families (and many variants within each) to choose from, how do you decide? That is the subject of the next page.

In brief: The eigenvector family measures standing among the important: `node_by_eigenvector()` for influence, `node_by_power()` for Bonacich power (with its sign-flipping `exponent`), `node_by_pagerank()` for a robust directed-network variant, and `node_by_hub()`/`node_by_authority()` for split sending/receiving reputations.

## Which centrality?

There are, by one count, over 200 centrality indices in the literature — so which should you reach for? Linton Freeman’s classic answer was that most of them boil down to just three ideas — degree, closeness, and betweenness — with everything else a variant; we add eigenvector as a fourth, giving the four families this tutorial is built around. Another way of thinking about each of the centrality measures included here is in terms of three questions.

First: does the measure count what radiates from a node — its own ties, its distances, its walks — or what passes through it on the way between others? Borgatti and Everett (2006) call the first radial and the second medial: degree, closeness, and eigenvector are radial; betweenness is medial.

Second: does it read only a node’s immediate neighbourhood (local), or its place in the whole network (global)? Degree is local; the other three are global. There is no medial-local family, so the four families are the useful corners of that space rather than a full grid. Note too that this is a dial rather than a switch: `node_by_reach()` takes a `cutoff` that walks a measure from one end to the other, and at `cutoff = 1` reach centrality simply is degree.

Third: what does the measure actually traverse? Closeness and eigenvector are both radial and both global, so the first two questions cannot tell them apart; what separates them is that closeness travels by shortest paths while eigenvector counts walks of every length. The possibilities are nested by how much repetition they allow:

> walks (anything goes) ⊇ trails (no tie used twice) ⊇ paths (no node visited twice) ⊇ geodesics (only the shortest paths)

Two measures can traverse the same thing and still differ in how they weight what they find. Eigenvector, alpha and subgraph centrality count all walks deterministically, discounting the longer ones; `node_by_pagerank()` and `node_by_randomwalk()` traverse those same walks but weight them by the probability that a random walker would take them. A few measures — `node_by_information()` and `node_by_flow()` — sit outside this scheme altogether, modelling current or maximum flow rather than any single route.

Family | Reads a node’s… | Traverses | Answers the question | Well-suited when
--- | --- | --- | --- | ---
Degree | activity (radial, local) | direct ties only | “Who is busiest, or most directly connected?” | only immediate ties matter, or influence spreads one step at a time; also the cheapest to compute on very large networks; outdegree associated with activity and indegree with popularity; tie weight and sign versions exist too
Closeness | reach (radial, global) | geodesics — shortest paths to all others | “Who can reach — or be reached by — everyone else in the fewest steps?” | the network is connected and things travel by efficient routes; use `node_by_harmonic()` if it is disconnected
Betweenness | brokerage (medial, global) | geodesics — shortest paths passing through it | “Who sits between others, brokering, bridging, or bottlenecking flow?” | you care about gatekeeping, bridges between groups, or where flow is vulnerable; costly on huge networks (use a `cutoff`)
Eigenvector | standing (radial, global) | walks of every length, weighted by neighbours’ scores | “Who is connected to other important, well-connected nodes?” | importance is recursive — prestige, status, influence; use `node_by_pagerank()` for directed or disconnected networks; use `node_by_power()` if influence depends on being connected to poorly-connected nodes

### Reading the numbers you get back

Choosing a measure is half the job; knowing what its scores mean is the other half. Every measure here reports how it was rescaled, and there are three possibilities worth telling apart.

A normalised score has been divided by a theoretical maximum — the largest value the measure could take on a network of this size and shape. Degree, closeness and betweenness all work this way, and because the yardstick does not depend on the particular network you measured, their scores can be compared between networks: a normalised degree of 0.6 means the same thing in either of two networks.

A scaled score has been divided by the observed maximum, the largest value that actually turned up in this network. Eigenvector centrality works this way, because an eigenvector is only defined up to a scalar multiple and so has no absolute units to preserve. The consequence is easy to miss: the top-scoring node always gets exactly 1.0, in every network, however central it really is. Scaled scores rank nodes within one network; they say nothing across networks.

A proportion — `node_by_pagerank()` is the example — divides each score by their total, so the values sum to one and can be read as shares.

And some measures are rescaled by none of these, because no sensible maximum exists: `node_by_alpha()` can return negative values, and `node_by_subgraph()` grows exponentially. There is no normalised version of every centrality measure, and it is better to know that than to be handed a number between 0 and 1 that was never really bounded.

Going further: For the full landscape see David Schoch’s periodic table of centrality and the `{netrankr}` package, which can even compare nodes without committing to a single index.

Now let’s see how to spot the most central nodes at a glance.

## Plotting centrality

On this page: Highlighting extremes · Two-mode

### Highlighting the most central node

It is straightforward in `{autograph}` to highlight nodes and ties with maximum or minimum (e.g. degree) scores. If the vector is numeric (i.e. a “measure”), then this can be easily converted into a logical vector that identifies the node/tie with the maximum/minimum score using e.g. `node_is_max()` or `tie_is_min()`. By passing this attribute to the `graphr()` argument “node_color” we can highlight which node or nodes hold the maximum score in a different colour.

    # plot the network, highlighting the node with the highest centrality score with a different color
    ison_brandes %>%
      mutate_nodes(color = node_is_max(node_by_degree())) %>%
      graphr(node_color = "color")

    ison_brandes %>%
      mutate_nodes(color = node_is_max(node_by_betweenness())) %>%
      graphr(node_color = "color")

    ison_brandes %>%
      mutate_nodes(color = node_is_max(node_by_closeness())) %>%
      graphr(node_color = "color")

    ison_brandes %>%
      mutate_nodes(color = node_is_max(node_by_eigenvector())) %>%
      graphr(node_color = "color")

How neat! Notice whether the same node lights up across the four measures or whether different nodes do — because the measures capture different things, a node can be top of one ranking and unremarkable on another.

### The two-mode version

Try it with the two-mode version. What can you see?

    # Instead of "ison_brandes", use "ison_brandes2"

    ison_brandes2 %>%
      add_node_attribute("color", node_is_max(node_by_degree(ison_brandes2))) %>%
      graphr(node_color = "color", layout = "bipartite")

    ison_brandes2 %>%
      add_node_attribute("color", node_is_max(node_by_betweenness(ison_brandes2))) %>%
      graphr(node_color = "color", layout = "bipartite")

    ison_brandes2 %>%
      add_node_attribute("color", node_is_max(node_by_closeness(ison_brandes2))) %>%
      graphr(node_color = "color", layout = "bipartite")

    ison_brandes2 %>%
      add_node_attribute("color", node_is_max(node_by_eigenvector(ison_brandes2))) %>%
      graphr(node_color = "color", layout = "bipartite")

In brief: Turn any numeric measure into a logical flag with `node_is_max()`/`node_is_min()`, then map it to `node_color` in `graphr()` to spotlight the most (or least) central node(s) directly on the graph.

## Centralisation

On this page: Distributions · Shape & centralisation · Measuring · Comparing

So far we have scored individual nodes. But we can also ask a question about the whole network: is its centrality piled up on a few nodes, or shared out evenly? That whole-network summary is called centralisation , and the bridge to it runs through the distribution of a nodal measure.

### Reading a distribution

Rather than reduce a measure to a single summary number, we can look at its whole distribution across the nodes. `{autograph}` offers a way to get a pretty good first look at this, though there are more elaborate ways to do this in base and grid graphics. Plot the degree distribution of `ison_brandes`.

    # distribution of degree centrality scores of nodes
    plot(node_by_degree(ison_brandes))

What’s plotted here by default is both the degree distribution as a histogram, as well as a density plot overlaid on it in red. Reading a distribution rather than a single summary number lets you see shape: whether ties are spread fairly evenly across nodes, or concentrated on a few.

The same works for any node measure, not just degree. Plot the distributions of the other three centralities you have met.

    plot(node_by_betweenness(ison_brandes))
    plot(node_by_closeness(ison_brandes))
    plot(node_by_eigenvector(ison_brandes))

### From distribution to centralisation

Here is the key idea. The shape of a degree distribution and the network’s centralisation are two views of the same thing. When degrees are spread evenly — a symmetric, roughly normal bell where most nodes are near-average — no node dominates, and centralisation is low. When the distribution is skewed into a long, exponential tail — a few hubs with very high degree, and many nodes with very little — a handful of nodes dominate, and centralisation is high.

To see this, let’s conjure two networks of the same size with deliberately different degree distributions: a random network (whose degrees cluster around a typical value) and a scale-free network (which grows a few dominant hubs). The seed just fixes the random draw so everyone sees the same picture. Run it, and compare both the plots and the two centralisation scores.

    set.seed(7)
    rand <- generate_random(50, 0.1)     # bell-shaped degree distribution
    scaf <- generate_scalefree(50, 1.3)  # heavy-tailed degree distribution
    plot(node_by_degree(rand)) + plot(node_by_degree(scaf))
    net_by_degree(rand)   # low centralisation
    net_by_degree(scaf)   # much higher centralisation

The random network’s degrees pile up in a bell, and its degree centralisation is low — no node is far above the crowd. The scale-free network’s degrees trail off into a long tail, and its centralisation is several times higher — a few hubs carry the network. Our `ison_brandes` example sits somewhere between these two extremes.

In brief: `plot()` on any node measure draws its distribution (a histogram plus a density curve). An even, bell-shaped distribution signals low centralisation; a skewed, heavy-tailed one signals high centralisation. Centralisation just turns that distributional inequality into a single number.

### Measuring centralisation

`{netrics}` also implements network centralisation functions. Here we are no longer interested in the level of the node, but in the level of the whole network, so the syntax replaces `node_` with `net_`:

    net_by_degree(ison_brandes)
    net_by_betweenness(ison_brandes)
    net_by_closeness(ison_brandes)
    print(net_by_eigenvector(ison_brandes), digits = 5)

By default, scores are printed up to 3 decimal places, but this can be modified and, in any case, the unrounded values are retained internally. This means that even if rounded values are printed (to respect console space), as much precision as is available is used in further calculations.

Going further: For centralisation in two-mode networks, `mode_by_*()` functions are available to return two values, one per mode.

### Comparing the measures

What if we want to have a single image/figure with multiple plots? This can be a little tricky with gg-based plots, but fortunately the `{patchwork}` package is here to help. We use `|` to place graphs side-by-side and `/` to stack them, and `ggtitle()` to record each measure’s centralisation score as a subtitle.

    ison_brandes <- ison_brandes |>
      add_node_attribute("degree", node_is_max(node_by_degree(ison_brandes)))  |>
      add_node_attribute("betweenness", node_is_max(node_by_betweenness(ison_brandes))) |>
      add_node_attribute("closeness", node_is_max(node_by_closeness(ison_brandes)))  |>
      add_node_attribute("eigenvector", node_is_max(node_by_eigenvector(ison_brandes)))
    gd <- graphr(ison_brandes, node_color = "degree") +
      ggtitle("Degree", subtitle = round(net_by_degree(ison_brandes), 2))
    gc <- graphr(ison_brandes, node_color = "closeness") +
      ggtitle("Closeness", subtitle = round(net_by_closeness(ison_brandes), 2))
    gb <- graphr(ison_brandes, node_color = "betweenness") +
      ggtitle("Betweenness", subtitle = round(net_by_betweenness(ison_brandes), 2))
    ge <- graphr(ison_brandes, node_color = "eigenvector") +
      ggtitle("Eigenvector", subtitle = round(net_by_eigenvector(ison_brandes), 2))
    (gd | gb) / (gc | ge)
    # ggsave("brandes-centralities.pdf")

In brief: Swap `node_` for `net_` to move from a node’s centrality to the whole network’s centralisation, summarising how unequally that centrality is distributed. For multimodal networks, `mode_by_*()` returns one value per mode, which can be compared to see which mode is more centralised with respect to the other.

## Free play

Choose another dataset included in `{manynet}` (browse them with `table_data()`). Name a plausible research question you could ask of the dataset relating to each of the four main centrality measures (degree, betweenness, closeness, eigenvector). Plot the network with nodes sized or coloured by each centrality measure, using titles or subtitles to record the question and/or centralisation measure.

If you are not sure where to start, here is one suggestion per flavour, each with real direction and/or weights to practise the measures from this tutorial on:

Classic (small, tidy) | Fiction (moderate) | Real-world (larger)
--- | --- | ---
`ison_networkers` (directed, weighted messages among early network researchers — compare in-degree with strength: who receives from many, and who receives a lot?) | `fict_starwars` (directed, weighted interactions between Star Wars characters — who dominates the dialogue, and is that the same as being influential?) | `irps_blogs` (directed hyperlinks among 1,490 political blogs — made for pagerank, hubs, and authorities)

And if betweenness is what intrigues you, `irps_911` — Krebs’ study of a covert network — is a classic setting for asking who brokers while staying inconspicuous.

## Summary

Well done – you have completed the tutorial on centrality! Along the way, you have learned to use these functions:

Function | What it does
--- | ---
`node_by_degree()`, `node_by_deg()` | degree centrality (ties per node); `node_by_deg()` returns the raw, unnormalised counts
`node_by_indegree()`, `node_by_outdegree()` | ties received and sent in directed networks
`node_by_deg(..., alpha = 1)` | strength: sums tie weights instead of counting ties
`node_by_betweenness()` | betweenness centrality (lying on others’ shortest paths)
`tie_by_betweenness()` | shortest paths through each tie (bridges)
`node_by_induced()`, `node_by_flow()`, `node_by_stress()` | betweenness variants: contribution to total, current flow, raw path counts
`node_by_closeness()` | closeness centrality (short paths to all others)
`node_by_reach()`, `node_by_distance()` | share of network within k steps; distances from a chosen node
`node_by_harmonic()`, `node_by_eccentricity()` | closeness variants for disconnected networks and worst-case distance
`node_by_eigenvector()` | eigenvector centrality (connected to well-connected others)
`node_by_power()` | Bonacich power, with its sign-flipping `exponent`
`node_by_pagerank()`, `node_by_hub()`, `node_by_authority()` | web-style variants for directed networks
`node_by_*(..., normalized = FALSE)` | returns raw rather than normalised scores
`plot()` | draws the distribution of a node measure (histogram + density)
`generate_random()`, `generate_scalefree()` | (manynet) make example networks with contrasting degree distributions
`node_is_max()`, `node_is_min()` | flags the node(s) with the extreme score, for highlighting
`graphr(..., node_color = )`, `graphr(..., edge_size = )` | maps node or tie measures onto the graph
`net_by_degree()`, `net_by_betweenness()`, `net_by_closeness()`, `net_by_eigenvector()` | whole-network centralisation
`+`, `/` (patchwork) | place graphs beside or above one another in one figure

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

Authority
  An authority is a node pointed to by many hubs.
Betweenness
  The betweenness centrality of a node is the proportion of shortest paths between all pairs of nodes that pass through that node.
Bridge
  A bridge or isthmus is a tie whose deletion increases the number of components.
Centralization
  A measure of how unequal the centralities of the nodes in a network are.
Closeness
  The closeness centrality of a node is the reciprocal of the sum of its distances to all other nodes.
Component
  A component is a connected subgraph not part of a larger connected subgraph.
Connected
  A connected network is one with a single (strong) component.
Degree
  The degree of a node is the number of connections it has.
Directed
  A directed network is a network where the ties have a direction, from a sender to a receiver.
Distribution
  A degree distribution is the frequency distribution of the degrees of the nodes in a network.
Eigenvector
  The eigenvector centrality of a node is the corresponding value in the dominant eigenvector of the adjacency matrix.
Geodesic
  A geodesic is a shortest path between two nodes.
Hub
  A hub is a node connected to many authorities.
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.
Power
  An eigenvector-style centrality that allows being connected to more peripheral nodes to contribute centrality.
Signed
  A signed network is one where ties are marked as positive or negative, such as friendship and enmity or alliance and conflict.
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.
Weighted
  A weighted network is where the ties have been assigned weights.

# Centrality

#### by James Hollway