Fitting models#
ergmx.ergm() fits a model to a network:
fit = ergmx.ergm(network, formula, seed=1)
How it fits depends on whether the model’s terms are dyad-independent:
whether the statistics’ change from adding a tie depends only on that tie
(edges, nodematch, nodecov…) or also on the rest of the network
(mutual, triangle, gwesp…).
Dyad-independent models: the exact MLE#
Without dependence, an ERGM is a logistic regression on the dyads, and
ergmx computes its maximum likelihood estimate exactly, with the
log-likelihood, AIC and BIC. Do wealthier Florentine families marry more?
import ergmx
from ergmx import datasets
flomarriage = datasets.load("flomarriage")
fit = ergmx.ergm(flomarriage, "edges + nodecov('wealth')")
fit.summary()
Maximum Likelihood Results:
Estimate Std. Error MCMC % z value Pr(>|z|)
edges -2.5949 0.5361 0 -4.841 <1e-04 ***
nodecov.wealth 0.0105 0.0047 0 2.256 0.02406 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Log-likelihood: -51.5543 AIC: 107.1086 BIC: 112.6836
Dyad-dependent models: the Monte Carlo MLE#
With dependence, the likelihood has a normalizing constant that sums over
every possible network, so ergmx estimates it by MCMC, as ergm does:
It starts from the maximum pseudo-likelihood estimate (MPLE).
It simulates networks at the current coefficients, in parallel chains, and moves the coefficients towards values whose simulated networks have, on average, the observed statistics (Hummel et al. 2012 step lengths with a log-normal approximation).
It stops when two consecutive steps are full steps, then draws a larger final sample for the standard errors.
mesa = datasets.load("faux.mesa.high")
fit = ergmx.ergm(
mesa, "edges + nodematch('Grade') + nodematch('Race') + gwesp(0.5, fixed=TRUE)", seed=1
)
fit.summary()
Monte Carlo Maximum Likelihood Results:
Estimate Std. Error MCMC % z value Pr(>|z|)
edges -6.3235 0.1541 0 -41.045 <1e-04 ***
nodematch.Grade 1.9798 0.1748 0 11.327 <1e-04 ***
nodematch.Race 0.2658 0.1184 0 2.244 0.02482 *
gwesp.fixed.0.5 1.2354 0.0831 0 14.864 <1e-04 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Log-likelihood: -868.4783 (MC SE 0.222) AIC: 1744.9566 BIC: 1776.7485
Converged after 6 iterations (4 chains, 1024 samples).
The standard errors include the Monte Carlo error of the estimate; the
MCMC % column is its share of the variance, as in ergm. A last line says
whether the estimation converged. The log-likelihood is estimated too: see
Model comparison.
The estimates are also available directly:
fit.coef
{'edges': -6.323542379477252,
'nodematch.Grade': 1.9797518041863202,
'nodematch.Race': 0.2657610639159213,
'gwesp.fixed.0.5': 1.2354403176948652}
fit.stderr
{'edges': 0.1540651294125193,
'nodematch.Grade': 0.17478930077864277,
'nodematch.Race': 0.11842283149765728,
'gwesp.fixed.0.5': 0.08311865912584615}
fit.params and fit.cov give the same as an array and a matrix, in the
order of fit.names.
Seeds and threads#
The MCMC is random: pass seed= for reproducible results. Chains run in
parallel threads, four by default (or one per CPU, if fewer), and a fit is
reproducible for a given seed and number of chains. n_chains=1 runs on a
single thread.
To watch the estimation, turn on logging:
import logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
fit = ergmx.ergm(mesa, "edges + nodematch('Grade') + gwesp(0.5, fixed=TRUE)", seed=2)
logging.getLogger().setLevel(logging.WARNING)
iteration 1: interval 1024, effective size 69, step length 0.72
iteration 2: interval 1024, effective size 39, step length 1.00
iteration 3: interval 2048, effective size 42, step length 1.00
iteration 4: interval 4096, effective size 102, step length 1.00
iteration 5: interval 4096, effective size 111, step length 1.00
final iteration: interval 4096, step length 1.00, p-value 0.328
Each iteration reports the MCMC interval (proposals between sampled networks), the effective sample size and the step length. When the samples are too autocorrelated, the interval grows.
Settings#
ergmx.Control holds the settings of the MCMC and the estimation, with
ergm’s defaults. Pass a Control, or override single settings as keyword
arguments:
ergmx.ergm(mesa, formula, samplesize=2048, n_chains=8)
ergmx.ergm(mesa, formula, control=ergmx.Control(interval=4096))
The most useful are samplesize (networks per iteration), interval,
burnin, n_chains and effective_size (the effective sample size the
interval adapts to).
Other estimates and starting values#
estimate="MPLE" stops at the MPLE, which is quick but has unreliable
standard errors for dyad-dependent models. estimate="CD" stops at the
contrastive divergence estimate: the coefficients at which networks a few
MCMC steps away from the observed one have, on average, its statistics. It has
no standard errors.
init sets where the Monte Carlo MLE starts: "MPLE" (the default), "CD",
or your own coefficients, such as those of a previous fit:
ergmx.ergm(mesa, formula, init="CD")
ergmx.ergm(mesa, formula, init=previous_fit.params)
When a fit doesn’t converge, or stops with a DegeneracyError, see
Convergence and MCMC diagnostics.
Estimating the decay: curved models#
gwesp(0.5, fixed=TRUE) fixes the decay at 0.5. Without fixed=TRUE, as is
ergm’s default, the decay is estimated with the other coefficients, starting
from 0.5, and the model is a curved exponential family:
curved = ergmx.ergm(
mesa, "edges + nodematch('Grade') + nodematch('Race') + gwesp(0.5)", seed=1
)
curved.summary()
Monte Carlo Maximum Likelihood Results:
Estimate Std. Error MCMC % z value Pr(>|z|)
edges -6.3659 0.1623 0 -39.220 <1e-04 ***
nodematch.Grade 1.9763 0.1736 0 11.381 <1e-04 ***
nodematch.Race 0.2715 0.1164 0 2.332 0.01969 *
gwesp 1.3441 0.1373 0 9.788 <1e-04 ***
gwesp.decay 0.3815 0.1053 0 3.624 0.00029 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Log-likelihood: -868.2745 (MC SE 0.192) AIC: 1746.5491 BIC: 1786.2890
Converged after 6 iterations (4 chains, 1024 samples).
gwesp.decay is the estimated decay, with its standard error. The model’s
statistics are then the counts of ties with each number of shared partners
(curved.observed), which the two parameters weight: see the
term reference. R’s ergm fits this model only
from a contrastive divergence start (init.method = "CD"): from its default
start, a simulated network exceeds its cutoff of 30 shared partners and the
fit stops with an error. ergmx counts those ties in an overflow statistic, and
its estimates match R’s.
Fixed coefficients#
offset(term) fixes a term’s coefficients instead of estimating them, at the
values given as offset_coef, in formula order:
fixed = ergmx.ergm(flomarriage, "offset(edges) + nodecov('wealth')", offset_coef=[-2.6])
fixed.summary()
Maximum Likelihood Results:
Estimate Std. Error MCMC % z value Pr(>|z|)
offset(edges) -2.6000 (offset)
nodecov.wealth 0.0106 0.0022 0 4.838 <1e-04 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Log-likelihood: -51.5543 AIC: 105.1087 BIC: 107.8962
Offsets don’t count as parameters in AIC and BIC. A coefficient of -inf
forbids the ties the term counts: see Fixed coefficients.
Constraints and missing ties#
constraints= restricts the networks the model puts probability on, for
example to bounded degrees in fixed-choice designs: see Constraints.
Dyads whose value is unknown are marked as edges with na=True; the fit is
then conditional on the observed ones: see Missing ties.
Saving fits#
A fit takes seconds to minutes; fit.save(path)
keeps it, with its network, estimates, MCMC sample and settings, and
ergmx.load_fit() brings it back, to summarize, simulate, check or
compare as before (R’s saveRDS() and readRDS()):
fit.save("mesa_fit.pkl")
fit = ergmx.load_fit("mesa_fit.pkl")
The file is a Python pickle (the fits pickle, too, with pickle.dump()),
so load only files you trust; loading a file saved by another version of
ergmx warns.