knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
library(mirt) # for IRT library(OpenMx) library(umx) library(R2spa) set.seed(591028)
#' Population parameters num_obs <- 5000 # relatively large sample cov_xm <- .45 betas <- c(.2, .3, -.4) # Results seem more accurate with more items lambdax <- 1.7 * c(.6, .7, .8, .5, .5, .3, .9, .7, .6) # on normal ogive metric lambdam <- 1.7 * c(.8, .7, .7, .6, .5) # on normal ogive metric lambday <- c(.5, .7, .9) thresx <- matrix(c(1, 1.5, 1.3, 0.5, 2, -0.5, -1, 0.5, 0.8), nrow = 1) # binary thresm <- matrix(c(-2, -2, -0.4, -0.5, 0, -1.5, -0.5, 0, 0, 0.5, -1, 1, 1.5, 0.8, 1), nrow = 3, byrow = TRUE) # ordinal with 4 categories inty <- 1.5 # intercept of y # error variance of y r2y <- crossprod( betas, matrix(c(1, cov_xm, 0, cov_xm, 1, 0, 0, 0, 1 + cov_xm^2), nrow = 3) ) %*% betas evar <- as.numeric(1 - r2y) #' Simulate latent variables X and M with variances of 1, and the disturbance of #' Y cov_xm_ey <- matrix(c(1, cov_xm, 0, cov_xm, 1, 0, 0, 0, evar), nrow = 3) eta <- MASS::mvrnorm(num_obs, mu = rep(0, 3), Sigma = cov_xm_ey, empirical = TRUE) # Add product term eta <- cbind(eta, eta[, 1] * eta[, 2]) #' Compute latent y (standardized) etay <- inty + eta[, -3] %*% betas + eta[, 3] # Verify the structural coefficients with eta known lm(etay ~ eta[, -3]) #' Simulate y (continuous) y <- t( lambday %*% t(etay) + rnorm(num_obs * length(lambday), sd = sqrt(1 - lambday^2)) ) #' Simulate latent continuous responses for X and M xstar <- eta[, 1] %*% t(lambdax) + rnorm(num_obs * length(lambdax)) mstar <- eta[, 2] %*% t(lambdam) + rnorm(num_obs * length(lambdam)) #' Obtain categorical items x <- vapply( seq_along(lambdax), FUN = \(i) { findInterval(xstar[, i], thresx[, i]) }, FUN.VALUE = numeric(num_obs)) m <- vapply( seq_along(lambdam), FUN = \(i) { findInterval(mstar[, i], thresm[, i]) }, FUN.VALUE = numeric(num_obs))
fsy <- get_fs(data = y, std.lv = TRUE)
In this example, we use EAP scores, which are conceptually similar to the regression scores for continuous data.
irtx <- mirt(data.frame(x), itemtype = "2PL", verbose = FALSE) # IRT (2-PL) for x marginal_rxx(irtx) # marginal reliability fsx <- fscores(irtx, full.scores.SE = TRUE) empirical_rxx(fsx) # empirical reliability
irtm <- mirt(data.frame(m), itemtype = "graded", verbose = FALSE) # IRT (GRM) for m marginal_rxx(irtm) # marginal reliability fsm <- fscores(irtm, full.scores.SE = TRUE) empirical_rxx(fsm) # empirical reliability
Let $\tilde \eta_X$ and $\tilde \eta_M$ be factor scores of the predictor and the moderator. Then
$$\tilde \eta_X \tilde \eta_M = (\lambda_X \eta_X + e_{\tilde X}) (\lambda_M \eta_M + e_{\tilde M}) = (\lambda_X \lambda_M \eta_X \eta_M) + \lambda_X \eta_X e_M + \lambda_M \eta_M e_X + e_X e_M$$
So the loading is $\lambda_X \lambda_M$, and the error variance for the product indicator is
$$\lambda_X^2 V(\eta_X) V(e_M) + \lambda_M^2 V(\eta_M) V(e_X) + V(e_X) V(e_M)$$
# Assemble data fs_dat <- data.frame(fsy[c(1, 3, 4)], fsx, fsm) |> setNames(c( "fsy", "rel_fsy", "ev_fsy", "fsx", "se_fsx", "fsm", "se_fsm" )) |> # Compute reliability; only needed for 2S-PA within(expr = { rel_fsx <- 1 - se_fsx^2 rel_fsm <- 1 - se_fsm^2 ev_fsx <- se_fsx^2 * (1 - se_fsx^2) ev_fsm <- se_fsm^2 * (1 - se_fsm^2) }) |> # Add interaction within(expr = { fsxm <- fsx * fsm ld_fsxm <- rel_fsx * rel_fsm ev_fsxm <- rel_fsx^2 * ev_fsm + rel_fsm^2 * ev_fsx + ev_fsm * ev_fsx })
lm(fsy ~ fsx * fsm, data = fs_dat) # some bias
fsreg_mx <- mxModel("str", type = "RAM", manifestVars = c("fsy", "fsx", "fsm", "fsxm"), # Path coefficients mxPath( from = c("fsx", "fsm", "fsxm"), to = "fsy", values = 0 ), # Variances mxPath( from = c("fsy", "fsx", "fsm", "fsxm"), to = c("fsy", "fsx", "fsm", "fsxm"), arrows = 2, values = c(0.6, 1, 1, 1) ), # Covariances mxPath( from = c("fsx", "fsm", "fsxm"), to = c("fsx", "fsm", "fsxm"), arrows = 2, connect = "unique.pairs", values = 0 ), mxPath( from = "one", to = c("fsy", "fsx", "fsm", "fsxm"), values = 0 ) ) fsreg_umx <- umxLav2RAM( " fsy ~ b1 * fsx + b2 * fsm + b3 * fsxm fsy + fsx + fsm + fsxm ~ 1 fsx ~~ vx * fsx + fsm + fsxm fsm ~~ vm * fsm + fsxm ", printTab = FALSE)
DiagrammeR::grViz(omxGraphviz(fsreg_mx))
# Loading matL <- mxMatrix( type = "Diag", nrow = 4, ncol = 4, free = FALSE, labels = c("data.rel_fsy", "data.rel_fsx", "data.rel_fsm", "data.ld_fsxm"), name = "L" ) # Error matE <- mxMatrix( type = "Diag", nrow = 4, ncol = 4, free = FALSE, labels = c("data.ev_fsy", "data.ev_fsx", "data.ev_fsm", "data.ev_fsxm"), name = "E" )
tspa_mx <- tspa_mx_model( mxModel(fsreg_umx, mxAlgebra(b1 * sqrt(vx), name = "stdx_b1"), mxAlgebra(b2 * sqrt(vm), name = "stdx_b2"), mxAlgebra(b3 * sqrt(vx) * sqrt(vm), name = "stdx_b3"), mxCI(c("stdx_b1", "stdx_b2", "stdx_b3"))), data = fs_dat, mat_ld = matL, mat_ev = matE) # Run OpenMx tspa_mx_fit <- mxRun(tspa_mx, intervals = TRUE) # Summarize the results (takes a minute or so) # Also include the X-Standardized coefficients summary(tspa_mx_fit) # Standard errors for X-Standardized coefficients mxSE(m1.stdx_b1, tspa_mx_fit) mxSE(m1.stdx_b2, tspa_mx_fit) mxSE(m1.stdx_b3, tspa_mx_fit)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.