Structural Holes and Ego-networks

In the operationalization of his structural hole theory, Ron Burt (1992) devised a set of measures that quantify the degree to which an actor can access distinct resources through their contacts. In the following, we will explore these measures in the context of the Florentine families’ marriage network, which we is contained in the statnet suite:

library(statnet)
data(florentine)

The structural hole measures are not included in either statnet or igraph, but they are not that hard to implement, so we will implement our own as we go.

Extract ego-networks

Burt’s structural hole measures operate on ego-networks, i.e., they only take the direct neighborhood of a node into account. Since we have a full network here, the first thing we need is a way to extract a specified node’s ego-network. Helpfully, statnet provides the function ego.extract(...), which does exactly that:

ego_medici <- ego.extract(flomarriage, ego = 9)
ego_medici
$Medici
           Medici Acciaiuoli Albizzi Barbadori Ridolfi Salviati Tornabuoni
Medici          0          1       1         1       1        1          1
Acciaiuoli      1          0       0         0       0        0          0
Albizzi         1          0       0         0       0        0          0
Barbadori       1          0       0         0       0        0          0
Ridolfi         1          0       0         0       0        0          1
Salviati        1          0       0         0       0        0          0
Tornabuoni      1          0       0         0       1        0          0

We see that the output of ego.extract is the adjacency matrix of the ego-network, wrapped in a named list, from which we can extract its sole element with $Medici or with double square braces and a string index ([["Medici"]]). From this adjacency matrix, we can then easily reconstruct a network object:

ego_medici <- network(ego_medici[["Medici"]], directed=FALSE)

Looking at the Medici’s ego-network, we can see that only a single pair of their alteri is connected:

gplot(ego_medici, gmode = "graph",
      label = ego_medici %v% "vertex.names",
      vertex.col = "black", edge.col = "grey70")

Effective size and efficiency

We are now ready to compute the measures. The effective size reduces the size of the ego-network \(n\) (i.e, the number of alteri) by the average degree of the alteri among each other:

\[n - \frac{2t}{n}\]

Here, \(t\) is the number of edges among the alteri, which we multiply by two because each edge contributes to the degree of two actors. To compute this in R, we write a function called egonet_effsize(...) which takes in an ego-network and returns the above number.

Effective size implementation
egonet_effsize <- function(egonet) {
  n <- network.size(egonet) - 1
  t <- network.edgecount(egonet) - n
  return(n - 2 * t / n)
}

Let’s call it on the Medici ego-network extracted before:

egonet_effsize(ego_medici)
[1] 5.666667

As we can see, the Medici’s degree of 6 got reduced slightly due to the one marriage tie among their alteri. A related quantity is the efficiency, which is just the ego-network’s size divided by its effective size. We can easily implement it using the definition of effective size above:

Efficiency implementation
egonet_efficiency <- function(egonet){
  size <- network.size(egonet) - 1
  effsize <- egonet_effsize(egonet)
  return(effsize/size)
}

We can again call our new efficiency function on the Medici ego-network:

egonet_efficiency(ego_medici)
[1] 0.9444444

This value is close to one because there is very littly redundancy in the ego-network, i.e., the Medici have ties to other families which are largely disconnected from each other

Network constraint

The network constraint is slightly more involved but is also designed to measure the degree to which a node’s network is constrained by redundancy, i.e., strong cohesion of their alteri.

It is formally defined as the sum of dyadic constraints \(c_{ij}\) over all neighbors of node \(i\), \(N(i)\):

\[ \sum_{j \in N(i)}c_{ij} \]

The dyadic constraint is in turn defined as the degree to which ego (i.e., node \(i\)) is invested into an alter \(j\) that the other alteri are also heavily invested in and is given by:

\[ c_{ij} = \left(p_{ij} + \sum_{q \in N(i) - j} p_{iq} p_{qj} \right)^2, \]

where \(p_{ij}\) is the investment of ego \(i\) in alter \(j\) and is defined in terms of the elements of the adjacency matrix, \(a\):

\[ p_{ij} = \frac{a_{ij} + a_{ji}}{\sum_j (a_{ij} + a_{ji})} \]

By default the constraint has no simple bounds (e.g., 0 and 1) and so we have to contend ourselves with the notion that higher constraint indicates more redundancy in an ego-network and thus a decrease in social capital.

Node level constraint is the sum of the dyadic constraint for each of ego’s alters. We here provide an implementation of both:

Constraint
dyadic_constraint <- function(egonet, alter) {
  pij <- egonet[1, alter] / sum(egonet[1,])
  pqj <- egonet[alter,-1] / sum(egonet[alter,])
  pqj[is.nan(pqj)] <- 0
  return((pij + sum(pij * pqj))^2)
}

egonet_constraint <- function(egonet) {
  alteri <- 2:network.size(egonet)
  dc <- sapply(alteri, function(a) dyadic_constraint(egonet, a))
  return(sum(dc))
}

We can now compare the degree to which the Medici’s social capital is constrained by, e.g., the Ridolfi (node 5) compared to the Albizzi (node 3):

dyadic_constraint(ego_medici, 5)
[1] 0.0625
dyadic_constraint(ego_medici, 3)
[1] 0.02777778

Similarly, we can compute the overall constraint:

egonet_constraint(ego_medici)
[1] 0.2361111

Apply a measure to all ego-networks

So far, we have only computed the structural hole measures for a single ego-network. Usually, we want to compare nodes on these measures, e.g., because we expect that constraint makes a difference for other node-level outcomes.

To automate the process of extracting ego-networks and applying a function to them, we here provide the apply_egonet(...) function, wich takes a network and a function to compute a statistic of interest on each extracted ego-network as arguments.

Apply to network
apply_egonet <- function(net, FUN) {
  if (is.directed(net)) stop("Not implemented for directed graphs.")
  egonets <- lapply(ego.extract(net), FUN = network, directed = FALSE)
  return(sapply(egonets, FUN))
}

Because the structural hole measures are not all stringently defined for isolated nodes, we first remove them from the network:

isol <- isolates(flomarriage)
noisol <- seq(network.size(flomarriage))[-isol]
flomarriage_noisol <- flomarriage %s% noisol 

Now we can compute the constraint for each ego-network using apply_egonet as follows:

apply_egonet(flomarriage_noisol, egonet_constraint)
  Acciaiuoli      Albizzi    Barbadori     Bischeri   Castellani       Ginori 
   1.0000000    0.3333333    0.5000000    0.6111111    0.6111111    1.0000000 
    Guadagni Lamberteschi       Medici        Pazzi      Peruzzi      Ridolfi 
   0.2500000    1.0000000    0.2361111    1.0000000    0.8086420    0.6111111 
    Salviati      Strozzi   Tornabuoni 
   0.5000000    0.5173611    0.6111111 

We can see that the Medici have the lowest constraints of all the Florentine families.

Exercises

  1. Load the Lazega advice network into an R session.
Solution
adjmat <- read.table("data/lazega_advice.csv", 
                     sep =";", 
                     header = TRUE, 
                     row.names = 1, 
                     check.names = FALSE)

adjmat <- as.matrix(adjmat)

net_advice <- network(adjmat)
net_advice
  1. Symmetrize the network, using the weak rule.
Solution
net_sym <- network(symmetrize(net_advice), directed=FALSE)
  1. Extract the ego-networks for node 40 and node 33.
Solution
e1 <- network(ego.extract(net_sym, ego=33)[["33"]], directed=FALSE)
e2 <- network(ego.extract(net_sym, ego=40)[["40"]], directed=FALSE)
  1. Plot the two ego-networks side by side. Color the ego node differently than the alteri and increase its size.
Solution
par(mfrow = c(1,2), mar=c(0,0,3,0))

gplot(e1,
      gmode = "graph",
      vertex.col = c("red", rep("grey20", network.size(e1) - 1)),
      vertex.cex = c(2, rep(1, network.size(e1) - 1)),
      edge.col = "grey70")

gplot(e2,
      gmode = "graph",
      vertex.col = c("red", rep("grey20", network.size(e2) - 1)),
      vertex.cex = c(2, rep(1, network.size(e2) - 1)),
      edge.col = "grey70")
  1. Compute the degree centrality, effective size, and efficiency for the two nodes. What do you observe?
Solution
degree(e1, gmode="graph")[1]
degree(e2, gmode="graph")[1]

egonet_effsize(e1)
egonet_effsize(e2)

egonet_efficiency(e1)
egonet_efficiency(e2)
  1. Compute the degree, constraint, effective size, and efficiency for all nodes in the network.
Solution
degree <- degree(net_sym, gmode="graph")
constraint <- apply_egonet(net_sym, egonet_constraint)
effsize <- apply_egonet(net_sym, egonet_effsize)
efficiency <- apply_egonet(net_sym, egonet_efficiency)
  1. Create a pairs plot (see session on centrality) of the structural hole measures and degree centrality. What do you observe?
Solution
source("helpers/pairsplot.R") # load helper script to improve the plot

measures <- cbind(degree, constraint, effsize, efficiency)

pairs(measures,
      upper.panel = panel.cor,
      diag.panel = panel.hist)