Networks over time#

A panel study observes the same people’s network several times: the friendships in a school each year, the advice ties in a firm before and after a merger. A temporal ERGM, as R’s tergm fits, models each network given the one before it: which ties form, which persist, and which dissolve.

Sampson’s monks named the brothers they liked at three times, before a crisis split the monastery:

import ergmx
from ergmx import datasets

waves = [datasets.load(f"samplk{t}") for t in (1, 2, 3)]
[g.ecount() for g in waves]
[55, 57, 56]

Formation and persistence#

tergm’s operators evaluate terms on views of each transition, from a previous network to the current one:

Operator

Evaluates its terms on

A positive coefficient means

Form()

the union of the previous and the current network

more ties form

Persist()

their intersection: the ties that persisted

more ties persist

Diss()

the same, with the statistics negated

more ties dissolve

Cross()

the current network

(a cross-sectional effect)

Change()

the dyads that changed

more change

Form(~edges) only changes when a tie forms, and Persist(~edges) when one persists or dissolves, so a model of only Form() and Persist() (or Diss()) is separable (Krivitsky and Handcock 2014): formation and dissolution are independent given the previous network, each an ERGM of its own. Terms outside the operators describe the current network, as Cross().

ergmx.tergm() fits the model to the transitions of a series by conditional maximum likelihood (CMLE), as tergm(..., estimate="CMLE"):

fit = ergmx.tergm(
    waves,
    "Form(~edges + mutual + gwesp(0.5, fixed=TRUE)) + Persist(~edges + mutual)",
    seed=1,
)
fit.summary()
Monte Carlo Conditional Maximum Likelihood Results:

                              Estimate  Std. Error  MCMC %  z value  Pr(>|z|)
Form(1)~edges                  -3.7735      0.3662       0  -10.304    <1e-04 ***
Form(1)~mutual                  1.9429      0.4092       0    4.748    <1e-04 ***
Form(1)~gwesp.OTP.fixed.0.5     0.3603      0.1530       0    2.356   0.01849 *
Persist(1)~edges                0.4592      0.2588       0    1.774   0.07599 .
Persist(1)~mutual               0.7163      0.4880       0    1.468   0.14221 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Log-likelihood: -188.1562 (MC SE 0.169)   AIC: 386.3123   BIC: 408.3960
Fitted to 2 transitions, each conditional on the network before it.
Converged after 3 iterations (4 chains, 1024 samples).

Formation is rare (the Form(1)~edges coefficient), but much more likely for a tie that reciprocates one (mutual) or closes two-paths (gwesp). Liking, once there, tends to persist, a little more so when reciprocated. R’s tergm gives the same estimates. The names follow tergm, with the linear model’s column as in N(): Form(1)~edges.

Every transition has the same coefficients by default. The operators’ lm argument lets them change over time, with the attributes .Time (the time of the current network: 1 and 2 here), .TimeID (the transition’s position) and .TimeDelta (the time since the previous network): Form(~edges, lm=~.Time) estimates a trend in formation. ergmx.NetSeries() builds the series with its times, when they are not 0, 1, 2…; tergm() builds it from a list. The operators also take N()’s subset, offset and label.

With a dyad-independent model, the CMLE is a logistic regression and exact: Form(~edges) + Persist(~edges) gives the log-odds that a non-tie became a tie, and that a tie persisted. Dyad-dependent models are fitted by Monte Carlo MLE; their samples mix, as in tergm, proposals that toggle a dyad that differs from the previous network, which undo formations and dissolutions efficiently. Diagnostics, goodness of fit (also by transition, with ergmx.gofN()) and model comparison work as for other fits.

Missing dyads#

Missing dyads (na edges) of the networks transitioned to are treated as missing, as in a cross-sectional fit. Those of the networks transitioned from (all but the last) are what the next transition is conditioned on, and must be imputed first, as tergm’s NA.impute: na_impute="next" (each missing dyad takes its value in the next wave), "previous", "majority" (the more common value of the network’s observed dyads), "0" or "1"; several apply in turn, so ["previous", "0"] fills what the previous wave can’t with non-ties. NetSeries() and tergm() take it:

fit = ergmx.tergm(waves_with_nonresponse, "Form(~edges + mutual) + Persist(~edges)",
                  na_impute="next")

Simulating the process#

A fitted temporal model describes a process: start from a network, and draw each next one from the model given the current one. fit.simulate(time_slices=...) runs it forward from the last network of the series (or nw_start="first", a position, or a network), as tergm’s simulate(fit, nw.start=, time.slices=). Coefficients that change over time (lm=~.Time) continue their trend: the k-th step after the last network has the time .Time + k .TimeDelta and the position .TimeID + k, and the linear models’ predictions for them.

future = fit.simulate(time_slices=20, seed=1, monitor="edges + mutual")
future
DynamicSimulation: 20 time steps from a network of 18 vertices

time   edges  formed  dissolved  MCMC steps
   0      56                               
   1      56      21         21        3102
   2      62      24         18        3956
   3      65      21         18        3479
   4      64      19         20        5092
   5      55      18         27        4721
 ...
  16      68      18         20        3643
  17      53      10         25        3084
  18      49      13         17        2936
  19      47      18         20        3754
  20      47      17         17        5449

The result, a ergmx.DynamicSimulation, has the networks (future.networks), the model’s statistics of each transition (future.stats), the monitor formula’s statistics of each network, the ties that formed and dissolved at each step, and their durations:

finished, ongoing = future.durations()
print(f"{len(finished)} ties dissolved after {finished.mean():.1f} steps on average; "
      f"{len(ongoing)} still present")
future.monitor["mutual"]
382 ties dissolved after 2.8 steps on average; 47 still present
array([13., 16., 15., 17., 14., 10., 14., 13., 13., 13., 14., 14., 16.,
       16., 21., 23., 15.,  9.,  9., 11.])

ergmx.simulate_dynamic() simulates from any coefficients and starting network. A time step’s Markov chain runs as in tergm: from the current network, until the number of dyads that differ from it stops growing (a test on its exponentially weighted increments), then as long again. Its length adapts to the model and the network size; min_steps and max_steps bound it, and equal values fix it.

A process from one network: the EGMME#

Often there is a single network, a cross-section, and some knowledge of how long ties last: a survey of current partnerships, with their mean duration. tergm’s equilibrium generalized method of moments (EGMME) finds a process whose equilibrium matches both: coefficients of formation and persistence such that, simulated for a long time, the network has the observed statistics and its ties the observed ages. tergm(estimate="EGMME") takes the network, the model, and targets, a formula of the statistics to match, which can include statistics of tie ages: mean.age, edge.ages (their sum), edges.ageinterval(from, to), edgecov.ages(x) and nodefactor.mean.age(attr), with a tie’s age 1 in the step it formed, as in tergm.

The Florentine marriages, as a process where marriages last ten time steps on average and families with a common partner marry more readily:

flo = datasets.load("flomarriage")
observed = ergmx.summary_stats(flo, "edges + gwesp(0, fixed=TRUE)")
egmme = ergmx.tergm(
    flo,
    "Form(~edges + gwesp(0, fixed=TRUE)) + Persist(~edges)",
    estimate="EGMME",
    targets="edges + gwesp(0, fixed=TRUE) + mean.age",
    target_stats=[*observed.values(), 10],
    seed=1,
)
egmme
Equilibrium Generalized Method of Moments Results:

                     Estimate  Std. Error   z value   Pr(>|z|)
Form~edges            -3.9943      0.4492    -8.892   6.03e-19
Form~gwesp.fixed.0     0.1372      0.3698     0.371      0.711
Persist~edges          2.1958      0.1945    11.287   1.52e-29

Targets:

                  target  simulated
edges             20.000     20.906
gwesp.fixed.0      8.000      8.704
mean.age          10.000      9.818

Converged.

The estimate is found as tergm does: from EpiModel’s approximation of the edges coefficients (formation, the log-odds of the density minus the log of the mean duration; persistence, the log of the duration minus one), stochastic approximation with Polyak averaging, and the delta method’s standard errors, with the gradient of the targets estimated by central differences under common random numbers. It needs at least as many targets as coefficients. The fit’s simulate() runs the process, and its tie ages are monitors there too:

run = egmme.simulate(time_slices=500, seed=2, monitor="edges + mean.age")
run.monitor["edges"][100:].mean(), run.monitor["mean.age"][100:].mean()
(np.float64(20.4825), np.float64(10.408881552375817))