amore

amore logo

R-CMD-check pkgdown docs status prototype

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.

What it aims to provide

Current capabilities

The package already supports the end-to-end workflow needed for exploratory and simulation-based REM studies:

Installation

# 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)

Quick start

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)

Core data components

Module A (Preprocesses) organizes dynamic network workflows around four objects that can be composed as needed:

  1. Relational event data — canonical log with sender, receiver, time, produced via simulations or ingested from data sources.
  2. Exogenous covariates — actor or dyad-level inputs not generated by the event process (e.g., geography, demographics). These can be simulated via simulate_actor_covariates() or supplied as baseline_logits/lookup tables.
  3. Endogenous covariates (eventnet) — summaries derived from the evolving event history (recency, reciprocity, shared partners). Use compute_endogenous_features() to generate baseline statistics that can be extended with custom feature builders.
  4. Inference data — nested case-control tables returned by 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")
)

Exogenous covariate definitions

simulate_actor_covariates() returns two lookup tables with one row per actor. For a sender/receiver and a given covariate name:

Endogenous network statistics

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
)

Sampling non-events from observed logs

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:

  1. scope
    • "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.
  2. mode
    • "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).
  3. risk
    • "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.

Inference with GAM

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)

Exogenous dyadic covariates

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.

Documentation

For function usage:

?simulate_relational_events
?simulate_actor_covariates

Development

License

MIT, see LICENSE.