---
title: "Dyadic Latent Class Relational Event Models"
subtitle: "Finite mixture models for relational event heterogeneity"
author: ""
date: ""
output:
rmarkdown::html_document:
toc: true
toc_depth: 3
theme: spacelab
highlight: pygments
code_folding: show
header-includes:
- \usepackage{amsmath,amssymb}
vignette: >
%\VignetteEngine{knitr::rmarkdown}
%\VignetteIndexEntry{Dyadic Latent Class Relational Event Models}
%\VignetteEncoding{UTF-8}
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(dpi = 72, collapse = TRUE, comment = "#>")
# Mixture fits need flexmix (declared in Suggests). If it is unavailable - e.g.
# a CRAN check run with _R_CHECK_FORCE_SUGGESTS_=false - the modelling chunks
# render code-only instead of erroring the vignette build.
has_flexmix <- requireNamespace("flexmix", quietly = TRUE)
```
---
Standard relational event models assume that all dyads follow the same relational dynamics — a single set of parameters governs the entire event process. In many empirical networks this is too restrictive. Some dyads exhibit strong reciprocity while others are driven by inertia; some have a high baseline rate while others interact rarely. A **dyadic latent class REM** (Lakdawala et al., 2026) captures this by assuming that dyads belong to one of $K$ distinct latent classes, each with its own parameter vector.
The model is a finite mixture of relational event models:
$$\mathscr{L}(\boldsymbol{\Theta}) = \prod_{(i,j) \in \mathcal{D}} \sum_{k=1}^{K} \pi_k \prod_{m: e_m \in \mathcal{R}_{ij}} f(e_m \mid \boldsymbol{\beta}_k)$$
where $\pi_k$ is the mixing proportion of class $k$ and $\boldsymbol{\beta}_k$ its class-specific parameters. The key feature is that all events of the same dyad share a latent class — the *dyadic* grouping that distinguishes this model from event-level mixtures.
In `remverse` this is requested through the `mixture` argument of `remstimate()`, or the convenience wrappers `remixture()` (general mixture) and `dlcrem()` (the dyadic-latent-class special case with a `~ (1 | dyad)` grouping). Fitting uses `flexmix`.
```{r load}
library(remverse)
```
---
# 1 Data
We use the `randomREH3` event history provided with `remverse`. Statistics use exponential memory decay (half-life set to 2000). To train the endogenous statistics we start the computation from the 100th event via the `first` argument.
```{r data}
data(randomREH3)
reh <- remify(edgelist = randomREH3, model = "tie", directed = TRUE)
stats <- remstats(
reh = reh,
tie_effects = ~ inertia(scaling = "std") + reciprocity(scaling = "std"),
first = 100, memory = "decay", memory_value = 2000
)
```
---
# 2 Fitting the model
`dlcrem()` fits the dyadic latent class model directly: it groups events by `dyad` and gives every included statistic a class-specific coefficient.
```{r fit-k2, message=FALSE, eval=has_flexmix}
fit_k2 <- dlcrem(reh = reh, stats = stats, k = 2)
fit_k2
```
Equivalently, `remixture(reh, stats, random = ~ (1 | dyad), k = 2)` or
`remstimate(reh, stats, mixture = list(k = 2, random = ~ (1 | dyad)))` fit the same model. All statistics vary across the $K$ classes; the `dyad` grouping ensures that events from the same dyad are assigned to the same class.
## 2.1 Interpreting the output
The output reports mixing proportions ($\pi_k$) and per-component coefficients ($\boldsymbol{\beta}_k$):
```{r coefs, eval=has_flexmix}
coef(fit_k2)
```
Each column is one latent class, sorted by mixing proportion (largest first). Differences between columns reveal *how* the classes differ — e.g. if reciprocity differs, some dyads are reciprocal while others are unidirectional; if the baseline differs, the classes capture high-rate vs. low-rate dyads.
---
# 3 Selecting the number of classes
The number of classes $K$ is usually unknown. Passing a vector to `k` fits all models at once and returns a list; `bic_table()` compares them by BIC.
```{r select-k, message=FALSE, eval=has_flexmix}
fit_multi <- remixture(
reh = reh,
stats = stats,
random = ~ (1 | dyad),
k = 1:4,
nrep = 5
)
bic_table(fit_multi)
```
The model with the lowest BIC is preferred. If $K = 1$ wins, there is no evidence for dyadic heterogeneity in the included effects. The `nrep` argument sets the number of random initializations per $K$, guarding against local optima. Based on these results, the model with `k = 2` latent classes is preferred by the BIC.
```{r plot-k, out.width="60%", fig.align="center", dev=c("jpeg"), dev.args = list(bg = "white"), eval=has_flexmix}
plot(fit_multi)
```
## 3.1 Inspecting the selected model
```{r best-model, eval=has_flexmix}
bics <- bic_table(fit_multi)
best_k <- bics$k[which.min(bics$BIC)]
cat("Best K:", best_k, "\n")
fit_best <- fit_multi[[paste0("k", best_k)]]
fit_best
```
---
# 4 Diagnostics
MIXREM diagnostics compute recall using posterior-weighted linear predictors: at each time point, a dyad's predicted rate is a weighted average across classes, weighted by its posterior class probabilities.
$$\hat{\lambda}_{ij}(t) = \sum_{k=1}^{K} P(Z_{ij} = k \mid \text{data}) \cdot \exp(\boldsymbol{\beta}_k^\top \mathbf{x}_{ij}(t))$$
As elsewhere in `remverse`, `diagnostics()` takes the fit together with the `reh` and `stats` it was built from.
```{r diagnostics, out.width="50%", dev=c("jpeg"), dev.args = list(bg = "white"), eval=has_flexmix}
diag_k2 <- diagnostics(fit_k2, reh, stats)
diag_k2
plot(diag_k2)
```
The diagnostics object also carries per-component recall (`recall_by_component`): how well each class's parameters predict that class's events. A component with much higher recall follows a clearer, more predictable pattern.
---
# 5 Comparison with MLE and GLMM
The three approaches address different sources of heterogeneity:
| Approach | Heterogeneity | Parameters | Interpretation |
|--------|--------------|------------|----------------|
| MLE | None assumed | Single $\boldsymbol{\beta}$ | One-size-fits-all |
| GLMM | Continuous (normal) | $\boldsymbol{\beta} + \mathbf{u}$ | Actor-level frailty |
| MIXREM | Discrete (classes) | $\boldsymbol{\beta}_1, \ldots, \boldsymbol{\beta}_K$ | Qualitatively different dyad types |
Use **GLMM** when actor effects vary continuously around a population mean; use **MIXREM** when you expect *distinct types* of relational dynamics (e.g. collaborative vs. hierarchical dyads).
```{r compare-loglik, message=FALSE, eval=has_flexmix}
fit_mle <- remstimate(reh = reh, stats = stats)
cat("MLE logLik: ", round(as.numeric(logLik(fit_mle)), 2), "\n")
cat("MIXREM logLik: ", round(as.numeric(logLik(fit_k2)), 2), "\n")
```
---
# 6 Practical considerations
**Convergence and local optima.** Finite mixture models are sensitive to starting values. Use `nrep >= 3` (the default); if results vary across runs, increase it.
**Sample size.** Each class needs enough data to estimate its parameters. With $P$ parameters and $K$ classes the model has $K \times P + (K-1)$ free parameters (`P` parameters per class, and `K-1` mixing proportions). The effective sample size per class is the number of dyad-event pairs assigned to it, not the total number of events.
**Identifiability.** If two components converge to the same parameters, the model is over-specified — reduce $K$.
**Grouping variable.** `dyad` grouping (via `dlcrem()`) is natural for tie-oriented models. For a different grouping, use `remixture(reh, stats, random = ~ (1 | actor1), ...)`.
---
#### References
Lakdawala, R., Leenders, R., & Mulder, J. (2026). Not all bonds are created equal: Dyadic latent class models for relational event data. *Social Networks*. https://doi.org/10.1016/j.socnet.2026.06.006
Grün, B., & Leisch, F. (2008). FlexMix version 2: Finite mixtures with concomitant variables and varying and constant parameters. *Journal of Statistical Software*, 28(4), 1–35.