Title: | Multiple Group Item Response Theory Alignment Helpers for 'lavaan' and 'mirt' |
---|---|
Description: | Allows for multiple group item response theory alignment a la 'Mplus' to be applied to lists of single-group models estimated in 'lavaan' or 'mirt'. Allows item sets that are overlapping but not identical, facilitating alignment in secondary data analysis where not all items may be shared across assessments. |
Authors: | Maxwell Mansolf [aut, cre] |
Maintainer: | Maxwell Mansolf <[email protected]> |
License: | MIT + file LICENSE |
Version: | 0.1.0.0 |
Built: | 2024-11-04 04:49:36 UTC |
Source: | https://github.com/mmansolf/alignlv |
Not generally intended to be used on its own, but exported anyway for didactic purposes.
align.optim( stacked, n, estimator, nstarts = 50, ncores = 3, hyper.first, center.means, eps.alignment, clf.ignore.quantile, verbose )
align.optim( stacked, n, estimator, nstarts = 50, ncores = 3, hyper.first, center.means, eps.alignment, clf.ignore.quantile, verbose )
stacked |
Stacked parameter estimates from |
n |
Sample size in each group |
estimator |
See |
nstarts |
Number of starting values for alignment; default is 10 |
ncores |
See |
hyper.first |
See |
center.means |
See |
eps.alignment |
See |
clf.ignore.quantile |
See |
verbose |
See |
See example for Alignment
for examples
A list
of results from multiple runs of the alignment optimizer:
mv
Means and variances from each alignment run.
parout
A table of outputs from link[stats]{optim}
containing the
function values, convergence information, and resulting estimates of means
and variances from each run.
nFailedRuns
The number of runs that failed to complete. An error is
returned if no runs fail.
mirt
or lavaan
Performs alignment (https://www.statmodel.com/Alignment.shtml) using single-group models estimated in mirt or lavaan.
Alignment( fitList, estimator, SE = FALSE, eps.alignment = 0.01, clf.ignore.quantile = 0.1, bifactor.marginal = FALSE, hyper.first = "variances", center.means = TRUE, nstarts = 10, ncores = 1, verbose = TRUE )
Alignment( fitList, estimator, SE = FALSE, eps.alignment = 0.01, clf.ignore.quantile = 0.1, bifactor.marginal = FALSE, hyper.first = "variances", center.means = TRUE, nstarts = 10, ncores = 1, verbose = TRUE )
fitList |
A |
estimator |
The model type used, either |
SE |
Whether to also return standard errors from parameter estimates
after alignment. SE's are transformed using the delta method from those
provided in the original model objects, which must (for |
eps.alignment |
A numeric scalar for the alignment simplicity function, given by (Asparouhov & Muthén, 2014, Structural Equation Modeling):
where $x$ is the difference between corresponding estimates in each pair of aligned models. Lower values may cause numerical instability; default 0.01 |
clf.ignore.quantile |
Another protection from numerical instability;
CLF values less than the |
bifactor.marginal |
A logical scalar indicating whether, for bifactor models, alignment should take place on the marginal, rather than conditional, metric for slopes (Ip, 2010, Applied Psychological Measurement). |
hyper.first |
A string scalar denoting which hyperparameter to align
first. Asparouhov & Muthén (2014) align all parameters simultaneously
( |
center.means |
A logical scalar. Alignment fixes the first group's mean
to zero to estimate the others. If |
nstarts |
Number of starting values for alignment; default is 10 |
ncores |
Number of processor cores to distribute alignment starts
across; on systems that support multicore processing, using additional cores
can speed up the alignment step by roughly a factor of the number of cores.
Defaults to 1 for no parallel processing. Requires the |
verbose |
Whether stuff gets printed to the console. May help with debugging. |
Currently, no automated process provides statistical tests for DIF.
Instead, I recommend interpreting the DIF impact directly by comparing
scores obtained from a single-group model combining all groups, and the
multiple models produced by Alignment
. If standard errors
are requested from getEstimates.mirt
, or
getEstimates.lavaan
, and then the corresponding
transformEstimates.mirt.grm
or
transformEstimates.lavaan.ordered
is applied, SE's after
alignment can be
obtained and used for multiple comparison testing, but this is not yet
automated. Alternatively, consider re-fitting models with means and
variances fixed to those obtained from alignment to obtain these standard
errors. In the latter case, especially when priors are used as in
mirt
, your estimates may not match those from Alignment
exactly.
For lavaan
, the metric for alignment must be the "theta"
parameterization, which is not the default, in order to properly search for
latent means and variances, because only then do the transformations apply.
My current thinking: under the delta parameterization, the transformed
estimates (calculate delta, incorporate it into parameters, then
transform parameters, BUT don't reverse the delta transformation) do NOT
yield an equivalent model, but DO yield a model that can be compared
across groups. In order to get an equivalent model, you also need to
reverse the delta transformation at the end. To account for this, if
the the extra argument toCompare should be turned on TRUE
if
transformed parameters are to be compared for equivalence across groups.
Turning it off results in NOT applying the reverse of the delta
transformation at the end. This currently is fixed to TRUE and cannot
be modified, but you can access
transformEstimates.lavaan.ordered
directly if you want to play around.
If parallel==TRUE
, a parallel backend with the doParallel
package
leverages multi-core processing if the number of cores specified in
ncores
is greater than one. Uses %dorng%
to pass the
R session's seed to the alignment optimizer, such that you can replicate
random starts with set.seed
(see example).
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
A list
with the following elements:
fit
A list
of fitted objects of type mirt
or lavaan
,
depending
on the estimator, where models were re-estimated with means and variances
set to those obtained from alignment.
est.og
A nested list
of parameter estimates and standard errors
provided to
the alignment optimizer from the provided models. Each element corresponds
to a provided model, and each element thereof corresponds to a parameter
name (e.g., a
and d
parameters from mirt.grm
) and contains a matrix of
the corresponding estimates.
est
The estimates from est.og
, transformed after alignment using the
obtained mean and variance estimates therefrom.
hypers
A list
of two-element numeric vectors, where mean
gives the
estimated mean from alignment in the corresponding group, and var
the
estimated variance.
parout
Optimizer output for the alignment step, used to examine
convergence. Contains the following columns:
f
The final complexity function value from alignment.
convergence
The convergence
output from optim
M.2
to M.
(number of groups minus 2) The estimated means from
alignment
V.2
to M.
(number of groups minus 2) The estimated variances from
alignment
#load data library(mirt) library(lavaan) library(purrr) library(tibble) library(magrittr) dat=expand.table(Bock1997) #fit configural models fit.mirt=mirt(dat,1,SE=TRUE) fit.lavaan=cfa(model='G =~ Item.1+Item.2+Item.3',data=dat, ordered=c('Item.1','Item.2','Item.3'), std.lv=TRUE,parameterization='delta') (fit.lavaan@ParTable)%>%tibble::as_tibble()%>%print(n=Inf) #test stuff tab=fit.lavaan@ParTable tab$start[23]=3 tab$est[23]=3 fit.lavaan2=lavaan(tab,data=fit.lavaan@Data) #get estimates est.mirt=getEstimates.mirt(fit.mirt,SE=TRUE,bifactor.marginal=FALSE) est.lavaan=getEstimates.lavaan(fit.lavaan,SE=TRUE) #test transformations newMean=10 newVar=2 test.mirt=transformEstimates.mirt.grm(newMean,newVar,est.mirt) test.lavaan=transformEstimates.lavaan.ordered( newMean,newVar,est.lavaan,toCompare=TRUE) #load and test equivalence tfit.mirt=loadEstimates.mirt.grm(fit.mirt,newMean,newVar,newpars=test.mirt, verbose=TRUE) test.mirt=mirt::coef(fit.mirt) test.mirt tfit.lavaan=loadEstimates.lavaan.ordered( fit.lavaan,newMean,newVar,newpars=test.lavaan, verbose=TRUE) tfit.lavaan@ParTable%>%tibble::as_tibble()%>%print(n=Inf) test.lavaan #now on stacked estimates estList=list(est.mirt%>%purrr::imap(function(x,n){ rownames(x)[2]=paste0(rownames(x)[2],'_ho') if(!n%in%c('a','se.a'))colnames(x)[2]=paste0(colnames(x)[2],'_ho') x }),est.mirt%>%purrr::imap(function(x,n){ rownames(x)[1]=paste0(rownames(x)[1],'_hi') if(!n%in%c('a','se.a'))colnames(x)[1]=paste0(colnames(x)[1],'_hi') x })) stack=stackEstimates(estList) test.stack=transformEstimates.mirt.grm(c(0,0),c(1,1),stack) sf.stack=SF.mplus3D(c(0,1),stack,combn(1:2,2),c(100,200),'mirt.grm', eps.alignment=0.01, clf.ignore.quantile=0.1) test.stack2=transformEstimates.mirt.grm(c(0,1),c(1,1/2),stack) #try align? #lavaan set.seed(0) sim.base=list(simdata(a=as.numeric(est.mirt$a),d=est.mirt$d,N=5000, itemtype='graded',sigma=matrix(1),mu=0), simdata(a=as.numeric(est.mirt$a),d=est.mirt$d,N=5000, itemtype='graded',sigma=matrix(2),mu=1)) fit.base=sim.base%>%purrr::map(~cfa(model="G =~ Item_1 + Item_2 + Item_3", data=as.data.frame(.), ordered=paste0('Item_',1:3),std.lv=TRUE, parameterization='delta')) fit.base%>%purrr::map(lavInspect,'est')%>%purrr::transpose() est.base=purrr::map(fit.base,getEstimates.lavaan,SE=TRUE) #not run: using parallel processes with ncores=3 set.seed(1) # align.stack=align.optim(stackEstimates(est.base),c(100,200),nstarts=3, # hyper.first='variances',ncores=3, # eps.alignment=0.01,clf.ignore.quantile=0.1, # estimator='lavaan.ordered',center.means=FALSE, # verbose=TRUE) # #same seed # set.seed(1) # align.stack=align.optim(stackEstimates(est.base),c(100,200),nstarts=3, # hyper.first='variances',ncores=3, # eps.alignment=0.01,clf.ignore.quantile=0.1, # estimator='lavaan.ordered',center.means=FALSE, # verbose=TRUE) #sequential align.stack=align.optim(stackEstimates(est.base),c(100,200),nstarts=3, hyper.first='variances',ncores=1, eps.alignment=0.01,clf.ignore.quantile=0.1, estimator='lavaan.ordered',center.means=FALSE, verbose=TRUE) align.stack fit.align=Alignment(fit.base,'lavaan.ordered',center.means=FALSE,SE=TRUE, verbose=TRUE) #mirt fit.base2=list() for(i in 1:length(sim.base)){ fit.base2[[i]]=mirt(sim.base[[i]],1,'graded',SE=TRUE) } est.base2=purrr::map(fit.base2,getEstimates.mirt,SE=TRUE, bifactor.marginal=FALSE) #not run: using parallel processes with ncores=3 # align.stack2=align.optim(stackEstimates(est.base2),c(100,200),nstarts=3, # hyper.first='variances',ncores=3, # eps.alignment=0.01,clf.ignore.quantile=0.1, # estimator='mirt.grm',center.means=FALSE) align.stack2=align.optim(stackEstimates(est.base2),c(100,200),nstarts=3, hyper.first='variances',ncores=1, eps.alignment=0.01,clf.ignore.quantile=0.1, estimator='mirt.grm',center.means=FALSE, verbose=TRUE) align.stack2 fit.align2=Alignment(fit.base2,'mirt.grm',center.means=FALSE,SE=TRUE) #did it work? fit.align$hypers fit.align2$hypers fit.align$est%>%purrr::transpose()%>%purrr::map(~mean(.[[1]]-.[[2]])) fit.align2$est%>%purrr::transpose()%>%purrr::map(~mean(.[[1]]-.[[2]])) fit.align$fit fit.align2$fit (fit.align$fit%>%purrr::map(~.@ParTable%>% tibble::as_tibble()%>%dplyr::filter(free!=0))%>% purrr::transpose())[c('start','est')]%>%purrr::map(~mean(.[[1]]-.[[2]])) (fit.align2$fit%>%purrr::map(coef)%>% purrr::transpose())[paste0('Item_',1:3)]%>% purrr::map(~mean(.[[1]]-.[[2]])) #appears so!
#load data library(mirt) library(lavaan) library(purrr) library(tibble) library(magrittr) dat=expand.table(Bock1997) #fit configural models fit.mirt=mirt(dat,1,SE=TRUE) fit.lavaan=cfa(model='G =~ Item.1+Item.2+Item.3',data=dat, ordered=c('Item.1','Item.2','Item.3'), std.lv=TRUE,parameterization='delta') (fit.lavaan@ParTable)%>%tibble::as_tibble()%>%print(n=Inf) #test stuff tab=fit.lavaan@ParTable tab$start[23]=3 tab$est[23]=3 fit.lavaan2=lavaan(tab,data=fit.lavaan@Data) #get estimates est.mirt=getEstimates.mirt(fit.mirt,SE=TRUE,bifactor.marginal=FALSE) est.lavaan=getEstimates.lavaan(fit.lavaan,SE=TRUE) #test transformations newMean=10 newVar=2 test.mirt=transformEstimates.mirt.grm(newMean,newVar,est.mirt) test.lavaan=transformEstimates.lavaan.ordered( newMean,newVar,est.lavaan,toCompare=TRUE) #load and test equivalence tfit.mirt=loadEstimates.mirt.grm(fit.mirt,newMean,newVar,newpars=test.mirt, verbose=TRUE) test.mirt=mirt::coef(fit.mirt) test.mirt tfit.lavaan=loadEstimates.lavaan.ordered( fit.lavaan,newMean,newVar,newpars=test.lavaan, verbose=TRUE) tfit.lavaan@ParTable%>%tibble::as_tibble()%>%print(n=Inf) test.lavaan #now on stacked estimates estList=list(est.mirt%>%purrr::imap(function(x,n){ rownames(x)[2]=paste0(rownames(x)[2],'_ho') if(!n%in%c('a','se.a'))colnames(x)[2]=paste0(colnames(x)[2],'_ho') x }),est.mirt%>%purrr::imap(function(x,n){ rownames(x)[1]=paste0(rownames(x)[1],'_hi') if(!n%in%c('a','se.a'))colnames(x)[1]=paste0(colnames(x)[1],'_hi') x })) stack=stackEstimates(estList) test.stack=transformEstimates.mirt.grm(c(0,0),c(1,1),stack) sf.stack=SF.mplus3D(c(0,1),stack,combn(1:2,2),c(100,200),'mirt.grm', eps.alignment=0.01, clf.ignore.quantile=0.1) test.stack2=transformEstimates.mirt.grm(c(0,1),c(1,1/2),stack) #try align? #lavaan set.seed(0) sim.base=list(simdata(a=as.numeric(est.mirt$a),d=est.mirt$d,N=5000, itemtype='graded',sigma=matrix(1),mu=0), simdata(a=as.numeric(est.mirt$a),d=est.mirt$d,N=5000, itemtype='graded',sigma=matrix(2),mu=1)) fit.base=sim.base%>%purrr::map(~cfa(model="G =~ Item_1 + Item_2 + Item_3", data=as.data.frame(.), ordered=paste0('Item_',1:3),std.lv=TRUE, parameterization='delta')) fit.base%>%purrr::map(lavInspect,'est')%>%purrr::transpose() est.base=purrr::map(fit.base,getEstimates.lavaan,SE=TRUE) #not run: using parallel processes with ncores=3 set.seed(1) # align.stack=align.optim(stackEstimates(est.base),c(100,200),nstarts=3, # hyper.first='variances',ncores=3, # eps.alignment=0.01,clf.ignore.quantile=0.1, # estimator='lavaan.ordered',center.means=FALSE, # verbose=TRUE) # #same seed # set.seed(1) # align.stack=align.optim(stackEstimates(est.base),c(100,200),nstarts=3, # hyper.first='variances',ncores=3, # eps.alignment=0.01,clf.ignore.quantile=0.1, # estimator='lavaan.ordered',center.means=FALSE, # verbose=TRUE) #sequential align.stack=align.optim(stackEstimates(est.base),c(100,200),nstarts=3, hyper.first='variances',ncores=1, eps.alignment=0.01,clf.ignore.quantile=0.1, estimator='lavaan.ordered',center.means=FALSE, verbose=TRUE) align.stack fit.align=Alignment(fit.base,'lavaan.ordered',center.means=FALSE,SE=TRUE, verbose=TRUE) #mirt fit.base2=list() for(i in 1:length(sim.base)){ fit.base2[[i]]=mirt(sim.base[[i]],1,'graded',SE=TRUE) } est.base2=purrr::map(fit.base2,getEstimates.mirt,SE=TRUE, bifactor.marginal=FALSE) #not run: using parallel processes with ncores=3 # align.stack2=align.optim(stackEstimates(est.base2),c(100,200),nstarts=3, # hyper.first='variances',ncores=3, # eps.alignment=0.01,clf.ignore.quantile=0.1, # estimator='mirt.grm',center.means=FALSE) align.stack2=align.optim(stackEstimates(est.base2),c(100,200),nstarts=3, hyper.first='variances',ncores=1, eps.alignment=0.01,clf.ignore.quantile=0.1, estimator='mirt.grm',center.means=FALSE, verbose=TRUE) align.stack2 fit.align2=Alignment(fit.base2,'mirt.grm',center.means=FALSE,SE=TRUE) #did it work? fit.align$hypers fit.align2$hypers fit.align$est%>%purrr::transpose()%>%purrr::map(~mean(.[[1]]-.[[2]])) fit.align2$est%>%purrr::transpose()%>%purrr::map(~mean(.[[1]]-.[[2]])) fit.align$fit fit.align2$fit (fit.align$fit%>%purrr::map(~.@ParTable%>% tibble::as_tibble()%>%dplyr::filter(free!=0))%>% purrr::transpose())[c('start','est')]%>%purrr::map(~mean(.[[1]]-.[[2]])) (fit.align2$fit%>%purrr::map(coef)%>% purrr::transpose())[paste0('Item_',1:3)]%>% purrr::map(~mean(.[[1]]-.[[2]])) #appears so!
lavaan
estimates for alignmentNot generally intended to be used on its own, but exported anyway for didactic purposes.
getEstimates.lavaan(fit, SE = TRUE)
getEstimates.lavaan(fit, SE = TRUE)
fit |
A |
SE |
logical; whether to also obtain standard errors. |
See example for Alignment
for examples
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
A list
of estimates in a format amenable to subsequent alignment
mirt
estimates for alignmentNot generally intended to be used on its own, but exported anyway for didactic purposes.
getEstimates.mirt(fit, SE = FALSE, bifactor.marginal = FALSE)
getEstimates.mirt(fit, SE = FALSE, bifactor.marginal = FALSE)
fit |
A |
SE |
logical; whether to also obtain standard errors. |
bifactor.marginal |
See |
See example for Alignment
for examples
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
A list
of estimates in a format amenable to subsequent alignment
lavaan
models using aligned parameter estimatesNot generally intended to be used on its own, but exported anyway for didactic purposes.
loadEstimates.lavaan.ordered( fit, align.mean, align.variance, newpars, do.fit = TRUE, verbose = TRUE )
loadEstimates.lavaan.ordered( fit, align.mean, align.variance, newpars, do.fit = TRUE, verbose = TRUE )
fit |
A |
align.mean |
Mean to transform model to. |
align.variance |
Variance to transform model to. |
newpars |
New (transformed) estimates to load into model object. |
do.fit |
Whether to re-fit the model after loading and fixing estimates. |
verbose |
See |
See example for Alignment
for examples
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
A lavaan
object, based on fit
but with modified
parameters.
mirt
models using aligned parameter estimatesNot generally intended to be used on its own, but exported anyway for didactic purposes.
loadEstimates.mirt.grm( fit, align.mean, align.variance, newpars, do.fit = TRUE, verbose = TRUE )
loadEstimates.mirt.grm( fit, align.mean, align.variance, newpars, do.fit = TRUE, verbose = TRUE )
fit |
A |
align.mean |
Mean to transform model to. |
align.variance |
Variance to transform model to. |
newpars |
New (transformed) estimates to load into model object. |
do.fit |
Whether to re-fit the model after loading and fixing estimates. |
verbose |
See |
See example for Alignment
for examples
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
A mirt
, object based on fit
but with modified
parameters.
Not generally intended to be used on its own, but exported anyway for didactic purposes.
SF.mplus3D( pars, est, comb, nobs, estimator, eps.alignment, clf.ignore.quantile, hyper = "all", otherHyper = NULL )
SF.mplus3D( pars, est, comb, nobs, estimator, eps.alignment, clf.ignore.quantile, hyper = "all", otherHyper = NULL )
pars |
Hyperparameters to feed into optimizer |
est |
Estimates to transform, from |
comb |
All combinations of groups from |
nobs |
Sample size in each group |
estimator |
See |
eps.alignment |
See |
clf.ignore.quantile |
See |
hyper |
Hyperparameter to calculate simplicity function for; see
|
otherHyper |
Non-included hyperparameter |
See example for Alignment
for examples
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
A value of the simplicity function from Asparouhuv & Muthen, 2014.
Not generally intended to be used on its own, but exported anyway for didactic purposes.
stackEstimates(estList)
stackEstimates(estList)
estList |
List of estimates from |
See example for Alignment
for examples
A set of estimates prepared for efficient use with
SF.mplus3D
lavaan
estimates using aligned estimates of latent
mean and varianceNot generally intended to be used on its own, but exported anyway for didactic purposes.
transformEstimates.lavaan.ordered( align.mean, align.variance, est, toCompare = FALSE )
transformEstimates.lavaan.ordered( align.mean, align.variance, est, toCompare = FALSE )
align.mean |
Mean to transform model to. |
align.variance |
Variance to transform model to. |
est |
Estimates to transform, from |
toCompare |
Accounts for discrepancies between delta and theta
parameterizations; see |
See example for Alignment
for examples
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
Estimates in the same structure as from
getEstimates.lavaan
, but transformed from (assumed) mean 0 and
variance 1 to the metric specified by align.mean
and align.variance
.
mirt
estimates using aligned estimates of latent mean
and varianceNot generally intended to be used on its own, but exported anyway for didactic purposes.
transformEstimates.mirt.grm(align.mean, align.variance, est)
transformEstimates.mirt.grm(align.mean, align.variance, est)
align.mean |
Mean to transform model to. |
align.variance |
Variance to transform model to. |
est |
Estimates to transform, from |
See example for Alignment
for examples
This program was designed based on the published work of Asparouhov & Muthen, and was not intended to match Mplus exactly, and may not.
Estimates in the same structure as from
getEstimates.mirt
, but transformed from (assumed) mean 0 and
variance 1 to the metric specified by align.mean
and align.variance
.