# Moving-Window Relational Event Models

### Detecting temporal drift in tie- and actor-oriented parameters

####

A standard relational event model assumes a single, fixed set of coefficients over the entire observation period. That assumption is often convenient rather than realistic: the strength of inertia, reciprocity, or any other effect can strengthen, weaken, or reverse as a network matures. `remwindow()` tests this by re-fitting the same model specification repeatedly over subregions (“windows”) of the event sequence, reusing a single `remify()` object and a single `remstats()` computation throughout — no re-processing of the event history or re-computation of statistics is needed per window.

This vignette covers moving-window estimation for both tie-oriented and actor-oriented models: how windows are chosen, how to read the stability diagnostics, and how recall-based diagnostics are computed continuously across windows via interpolation.

    library(remverse)

# 1 When to use moving-window estimation

Moving-window estimation is useful when:

- Temporal stability is itself the question — e.g., did the tendency to reciprocate change over the observation period, without specifying a shift point in advance?
- A single global fit hides local behavior — a large positive coefficient early and a near-zero coefficient later can average to a modest global estimate that misrepresents both periods.
- A quick diagnostic is wanted before a formal change-point model — moving windows are cheap and give a first visual read.

As each window is an independent fit on a subset of events, the moving-window appraoch cannot be viewed as a substitute for a fully specified time-varying-coefficients model.

# 2 Data

We use `randomREH3` (provided with `remverse`): 999 directed events among 5 actors, together with the actor attributes in `info3`. The sequence was generated in three successive regimes with different parameter values, so it carries genuine temporal drift — a natural showcase for moving-window estimation. Statistics use exponential memory decay (half-life 2000).

    data(randomREH3)
    data(info3)
    str(randomREH3)
    #> 'data.frame':    999 obs. of  6 variables:
    #>  $ time    : num  18 57 64 127 228 274 302 330 358 425 ...
    #>  $ actor1  : num  5 3 1 5 1 2 1 1 1 2 ...
    #>  $ actor2  : num  1 2 5 1 4 4 5 5 5 4 ...
    #>  $ end     : num  24 70 85 159 239 320 314 339 362 521 ...
    #>  $ setting : chr  "social" "social" "social" "social" ...
    #>  $ duration: num  6 13 21 32 11 46 12 9 4 96 ...

With roughly a thousand events and only a handful of estimated parameters, the default window sizing rule (20 events per parameter, floored at 50 events per window) supports the default `n.windows = 5`. Manual choices of the window size and number of windows can be changed via the function’s arguments.

# 3 Tie-oriented moving-window models

## 3.1 Default (auto) windowing

    reh <- remify(
      edgelist = randomREH3,
      model    = "tie",
      directed = TRUE
    )

    stats_tie <- remstats(
      reh = reh,
      tie_effects = ~ inertia(scaling = "std") + reciprocity(scaling = "std") +
                       send("age", attr_actors = info3, scaling = "std"),
      memory = "decay", memory_value = 2000
    )

    mw_tie_auto <- remwindow(reh = reh, stats = stats_tie)
    mw_tie_auto
    #> Moving-window relational event model (tie)
    #> windows = 5  (mode: auto )
    #>
    #> Coefficients:
    #>                  W1         W2          W3          W4          W5
    #> time_range  57-7131 7178-15441 15562-25568 25665-37469 37710-50676
    #> n_events        198        198         199         198         199
    #> converged      TRUE       TRUE        TRUE        TRUE        TRUE
    #> baseline     -6.822     -7.062      -7.212      -7.418      -7.628
    #> inertia       0.388      0.544       0.536       0.482       0.455
    #> reciprocity   0.313      0.345       0.285       0.334       0.431
    #> send_age     -0.002     -0.005      -0.049       0.091       0.082

With four estimated parameters (`baseline`, `inertia`, `reciprocity`, `send_age`) and roughly 200 events per window, the default target of five windows is met directly.

    summary(mw_tie_auto)
    #> Moving-window relational event model (tie)
    #> windows = 5
    #>
    #> Fit:
    #>  window  time_range n_events converged    loglik      AIC      BIC
    #>       1     57-7131      198      TRUE -1450.533 2909.065 2922.218
    #>       2  7178-15441      198      TRUE -1468.033 2944.066 2957.219
    #>       3 15562-25568      199      TRUE -1530.498 3068.997 3082.170
    #>       4 25665-37469      198      TRUE -1536.591 3081.183 3094.336
    #>       5 37710-50676      199      TRUE -1540.689 3089.378 3102.551
    #>
    #> Stability (across converged, non-separated windows):
    #>       effect n   mean    sd    min    max mean_se sd_over_se excluded
    #>     baseline 5 -7.229 0.312 -7.628 -6.822   0.092      3.394        -
    #>      inertia 5  0.481 0.064  0.388  0.544   0.080      0.797        -
    #>  reciprocity 5  0.342 0.055  0.285  0.431   0.080      0.686        -
    #>     send_age 5  0.023 0.061 -0.049  0.091   0.078      0.775        -

The Fit block reports one column per window: its time range, event count, convergence status, and log-likelihood/AIC/BIC. The Stability block collapses across windows per effect: mean, standard deviation, and range of the estimate, together with `sd_over_se` — the ratio of across-window spread to the average within-window standard error. Values well above 1 suggest genuine drift; values near or below 1 are consistent with a single stable coefficient.

    plot(mw_tie_auto)

The ribbon is a 95% Wald interval around each window’s point estimate; the x-axis is the window midpoint in event-time.

## 3.2 Manual, overlapping windows

Windows can also be specified directly via `window.width` and `step.size.window`, which controls overlap explicitly rather than relying on the non-overlapping automatic default.

    mw_tie <- remwindow(
      reh              = reh,
      stats            = stats_tie,
      window.width     = 200,
      step.size.window = 50
    )
    mw_tie
    #> Moving-window relational event model (tie)
    #> windows = 20  (mode: manual )
    #>
    #> Coefficients:
    #>                  W1        W2         W3         W4         W5         W6
    #> time_range  57-7232 2018-9436 3735-11352 5240-13436 7262-15708 9439-18242
    #> n_events        200       200        200        200        200        200
    #> converged      TRUE      TRUE       TRUE       TRUE       TRUE       TRUE
    #> baseline     -6.828    -6.879     -6.902     -7.007     -7.067     -7.116
    #> inertia       0.391     0.525      0.474      0.543      0.523      0.465
    #> reciprocity   0.315      0.22      0.298      0.276      0.358      0.421
    #> send_age     -0.008     0.002      0.091      0.044      0.019     -0.034
    #>                      W7          W8          W9         W10         W11
    #> time_range  11397-20472 13450-23288 15850-26015 18282-28770 20505-31575
    #> n_events            200         200         200         200         200
    #> converged          TRUE        TRUE        TRUE        TRUE        TRUE
    #> baseline         -7.141      -7.223      -7.221      -7.247      -7.317
    #> inertia           0.537       0.554       0.538       0.549       0.526
    #> reciprocity       0.324       0.311       0.275       0.288       0.318
    #> send_age         -0.068      -0.039      -0.036      -0.017      -0.005
    #>                     W12         W13         W14         W15         W16
    #> time_range  23328-34285 26161-38157 28778-42174 31691-45324 34320-48242
    #> n_events            200         200         200         200         200
    #> converged          TRUE        TRUE        TRUE        TRUE        TRUE
    #> baseline         -7.298      -7.446      -7.591      -7.615      -7.694
    #> inertia           0.469       0.479       0.434       0.459       0.474
    #> reciprocity       0.332       0.351       0.411       0.365        0.42
    #> send_age          0.017       0.063       0.086       0.038       0.052
    #>                     W17         W18         W19         W20
    #> time_range  38166-50676 42218-50676 45346-50676 48436-50676
    #> n_events            192         142          92          42
    #> converged          TRUE        TRUE        TRUE        TRUE
    #> baseline         -7.596      -7.488      -7.474      -7.351
    #> inertia           0.472       0.473       0.367      -0.172
    #> reciprocity       0.403       0.387       0.522       0.894
    #> send_age          0.099       0.077       0.143       0.178

Because `step.size.window` (50) is smaller than `window.width` (200), consecutive windows overlap by 150 events. The final window absorbs any remainder, so no events are silently dropped. Because windows overlap, a single event can fall in more than one window’s range — this is why the diagnostics in Section 5 are computed from a continuously interpolated coefficient rather than assigning each event to one “owning” window.

    plot(mw_tie)

## 3.3 Reading the stability table

    summary(mw_tie)
    #> Moving-window relational event model (tie)
    #> windows = 20
    #>
    #> Fit:
    #>  window  time_range n_events converged    loglik      AIC      BIC
    #>       1     57-7232      200      TRUE -1465.504 2939.007 2952.201
    #>       2   2018-9436      200      TRUE -1467.755 2943.510 2956.704
    #>       3  3735-11352      200      TRUE -1476.198 2960.396 2973.589
    #>       4  5240-13436      200      TRUE -1487.847 2983.694 2996.887
    #>       5  7262-15708      200      TRUE -1485.673 2979.346 2992.539
    #>       6  9439-18242      200      TRUE -1492.908 2993.816 3007.010
    #>       7 11397-20472      200      TRUE -1505.300 3018.600 3031.794
    #>       8 13450-23288      200      TRUE -1518.054 3044.109 3057.302
    #>       9 15850-26015      200      TRUE -1541.232 3090.464 3103.657
    #>      10 18282-28770      200      TRUE -1550.364 3108.728 3121.921
    #>      11 20505-31575      200      TRUE -1550.158 3108.316 3121.509
    #>      12 23328-34285      200      TRUE -1546.219 3100.437 3113.630
    #>      13 26161-38157      200      TRUE -1549.222 3106.443 3119.637
    #>      14 28778-42174      200      TRUE -1550.387 3108.773 3121.966
    #>      15 31691-45324      200      TRUE -1553.706 3115.413 3128.606
    #>      16 34320-48242      200      TRUE -1549.608 3107.216 3120.410
    #>      17 38166-50676      192      TRUE -1487.010 2982.019 2995.049
    #>      18 42218-50676      142      TRUE -1094.357 2196.715 2208.538
    #>      19 45346-50676       92      TRUE  -709.959 1427.917 1438.005
    #>      20 48436-50676       42      TRUE  -330.323  668.646  675.597
    #>
    #> Stability (across converged, non-separated windows):
    #>       effect  n   mean    sd    min    max mean_se sd_over_se excluded
    #>     baseline 20 -7.275 0.259 -7.694 -6.828   0.101      2.575        -
    #>      inertia 20  0.454 0.156 -0.172  0.554   0.099      1.582        -
    #>  reciprocity 20  0.374 0.140  0.220  0.894   0.099      1.413        -
    #>     send_age 20  0.035 0.064 -0.068  0.178   0.085      0.756        -

Two columns matter most when a coefficient looks unstable:

- `n` — the number of windows that contributed to the mean/sd/min/max for that effect (can be less than the total window count).
- `excluded` — which windows were left out: a window is excluded from an effect’s stability summary when that effect’s standard error there exceeds `k` times the median SE across windows (default `k = 10`). This flags quasi-complete separation — a coefficient running large because a pattern was rare or absent in that specific window.

A coefficient with a small `sd_over_se` and no exclusions behaves like a single stable parameter. A coefficient with excluded windows should be interpreted with those windows’ event counts and convergence status in mind.

# 4 Actor-oriented moving-window models

## 4.1 Sender + receiver model

    reh_ao <- remify(
      edgelist = randomREH3,
      model    = "actor",
      directed = TRUE
    )

    stats_ao <- remstats(
      reh = reh_ao,
      sender_effects   = ~ outdegreeSender(scaling = "std"),
      receiver_effects = ~ inertia(scaling = "std") + reciprocity(scaling = "std"),
      memory = "decay", memory_value = 2000
    )

    mw_actor <- remwindow(reh = reh_ao, stats = stats_ao)
    mw_actor
    #> Moving-window relational event model (actor)
    #> windows = 5  (mode: auto )
    #>
    #> Sender (rate) model:
    #>                      W1         W2          W3          W4          W5
    #> time_range      57-7131 7178-15441 15562-25568 25665-37469 37710-50676
    #> n_events            198        198         199         198         199
    #> converged          TRUE       TRUE        TRUE        TRUE        TRUE
    #> baseline         -5.187      -5.35      -5.531      -5.704      -5.831
    #> outdegreeSender  -0.049      0.088       0.062       0.094       0.298
    #>
    #> Receiver (choice) model:
    #>                  W1         W2          W3          W4          W5
    #> time_range  57-7131 7178-15441 15562-25568 25665-37469 37710-50676
    #> n_events        198        198         199         198         199
    #> converged      TRUE       TRUE        TRUE        TRUE        TRUE
    #> inertia       0.196      0.031      -0.129       0.158       0.094
    #> reciprocity   0.646     -0.029       0.169       0.028      -0.078

The printed table shows the sender (rate) and receiver (choice) submodels separately, each with its own coefficient rows, since they are two independently optimized fits per window.

    summary(mw_actor)
    #> Moving-window relational event model (actor)
    #> windows = 5
    #>
    #> Fit:
    #>  window  time_range n_events converged    loglik      AIC      BIC
    #>       1     57-7131      198      TRUE -1449.482 2906.963 2920.116
    #>       2  7178-15441      198      TRUE -1530.431 3068.862 3082.015
    #>       3 15562-25568      199      TRUE -1590.065 3188.131 3201.304
    #>       4 25665-37469      198      TRUE -1606.715 3221.430 3234.583
    #>       5 37710-50676      199      TRUE -1637.005 3282.009 3295.183
    #>
    #> Stability - sender (rate) model:
    #>           effect n   mean    sd    min    max mean_se sd_over_se excluded
    #>         baseline 5 -5.520 0.260 -5.831 -5.187   0.071      3.642        -
    #>  outdegreeSender 5  0.099 0.126 -0.049  0.298   0.079      1.597        -
    #>
    #> Stability - receiver (choice) model:
    #>       effect n  mean    sd    min   max mean_se sd_over_se excluded
    #>      inertia 5 0.070 0.128 -0.129 0.196   0.200      0.638        -
    #>  reciprocity 5 0.147 0.294 -0.078 0.646   0.199      1.475        -

    plot(mw_actor)

The sender-model panels are shown first, followed by the receiver-model panels.

## 4.2 Receiver-only actor model

An actor model need not specify a sender (rate) submodel — a receiver-choice-only specification is valid, and `remwindow()` handles the missing submodel throughout (fitting, stability tables, plots, diagnostics all skip the absent piece cleanly).

    stats_choice_only <- remstats(
      reh = reh_ao,
      receiver_effects = ~ inertia(scaling = "std") + reciprocity(scaling = "std"),
      memory = "decay", memory_value = 2000
    )

    mw_actor_choice <- remwindow(reh = reh_ao, stats = stats_choice_only)
    mw_actor_choice
    #> Moving-window relational event model (actor)
    #> windows = 5  (mode: auto )
    #>
    #> Receiver (choice) model:
    #>                  W1         W2          W3          W4          W5
    #> time_range  57-7131 7178-15441 15562-25568 25665-37469 37710-50676
    #> n_events        198        198         199         198         199
    #> converged      TRUE       TRUE        TRUE        TRUE        TRUE
    #> inertia       0.196      0.031      -0.129       0.158       0.094
    #> reciprocity   0.646     -0.029       0.169       0.028      -0.078

Only the receiver (choice) model appears — there is no sender block, and no error is raised for its absence.

# 5 Diagnostics across windows

Because windows can overlap (Section 3.2), assigning each event to a single window for recall-based diagnostics is not well defined at the boundary. Instead, `diagnostics()` on a `remstimate_window` object builds, for every event, a coefficient vector obtained by linear interpolation between window midpoints in event-time:

\[\hat{\boldsymbol\beta}(t) = \hat{\boldsymbol\beta}(t_w) + \frac{t - t_w}{t_{w+1} - t_w}\Big(\hat{\boldsymbol\beta}(t_{w+1}) - \hat{\boldsymbol\beta}(t_w)\Big), \qquad t_w \le t < t_{w+1}\]

where \(t_w\) is the midpoint time of window \(w\). Before \(t_1\) or after the last midpoint, the nearest window’s estimate is used unchanged (no extrapolation). Windows flagged as separated for a given effect (Section 3.3) are skipped as interpolation anchors for that effect only.

Recall is then computed once, continuously, over every event using this time-varying coefficient.

## 5.1 Tie model

    diag_tie <- diagnostics(mw_tie, reh, stats_tie)
    diag_tie
    #> Moving-window diagnostics (tie, interpolated coefficients)
    #> windows = 20
    #>
    #> Recall:
    #>   mean rank = 0.749  |  median rank = 0.842  |  prob ratio = 1.87  |  top 5% = 17.3%

    plot(diag_tie)
    #> Warning in regularize.values(x, y, ties, missing(ties), na.rm = na.rm):
    #> collapsing to unique 'x' values

Each point is one observed event’s relative rank among its competing dyads at that moment, scored against the interpolated coefficient. The dashed horizontal line is the overall median rank; the solid line is a robust (median-smoothed) trend. Dotted vertical lines mark the original window boundaries.

## 5.2 Actor model

    diag_actor <- diagnostics(mw_actor, reh_ao, stats_ao)

    plot(diag_actor)
    #> Warning in regularize.values(x, y, ties, missing(ties), na.rm = na.rm):
    #> collapsing to unique 'x' values

    #> Warning in regularize.values(x, y, ties, missing(ties), na.rm = na.rm):
    #> collapsing to unique 'x' values

Sender and receiver recall scatterplots are shown in turn, following the same layout as the tie-model version.

# 6 Parallelization

Each window is an independent fit, so windows can be estimated in parallel via `parallel::mclapply` (Unix/macOS; falls back to sequential with a message on Windows):

    mw_parallel <- remwindow(
      reh           = reh,
      stats         = stats_tie,
      parallel      = TRUE,
      ncores.window = 4
    )

When parallelizing across windows, it is usually best to leave the within-fit `ncores` at 1; `remwindow()` warns when the product of `ncores.window` and a within-fit `ncores` exceeds the detected cores.

# 7 Current limitations

- Duration models (`durem`) are not yet supported. `remwindow()` accepts `tomstats`/`aomstats` objects for tie- and actor-oriented models only; a duration-model `remstats` object raises an informative error. Windowing an active-state, interval-based process raises separate design questions (an interval can straddle a window boundary in a way a point event cannot) and is on the roadmap.
- Case-control sampled statistics (`tomstats_sampled`) are not yet supported.
- The stability table’s separation flag (`k = 10`) is a heuristic, not a formal test.

# 8 Summary

Choice | Argument | Notes
--- | --- | ---
Window count (auto) | `remwindow(n.windows = ...)` | Default 5; silently reduced if the event floor cannot be met
Window size (manual) | `remwindow(window.width = ...)` | Disables auto mode
Window step | `remwindow(step.size.window = ...)` | Defaults to `window.width` (non-overlapping); smaller values overlap
Minimum events/window | `remwindow(min.events = ...)` | Floor on auto-computed width
Separation threshold | `summary(..., k = ...)`, `coef(..., k = ...)` | SE-ratio multiplier for flagging a diverged window, per effect
Estimation approach | `remwindow(approach = ...)` | `"frequentist"` (default) or `"Bayesian"`, forwarded to `remstimate()`
Parallel windows | `remwindow(parallel = TRUE, ncores.window = ...)` | Unix/macOS only

`remwindow()` and its methods (`print`, `summary`, `plot`, `coef`, `diagnostics`) sit alongside the standard single-fit pipeline: the same `reh` and `stats` objects are reused unchanged, and only the estimation step is repeated across subregions of the event sequence.

#### References

Meijerink-Bosman, M., Leenders, R., & Mulder, J. (2022). Dynamic relational event modeling: Testing, exploring, and applying. PLoS One, 17(8). https://doi.org/10.1371/journal.pone.0272309

Mulder, J., & Leenders, R. T. A. (2019). Modeling the evolution of interaction behavior in social networks: A dynamic relational event approach for real-time analysis. Chaos, Solitons & Fractals, 119, 73-85. https://doi.org/10.1016/j.chaos.2018.11.027