######################################################################
## Copyright 2017 Carl F. Falk and Katerina M. Marcoulides
##
## This program is free software: you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation, either version 2 of
## the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
##
# Given a fitted lavaan model (e.g., CFA), prepares a table that contains parameters
# that can be fixed/freed as part of a model specification search.
#
# fitted.model - an object of class "lavaan" that contains the initially fitted model for the search
# loadings - a logical value that indicates whether cross-loadings will be part of the search
# fcov - a logical value indicating whether factor covariances will be part of the search
# errors - a logical value indicating whether error covariances will be part of the search
search.prep<-function(fitted.model,loadings=TRUE,fcov=TRUE,errors=FALSE){
# Parameter table from fitted model
ptab<-partable(fitted.model)
# Full parameter table
fulltab<-lavaan:::lav_partable_full(ptab)
# Merge together so we have what's free
mergetab<-lavaan:::lav_partable_merge(ptab,fulltab, remove.duplicated=TRUE, warn=FALSE)
idx<-vector("numeric")
null.val<-vector("numeric")
lv.names<-lavaan:::vnames(ptab,type="lv") # extract names of latent vars
ov.names<-lavaan:::vnames(ptab,type="ov.nox") # indicators
# Loadings
if(loadings){
# Index for which rows of table are loadings
indx<-mergetab$op=="=~"
# Omit any loadings that are fixed to a non-zero value
# These may be fixed for identification
indx[mergetab$free[indx]==0&mergetab$ustart[indx]!=0]<-FALSE
idx<-c(idx,which(indx))
null.val<-c(null.val,rep(0,sum(indx)))
}
# Factor cov
if(fcov){
# Index for which rows are cov among latent vars
indx<-mergetab$op=="~~"&mergetab$lhs%in%lv.names&mergetab$lhs!=mergetab$rhs
idx<-c(idx,which(indx))
null.val<-c(null.val,rep(0,sum(indx)))
}
# Error covariances among indicators
if(errors){
# Index for which rows are cov among indicators
indx<-mergetab$op=="~~"&mergetab$lhs%in%ov.names&mergetab$lhs!=mergetab$rhs
idx<-c(idx,which(indx))
null.val<-c(null.val,rep(0,sum(indx)))
}
# Check whether any parameters in the table are involved in any constraints
flag<-check.const(fitted.model,mergetab[idx,])
if(flag){
warning("Some parameters in this table may be involved in (in)equality constraints")
}
# Clean up
mergetab<-mergetab[idx,c("lhs","op","rhs","block","free","label")]
mergetab$nullval<-null.val
mergetab$free<-ifelse(mergetab$free==0,0,1)
# Sort table
lvnames<-lavNames(mergetab,"lv")
ovnames<-lavNames(mergetab,"ov")
mergetab<-mergetab[order(mergetab$block, # by group (or block)
is.na(match(mergetab$lhs,c(lvnames))), # put latent vars first
mergetab$op, # by op w/in lv and ov sections
match(mergetab$lhs,c(lvnames,ovnames)), # then by lv and ov names on lhs
match(mergetab$rhs,c(lvnames,ovnames))),] # then by same on rhs
rownames(mergetab)<-1:nrow(mergetab)
ret<-mergetab
return(ret)
}
# Given a fitted lavaan model and a search table, checks whether any of the
# parameters in the search table are involved in (in)equality constraints
#
# fitted.model - fitted lavaan model
# pars - specification table (e.g., prepared by search.prep and optionally further modified by user)
check.const<-function(fitted.model,pars){
ptab<-parTable(fitted.model)
const<-ptab[ptab$op=="=="|ptab$op==">"|ptab$op=="<",]
if(nrow(const)>0){
plabels<-c(pars$label,pars$plabel)
plabels<-plabels[plabels!=""]
flag<-FALSE
for(j in 1:nrow(const)){
var.lhs<-all.vars(parse(file="", text=const$lhs[j]))
var.rhs<-all.vars(parse(file="", text=const$rhs[j]))
if(any(plabels %in% c(var.lhs,var.rhs) ))
flag<-TRUE
}
} else {
flag<-FALSE
}
return(flag)
}
# Adds a parameter to the given search table. Checks whether parameter
# is involved in any (in)equality constraints in a fitted lavaan model
#
# fitted.model - fitted lavaan model
# ptab - search table
# syntax - model.syntax specifying the parameter to add to the current table
# nullval - optional numeric value specifying what the parameter should be fixed to (when fixed)
# free - optional logical value specifying whether the parameter should initially be set free (or not)
# block - optional numeric value specifying the group number to which the parameter corresponds
add.param<-function(fitted.model, ptab, syntax, nullval=NULL, free=NULL, block=NULL) {
newpar<-lavaanify(syntax)
if(!is.null(block)){
newpar$block<-block
}
if(!is.null(free)){
newpar$free<-ifelse(free,1,0)
}
if(!is.null(nullval)){
newpar$nullval<-nullval
} else {
newpar$nullval<-newpar$ustart
}
newpar<-newpar[,c("lhs","op","rhs","block","free","label","nullval")]
# Check whether newpar includes more than one row
if(nrow(newpar)>1){
newpar<-newpar[1,]
warning("lavaanify() results in more than one row of parameters, adding only the first row")
}
# Check whether parameter already exists in table
matches<-par.matches(ptab,paste(newpar$lhs,newpar$op,newpar$rhs,sep=""),block=block)
if(any(matches)){
stop("Parameter already found in table")
}
# Check to see if parameter is part of constraint in existing model
flag<-check.const(fitted.model, newpar)
if(flag){
warning("Some parameters added to this table may be involved in (in)equality constraints")
}
# Add newpar to existing table
ptab<-rbind(ptab,newpar)
return(ptab)
}
# Internal function used to match parameters in label against
# those in a search table (ptab)
par.matches<-function(ptab,label,block=NULL){
# Collapse whitespace in label (if any)
label<-gsub("[[:space:]]","", label)
# Look through lhsoprhs
lhsoprhs<-paste(ptab$lhs,ptab$op,ptab$rhs,sep="")
# Look through labels
matches<-label==lhsoprhs | label==ptab$label
# Check against block
if(!is.null(block)){
matches <- matches & ptab$block==block
}
return(matches)
}
# Internal function that modifies search table
manip.ptab<-function(ptab,label,task,nullval=NULL,block=NULL){
if(!task %in% c("remove","free","fix","nullval")){
stop()
}
matches<-par.matches(ptab,label,block=block)
nmatches<-sum(matches)
if(nmatches>1){
warning("More than one match for parameter found")
} else if (nmatches<1){
warning("No matches for parameter found")
}
if(task=="remove"){
# Remove from table
ptab<-ptab[!matches,]
} else if (task=="free"){
# Set free
ptab$free[matches]<-1
} else if (task=="fix"){
# Fix
ptab$free[matches]<-0
if(!is.null(nullval)){
ptab$nullval[matches]<-nullval
}
} else if (task=="nullval"){
# Change nullval
ptab$nullval[matches]<-nullval
}
return(ptab)
}
# Given a search table and a label, sets the parameter free in the search table
#
# ptab - search table
# label - combination of lhs, op, and rhs as would appear in lavaan model.syntax
# or a parameter label from the label column of a lavaan parameter table
# block - optional numeric value specifying the group number to which the parameter corresponds
free.param<-function(ptab, label, block=NULL){
ptab<-manip.ptab(ptab,label,"free",block=block)
return(ptab)
}
# Given a search table and a label, sets the parameter fixed in the search table
#
# ptab - search table
# label - combination of lhs, op, and rhs as would appear in lavaan model.syntax
# or a parameter label from the label column of a lavaan parameter table
# nullval - optional numeric value specifying what the parameter should be set to when fixed (existing value in ptab is otherwise used)
# block - optional numeric value specifying the group number to which the parameter corresponds
fix.param<-function(ptab, label, nullval=NULL, block=NULL){
ptab<-manip.ptab(ptab,label,"fix",nullval=nullval,block=block)
return(ptab)
}
# Given a search table and a label, removes the parameter from the search table
#
# ptab - search table
# label - combination of lhs, op, and rhs as would appear in lavaan model.syntax
# or a parameter label from the label column of a lavaan parameter table
# block - optional numeric value specifying the group number to which the parameter corresponds
rm.param<-function(ptab, label, block=NULL){
ptab<-manip.ptab(ptab,label,"remove",block=block)
return(ptab)
}
# Given a search table and a label, sets the value that the parameter would be
# fixed to if the parameter is fixed
#
# ptab - search table
# label - combination of lhs, op, and rhs as would appear in lavaan model.syntax
# or a parameter label from the label column of a lavaan parameter table
# nullval - numeric value specifying what the parameter should be set to when fixed
# block - optional numeric value specifying the group number to which the parameter corresponds
nullval.param<-function(ptab, label, nullval, block=NULL){
ptab<-manip.ptab(ptab,label,"nullval",nullval=nullval,block=block)
return(ptab)
}
# Given a fitted lavaan model and a search table, refits the model using the search
# table as specifying what changes should be done (parmeters fixed/freed)
#
# fitted.model - fitted model of class lavaan
# ptab - search table
refit.model<-function(fitted.model, ptab){
# Extract parameter table
tab<-parTable(fitted.model)
# Expand to get full table
fulltab<-lavaan:::lav_partable_full(tab)
mergetab<-lavaan:::lav_partable_merge(tab,fulltab, remove.duplicated=TRUE, warn=FALSE)
# Modify parameter table based on new input matrix
# Obtain indices for matching
mergetab$labels<-paste(mergetab$lhs,mergetab$op,mergetab$rhs,sep="")
ptab$labels<-paste(ptab$lhs,ptab$op,ptab$rhs,sep="")
midx<-vector("numeric")
nidx<-vector("numeric")
for(j in 1:nrow(ptab)){
idx<-which(ptab$labels[j]==mergetab$labels & ptab$block[j]==mergetab$block)
if(length(idx)>0){
midx<-c(midx,idx)
nidx<-c(nidx,j)
}
}
# Replace free parameters
mergetab$free[midx]<-ptab$free[nidx]
# If some parameters are now fixed; what are they fixed to?<-function
mergetab$ustart[mergetab$free!=0]<-NA
mergetab[midx,]$ustart[mergetab[midx,]$free==0]<-ptab[nidx,]$nullval[ptab[nidx,]$free==0]
# Get rid of old parameter estimates and starting values
mergetab$est<-NULL
mergetab$se<-NULL
mergetab$start<-NULL
mergetab$labels<-NULL
# Re-order values in mergetab
lvnames<-lavNames(mergetab,"lv")
ovnames<-lavNames(mergetab,"ov")
mergetab<-mergetab[order(mergetab$block, # by group (or block)
is.na(match(mergetab$lhs,c(lvnames))), # put latent vars first
mergetab$op, # by op w/in lv and ov sections
match(mergetab$lhs,c(lvnames,ovnames)), # then by lv and ov names on lhs
match(mergetab$rhs,c(lvnames,ovnames))),] # then by same on rhs
# Refit model
prevmodel<-as.list(fitted.model@call)
prevmodel$model<-mergetab
newmod<-try(do.call("lavaan",prevmodel[-1]),silent=TRUE)
return(newmod)
}
# Given a fitted lavaan model, a search table, and an objective criterion,
# performs a Tabu model specification search. Currently only supports
# neighbors that are 1 move away from the current model.
#
# fitted.model - fitted model of class lavaan
# ptab - search table (e.g., created by search.prep) that lists candidate parameters
# that can be modified as part of the search and how the parameters can be modified (fixed to what values)
# obj - objective function to be MINIMIZED. Any function that takes a lavaan object as the sole argument and returns
# a numeric value can be used.
# niter - number of Tabu iterations to perform
# tabu.size - size of Tabu list
tabu.sem<-function(init.model,ptab,obj,niter=30,tabu.size=5){
# Initialize objective function and best model
best.obj<-current.obj<-obj(init.model)
best.model<-current.model<-init.model
best.binvec<-current.binvec<-ptab
tabu.list<-vector("numeric")
# Do iterations
for(it in 1:niter){
# Loop through all neighbors
tmp.obj<-vector("numeric")
tmp.mod<-list()
tmp.vec<-list()
for(j in 1:nrow(current.binvec)){
tmp.binvec<-current.binvec
bin<- 1-tmp.binvec$free[j]
tmp.binvec$free[j]<-bin
fitmodel<-refit.model(init.model,tmp.binvec)
if(fitmodel@Fit@converged&!any(is.na(fitmodel@Fit@se))){
fit.val<-obj(fitmodel)
} else {
fit.val<-NA
}
tmp.obj<-c(tmp.obj,fit.val)
tmp.mod[[j]]<-fitmodel
tmp.vec[[j]]<-tmp.binvec
}
# Check which indices result in a valid objective function
valid<-which(!is.na(tmp.obj))
# Get just models not on Tabu list
valid<-valid[!(valid %in% tabu.list)]
# Out of valid models, pick model with best objective function value
indx<-which.min(tmp.obj[valid])
# Move current state to next model
current.obj<-(tmp.obj[valid])[indx]
current.mod<-(tmp.mod[valid])[[indx]]
current.binvec<-(tmp.vec[valid])[[indx]]
# Update Tabu list
tabu.list<-c(valid[indx],tabu.list)
if(length(tabu.list)>tabu.size){
tabu.list<-tabu.list[1:tabu.size]
}
# Update if the current model is better than the best model
if(current.obj