--- title: "Model comparison on a real REM dataset" author: "Francisco Richter" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Model comparison on a real REM dataset} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 3.5 ) ``` This vignette walks through the canonical "candidate-specifications → AIC ranking" workflow used in REM literature (Juozaitienė & Wit 2024, *JRSS-A* 188(4)) — with `amorem`'s bundled real-world dataset and the one-call `compare_models()` helper. ```{r} library(amorem) ``` ## 1. Load a bundled REM dataset The package ships four real-world REM datasets directly. We use the McFarland (2001) high-school classroom session: 691 directed interactions among 20 students and one instructor, recorded on 16 October 1996. ```{r} data(classroom_events) data(classroom_actors) nrow(classroom_events) head(classroom_events, 3) table(classroom_actors$role) ``` `classroom_events` follows the package's `(sender, receiver, time, ...)` contract — the same contract every downstream helper expects. ## 2. Build candidate specifications `compare_models()` accepts a named list of character vectors. Each entry is one candidate specification; the vector contents are the endogenous statistic names. Here we compare three minimal specifications: ```{r} specs <- list( count = c("reciprocity_count", "transitivity_count"), continuous = c("reciprocity_time_recent", "transitivity_time_recent"), interrupted = c("reciprocity_time_recent_interrupted", "transitivity_time_recent_interrupted")) ``` These are the paper's definitions r^(1c)/t^(1c) (count), r^(4ac)/t^(7ac) (continuous timing), and r^(4ai)/t^(7ai) (interrupted timing). ## 3. Compare by AIC ```{r} res <- compare_models(classroom_events, specs, seed = 11) res ``` The helper draws one case-control sample (default `n_controls = 1`) shared across every specification, computes the union of all requested statistics with `endogenous_features()`, builds case-minus-control differences, and fits one no-intercept binomial GLM per spec. Returned rows are sorted by AIC; the winning spec has `delta_AIC = 0`. On Classroom the count-based specification wins this stripped-down comparison — recall that this is a no-smooth, no-random-effect baseline; richer fits (thin-plate splines on the differences, sender/receiver random effects) typically reweight the ranking in favour of the temporal definitions, matching the empirical findings of the paper. ### Multiple controls per case For 1:1 matching the helper fits a no-intercept binomial GLM on case-minus-control differences. Set `n_controls > 1` to switch to stratified conditional logistic regression via `survival::coxph` — the right tool when you want more controls per case for tighter inference: ```{r eval = requireNamespace("survival", quietly = TRUE)} compare_models(classroom_events, specs, n_controls = 3, seed = 11) ``` The `n_obs` column now reports the number of strata (one per case), and `survival` is in the package's *Suggests* — required only when `n_controls > 1`. AIC values across specs remain comparable because every spec sees the same shared case-control sample. ## 4. Inspect coefficients of a chosen specification `compare_models()` returns AIC summaries. To inspect coefficients for a single spec, build the case-control sample once and fit directly: ```{r} stat_set <- specs$interrupted cc <- sample_non_events(classroom_events, n_controls = 1, scope = "all", mode = "one", seed = 11) cc_feat <- endogenous_features(cc, stats = stat_set) for (st in stat_set) cc_feat[[st]][is.na(cc_feat[[st]])] <- 0 cases <- cc_feat[cc_feat$event == 1L, ] ctrls <- cc_feat[cc_feat$event == 0L, ] cases <- cases[order(cases$stratum), ] ctrls <- ctrls[order(ctrls$stratum), ] df <- data.frame( one = rep(1, nrow(cases)), d_rec = cases[[stat_set[1]]] - ctrls[[stat_set[1]]], d_trans = cases[[stat_set[2]]] - ctrls[[stat_set[2]]]) fit <- glm(one ~ d_rec + d_trans - 1, family = "binomial", data = df) summary(fit)$coefficients ``` The same recipe scales to any subset of the statistics in the catalogue. Use `?endogenous_features` to see the full list. ## 5. Cross-implementation guarantee Every statistic the post-hoc engine computes is also generated by `simulate_relational_events()` using the same paper definitions. The package ships a parity test (`test-sim-vs-posthoc-parity.R`) that runs the simulator on every shared stat and verifies that re-running `endogenous_features()` on the resulting event log reproduces the simulator's columns row-for-row. This means: if you want to test a model selection pipeline against ground-truth coefficients, you can simulate data with `simulate_relational_events()` using known effects and use `compare_models()` to confirm the ranking. ```{r} set.seed(2026) sim <- simulate_relational_events( n_events = 600, senders = LETTERS[1:8], receivers = LETTERS[1:8], baseline_rate = 1, allow_loops = FALSE, endogenous_stats = c("reciprocity_count", "transitivity_count"), endogenous_effects = c(reciprocity_count = 0.4, transitivity_count = 0.0)) # Among these three specs, the "count" spec is the true generative # process. compare_models() should rank it first. res2 <- compare_models(sim, specs, seed = 7) res2 ``` ## References - Juozaitienė R, Wit EC (2024). It's about time: revisiting reciprocity and triadicity in relational event analysis. *Journal of the Royal Statistical Society Series A* 188(4), 1246–1262. doi:10.1093/jrsssa/qnae132. - Vu D, Pattison P, Robins G (2017). Relational event models for social learning in MOOCs. *Social Networks* 43, 121–135. - McFarland D (2001). Student resistance. *AJS* 107(3), 612–678.