---
title: "Finding Empirical Equivalent Models in SEM"
date: "2026-09-13"
output:
  rmarkdown::html_vignette:
    options:
      toc:
        depth: 2
      number_sections: true
vignette: >
  %\VignetteIndexEntry{Finding Empirical Equivalent Models in SEM}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
bibliography: references.bib
csl: apa.csl
---



# Introduction

This article is a brief illustration of how
to use `semeqmodels()`
from the package
[semeqmodels](https://sfcheung.github.io/semeqmodels/)
to find empirically equivalent models
of a model fitted by structural
equation modeling using `lavaan`.

# Data

A test dataset of `semeqmodels` will be
used for illustration:


``` r
library(semeqmodels)
packageVersion("semeqmodels")
#> [1] '0.0.1.5'
head(round(data_test_3_factor_3_item, 2))
#>      x1    x2    x3    m1    m2    m3    y1    y2    y3
#> 1 -2.13 -1.21 -0.05  1.88  0.99  0.40  3.24  2.23  1.44
#> 2 -1.77 -0.91 -0.23 -0.02  0.12 -0.33  0.85 -0.71 -0.72
#> 3 -0.54  0.69 -0.10 -0.33 -0.34  2.31 -1.35 -0.02  0.76
#> 4 -1.72 -1.04  0.07  0.11 -0.13 -0.28 -0.49  1.77  0.64
#> 5 -0.44 -0.41 -0.12 -0.34 -0.27  0.57  2.43 -0.11 -0.20
#> 6  1.42  1.24  0.78  0.47 -0.38  1.72  0.68  1.22 -1.16
```

# Model

Suppose we are going to fit a model
with three latent variables:

![Model: Structural Only](semeqmodels_model-1.png)


``` r
mod <-
"
fx =~ x1 + x2 + x3
fm =~ m1 + m2 + m3
fy =~ y1 + y2 + y3
fm ~ fx
fy ~ fm
"
```


``` r
library(lavaan)
fit <- sem(
  model = mod,
  data = data_test_3_factor_3_item
)
fit
#> lavaan 0.7-2.3166 ended normally after 40 iterations
#> 
#>   Estimator                                         ML
#>   Optimization method                           NLMINB
#>   Number of model parameters                        20
#> 
#>   Number of observations                           200
#> 
#> Model Test User Model:
#>                                                       
#>   Test statistic                                25.503
#>   Degrees of freedom                                25
#>   P-value (Chi-square)                           0.434
#>                                                       
#>   Browne's residual (NT model-based) test             
#>   Test statistic                                24.070
#>   Degrees of freedom                                25
#>   P-value (Chi-square)                           0.515
```



# Empirical Equivalence

Suppose we would like to find *some*
models that are *empirically*
*equivalent* to this model:

In this package,
two models are defined to be empirically equivalent
if the following conditions are met:

- They have the same model degrees of freedom.

- Their absolute differences on selected
  fit measures are equal to or smaller than
  a user-defined tolerance.

See [this section](#equivalence-in-principle-and-empirical-equivalence)
on a discussion of equivalence

We will start the demonstration using
model $\chi^2$, with a tolerance of
0.00001.

# Example 1: Model Chi-Squares

## Calling `eq_models()`

To find a list of models that are empirically
equivalent to the model fitted above, based
on model $\chi^2$, the default,
we can use `eq_models()`:


``` r
mod_eq <- eq_models(
  original_model = fit
)
```

For large models, this process can take
some time to run (can be over ten minutes for
complicated models) because the number of
possible models can be several hundreds
or even several thousands.

## Examine the Models

The output is a list of models, defined
by `lavaan` parameter tables:


``` r
mod_eq
#> 
#> Number of models: 5
#> 
#> The models:
#> 
#>   Model   
#> 1 c8181b7d
#> 2 34be955a
#> 3 3ded8a97
#> 4 d882121d
#> 5 fada78e4 
#> 
#> NOTE: 'default' names are used. Call 'print()' and add 'names_to_use =
#> "long"' to use the long descriptive names, if available, for the
#> models.
```

For a more readable printout, call `print()`
and add `names_to_use = "long"`:


``` r
print(mod_eq,
      names_to_use = "long")
#> 
#> Number of models: 5
#> 
#> The models:
#> 
#> Model
#> 1 c8181b7d := original, drop:fm~fx, add:fx~~fm, drop:fy~fm, add:fm~~fy,
#>     drop:fx~~fm, add:fx~fm, drop:fm~~fy, add:fm~fy, drop:fm~fy,
#>     add:fm~~fy
#> 2 34be955a := original, drop:fm~fx, add:fx~~fm, drop:fy~fm, add:fm~~fy,
#>     drop:fx~~fm, add:fx~fm, drop:fm~~fy, add:fm~fy
#> 3 3ded8a97 := original, drop:fm~fx, add:fx~~fm, drop:fy~fm, add:fm~~fy,
#>     drop:fx~~fm, add:fx~fm, drop:fm~~fy, add:fy~fm
#> 4 d882121d := original, drop:fm~fx, add:fx~~fm, drop:fy~fm, add:fm~~fy,
#>     drop:fm~~fy, add:fy~fm
#> 5 fada78e4 := original
```

The long names show the modifications
from the original model to the final
models.

By default, the original model is not
removed, although it may still be shown
with steps of modifications.

The function `eq_df()` can be used to
retrieve the model *df*s:


``` r
eq_df(mod_eq)
#> c8181b7d 34be955a 3ded8a97 d882121d fada78e4 
#>       25       25       25       25       25
```

As expected, all models have the same model
*df*s.

The function `eq_chisq()` can be used
to retrieve the model $\chi^2$s of
the models:


``` r
eq_chisq(mod_eq)
#> c8181b7d 34be955a 3ded8a97 d882121d fada78e4 
#> 25.50339 25.50339 25.50339 25.50339 25.50339
```

It can be confirmed that all models have
nearly the same model $\chi^2$.

Actually, in this case, the models in this
case are not just empirically equivalent.
They are also mathematically equivalent.

The function `model_diff_many()` can
be used to list the differences between
the fitted models and each of the
empirically equivalent models:


``` r
model_diff_many(
  target_model = fit,
  other_models = mod_eq
)
#> 
#> -------------
#> 
#> Models: fit vs. c8181b7d
#> 
#> Model: fit
#> fm~fx (free)
#> fy~fm (free)
#> 
#> Model: c8181b7d
#> fx~fm (free)
#> fm~~fy (free)
#> 
#> -------------
#> 
#> Models: fit vs. 34be955a
#> 
#> Model: fit
#> fm~fx (free)
#> fy~fm (free)
#> 
#> Model: 34be955a
#> fx~fm (free)
#> fm~fy (free)
#> 
#> -------------
#> 
#> Models: fit vs. 3ded8a97
#> 
#> Model: fit
#> fm~fx (free)
#> 
#> Model: 3ded8a97
#> fx~fm (free)
#> 
#> -------------
#> 
#> Models: fit vs. d882121d
#> 
#> Model: fit
#> fm~fx (free)
#> 
#> Model: d882121d
#> fm~~fx (free)
#> 
#> -------------
#> 
#> Models: fit vs. fada78e4
#> 
#> Model: fit
#> No parameter only in this model.
#> 
#> Model: fada78e4
#> No parameter only in this model.
```

A better way to see the differences is
to draw the models, described next.

## Visualize the Models

The function `partables_plots()` can
be used to visualize the models,
using `semPaths()` from the `semPlot`
package. Basic knowledge of `semPlot::semPaths()`
is required.


``` r
layout_i <- matrix(c(  NA, "fm",  NA,
                     "fx",   NA, "fy"),
                   ncol = 3,
                   nrow = 2,
                   byrow = TRUE)
p <- partables_plots(
  mod_eq,
  original_model = fit,
  layout = layout_i,
  label.cex = 1.5,
  sizeLat = 15,
  edge.width = 5,
  asize = 5,
  structural = TRUE
)
```

The `plot()` method can be used to plot
the models:


``` r
plot(
  p,
  ncol = 3,
  nrow = 2
)
```

![Empirical Equivalent Models](semeqmodels_eq_models-1.png)

By default, paths different from the
those in the original model will be
displayed in blue. Covariances (both
covariances and error covariances) will
be represented by curves.

# Example 2: CFI

## Calling `eq_models()` with `tolerance`

Suppose we would like to define empirical
equivalence using another fit measure,
such as CFI. This can be done using the
argument `tolerance`.


``` r
mod_eq_cfi <- eq_models(
  original_model = fit,
  tolerance = c(cfi = .01)
)
```

The argument `tolerance` accepts a
*named* numeric vector. For each element:

- The *name* is the name of a fit measure
as appeared in `lavaan::fitMeasures()`.

- The value is the maximum absolute difference
  on this fit measure for two models to be
  considered empirically equivalent.

If the vector has more than one value,
empirical equivalence is checked using
*all* fit measures specified in `tolerance`.

In this example, `c(cfi = .01)` indicates
that two models are considered empirically
equivalent if their absolute difference in
CFI is at most .01.

## Examine the Models

The output is a list of models, defined
by `lavaan` parameter tables:


``` r
mod_eq_cfi
#> 
#> Number of models: 9
#> 
#> The models:
#> 
#>   Model   
#> 1 bf4767ac
#> 2 c8181b7d
#> 3 34be955a
#> 4 3ded8a97
#> 5 4588fdf2
#> 6 0e0eca72
#> 7 c15465c0
#> 8 d882121d
#> 9 fada78e4 
#> 
#> NOTE: 'default' names are used. Call 'print()' and add 'names_to_use =
#> "long"' to use the long descriptive names, if available, for the
#> models.
```

Because a more liberal criterion is used,
the number of models is larger than when using
model $\chi^2$.


``` r
eq_df(mod_eq_cfi)
#> bf4767ac c8181b7d 34be955a 3ded8a97 4588fdf2 0e0eca72 c15465c0 d882121d 
#>       25       25       25       25       25       25       25       25 
#> fada78e4 
#>       25
```

Even with this liberal criterion, all
models still have the same model
*df*s.

The function `eq_fitMeasures()` can be used
to retrieve selected fit measure(s) of all
models:


``` r
eq_fitMeasures(
  mod_eq_cfi,
  "cfi"
)
#>     bf4767ac  c8181b7d  34be955a  3ded8a97 4588fdf2 0e0eca72 c15465c0  d882121d
#> cfi        1 0.9980126 0.9980126 0.9980126        1        1        1 0.9980126
#>      fada78e4
#> cfi 0.9980126
```

In this case, the models are no longer
necessarily mathematically equivalent
to the original model.

The function `model_diff_many()`,
introduced before, can
be used to list the differences between
the fitted models and each of the
empirically equivalent models.

## Visualize the Models

The function `partables_plots()` can
be used to visualize the models,
using `semPaths()` from the `semPlot`
package. Basic knowledge of `semPlot::semPaths()`
is required.


``` r
layout_i <- matrix(c(  NA, "fm",  NA,
                     "fx",   NA, "fy"),
                   ncol = 3,
                   nrow = 2,
                   byrow = TRUE)
p_cfi <- partables_plots(
  mod_eq_cfi,
  original_model = fit,
  layout = layout_i,
  label.cex = 1.5,
  sizeLat = 15,
  edge.width = 5,
  asize = 5,
  structural = TRUE
)
```

Let's plot all the models:


``` r
plot(
  p_cfi,
  ncol = 3,
  nrow = 3
)
```

![Empirical Equivalent Models (CFI)](semeqmodels_eq_models_cfi-1.png)

## Selecting Models

Based on knowledge about the variables,
the research design, or theoretical
reasons, some models may need to be
removed.

For example, the factor `fx` may be measured
a certain period before `fm` and `fy`,
while `fm` and `fy` are measured in the
same wave. Therefore, `fx` cannot be
an "y"-variable ("dependent variable")
that is affected by other variables.

There is a set of model selectors
functions (see `?partable_select`) that
can be used to select models. The function
`must_not_be_y()` will be demonstrated in
this vignette.


``` r
p_cfi_fx_not_y <- must_not_be_y(
  p_cfi,
  vars = "fx"
)
```


``` r
plot(
  p_cfi_fx_not_y,
  ncol = 3,
  nrow = 2
)
```

![Empirical Equivalent Models (Filtered)](semeqmodels_eq_models_selected-1.png)

See `?partable_select` for other ways to
select models.



# Final Remarks

## Customize the Search

There are many other ways to customize
the search and the plots. Please refer
to the corresponding help pages for
details.

Demonstrations of other models and
cases can be found in
the [other demonstration articles](https://sfcheung.github.io/semeqmodels/articles/index.html#demonstrations)

## Equivalence-In-Principle and Empirical Equivalence

The concept of mathematical
equivalence models, or two models
being equivalent in principle [@lee_simple_1990],
has
a long history in the literature on
structural equation modeling
[@maccallum_problem_1993, @williams_equivalent_2012].
Two models are considered to be
mathematically equivalent
if they necessarily imply the same
covariance matrix regardless of the data.
There are methods to generate them and
tools to generate them automatically
[e.g., @lee_simple_1990].

Our definition of empirical equivalence
is similar to *empirical occurrence of equivalence*
[EOE, @lee_simple_1990]. However, we include
the requirement of equal degrees of freedom:
two models must also be equal in parsimony.
We also allow for the possibility of using
any fit measures deemed appropriate (e.g., CFI,
RMSEA), and also the use of tolerance values
that are appropriate for a situation.

Although our focus is on empirical equivalence,
two models that are mathematically equivalent in the
conventional sense are necessarily
empirically equivalent.
Note that the reverse is
not true: two models that are empirically
equivalent are not necessarily mathematically
equivalent.

Nevertheless, when the tolerance is set to
be very small, the models identified,
though not necessarily, are likely to be
mathematically equivalent. Therefore,
the package can also be used to identify
models that are likely mathematically
equivalent.


# References
