amore (Advanced Modelling of Relational Events) is an R package for simulation and inference in relational event models (REMs). It targets dynamic network data in continuous time, with a focus on reproducible workflows: event logs, covariates, model fitting, and diagnostics.
The package already supports the end-to-end workflow needed for exploratory and simulation-based REM studies:
standardize_event_log() harmonizes raw logs,
drops loops/duplicates, and tags them as amore_event_log objects.simulate_actor_covariates() generates static
or AR(1) dynamic traits, while attach_static_covariates() merges user data.compute_endogenous_features() now implements 28
endogenous effects (recency, multiple reciprocity forms, transitivity,
cyclic closure, sending/receiving balance) following Juozaitienė & Wit (2025).simulate_relational_events() runs
Gillespie-style simulations with optional controls for partial likelihood.sample_non_events() constructs nested case-control
tables with appearance, citation, and remove risk-set rules.# install the development version from GitHub
# install.packages("pak")
pak::pak("franciscorichter/amore")
# alternatively, install from a local checkout
install.packages(".", repos = NULL, type = "source")
library(amore)
The Gillespie algorithm generates relational events where inter-event times are exponentially distributed with rate equal to the sum of all dyadic hazards, and dyads are selected proportionally to their intensity.
library(amore)
set.seed(1)
p <- 20
actors <- as.character(1:p)
# Dyadic covariate x ~ N(0,1) with true effect b1 = 1
x <- matrix(rnorm(p * p), nrow = p, ncol = p)
b1 <- 1
baseline <- b1 * x
# Simulate 500 events + 1 control per event for partial likelihood
events <- simulate_relational_events(
n_events = 500,
senders = actors,
receivers = actors,
baseline_logits = baseline,
allow_loops = FALSE,
n_controls = 1
)
head(events)
Module A (Preprocesses) organizes dynamic network workflows around four objects that can be composed as needed:
sender, receiver,
time, produced via simulations or ingested from data sources.simulate_actor_covariates() or supplied as
baseline_logits/lookup tables.compute_endogenous_features() to generate baseline statistics that can
be extended with custom feature builders.simulate_relational_events(..., n_controls > 0) to drive conditional
logistic / GAM estimation.A small preprocessing example:
library(amore)
# 1. Event log direct from a data source
raw_events <- data.frame(
source = c("a", "b", "b", "c"),
target = c("b", "c", "a", "a"),
ts = c(2.1, 2.4, 3.0, 3.5)
)
event_log <- standardize_event_log(
raw_events,
sender_col = "source",
receiver_col = "target",
time_col = "ts",
drop_loops = TRUE
)
# 2. Exogenous covariates
covs <- simulate_actor_covariates(
senders = unique(event_log$sender),
receivers = unique(event_log$receiver),
covariate_names = c("activity", "popularity"),
seed = 123
)
event_log <- attach_static_covariates(
event_log,
sender_covariates = covs$sender_covariates,
receiver_covariates = covs$receiver_covariates
)
# 3. Endogenous stats from the evolving event net
event_log <- compute_endogenous_features(event_log,
stats = c("sender_outdegree", "receiver_indegree", "reciprocity", "recency")
)
simulate_actor_covariates() returns two lookup tables with one row per actor.
For a sender/receiver and a given covariate name:
attach_static_covariates() stores them in the event log as
sender_<name> or receiver_<name>. In the example, activity represents a
baseline propensity for sending events and popularity captures how
attractive an actor is as a receiver.time_points is supplied. Each actor and
covariate follows an AR(1) trajectory controlled by the rho and sd
arguments, so values drift smoothly through time while remaining correlated
from one time stamp to the next.All endogenous summaries are evaluated immediately before an event is
logged. They follow the taxonomy of
Juozaitienė & Wit (2025, JRSS-A)
and use the continuous convention (effects persist even after a closure
event). Pass one or more stat names to compute_endogenous_features().
Degree / baseline
| Stat name | Description |
|---|---|
sender_outdegree |
Number of events the sender has issued so far. |
receiver_indegree |
Number of events the receiver has received so far. |
recency |
Elapsed time since the last event on the same ordered pair; NA when the dyad is brand new. |
Reciprocity — history of the reverse dyad (receiver → sender)
| Stat name | Description |
|---|---|
reciprocity / reciprocity_binary |
1 if the reverse dyad has ever been observed, 0 otherwise. |
reciprocity_count |
Total number of past reverse-dyad events. |
reciprocity_exp_decay |
Exponentially weighted sum of past reverse-dyad events; older events contribute less according to half_life. |
reciprocity_time_recent |
Elapsed time since the most recent reverse-dyad event; NA if none. |
reciprocity_time_first |
Elapsed time since the first reverse-dyad event; NA if none. |
Transitivity — two-path s → k → r (the sender previously contacted some intermediary k who in turn contacted the receiver)
| Stat name | Description |
|---|---|
transitivity_binary |
1 if any such intermediary k exists, 0 otherwise. |
transitivity_count |
Number of distinct intermediaries. |
transitivity_binary_ordered |
Like binary, but requiring the s → k event to precede the k → r event in time. |
transitivity_count_ordered |
Count with order restriction. |
transitivity_exp_decay |
Exp-decay weighted sum over two-paths (requires half_life). |
transitivity_exp_decay_ordered |
Exp-decay with order restriction. |
transitivity_time_recent |
Time since the most recently completed two-path; NA if none. |
transitivity_time_first |
Time since the earliest two-path; NA if none. |
transitivity_time_recent_ordered |
Time since the most recent ordered two-path; NA if none. |
transitivity_time_first_ordered |
Time since the earliest ordered two-path; NA if none. |
Cyclic closure — two-path r → k → s, closed by event s → r (the receiver previously contacted k, and k previously contacted the sender)
| Stat name | Description |
|---|---|
cyclic_binary |
1 if any cyclic two-path exists, 0 otherwise. |
cyclic_count |
Number of cyclic intermediaries. |
cyclic_time_recent |
Time since the most recent cyclic two-path; NA if none. |
Sending balance — shared target: both s → k and r → k exist (the sender and receiver have both contacted the same third actor k)
| Stat name | Description |
|---|---|
sending_balance_binary |
1 if any shared target exists, 0 otherwise. |
sending_balance_count |
Number of shared targets. |
sending_balance_time_recent |
Time since the most recent shared-target two-path; NA if none. |
Receiving balance — shared source: both k → s and k → r exist (the sender and receiver have both been contacted by the same third actor k)
| Stat name | Description |
|---|---|
receiving_balance_binary |
1 if any shared source exists, 0 otherwise. |
receiving_balance_count |
Number of shared sources. |
receiving_balance_time_recent |
Time since the most recent shared-source two-path; NA if none. |
All *_exp_decay statistics require a half_life argument that controls
how quickly the influence of past events diminishes.
# 4. Inference-ready case-control data
cases_controls <- simulate_relational_events(
n_events = 100,
senders = unique(event_log$sender),
receivers = unique(event_log$receiver),
baseline_logits = matrix(0, nrow = 3, ncol = 3),
allow_loops = FALSE,
n_controls = 1
)
To create case-control tables from empirical event data, use
sample_non_events() to append synthetic controls to each realized event:
case_control_df <- sample_non_events(
event_log,
n_controls = 2,
scope = "appearance",
mode = "two",
allow_loops = FALSE,
seed = 2026
)
head(case_control_df[, c("sender", "receiver", "event", "stratum")])
The helper keeps the original events (event = 1) and appends n_controls
counterfactual dyads (event = 0) per stratum so conditional logistic / GAM
estimators can compare realized vs. sampled alternatives. Candidate dyads are
constructed via two knobs plus an optional risk-set rule:
"all": every actor ever seen in the data belongs to the sampling pool."appearance": only actors that have appeared prior to the focal event are
eligible, which mimics nested case-control sampling."one": draw both sender and receiver from the same candidate set (useful
for single-mode networks)."two": draw senders and receivers from separate candidate pools (default
for bipartite or directed settings)."standard": risk set never shrinks beyond the chosen scope."remove": once a realized dyad (s_i, r_i) occurs, it is removed from
consideration in later strata (e.g., species invasion that cannot repeat).Set allow_loops = TRUE when self-ties should be considered and adjust
max_attempts to control resampling when many candidate pairs coincide with the
observed event.
The three sampling schemes we discussed earlier map directly onto these knobs:
| Strategy label | scope |
mode |
|---|---|---|
| all + one-mode | "all" |
"one" |
| all + two-mode | "all" |
"two" |
| appearance + one/two-mode | "appearance" |
"one" or "two" |
| citation | "citation" |
typically "two" |
| remove one/two-mode | "all" or "appearance" |
"one" / "two"; set risk = "remove" |
The last option is listed twice because you may want either a single-mode or a two-mode draw while still restricting to previously active actors.
For the citation sampler, senders are the papers that debut at the event time
while receivers must have appeared strictly earlier. The risk = "remove" flag
deletes realized dyads from future candidate sets to mimic one-off events such as
biological invasions. Regardless of the configuration, each stratum contains the
observed event (event = 1) followed by its sampled controls (event = 0), so
conditional likelihood estimators can contrast what happened with what could have
happened instead.
The case-control output lets you recover parameters via a GAM:
library(mgcv)
get_x <- function(s, r) x[cbind(as.integer(s), as.integer(r))]
events$x_val <- mapply(get_x, events$sender, events$receiver)
cases <- events[events$event == 1, ]
controls <- events[events$event == 0, ]
cases <- cases[order(cases$stratum), ]
controls <- controls[order(controls$stratum), ]
fit_df <- data.frame(y = 1, delta_x = cases$x_val - controls$x_val)
fit <- gam(y ~ delta_x - 1, family = binomial, data = fit_df)
coef(fit)
#> delta_x ≈ 1 (recovers b1)
The package ships a 56 × 56 US state distance matrix and supports
non-linear effects via baseline_logits. For example, using geographic
distance with a smooth true effect:
data("dist_matrix", package = "amore")
dist_log <- log(dist_matrix / 100000 + 1)
true_effect <- sin(-dist_log / 1.5)
events <- simulate_relational_events(
n_events = 800,
senders = rownames(dist_matrix),
receivers = rownames(dist_matrix),
baseline_logits = true_effect,
allow_loops = FALSE,
n_controls = 1
)
See vignette("exogenous-covariates") for the full workflow including GAM
recovery of the non-linear distance effect.
For function usage:
?simulate_relational_events
?simulate_actor_covariates
During development, work from the package root and let R load the in-tree code with:
devtools::load_all()
devtools::document()devtools::test()devtools::check()pkgdown::build_site()MIT, see LICENSE.