--- title: "Introduction" author: - "Mohammed Saqr" - "Sonsoles López-Pernas" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE, dpi = 130, fig.width = 6.8, fig.height = 5.4, out.width = "100%", fig.align = "center") set.seed(2026) library(lagdynamics) old_options <- options(digits = 3) has_cograph <- requireNamespace("cograph", quietly = TRUE) has_ggplot2 <- requireNamespace("ggplot2", quietly = TRUE) ``` # Summary `lagdynamics` implements lag-sequential analysis (LSA) and its network representation, the lag transition network, for categorical event and sequence data. LSA is the classical inferential method for temporal contingency in observed behaviour: it tests, for every ordered pair of states, whether one state follows another more or less often than independence predicts. The package restates this method as a modern statistical workflow built on six commitments: - **Tidy.** Every result is produced by a verb -- `transitions()`, `nodes()`, `tests()`, `initial()`, `summary()`, `transition_probabilities()` -- that returns a one-row-per-observation data frame. `transitions()` reads the fitted model and every inference result through the same arguments. Filters are arguments (`significant = TRUE`, `direction = "over"`, `min_count = `), never subsetting; the user never indexes into an object. - **Confirmatory, following Dynalytics.** The package operationalizes the Dynalytics framework, whose central tenet is a scientific contract: every analytical claim is matched by evidence appropriate to its structure, scope, and complexity. Validation is therefore expanded far beyond the classical residual test into a full battery -- analytic Bayesian certainty, sequence-level bootstrap, split-half reliability, case-drop stability, and a within-sequence permutation null. - **Expanded.** Beyond the classical engine the package offers five estimation engines plus an open registry for user-defined ones, multi-lag analysis with positive and negative lags, structural-zero constraints under quasi-independence, initial and transition probabilities, grouped estimation in a shared state space, and an experimental directed transfer-entropy measure. - **Group-aware.** A grouping variable yields one model per group, and group differences are tested rather than eyeballed: `compare_lsa()` permutes labels over whole sequences and `bayes_compare_lsa()` gives the analytic Bayesian counterpart, both with multiplicity adjustment and pairwise comparison for more than two groups. - **Visual.** A large array of plots covers every level of the analysis: residual heatmaps, residual networks, probability-weighted TNA transition networks, chord diagrams, polar sunbursts, uncertainty forests for the bootstrap and certainty results, and group barrels and difference heatmaps for the comparisons. - **Interoperable.** The package plugs into the other dynamics packages on both sides: `lsa()` ingests long event logs, wide data, lists of sequences, `TraMineR` state sequences, `tna` models and sequence data, and `Nestimate` prepared data, while the fitted object is a `cograph` network that cograph renders directly and exposes the transition and initial probabilities that TNA-style tooling consumes. The numerical core is a clean-room implementation. Every formula was transcribed from its primary source -- with page and equation numbers recorded in the package's formula reference map (`inst/REFERENCES.md`) -- and implemented without consulting any prior LSA software. The implementation is cross-validated against base-R primitives (`stats::loglin()` for iterative proportional fitting, `stats::chisq.test()` standardized residuals, `stats::binom.test()`, `stats::pchisq()`) and against hand-derived algebraic identities. The analytical core depends only on base R; the plotting functions additionally use `ggplot2` and `cograph`. # The method Let a process visit $K$ discrete states. At lag $\ell$, the data reduce to a $K \times K$ transition table whose cell $O_{ij}$ counts the occasions on which state $i$ was followed, $\ell$ positions later, by state $j$. Transitions are counted within a sequence only; pairs never span sequence boundaries. The reference model is sequential independence: the next state carries no memory of the current one, so each cell is expected to occur at the rate implied by the row and column margins alone, $$E_{ij} = \frac{R_i \, C_j}{N},$$ where $R_i$ and $C_j$ are the margins of the table and $N$ its total. The test statistic for each cell is the adjusted (standardized Pearson) residual, $$z_{ij} = \frac{O_{ij} - E_{ij}} {\sqrt{E_{ij}\,(1 - R_i/N)\,(1 - C_j/N)}},$$ which is asymptotically standard normal under independence. A positive residual marks a transition that occurs more often than its states' base rates predict -- a sequential regularity. A negative residual marks an avoided transition. The residual, not the raw transition probability, is the evidential unit of LSA: a transition can be frequent yet unremarkable, or rare yet strongly over-represented. When some transitions are impossible by design -- self-transitions under continuous coding are the standard case -- the affected cells are structural zeros. Expected counts then come from the quasi-independence model, fitted by iterative proportional fitting, and the residuals use the design-matrix form. The two formulations provably coincide when no structural zeros are present, and the package pins that identity in its test suite. Structural-zero cells are reported as non-estimable (`NA`), never as "expected zero": no test is defined there. A **lag transition network** is the graph form of the fitted model: states are nodes, each ordered pair is a directed edge, and the edge weight is the tested departure from independence. This places LSA next to transition network analysis (TNA), with a precise division of labour: a TNA edge is a transition tendency (a conditional probability), whereas an LSA edge is a tested departure from a no-memory baseline. The same data can support both models; the claims differ. # What the package provides Beyond the classical model, `lagdynamics` adds the following. Each item is exercised in this vignette or in a dedicated companion vignette. **Estimation** - Five estimation engines: `classical` (the default), `two_cell`, `bidirectional` (the matched-pair test on the symmetrized table), `parallel_dominance`, and `nonparallel_dominance`, each available through `lsa(engine = )` or a direct wrapper such as `lsa_two_cell()`. - An open engine registry: `register_lsa_engine()` / `unregister_lsa_engine()` accept user-defined engines, and `list_lsa_engines()` enumerates what is installed. - Multi-lag analysis: any positive or negative lag in `lsa(lag = )`, full lag profiles with `lsa_lags()`, and single-edge profiles with `lag_profile()`. - Structural-zero support: `loops = FALSE` for the no-self-transition model, or an arbitrary 0/1 constraint matrix via `structural_zeros`, with quasi-independence expectations from the exported `lsa_ipf()`. - Grouped estimation: a grouping variable yields one fit per group in a shared state space, so groups remain comparable even when a group never visits a state. - An experimental directed transfer-entropy measure, `transfer_entropy()`. **Inference and validation** - Analytic edge uncertainty: `certainty_lsa()` derives credible intervals for transition probabilities from a Dirichlet-Multinomial posterior, without resampling. - Resampling edge uncertainty: `bootstrap_lsa()` resamples whole sequences -- preserving within-sequence dependence -- and refits. - Whole-network reproducibility: `reliability_lsa()` (repeated split-half refitting) and `stability_lsa()` (case-dropping). - An assumption-free null: `permute_lsa()` shuffles events within sequences and recomputes the residuals. - Group comparison: `compare_lsa()` permutes group labels at the sequence level, the empirically exchangeable unit; `bayes_compare_lsa()` is its analytic Bayesian counterpart. Both support multiplicity adjustment and, for more than two groups, pairwise comparison. **Interface** - Tidy accessors throughout: `transitions()`, `nodes()`, `tests()`, `initial()`, `transition_probabilities()`, and `summary()`. `transitions()` reads the fitted model and every inference result with the same arguments. No result requires indexing into an object. - One plotting verb with several geometries, plus dedicated plots for uncertainty and comparison results (see the gallery below). - Ingestion of long event logs, wide sequence data, lists of sequences, and foreign sequence objects (see Interoperability). # Fitting and reading a model The examples use the bundled `engagement` data: weekly engagement states (`active`, `average`, `disengaged`) for 138 students, one row per student. ```{r fit} fit <- lsa(engagement) fit ``` Every fitted quantity is read through a verb that returns a tidy data frame. `transitions()` gives one row per ordered pair, with observed and expected counts, transition probability, adjusted residual, p-value, and association measures; filters are arguments, not subsetting. ```{r read} transitions(fit) transitions(fit, significant = TRUE) nodes(fit) tests(fit) initial(fit) summary(fit) ``` The lag is a first-class argument. A lag profile traces one edge across temporal distances; `lsa_lags()` fits the full multi-lag model. ```{r lags} lag_profile(engagement, "Active", "Disengaged", lags = 1:3) transitions(lsa_lags(engagement, lags = 1:2)) |> head(6) ``` Structural zeros restate the model rather than filter its output: forbidden cells are non-estimable. ```{r structural} fz <- lsa(engagement, loops = FALSE) transitions(fz) |> subset(from == to) ``` Alternative engines and the open registry: ```{r engines} list_lsa_engines() lsa_two_cell(engagement) ``` # Visualisation One verb, `plot(fit, type = )`, renders the model; dedicated methods cover the inference objects. Every view of a fit displays the adjusted residual (or, on request, another fitted matrix) and differs only in geometry. | view | call | shows | |---|---|---| | residual heatmap | `plot(fit)` | the fitted residual matrix | | residual network | `plot(fit, type = "network")` | tested departures as directed edges | | transition (TNA) network | `plot(fit, type = "network", weights = "tna")` | conditional transition probabilities | | chord diagram | `plot(fit, type = "chord")` | transition flow between states | | polar sunburst | `plot(fit, type = "sunburst")` | outgoing distribution per state | | uncertainty forest | `plot(bootstrap_lsa(fit))`, `plot(certainty_lsa(fit))` | edge-level intervals | | group barrel | `plot(compare_lsa(g))` | per-group edge estimates with test results | | difference heatmap | `plot(compare_lsa(g), style = "heatmap")` | signed group differences | Two colour conventions are used, by purpose. The heatmap, chord, and sunburst use a diverging residual scale (warm = over-represented, cool = avoided). The network and comparison plots follow the TNA convention -- blue = more than chance, red = less, avoided edges dashed -- so an `lsa` network reads like any other transition network. ```{r heatmap, eval = has_ggplot2} plot(fit) ``` ```{r network, eval = has_cograph} plot(fit, type = "network") ``` ```{r tna-network, eval = has_cograph} plot(fit, type = "network", weights = "tna") ``` ```{r chord, eval = has_cograph} plot(fit, type = "chord") ``` ```{r sunburst, eval = has_ggplot2} plot(fit, type = "sunburst") ``` The full gallery, including the `which = ` matrix selector and the grouped-plot variants, is in `vignette("plotting", package = "lagdynamics")`. # Group comparison A grouping variable produces one fit per group in a shared state space. Here the long-format `group_regulation_long` event log is split by achievement level, using the `actor` / `action` / `time` grammar. ```{r groups} gfit <- lsa(group_regulation_long, actor = "Actor", action = "Action", time = "Time", group = "Achiever") gfit transitions(gfit, significant = TRUE) |> head(4) summary(gfit) ``` `compare_lsa()` tests whether the groups differ, edge by edge, by permuting group labels over whole sequences -- the unit that is actually exchangeable under the null -- with optional multiplicity adjustment. `bayes_compare_lsa()` reaches the same question analytically, reporting the credible range of each group difference in transition probability. With more than two groups, both return pairwise results. ```{r compare} cmp <- compare_lsa(gfit, R = 100, adjust = "BH") cmp transitions(cmp) |> head(4) bcmp <- bayes_compare_lsa(gfit, draws = 1000, adjust = "BH", seed = 1) bcmp transitions(bcmp) |> head(4) ``` ```{r compare-plots, eval = has_ggplot2} plot(cmp) plot(cmp, style = "heatmap") ``` # Confirmatory battery The adjusted residual tests each edge against independence, but that test rests on large-sample assumptions that sequential data can violate, and stronger claims -- about precision, robustness, or the network as a whole -- need their own evidence. The battery pairs each kind of claim with a matching procedure: | claim | evidence | verb | |---|---|---| | a specific transition is real; how precise | edge-level uncertainty | `certainty_lsa()`, `bootstrap_lsa()` | | a significant transition is not fragile | robustness to case loss | `stability_lsa()` | | the whole network is reproducible | split-half reliability | `reliability_lsa()` | | the structure exceeds chance | assumption-free empirical null | `permute_lsa()` | | two groups differ | inference under exchangeability | `compare_lsa()`, `bayes_compare_lsa()` | ```{r battery} cert <- certainty_lsa(fit) transitions(cert) |> head(3) boot <- bootstrap_lsa(fit, R = 50) transitions(boot) |> head(3) rel <- reliability_lsa(fit, R = 20) rel stab <- stability_lsa(fit, R = 30) stab pm <- permute_lsa(fit, R = 100) pm ``` ```{r forest, eval = has_ggplot2} plot(boot) ``` Bootstrap replication is reproducible across languages and sessions: `bootstrap_lsa(indices = )` accepts a precomputed resampling matrix for bit-identical replay. The full treatment of the battery is in `vignette("confirmatory", package = "lagdynamics")`. # Interoperability `lagdynamics` is designed to sit inside an existing analysis stack rather than replace it, on both the input and the output side. **Input.** `lsa()` accepts, through one front door: - wide data frames and matrices (rows = sequences, columns = ordered positions), as in the bundled `engagement` and `group_regulation`; - lists of categorical vectors, one element per sequence; - long event logs via the `actor` / `action` / `time` grammar, including Unix and custom time formats, as in `group_regulation_long` and `ai_long`; - foreign sequence objects, duck-typed so that none of their packages becomes a runtime dependency: `TraMineR` state sequences (`stslist`), `tna` models and their grouped and sequence-data forms (`tna`, `group_tna`, `tna_seq_data`, `tna_data`), and `Nestimate` prepared data (`nestimate_data`). ```{r ingest} fit_log <- lsa(group_regulation_long, actor = "Actor", action = "Action", time = "Time") fit_log ``` **Output.** The fitted object carries its network identity with it: it inherits the `cograph_network` class, so `cograph`'s rendering verbs accept it directly, and the plotting gallery above draws its network, chord, and comparison views through that seam. The fitted probabilities are exposed for any downstream transition-network tooling -- `transition_probabilities()` returns the row-stochastic matrix $P(\text{to} \mid \text{from})$ and `initial()` the initial-state distribution, the two ingredients a TNA-style model requires -- and every result object reads as a plain data frame through `transitions()`, so results move into any tabular pipeline without package-specific code. ```{r output-interop} transition_probabilities(fit) initial(fit) ``` # Bundled data Four data sets ship with the package, spanning the supported input shapes: `engagement` (wide; 138 students, weekly engagement states), `group_regulation` (wide; 2,000 collaborative-learning sessions), `group_regulation_long` (long event log with actor, action, time, and an achievement group), and `ai_long` (long event log of coded AI actions in human--AI coding sessions). ```{r cleanup, include = FALSE} options(old_options) ```