Endogenous mechanisms and time-varying global covariates

Francisco Richter

library(amorem)

Beyond static dyadic covariates, the rate at which relational events fire typically depends on what has already happened (endogenous mechanisms such as reciprocity) and on when in time the process is observed (time-varying global factors such as weekday/weekend). simulate_relational_events() exposes both through the endogenous_stats / endogenous_effects and global_covariates / global_effects argument families. This vignette walks through a minimal example for each, then shows how they compose.

Endogenous reciprocity

reciprocity_count is a built-in endogenous statistic: the value of the statistic at candidate dyad (s, r) is the number of past events (r, s). A positive coefficient means an event raises the future rate of its reverse dyad.

set.seed(2024)
actors <- as.character(1:10)
true_beta <- 0.6

cc <- simulate_relational_events(
  n_events = 1200,
  senders = actors,
  receivers = actors,
  baseline_rate = 1,
  allow_loops = FALSE,
  n_controls = 1,
  endogenous_stats = "reciprocity_count",
  endogenous_effects = true_beta
)
head(cc)
#>   stratum event sender receiver        time reciprocity_count
#> 1       1     1     10        3 0.007487612                 0
#> 2       1     0      1        6 0.007487612                 0
#> 3       2     1      2        5 0.011851596                 0
#> 4       2     0      9        2 0.011851596                 0
#> 5       3     1      8        3 0.026647847                 0
#> 6       3     0      3        4 0.026647847                 0

Recover the coefficient with a one-parameter conditional logit fit on the within-stratum statistic difference:

library(mgcv)
cases    <- cc[cc$event == 1L, ]
controls <- cc[cc$event == 0L, ]
cases    <- cases[order(cases$stratum), ]
controls <- controls[order(controls$stratum), ]
fit_df <- data.frame(
  one     = 1,
  delta_r = cases$reciprocity_count - controls$reciprocity_count
)
fit <- gam(one ~ delta_r - 1, family = "binomial", data = fit_df)
unname(coef(fit)[1])
#> [1] 0.4563272

The estimate sits in the same ballpark as the simulated true_beta = 0.6.

Time-varying global covariates

global_covariates is a data.frame with a strictly increasing time_start column and one numeric column per global covariate. Between two breaks the covariate value is constant. Internally, simulate_relational_events() uses a boundary-aware Gillespie scheme that redraws the next waiting time whenever the proposed event would jump into the next interval.

set.seed(2024)
gc <- data.frame(
  time_start = seq(0, 10, by = 1),
  weekday    = rep(c(0, 1), length.out = 11)
)
ev <- simulate_relational_events(
  n_events = 200,
  senders = letters[1:5],
  receivers = letters[1:5],
  baseline_rate = 0.3,
  horizon = 11,
  global_covariates = gc,
  global_effects = c(weekday = 3)
)

share_weekday <- mean(ev$weekday == 1)
share_weekday
#> [1] 0.93

With exp(3) ~= 20 weekday-to-weekend rate ratio, the bulk of realised events falls in weekday == 1 intervals.

Composing endogenous and global

The two features can be active at the same time. The per-step total weight is recomputed from the current endogenous state and then rescaled by the global multiplier. The output frame carries one column per endogenous statistic and one column per global covariate.

set.seed(7)
actors <- letters[1:5]
gc <- data.frame(time_start = c(0, 2, 4, 6), weekday = c(1, 0, 1, 0))

ev <- simulate_relational_events(
  n_events = 60,
  senders = actors,
  receivers = actors,
  baseline_rate = 1,
  horizon = 7,
  endogenous_stats = "reciprocity_count",
  endogenous_effects = c(reciprocity_count = 0.4),
  global_covariates = gc,
  global_effects = c(weekday = 1.5)
)
head(ev)
#>   sender receiver         time reciprocity_count weekday
#> 1      d        e 0.0005393737                 0       1
#> 2      c        b 0.0068986251                 0       1
#> 3      e        a 0.0081210792                 0       1
#> 4      a        e 0.0137901110                 1       1
#> 5      a        e 0.0214270093                 1       1
#> 6      b        a 0.0234383656                 0       1

Both the reciprocity_count column (endogenous state at event time) and the weekday column (global covariate at event time) appear in the output, ready for downstream conditional-logit or partial-likelihood inference.

Caveat

The current endogenous-state implementation maintains a single (senders × receivers) reciprocity matrix and requires senders and receivers to be the same character vector in the same order (a one-mode network). Passing different sender/receiver sets while using endogenous_stats will throw a clear error. Bipartite/two-mode support is on the roadmap.