Skip to content

Commit 0db01c7

Browse files
yrosseelclaude
andcommitted
correlation structures: composites support (ML and GLS routes)
Composite models (the <~ operator) now work with correlation = TRUE, on both the D-augmented ML route (the default) and the GLS route. Root cause of the old failure (the GLS combination never converged; the ML route stopped with a clean error): the correlation completion ran BEFORE lav_lisrel_comp_set_intresvar(), so the completed residual variances were computed from stale composite quantities, and the implied diagonal drifted away from the unit target during optimization (objective/gradient mismatch). The composite completion now runs FIRST in lav_model_set_parameters() and lav_model_x2glist(); plain composite models are unaffected (nothing runs between the two blocks when no correlation structure is present). Two more fixes in the same setting: - lav_pt_ndat(): the fixed composite-indicator VARIANCES were removed from the statistic count on top of the correlation correction (fixed ~*~ rows) or the free ~*~ scales that fit them -- the df now match an ordinary composite ML fit (which has the same chi-square). - lav_lisrel_dimplied_dx(): in the D-augmented mode the composite psi* chain-rule block must not reintroduce diagonal-row entries for the non-delta columns (the unit-diagonal completion makes diag(Sigma*) constant); the analytic gradient again matches numDeriv at and off the solution. missing = "ml" works for composite correlation models as well (same likelihood as an ordinary composite FIML fit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 453f781 commit 0db01c7

6 files changed

Lines changed: 76 additions & 58 deletions

File tree

R/lav_lavaan_step02_options.R

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,6 @@ lav_step02_options <- function(slot_options = NULL,
262262
opt$meanstructure <- TRUE
263263
}
264264

265-
# composites marker: the D-augmented ML correlation mode has no
266-
# composite support (yet); lav_options_set() keeps the (defaulted)
267-
# estimator at GLS in that case
268-
opt$.flat.composites <- any(flat_model$op == "<~")
269-
270265
# conditional.x
271266
if ((is.list(ov_names_x) &&
272267
sum(sapply(ov_names_x, FUN = length)) == 0L) ||
@@ -324,16 +319,6 @@ lav_step02_options <- function(slot_options = NULL,
324319
\"delta\" parameterization."))
325320
}
326321

327-
# D-augmented ML mode (correlation + ML-family estimator): no
328-
# composites support (the composite completion and the unit-diagonal
329-
# completion both reparameterize the same matrices)
330-
if (isTRUE(lavoptions$.correlation.ml) && any(flat_model$op == "<~")) {
331-
lav_msg_stop(gettext(
332-
"estimator = \"ML\" with correlation structures is not supported
333-
(yet) for models with composites (the \"<~\" operator); use
334-
estimator = \"GLS\"."))
335-
}
336-
337322
# correlation = TRUE with observed product (interaction) terms: the
338323
# product of standardized variables does not have unit variance, so the
339324
# product terms must keep a free variance (github issue #427). We turn

R/lav_model_utils.R

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ lav_model_set_parameters <- function(lavmodel = NULL, x = NULL) {
100100
# parameters): the completion then targets diag(Sigma*) = 1
101101
correlation_ml <- correlation && lav_model_delta_free(lavmodel)
102102

103+
# composites first: the derived composite quantities (variances,
104+
# intercepts) feed the correlation completion below
105+
if (lavmodel@composites) {
106+
# for package stdmod only! (vignette stdmod_lavaan uses old fit object)
107+
# if (.hasSlot(lavmodel, "composites") && lavmodel@composites) {
108+
nmat <- lavmodel@nmat
109+
if (lavmodel@representation == "LISREL") {
110+
for (g in 1:lavmodel@nblocks) {
111+
# which mm belong to group g?
112+
mm_in_group <- 1:nmat[g] + cumsum(c(0L, nmat))[g]
113+
114+
tmp[mm_in_group] <-
115+
lav_lisrel_comp_set_intresvar(mlist = tmp[mm_in_group])
116+
}
117+
} else {
118+
cat("FIXME: deal with Composites if representation = RAM")
119+
}
120+
}
121+
103122
# categorical? set categorical theta elements (if any)
104123
if (lavmodel@categorical || correlation) {
105124
nmat <- lavmodel@nmat
@@ -140,23 +159,6 @@ lav_model_set_parameters <- function(lavmodel = NULL, x = NULL) {
140159
}
141160
}
142161

143-
if (lavmodel@composites) {
144-
# for package stdmod only! (vignette stdmod_lavaan uses old fit object)
145-
# if (.hasSlot(lavmodel, "composites") && lavmodel@composites) {
146-
nmat <- lavmodel@nmat
147-
if (lavmodel@representation == "LISREL") {
148-
for (g in 1:lavmodel@nblocks) {
149-
# which mm belong to group g?
150-
mm_in_group <- 1:nmat[g] + cumsum(c(0L, nmat))[g]
151-
152-
tmp[mm_in_group] <-
153-
lav_lisrel_comp_set_intresvar(mlist = tmp[mm_in_group])
154-
}
155-
} else {
156-
cat("FIXME: deal with Composites if representation = RAM")
157-
}
158-
}
159-
160162
lavmodel@GLIST <- tmp
161163

162164
lavmodel
@@ -219,6 +221,23 @@ lav_model_x2glist <- function(lavmodel = NULL, x = NULL,
219221
# }
220222
# }
221223

224+
# composites first: the derived composite quantities feed the
225+
# correlation completion below
226+
if (lavmodel@composites) {
227+
nmat <- lavmodel@nmat
228+
if (lavmodel@representation == "LISREL") {
229+
for (g in 1:lavmodel@nblocks) {
230+
# which mm belong to group g?
231+
mm_in_group <- 1:nmat[g] + cumsum(c(0L, nmat))[g]
232+
233+
glist[mm_in_group] <-
234+
lav_lisrel_comp_set_intresvar(mlist = glist[mm_in_group])
235+
}
236+
} else {
237+
cat("FIXME: deal with Composites when representation = RAM")
238+
}
239+
}
240+
222241
# in 0.6-13: we always set theta/delta
223242
if ((lavmodel@categorical || correlation) && set_delta) {
224243
nmat <- lavmodel@nmat
@@ -250,21 +269,6 @@ lav_model_x2glist <- function(lavmodel = NULL, x = NULL,
250269
}
251270
}
252271

253-
if (lavmodel@composites) {
254-
nmat <- lavmodel@nmat
255-
if (lavmodel@representation == "LISREL") {
256-
for (g in 1:lavmodel@nblocks) {
257-
# which mm belong to group g?
258-
mm_in_group <- 1:nmat[g] + cumsum(c(0L, nmat))[g]
259-
260-
glist[mm_in_group] <-
261-
lav_lisrel_comp_set_intresvar(mlist = glist[mm_in_group])
262-
}
263-
} else {
264-
cat("FIXME: deal with Composites when representation = RAM")
265-
}
266-
}
267-
268272
glist
269273
}
270274

R/lav_options.R

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,7 +1653,7 @@ lav_options_set <- function(opt = NULL) {
16531653
# do not depend on the scaling of the input). Since 0.7-1 this is
16541654
# also the DEFAULT route: when the estimator is left unspecified,
16551655
# we only fall back to GLS (quietly) for settings the
1656-
# D-augmentation does not support (yet): composites.
1656+
# D-augmentation does not support.
16571657
# missing = "ml" (FIML) is supported on the ML route (since 0.7-2):
16581658
# the casewise/pattern likelihood simply consumes the D-augmented
16591659
# implied moments Sigma = Delta P(theta) Delta and Mu.
@@ -1664,10 +1664,6 @@ lav_options_set <- function(opt = NULL) {
16641664
correlation_ml_ok <-
16651665
(!opt$conditional.x && any(opt$missing == c("listwise", "ml"))) ||
16661666
(opt$conditional.x && opt$missing == "listwise")
1667-
if (estimator_default) {
1668-
correlation_ml_ok <- correlation_ml_ok &&
1669-
!isTRUE(opt$.flat.composites)
1670-
}
16711667
if (lav_options_estimatorgroup(opt$estimator) == "ML" &&
16721668
correlation_ml_ok) {
16731669
opt$.correlation.ml <- TRUE

R/lav_partable_utils.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,21 @@ lav_pt_ndat <- function(partable) {
184184
partable$lhs %in% ov_cind &
185185
partable$rhs %in% ov_cind &
186186
partable$free == 0L)
187+
# correlation structures: the indicator VARIANCE moments are
188+
# already accounted for -- a FIXED ~*~ row subtracts them via the
189+
# correlation correction below, a FREE ~*~ row (D-augmented ML)
190+
# fits them -- so only the fixed indicator COVARIANCES remove a
191+
# sample statistic here
192+
if (correlation && !categorical && length(covar_idx) > 0L) {
193+
scaled_ov <- partable$lhs[partable$op == "~*~" &
194+
partable$block == b]
195+
drop_idx <- which(partable$lhs[covar_idx] ==
196+
partable$rhs[covar_idx] &
197+
partable$lhs[covar_idx] %in% scaled_ov)
198+
if (length(drop_idx) > 0L) {
199+
covar_idx <- covar_idx[-drop_idx]
200+
}
201+
}
187202
ndat[b] <- ndat[b] - length(covar_idx)
188203
}
189204

R/lav_representation_lisrel.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4199,6 +4199,21 @@ lav_lisrel_dimplied_dx <- function(mlist = NULL,
41994199
sigma_chain <- sigma_chain * delta_weight
42004200
}
42014201

4202+
# D-augmented ML mode (FREE ~*~ scales, full vech layout): the
4203+
# unit-diagonal completion makes diag(Sigma*) constant, so the
4204+
# composite chain must not reintroduce diagonal-row entries for the
4205+
# non-delta columns (the num_idx variables keep a free variance)
4206+
if (!categorical && correlation && n_del > 0L) {
4207+
diag_pos_c <- lav_mat_diagh_idx(nvar)
4208+
if (length(num_idx) > 0L) {
4209+
diag_pos_c <- diag_pos_c[-num_idx]
4210+
}
4211+
nondelta_cols <- setdiff(seq_len(ncol(sigma_chain)), x_delta_idx)
4212+
if (length(diag_pos_c) > 0L && length(nondelta_cols) > 0L) {
4213+
sigma_chain[diag_pos_c, nondelta_cols] <- 0
4214+
}
4215+
}
4216+
42024217
# categorical / correlation layouts subset and reorder the sigma rows;
42034218
# bring the (full-vech) chain term into the same layout
42044219
if (!is.null(sigma_row_map)) {

man/lavOptions.Rd

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,15 @@ Model features:
111111
scales are free \code{~*~} parameters, and the unit-variance
112112
constraint applies to the marginal variances; the result is
113113
identical (same conditional likelihood) to the joint
114-
\code{fixed.x = TRUE} analysis. The ML route requires
115-
single-level data and no composites (and \code{missing = "ml"}
116-
requires \code{conditional.x = FALSE}); in all other settings, a
117-
defaulted estimator quietly falls back to GLS. Estimator WLS (=
118-
ADF weights in the correlation metric) covers the non-normal
119-
case.
114+
\code{fixed.x = TRUE} analysis. Composite models (the \code{<~}
115+
operator) are supported as well (since 0.7-2, on both the ML and
116+
the GLS route): the composite-indicator covariances are fixed in
117+
the correlation metric, and the indicator variances are absorbed
118+
by the \code{~*~} scales. The ML route requires single-level data
119+
(and \code{missing = "ml"} requires \code{conditional.x =
120+
FALSE}); in all other settings, a defaulted estimator quietly
121+
falls back to GLS. Estimator WLS (= ADF weights in the
122+
correlation metric) covers the non-normal case.
120123
Both
121124
\code{fixed.x = FALSE} and \code{fixed.x = TRUE} are supported (the
122125
latter affects the way the standard errors are computed), and (since

0 commit comments

Comments
 (0)