Skip to content

Commit

Permalink
Fix impliedS
Browse files Browse the repository at this point in the history
  • Loading branch information
mikewlcheung committed Mar 30, 2024
1 parent 7d324ba commit 9831c63
Show file tree
Hide file tree
Showing 13 changed files with 521 additions and 132 deletions.
6 changes: 3 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Package: symSEM
Type: Package
Title: Symbolic Computation for Structural Equation Models
Version: 0.2
Date: 2023-05-31
Version: 0.4
Date: 2024-02-10
Depends: caracas
Imports: OpenMx, metaSEM
Suggests: testthat, roxygen2
Expand All @@ -21,4 +21,4 @@ LazyLoad: yes
LazyData: yes
ByteCompile: yes
URL: https://github.com/mikewlcheung/symsem
RoxygenNote: 7.2.3
RoxygenNote: 7.3.1
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Generated by roxygen2: do not edit by hand

S3method(print,impliedS)
export(JacobianRAM)
export(deltamethod)
export(impliedS)
import(caracas)
Expand Down
12 changes: 11 additions & 1 deletion NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
Release 0.4 (Feb 26, 2024)
====================================
* Add replace.constraints argument in impliedS().

Release 0.3 (Dec 27, 2023)
====================================
* Convert some reserved words, e.g., "lambda" and "I", before applying the functions.
* Fix two bugs in impliedS.
* Add JacobianRAM to calculate Jacobian matrix based on an RAM model.

Release 0.2 (May 31, 2023)
====================================
* Release to CRAN.
Expand All @@ -9,4 +19,4 @@ Release 0.1.1 (Sep 26, 2020)

Release 0.1 (Jul 16, 2020)
====================================
* Release to CRAN.
* Release to CRAN.
106 changes: 106 additions & 0 deletions R/JacobianRAM.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#' Compute a Jacobian Matrix of the Implied Covariance/Correlation Matrix based on a RAM model.
#'
#' It computes a symbolic Jacobian matrix of the model-implied covariance (or correlation) matrix in
#' SEM using the RAM specification.
#'
#' @param RAM A RAM object including a list of matrices of the model returned
#' from \code{\link[metaSEM]{lavaan2RAM}}
#' @param vars A vector of characters of the random variables. If the random
#' variables are not listed in `vars`, they are treated as constants. If `vars`
#' is missing, all names in `RAM` are treated as random variables.
#' @param corr Whether the model implied matrix is covariance (default) or
#' correlation structure.
#' @return A Jacobian matrix.
#' @author Mike W.-L. Cheung <mikewlcheung@@nus.edu.sg>
#' @export
#' @examples
#'
#' #### A mediation model
#' model1 <- "y ~ c*x + b*m
#' m ~ a*x
#' ## Means
#' y ~ b0*1
#' m ~ m0*1
#' x ~ x0*1"
#'
#' RAM1 <- metaSEM::lavaan2RAM(model1)
#'
#' ## Model-implied covariance matrix and mean structure
#' JacobianRAM(RAM1, corr=FALSE)
#'
#' ## Model-implied correlation matrix
#' JacobianRAM(RAM1, corr=TRUE)
#'
#' #### A CFA model
#' model2 <- "f =~ x1 + x2 + x3 + x4#'
#' ## Mean
#' f ~ fmean*1"
#'
#' RAM2 <- metaSEM::lavaan2RAM(model2)
#'
#' ## Model-implied covariance matrix
#' JacobianRAM(RAM2, corr=FALSE)
#'
#' ## Model-implied correlation matrix
#' JacobianRAM(RAM2, corr=TRUE)
#'
JacobianRAM <- function(RAM, vars, corr=FALSE) {
RAM.old <- RAM

## Convert reserved words to random strings
RAM$A <- .convert(RAM$A, type="sympy2random")
RAM$S <- .convert(RAM$S, type="sympy2random")
RAM$M <- .convert(RAM$M, type="sympy2random")
dim(RAM$M) <- dim(RAM.old$M)
dimnames(RAM$M) <- dimnames(RAM.old$M)

## Keep the random strings as sympy reserved words are not allowed in
## Jacobian in Sympy
x <- impliedS(RAM, corr=corr, convert=FALSE)

vecM <- x$Mu
Sigma <- x$Sigma
labels.mu <- paste0("Mu_", colnames(vecM))

p <- ncol(Sigma)
labels.sigma <- outer(seq_len(p), seq_len(p), function(x, y) paste0("Cov", x, "_", y))

## Vector of correlation matrix only
if (corr) {
vecS <- matrix(OpenMx::vechs(Sigma), ncol=1)
labels <- OpenMx::vechs(labels.sigma)
} else {
## Vector of covariance matrix and then vector of means
vecS <- matrix(c(OpenMx::vech(Sigma), vecM), ncol=1)
labels <- c(OpenMx::vech(labels.sigma), labels.mu)
}

varlist <- sort(all.vars(parse(text=vecS)))
vecS <- caracas::as_sym(vecS)

## Ensure the column order of the Jmatrix is sorted according to the parameters
if (missing(vars)) {
vars <- sort(varlist)
} else {
vars <- .convert(sort(vars), type="sympy2random")
if (any(!vars %in% varlist)) {
stop("Some of \"vars\" do not agree with those names in \"x\".\n")
}
}

## Jmatrix <- caracas::jacobian(vecS, vars)
## It cannot handle many vars, say 15
## Error in py_call_impl(callable, call_args$unnamed, call_args$named) :
## ValueError: too many values to unpack (expected 2)

## Adhoc solution to split vars to batches
vars.batch <- split(x=vars,
f=rep(1:length(vars), each=10, length.out = length(vars)) )
J.batch <- lapply(vars.batch, function(x) caracas::jacobian(vecS, x))
Jmatrix <- do.call(cbind, J.batch)
Jmatrix <- caracas::as_character_matrix(Jmatrix)
Jmatrix <- .convert(Jmatrix, type="random2sympy")
dimnames(Jmatrix) <- list(labels, .convert(vars, type="random2sympy"))
Jmatrix
}

96 changes: 48 additions & 48 deletions R/deltamethod.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
#'
#'
#' @param fn A function in character strings or a vector of functions.
#' @param Covvars Variance-covariance matrix of the variables. If it is not
#' specified, they are automatically generated.
#' @param Covvars Variance-covariance matrix of the variables. Users must
#' ensure the order of variables is the same as that in \code{vars};
#' Otherwise, the results are likely incorrect. If it is not specified,
#' they are automatically generated.
#' @param vars A vector of characters of the random variables. If the random
#' variables are not listed in `vars`, they are treated as constants. If `vars`
#' is missing, all names in `RAM` are treated as random variables.
Expand All @@ -21,7 +23,6 @@
#' @author Mike W.-L. Cheung <mikewlcheung@@nus.edu.sg>
#' @export
#' @examples
#' \dontrun{
#'
#' #### Fisher-z-transformation
#' fn <- "0.5*log((1+r)/(1-r))"
Expand Down Expand Up @@ -107,59 +108,58 @@
#' ## $Jmatrix
#' ## p
#' ## fn1 "((1-p+p)*(1-p))/((1-p)^2*p)"
#' }
deltamethod <- function(fn, Covvars, vars, Var.name="V", Cov.name="C",
simplify=TRUE) {

## Univariate or multivariate
fn.p <- length(fn)
## Univariate or multivariate
fn.p <- length(fn)

## function names
fn.names <- paste0("fn", seq_len(fn.p))

## convert it to a column vector
fn <- matrix(fn, ncol=1)
rownames(fn) <- fn.names

## function names
fn.names <- paste0("fn", seq_len(fn.p))

## convert it to a column vector
fn <- matrix(fn, ncol=1)
rownames(fn) <- fn.names

fn.S <- as_sym(fn)

## Get the variable names
varlist <- sort(unique(all.names(parse(text=fn), functions=FALSE)))
if (missing(vars)) {
vars <- varlist
} else {
if (any(!vars %in% varlist)) {
stop("Some of \"vars\" do not agree with the names in \"fn\".\n")
}
fn.S <- as_sym(fn)

## Get the variable names
varlist <- sort(unique(all.names(parse(text=fn), functions=FALSE)))
if (missing(vars)) {
vars <- varlist
} else {
if (any(!vars %in% varlist)) {
stop("Some of \"vars\" do not agree with the names in \"fn\".\n")
}
}

##
if (missing(Covvars)) {
## Variance covariance matrix of x
Covvars <- outer(vars, vars, function(y, z) paste0(Cov.name, y, z))
metaSEM::Diag(Covvars) <- paste0(Var.name, vars)
## Make it symmetric
Covvars <- metaSEM::vec2symMat(OpenMx::vech(Covvars))
Covvars.S <- caracas::as_sym(Covvars)
} else {
Covvars.S <- caracas::as_sym(Covvars)
}

##
if (missing(Covvars)) {
## Variance covariance matrix of x
Covvars <- outer(vars, vars, function(y, z) paste0(Cov.name, y, z))
metaSEM::Diag(Covvars) <- paste0(Var.name, vars)
## Make it symmetric
Covvars <- metaSEM::vec2symMat(OpenMx::vech(Covvars))
Covvars.S <- caracas::as_sym(Covvars)
} else {
Covvars.S <- caracas::as_sym(Covvars)
}

## Jacobian matrix
Jmatrix <- caracas::jacobian(fn.S, vars)

Covfn <- Jmatrix %*% Covvars.S %*% t(Jmatrix)

if (simplify) {Covfn <- caracas::simplify(Covfn)}
## Jacobian matrix
Jmatrix <- caracas::jacobian(fn.S, vars)

Covfn <- caracas::as_character_matrix(Covfn)
dimnames(Covfn) <- list(fn.names, fn.names)
Covvars <- as.matrix(Covvars)
dimnames(Covvars) <- list(vars, vars)
Covfn <- Jmatrix %*% Covvars.S %*% t(Jmatrix)

if (simplify) {Covfn <- caracas::simplify(Covfn)}

Covfn <- caracas::as_character_matrix(Covfn)
dimnames(Covfn) <- list(fn.names, fn.names)
Covvars <- as.matrix(Covvars)
dimnames(Covvars) <- list(vars, vars)

Jmatrix <- caracas::as_character_matrix(Jmatrix)
dimnames(Jmatrix) <- list(fn.names, vars)
Jmatrix <- caracas::as_character_matrix(Jmatrix)
dimnames(Jmatrix) <- list(fn.names, vars)

list(fn=fn, Covfn=Covfn, vars=vars, Covvars=Covvars, Jmatrix=Jmatrix)
list(fn=fn, Covfn=Covfn, vars=vars, Covvars=Covvars, Jmatrix=Jmatrix)
}

35 changes: 35 additions & 0 deletions R/helpers.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.convert <- function(x, type=c("sympy2random", "random2sympy")) {
x.old <- x

## Conversion the reserved words in sympy with some random strings
words <- matrix( c(
## random sympy
"a4dbepz", "I",
"ajljael", "lambda",
"akr0dsf", "E",
"data_", "data.",
"jeljsxe", "oo"
), byrow = TRUE, ncol = 2)
colnames(words) <- c("random", "sympy")

type <- match.arg(type)
switch (type,
sympy2random = for (i in 1:nrow(words)) {
x <- gsub(words[i, 2], words[i, 1], x, fixed=TRUE)},
random2sympy = for (i in 1:nrow(words)) {
x <- gsub(words[i, 1], words[i, 2], x, fixed=TRUE)}
)

## Try to convert these matrices from string to numeric, if possible
if (suppressWarnings(all(!is.na(as.numeric(x))))) {
x <- as.numeric(x)
}

if (is.matrix(x.old)) {
x <- matrix(x, nrow=nrow(x.old), ncol=ncol(x.old),
dimnames=dimnames(x.old))
} else if (is.matrix(x)) {
names(x) <- names(x.old)
}
x
}
Loading

0 comments on commit 9831c63

Please sign in to comment.