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.
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 0Recover 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.4563272The estimate sits in the same ballpark as the simulated
true_beta = 0.6.
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.93With exp(3) ~= 20 weekday-to-weekend rate ratio, the
bulk of realised events falls in weekday == 1
intervals.
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 1Both 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.
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.