--- title: "Duration Relational Event Models with remverse" subtitle: "A tutorial" author: "" date: "" output: rmarkdown::html_document: toc: true toc_depth: 3 header-includes: - \usepackage{amsmath,amssymb} - \usepackage{bm} vignette: > %\VignetteEngine{knitr::rmarkdown} %\VignetteIndexEntry{Duration Relational Event Models with remverse} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(dpi = 72, collapse = TRUE, comment = "#>") ``` --- A **Duration Relational Event Model (DuREM)** extends the standard relational event model (REM) to events that have both a *start time* and an *end time*. Instead of a single process governing when the next event fires, there are two linked sub-models: - a **start model**, which describes which inactive dyad initiates a new event and when; - an **end model**, which describes which currently active dyad terminates and when. The two sub-models are estimated jointly. Statistics for each sub-model can draw on (a) the accumulated event history — duration-weighted versions of standard remstats effects such as `inertia()` or `reciprocity()` — and (b) the *current active state* of the network, i.e. the set of events that have started but not yet ended. These active-state statistics can only be used for relational events with duration. The remverse DuREM pipeline follows the same three-step pattern as a standard REM: ``` remify(edgelist, duration = TRUE, ...) → remify_durem object remstats(reh, start_effects = ..., end_effects = ...) → remstats_durem object remstimate(reh, stats, ...) → remstimate_durem object ``` This vignette walks through each step using the `randomREH3` dataset provided with `remverse`. --- # 1 Data ## 1.1 The `randomREH3` dataset `randomREH3` is a simulated sequence of 999 directed duration events among 5 actors. Each row records one event with an observed start time and end time: | Column | Description | |------------|-------------| | `time` | Start time of the event | | `actor1` | Initiating actor (integer ID) | | `actor2` | Receiving actor (integer ID) | | `end` | End time of the event (`NA` for right-censored events) | | `setting` | Context of the interaction: `"work"` or `"social"` | | `duration` | Length of the event (`end - time`) | ```{r load-data} library(remverse) # loads remify, remstats, remstimate; remdata is a dependency data(randomREH3) data(info3) # actor attributes (age, sex) head(randomREH3) ``` Although the data contains both an `end` column and a `duration`, only one of these columns (with these exact names) is necessary for a DuREM analysis. The data were generated from a known duration model: starting an event depended on inertia, reciprocity, sender out-degree, incoming shared partners, and sender age; ending an event depended on the dyad's total active degree and the age difference of the two actors. ```{r data-summary} cat(sprintf( "%d events: %d complete, %d right-censored\n", nrow(randomREH3), sum(!is.na(randomREH3$end)), sum( is.na(randomREH3$end)) )) ``` This particular dataset has no right-censored events (every event has an observed end), but the pipeline fully supports censoring (`end = NA`); see §5.1. --- # 2 Step 1 — Processing with `remify()` Pass `duration = TRUE` to tell remify that the edgelist contains duration information. DuREM analysis are only possible under a `tie` oriented model. The function returns an object of class `c("remify_durem", "remify")`. ```{r remify} reh <- remify( edgelist = randomREH3, duration = TRUE, model = "tie", directed = TRUE # actor1 initiates; the end process is undirected by default ) ``` ## 2.1 The `remify_durem` object ```{r print-reh} reh ``` ```{r summary-reh} summary(reh) ``` The `plot()` method for a `remify_durem` object shows the duration-aware descriptives — including the distribution of event durations and how many events are active over time — in addition to the standard relational event descriptives of a `remify` object. ```{r plot-reh, out.width="70%", dev=c("jpeg"), dev.args = list(bg = "white")} plot(reh) ``` Alongside the standard `remify` slots (`$M`, `$N`, `$D`, `$edgelist`, `$meta`, …), a `remify_durem` object carries DuREM-specific elements. **`$durem`** — a named list of DuREM metadata: ```{r durem-slot} reh$durem ``` | Field | Meaning | |---------------------|---------| | `n_complete` | Events with an observed end time | | `n_censored` | Right-censored events (`end = NA`) | | `has_censored` | Whether any events are censored | | `dur_directed_end` | Whether the end process is directed | | `has_who_ended` | Whether a `who_ended` column is present | | `dur_type_exclusive`| Whether being active in one type blocks other types | **`$edgelist_dual`** — the dual-event expansion: one `"start"` row and one `"end"` row per complete event (censored events contribute only a `"start"` row), sorted by time. The `status` column states whether the observation at that time point is either the `start` of a new event or an `end` of an active event. The DuREM models these observed outcomes jointly by specifying separate sub-models for starting an event and for ending an event. ```{r edgelist-dual} head(reh$edgelist_dual, 12) ``` --- # 3 Step 2 — Statistics with `remstats()` For duration models, `remstats()` accepts two formulas — `start_effects` and `end_effects` — one per sub-model. Each formula may mix two kinds of terms: 1. **History-weighted effects** — the familiar tie-oriented effects (`inertia()`, `reciprocity()`, `totaldegreeDyad()`, `send()`, `difference()`, …), computed on the duration-weighted event history. An overview can be shown when running \code{?tie_effects}. 2. **Active-state effects** — effects that describe the *currently active* network (events that have started but not yet ended). These are the `active*()` family and are unique to the duration model. An overview can be shown when running \code{?active_effects}. `remstats()` inspects each term, routes it to the appropriate backend, and combines the results together automatically. ## 3.1 Active-state effect functions Which active-state effects are available depends on whether the sub-model is **directed**. The **start model** is directed whenever `directed = TRUE`; the **end model** is undirected by default (set `dur_directed_end = TRUE` in `remify()` to make it directed). **Directed active-state effects** (start model, or end model when `dur_directed_end = TRUE`): | Effect | Description | |--------|-------------| | `activeTie()` | Is there currently an active event from actor $i$ to actor $j$? | | `activeOutdegreeSender()` | Number of active events actor $i$ (sender) currently has as initiator | | `activeIndegreeReceiver()` | Number of active events actor $j$ (receiver) currently has as target | | `activeTotaldegreeSender()` | Total active degree of actor $i$ (as sender or receiver) | | `activeTotaldegreeReceiver()` | Total active degree of actor $j$ | | `activeTotaldegreeDyad()` | Combined active degree of both actors | | `activeReciprocalTie()` | Is the reverse dyad $j \to i$ currently active? | | `activeSharedPartners_otp()` / `_itp()` / `_osp()` / `_isp()` | Active shared-partner effects (outgoing/incoming two-path, outgoing/incoming shared partner) | **Undirected active-state effects** (end model, default): | Effect | Description | |--------|-------------| | `activeTie()` | Is the dyad currently active? | | `activeDegreeMin()` | Minimum active degree of the two actors | | `activeDegreeMax()` | Maximum active degree of the two actors | | `activeDegreeDyad()` | Combined active degree of the two actors | | `activeSharedPartners()` | Number of shared active partners | All active-state effects share the optional arguments `scaling = c("none", "std")` and `consider_type = c("ignore", "separate", "interact")` (see §3.3). > **Note.** A *directed* active-state effect such as `activeOutdegreeSender()` cannot be used in the (undirected) end model — it would be rejected as an unknown effect for that sub-model. Put directed active-state effects in the directed start model, and use the undirected active-state effects (or history-weighted effects) in the default end model. ## 3.2 Computing statistics Here we formulate a DuREM where the start model mixes history-weighted effects with a directed active-state effect, while the (undirected) end model uses history-weighted effects only. ```{r remstats, message=FALSE} # Start model (directed): history-weighted effects + active out-degree of sender start_fx <- ~ inertia(scaling = "std") + reciprocity(scaling = "std") + activeOutdegreeSender(scaling = "std") # End model (undirected): history-weighted effects end_fx <- ~ totaldegreeDyad(scaling = "std") + difference("age", attr_actors = info3, scaling = "std") dstats <- remstats( reh = reh, start_effects = start_fx, end_effects = end_fx, memory = "decay", memory_value = 2000, display_progress = FALSE ) ``` ```{r print-dstats} dstats ``` The returned object is of class `"remstats_durem"`. The statistics element can be viewed as a stacked remstats object: ```{r dstats-dims} head(dstats$stacked$remstats_stack) ``` ## 3.3 Event types and `consider_type` Similar as history-based statistics, the active-state statistics accept a `consider_type` argument that controls how the `setting` event type is handled when the model is built on a typed risk set: | Value | Behaviour | |-------|-----------| | `"ignore"` (default) | Counts active events of any type | | `"separate"` | One statistic per type; only type-$c$ events contribute to the type-$c$ stat | | `"interact"` | One statistic for every combination of past event types and the event type in the riskset (only possible when setting \code{extend_riskset_by_type = TRUE} in of the `remify` object). In case of `C` event types, this results in `C^2` distinct statistics.| ```{r consider-type-demo, eval=FALSE} # Rename `setting` to `type` and build a typed duration history history_typed <- randomREH3 names(history_typed)[names(history_typed) == "setting"] <- "type" reh_typed <- remify(history_typed, duration = TRUE, model = "tie", directed = TRUE) # "separate": one activeTie statistic per setting dstats_sep <- remstats( reh_typed, start_effects = ~ activeTie(consider_type = "separate") ) dimnames(dstats_sep$start_stats)[[3]] ``` --- # 4 Step 3 — Estimation with `remstimate()` `remstimate()` dispatches on the `"remstats_durem"` class and fits the joint start/end model. The start and end sub-models are stacked into a single long-format design matrix; start statistics are zero for end rows and vice versa. ## 4.1 Fitting the model ```{r remstimate} fit <- remstimate(reh = reh, stats = dstats) ``` ```{r print-fit} fit ``` ## 4.2 Coefficients and summary ```{r coef} coef(fit) ``` ```{r summary-fit} summary(fit) ``` Each coefficient quantifies the effect of the corresponding statistic on the start or end event rate. For example, `activeOutdegreeSender.start` captures engagement: a sender who is already in many active events may be less (or more) likely to initiate another, corresponding to a negative (or positive) coefficient, while `totaldegreeDyad.end` describes how a dyad's accumulated activity in past events relates to how quickly its events terminate. ## 4.3 Diagnostics ```{r durem-diag, out.width="60%", dev=c("jpeg"), dev.args = list(bg = "white")} diag <- diagnostics(fit, reh, dstats) plot(diag) ``` ## 4.4 Model comparison Because `remstimate_durem` returns a standard `logLik` object, information criteria compare nested models in the usual way: ```{r model-comparison, message=FALSE} # A simpler model: baseline dynamics only dstats_simple <- remstats( reh, start_effects = ~ inertia(scaling = "std"), end_effects = ~ totaldegreeDyad(scaling = "std"), memory = "decay", memory_value = 2000 ) fit_simple <- remstimate(reh = reh, stats = dstats_simple) logLik(fit) logLik(fit_simple) AIC(fit) AIC(fit_simple) ``` --- # 5 Additional topics ## 5.1 Duration based event weights When computing the history-weighted (endogenous) effects, `inertia()`, `reciprocity()`, etc., the duration of past events are used as event weights according to the formulas: - `(duration + 1)^psi_start` for the `start_effects` - `(duration + 1)^psi_end` for the `end_effects`. Thus, the `psi_start` and `psi_end` arguments of `remstats` control the duration-weighting exponent applied to the history-weighted terms (and are ignored for active-state terms). Defaults are `psi_start = 1` and `psi_end = 1`, implying that events with longer duration result in a larger weight in the endogenous statistics. On the other hand, if these arguments would be set to a negative value, then events with a longer duration have a smaller weight in the endogenous statistics. Thus, the `psi_start` and `psi_end` have a comparable role as the `memory_value` argument which affects the weight of events as function of the transpired time. Moreover, if events also have a weight by its (e.g., for quantifying the intensity of an event), specified via `event_weight`, the weight of the past event is computed by multiplying its `weight` with the duration-based weight. ## 5.2 Right-censored events Right-censored events (`end = NA` in the edgelist) are fully supported. They contribute a start row to the dual edgelist and appear in the active-state counts until the end of the observation window, but never produce an observed end event. The `$durem` slot records how many events are censored: ```{r censored} reh$durem$n_censored reh$durem$n_complete ``` ## 5.3 Simultaneous events and boundary coincidences The dual edgelist may contain multiple rows at the same time point — two events starting simultaneously, or one event ending exactly when another begins. `remify_durem` handles these automatically, computing the active state consistently just before each time point. ```{r simultaneous} # Time points in the dual edgelist shared by more than one row tab <- table(reh$edgelist_dual$time) head(tab[tab > 1], 6) ``` ## 5.3 Directed end process By default the end process is undirected — either actor can terminate an event. To model a directed end process (e.g. when a specific actor ends the event), set `dur_directed_end = TRUE`. This makes the *directed* active-state effects available in `end_effects` as well: ```{r directed-end, eval=FALSE} reh_de <- remify(randomREH3, duration = TRUE, model = "tie", directed = TRUE, dur_directed_end = TRUE) dstats_de <- remstats( reh_de, start_effects = ~ inertia(scaling = "std") + activeOutdegreeSender(scaling = "std"), end_effects = ~ activeOutdegreeSender(scaling = "std") ) fit_de <- remstimate(reh = reh_de, stats = dstats_de) coef(fit_de) ``` --- #### References Butts, C. T. (2008). A relational event framework for social action. *Sociological Methodology*, 38(1), 155–200. https://doi.org/10.1111/j.1467-9531.2008.00203.x Hoffman, M., Block, P., Elmer, T., & Stadtfeld, C. (2020). A model for the dynamics of face-to-face interactions in social groups. *Network Science*, 8(S1), S4–S25. https://doi.org/10.1017/nws.2020.3 Lakdawala, R., Leenders, R., Ejbye-Ernst, P., & Mulder, J. (2026). Modelling interaction duration in relational event models. *arXiv preprint*. https://doi.org/10.48550/arXiv.2602.21000