Learn R Programming

psychonetrics (version 0.17.8)

ri_clpm: Random intercept cross-lagged panel models and networks

Description

The ri_clpm function sets up a random intercept cross-lagged panel model (RI-CLPM) as a special case of the latent variable model (lvm) framework. The ri_clpm_stationary function adds stationarity (equality over time) constraints to such a model, and ri_clpm_search runs a guarded decision-tree search over these constraints. The functions ri_clpn, ri_clpn_stationary and ri_clpn_search are the network-parameterization counterparts (see Details).

Usage

ri_clpm(data, vars,
        datatype = c("auto", "wide", "long"),
        idvar, beepvar,
        standardize = c("none", "z", "quantile", "z_per_wave"),
        lambda,
        type = c("cor", "cov", "chol", "prec", "ggm"),
        verbose = FALSE, ...)

ri_clpn(data, vars, type = c("ggm", "cor", "cov", "chol", "prec"), ...)

ri_clpm_stationary(x, stationary = c("intercepts", "contemporaneous", "innovation", "temporal"))

ri_clpn_stationary(...)

ri_clpm_search(x, criterion = c("Chisq", "AIC", "BIC"), alpha = 0.05, include_panelvar = TRUE, verbose = TRUE, on_rejection = c("release", "stop"), ...)

ri_clpn_search(...)

Value

ri_clpm, ri_clpn and ri_clpm_stationary return a psychonetrics model object. ri_clpm_search returns a list of class "ri_clpm_search" with elements selected (the chosen psychonetrics model), selected_name (a compact description of the selected model, for example "temporal+contemporaneous+innovation (means free)" or "panelVAR (means free)"), models (all fitted models), comparison (a compare table; note that its rows are sorted by degrees of freedom and that consecutive rows need not be nested when constraints were released), path (a data frame describing each fitted model and decision), verdicts, criterion, alpha and on_rejection.

The verdicts element is a data frame with one row per step and the columns step ("temporal", "contemporaneous", "intercepts", "innovation", "wave1_endogenous"), tested (logical), decision ("retained", "released", "rejected (gate)", "not tested" or "not applicable"), Chisq_diff, DF_diff, p_value, dAIC, dBIC (each relative to the working model that the step was tested against) and note (a short explanation, e.g. why a step was not applicable or why a fit was discarded).

Arguments

data

A data frame encoding the data used in the analysis. Can be missing if covs and nobs are supplied (via ...).

vars

Required argument. For wide-format data (the default), a matrix with each row indicating a variable and each column indicating a measurement (wave); each cell must contain the name of the variable in data corresponding to that variable at that wave. The row names of this matrix are used as variable names. For long-format data, a character vector of variable names (see datatype, idvar and beepvar).

datatype

The data format. "auto" (default) detects "wide" when vars is a matrix and "long" when vars is a character vector.

idvar

Only used for long-format data: the name of the variable encoding the subject/cluster ID.

beepvar

Optional, only used for long-format data: the name of the variable encoding the measurement occasion (wave). If omitted, observations are assumed to be ordered within each subject.

standardize

Standardization to apply to the data before estimation. "none" (default) applies none, "z" standardizes each variable to mean 0 and SD 1 pooling across all waves, "z_per_wave" standardizes each variable separately at each wave, and "quantile" applies a (pooled) nonparametric quantile transformation per variable.

lambda

A model matrix encoding the factor loading structure. Latent variables are not yet supported for the RI-CLPM; supplying lambda currently raises an error. When omitted, an observed-variable model is created.

type

The parameterization used for the latent (innovation and between-person random intercept) covariance structure: "cor" (correlations with separate standard deviations; the default of ri_clpm), "cov" (covariances), "chol" (Cholesky), "prec" (precision), or "ggm" (Gaussian graphical model / partial correlations; the default of ri_clpn). Because the latent covariance matrix is block-diagonal over time, this parameterization is applied to both the within-person (innovation) blocks and the between-person (random intercept) block. The default "cor" parameterization separates the innovation correlations from the innovation standard deviations, so that ri_clpm_search can test equality of the contemporaneous relations and equality of the innovation variances as two distinct, nested steps.

verbose

Logical, should progress be printed to the console?

x

A psychonetrics RI-CLPM model (as returned by ri_clpm).

stationary

The part of the model to constrain to be stationary (equal over time): "intercepts" (also frees the random-intercept means), "temporal" (the cross-lagged/autoregressive effects), "contemporaneous" (the innovation covariances/network, waves 2 and later), or "innovation" (the innovation variances, waves 2 and later).

criterion

The criterion used by ri_clpm_search to decide whether to retain each constraint: "Chisq" (the default) retains it when the likelihood-ratio test is not significant at level alpha, while "AIC" and "BIC" retain it when that criterion does not increase. The default was "BIC" up to and including psychonetrics 0.17.4; pass criterion = "BIC" explicitly to reproduce analyses run with those versions.

alpha

Significance level used when criterion = "Chisq".

include_panelvar

Logical. If TRUE (default) and the covariance-stationarity constraints are retained, ri_clpm_search additionally tests the panel(G)VAR model, which treats the first wave as the stationary distribution of the process (wave 1 endogenous). See the "wave1_endogenous" step in the Details section.

on_rejection

What ri_clpm_search does when a constraint is rejected. "release" (default) runs the guarded decision tree: only a rejected temporal constraint stops the search, and a rejection of any other constraint releases that constraint (it is not imposed) but the search continues with the remaining steps. "stop" reproduces the pre-0.17.2 behavior of stopping at the first rejection, and is provided for reproducibility of analyses run with earlier versions.

...

Arguments sent to lvm (for ri_clpm), to ri_clpm (for ri_clpn), to ri_clpm_stationary (for ri_clpn_stationary), or to runmodel (for ri_clpm_search) / ri_clpm_search (for ri_clpn_search).

Author

Sacha Epskamp

Details

The RI-CLPM decomposes each observed score into a stable between-person random intercept and a within-person deviation (innovation). The within-person deviations follow a lag-1 (cross-lagged) structure over time. In this implementation the innovations and the random intercepts are modeled as latent variables under the lvm framework, with the latent covariance matrix being block-diagonal over time, so that the within-person (contemporaneous) and between-person covariance structures can each be modeled as a covariance matrix, a Cholesky decomposition, a precision matrix, or a Gaussian graphical model (GGM).

The RI-CLPN. When the innovation and between-person structures are parameterized as Gaussian graphical models (type = "ggm"), the RI-CLPM becomes the random-intercept cross-lagged panel network model (RI-CLPN): the contemporaneous and the between-person relations are then partial-correlation networks rather than covariance matrices, while the temporal (cross-lagged) structure is the directed lag-1 network in either case. The function ri_clpn is a thin wrapper around ri_clpm that uses type = "ggm" as its default (the type argument can still be overridden), mirroring how panelgvar wraps panelvar. Because ri_clpm_stationary and ri_clpm_search operate on an already specified model object, no parameterization is involved there, and ri_clpn_stationary and ri_clpn_search are pure aliases of those functions, provided so that a network-parameterized analysis can be written entirely in RI-CLPN notation.

The stationarity search. ri_clpm_search formalizes the typical analysis workflow as a guarded decision tree over the four stationarity constraints, closed off by the panel(G)VAR model. The steps are visited in the order "temporal", "contemporaneous", "intercepts", "innovation", "wave1_endogenous", and each step is tested against the current working model (the base model plus all constraints retained so far). A constraint is retained if it does not worsen fit according to criterion: with criterion = "Chisq" when the likelihood-ratio test has \(p \ge\) alpha, and with "AIC" or "BIC" when that criterion does not increase. A candidate model that did not converge, or that produced a negative or non-finite chi-square or a non-finite criterion, is never retained; such a fit counts as a rejection of its constraint and the reason is reported in the verdict table.

The tree treats the steps differently according to the targets of inference:

  • "temporal" is the only hard gate. The temporal (cross-lagged) effects are the primary target of inference in a cross-lagged panel analysis. If they are not stationary there is no single lag-1 temporal network to report, and the target of inference changes to a set of per-transition temporal networks (one for each wave-to-wave step). The search therefore stops entirely, the unconstrained base model is selected, and the remaining steps are recorded as "not tested", since each of them would only be interpretable conditional on a temporal structure that does not hold. When this happens, check whether the waves are equally spaced in time: unequal measurement intervals produce wave-varying lag-1 effects even when the underlying process is perfectly stationary, and so mimic temporal non-stationarity.

  • "contemporaneous", "intercepts" and "innovation" are nuisance- or secondary-structure questions, so a rejection is released rather than fatal: the constraint is not imposed (that set of parameters stays wave-specific) but the search continues to the next step. Each of these constraints is therefore tested exactly once, conditional only on the constraints that survived their own test. This reaches endpoints that a strict chain cannot reach, for example wave-varying contemporaneous relations combined with stationary intercepts and stationary innovation variances.

  • "wave1_endogenous" tests the panel(G)VAR model, which additionally treats the first wave as a draw from the stationary distribution of the within-person process. It is gated on covariance stationarity and therefore requires the temporal, contemporaneous and innovation constraints to all have been retained: a within-person process whose innovation covariance structure varies over waves has no stationary distribution, so there is nothing for an endogenous first wave to be drawn from and the step is undefined. In that case the step is reported as "not applicable" and the search finishes. When the step does apply, the working model is compared to panelvar(vars = ..., sampleStats = ..., within_latent = type, between_latent = type, baseline = "none"); if the intercept constraint was released, that panel model is fit with stationary_means = FALSE, so that both models carry a saturated (wave-specific) mean structure and the comparison isolates the wave-1 block. The panel model is the constrained special case here: it has \(m(m+1)/2\) fewer free parameters, and hence \(m(m+1)/2\) more degrees of freedom, than the working RI-CLPM, namely the freed exogenous wave-1 within-person covariance block (\(m\) = number of variables). The likelihood-ratio test is thus the chi-square of the panel model minus the chi-square of the working model on \(m(m+1)/2\) degrees of freedom. This degrees-of-freedom gap is verified at run time and a warning is issued when it differs, which indicates that the constraint bookkeeping of the working model was disturbed upstream (for example by prune or fixpar).

Setting on_rejection = "stop" reproduces the pre-0.17.2 behavior exactly: the same tree is walked but the search breaks at the first rejection, so no constraint is ever released and the panel(G)VAR step is only reached when all four constraints are retained.

With the default type = "cor" parameterization the contemporaneous (correlation) step and the innovation-variance (standard deviation) step are cleanly separated and nested; the innovation variances are never equated with the exogenous wave-1 variances nor with the between-person random-intercept variances.

Extracting and plotting the networks. Because the model is set up in the lvm framework, its matrices span all waves at once and the temporal, contemporaneous and between-person networks are blocks of those matrices. Those blocks can be addressed directly, using the same matrix names as panelvar / panelgvar together with a time argument:

getmatrix(x, "beta", time = 2)               # temporal network, wave 2 -> 3
getmatrix(x, "PDC",  time = 2)               # ... as partial directed correlations
getmatrix(x, "omega_zeta_within", time = 3)  # contemporaneous network at wave 3
getmatrix(x, "omega_zeta_between")           # between-person network
getmatrix(x, "nu_between")                   # random-intercept means
CIplot(x, "omega_zeta_within", time = 3)     # confidence intervals of that block

Blocks carry the variable names of the design matrix, so they can be passed straight to qgraph. Omitting time returns one block per wave, with waves that were equated by ri_clpm_stationary collapsed into a single entry. See getmatrix and CIplot for the details.

Structural missing waves (NA entries in the vars design matrix, i.e. a variable not measured at a particular wave) are not yet supported. Incomplete cases (missing data for some subjects) are handled automatically through full-information maximum likelihood.

See Also

lvm, dlvm1, panelvar, panelgvar, getmatrix, CIplot

Examples

Run this code
# Simulate a small panel data set (3 variables, 4 waves):
set.seed(1)
nPerson <- 300; nVar <- 3; nWave <- 4

# Between-person random intercepts:
RI <- matrix(rnorm(nPerson * nVar), nPerson, nVar)

# Lag-1 temporal structure:
beta <- diag(0.3, nVar); beta[2, 1] <- 0.2; beta[3, 2] <- 0.2

# Within-person process:
within <- array(0, c(nPerson, nVar, nWave))
within[, , 1] <- matrix(rnorm(nPerson * nVar), nPerson, nVar)
for (w in 2:nWave){
  within[, , w] <- within[, , w - 1] %*% t(beta) +
                   matrix(rnorm(nPerson * nVar, sd = 0.8), nPerson, nVar)
}

# Observed scores = within-person deviation + random intercept:
data <- do.call(cbind, lapply(1:nWave, function(w) within[, , w] + RI))
colnames(data) <- as.vector(outer(paste0("V", 1:nVar), 1:nWave, paste, sep = "_"))
data <- as.data.frame(data)

# Design matrix (rows = variables, columns = waves):
design <- matrix(colnames(data), nVar, nWave)
rownames(design) <- paste0("V", 1:nVar)

# Form the unconstrained RI-CLPN (= RI-CLPM with GGM innovation/between
# structures). Equivalent to ri_clpm(data, design, type = "ggm"):
mod <- ri_clpn(data, design)

# \donttest{
# Run the unconstrained model:
mod <- runmodel(mod)

# Add stationarity constraints step by step:
mod_temp <- runmodel(ri_clpm_stationary(mod,      "temporal"))
mod_con  <- runmodel(ri_clpm_stationary(mod_temp, "contemporaneous"))
compare(base = mod, temporal = mod_temp, contemporaneous = mod_con)

# Or run the whole guarded decision-tree search automatically
# (chi-square difference tests at alpha = 0.05 by default):
res <- ri_clpn_search(mod)
res
res$verdicts
res$selected

# Information criteria can be used instead:
ri_clpn_search(mod, criterion = "BIC")

# Extract the networks as nVar x nVar blocks (rather than the full
# latent-sized matrices of the underlying lvm model):
getmatrix(mod, "omega_zeta_between")           # between-person network
getmatrix(mod, "beta", time = 1)               # temporal network, T1 -> T2
getmatrix(mod, "PDC", time = 1)                # ... as partial directed correlations
getmatrix(mod, "omega_zeta_within", time = 2)  # contemporaneous network at wave 2

# The pre-0.17.2 behavior: BIC, stopping at the first rejection:
ri_clpn_search(mod, criterion = "BIC", on_rejection = "stop")
# }

Run the code above in your browser using DataLab