suppressMessages({
library(ergm)
library(network)
library(networkdata)
})
data(lazega)
c(lawyers = network.size(lazega),
ties = network.edgecount(lazega),
directed = network::is.directed(lazega))
#> lawyers ties directed
#> 71 892 1
network::list.vertex.attributes(lazega)
#> [1] "Age" "Gender" "Office" "Practice" "School"
#> [6] "Seniority" "Status" "vertex.names" "Years"Ego-ERGMs: roles from local structure
Ancillary self-study · ICPSR Network Analysis: Advanced Topics
This document is not part of any taught session. Nobody is going to lecture it at you, nobody is going to grade you on it, and you can ignore it completely without falling behind.
It exists because it is a thing you will plausibly need next, and because it is easier to hand you something that works than to hand you a citation and wish you luck. Everything here runs end to end on the same machine setup you used in class. Every number and every figure on this page was produced by a chunk in this file: there are no screenshots of R output anywhere in the ancillary package.
How to work through it alone. Get the source (click the </> Code button at the top right, then View Source, and copy it into a .qmd file: or open the .qmd you were given directly), run the chunks in order, and read the prose between them: the prose is where the argument lives. Slow fits are identified in the prose, and precomputed fits load a shipped result while still showing you the code that produced it. Folded Depth boxes are optional extras; folded Stuck? boxes are hints. Open whichever you need and ignore the other. Exercises have a Solution tab: you are working alone, so use it.
Extends: the ERGM session (day 11, W3 Mon), and it connects back to Olga’s week-1 community detection.
The question it answers. Community detection asks “which nodes cluster together?” An ego-ERGM asks a different and often more useful question: “which nodes play the same structural role?” Here, “role” means “the local network around them has the same generative recipe,” even if they sit in completely different parts of the graph. Two managers in different departments can play the same brokering role; community detection puts them in different communities, an ego-ERGM puts them in the same role.
How it works, in one sentence. Fit a small ERGM to each node’s ego-network (the node plus its neighbours), then cluster nodes by the ERGM coefficients those local fits produce. Nodes whose local structure is generated the same way land in the same role.
Why it is optional in 2026: it is a specialised technique, it overlaps with material Olga already covered from a different angle, and it depends on a function from a GitHub-only package. Here for the people whose research is about roles rather than communities.
What you need: ergm, network, and ego_ergm() from networkdata (already installed for the course, but see the install note below if you are on your own machine).
ego_ergm() is not on CRAN
ego_ergm() lives in the networkdata package by Olga Chyzh, installed from GitHub, not CRAN. On the course machines it is already there. On your own machine:
# install.packages("remotes")
remotes::install_github("ochyzh/networkdata")Everything else in this document is CRAN-standard ergm. If the install fails, the concepts in §1 and §5 are still worth reading: the method is simple enough to understand without running it.
1. The idea, before the code
Here is the recipe the ego_ergm() function automates, so that the output makes sense rather than arriving as a black box:
- For each node, extract its ego-network: the node, its neighbours, and the ties among them.
- Fit a small ERGM to each ego-network, using a handful of local terms (edges, mutuality, triangles). This gives every node a short vector of coefficients describing how its local neighbourhood is put together.
- Cluster those coefficient vectors with a mixture model. Nodes whose ego-networks are generated by similar coefficients get grouped into the same role.
- Report one representative ERGM per role: the generative recipe for that kind of local structure.
The output is not “node 12 is in community 3.” It is “node 12 plays role B, and role B is the kind of local neighbourhood where ties are reciprocated but rarely closed into triangles.” That is a statement about position type, and it is invariant to where in the graph the node sits.
Community detection partitions nodes by connection: a community is a set of nodes densely tied to each other. Two nodes in the same community are, more or less by definition, near each other in the graph.
An ego-ERGM partitions nodes by generative structure: a role is a set of nodes whose local neighbourhoods follow the same ERGM. Two nodes in the same role may be on opposite sides of the graph with no path between their neighbourhoods: they share a role because the rules generating their local ties are the same.
The canonical example is brokerage. A broker sits between two dense clusters; its ego-network has a distinctive low-transitivity, high-degree signature. Every broker in the graph shares that signature and lands in the same ego-ERGM role, while community detection scatters them across whichever communities they happen to bridge. If your theory is about kinds of position (brokers, hubs, peripherals), roles are what you want. If it is about groups, communities are.
This is “structural equivalence” in the classic blockmodeling sense, approached through a generative model rather than through algebraic role-equivalence. It connects directly to the blockmodeling day (day 9, W2 Thu).
2. The data
Lazega’s law firm: 71 lawyers, with directed ties recording who works with whom, plus attributes like office, seniority, and practice area. It ships with networkdata.
3. Fitting the ego-ERGM
We ask for three roles and describe each ego-network with three local terms: edges (density), mutual (reciprocity), and triangle (closure). core_size = 1 means “ego plus its immediate neighbours”; min_size = 5 drops any ego-network too small to fit an ERGM to.
role_fit <- fit_or_load("ego_lazega",
ego_ergm(net = lazega,
form = c("edges", "mutual", "triangle"),
core_size = 1,
min_size = 5,
roles = 3,
directed = TRUE,
edge_covariates = FALSE,
seed = 6886,
steps = 50,
tol = 1e-6))names(role_fit)
#> [1] "model.fit" "lambda" "group.theta" "EE.BIC"
#> [5] "role_assignments" "reduced_networks" "form"sampson from sna and it failed
The original answer script for this material began by loading a sampson dataset from the sna package to test the pipeline, and that line errors: data set 'sampson' not found. The reason is mundane: sampson lives in ergm, not sna (data(sampson, package = "ergm")). It is a good illustration of a general habit: when data("something") fails, the dataset almost always exists, just in a different package. data(package = .packages(all.available = TRUE)) lists everything available, and searching that list for the name is usually the fastest way to find it.
We do not need sampson here: Lazega is the example, so this is only a note for anyone working from the old script.
4. Reading the roles
The group.theta matrix has one row per role and one column per term. It is the generative recipe for each kind of local neighbourhood.
role_theta <- role_fit$group.theta
dimnames(role_theta) <- list(paste("role", seq_len(nrow(role_theta))),
c("edges", "mutual", "triangle"))
round(role_theta, 3)
#> edges mutual triangle
#> role 1 0.255 0.306 3.62e-01
#> role 2 0.666 0.792 1.58e-01
#> role 3 0.519 1.423 -1.00e+06assignments <- role_fit$role_assignments[, 2]
table(role = assignments)
#> role
#> 1 2 3
#> 28 41 1Read the roles off the coefficients, comparing the two substantive roles (the third is a singleton: see the caveat below). They trade off reciprocity against closure, which is exactly the kind of distinction ego-ERGMs are built to surface:
- The larger role (41 lawyers) has a clearly higher
mutualcoefficient but a lowertrianglecoefficient: local neighbourhoods where working relationships are reciprocated but do not close into tight triads. Think coordinating positions: you work back-and-forth with many people who are not themselves connected. - The smaller role (28 lawyers) has lower reciprocity but relatively higher closure: local neighbourhoods that are more triangulated, more clique-like.
That is a statement about kind of position, and: the whole point: two lawyers can share a role while working in different offices on different practice areas. Note this is the opposite of a “some are central, some are peripheral” story: both roles are substantive structural types, not a core-periphery ranking.
Look at the role sizes: two substantive roles and a third role with a single member. That lone-member role is not a finding: it is the mixture model isolating one lawyer whose ego-network is an outlier and could not be forced into either main group.
This is characteristic of ego-ERGMs on smallish networks, and you should treat it as a warning about the method’s stability rather than as a discovered “role.” Three things follow:
- Do not over-interpret a singleton or near-singleton role. It is often an artefact.
- The number of roles is a choice you impose, not something the data hand you. Try
roles = 2androles = 4and see whether the substantive story is stable. If it is not, the roles are not robust and you should say so. - Compare the package’s
EE.BICscore, where larger (less negative) is better. The function computes \(2\ell - k\log N\), the negative of the conventional BIC orientation. Do not trust the score alone on a network this small. The same asymptotic cautions from the ERGM session apply here.
Honesty about this is more useful than a clean-looking three-role table would have been. On a larger network the roles are far more stable; on 71 nodes they are suggestive, not settled.
round(role_fit$EE.BIC, 1)
#> [1] -29299.1
checkpoint(roles = nrow(role_theta),
largest_role = max(table(assignments)),
BIC = round(role_fit$EE.BIC, 0))
#> ------------------------------------------------------------------
#> CHECKPOINT: roles = 3 | largest_role = 41 | BIC = -29299
#> ------------------------------------------------------------------5. Exercise
Refit with two roles instead of three and compare. Does the two-role solution give you two substantively interpretable groups, or does it also isolate an outlier?
role_fit2 <- ego_ergm(net = lazega, form = c("edges", "mutual", "triangle"),
core_size = 1, min_size = 5, roles = ______,
directed = TRUE, edge_covariates = FALSE,
seed = 6886, steps = 50, tol = 1e-6)
table(role_fit2$role_assignments[, 2])Then ask which solution has the higher (less negative) EE.BIC score and which one you would actually report? Explain whether the numerical and substantive criteria agree.
role_fit2 <- fit_or_load("ego_lazega_2",
ego_ergm(net = lazega, form = c("edges", "mutual", "triangle"),
core_size = 1, min_size = 5, roles = 2,
directed = TRUE, edge_covariates = FALSE,
seed = 6886, steps = 50, tol = 1e-6))table(role = role_fit2$role_assignments[, 2])
#> role
#> 1
#> 70
c(BIC_2roles = round(role_fit2$EE.BIC, 0),
BIC_3roles = round(role_fit$EE.BIC, 0))
#> BIC_2roles BIC_3roles
#> -29459 -29299The result. The two-role solution is worse than three, not better: it collapses entirely, putting all 70 fittable lawyers into a single role and leaving the other role empty. So neither low-dimensional solution here produces the balanced, interpretable partition you would hope for: three roles gave you two groups plus an outlier, two roles gives you no separation at all. That instability is itself the finding: role structure in this 71-node firm is not cleanly separable, and the method is telling you so if you ask it more than once.
The score and what to report. EE.BIC is easy to read backwards because the field name says “BIC,” but the function computes \(2\ell-k\log N\). Larger (less negative) values are preferred. The three-role score is therefore better than the two-role score, and the two-role solution is also substantively degenerate. Both criteria favor three roles over two. That still does not turn the singleton third role into a stable discovery. On a network this small, the score informs your judgment; it does not replace stability checks across seeds and role counts.
What you would actually report: that ego-ERGM role structure on this network is unstable across the number of roles, that the recurring feature is one or two outlier egos rather than a robust partition, and that a firmer conclusion needs either a larger network or a substantive prior on how many roles to expect. That honest non-result is worth more than a confident three-role table you cannot reproduce at roles = 2.
6. Readings
- Salter-Townshend & Murphy (2015), “Role analysis in networks using mixtures of exponential random graph models,” Journal of Computational and Graphical Statistics 24(2):520–538. The method
ego_ergm()implements. - Lazega (2001), The Collegial Phenomenon: The Social Mechanisms of Cooperation Among Peers in a Corporate Law Partnership. Oxford University Press. The data and their substantive context.
- Connects to the blockmodeling session (day 9, W2 Thu): Abbe (2018) on stochastic block models: and to Olga’s week-1 community detection. Ego-ERGMs, blockmodels, and community detection are three answers to overlapping questions; knowing which one matches your theory is the useful skill.
Versions: expand if your numbers differ from mine
pk <- c("ergm", "network", "networkdata")
data.frame(package = pk,
version = sapply(pk, function(p) as.character(packageVersion(p))),
row.names = NULL)
#> package version
#> 1 ergm 4.12.0
#> 2 network 1.20.0
#> 3 networkdata 0.1
cat("R", as.character(getRversion()), "\n")
#> R 4.3.3