The remverse suite provides a three-step pipeline for
relational event modeling:
remify() — process a raw edgelist into
a standardized event historyremstats() — compute network
statistics (endogenous and exogenous)remstimate() — estimate model
parametersThis vignette walks through the full pipeline for a range of modeling choices: tie-oriented vs. actor-oriented models, interval vs. ordinal timing, different risk set definitions, typed events, and a variety of endogenous and exogenous statistics. Each section builds on the same dataset so the results can be compared directly.
Throughout the remverse vignettes we use the simulated
dataset randomREH3, provided with remverse. It
is a sequence of 999 directed events among 5 actors, generated from a
known relational event model so that the recovered coefficients can be
checked against the truth.
data(randomREH3) # event history
data(info3) # actor attributes
head(randomREH3)
#> time actor1 actor2 end setting duration
#> 2 18 5 1 24 social 6
#> 3 57 3 2 70 social 13
#> 4 64 1 5 85 social 21
#> 5 127 5 1 159 social 32
#> 6 228 1 4 239 work 11
#> 7 274 2 4 320 work 46
head(info3)
#> name time age sex
#> 1 1 0 24 0
#> 2 2 0 28 0
#> 3 3 0 19 1
#> 4 4 0 20 1
#> 5 4 10000 21 1
#> 6 5 0 28 0Each row of randomREH3 records a time
(event start time), a sender (actor1), a
receiver (actor2), a setting
(event type: "work" or "social"), an
end time, and a duration
(end - time). The duration column can double
as an intrinsic event weight. The
end/duration columns are only used by the
duration model (see the Duration Relational Event Models
vignette); the tie- and actor-oriented models in this vignette use
time, actor1, actor2, and
setting only.
info3 provides actor attributes: age and
sex, indexed by actor name and a
time of change (so age can be time-varying — actors 4 and 5
age during the observation window).
The simplest specification: a directed tie-oriented model with interval timing (exact event times), a full risk set, and a few endogenous statistics.
# Step 1: process the event history
reh <- remify(edgelist = randomREH3, model = "tie", directed = TRUE)
summary(reh)
#> Relational Event History processed for tie-oriented modeling:
#> > events = 999 (time points = 993)
#> > actors = 5
#> > event types = 1
#> > riskset = full
#> >> included dyads = 20
#> > directed = TRUE
#> > ordinal = FALSE
#> > weighted = FALSE
#> > time length ~ 50676
#> > interevent time
#> >> minimum ~ 1
#> >> median ~ 36
#> >> maximum ~ 448
# Step 2: define effects and compute statistics
effects <- ~ inertia(scaling = "std") + reciprocity(scaling = "std") +
outdegreeSender(scaling = "std") + isp(scaling = "std")
stats <- remstats(reh = reh, tie_effects = effects)
stats
#> Relational Event Network Statistics
#> > Model: tie-oriented
#> > Computation method: per time point
#> > Dimensions: 992 time points x 20 dyads x 5 statistics
#> > Statistics:
#> >> 1: baseline
#> >> 2: inertia
#> >> 3: reciprocity
#> >> 4: outdegreeSender
#> >> 5: isp
# Step 3: estimate the model
fit <- remstimate(reh = reh, stats = stats)
summary(fit)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + reciprocity(scaling = "std") + outdegreeSender(scaling = "std") + isp(scaling = "std")
#>
#>
#> Coefficients (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -7.253328 0.043175 -167.999084 0e+00 < 2.2e-16
#> inertia 0.421529 0.048117 8.760442 0e+00 6.811e-16
#> reciprocity 0.201510 0.047128 4.275845 0e+00 0.003363
#> outdegreeSender -0.121893 0.034797 -3.502931 5e-04 0.063840
#> isp -0.363749 0.060352 -6.027102 0e+00 4.075e-07
#> Null deviance: 15813.97 on 992 degrees of freedom
#> Residual deviance: 15227.19 on 987 degrees of freedom
#> Chi-square: 586.7778 on 5 degrees of freedom, asymptotic p-value 0
#> AIC: 15237.19 AICC: 15237.25 BIC: 15261.69The baseline intercept is automatically included for interval timing
(ordinal = FALSE). It captures the average event rate
across all dyads. The endogenous effects quantify how the accumulated
event history shapes future interaction patterns: inertia
measures the tendency to repeat past interactions,
reciprocity captures the tendency to reciprocate received
events, outdegreeSender captures sender activity, and
isp (incoming shared partners) captures a form of triadic
closure. Standardising the statistics (scaling = "std")
puts the coefficients on a comparable scale.
By default remstimate() fits the model by maximum
likelihood (the frequentist approach). The estimation
approach and model structure are controlled by the
approach, random, penalty, and
mixture arguments — see the dedicated vignettes on frailty
(GLMM), penalized, and mixture models.
diag <- diagnostics(object = fit, reh = reh, stats = stats)
diag
#> Diagnostics for a Relational Event Model
#> ------------------------------------------
#> Model : tie
#> Actors : 5
#> Events : 992
#>
#> Tie model
#> Statistics : inertia, reciprocity, outdegreeSender, isp
#> Recall : mean rank = 0.724 | prob ratio = 1.61 | top 5% = 9.8% | lowest 20% = 2.3%
plot(x = fit, reh = reh, diagnostics = diag)#> Warning in regularize.values(x, y, ties, missing(ties), na.rm = na.rm):
#> collapsing to unique 'x' values
The diagnostics() function computes recall statistics:
for each time point, the observed event is ranked among all competing
dyads based on its predicted rate. A well-fitting model assigns high
ranks to observed events.
When only the order of events matters (or when exact timestamps are
unreliable), set ordinal = TRUE. The model reduces to a
stratified Cox proportional-hazard model — no baseline intercept is
estimated.
reh_ord <- remify(edgelist = randomREH3, model = "tie", ordinal = TRUE)
stats_ord <- remstats(reh = reh_ord,
tie_effects = ~ inertia(scaling = "std") +
reciprocity(scaling = "std"))
fit_ord <- remstimate(reh = reh_ord, stats = stats_ord)
summary(fit_ord)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + reciprocity(scaling = "std")
#>
#>
#> Coefficients (MLE with ordinal likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> inertia 0.435997 0.041208 10.580335 0 < 2.2e-16
#> reciprocity 0.298866 0.042690 7.000838 0 7.17e-10
#> Null deviance: 5936.275 on 992 degrees of freedom
#> Residual deviance: 5406.784 on 990 degrees of freedom
#> Chi-square: 529.4906 on 2 degrees of freedom, asymptotic p-value 0
#> AIC: 5410.784 AICC: 5410.796 BIC: 5420.584The coefficients are comparable to the interval model but the intercept and inter-event-time offset are absent.
The risk set is the collection of dyads considered
“at risk” of an event at each time point — the denominator against which
each observed event is compared. It is chosen with the
riskset argument of remify(), which takes four
values.
The default, riskset = "full", puts every
possible directed dyad at risk at every time point: \(D = N(N-1)\) dyads (or \(N(N-1)/2\) undirected), fixed over the
whole sequence. This is the natural choice for small, densely connected
networks like the one used here. In larger or sparser networks, however,
a full risk set is often not realistic: it assumes that any
actor could plausibly interact with any other at any moment, and it
grows quadratically in the number of actors, which quickly becomes both
substantively implausible and computationally expensive. The
alternatives below restrict the risk set to something more
defensible.
riskset = "active" includes only dyads observed at least
once anywhere in the sequence — a common, computationally lighter choice
for sparse networks.
reh_active <- remify(edgelist = randomREH3, model = "tie", riskset = "active")
cat("Full risk set: ", reh$D, "dyads\n")
#> Full risk set: 20 dyads
cat("Active risk set:", reh_active$activeD, "dyads\n")
#> Active risk set: 20 dyads
stats_active <- remstats(reh = reh_active,
tie_effects = ~ inertia(scaling = "std") +
reciprocity(scaling = "std"))
fit_active <- remstimate(reh = reh_active, stats = stats_active)
summary(fit_active)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + reciprocity(scaling = "std")
#>
#>
#> Coefficients (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -7.182955 0.039269 -182.917348 0 < 2.2e-16
#> inertia 0.443331 0.044428 9.978525 0 < 2.2e-16
#> reciprocity 0.291811 0.045432 6.423043 0 3.465e-08
#> Null deviance: 15813.97 on 992 degrees of freedom
#> Residual deviance: 15278.25 on 989 degrees of freedom
#> Chi-square: 535.7245 on 3 degrees of freedom, asymptotic p-value 0
#> AIC: 15284.25 AICC: 15284.27 BIC: 15298.95riskset = "active_saturated" extends the active risk set
by adding, for every observed actor pair, the reverse direction
(if \(A \to B\) is observed, \(B \to A\) is also placed at risk) and —
when events are typed — all event types for that pair. This encodes the
assumption that observing any interaction between two actors implies
that both directions (and all types) are possible for them, without
opening up the full risk set to actor pairs that never interact at
all.
riskset = "manual" restricts the risk set to a
user-supplied set of dyads, passed via manual_riskset.
Observed dyads not included in the manual specification are added
automatically.
# Define a manual risk set: a subset of directed dyads among actors 1-5
my_riskset <- data.frame(
actor1 = c(1, 2, 3, 4, 5),
actor2 = c(2, 3, 4, 5, 1)
)
reh_manual <- remify(
edgelist = randomREH3,
model = "tie",
riskset = "manual",
manual_riskset = my_riskset
)
#> Warning in remify(edgelist = randomREH3, model = "tie", riskset = "manual", :
#> 15 observed dyads were added to the manual risk set.
cat("Manual risk set:", reh_manual$activeD, "dyads\n")
#> Manual risk set: 20 dyadsOrthogonal to the four risk-set types above is the
extend_riskset_by_type argument, relevant only when events
carry a type (via event_type). With
extend_riskset_by_type = TRUE, each actor pair is
duplicated for every event type, so a dyad-type combination — not just a
dyad — is the unit at risk, and the risk set has size \(D = N(N-1) \times C\) (directed, \(C\) types). With FALSE (the
default), the event type is treated as a mark on events only and does
not expand the risk set. This choice is revisited in the typed-events
section below.
When events carry a type label (here, "work"
vs. "social" in the setting column), the
consider_type argument controls how statistics account for
event types. The risk set can optionally be extended by type
(extend_riskset_by_type = TRUE), so that each dyad-type
combination is a separate unit at risk.
consider_type = "ignore"Event types are not distinguished — statistics aggregate over all types.
consider_type = "separate"One statistic per type: separate counts of past "work"
events and past "social" events.
consider_type = "interact"The statistic conditions on the type of the event at the current time point, allowing the effect of event history to depend on the triggering type.
stats_int <- remstats(
reh = reh_typed,
tie_effects = ~ inertia(consider_type = "interact")
)
#> Warning in prepare_tomstats(effects = effects, reh = reh, attr_actors =
#> attr_actors, : "interact" requires extend_riskset_by_type = TRUE; coercing to
#> "separate".
dimnames(stats_int)[[3]]
#> [1] "baseline" "inertia.social" "inertia.work"stats_typed <- remstats(
reh = reh_typed,
tie_effects = ~ inertia(consider_type = "separate") +
reciprocity(consider_type = "separate")
)
fit_typed <- remstimate(reh = reh_typed, stats = stats_typed)
summary(fit_typed)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(consider_type = "separate") + reciprocity(consider_type = "separate")
#>
#>
#> Coefficients (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -7.2518e+00 5.0306e-02 -1.4415e+02 0.0000 < 2.2e-16
#> inertia.social -4.9709e-03 4.2046e-03 -1.1823e+00 0.2371 0.9399697
#> inertia.work 5.4111e-02 1.3646e-02 3.9653e+00 0.0001 0.0119841
#> reciprocity.social -1.7126e-02 4.2649e-03 -4.0155e+00 0.0001 0.0098328
#> reciprocity.work 6.3544e-02 1.3626e-02 4.6633e+00 0.0000 0.0005967
#> Null deviance: 15813.97 on 992 degrees of freedom
#> Residual deviance: 15527.04 on 987 degrees of freedom
#> Chi-square: 286.9303 on 5 degrees of freedom, asymptotic p-value 0
#> AIC: 15537.04 AICC: 15537.1 BIC: 15561.54The type-separated coefficients tell us whether inertia and
reciprocity operate differently across event types — for instance,
whether "work" interactions are more habitual than
"social" ones.
The consider_type argument (above) changes how
statistics are computed, but by default the risk set still
contains one entry per dyad. Setting
extend_riskset_by_type = TRUE in remify() goes
a step further and makes each dyad-type combination its own unit at
risk, so the model treats “a work event from \(i\) to \(j\)” and “a social event from \(i\) to \(j\)” as competing, distinct events. The
risk set then has size \(D = N(N-1) \times
C\).
reh_typed_ext <- remify(
edgelist = randomREH3,
model = "tie",
event_type = "setting",
extend_riskset_by_type = TRUE
)
cat("Dyad-only risk set: ", reh_typed$D, "\n")
#> Dyad-only risk set: 20
cat("Type-extended risk set: ", reh_typed_ext$D, "\n")
#> Type-extended risk set: 40Use extend_riskset_by_type = TRUE when the choice of
type is itself part of the event process you want to model; keep
the default FALSE when the type is merely an attribute
recorded alongside each event.
Exogenous actor attributes and dyadic covariates can be included
alongside endogenous effects. The info3 dataset provides
the actor covariate age.
stats_exo <- remstats(
reh = reh,
tie_effects = ~ inertia(scaling = "std") +
reciprocity(scaling = "std") +
send("age", attr_actors = info3, scaling = "std") +
receive("age", attr_actors = info3, scaling = "std") +
difference("age", attr_actors = info3, scaling = "std")
)
fit_exo <- remstimate(reh = reh, stats = stats_exo)
summary(fit_exo)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + reciprocity(scaling = "std") + send("age", attr_actors = info3, scaling = "std") + receive("age", attr_actors = info3, scaling = "std") + difference("age", attr_actors = info3, scaling = "std")
#>
#>
#> Coefficients (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -7.238625 0.041354 -175.041072 0.0000 < 2.2e-16
#> inertia 0.402356 0.054313 7.408075 0.0000 3.813e-11
#> reciprocity 0.240859 0.055845 4.312992 0.0000 0.002869
#> send_age -0.018312 0.036131 -0.506837 0.6123 0.965156
#> receive_age -0.061821 0.035888 -1.722593 0.0850 0.877202
#> difference_age -0.322162 0.038607 -8.344572 0.0000 2.387e-14
#> Null deviance: 15813.97 on 992 degrees of freedom
#> Residual deviance: 15200.25 on 986 degrees of freedom
#> Chi-square: 613.7194 on 6 degrees of freedom, asymptotic p-value 0
#> AIC: 15212.25 AICC: 15212.34 BIC: 15241.65send("age"): the sender’s age — do older actors
initiate more events?receive("age"): the receiver’s age — are older actors
more often chosen as receivers?difference("age"): the absolute age difference within
the dyad — do events flow between similarly aged actors?Effects can be interacted to test whether endogenous tendencies (e.g., inertia) depend on actor attributes:
stats_ix <- remstats(
reh = reh,
tie_effects = ~ inertia(scaling = "std") +
send("age", attr_actors = info3, scaling = "std") +
inertia(scaling = "std"):send("age", attr_actors = info3, scaling = "std")
)
fit_ix <- remstimate(reh = reh, stats = stats_ix)
summary(fit_ix)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + send("age", attr_actors = info3, scaling = "std") + inertia(scaling = "std"):send("age", attr_actors = info3, scaling = "std")
#>
#>
#> Coefficients (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -7.153090 0.038310 -186.717881 0.0000 <2e-16
#> inertia 0.674666 0.029403 22.945394 0.0000 <2e-16
#> send_age -0.064890 0.039264 -1.652652 0.0984 0.8894
#> inertia:send_age -0.069468 0.030007 -2.315038 0.0206 0.6836
#> Null deviance: 15813.97 on 992 degrees of freedom
#> Residual deviance: 15301.09 on 988 degrees of freedom
#> Chi-square: 512.8841 on 4 degrees of freedom, asymptotic p-value 0
#> AIC: 15309.09 AICC: 15309.13 BIC: 15328.69A significant interaction would indicate that the tendency to repeat past interactions depends on the sender’s age.
The memory argument controls which past events
contribute to endogenous statistics at each time point.
# Full memory (default): entire event history
stats_full <- remstats(reh = reh, tie_effects = ~ inertia(), memory = "full")
# Window memory: only events within the last 2000 time units
stats_win <- remstats(reh = reh, tie_effects = ~ inertia(),
memory = "window", memory_value = 2000)
# Decay memory: exponential decay with half-life 1000
stats_dec <- remstats(reh = reh, tie_effects = ~ inertia(),
memory = "decay", memory_value = 1000)
# Compare the inertia statistic at the last time point for the first dyad
M <- dim(stats_full)[1]
cat("Full memory: ", stats_full[M, 1, "inertia"], "\n")
#> Full memory: 4
cat("Window (2000): ", stats_win[M, 1, "inertia"], "\n")
#> Window (2000): 0
cat("Decay (1000): ", stats_dec[M, 1, "inertia"], "\n")
#> Decay (1000): 3.348464e-07Full memory accumulates all past events equally; window memory discards events older than the threshold; decay memory down-weights older events exponentially.
Full memory is the default, but it is often not the most
realistic choice: it implies that an interaction from the distant past
influences the current rate exactly as strongly as one that happened
moments ago. In most social processes, influence fades. The
decay memory captures this with a single half-life
parameter (memory_value): the weight of a past event halves
every memory_value time units. Rather than fixing the
half-life arbitrarily, it can be tuned like any other modeling choice —
by fitting the model across a grid of half-lives and comparing
information criteria (AIC/BIC) or predictive recall, and selecting the
value that optimizes them (see the model-assessment section below). For
the randomREH3 data a half-life around 2000 is close to
optimal, and we adopt memory = "decay",
memory_value = 2000 as the default in the more advanced
vignettes.
It is instructive to see where the three numbers above come from. The
first dyad (the directed pair 1 → 2) has four past events in
randomREH3, at times 3951, 5190, 9987 and 29149, and the
last time point at which the statistic is evaluated is around 50676.
Under full memory each of the four events counts once,
giving 4. Under window memory with a width of 2000,
only events in the trailing interval — here roughly the last 2000 time
units before the final time point — are counted; since the most recent 1
→ 2 event (at 29149) is far outside that window, the count is 0. Under
decay memory with a half-life of 1000, each past event
is down-weighted by one half for every 1000 time units that have elapsed
since it occurred (measured, in the default "pt" scheme,
from the previous time point). The three oldest events are effectively
forgotten, and the most recent one — about 21500 time units, or roughly
21.5 half-lives, in the past — contributes a weight of about
0.5^21.5, so the whole statistic collapses to roughly
3.3e-07. The window value of 0 and the tiny decay value therefore tell
the same story from two angles: this dyad has simply not been active
recently.
For large networks, computing statistics over the full risk set at every time point is expensive. Case-control sampling draws a random subset of non-event dyads as the comparison set.
stats_sampled <- remstats(
reh = reh,
tie_effects = ~ inertia(scaling = "std") + reciprocity(scaling = "std"),
sampling = TRUE,
samp_num = 5,
seed = 42
)
fit_sampled <- remstimate(reh = reh, stats = stats_sampled)
summary(fit_sampled)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + reciprocity(scaling = "std")
#>
#>
#> Coefficients (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -7.145876 0.038694 -184.675932 0 < 2.2e-16
#> inertia 0.465151 0.056679 8.206732 0 7.470e-14
#> reciprocity 0.387469 0.056400 6.870020 0 1.776e-09
#> Null deviance: 15813.97 on 992 degrees of freedom
#> Residual deviance: 15146.15 on 989 degrees of freedom
#> Chi-square: 667.8199 on 3 degrees of freedom, asymptotic p-value 0
#> AIC: 15152.15 AICC: 15152.18 BIC: 15166.85The estimates are consistent as the sample size grows but will differ slightly from the full-risk-set estimates due to sampling variability.
Having seen the individual modeling choices, how do we decide
which effects to keep? Different criteria are available
directly from a fitted model: the AIC and
BIC (in fit$AIC / fit$BIC,
trading fit against the number of parameters, with BIC penalizing
complexity more heavily), and the median recall from
diagnostics() (a prediction-based measure — higher is
better). We use a decay memory with half-life 2000 from
here on. Additionally, p-values, Pr(>|z|), or posterior
probability of a null value, Pr(=0), (based on the default
Bayes factor methodology of R packages BFpack and
bain) can be used to inform user about including or
excluding certain effects.
The helper below fits a tie model and returns these quantities, so
several candidate specifications can be compared on a common footing. We
use first = 200 so every model is scored on the same set of
events.
assess <- function(formula, memory_value = 2000) {
s <- remstats(reh, tie_effects = formula, first = 200,
memory = "decay", memory_value = memory_value)
m <- remstimate(reh, s)
d <- diagnostics(m, reh, s)
data.frame(
npar = length(coef(m)),
AIC = round(m$AIC, 1),
BIC = round(m$BIC, 1),
median_recall = round(d$recall$summary$median_rel_rank, 3)
)
}We compare four specifications: an empty (baseline-only) model, an
endogenous-only model (no_age), that model plus the
sender’s age (wage), and a richer model with
extra degree and shared-partner effects.
f_empty <- ~ 1
f_noage <- ~ inertia(scaling = "std") + reciprocity(scaling = "std") +
outdegreeSender(scaling = "std") + isp(scaling = "std")
f_wage <- ~ inertia(scaling = "std") + reciprocity(scaling = "std") +
outdegreeSender(scaling = "std") +
send("age", attr_actors = info3, scaling = "std") +
isp(scaling = "std")
f_rich <- ~ inertia(scaling = "std") + reciprocity(scaling = "std") +
outdegreeSender(scaling = "std") + indegreeReceiver(scaling = "std") +
send("age", attr_actors = info3, scaling = "std") +
receive("age", attr_actors = info3, scaling = "std") +
isp(scaling = "std") + osp(scaling = "std")
comparison <- rbind(
empty = assess(f_empty),
no_age = assess(f_noage),
wage = assess(f_wage),
rich = assess(f_rich)
)
comparison
#> npar AIC BIC median_recall
#> empty 1 12776.3 12781.0 0.500
#> no_age 5 12086.1 12109.5 0.842
#> wage 6 12088.0 12116.1 0.842
#> rich 9 12086.2 12128.3 0.842The empty model anchors the scale: its median recall sits at 0.5 (no
better than chance) and its information criteria are worse. Adding the
endogenous effects improves everything sharply. Among the three
non-empty models the median recall is identical (0.842) — with only five
actors the risk set has just 20 dyads, so the median relative rank takes
few distinct values and is too coarse to separate them here; the
information criteria do the discriminating. Both AIC and BIC are lowest
for the parsimonious no_age model: adding the sender’s
age (wage) or the extra structural effects
(rich) leaves the fit essentially unchanged while spending
more parameters, so both criteria go slightly up. The verdict
is that age is not supported by these data, and the
endogenous-only model is preferred.
The per-effect output of summary() gives a
complementary, one-model-at-a-time view that corroborates this. For each
coefficient it reports a frequentist p-value in the
Pr(>|z|) column and a posterior probability that the
effect equals zero in the Pr(=0) column. In the model that
includes age, the send.age row carries a large
p-value and a high posterior probability of a zero effect — the same
signal the model comparison gave, namely that this effect can be
dropped.
stats_wage <- remstats(reh, tie_effects = f_wage, first = 200,
memory = "decay", memory_value = 2000)
summary(remstimate(reh, stats_wage))
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + reciprocity(scaling = "std") + outdegreeSender(scaling = "std") + send("age", attr_actors = info3, scaling = "std") + isp(scaling = "std")
#>
#>
#> Coefficients (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -7.523964 0.056829 -132.397108 0.0000 < 2.2e-16
#> inertia 0.478865 0.045637 10.492805 0.0000 < 2.2e-16
#> reciprocity 0.220127 0.042121 5.226112 0.0000 3.305e-05
#> outdegreeSender -0.126343 0.042465 -2.975208 0.0029 0.2521
#> send_age 0.010830 0.037216 0.291015 0.7710 0.9643
#> isp -0.612400 0.066844 -9.161629 0.0000 < 2.2e-16
#> Null deviance: 12774.29 on 794 degrees of freedom
#> Residual deviance: 12076.04 on 788 degrees of freedom
#> Chi-square: 698.2479 on 6 degrees of freedom, asymptotic p-value 0
#> AIC: 12088.04 AICC: 12088.15 BIC: 12116.1The recall and information criteria can also be used to tune the
memory value (half-life for a decay memory). Refitting the
preferred (no_age) specification across a grid of
half-lives and tracking BIC and recall points to a best value — for
these data, BIC bottoms out near a half-life of 2000 and the relative
recall is maximal, which is why we use it
half_lives <- c(500, 1000, 2000, 3000, 4000)
mem_path <- do.call(rbind, lapply(half_lives, function(h)
cbind(half_life = h, assess(f_noage, memory_value = h))))
mem_path[, c("half_life", "BIC", "median_recall")]
#> half_life BIC median_recall
#> 1 500 12198.0 0.842
#> 2 1000 12125.5 0.842
#> 3 2000 12109.5 0.842
#> 4 3000 12123.7 0.789
#> 5 4000 12144.3 0.789
plot(mem_path$half_life, mem_path$BIC, type = "b", pch = 19,
xlab = "decay half-life", ylab = "BIC", main = "BIC vs. memory half-life")Actor-oriented models decompose the event process into two steps: a
sender rate model (which actor initiates next?) and a receiver choice
model (whom does the sender choose?). This requires
directed = TRUE.
reh_ao <- remify(edgelist = randomREH3, model = "actor", directed = TRUE)
rate_effects <- ~ outdegreeSender(scaling = "std") + indegreeSender(scaling = "std")
choice_effects <- ~ inertia(scaling = "std") + reciprocity(scaling = "std")
stats_ao <- remstats(
reh = reh_ao,
sender_effects = rate_effects,
receiver_effects = choice_effects
)
stats_ao
#> Relational Event Network Statistics
#> > Model: actor-oriented
#> > Computation method: per time point
#> > Sender model:
#> >> Dimensions: 992 time points x 5 actors x 3 statistics
#> >> Statistics:
#> >>> 1: baseline
#> >>> 2: outdegreeSender
#> >>> 3: indegreeSender
#> > Receiver model:
#> >> Dimensions: 992 events x 5 actors x 2 statistics
#> >> Statistics:
#> >>> 1: inertia
#> >>> 2: reciprocity
fit_ao <- remstimate(reh = reh_ao, stats = stats_ao)
summary(fit_ao)
#> Relational Event Model (actor oriented)
#>
#> Call rate model **for sender**:
#>
#> ~outdegreeSender(scaling = "std") + indegreeSender(scaling = "std")
#>
#>
#> Coefficients rate model (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -5.5381e+00 3.1703e-02 -1.7469e+02 0.0000 <2e-16
#> outdegreeSender 4.9827e-03 3.6671e-02 1.3588e-01 0.8919 0.9690
#> indegreeSender 6.0544e-02 3.6147e-02 1.6749e+00 0.0940 0.8857
#> Null deviance: 13046.93 on 992 degrees of freedom
#> Residual deviance: 13043.82 on 989 degrees of freedom
#> Chi-square: 3.10366 on 3 degrees of freedom, asymptotic p-value 0.3759173
#> AIC: 13049.82 AICC: 13049.85 BIC: 13064.52
#> --------------------------------------------------------------------------------
#>
#> Call choice model **for receiver**:
#>
#> ~inertia(scaling = "std") + reciprocity(scaling = "std")
#>
#>
#> Coefficients choice model (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> inertia 0.16805 0.10140 1.65730 0.0975 0.8886
#> reciprocity 0.10580 0.10164 1.04091 0.2979 0.9482
#> Null deviance: 2767.044 on 992 degrees of freedom
#> Residual deviance: 2722.256 on 990 degrees of freedom
#> Chi-square: 44.78752 on 2 degrees of freedom, asymptotic p-value 1.88154e-10
#> AIC: 2726.256 AICC: 2726.268 BIC: 2736.055The sender model estimates a baseline rate and the effects of out-degree and in-degree on sender activity. The receiver model describes how inertia and reciprocity shape the choice of receiver, conditional on the sender.
rate_effects_exo <- ~ outdegreeSender(scaling = "std") +
send("age", attr_actors = info3, scaling = "std")
choice_effects_exo <- ~ inertia(scaling = "std") + reciprocity(scaling = "std") +
receive("age", attr_actors = info3, scaling = "std")
stats_ao_exo <- remstats(
reh = reh_ao,
sender_effects = rate_effects_exo,
receiver_effects = choice_effects_exo
)
fit_ao_exo <- remstimate(reh = reh_ao, stats = stats_ao_exo)
summary(fit_ao_exo)
#> Relational Event Model (actor oriented)
#>
#> Call rate model **for sender**:
#>
#> ~outdegreeSender(scaling = "std") + send("age", attr_actors = info3, scaling = "std")
#>
#>
#> Coefficients rate model (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> baseline -5.536886 0.031666 -174.855347 0.0000 <2e-16
#> outdegreeSender 0.042033 0.050524 0.831932 0.4054 0.957
#> send -0.031186 0.050342 -0.619494 0.5356 0.963
#> Null deviance: 13046.93 on 992 degrees of freedom
#> Residual deviance: 13046.23 on 989 degrees of freedom
#> Chi-square: 0.695269 on 3 degrees of freedom, asymptotic p-value 0.8743162
#> AIC: 13052.23 AICC: 13052.26 BIC: 13066.93
#> --------------------------------------------------------------------------------
#>
#> Call choice model **for receiver**:
#>
#> ~inertia(scaling = "std") + reciprocity(scaling = "std") + receive("age", attr_actors = info3, scaling = "std")
#>
#>
#> Coefficients choice model (MLE with interval likelihood):
#>
#> Estimate Std. Err z value Pr(>|z|) Pr(=0)
#> inertia 0.067447 0.108193 0.623391 0.5330 0.9629
#> reciprocity 0.204916 0.109116 1.877969 0.0604 0.8438
#> receive -0.117852 0.043621 -2.701750 0.0069 0.4502
#> Null deviance: 2767.044 on 992 degrees of freedom
#> Residual deviance: 2714.937 on 989 degrees of freedom
#> Chi-square: 52.10606 on 3 degrees of freedom, asymptotic p-value 2.843181e-11
#> AIC: 2720.937 AICC: 2720.962 BIC: 2735.637diag_ao <- diagnostics(object = fit_ao, reh = reh_ao, stats = stats_ao)
plot(x = fit_ao, reh = reh_ao, diagnostics = diag_ao)#> Warning in regularize.values(x, y, ties, missing(ties), na.rm = na.rm):
#> collapsing to unique 'x' values
#> Warning in regularize.values(x, y, ties, missing(ties), na.rm = na.rm):
#> collapsing to unique 'x' values
Basic tie-oriented models can also be estimated by Hamiltonian Monte
Carlo (HMC) via approach = "Bayesian". Sampler settings are
passed through the bayes list. Bayesian HMC is available
for the basic tie (and actor) model only; for penalized models the
Bayesian approach uses shrinkage priors instead (see the penalized-REM
vignette).
fit_hmc <- remstimate(
reh = reh,
stats = stats,
approach = "Bayesian",
bayes = list(
nsim = 300L,
nchains = 2L,
burnin = 300L,
L = 100L,
epsilon = 0.001,
thin = 2L
),
seed = 42
)
summary(fit_hmc)
#> Relational Event Model (tie oriented)
#>
#> Call:
#> ~inertia(scaling = "std") + reciprocity(scaling = "std") + outdegreeSender(scaling = "std") + isp(scaling = "std")
#>
#>
#> Posterior Modes (HMC with interval likelihood):
#>
#> Post.Mode Post.SD Q2.5% Q50% Q97.5% Pr(=0|x)
#> baseline -7.251976 0.026655 -7.304215 -7.251976 -7.1974 < 2.2e-16
#> inertia 0.422994 0.025578 0.376685 0.421789 0.4697 < 2.2e-16
#> reciprocity 0.202351 0.023501 0.154995 0.201360 0.2447 2.512e-15
#> outdegreeSender -0.117026 0.023227 -0.159971 -0.119142 -0.0829 9.678e-05
#> isp -0.358685 0.031968 -0.422702 -0.353837 -0.2972 < 2.2e-16
#> Log posterior: -7613.877diag_hmc <- diagnostics(object = fit_hmc, reh = reh, stats = stats)
plot(x = fit_hmc, reh = reh, diagnostics = diag_hmc)#> Warning in regularize.values(x, y, ties, missing(ties), na.rm = na.rm):
#> collapsing to unique 'x' values
The HMC output includes posterior means, standard deviations, and trace plots for convergence assessment. For most applied work the maximum-likelihood fit is sufficient; HMC is useful mainly when full posterior uncertainty is required.
The table below summarizes the key modeling choices and how they map to function arguments:
| Choice | Argument | Options |
|---|---|---|
| Model type | remify(model = ...) |
"tie", "actor" |
| Timing | remify(ordinal = ...) |
FALSE (interval), TRUE (ordinal) |
| Risk set | remify(riskset = ...) |
"full", "active",
"active_saturated", "manual" |
| Event types | remify(event_type = ...) |
column name or NULL |
| Type-extended risk set | remify(extend_riskset_by_type = ...) |
FALSE (default), TRUE |
| Type handling | inertia(consider_type = ...) |
"ignore", "separate",
"interact" |
| Memory | remstats(memory = ...) |
"full", "window", "interval",
"decay" |
| Sampling | remstats(sampling = ...) |
TRUE/FALSE |
| Estimation approach | remstimate(approach = ...) |
"frequentist" (default), "Bayesian" |
| Model structure | remstimate(random=, penalty=, mixture=) |
GLMM / penalized / mixture |
These choices can be freely combined — for example, an ordinal
actor-oriented model with an active risk set and typed events using
consider_type = "separate". The random,
penalty, and mixture arguments — covered in
the frailty, penalized, and mixture vignettes — extend the basic model
without changing the remify()/remstats()
steps.