Bounded Estimation in MGCFA

138 views
Skip to first unread message

meijun yao

unread,
Jan 13, 2025, 3:53:08 AM1/13/25
to lavaan
Dear community, 

I have a one-factor model with 3 items and I would like to conduct measurement invariance testing across 23 groups. Specifically, it will be for 23 EU countries using European Social Survey data and each country has a sample size of at least 800. 

The model is as below:
CCPolSupport.Model<-'
CCPolicySupport=~support1+support2+support3
'

When I use the cfa() function to include these arguments: estimator="MLR", missing="FIML", std.lv=T, the configural invariance model could not converge.

For this reason, I try with the below code for the configural invariance model with an extra "bounds" argument (based on the code in Appendix H in this paper: De Jonckere, J., & Rosseel, Y. (2022). Using bounded estimation to avoid nonconvergence in small sample structural equation modeling. Structural Equation Modeling: A Multidisciplinary Journal29(3), 412-427.)

CCPolSupport.Config.Fit1.WideBound<-cfa(model =  CCPolSupport.Model  ,
                              data = ESS8,
                              group = "country",
                              estimator="MLR",
                              missing="FIML",
                              std.lv=T,
                              bounds="wide")

This works well for the configural invariance model. 

However, when I tried to implement it on the full metric invariance model with an extra argument of 'group.equal="loadings"', I noticed that the bound constrains the estimate of factor variance in all groups to be 1, when in reality only the estimate of the factor variance in group 1 should be 1 and the estimate of other groups should be in reference to group 1 (e.g. 1.018 in group 2, 1.066 in group 3) . 

Therefore, my questions are the below:
  • Is there a specific/better way to use bounded estimation correctly in the setting of MGCFA?
  • In terms of metric invariance model with std.lv=T, is there anyway to still use bounded estimation without constraining the estimate of factor variance in all groups to be 1?

Thank you very much in advance!

Meijun

Yago Luksevicius de Moraes

unread,
Jan 14, 2025, 9:05:50 AM1/14/25
to lavaan
Hi, Meijun.

I suspect the problem is that 3 items result in a model with 0 degrees of freedom in the configural model.
I guess you need to add a restriction, such as forcing all factor loadings to be equal in the first group.

```
CCPolSupport.Model<-'
CCPolicySupport=~c(1,NA)*support1+c(1,NA)*support2+c(1,NA)*support3'
```

See if this settles your problem.

Best regards,
Yago

meijun yao

unread,
Jan 14, 2025, 9:42:49 AM1/14/25
to lavaan
Hi Yago!

Thank you for sharing your insights regarding this issue. 

It is true that when I use the marker approach (as you suggested) to make the first factor loading to be 1, the bounded estimation works perfectly. 

As I was interested in the other approach that fix factor variance a prior to unity (instead of using the marker variable approach), I was wondering if there is any way I could keep the std.lv=T argument (for fixing factor variance) while allowing the bounded estimation?

To be more specific, the approach of fixing factor variance to unity is simple when there is only one group, as the factor variance is directly 1. When I have 23 groups and I would like to do MGCFA, the approach of "fixing factor variance to unity" should only fix the factor variance of the first group to be 1 and the factor variance estimate of the other group will be in reference to group 1 (if I understand it correctly). 

An example of estimation with no bounds look like the below:
Group 1 [AT]:
Variances:
                           Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .support1          1.228    0.033   37.695    0.000    1.228    0.838
   .support2          0.546    0.029   19.109    0.000    0.546    0.675
   .support3          0.883    0.038   23.081    0.000    0.883    0.733
    CCPolicySupprt    1.000                               1.000    1.000


Group 2 [BE]:
Variances:
                           Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .support1          1.243    0.038   33.121    0.000    1.243    0.837
   .support2          0.808    0.042   19.219    0.000    0.808    0.751
   .support3          0.908    0.040   22.830    0.000    0.908    0.735
    CCPolicySupprt    1.018    0.115    8.839    0.000    1.000    1.000


Group 3 [CH]:
Variances:
                            Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .support1          1.069    0.039   27.127    0.000    1.069    0.808
   .support2          0.516    0.033   15.510    0.000    0.516    0.648
   .support3          1.014    0.044   23.093    0.000    1.014    0.747
    CCPolicySupprt    1.066    0.117    9.121    0.000    1.000    1.000

An example of bounded estimation looks like the below:

Group 1 [AT]:

Latent Variables:
                              Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
Variances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .support1          1.215    0.032   37.632    0.000    1.215    0.813
   .support2          0.548    0.029   18.717    0.000    0.548    0.660
   .support3          0.851    0.036   23.881    0.000    0.851    0.687
    CCPolicySupprt    1.000                               1.000    1.000


Group 2 [BE]:
Variances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .support1          1.229    0.036   34.467    0.000    1.229    0.815
   .support2          0.810    0.041   19.880    0.000    0.810    0.741
   .support3          0.883    0.039   22.528    0.000    0.883    0.695
    CCPlcySpp (lb)    1.000       NA                      1.000    1.000


Group 3 [CH]:
Variances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .support1          1.057    0.037   28.551    0.000    1.057    0.791
   .support2          0.523    0.033   15.734    0.000    0.523    0.649
   .support3          0.995    0.044   22.770    0.000    0.995    0.720
    CCPlcySpp (lb)    1.000       NA                      1.000    1.000



I was wondering if I could use the bounded estimation but without constraining the estimate of factor variance of Group 2 and Group 3 to 1?

Thank you!

Best,
Meijun

Yago Luksevicius de Moraes

unread,
Jan 15, 2025, 8:45:24 AM1/15/25
to lav...@googlegroups.com

Hi, Meijun.

You can constrain manually in the model syntax.

CCPolSupport.Model<-'
CCPolicySupport =~ c(NA,NA)*support1+c(NA,NA)*support2+c(NA,NA)*support3

#Set factor mean to zero to the first group
CCPolicySupport ~ c(0,NA)*1

#Set factor variance to unity to the first group
CCPolicySupport ~~ c(1,NA)*CCPolicySupport
'

CCPolSupport.Config.Fit1.WideBound<-cfa(model =  CCPolSupport.Model  ,
                              data = ESS8,
                              group = "country",
                              estimator="MLR",
                              missing="FIML",
                              bounds="wide")

But I am not sure if this would be sufficient to identify the model.

Best regards,
Yago


Não contém vírus.www.avg.com

--
You received this message because you are subscribed to a topic in the Google Groups "lavaan" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/lavaan/TFiJDQfTGdA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to lavaan+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/lavaan/3eecc16c-f282-460c-99d0-60f15afb7f21n%40googlegroups.com.


--
Yago Luksevicius de Moraes
Master in Experimental Psychology
Bachelor in Psychology

meijun yao

unread,
Jan 16, 2025, 10:21:54 AM1/16/25
to lav...@googlegroups.com
Hello Yago!

Thanks for your valuable input!

I followed your suggestions and specified the syntax as below. (As I would like to have full metric invariance across all 23 groups, I use the same label for the factor loadings and take out the group.equal and std.lv arguments.)

CCPolSupport.Metric.M1.wide<-'
##constrain the loadings to be equal across group
CCPolicySupport=~c(L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1,L1)*support1+
                  c(L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2,L2)*support2+
                  c(L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3,L3)*support3

##Constrain the first group to have the variance as 1:
CCPolicySupport~~c(1,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA)*CCPolicySupport

'

CCPolSupport.Metric.Fit1.WideBound<-cfa(model = CCPolSupport.Metric.M1.wide,

                                        data = ESS8,
                                        group = "country",
                                        estimator="MLR",
                                        missing="FIML",
                                        #group.equal="loadings",
                                        #std.lv=T,
                                        bounds="wide")

Good news is that the factor variance of group 1 is constrained to 1 while the factor variances of the other groups can be freely estimated now. 

However, with the labels inside the syntax to constrain the factor loadings, it returns to the marker variable approach. Please see the image below:
image.png

I am wondering if there is any way to not constrain the first factor loading to be 1 while keeping the factor variances of other groups to be freely estimated?

Thank you so much!

Best regards,
Meijun

Yago Luksevicius de Moraes

unread,
Jan 17, 2025, 9:20:08 AM1/17/25
to lav...@googlegroups.com
Hi, Meijun,

I am glad it worked.
I assume that there is a way to make it not constrain the first factor loading to 1, but I do not know exactly how. Based on the report of `lavaan`, v. 0.6-18, I tried to use the syntax `F=~l1*NA*x1+x2+x3+x4` with the `HolzingerSwineford1939` dataframe and it worked. Check if it works in your case.

Best,
Yago

Alexander Miles

unread,
Jan 17, 2025, 3:42:08 PM1/17/25
to lavaan
Hi, Meijun and Yago,

Lavaan (v. 0.6-19) will by default fix the first factor loadings to 1, even if they are labelled. To get around this, as Yago pointed out, you can write it as:
`CCPolicySupport  =~ L1* NA*support1 + L2*support2 + L3*support3`
If that does not work: Using "std.lv" also has the same effect, without needing to specify the NA in the first loading.

(And just in case: You can use `ParTable(CCPolSupport.Metric.Fit1.WideBound)` to make sure your parameters are all what you want them to be).

Best,
Alex

meijun yao

unread,
Jan 21, 2025, 11:30:25 AM1/21/25
to al...@milesfamily.name, lavaan
Hello Yago and Alex!

First of all, thank you so much for your valuable insights!

For the full metric invariance model with the below codes, your suggestions work perfectly!

CCPolSupport.Metric.M1.wide<-'
##constrain the loadings to be equal across group
CCPolicySupport=~L1*NA*support1+
                  L2*support2+
                  L3*support3


##Constrain the first group to have the variance as 1:
CCPolicySupport~~c(1,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA)*CCPolicySupport
'

CCPolSupport.Metric.Fit1.WideBound<-cfa(model = CCPolSupport.Metric.M1.wide,
                                        data = ESS8,
                                        group = "country",
                                        estimator="MLR",
                                        missing="FIML",
                                        group.equal="loadings",
                                        bounds="wide")



I would like to ask an additional question regarding the modification indices if you might have an idea of what is happening below.

When I am trying to request the modification indices of the solution without bounded estimation below, there are more than 22 parameters with modification indices larger than 10:
CCPolSupport.Metric.M1<-'
CCPolicySupport=~support1+support2+support3
'

CCPolSupport.Metric.Fit1<-cfa(model = CCPolSupport.Metric.M1,

                              data = ESS8,
                              group = "country",
                              estimator="MLR",
                              missing="FIML",
                              group.equal="loadings",
                              std.lv=T)


CCPolSupport.MI.Metric.M1<-lavTestScore(CCPolSupport.Metric.Fit1, epc = T)

However, when I request the modification indices of the solution with wide bounded estimation below, there are only 4 parameters with modification indices larger than 10. 
CCPolSupport.Metric.M1.wide<-'
##constrain the loadings to be equal across group
CCPolicySupport=~L1*NA*support1+
                  L2*support2+
                  L3*support3


##Constrain the first group to have the variance as 1:
CCPolicySupport~~c(1,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,NA)*CCPolicySupport
'

CCPolSupport.Metric.Fit1.WideBound<-cfa(model = CCPolSupport.Metric.M1.wide,
                                        data = ESS8,
                                        group = "country",
                                        estimator="MLR",
                                        missing="FIML",
                                        group.equal="loadings",
                                        bounds="wide")

CCPolSupport.MI.Metric.M1<-lavTestScore(CCPolSupport.Metric.Fit1.WideBound, epc = T)

The output (including (standardized) factor loadings of each group, fit indexes) are identical, but the modification indices are different. I am wondering if you might know the reasons for this?

Thank you!

Best regards,
Meijun

Jeremy Miles

unread,
Jan 21, 2025, 12:31:45 PM1/21/25
to lav...@googlegroups.com, al...@milesfamily.name
[snip]

The output (including (standardized) factor loadings of each group, fit indexes) are identical, but the modification indices are different. I am wondering if you might know the reasons for this?


What about the (unstandardized) factor loadings? Are these the same?

If not, the models are not the same (but are equivalent) and one would not expect the modification indices to be the same.

Jeremy

 

meijun yao

unread,
Jan 22, 2025, 4:40:59 AM1/22/25
to lav...@googlegroups.com, al...@milesfamily.name
Hi Jeremy!

Thank you for your reply!

I have constrained the factor loadings to be equal for full metric invariance. Therefore, for the same item like supprt1, the unstandardized factor loadings are not only identical between no bound estimation and wide bounded estimation but also identical across all groups. The standardized factor loadings (from std.all) are identical between no bound estimation and wide bounded estimation for the same item in each group. 

The below image is part of the output, which includes estimates of 2 groups. (The complete output contains 23 groups.)

image.png

And just to make it more clear, please allow me to share part of the output for modification indices below:

image.png

As you can see, for the same parameters, the X2 is different. For example, for rhs with label .p199, the X2 is 34.464 for no bound estimation and 14.699 for the bounded estimation. This rhs .p199 refers to the factor loadings of support1 in group 19 in both no bound and wide bound estimation. Despite the fact that X2 is different between no-bound and wide-bound estimation, epc is the same in both cases. 

I am wondering if anyone might have an idea of why this happens?

Thanks!

Best regards,
Meijun

--
You received this message because you are subscribed to a topic in the Google Groups "lavaan" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/lavaan/TFiJDQfTGdA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to lavaan+un...@googlegroups.com.

Yago Luksevicius de Moraes

unread,
Jan 22, 2025, 8:50:02 AM1/22/25
to lavaan
Hi, Meijun.

I am afraid I do not know the answer to your question. It might be related to how bounded and non-bounded estimations assess the model's fit, but this is outside my expertise.

Just two quick notes:
1) modification indices should not be used without theoretical considerations, because, when you use them, you are testing a model different from your original theory.
2) the output you posted does not looks like how lavaan prints modification indices. It looks like the Lagrange Multiplier Test for releasing constrained parameters. This would be used to fit a partial invariant model, but I have never seen anyone supporting the interpretability of partial invariance at the metric invariance step.

Best,
Yago

Terrence Jorgensen

unread,
Jan 23, 2025, 4:26:20 AM1/23/25
to lavaan
for the same parameters, the X2 is different. For example, for rhs with label .p199, the X2 is 34.464 for no bound estimation and 14.699 for the bounded estimation. This rhs .p199 refers to the factor loadings of support1 in group 19 in both no bound and wide bound estimation.

Modification indices (and score tests more generally) are calculated as a function of first and second derivatives of the model's likelihood function, augmented with additional parameters.  Formulas are provided here, and in references contained therein:


Using (different) bounded estimation methods will change the gradient (first derivative) and Hessian (second derivative), thus changing the modification index.

Despite the fact that X2 is different between no-bound and wide-bound estimation, epc is the same in both cases. 

I can't say the EPC will always be identical, but the EPC is a rescaling of the score-test statistic (modification index; see Eq. 14 in the paper linked above).  I expect the degree to which bounded estimation changes the gradient (score) for that parameter is proportionally equivalent to the degree to which bounded estimation changes the Hessian (which reflects sampling variance of the squared score/gradient element).  So it just balances out to expect the same parameter changes, despite one of your sampling methods having less uncertainty about that expected change.

Terrence D. Jorgensen    (he, him, his)
Assistant Professor, Methods and Statistics
Research Institute for Child Development and Education, the University of Amsterdam
http://www.uva.nl/profile/t.d.jorgensen

meijun yao

unread,
Jan 23, 2025, 10:42:37 AM1/23/25
to lav...@googlegroups.com
Thank you Terrence! 

This is extremely helpful and answers my questions perfectly. 

Best,
Meijun

--
You received this message because you are subscribed to a topic in the Google Groups "lavaan" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/lavaan/TFiJDQfTGdA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to lavaan+un...@googlegroups.com.

Yves Rosseel

unread,
3:09 AM (4 hours ago) 3:09 AM
to lav...@googlegroups.com, meij...@gmail.com
Searching the lavaangroup archive for potential bugs, I stumbled upon
this message (which I missed at the time).

(in the future, it is better to open a github issue to report this)

This is now fixed in the github version of lavaan (to become 0.7-1).

Yves.

On 1/14/25 15:42, meijun yao wrote:
> Hi Yago!
>
> Thank you for sharing your insights regarding this issue.
>
> It is true that when I use the marker approach (as you suggested) to
> make the first factor loading to be 1, the bounded estimation works
> perfectly.
>
> As I was interested in the other approach that fix factor variance a
> prior to unity (instead of using the marker variable approach), I was
> wondering if there is any way I could keep the std.lv=T argument (for
> fixing factor variance) while allowing the bounded estimation?
>
> To be more specific, the approach of fixing factor variance to unity is
> simple when there is only one group, as the factor variance is directly
> 1. When I have 23 groups and I would like to do MGCFA, the approach of
> "fixing factor variance to unity" should only fix the factor variance of
> the first group to be 1 and the factor variance estimate of the other
> group will be in reference to group 1 (if I understand it correctly).
>
> An example of estimation with no bounds look like the below:
> Group 1 [AT]:
> Variances:
>                            Estimate  Std.Err  z-value  P(>|z|)   Std.lv
>  Std.all
>    .support1          1.228    0.033   37.695    0.000    1.228    0.838
>    .support2          0.546    0.029   19.109    0.000    0.546    0.675
>    .support3          0.883    0.038   23.081    0.000    0.883    0.733
>     CCPolicySupprt * 1.000* *1.000    1.000*
>
>
> Group 2 [BE]:
> Variances:
>                            Estimate  Std.Err  z-value  P(>|z|)   Std.lv
>  Std.all
>    .support1          1.243    0.038   33.121    0.000    1.243    0.837
>    .support2          0.808    0.042   19.219    0.000    0.808    0.751
>    .support3          0.908    0.040   22.830    0.000    0.908    0.735
>     CCPolicySupprt *1.018 * * 0.115    8.839    0.000    1.000    1.000*
>
>
> Group 3 [CH]:
> Variances:
>                             Estimate  Std.Err  z-value  P(>|z|)
> Std.lv  Std.all
>    .support1          1.069    0.039   27.127    0.000    1.069    0.808
>    .support2          0.516    0.033   15.510    0.000    0.516    0.648
>    .support3          1.014    0.044   23.093    0.000    1.014    0.747
>     CCPolicySupprt *1.066* *0.117    9.121    0.000    1.000    1.000*
>
> An example of bounded estimation looks like the below:
>
> Group 1 [AT]:
>
> Latent Variables:
>                               Estimate  Std.Err  z-value  P(>|z|)
> Std.lv  Std.all
> Variances:
>                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
>    .support1          1.215    0.032   37.632    0.000    1.215    0.813
>    .support2          0.548    0.029   18.717    0.000    0.548    0.660
>    .support3          0.851    0.036   23.881    0.000    0.851    0.687
> *CCPolicySupprt    1.000                               1.000    1.000*
>
>
> Group 2 [BE]:
> Variances:
>                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
>    .support1          1.229    0.036   34.467    0.000    1.229    0.815
>    .support2          0.810    0.041   19.880    0.000    0.810    0.741
>    .support3          0.883    0.039   22.528    0.000    0.883    0.695
> *CCPlcySpp (lb)    1.000       NA                      1.000    1.000*
>
>
> Group 3 [CH]:
> Variances:
>                    Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
>    .support1          1.057    0.037   28.551    0.000    1.057    0.791
>    .support2          0.523    0.033   15.734    0.000    0.523    0.649
>    .support3          0.995    0.044   22.770    0.000    0.995    0.720
> *CCPlcySpp (lb)    1.000       NA                      1.000    1.000*
> _estimator="MLR", missing="FIML", std.lv <http://std.lv>=T_, the
> configural invariance model *could not converge*.
>
> For this reason, I try with the below code for the configural
> invariance model with an extra "bounds" argument (based on the
> code in Appendix H in this paper: De Jonckere, J., & Rosseel, Y.
> (2022). Using bounded estimation to avoid nonconvergence in
> small sample structural equation modeling. /Structural Equation
> Modeling: A Multidisciplinary Journal/, /29/(3), 412-427.)
>
> CCPolSupport.Config.Fit1.WideBound<-cfa(model =
> CCPolSupport.Model  ,
>                               data = ESS8,
>                               group = "country",
>                               estimator="MLR",
>                               missing="FIML",
> std.lv <http://std.lv>=T,
> *bounds="wide"*)
>
> This works well for the configural invariance model.
>
> However, when I tried to implement it on the full metric
> invariance model with an extra argument of
> 'group.equal="loadings"', I noticed that the bound constrains
> the estimate of factor variance in *all groups* to be 1, when in
> reality only the estimate of the factor variance in group 1
> should be 1 and the estimate of other groups should be in
> reference to group 1 (e.g. 1.018 in group 2, 1.066 in group 3) .
>
> Therefore, my questions are the below:
>
> * Is there a specific/better way to use bounded estimation
> correctly in the setting of MGCFA?
> * In terms of metric invariance model with std.lv <http://
> std.lv>=T, is there anyway to still use bounded estimation
> *without* constraining the estimate of factor variance in
> all groups to be 1?
>
>
> Thank you very much in advance!
>
> Meijun
>
> --
> You received this message because you are subscribed to the Google
> Groups "lavaan" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to lavaan+un...@googlegroups.com
> <mailto:lavaan+un...@googlegroups.com>.
> To view this discussion visit https://groups.google.com/d/msgid/
> lavaan/3eecc16c-f282-460c-99d0-60f15afb7f21n%40googlegroups.com
> <https://groups.google.com/d/msgid/lavaan/3eecc16c-
> f282-460c-99d0-60f15afb7f21n%40googlegroups.com?
> utm_medium=email&utm_source=footer>.

--
Yves Rosseel
Department of Data Analysis, Ghent University

Reply all
Reply to author
Forward
0 new messages