Setup

The Goal

I wanted a function for dMACS_Signed to allow for the quick computation of bias directions with respect to indicator variables in multi-group structural equation models. The formula for dMACS is

\[d_{MACS} = \frac{1}{SD_{jPooled}}\sqrt{\int (\hat{Y}_{j1}-\hat{Y}_{j2}|\eta)^2\cdot f_2(\eta)d\eta}\]

whereas dMACS_Signed is

\[d_{MACS\_Signed} = \frac{1}{SD_{jPooled}}\int (\hat{Y}_{j1}-\hat{Y}_{j2}|\eta)\cdot f_2(\eta)d\eta\]

Though I’m not doing it right now, it is possible to make a “gMACS” or other such effect sizes.

The Function

Below is a function for computing dMACS_Signed from a multigroup lavaan object. This is a modification of Johannes A. Karl’s dMACS, described here: https://rdrr.io/github/Jo-Karl/ccpsyc/man/dMACS.html. This maintains the same sign as SDI2 and the magnitude of results is extremely similar. Some of the difference in my example below is due to rounding error.

dMACS_Signed <- function(fit.cfa, group1, group2){
  
  nitems <- lavaan::lavInspect(fit.cfa, what = "rsquare") %>%
    .[[1]] %>%
    names(.) %>%
    length(.)

  cfa_minmax <- function(fit.cfa) {
    dt <- lavaan::inspect(fit.cfa, what = "data")
    latentMin <- min(dt[[1]]) - 1
    latentMax <- max(dt[[1]]) + 1
    out <- cbind(as.numeric(latentMin), as.numeric(latentMax))
    return(out)
  }

  reference_load <- lavaan::inspect(fit.cfa, what = "est") %>%
    .[[1]] %>%
    .$lambda
  focal_load <- lavaan::inspect(fit.cfa, what = "est") %>%
    .[[2]] %>%
    .$lambda

  reference_intrcp <- lavaan::inspect(fit.cfa, what = "est") %>%
    .[[1]] %>%
    .$nu
  focal_intrcp <- lavaan::inspect(fit.cfa, what = "est") %>%
    .[[2]] %>%
    .$nu

  pool.sd <- function(fit.cfa) {
    cfa.se <- lavaan::lavInspect(fit.cfa, what = "se")
    cfa.n <- lavaan::lavInspect(fit.cfa, what = "nobs")
    l <- list()
    test <- lavaan::lavInspect(fit.cfa, what = "rsquare") %>%
      .[[1]] %>%
      names(.) %>%
      length(.)
    for (i in 1:test) {
      grp1 <- cfa.se[[group1]]$nu[i] * sqrt(cfa.n[1])
      grp2 <- cfa.se[[group2]]$nu[i] * sqrt(cfa.n[2])
      numerator <- ((cfa.n[1] - 1) * grp1 + (cfa.n[2] - 1) * grp2)
      denominator <- (cfa.n[1] - 1) + (cfa.n[2] - 1)
      pooled.sd <- numerator / denominator
      l[[paste("item", i)]] <- pooled.sd
    }
    result <- matrix(unlist(l), nrow = test, byrow = TRUE)
    return(result)
  }

  pld_sd <- pool.sd(fit.cfa)

  fcl_lt_vrnc <- lavaan::inspect(fit.cfa, what = "est") %>%
    .[[2]] %>%
    .$psi

  l <- list()
  rowlab <- c()
  for (i in c(1:nitems)) {
    focal.fn <- function(x) {
      mpr <- focal_intrcp[i] + focal_load[i] * x
      return(mpr)
    }
    reference.fn <- function(x) {
      mpr <- reference_intrcp[i] + reference_load[i] * x
      return(mpr)
    }
    diff.fn <- function(x, i = i) {
      d <- ((reference.fn(x) - focal.fn(x))) * dnorm(x, mean = 0, sd = sqrt(fcl_lt_vrnc))
      return(d)
    }
    dMACS <- round((1 / pld_sd[i]) * integrate(diff.fn,
      lower = cfa_minmax(fit.cfa)[, 1],
      upper = cfa_minmax(fit.cfa)[, 2])$value, 3)

    l[[length(l) + 1]] <- dMACS
    rowlab[[length(rowlab) + 1]] <- paste("Item", i)
  }
  m <- matrix(unlist(l), nrow = nitems, dimnames = list(rowlab, "dMACS_Signed"))
  return(m)}

Quick Example

NeurotModel <- '
LatentNeuroticism =~ Vulnerability + Immoderation + SelfConsciousness + Depression + Anger + Anxiety'

NeurUSA <- cfa(NeurotModel, data = USAIP, group = "SEXO"); dUSA <- dMACS_Signed(NeurUSA, group1 = "MALE", group2 = "FEMALE")/6; dUSA; sum(dUSA)
##        dMACS_Signed
## Item 1   0.02933333
## Item 2  -0.01666667
## Item 3  -0.01050000
## Item 4   0.02650000
## Item 5  -0.01933333
## Item 6   0.00400000
## [1] 0.01333333
Np = 6 
Nl1 = c(0.535, 0.371, 0.527, 0.691, 0.541, 0.496)
Nl2 = c(0.604, 0.291, 0.560, 0.695, 0.482, 0.554)
Ni1 = c(3.477, 0.501, 6.596, 11.430, -0.761, 1.636)
Ni2 = c(2.774, 0.924, 6.853, 10.622, -0.293, 1.546)
Nsd = c(3.616231, 3.889883, 3.714411, 4.654757, 3.702424, 3.461905)
Nfm = -0.087
Nfsd = 1

NES <- SDI2.UDI2(Np, Nl1, Nl2, Ni1, Ni2, Nsd, Nfm, Nfsd)

NES$SDI2/6; sum(NES$SDI2/6)
##              SDI2
## [1,]  0.032683333
## [2,] -0.018416667
## [3,] -0.011400000
## [4,]  0.028950000
## [5,] -0.021300000
## [6,]  0.004583333
## [1] 0.0151

Both effect sizes can be compared to the unsigned varieties.

dUSA <- dMACS(NeurUSA, group1 = "MALE", group2 = "FEMALE")/6; dUSA; sum(dUSA)
##               dMAC
## Item 1 0.029333333
## Item 2 0.023833333
## Item 3 0.011166667
## Item 4 0.028000000
## Item 5 0.024833333
## Item 6 0.004833333
## [1] 0.122
NES$UDI2/6; sum(NES$UDI2/6)
##            UDI2
## [1,] 0.03268333
## [2,] 0.01841667
## [3,] 0.01140000
## [4,] 0.02895000
## [5,] 0.02130000
## [6,] 0.00470000
## [1] 0.11745

And the need for a signed effect size should now be obvious. With dMACS, the unsigned version would lead the researcher to think bias was aggregately much worse than it really was thanks to cancellation. In any case, all the bias was small and certainly not significant, though effects could be exacerbated (or reduced) in sumscore computation by weighting. At least with a unit-weighted sumscore, there is of course little bias. Finally, in case it is not understood, a positive effect signifies that the bias is raising the first group’s score relative to the second and vice-versa.