StructuralEquationModels / StructuralEquationModels.jl

A fast and flexible Structural Equation Modelling Framework
https://structuralequationmodels.github.io/StructuralEquationModels.jl/dev/
MIT License
46 stars 6 forks source link

Fixed **and** Labeled loadings don't show up in the degrees of freedom. #208

Open nickhaf opened 2 months ago

nickhaf commented 2 months ago

I have a question regarding the degrees of freedom for fixed and labeled loadings. If a loading is labeled and fixed, the degrees of freedom are not updated compared to the fit with the loading unfixed. If the fixed loading is not labeled, there is no problem:

Just fixed, not labeled

using StructuralEquationModels, StenoGraphs
data = example_data("political_democracy")

observed_vars = [:x1, :x2, :x3, :y1, :y2, :y3, :y4, :y5, :y6, :y7, :y8]
latent_vars = [:ind60, :dem60, :dem65]

graph_2 = @StenoGraph begin

    # loadings
    ind60 → fixed(1)*x1 + fixed(0)*x2 + x3
    dem60 → fixed(1)*y1 + y2 + y3 + y4
    dem65 → fixed(1)*y5 + y6 + y7 + y8

    # latent regressions
    ind60 → dem60
    dem60 → dem65
    ind60 → dem65

    # variances
    _(observed_vars) ↔ _(observed_vars)
    _(latent_vars) ↔ _(latent_vars)

    # covariances
    y1 ↔ y5
    y2 ↔ y4 + y6
    y3 ↔ y7
    y8 ↔ y4 + y6

end

partable_2 = ParameterTable(
    latent_vars = latent_vars,
    observed_vars = observed_vars,
    graph = graph_2)

model_2 = Sem(
    specification = partable_2,
    data = data
)

df(model_2)

Fixed and labeled

graph_3 = @StenoGraph begin

    # loadings
    ind60 → fixed(1)*x1 + label(:loading_12)*fixed(0)*x2 + x3
    dem60 → fixed(1)*y1 + y2 + y3 + y4
    dem65 → fixed(1)*y5 + y6 + y7 + y8

    # latent regressions
    ind60 → dem60
    dem60 → dem65
    ind60 → dem65

    # variances
    _(observed_vars) ↔ _(observed_vars)
    _(latent_vars) ↔ _(latent_vars)

    # covariances
    y1 ↔ y5
    y2 ↔ y4 + y6
    y3 ↔ y7
    y8 ↔ y4 + y6

end

partable_3 = ParameterTable(
    latent_vars = latent_vars,
    observed_vars = observed_vars,
    graph = graph_3)

model_3 = Sem(
    specification = partable_3,
    data = data
)

df(model_3)

When fitting this model with sem_fit(model_2) I get a warning that I'm using a labeled constant. However, I can also extract the df before fitting the model, in which case I don't get the warning. In either case, I found this a bit counterintuitive, in lavaan both is possible:

Lavaan

library(lavaan)

## Not fixed: 
model <- '
  # measurement model
    ind60 =~ x1 + x2 + x3
    dem60 =~ y1 + y2 + y3 + y4
    dem65 =~ y5 + y6 + y7 + y8
  # regressions
    dem60 ~ ind60
    dem65 ~ ind60 + dem60
  # residual correlations
    y1 ~~ y5
    y2 ~~ y4 + y6
    y3 ~~ y7
    y4 ~~ y8
    y6 ~~ y8
'

fit_1 <- sem(model, data = PoliticalDemocracy)
fitMeasures(fit_1, "df")

## Fixed and labeled:
model <- '
  # measurement model
    ind60 =~ x1 + 0*testlabel*x2 + x3
    dem60 =~ y1 + y2 + y3 + y4
    dem65 =~ y5 + y6 + y7 + y8
  # regressions
    dem60 ~ ind60
    dem65 ~ ind60 + dem60
  # residual correlations
    y1 ~~ y5
    y2 ~~ y4 + y6
    y3 ~~ y7
    y4 ~~ y8
    y6 ~~ y8
'

fit_2 <- sem(model, data = PoliticalDemocracy)
fitMeasures(fit_2, "df")
Maximilian-Stefan-Ernst commented 2 months ago

Thanks a lot for raising this issue, I will have a look!

Maximilian-Stefan-Ernst commented 2 months ago

I'm not sure how to proceed - internally, we label all fixed parameters as :const and reserve that label for fixed parameters. So we could either allow for labeled fixed parameters, or throw an error on model construction. @nickhaf is there actually a use case for labeled & fixed parameters?

nickhaf commented 2 months ago

I played around with a "relaxed lasso" implementation when I noticed this issue. For this very niche application it might be helpful to be able to label and fix parameters, but even here it is definitely not necessary. So in my opinion throwing a clear error message would be enough for now.

Maximilian-Stefan-Ernst commented 1 month ago

This should be fixed in the next release. Thanks again for reporting.