Performs penalized estimation on a lavaan model object by optimizing a penalized objective function. The function extracts the objective function from a lavaan model, applies a penalty function to specified parameters or pairwise differences of parameters, and returns an updated model with the optimized parameter estimates.
Arguments
- x
A fitted lavaan model object from which estimation components will be extracted.
- w
Numeric scalar. Penalty weight (multiplier) applied to the penalty terms.
- pen_par_id
Integer vector of parameter IDs to apply the penalty function directly to, in the same order as returned by
lavaan::coef()and bylavaan::parTable(), with only the free elements.- pen_diff_id
A named list of integer matrices of free-parameter IDs (same order as
lavaan::coef()/ thefreecolumn oflavaan::parTable()). Each matrix has one row per group or time point and one column per matched parameter; the penalty is the sum of pairwise row differences within each column, rescaled by(nrow - 1) / ncombn(nrow, 2). StructuralNAentries mark a parameter absent in that row and are excluded from the differences.- pen_fn
A character string (
"l0a"or"alf") or a function that computes the penalty. Default is"l0a".- pen_gr
A function that computes the gradient of the penalty function. If
pen_fnis"l0a"or"alf", this is automatically set.- se
Character string specifying the type of standard errors to compute. Options are
"none"(default; no standard errors) or"robust.huber.white"(robust sandwich estimator using numerical Hessian and first-order information, which is the same as used in the"mlr"estimator).- test
Character string specifying the model test used by the fit evaluation on the returned object (
fitmeasures()and the chi-square test insummary()), via an internal "frozen" refit. Fit evaluation for penalized fits is experimental, so it is disabled by default:"none"(default) means no model test is run,fitmeasures()is unavailable, andsummary()shows no chi-square test. Set to"Chisq"(ML/PML estimators) or"SatorraBentler"(WLSMV/MLM/MLR) to enable fit measures and the chi-square test; an experimental notice is then shown whenfitmeasures()orsummary()is called.- opt_control
A list of control parameters passed to
stats::nlminb(). Default includeseval.max = 2e4,iter.max = 1e4, andabs.tol = 1e-20.- start
Numeric vector of starting values for the optimizer, or
NULL(default) to use lavaan's default starting values. If supplied, its length must match the number of free parameters in the model.- eps
A positive numeric scalar used by the built-in penalties, or
"telescoping"to fit a sequence of decreasing epsilon values. Default is.01. This argument does not alter custompen_fnorpen_grfunctions.- telescoping_control
A named list controlling telescoping, with
eps_1(default1),eps_end(default1e-5),eps_steps(default20), andwarm_start(defaultFALSE). Whenwarm_startisFALSE, every epsilon stage uses the original starting values; whenTRUE, each stage after the first uses the preceding stage's estimates.- ...
Additional arguments passed to a user-supplied
pen_fn/pen_gr. Custom penalty functions must accept.... Built-in penalties ("l0a","alf") ignore it.
Value
A lavaan model object updated with the penalized parameter estimates.
The object has S4 class plavaan (a subclass of lavaan) and a
penalized attribute recording the penalty specification, which enables
effective_df() and, when test is not "none", fitmeasures() and
summary() with effective degrees of freedom. With eps = "telescoping",
it also includes a "telescoping" data frame with per-stage epsilon
values, parameter changes, objective values, and convergence indicators.
Details
The function uses nlminb() to minimize a penalized objective function that
combines the standard lavaan objective function with a penalty term. Only the
parameter estimates and the log-likelihood should be interpreted. The
returned object was not "fitted" (do.fit = FALSE) to avoid users
interpreting the standard errors, which are generally not valid with
penalized estimation. The nominal model degrees of freedom can also be
misleading, as the penalized model is often under-identified;
effective_df() reports the effective number of parameters and the
effective model degrees of freedom. When test is not "none",
fitmeasures() / summary() on the returned object additionally report
fit indices at the effective df (frozen refit at the penalized estimates);
this fit evaluation is experimental and disabled by default. If the
optimization does not converge (convergence code != 0), a warning is issued.
With eps = "telescoping", the model is fit along a log-spaced sequence from
telescoping_control$eps_1 to telescoping_control$eps_end. By default,
each stage uses the original starting values; set
telescoping_control$warm_start = TRUE to initialize later stages from the
preceding solution. The sequence stops when the largest absolute change
between consecutive parameter vectors is at most 5e-4. The returned object
has a "telescoping" attribute with stage diagnostics.
Warning
The returned object is not fitted using standard ML. Standard errors reported
by summary() or parameterEstimates() will be missing unless
se = "robust.huber.white" was specified. Even then, they are based on an
experimental sandwich approximation and should be interpreted with caution.
Fit evaluation (fitmeasures() and the chi-square test in summary()) is
also experimental and disabled by default (test = "none"). Enable it
with test = "Chisq" (or "SatorraBentler"); interpret any resulting fit
indices with caution, as they are based on a frozen refit at the penalized
estimates with the effective degrees of freedom.
Examples
library(lavaan)
# Define a longitudinal factor model with PoliticalDemocracy data
model <- "
dem60 =~ y1 + y2 + y3 + y4
dem65 =~ y5 + y6 + y7 + y8
dem60 ~~ dem65
dem60 ~~ 1 * dem60
dem65 ~~ NA * dem65
dem60 ~ 0
dem65 ~ NA * 1
y1 ~~ y5
y2 ~~ y6
y3 ~~ y7
y4 ~~ y8
"
# Fit the model without constraints first to get parameter table
fit_un <- cfa(model, data = PoliticalDemocracy, std.lv = TRUE,
meanstructure = TRUE, do.fit = FALSE)
# Get parameter IDs
pt <- parTable(fit_un)
# Loadings
load_60 <- pt$free[pt$op == "=~" & pt$lhs == "dem60"]
load_65 <- pt$free[pt$op == "=~" & pt$lhs == "dem65"]
# Intercepts
int_60 <- pt$free[pt$op == "~1" & pt$lhs %in% c("y1", "y2", "y3", "y4")]
int_65 <- pt$free[pt$op == "~1" & pt$lhs %in% c("y5", "y6", "y7", "y8")]
# Apply penalized estimation to penalize differences in loadings and intercepts
pen_fit <- penalized_est(
x = fit_un,
w = 0.03,
pen_diff_id = list(
loadings = rbind(load_60, load_65),
intercepts = rbind(int_60, int_65)
),
pen_fn = "l0a"
)
# Compare parameter estimates
summary(pen_fit)
#> Penalized fit (w = 0.03, eps = 0.01, penalty = l0a): effective npar = 24, effective df = 20 (nominal df = 13).
#> lavaan 0.7-2 ended normally after 129 iterations
#>
#> Estimator ML
#> Optimization method NLMINB
#> Number of model parameters 24
#>
#> Number of observations 75
#>
#>
#> Parameter Estimates:
#>
#>
#> Latent Variables:
#> Estimate
#> dem60 =~
#> y1 2.098
#> y2 2.830
#> y3 2.561
#> y4 2.901
#> dem65 =~
#> y5 2.092
#> y6 2.825
#> y7 2.572
#> y8 2.900
#>
#> Covariances:
#> Estimate
#> dem60 ~~
#> dem65 0.917
#> .y1 ~~
#> .y5 0.838
#> .y2 ~~
#> .y6 1.825
#> .y3 ~~
#> .y7 1.205
#> .y4 ~~
#> .y8 0.286
#>
#> Intercepts:
#> Estimate
#> dem60 0.000
#> dem65 -0.147
#> .y1 5.457
#> .y2 4.252
#> .y3 6.570
#> .y4 4.461
#> .y5 5.456
#> .y6 3.396
#> .y7 6.570
#> .y8 4.462
#>
#> Variances:
#> Estimate
#> dem60 1.000
#> dem65 0.950
#> .y1 2.128
#> .y2 6.658
#> .y3 5.385
#> .y4 2.600
#> .y5 2.813
#> .y6 4.000
#> .y7 3.615
#> .y8 2.456
#>
# Effective number of parameters and degrees of freedom
effective_df(pen_fit)
#> npar npar_effective df_saved
#> loadings 8 4.016903 3.983097
#> intercepts 8 4.986691 3.013309
#> TOTAL 31 24.003594 6.996406
#>
#> n_stats (sample moments): 44
#> nominal model df: 13
#> effective model df: 20
#> penalty: l0a (w = 0.03, eps = 0.01)