In a factorial invariance analysis, the groups sometimes do not share the same set of observed variables. A common instance is a shorter or longer scale form: one group answers four items and another answers five. A single shared configural model cannot be written for such data, because it would reference an item that does not exist in every group.
lavaan handles this with group-specific model
syntax: a group: block defines a separate model
for each group. pinSearch() supports this syntax, so a
partial invariance specification search can still be run when the item
sets differ across groups.
Write the configural model as a string (or a character vector) with
one group: N block per group; each group: N
starts on its own line:
mod <- c(
"group: 1", "F =~ y1 + y2 + y3 + y4", # e.g. the short form
"group: 2", "F =~ y1 + y2 + y3 + y4 + y5") # e.g. the long formThe number of group: blocks, and their order, must match
the groups used to split the data. As usual, pinSearch()
passes config_mod straight through to
[lavaan::cfa()], so you still pass
group = "..." (via ...) to split the data;
group: N then refers to the Nth level of
that grouping factor.
We simulate a single five-item trait. Item y5 is
answered only by the “long” form, and item y3 loads more
weakly in the “long” form, so the search has something to find.
library(lavaan)
library(pinsearch)
library(MASS, include.only = "mvrnorm")
set.seed(8)
lamS <- c(.90, .85, .80, .75) # short form: y1-y4
lamL <- c(.90, .85, .50, .75, .70) # long form: y1-y5 (y3 weakened)
sigS <- tcrossprod(lamS) + diag(1 - lamS^2)
sigL <- tcrossprod(lamL) + diag(1 - lamL^2)
n <- 500
gS <- mvrnorm(n, rep(0, 4), sigS) # short form
gL <- mvrnorm(n, rep(0, 5), sigL) # long form
dS <- as.data.frame(gS); names(dS) <- paste0("y", 1:4)
dL <- as.data.frame(gL); names(dL) <- paste0("y", 1:5)
df <- rbind(cbind(dS, y5 = NA, group = "short"),
cbind(dL, group = "long"))
df$group <- factor(df$group, levels = c("short", "long"))A single model F =~ y1 + y2 + y3 + y4 + y5 cannot
describe both groups, because y5 is absent for the “short”
form. With the group-specific syntax, both forms are handled in one
call:
mod <- c(
"group: 1", "F =~ y1 + y2 + y3 + y4", # short: no y5
"group: 2", "F =~ y1 + y2 + y3 + y4 + y5") # long: full form
ps <- pinSearch(mod,
data = df, group = "group",
type = "intercepts" # search loadings, then intercepts
)
ps$`Non-Invariant Items`
#> group lhs rhs type
#> 1 2 F y3 loadingsThe specification search flags the loading of y3 in the
long form. The final partial invariance model is
summary(ps$`Partial Invariance Fit`)
#> lavaan 0.7-2 ended normally after 26 iterations
#>
#> Estimator ML
#> Optimization method NLMINB
#> Number of model parameters 29
#> Number of equality constraints 6
#>
#> Number of observations per group:
#> short 500
#> long 500
#>
#> Model Test User Model:
#>
#> Test statistic 10.116
#> Degrees of freedom 11
#> P-value (Chi-square) 0.520
#> Test statistic for each group:
#> short 3.817
#> long 6.299
#>
#> Parameter Estimates:
#>
#> Standard errors Standard
#> Information Expected
#> Information saturated (h1) model Structured
#>
#>
#> Group 1 [short]:
#>
#> Latent Variables:
#> Estimate Std.Err z-value P(>|z|)
#> F =~
#> y1 (.p1.) 0.907 0.035 26.099 0.000
#> y2 (.p2.) 0.874 0.035 24.935 0.000
#> y3 (.p3.) 0.845 0.039 21.625 0.000
#> y4 (.p4.) 0.758 0.034 22.164 0.000
#>
#> Intercepts:
#> Estimate Std.Err z-value P(>|z|)
#> F 0.000
#> .y1 (.11.) 0.069 0.044 1.567 0.117
#> .y2 (.12.) 0.085 0.044 1.929 0.054
#> .y3 (.13.) 0.028 0.046 0.603 0.547
#> .y4 (.14.) 0.042 0.041 1.025 0.305
#>
#> Variances:
#> Estimate Std.Err z-value P(>|z|)
#> F 1.000
#> .y1 0.208 0.022 9.378 0.000
#> .y2 0.288 0.025 11.448 0.000
#> .y3 0.352 0.028 12.380 0.000
#> .y4 0.443 0.032 13.767 0.000
#>
#>
#> Group 2 [long]:
#>
#> Latent Variables:
#> Estimate Std.Err z-value P(>|z|)
#> F =~
#> y1 (.p1.) 0.907 0.035 26.099 0.000
#> y2 (.p2.) 0.874 0.035 24.935 0.000
#> y3 0.598 0.047 12.615 0.000
#> y4 (.p4.) 0.758 0.034 22.164 0.000
#> y5 0.750 0.047 15.818 0.000
#>
#> Intercepts:
#> Estimate Std.Err z-value P(>|z|)
#> F -0.090 0.067 -1.350 0.177
#> .y1 (.11.) 0.069 0.044 1.567 0.117
#> .y2 (.12.) 0.085 0.044 1.929 0.054
#> .y3 0.055 0.049 1.130 0.259
#> .y4 (.14.) 0.042 0.041 1.025 0.305
#> .y5 0.007 0.051 0.132 0.895
#>
#> Variances:
#> Estimate Std.Err z-value P(>|z|)
#> F 0.982 0.100 9.844 0.000
#> .y1 0.200 0.023 8.642 0.000
#> .y2 0.332 0.028 11.689 0.000
#> .y3 0.729 0.049 14.947 0.000
#> .y4 0.457 0.033 13.652 0.000
#> .y5 0.572 0.041 14.047 0.000Effect sizes for the flagged item follow the usual workflow:
pinSearch(mod, data = df, group = "group",
type = "intercepts", effect_size = TRUE)
#> $`Partial Invariance Fit`
#> lavaan 0.7-2 ended normally after 26 iterations
#>
#> Estimator ML
#> Optimization method NLMINB
#> Number of model parameters 29
#> Number of equality constraints 6
#>
#> Number of observations per group:
#> short 500
#> long 500
#>
#> Model Test User Model:
#>
#> Test statistic 10.116
#> Degrees of freedom 11
#> P-value (Chi-square) 0.520
#> Test statistic for each group:
#> short 3.817
#> long 6.299
#>
#> $`Non-Invariant Items`
#> group lhs rhs type
#> 1 2 F y3 loadings
#>
#> $effect_size
#> y3-F
#> dmacs 0.2394173Yoon, M., & Millsap, R. E. (2007). Detecting violations of factorial invariance using data-based specification searches: A Monte Carlo study. Structural Equation Modeling: A Multidisciplinary Journal, 14(3), 435-463.