Lab 9: Let's make a hairball!

The purpose of this lab is learn about the form of network data and to introduce some basic techniques for exploring it. Make sure you have updated the course package since Sunday (restart R first):

remotes::install_github("agoldst/dataculture")

Some new packages will be installed automatically, notably igraph (the main R library used for network analysis) and ggraph (for visualization). You should load all these packages as follows:

library(dataculture)
library(igraph)
library(ggraph)

Reproducing some network figures

Moretti’s discussion of Hamlet is based on a hand-created dataset which he never released. It is possible to construct a similar dataset using a publicly-available XML edition of Shakespeare’s play, the New Folger Shakespeare, edited by Barbara A. Mowat and Paul Werstine. I have extracted the data you need and made it available as the data frame hamlet. hamlet contains, for each act and scene, one row for each pair of characters who speak after one another, with the tally of the number of time this happens (with the characters in either order).

hamlet |> filter(act == 1, scene == 1)
## # A tibble: 6 × 5
##   s1        s2        act   scene turns
##   <chr>     <chr>     <chr> <chr> <int>
## 1 Barnardo  Francisco 1     1        11
## 2 Barnardo  Horatio   1     1        16
## 3 Barnardo  Marcellus 1     1        10
## 4 Francisco Horatio   1     1         1
## 5 Francisco Marcellus 1     1         4
## 6 Horatio   Marcellus 1     1        17

Make sure you understand what this data says. Here is a synopsis of the scene from the New Folger Shakespeare online text:

On the guards’ platform at Elsinore, Horatio waits with Barnardo and Marcellus to question a ghost that has twice before appeared. The Ghost, in the form of the late King Hamlet of Denmark, appears but will not speak. Horatio decides to tell his fellow student, Prince Hamlet, about the Ghost’s appearance.

  1. Why doesn’t the data for Act 1, Scene 1 include the Ghost or Hamlet?

  2. Draw, by hand, the network for Act 1, Scene 1. (You can ignore the number of turns.)

Let’s look at this scene as a graph. We will use the igraph function graph_from_data_frame, which takes an edge list from the first two columns of its argument and return a graph data structure. For now, we’ll ignore the number of turns.

h11 <- hamlet |>
    filter(act == 1, scene == 1) |>
    select(s1, s2) |>
    graph_from_data_frame(directed=F)

In words, this takes just the list of speaker pairs s1,s2 from the Act 1, Scene 1 rows of hamlet and passes them on to graph_from_data_frame. A new variable, h11, is created to refer to the special graph data structure which is created.

Remember: a graph is simply a list of vertices (a.k.a. “nodes”) and a list of the connections between them. These two components can be accessed with the V and E functions. Try:

V(h11)
E(h11)

These two lists fully specify the graph. Or it could also be specified with a table showing which nodes are connected. This table is called the adjacency matrix.

as_adj(h11)

But you want the pictures. Try:

plot(h11)

The plot (not the same as ggplot) function has many uses in R; in this case it causes igraph to produce its default picture of the graph.

  1. Repeat this process for Act 1, Scene 5. You should have another four-vertex graph. What is different about it, as a structure?

In order to produce a graph of interaction covering the whole play, you need to combine the scene-by-scene data. If you remember our baby names exploration, you may be able to do this yourself (hint: summarize, sum). Before scrolling down for the answer, see if you can work with your group to figure it out.







Here’s one approach:

ham_net <- hamlet |>
    group_by(s1, s2) |>
    summarize(turns=sum(turns)) |>
    graph_from_data_frame(directed=F)

Here we go: let’s make a hairball! Use the plot function.

  1. Discuss: what is the grammar of the resulting graphic? Explain what the graphical elements are and what data is used to determine where they go and what they look like.

    1. Make sure to compare what other members of your group produce! Hint: your pictures are unlikely to be all exactly the same.

    2. Can you tell what the diameter of the network is? Recall that the diameter is the maximum of the lengths of the shortest paths between nodes. (The “six degrees of separation” measurement.) Try eyeballing it…then check with the diameter function.

Now switch from using plot to ggraph, which works like ggplot (it actually is using ggplot under the hood). The call looks like this:

ggraph(ham_net, layout="stress") +
    geom_edge_link() +
    geom_node_label(aes(label=name))

(The name column doesn’t exist in ham_net but it is created by ggraph from the node name information in V(ham_net)).

  1. Let’s tweak parameters to understand the graphical grammar better.

    1. To get a sense of what layout is, try the following alternatives to stress (in quotes): circle, randomly, fr. Where does the network say the nodes belong in space? How should you decide how to place them?

    2. Nodes: try geom_node_text and geom_node_point (you’ll get a warning).

    3. Edges: try leaving geom_edge_link() out. Try geom_edge_arc. Try the command vignette("Edges", package="ggraph").

    4. Now compare Moretti’s Figure 1. It’s “made by hand,” he says (215). What is the same, and what is different?

One more. Try:

hamlet |>
    group_by(s1, s2) |>
    summarize(turns=sum(turns)) |>
    ggplot() +
        geom_point(aes(s1, s2)) +
        geom_point(aes(s2, s1)) +
        guides(x=guide_axis(angle=90))

This is a visualization of the adjacency matrix. The same picture, without the labels, can be made with

ggraph(hamlet, layout="matrix") +
    geom_edge_tile(mirror=T)

We can try to squeeze a little more detail on the graph and look at the turns information. Return to your preferred edge and node geoms.

  1. Try aes(edge_width=turns) inside the edge geom; then try edge_alpha.

    1. How informative is this? you can replace turns with tidygraph::centrality_edge_betweenness() to show, instead of the number of turns, a measure of the importance of the edge to connections in the network.

For a particular node, the subgraph of vertices that are directly connected to it is called its neighborhood, or sometimes its “ego-network.” This graph can be directly extracted with the ego function, but for exploration it helps to see it on a visualization of the whole graph. To more or less reproduce Moretti’s fig. 2, “Hamlet’s space”, let’s fall back to the igraph function plot. Try:

plot(ham_net,
     mark.groups=ego(ham_net, nodes="Hamlet"))

and for fig. 3, “Hamlet and Claudius”:

plot(ham_net,
     mark.groups=ego(ham_net, nodes=c("Hamlet", "Claudius")))

Moretti says: “the tragedy is all here,” because pretty much everyone in the overlap dies by the end of the play.

  1. Reproduce the “Gertrude and Ophelia” plot too.

    1. What about Horatio? What is different about his ego-network?

Now let’s explore how to deal with the “hairball” quality of the graphic. As in other types of visualizations, in order to see better, it sometimes makes sense to show less. One strategy is simply not to count every speech sequence as a link; let’s leave out “interactions” with only a few turns. Remember that the hamlet data frame had a turns column. A histogram of this variable will help you choose:

hamlet |>
    group_by(s1, s2) |>
    summarize(turns=sum(turns)) |>
    ggplot(aes(turns)) + geom_histogram(binwidth=5)
  1. Repeat the pipeline used to create ham_net but modify it to filter out pairs who interact only a few times. Decide what “few” should be. The requisite line will read something like filter(turns >= 2) (but choose a number bigger than 2).

    1. Describe: what are the “social” groups of the play?
    2. Who appear to be the key intermediaries?
    3. Go back to the original hamlet data frame, and use the complement of your filter to examine at least one example of what you have filtered out. Is the filtering valid? What would you need to know in order to decide?

Graph surgery

Finally, removing characters. To take Hamlet away, try:

no_ham <- ham_net - V(ham_net)["Hamlet"]

This says: no_ham is the graph created by removing the vertex “Hamlet” from ham_net.

  1. Use your ggraph call examine this graph.

  2. Now, using the diagram, figure out what you have to do to sever the graph into disconnected components.

Bonus: put sequence back

If you zip through all of the above, try going back and examining the graphs of each scene and/or each act of the play. Are some parts of the play differently configured than others, as networks?